app.hypergames.hypersdk 1.10.0 → 1.11.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/Editor/Scripts/Setup/HyperSDKInstaller.cs +42 -0
- package/Editor/Scripts/Tools/HyperGameModeMenu.cs +27 -7
- package/Runtime/Internal/AssemblyInfo.cs +1 -0
- package/Runtime/Internal/Bootstrap/HyperLoader.cs +186 -8
- package/Runtime/Internal/Core/HyperRuntime.cs +82 -0
- package/Runtime/Internal/Core/HyperRuntimeHost.cs +6 -0
- package/Runtime/Internal/Data/URLData.cs +18 -1
- package/Runtime/Internal/Managers/BackendManager.cs +281 -24
- package/Runtime/Internal/Managers/DataManager.cs +207 -21
- package/Runtime/Internal/Managers/GameManager.cs +365 -17
- package/Runtime/Internal/Managers/ReplayManager.cs +117 -0
- package/Runtime/Internal/Managers/ReplayManager.cs.meta +11 -0
- package/Runtime/Internal/Managers/TimeManager.cs +54 -12
- package/Runtime/Internal/Replay/ReplayActivityRecorder.cs +182 -0
- package/Runtime/Internal/Replay/ReplayActivityRecorder.cs.meta +2 -0
- package/Runtime/Internal/Replay/ReplaySessionState.cs +181 -0
- package/Runtime/Internal/Replay/ReplaySessionState.cs.meta +2 -0
- package/Runtime/Internal/Replay.meta +8 -0
- package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +5 -0
- package/Runtime/Internal/UI/Animation/LoadingTextAnimation.cs +15 -1
- package/Runtime/Internal/UI/Components/ControlBar.cs +25 -2
- package/Runtime/Internal/UI/Components/PauseButton.cs +4 -1
- package/Runtime/Internal/UI/Components/ReplayController.cs +527 -0
- package/Runtime/Internal/UI/Components/ReplayController.cs.meta +2 -0
- package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs +89 -0
- package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs.meta +2 -0
- package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs +183 -0
- package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs.meta +2 -0
- package/Runtime/Internal/UI/Modals/LoadingFailedModal.cs +12 -0
- package/Runtime/Internal/UI/Modals/ReplayOverModal.cs +61 -0
- package/Runtime/Internal/UI/Modals/ReplayOverModal.cs.meta +2 -0
- package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +38 -29
- package/Runtime/Public/AssemblyInfo.cs +3 -0
- package/Runtime/Public/AssemblyInfo.cs.meta +2 -0
- package/Runtime/Public/Core/HyperSDK.cs +305 -109
- package/Runtime/Public/Hyper.asmdef +2 -1
- package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +555 -0
- package/Runtime/Public/Replay/ReplayPlaybackEngine.cs.meta +2 -0
- package/Runtime/Public/Replay.meta +8 -0
- package/Runtime/Resources/Fonts/Nunito/Nunito-Light SDF.asset +528 -521
- package/Runtime/Resources/Materials/HyperReplayScanlines.preset +340 -0
- package/Runtime/Resources/Materials/HyperReplayScanlines.preset.meta +8 -0
- package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat +55 -0
- package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat.meta +8 -0
- package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader +150 -0
- package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader.meta +10 -0
- package/Runtime/Resources/Sprites/Replays/Fallback avatar.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Fallback avatar.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays/Recording.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Recording.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays/Replay frame.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Replay frame.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays/Replay modal icon.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Replay modal icon.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays.meta +8 -0
- package/Runtime/Resources/UIPrefabs/ControlBar.prefab +169 -2
- package/Runtime/Resources/UIPrefabs/GamePausedModal_Battle.prefab +1 -1
- package/Runtime/Resources/UIPrefabs/ReplayController.prefab +1269 -0
- package/Runtime/Resources/UIPrefabs/ReplayController.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab +90 -0
- package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab +958 -0
- package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +189 -0
- package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/SnackBar.prefab +2 -2
- package/Runtime/Shared/AssemblyInfo.cs +5 -0
- package/Runtime/Shared/AssemblyInfo.cs.meta +11 -0
- package/Runtime/Shared/DTOs/Enums.cs +8 -3
- package/Runtime/Shared/DTOs/Score/ScoreSnapshot.cs +13 -1
- package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs +37 -0
- package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs.meta +11 -0
- package/Runtime/Shared/Replay.meta +8 -0
- package/Samples~/LaneRunner/LaneRunnerSample.unity +1603 -0
- package/Samples~/LaneRunner/LaneRunnerSample.unity.meta +7 -0
- package/Samples~/LaneRunner/Materials/DoubleScore.mat +86 -0
- package/Samples~/LaneRunner/Materials/DoubleScore.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Ground.mat +84 -0
- package/Samples~/LaneRunner/Materials/Ground.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Lane.mat +84 -0
- package/Samples~/LaneRunner/Materials/Lane.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/LaneMarker.mat +84 -0
- package/Samples~/LaneRunner/Materials/LaneMarker.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Obstacle.mat +84 -0
- package/Samples~/LaneRunner/Materials/Obstacle.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Player.mat +84 -0
- package/Samples~/LaneRunner/Materials/Player.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials.meta +8 -0
- package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +152 -0
- package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab.meta +7 -0
- package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +152 -0
- package/Samples~/LaneRunner/Prefabs/Obstacle.prefab.meta +7 -0
- package/Samples~/LaneRunner/Prefabs.meta +8 -0
- package/Samples~/LaneRunner/README.md +35 -0
- package/Samples~/LaneRunner/README.md.meta +7 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs +9 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs +93 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs +155 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +110 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs +41 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay.meta +8 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs +62 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +64 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Hyper.meta +8 -0
- package/Samples~/LaneRunner/Scripts.meta +8 -0
- package/Tests.meta +8 -0
- package/package.json +8 -1
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using Hyper.Shared;
|
|
3
|
+
using Newtonsoft.Json.Linq;
|
|
4
|
+
using UnityEngine;
|
|
5
|
+
|
|
6
|
+
namespace Hyper
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Internal fixed-step scheduler for compact NDJSON replay activities.
|
|
10
|
+
/// </summary>
|
|
11
|
+
internal sealed class ReplayPlaybackEngine : IReplayPlaybackEngine
|
|
12
|
+
{
|
|
13
|
+
private const string GameStarted = "GS";
|
|
14
|
+
private const string GamePaused = "GP";
|
|
15
|
+
private const string GameResumed = "GR";
|
|
16
|
+
private const string GameEnded = "GE";
|
|
17
|
+
private const int MaxActivitiesPerTick = 4096;
|
|
18
|
+
|
|
19
|
+
private string _ndjson;
|
|
20
|
+
private int _cursor;
|
|
21
|
+
private ReplayActivity _next;
|
|
22
|
+
private bool _hasNext;
|
|
23
|
+
private bool _endedRaised;
|
|
24
|
+
private bool _nextActionPrepared;
|
|
25
|
+
private double _sourceStartTime;
|
|
26
|
+
private double _elapsedSeconds;
|
|
27
|
+
private double _removedPauseDuration;
|
|
28
|
+
private double _stoppedElapsed;
|
|
29
|
+
private bool _isSuspended;
|
|
30
|
+
private bool _completionPending;
|
|
31
|
+
|
|
32
|
+
public event Action<float, float> OnValues2;
|
|
33
|
+
public event Action<float> OnValue;
|
|
34
|
+
public event Action<float, float, float> OnValues;
|
|
35
|
+
public event Action<string> OnAction;
|
|
36
|
+
public event Action<string, string> OnActions;
|
|
37
|
+
public event Action<string, float, float> OnActionValues;
|
|
38
|
+
public event Action<float, float, string> OnValuesAction;
|
|
39
|
+
public event Action<string, float> OnActionValue;
|
|
40
|
+
public event Action<float, string> OnValueAction;
|
|
41
|
+
/// <summary>
|
|
42
|
+
/// Invoked shortly before an ordinary string action when
|
|
43
|
+
/// <see cref="ActionPreparationLeadSeconds"/> is greater than zero.
|
|
44
|
+
/// Use this for presentation only; gameplay belongs in <see cref="OnAction"/>.
|
|
45
|
+
/// </summary>
|
|
46
|
+
public event Action<string> OnActionPreparing;
|
|
47
|
+
public event Action OnGameStarted;
|
|
48
|
+
public event Action OnGameEnded;
|
|
49
|
+
|
|
50
|
+
public bool IsPlaying { get; private set; }
|
|
51
|
+
public double DurationSeconds { get; private set; }
|
|
52
|
+
public double ElapsedSeconds => GetElapsedSeconds();
|
|
53
|
+
/// <summary>
|
|
54
|
+
/// Optional lead time for preparing visual feedback before an ordinary string action.
|
|
55
|
+
/// The action itself is still dispatched at its recorded timestamp.
|
|
56
|
+
/// </summary>
|
|
57
|
+
public double ActionPreparationLeadSeconds { get; set; }
|
|
58
|
+
public float Progress01 => DurationSeconds <= 0d
|
|
59
|
+
? (IsPlaying ? 0f : 1f)
|
|
60
|
+
: Mathf.Clamp01((float)(ElapsedSeconds / DurationSeconds));
|
|
61
|
+
|
|
62
|
+
public void Begin(string ndjson)
|
|
63
|
+
{
|
|
64
|
+
Stop();
|
|
65
|
+
|
|
66
|
+
_ndjson = ndjson ?? string.Empty;
|
|
67
|
+
_cursor = 0;
|
|
68
|
+
_removedPauseDuration = 0d;
|
|
69
|
+
_elapsedSeconds = 0d;
|
|
70
|
+
_stoppedElapsed = 0d;
|
|
71
|
+
_isSuspended = false;
|
|
72
|
+
_endedRaised = false;
|
|
73
|
+
_completionPending = false;
|
|
74
|
+
_nextActionPrepared = false;
|
|
75
|
+
DurationSeconds = CalculateDuration(_ndjson);
|
|
76
|
+
_hasNext = TryReadNext(out _next);
|
|
77
|
+
|
|
78
|
+
if (!_hasNext)
|
|
79
|
+
{
|
|
80
|
+
Complete();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_sourceStartTime = _next.Time;
|
|
85
|
+
IsPlaying = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// <summary>
|
|
89
|
+
/// Advances replay by one Unity fixed step. Call this from FixedUpdate.
|
|
90
|
+
/// Playback speed comes from Unity's fixed-step frequency through Time.timeScale.
|
|
91
|
+
/// </summary>
|
|
92
|
+
public void Tick()
|
|
93
|
+
{
|
|
94
|
+
Tick(Time.fixedDeltaTime);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// <summary>
|
|
98
|
+
/// Advances replay using an explicit simulation-step duration.
|
|
99
|
+
/// </summary>
|
|
100
|
+
public void Tick(double fixedSimulationDeltaSeconds)
|
|
101
|
+
{
|
|
102
|
+
if (!IsPlaying || _isSuspended) return;
|
|
103
|
+
|
|
104
|
+
// GE becomes observable on the next fixed tick. The simulation step
|
|
105
|
+
// containing GE can therefore complete before game-end callbacks run.
|
|
106
|
+
if (_completionPending)
|
|
107
|
+
{
|
|
108
|
+
Complete();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
double delta = double.IsNaN(fixedSimulationDeltaSeconds) ||
|
|
113
|
+
double.IsInfinity(fixedSimulationDeltaSeconds)
|
|
114
|
+
? 0d
|
|
115
|
+
: Math.Max(0d, fixedSimulationDeltaSeconds);
|
|
116
|
+
_elapsedSeconds += delta;
|
|
117
|
+
|
|
118
|
+
int processed = 0;
|
|
119
|
+
while (IsPlaying && !_completionPending && _hasNext && processed++ < MaxActivitiesPerTick)
|
|
120
|
+
{
|
|
121
|
+
double elapsed = GetElapsedSeconds();
|
|
122
|
+
double dueTime = _next.Time - _sourceStartTime - _removedPauseDuration;
|
|
123
|
+
PrepareNextActionIfDue(elapsed, dueTime);
|
|
124
|
+
if (elapsed + 0.000001d < dueTime) return;
|
|
125
|
+
|
|
126
|
+
ReplayActivity activity = _next;
|
|
127
|
+
_hasNext = TryReadNext(out _next);
|
|
128
|
+
_nextActionPrepared = false;
|
|
129
|
+
Dispatch(activity);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (IsPlaying && !_completionPending && !_hasNext)
|
|
133
|
+
{
|
|
134
|
+
ScheduleCompletion();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public void Stop()
|
|
139
|
+
{
|
|
140
|
+
_stoppedElapsed = IsPlaying ? _elapsedSeconds : _stoppedElapsed;
|
|
141
|
+
IsPlaying = false;
|
|
142
|
+
_ndjson = null;
|
|
143
|
+
_cursor = 0;
|
|
144
|
+
_hasNext = false;
|
|
145
|
+
_isSuspended = false;
|
|
146
|
+
_completionPending = false;
|
|
147
|
+
_nextActionPrepared = false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public void Suspend()
|
|
151
|
+
{
|
|
152
|
+
if (!IsPlaying || _isSuspended) return;
|
|
153
|
+
_isSuspended = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public void Resume()
|
|
157
|
+
{
|
|
158
|
+
if (!IsPlaying || !_isSuspended) return;
|
|
159
|
+
_isSuspended = false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private void Dispatch(ReplayActivity activity)
|
|
163
|
+
{
|
|
164
|
+
if (activity.Kind == ReplayActivityKind.Values2)
|
|
165
|
+
{
|
|
166
|
+
OnValues2?.Invoke(activity.X, activity.Y);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (activity.Kind == ReplayActivityKind.Value)
|
|
171
|
+
{
|
|
172
|
+
OnValue?.Invoke(activity.X);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (activity.Kind == ReplayActivityKind.Values)
|
|
177
|
+
{
|
|
178
|
+
OnValues?.Invoke(activity.X, activity.Y, activity.Z);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (activity.Kind == ReplayActivityKind.Actions)
|
|
183
|
+
{
|
|
184
|
+
OnActions?.Invoke(activity.Action, activity.SecondAction);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (activity.Kind == ReplayActivityKind.ActionValues)
|
|
189
|
+
{
|
|
190
|
+
OnActionValues?.Invoke(activity.Action, activity.X, activity.Y);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (activity.Kind == ReplayActivityKind.ValuesAction)
|
|
195
|
+
{
|
|
196
|
+
OnValuesAction?.Invoke(activity.X, activity.Y, activity.Action);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (activity.Kind == ReplayActivityKind.ActionValue)
|
|
201
|
+
{
|
|
202
|
+
OnActionValue?.Invoke(activity.Action, activity.X);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (activity.Kind == ReplayActivityKind.ValueAction)
|
|
207
|
+
{
|
|
208
|
+
OnValueAction?.Invoke(activity.X, activity.Action);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
switch (activity.Action)
|
|
213
|
+
{
|
|
214
|
+
case GameStarted:
|
|
215
|
+
OnGameStarted?.Invoke();
|
|
216
|
+
break;
|
|
217
|
+
case GamePaused:
|
|
218
|
+
SkipRecordedPause(activity.Time);
|
|
219
|
+
break;
|
|
220
|
+
case GameResumed:
|
|
221
|
+
break;
|
|
222
|
+
case GameEnded:
|
|
223
|
+
ScheduleCompletion();
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
OnAction?.Invoke(activity.Action);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private void PrepareNextActionIfDue(double elapsed, double dueTime)
|
|
232
|
+
{
|
|
233
|
+
if (_nextActionPrepared || ActionPreparationLeadSeconds <= 0d ||
|
|
234
|
+
_next.Kind != ReplayActivityKind.Action || IsLifecycleAction(_next.Action))
|
|
235
|
+
{
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
double preparationTime = dueTime - ActionPreparationLeadSeconds;
|
|
240
|
+
if (elapsed + 0.000001d < preparationTime) return;
|
|
241
|
+
|
|
242
|
+
_nextActionPrepared = true;
|
|
243
|
+
OnActionPreparing?.Invoke(_next.Action);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private static bool IsLifecycleAction(string action)
|
|
247
|
+
{
|
|
248
|
+
return action == GameStarted || action == GamePaused ||
|
|
249
|
+
action == GameResumed || action == GameEnded;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private void SkipRecordedPause(double pauseStartedAt)
|
|
253
|
+
{
|
|
254
|
+
_nextActionPrepared = false;
|
|
255
|
+
while (_hasNext)
|
|
256
|
+
{
|
|
257
|
+
ReplayActivity activity = _next;
|
|
258
|
+
_hasNext = TryReadNext(out _next);
|
|
259
|
+
|
|
260
|
+
if (activity.Kind != ReplayActivityKind.Action) continue;
|
|
261
|
+
|
|
262
|
+
if (activity.Action == GameResumed)
|
|
263
|
+
{
|
|
264
|
+
_removedPauseDuration += Math.Max(0d, activity.Time - pauseStartedAt);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (activity.Action == GameEnded)
|
|
269
|
+
{
|
|
270
|
+
ScheduleCompletion();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
ScheduleCompletion();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private void ScheduleCompletion()
|
|
279
|
+
{
|
|
280
|
+
_stoppedElapsed = DurationSeconds;
|
|
281
|
+
_completionPending = true;
|
|
282
|
+
_hasNext = false;
|
|
283
|
+
_nextActionPrepared = false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private void Complete()
|
|
287
|
+
{
|
|
288
|
+
_stoppedElapsed = DurationSeconds;
|
|
289
|
+
IsPlaying = false;
|
|
290
|
+
_completionPending = false;
|
|
291
|
+
_ndjson = null;
|
|
292
|
+
_hasNext = false;
|
|
293
|
+
if (_endedRaised) return;
|
|
294
|
+
|
|
295
|
+
_endedRaised = true;
|
|
296
|
+
OnGameEnded?.Invoke();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private double GetElapsedSeconds()
|
|
300
|
+
{
|
|
301
|
+
if (!IsPlaying) return Math.Max(0d, _stoppedElapsed);
|
|
302
|
+
return Math.Max(0d, _elapsedSeconds);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private static double CalculateDuration(string ndjson)
|
|
306
|
+
{
|
|
307
|
+
if (string.IsNullOrWhiteSpace(ndjson)) return 0d;
|
|
308
|
+
|
|
309
|
+
int cursor = 0;
|
|
310
|
+
bool hasFirst = false;
|
|
311
|
+
double firstTime = 0d;
|
|
312
|
+
double lastEffectiveTime = 0d;
|
|
313
|
+
double removedPauses = 0d;
|
|
314
|
+
double? pauseStartedAt = null;
|
|
315
|
+
|
|
316
|
+
while (TryReadLine(ndjson, ref cursor, out string line))
|
|
317
|
+
{
|
|
318
|
+
if (!TryParse(line, out ReplayActivity activity)) continue;
|
|
319
|
+
if (!hasFirst)
|
|
320
|
+
{
|
|
321
|
+
hasFirst = true;
|
|
322
|
+
firstTime = activity.Time;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (activity.Kind == ReplayActivityKind.Action && activity.Action == GamePaused)
|
|
326
|
+
{
|
|
327
|
+
pauseStartedAt ??= activity.Time;
|
|
328
|
+
lastEffectiveTime = Math.Max(0d, activity.Time - firstTime - removedPauses);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (activity.Kind == ReplayActivityKind.Action && activity.Action == GameResumed && pauseStartedAt.HasValue)
|
|
333
|
+
{
|
|
334
|
+
removedPauses += Math.Max(0d, activity.Time - pauseStartedAt.Value);
|
|
335
|
+
pauseStartedAt = null;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (pauseStartedAt.HasValue)
|
|
340
|
+
{
|
|
341
|
+
if (activity.Kind == ReplayActivityKind.Action && activity.Action == GameEnded) break;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
lastEffectiveTime = Math.Max(0d, activity.Time - firstTime - removedPauses);
|
|
346
|
+
if (activity.Kind == ReplayActivityKind.Action && activity.Action == GameEnded) break;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return hasFirst ? lastEffectiveTime : 0d;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private bool TryReadNext(out ReplayActivity activity)
|
|
353
|
+
{
|
|
354
|
+
while (TryReadLine(out string line))
|
|
355
|
+
{
|
|
356
|
+
if (TryParse(line, out activity)) return true;
|
|
357
|
+
if (IsMetadataRecord(line)) continue;
|
|
358
|
+
Debug.LogWarning($"[Hyper Replay] Skipping malformed activity: {line}");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
activity = default;
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private bool TryReadLine(out string line)
|
|
366
|
+
{
|
|
367
|
+
return TryReadLine(_ndjson, ref _cursor, out line);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private static bool TryReadLine(string ndjson, ref int cursor, out string line)
|
|
371
|
+
{
|
|
372
|
+
line = null;
|
|
373
|
+
if (string.IsNullOrEmpty(ndjson) || cursor >= ndjson.Length) return false;
|
|
374
|
+
|
|
375
|
+
int start = cursor;
|
|
376
|
+
while (cursor < ndjson.Length && ndjson[cursor] != '\n' && ndjson[cursor] != '\r') cursor++;
|
|
377
|
+
line = ndjson.Substring(start, cursor - start).Trim();
|
|
378
|
+
while (cursor < ndjson.Length && (ndjson[cursor] == '\n' || ndjson[cursor] == '\r')) cursor++;
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private static bool TryParse(string line, out ReplayActivity activity)
|
|
383
|
+
{
|
|
384
|
+
activity = default;
|
|
385
|
+
if (string.IsNullOrWhiteSpace(line)) return false;
|
|
386
|
+
|
|
387
|
+
try
|
|
388
|
+
{
|
|
389
|
+
var values = JArray.Parse(line);
|
|
390
|
+
if (values.Count < 2 || values.Count > 4 || values[0].Type != JTokenType.Integer && values[0].Type != JTokenType.Float)
|
|
391
|
+
{
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
double time = values[0].Value<double>();
|
|
396
|
+
if (values.Count == 3 && IsNumber(values[1]) && IsNumber(values[2]))
|
|
397
|
+
{
|
|
398
|
+
activity = ReplayActivity.Values2(time, values[1].Value<float>(), values[2].Value<float>());
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (values.Count == 2 && IsNumber(values[1]))
|
|
403
|
+
{
|
|
404
|
+
activity = ReplayActivity.Value(time, values[1].Value<float>());
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (values.Count == 2 && values[1].Type == JTokenType.String)
|
|
409
|
+
{
|
|
410
|
+
string action = values[1].Value<string>();
|
|
411
|
+
if (string.IsNullOrWhiteSpace(action)) return false;
|
|
412
|
+
activity = ReplayActivity.NamedAction(time, action);
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
if (values.Count == 3 && values[1].Type == JTokenType.String && values[2].Type == JTokenType.String)
|
|
418
|
+
{
|
|
419
|
+
string firstAction = values[1].Value<string>();
|
|
420
|
+
string secondAction = values[2].Value<string>();
|
|
421
|
+
if (string.IsNullOrWhiteSpace(firstAction) || string.IsNullOrWhiteSpace(secondAction)) return false;
|
|
422
|
+
activity = ReplayActivity.NamedActions(time, firstAction, secondAction);
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (values.Count == 3 && values[1].Type == JTokenType.String && IsNumber(values[2]))
|
|
427
|
+
{
|
|
428
|
+
string action = values[1].Value<string>();
|
|
429
|
+
if (string.IsNullOrWhiteSpace(action)) return false;
|
|
430
|
+
activity = ReplayActivity.ActionValue(time, action, values[2].Value<float>());
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (values.Count == 3 && IsNumber(values[1]) && values[2].Type == JTokenType.String)
|
|
435
|
+
{
|
|
436
|
+
string action = values[2].Value<string>();
|
|
437
|
+
if (string.IsNullOrWhiteSpace(action)) return false;
|
|
438
|
+
activity = ReplayActivity.ValueAction(time, values[1].Value<float>(), action);
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (values.Count == 4 && IsNumber(values[1]) && IsNumber(values[2]) && IsNumber(values[3]))
|
|
443
|
+
{
|
|
444
|
+
activity = ReplayActivity.Values(time, values[1].Value<float>(), values[2].Value<float>(), values[3].Value<float>());
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (values.Count == 4 && values[1].Type == JTokenType.String && IsNumber(values[2]) && IsNumber(values[3]))
|
|
449
|
+
{
|
|
450
|
+
string action = values[1].Value<string>();
|
|
451
|
+
if (string.IsNullOrWhiteSpace(action)) return false;
|
|
452
|
+
activity = ReplayActivity.ActionValues(time, action, values[2].Value<float>(), values[3].Value<float>());
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (values.Count == 4 && IsNumber(values[1]) && IsNumber(values[2]) && values[3].Type == JTokenType.String)
|
|
457
|
+
{
|
|
458
|
+
string action = values[3].Value<string>();
|
|
459
|
+
if (string.IsNullOrWhiteSpace(action)) return false;
|
|
460
|
+
activity = ReplayActivity.ValuesAction(time, values[1].Value<float>(), values[2].Value<float>(), action);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
catch
|
|
465
|
+
{
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private static bool IsNumber(JToken value)
|
|
473
|
+
{
|
|
474
|
+
return value.Type == JTokenType.Integer || value.Type == JTokenType.Float;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private static bool IsMetadataRecord(string line)
|
|
478
|
+
{
|
|
479
|
+
try
|
|
480
|
+
{
|
|
481
|
+
var values = JArray.Parse(line);
|
|
482
|
+
if (values.Count != 2 || values[0].Type != JTokenType.String) return false;
|
|
483
|
+
|
|
484
|
+
string key = values[0].Value<string>();
|
|
485
|
+
return key == "seed" || key == "name" || key == "url";
|
|
486
|
+
}
|
|
487
|
+
catch
|
|
488
|
+
{
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private enum ReplayActivityKind
|
|
494
|
+
{
|
|
495
|
+
Values2,
|
|
496
|
+
Value,
|
|
497
|
+
Values,
|
|
498
|
+
Actions,
|
|
499
|
+
ActionValues,
|
|
500
|
+
ValuesAction,
|
|
501
|
+
ActionValue,
|
|
502
|
+
ValueAction,
|
|
503
|
+
Action
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private readonly struct ReplayActivity
|
|
507
|
+
{
|
|
508
|
+
internal readonly double Time;
|
|
509
|
+
internal readonly ReplayActivityKind Kind;
|
|
510
|
+
internal readonly float X;
|
|
511
|
+
internal readonly float Y;
|
|
512
|
+
internal readonly float Z;
|
|
513
|
+
internal readonly string Action;
|
|
514
|
+
internal readonly string SecondAction;
|
|
515
|
+
|
|
516
|
+
private ReplayActivity(double time, ReplayActivityKind kind, float x, float y, float z, string action, string secondAction)
|
|
517
|
+
{
|
|
518
|
+
Time = time;
|
|
519
|
+
Kind = kind;
|
|
520
|
+
X = x;
|
|
521
|
+
Y = y;
|
|
522
|
+
Z = z;
|
|
523
|
+
Action = action;
|
|
524
|
+
SecondAction = secondAction;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
internal static ReplayActivity Values2(double time, float first, float second) =>
|
|
528
|
+
new ReplayActivity(time, ReplayActivityKind.Values2, first, second, 0f, null, null);
|
|
529
|
+
|
|
530
|
+
internal static ReplayActivity Value(double time, float value) =>
|
|
531
|
+
new ReplayActivity(time, ReplayActivityKind.Value, value, 0f, 0f, null, null);
|
|
532
|
+
|
|
533
|
+
internal static ReplayActivity Values(double time, float first, float second, float third) =>
|
|
534
|
+
new ReplayActivity(time, ReplayActivityKind.Values, first, second, third, null, null);
|
|
535
|
+
|
|
536
|
+
internal static ReplayActivity NamedAction(double time, string action) =>
|
|
537
|
+
new ReplayActivity(time, ReplayActivityKind.Action, 0f, 0f, 0f, action, null);
|
|
538
|
+
|
|
539
|
+
internal static ReplayActivity NamedActions(double time, string firstAction, string secondAction) =>
|
|
540
|
+
new ReplayActivity(time, ReplayActivityKind.Actions, 0f, 0f, 0f, firstAction, secondAction);
|
|
541
|
+
|
|
542
|
+
internal static ReplayActivity ActionValues(double time, string action, float first, float second) =>
|
|
543
|
+
new ReplayActivity(time, ReplayActivityKind.ActionValues, first, second, 0f, action, null);
|
|
544
|
+
|
|
545
|
+
internal static ReplayActivity ValuesAction(double time, float first, float second, string action) =>
|
|
546
|
+
new ReplayActivity(time, ReplayActivityKind.ValuesAction, first, second, 0f, action, null);
|
|
547
|
+
|
|
548
|
+
internal static ReplayActivity ActionValue(double time, string action, float value) =>
|
|
549
|
+
new ReplayActivity(time, ReplayActivityKind.ActionValue, value, 0f, 0f, action, null);
|
|
550
|
+
|
|
551
|
+
internal static ReplayActivity ValueAction(double time, float value, string action) =>
|
|
552
|
+
new ReplayActivity(time, ReplayActivityKind.ValueAction, value, 0f, 0f, action, null);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|