-
Notifications
You must be signed in to change notification settings - Fork 8
/
ClientPrediction.cs
134 lines (121 loc) · 6.98 KB
/
ClientPrediction.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
using Swihoni.Collections;
using Swihoni.Components;
using Swihoni.Sessions.Components;
using Swihoni.Sessions.Config;
using Swihoni.Sessions.Player.Modifiers;
using UnityEngine;
namespace Swihoni.Sessions
{
public partial class Client
{
private readonly CyclicArray<Container> m_PlayerPredictionHistory;
private readonly CyclicArray<ClientCommandsContainer> m_CommandHistory;
public int PredictionErrors { get; private set; }
private void Predict(uint tick, uint timeUs, int localPlayerId)
{
Container previousPredictedPlayer = m_PlayerPredictionHistory.Peek(),
predictedPlayer = m_PlayerPredictionHistory.ClaimNext();
ClientCommandsContainer previousCommand = m_CommandHistory.Peek(),
commands = m_CommandHistory.ClaimNext();
if (predictedPlayer.Without(out ClientStampComponent predictedStamp)) return;
predictedPlayer.SetTo(previousPredictedPlayer);
commands.SetTo(previousCommand);
if (IsLoading)
{
commands.Require<ClientStampComponent>().Clear();
}
else
{
predictedStamp.tick.Value = tick;
predictedStamp.timeUs.Value = timeUs;
var previousClientStamp = previousPredictedPlayer.Require<ClientStampComponent>();
if (previousClientStamp.timeUs.WithValue)
{
uint lastTime = previousClientStamp.timeUs.Else(timeUs),
durationUs = timeUs - lastTime;
predictedStamp.durationUs.Value = durationUs;
}
// Inject trusted component
commands.Require<ClientStampComponent>().SetTo(predictedStamp);
MergeCommandInto(predictedPlayer, commands);
if (IsLoading || predictedStamp.durationUs.WithoutValue) return;
PlayerModifierDispatcherBehavior modifier = GetPlayerModifier(predictedPlayer, localPlayerId);
if (!modifier) return;
var context = new SessionContext(this, GetLatestSession(), commands, localPlayerId, predictedPlayer,
timeUs: timeUs, durationUs: predictedStamp.durationUs, tickDelta: 1);
modifier.ModifyChecked(context);
}
}
private void CheckPrediction(Container serverPlayer, int localPlayerId)
{
UIntProperty targetClientTick = serverPlayer.Require<ClientStampComponent>().tick;
if (targetClientTick.WithoutValue)
return;
var playerHistoryIndex = 0;
Container basePredictedPlayer = null;
for (; playerHistoryIndex < m_PlayerPredictionHistory.Size; playerHistoryIndex++)
{
Container predictedPlayer = m_PlayerPredictionHistory.Get(-playerHistoryIndex);
if (predictedPlayer.Require<ClientStampComponent>().tick == targetClientTick)
{
basePredictedPlayer = predictedPlayer;
break;
}
}
if (basePredictedPlayer is null)
return;
/* We are checking predicted */
Container latestPredictedPlayer = m_PlayerPredictionHistory.Peek();
_predictionIsAccurate = true; // Set by the following navigation
ElementExtensions.NavigateZipped(basePredictedPlayer, latestPredictedPlayer, serverPlayer, VisitPredictedFunction);
if (_predictionIsAccurate)
return;
/* We did not predict properly */
PredictionErrors++;
// Place base from verified server
basePredictedPlayer.SetTo(serverPlayer);
// Replay old commands up until most recent to get back on track
for (int commandHistoryIndex = playerHistoryIndex - 1; commandHistoryIndex >= 0; commandHistoryIndex--)
{
ClientCommandsContainer commands = m_CommandHistory.Get(-commandHistoryIndex);
Container pastPredictedPlayer = m_PlayerPredictionHistory.Get(-commandHistoryIndex);
ClientStampComponent stamp = pastPredictedPlayer.Require<ClientStampComponent>().Clone(); // TODO:performance remove clone
pastPredictedPlayer.SetTo(m_PlayerPredictionHistory.Get(-commandHistoryIndex - 1));
pastPredictedPlayer.Require<ClientStampComponent>().SetTo(stamp);
PlayerModifierDispatcherBehavior localPlayerModifier = GetPlayerModifier(pastPredictedPlayer, localPlayerId);
if (commands.Require<ClientStampComponent>().durationUs.WithValue)
{
// TODO:architecture use latest session?
Container serverSession = GetLatestSession();
var context = new SessionContext(this, serverSession, commands, localPlayerId, pastPredictedPlayer,
durationUs: commands.Require<ClientStampComponent>().durationUs, tickDelta: 1);
localPlayerModifier.ModifyChecked(context);
}
}
}
private static Navigation VisitPredicted(ElementBase _predicted, ElementBase _latestPredicted, ElementBase _server)
{
if (_predicted.WithAttribute<OnlyServerTrustedAttribute>())
{
_latestPredicted.SetTo(_server);
return Navigation.SkipDescendents;
}
if (_predicted.WithAttribute<ClientTrustedAttribute>() || _predicted.WithAttribute<ClientNonCheckedAttribute>()) return Navigation.SkipDescendents;
switch (_predicted)
{
case FloatProperty f1 when _server is FloatProperty f2 && (f1.WithValue && f2.WithValue && f1.TryAttribute(out PredictionToleranceAttribute fTolerance)
&& !f1.CheckWithinTolerance(f2, fTolerance.tolerance)
|| f1.WithoutValue && f2.WithValue || f1.WithValue && f2.WithoutValue):
case VectorProperty v1 when _server is VectorProperty v2 && (v1.WithValue && v2.WithValue && v1.TryAttribute(out PredictionToleranceAttribute vTolerance)
&& !v1.CheckWithinTolerance(v2, vTolerance.tolerance)
|| v1.WithoutValue && v2.WithValue || v1.WithValue && v2.WithoutValue):
case PropertyBase p1 when _server is PropertyBase p2 && !p1.Equals(p2):
_predictionIsAccurate = false;
if (DefaultConfig.Active.logPredictionErrors)
Debug.LogWarning($"Error with predicted: {_predicted} and verified: {_server}");
return Navigation.Exit;
}
return Navigation.Continue;
}
}
}