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.
Files changed (115) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/Editor/Scripts/Setup/HyperSDKInstaller.cs +42 -0
  3. package/Editor/Scripts/Tools/HyperGameModeMenu.cs +27 -7
  4. package/Runtime/Internal/AssemblyInfo.cs +1 -0
  5. package/Runtime/Internal/Bootstrap/HyperLoader.cs +186 -8
  6. package/Runtime/Internal/Core/HyperRuntime.cs +82 -0
  7. package/Runtime/Internal/Core/HyperRuntimeHost.cs +6 -0
  8. package/Runtime/Internal/Data/URLData.cs +18 -1
  9. package/Runtime/Internal/Managers/BackendManager.cs +281 -24
  10. package/Runtime/Internal/Managers/DataManager.cs +207 -21
  11. package/Runtime/Internal/Managers/GameManager.cs +365 -17
  12. package/Runtime/Internal/Managers/ReplayManager.cs +117 -0
  13. package/Runtime/Internal/Managers/ReplayManager.cs.meta +11 -0
  14. package/Runtime/Internal/Managers/TimeManager.cs +54 -12
  15. package/Runtime/Internal/Replay/ReplayActivityRecorder.cs +182 -0
  16. package/Runtime/Internal/Replay/ReplayActivityRecorder.cs.meta +2 -0
  17. package/Runtime/Internal/Replay/ReplaySessionState.cs +181 -0
  18. package/Runtime/Internal/Replay/ReplaySessionState.cs.meta +2 -0
  19. package/Runtime/Internal/Replay.meta +8 -0
  20. package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +5 -0
  21. package/Runtime/Internal/UI/Animation/LoadingTextAnimation.cs +15 -1
  22. package/Runtime/Internal/UI/Components/ControlBar.cs +25 -2
  23. package/Runtime/Internal/UI/Components/PauseButton.cs +4 -1
  24. package/Runtime/Internal/UI/Components/ReplayController.cs +527 -0
  25. package/Runtime/Internal/UI/Components/ReplayController.cs.meta +2 -0
  26. package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs +89 -0
  27. package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs.meta +2 -0
  28. package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs +183 -0
  29. package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs.meta +2 -0
  30. package/Runtime/Internal/UI/Modals/LoadingFailedModal.cs +12 -0
  31. package/Runtime/Internal/UI/Modals/ReplayOverModal.cs +61 -0
  32. package/Runtime/Internal/UI/Modals/ReplayOverModal.cs.meta +2 -0
  33. package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +38 -29
  34. package/Runtime/Public/AssemblyInfo.cs +3 -0
  35. package/Runtime/Public/AssemblyInfo.cs.meta +2 -0
  36. package/Runtime/Public/Core/HyperSDK.cs +305 -109
  37. package/Runtime/Public/Hyper.asmdef +2 -1
  38. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +555 -0
  39. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs.meta +2 -0
  40. package/Runtime/Public/Replay.meta +8 -0
  41. package/Runtime/Resources/Fonts/Nunito/Nunito-Light SDF.asset +528 -521
  42. package/Runtime/Resources/Materials/HyperReplayScanlines.preset +340 -0
  43. package/Runtime/Resources/Materials/HyperReplayScanlines.preset.meta +8 -0
  44. package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat +55 -0
  45. package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat.meta +8 -0
  46. package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader +150 -0
  47. package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader.meta +10 -0
  48. package/Runtime/Resources/Sprites/Replays/Fallback avatar.png +0 -0
  49. package/Runtime/Resources/Sprites/Replays/Fallback avatar.png.meta +156 -0
  50. package/Runtime/Resources/Sprites/Replays/Recording.png +0 -0
  51. package/Runtime/Resources/Sprites/Replays/Recording.png.meta +156 -0
  52. package/Runtime/Resources/Sprites/Replays/Replay frame.png +0 -0
  53. package/Runtime/Resources/Sprites/Replays/Replay frame.png.meta +156 -0
  54. package/Runtime/Resources/Sprites/Replays/Replay modal icon.png +0 -0
  55. package/Runtime/Resources/Sprites/Replays/Replay modal icon.png.meta +156 -0
  56. package/Runtime/Resources/Sprites/Replays.meta +8 -0
  57. package/Runtime/Resources/UIPrefabs/ControlBar.prefab +169 -2
  58. package/Runtime/Resources/UIPrefabs/GamePausedModal_Battle.prefab +1 -1
  59. package/Runtime/Resources/UIPrefabs/ReplayController.prefab +1269 -0
  60. package/Runtime/Resources/UIPrefabs/ReplayController.prefab.meta +7 -0
  61. package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab +90 -0
  62. package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab.meta +7 -0
  63. package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab +958 -0
  64. package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab.meta +7 -0
  65. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +189 -0
  66. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab.meta +7 -0
  67. package/Runtime/Resources/UIPrefabs/SnackBar.prefab +2 -2
  68. package/Runtime/Shared/AssemblyInfo.cs +5 -0
  69. package/Runtime/Shared/AssemblyInfo.cs.meta +11 -0
  70. package/Runtime/Shared/DTOs/Enums.cs +8 -3
  71. package/Runtime/Shared/DTOs/Score/ScoreSnapshot.cs +13 -1
  72. package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs +37 -0
  73. package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs.meta +11 -0
  74. package/Runtime/Shared/Replay.meta +8 -0
  75. package/Samples~/LaneRunner/LaneRunnerSample.unity +1603 -0
  76. package/Samples~/LaneRunner/LaneRunnerSample.unity.meta +7 -0
  77. package/Samples~/LaneRunner/Materials/DoubleScore.mat +86 -0
  78. package/Samples~/LaneRunner/Materials/DoubleScore.mat.meta +8 -0
  79. package/Samples~/LaneRunner/Materials/Ground.mat +84 -0
  80. package/Samples~/LaneRunner/Materials/Ground.mat.meta +8 -0
  81. package/Samples~/LaneRunner/Materials/Lane.mat +84 -0
  82. package/Samples~/LaneRunner/Materials/Lane.mat.meta +8 -0
  83. package/Samples~/LaneRunner/Materials/LaneMarker.mat +84 -0
  84. package/Samples~/LaneRunner/Materials/LaneMarker.mat.meta +8 -0
  85. package/Samples~/LaneRunner/Materials/Obstacle.mat +84 -0
  86. package/Samples~/LaneRunner/Materials/Obstacle.mat.meta +8 -0
  87. package/Samples~/LaneRunner/Materials/Player.mat +84 -0
  88. package/Samples~/LaneRunner/Materials/Player.mat.meta +8 -0
  89. package/Samples~/LaneRunner/Materials.meta +8 -0
  90. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +152 -0
  91. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab.meta +7 -0
  92. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +152 -0
  93. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab.meta +7 -0
  94. package/Samples~/LaneRunner/Prefabs.meta +8 -0
  95. package/Samples~/LaneRunner/README.md +35 -0
  96. package/Samples~/LaneRunner/README.md.meta +7 -0
  97. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs +9 -0
  98. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs.meta +2 -0
  99. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs +93 -0
  100. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs.meta +2 -0
  101. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs +155 -0
  102. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs.meta +2 -0
  103. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +110 -0
  104. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs.meta +2 -0
  105. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs +41 -0
  106. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs.meta +2 -0
  107. package/Samples~/LaneRunner/Scripts/Gameplay.meta +8 -0
  108. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs +62 -0
  109. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs.meta +2 -0
  110. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +64 -0
  111. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs.meta +2 -0
  112. package/Samples~/LaneRunner/Scripts/Hyper.meta +8 -0
  113. package/Samples~/LaneRunner/Scripts.meta +8 -0
  114. package/Tests.meta +8 -0
  115. package/package.json +8 -1
@@ -5,6 +5,7 @@ using System.Collections;
5
5
  using System.Collections.Generic;
6
6
  using System.Linq;
7
7
  using System.Globalization;
8
+ using System.IO;
8
9
  using System.Runtime.InteropServices;
9
10
  using System.Text;
10
11
  using UnityEngine;
@@ -139,6 +140,233 @@ namespace Hyper.Internal.Managers
139
140
 
140
141
  #endregion
141
142
 
143
+ #region Replay
144
+
145
+ internal IEnumerator FetchReplay(
146
+ string entryId,
147
+ Action onSuccess,
148
+ Action<string> onFailure)
149
+ {
150
+ if (string.IsNullOrWhiteSpace(entryId))
151
+ {
152
+ onFailure?.Invoke("Replay entryId is missing.");
153
+ yield break;
154
+ }
155
+
156
+ string escapedEntryId = UnityWebRequest.EscapeURL(entryId.Trim());
157
+ string replayAccessUrl =
158
+ $"https://staging-ingress.nestechops.com/hyper/tournaments/HYP/me/tournaments/entries/{escapedEntryId}/replay";
159
+ string lastError = "Unknown replay download error.";
160
+
161
+ for (int attempt = 1; attempt <= MAX_RETRY_COUNT; attempt++)
162
+ {
163
+ using (UnityWebRequest request = UnityWebRequest.Get(replayAccessUrl))
164
+ {
165
+ request.timeout = GENERAL_REQUEST_TIMEOUT;
166
+ request.SetRequestHeader(
167
+ "Accept",
168
+ "application/x-ndjson, application/ndjson, text/plain");
169
+
170
+ yield return request.SendWebRequest();
171
+
172
+ if (request.result == UnityWebRequest.Result.Success)
173
+ {
174
+ if (TryParseReplayResponse(
175
+ request.downloadHandler.text,
176
+ out ReplayResponse replay,
177
+ out string parseError))
178
+ {
179
+ ReplaySessionState.SetMetadata(
180
+ replay.PlayerName,
181
+ replay.AvatarUrl,
182
+ replay.Seed);
183
+ ReplaySessionState.SetReplayData(replay.ActivityNdjson);
184
+ HyperDebug.Log($"Replay downloaded for entry {entryId}.");
185
+ onSuccess?.Invoke();
186
+ yield break;
187
+ }
188
+
189
+ lastError = parseError;
190
+ }
191
+ else
192
+ {
193
+ lastError = $"HTTP {request.responseCode}: {request.error}";
194
+ }
195
+ }
196
+
197
+ HyperDebug.LogWarning(
198
+ $"Replay download attempt {attempt}/{MAX_RETRY_COUNT} failed: {lastError}");
199
+
200
+ if (attempt < MAX_RETRY_COUNT)
201
+ {
202
+ yield return new WaitForSecondsRealtime(0.5f);
203
+ }
204
+ }
205
+
206
+ onFailure?.Invoke(lastError);
207
+ }
208
+
209
+ internal IEnumerator FetchReplayAvatar(
210
+ string avatarUrl,
211
+ Action<Texture2D> onSuccess,
212
+ Action<string> onFailure)
213
+ {
214
+ if (string.IsNullOrWhiteSpace(avatarUrl))
215
+ {
216
+ onSuccess?.Invoke(null);
217
+ yield break;
218
+ }
219
+
220
+ string lastError = "Unknown replay avatar download error.";
221
+ for (int attempt = 1; attempt <= MAX_RETRY_COUNT; attempt++)
222
+ {
223
+ using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(avatarUrl, false))
224
+ {
225
+ request.timeout = GENERAL_REQUEST_TIMEOUT;
226
+ yield return request.SendWebRequest();
227
+
228
+ if (request.result == UnityWebRequest.Result.Success)
229
+ {
230
+ Texture2D avatar = DownloadHandlerTexture.GetContent(request);
231
+ if (avatar != null)
232
+ {
233
+ HyperDebug.Log("Replay avatar downloaded.");
234
+ onSuccess?.Invoke(avatar);
235
+ yield break;
236
+ }
237
+
238
+ lastError = "The replay avatar response contained no texture.";
239
+ }
240
+ else
241
+ {
242
+ lastError = $"HTTP {request.responseCode}: {request.error}";
243
+ }
244
+ }
245
+
246
+ HyperDebug.LogWarning(
247
+ $"Replay avatar attempt {attempt}/{MAX_RETRY_COUNT} failed: {lastError}");
248
+
249
+ if (attempt < MAX_RETRY_COUNT)
250
+ {
251
+ yield return new WaitForSecondsRealtime(0.5f);
252
+ }
253
+ }
254
+
255
+ onFailure?.Invoke(lastError);
256
+ }
257
+
258
+ internal IEnumerator CheckInternetConnection(Action<bool> onComplete)
259
+ {
260
+ using (UnityWebRequest request = UnityWebRequest.Head(GetServerUrl()))
261
+ {
262
+ request.timeout = 2;
263
+ yield return request.SendWebRequest();
264
+
265
+ // Any HTTP response proves that the backend was reachable. A 4xx/5xx
266
+ // response is still connectivity; replay validation belongs to bootstrap.
267
+ bool isConnected = request.responseCode > 0 &&
268
+ request.result != UnityWebRequest.Result.ConnectionError;
269
+ onComplete?.Invoke(isConnected);
270
+ }
271
+ }
272
+
273
+ internal static bool TryParseReplayResponse(
274
+ string ndjson,
275
+ out ReplayResponse replay,
276
+ out string error)
277
+ {
278
+ replay = default;
279
+ error = null;
280
+
281
+ if (string.IsNullOrWhiteSpace(ndjson))
282
+ {
283
+ error = "The replay response was empty.";
284
+ return false;
285
+ }
286
+
287
+ using (var reader = new StringReader(ndjson))
288
+ {
289
+ string seedLine = reader.ReadLine();
290
+ string nameLine = reader.ReadLine();
291
+ string avatarLine = reader.ReadLine();
292
+ string activities = reader.ReadToEnd();
293
+
294
+ if (seedLine == null || nameLine == null || avatarLine == null)
295
+ {
296
+ error = "The replay response is missing required metadata.";
297
+ return false;
298
+ }
299
+
300
+ try
301
+ {
302
+ JArray seedData = JArray.Parse(seedLine);
303
+ JArray nameData = JArray.Parse(nameLine);
304
+ JArray avatarData = JArray.Parse(avatarLine);
305
+
306
+ if (seedData.Count != 1 ||
307
+ seedData[0].Type != JTokenType.Integer ||
308
+ !long.TryParse(
309
+ seedData[0].ToString(),
310
+ NumberStyles.Integer,
311
+ CultureInfo.InvariantCulture,
312
+ out long seed))
313
+ {
314
+ error = "Replay metadata contains an invalid seed.";
315
+ return false;
316
+ }
317
+
318
+ if (nameData.Count != 1 || nameData[0].Type != JTokenType.String)
319
+ {
320
+ error = "Replay metadata contains an invalid player name.";
321
+ return false;
322
+ }
323
+
324
+ if (avatarData.Count != 1 || avatarData[0].Type != JTokenType.String)
325
+ {
326
+ error = "Replay metadata contains an invalid avatar URL.";
327
+ return false;
328
+ }
329
+
330
+ if (string.IsNullOrWhiteSpace(activities))
331
+ {
332
+ error = "The replay response contains no gameplay activities.";
333
+ return false;
334
+ }
335
+
336
+ replay = new ReplayResponse(
337
+ seed,
338
+ nameData[0].Value<string>() ?? string.Empty,
339
+ avatarData[0].Value<string>() ?? string.Empty,
340
+ activities);
341
+
342
+ return true;
343
+ }
344
+ catch (JsonException)
345
+ {
346
+ error = "Replay metadata contains invalid JSON.";
347
+ return false;
348
+ }
349
+ }
350
+ }
351
+
352
+ internal readonly struct ReplayResponse
353
+ {
354
+ internal long Seed { get; }
355
+ internal string PlayerName { get; }
356
+ internal string AvatarUrl { get; }
357
+ internal string ActivityNdjson { get; }
358
+
359
+ internal ReplayResponse(long seed, string playerName, string avatarUrl, string activityNdjson)
360
+ {
361
+ Seed = seed;
362
+ PlayerName = playerName;
363
+ AvatarUrl = avatarUrl;
364
+ ActivityNdjson = activityNdjson;
365
+ }
366
+ }
367
+
368
+ #endregion
369
+
142
370
  #region Session Management
143
371
 
144
372
  private Coroutine _createSessionCoroutine;
@@ -832,6 +1060,14 @@ namespace Hyper.Internal.Managers
832
1060
 
833
1061
  public void StartReconnectionCoroutine()
834
1062
  {
1063
+ if (_gameManager == null) _gameManager = HyperRuntime.GameManager;
1064
+
1065
+ if (_gameManager != null && _gameManager.IsReplayMode)
1066
+ {
1067
+ _gameManager.RetryReplayLoading();
1068
+ return;
1069
+ }
1070
+
835
1071
  if (_reconnectionCoroutine != null)
836
1072
  {
837
1073
  HyperDebug.LogWarning("Reconnection already in progress");
@@ -1075,23 +1311,9 @@ namespace Hyper.Internal.Managers
1075
1311
  _finalScoreSubmitted = false;
1076
1312
  _isWaitingForFinalScoreResponse = true;
1077
1313
 
1078
- float currentScoreDataScoreTime = _dataManager.CurrentScoreData.cumulativeScore + _dataManager.CurrentScoreData.scoreSnapshot.scoreTime;
1079
- float lastCapturedScoreDataScoreTime = _lastSentBatch.Last().cumulativeScore + _lastSentBatch.Last().scoreSnapshot.scoreTime;
1080
- ScoreData finalScoreData;
1081
-
1082
- if (currentScoreDataScoreTime > lastCapturedScoreDataScoreTime)
1083
- {
1084
- finalScoreData = _dataManager.CurrentScoreData;
1085
- finalScoreData.isFinal = true;
1086
- }
1087
- else
1088
- {
1089
- finalScoreData = _lastSentBatch.Last();
1090
- finalScoreData.isFinal = true;
1091
- finalScoreData.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
1092
- finalScoreData.scoreSnapshot.scoreDelta = 0;
1093
- finalScoreData.scoreSnapshot.gameplayModifiers = new GameplayModifiers();
1094
- }
1314
+ ScoreData finalScoreData = CreateFinalScoreMarker(
1315
+ _dataManager.CurrentScoreData,
1316
+ _lastSentBatch.LastOrDefault());
1095
1317
 
1096
1318
  // For consistency with per-second and backlog submissions, always send
1097
1319
  // the final score as an array of ScoreData.
@@ -1207,8 +1429,9 @@ namespace Hyper.Internal.Managers
1207
1429
 
1208
1430
  _gameManager.GameOverModalComponent.Init();
1209
1431
 
1210
- var finalScoreData = _dataManager.CurrentScoreData;
1211
- finalScoreData.isFinal = true;
1432
+ var finalScoreData = CreateFinalScoreMarker(
1433
+ _dataManager.CurrentScoreData,
1434
+ _lastSentBatch.LastOrDefault());
1212
1435
 
1213
1436
  yield return new WaitForSecondsRealtime(3f);
1214
1437
 
@@ -1265,10 +1488,6 @@ namespace Hyper.Internal.Managers
1265
1488
  // Scores will be flushed when the connection is restored.
1266
1489
  if (!isSocketConnected)
1267
1490
  {
1268
- #if UNITY_WEBGL && !UNITY_EDITOR
1269
- int pendingCount = _dataManager?.PendingScores?.Count ?? 0;
1270
- HyperDebug.Log($"[Score] Socket disconnected - accumulating scores for offline batch. PendingCount={pendingCount}");
1271
- #endif
1272
1491
  _isWaitingForTokenLogEmitted = false;
1273
1492
  _hasLoggedWaitingForGameStartAck = false;
1274
1493
  yield return _scoreSendInterval;
@@ -1524,7 +1743,8 @@ namespace Hyper.Internal.Managers
1524
1743
  var clonedSnapshot = new ScoreSnapshot
1525
1744
  {
1526
1745
  scoreTime = snapshot.scoreTime,
1527
- scoreDelta = snapshot.scoreDelta
1746
+ scoreDelta = snapshot.scoreDelta,
1747
+ playActivities = ClonePlayActivities(snapshot.playActivities)
1528
1748
  };
1529
1749
 
1530
1750
  if (snapshot.gameplayModifiers != null)
@@ -1548,6 +1768,43 @@ namespace Hyper.Internal.Managers
1548
1768
  return cloned;
1549
1769
  }
1550
1770
 
1771
+ internal static ScoreData CreateFinalScoreMarker(ScoreData currentScore, ScoreData lastSentScore)
1772
+ {
1773
+ ScoreData scoreSource = currentScore ?? lastSentScore;
1774
+ float currentScoreTime = currentScore?.scoreSnapshot?.scoreTime ?? 0f;
1775
+ float lastSentScoreTime = lastSentScore?.scoreSnapshot?.scoreTime ?? 0f;
1776
+
1777
+ return new ScoreData
1778
+ {
1779
+ cumulativeScore = scoreSource?.cumulativeScore ?? 0,
1780
+ isFinal = true,
1781
+ timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
1782
+ scoreSnapshot = new ScoreSnapshot
1783
+ {
1784
+ // CurrentScoreData is refreshed only when the score changes, while
1785
+ // periodic snapshots can continue afterward. Never let the final
1786
+ // completion marker move the score timeline backward.
1787
+ scoreTime = Mathf.Max(currentScoreTime, lastSentScoreTime),
1788
+ scoreDelta = 0,
1789
+ gameplayModifiers = new GameplayModifiers(),
1790
+ playActivities = null
1791
+ }
1792
+ };
1793
+ }
1794
+
1795
+ private static List<object[]> ClonePlayActivities(List<object[]> source)
1796
+ {
1797
+ if (source == null || source.Count == 0) return null;
1798
+
1799
+ var clone = new List<object[]>(source.Count);
1800
+ for (int i = 0; i < source.Count; i++)
1801
+ {
1802
+ object[] activity = source[i];
1803
+ clone.Add(activity == null ? null : (object[])activity.Clone());
1804
+ }
1805
+ return clone;
1806
+ }
1807
+
1551
1808
  [Serializable]
1552
1809
  private class WebSocketScoreMessage
1553
1810
  {
@@ -3,8 +3,14 @@ using System.Collections;
3
3
  using System.Collections.Generic;
4
4
  using System.Linq;
5
5
  using System.Runtime.InteropServices;
6
+ #if UNITY_EDITOR
7
+ using System.IO;
8
+ using System.Text;
9
+ using Newtonsoft.Json;
10
+ #endif
6
11
  using UnityEngine;
7
12
  using Hyper.Internal;
13
+ using Hyper.Internal.Replay;
8
14
  using Hyper.Shared;
9
15
 
10
16
  namespace Hyper.Internal.Managers
@@ -18,6 +24,7 @@ namespace Hyper.Internal.Managers
18
24
 
19
25
  private EventManager _eventManager;
20
26
  private GameManager _gameManager;
27
+ private ReplayActivityRecorder _replayRecorder;
21
28
 
22
29
  void Awake()
23
30
  {
@@ -26,13 +33,32 @@ namespace Hyper.Internal.Managers
26
33
 
27
34
  internal void Initialize()
28
35
  {
36
+ _replayRecorder = new ReplayActivityRecorder(GetUnixTimeSeconds, GetSimulationTimeSeconds);
37
+ #if UNITY_EDITOR
38
+ _replayRecorder.ActivityRecorded += CaptureEditorReplayActivity;
39
+ #endif
29
40
  _eventManager.OnTutorialFetchComplete += GetAndAssignTutorialResources;
41
+ _eventManager.OnGameStart += HandleGameStarted;
42
+ _eventManager.OnGamePaused += HandleGamePaused;
43
+ _eventManager.OnGameResumed += HandleGameResumed;
44
+ _eventManager.OnGameEnded += HandleGameEnded;
30
45
  AudioListener.volume = 0;
31
46
  }
32
47
 
33
48
  internal void Cleanup()
34
49
  {
35
50
  StopAllCoroutines();
51
+ #if UNITY_EDITOR
52
+ if (_replayRecorder != null)
53
+ _replayRecorder.ActivityRecorded -= CaptureEditorReplayActivity;
54
+ #endif
55
+ if (_eventManager == null) return;
56
+
57
+ _eventManager.OnTutorialFetchComplete -= GetAndAssignTutorialResources;
58
+ _eventManager.OnGameStart -= HandleGameStarted;
59
+ _eventManager.OnGamePaused -= HandleGamePaused;
60
+ _eventManager.OnGameResumed -= HandleGameResumed;
61
+ _eventManager.OnGameEnded -= HandleGameEnded;
36
62
  }
37
63
 
38
64
  #endregion
@@ -50,7 +76,7 @@ namespace Hyper.Internal.Managers
50
76
  CurrentScore = score;
51
77
  _gameManager.UpdateScoreLabel();
52
78
 
53
- float currentTimestamp = _gameManager.Config.GameMode == GameMode.Practice
79
+ float currentTimestamp = _gameManager.IsPracticeMode
54
80
  ? HyperRuntime.Timer.CurrentTime
55
81
  : _gameManager.Config.TimeLimit - HyperRuntime.Timer.CurrentTime;
56
82
 
@@ -77,12 +103,66 @@ namespace Hyper.Internal.Managers
77
103
  modifiers.globalModifiers = GlobalMultiplier;
78
104
  modifiers.activePowerUps = ActivePowerups;
79
105
 
80
- if (score > GetBestScore())
106
+ if (!_gameManager.IsReplayMode && score > GetBestScore())
81
107
  {
82
108
  SetBestScore(CurrentScoreData, _gameManager.Config.SessionId);
83
109
  }
84
110
  }
85
111
 
112
+ public void RecordActivity(float value)
113
+ {
114
+ if (!CanRecordReplayActivity()) return;
115
+ _replayRecorder?.RecordValue(value);
116
+ }
117
+
118
+ public void RecordActivity(float first, float second)
119
+ {
120
+ if (!CanRecordReplayActivity()) return;
121
+ _replayRecorder?.RecordValues(first, second);
122
+ }
123
+
124
+ public void RecordActivity(float first, float second, float third)
125
+ {
126
+ if (!CanRecordReplayActivity()) return;
127
+ _replayRecorder?.RecordValues(first, second, third);
128
+ }
129
+
130
+ public void RecordActivity(string action)
131
+ {
132
+ if (!CanRecordReplayActivity()) return;
133
+ _replayRecorder?.RecordAction(action);
134
+ }
135
+
136
+ public void RecordActivity(string firstAction, string secondAction)
137
+ {
138
+ if (!CanRecordReplayActivity()) return;
139
+ _replayRecorder?.RecordActions(firstAction, secondAction);
140
+ }
141
+
142
+ public void RecordActivity(string action, float first)
143
+ {
144
+ if (!CanRecordReplayActivity()) return;
145
+ _replayRecorder?.RecordActionValue(action, first);
146
+ }
147
+
148
+ public void RecordActivity(float first, string action)
149
+ {
150
+ if (!CanRecordReplayActivity()) return;
151
+ _replayRecorder?.RecordValueAction(first, action);
152
+ }
153
+
154
+ public void RecordActivity(string action, float first, float second)
155
+ {
156
+ if (!CanRecordReplayActivity()) return;
157
+ _replayRecorder?.RecordActionValues(action, first, second);
158
+ }
159
+
160
+ public void RecordActivity(float first, float second, string action)
161
+ {
162
+ if (!CanRecordReplayActivity()) return;
163
+ _replayRecorder?.RecordValuesAction(first, second, action);
164
+ }
165
+
86
166
  public void SetComboMultiplier(float value)
87
167
  {
88
168
  ComboMultiplier = Mathf.Max(1f, value);
@@ -243,7 +323,7 @@ namespace Hyper.Internal.Managers
243
323
  {
244
324
  while (true)
245
325
  {
246
- float currentTimestamp = _gameManager.Config.GameMode == GameMode.Practice
326
+ float currentTimestamp = _gameManager.IsPracticeMode
247
327
  ? HyperRuntime.Timer.CurrentTime
248
328
  : _gameManager.Config.TimeLimit - HyperRuntime.Timer.CurrentTime;
249
329
 
@@ -251,34 +331,139 @@ namespace Hyper.Internal.Managers
251
331
 
252
332
  if (currentGameSecond > _lastCapturedGameSecond)
253
333
  {
254
- // Get objects from pools to avoid allocations in this hot path
255
- var newScore = GetPooledScoreData();
256
- var snapshot = GetPooledScoreSnapshot();
257
- var modifiers = GetPooledGameplayModifiers();
334
+ CaptureScoreSnapshot(currentGameSecond, _replayRecorder?.Drain());
335
+ }
258
336
 
259
- newScore.cumulativeScore = CurrentScore;
260
- newScore.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
261
- newScore.isFinal = false;
337
+ yield return null;
338
+ }
339
+ }
262
340
 
263
- snapshot.scoreTime = currentGameSecond;
264
- snapshot.scoreDelta = CurrentScore - _lastCapturedScore;
341
+ private void CaptureScoreSnapshot(float gameSecond, List<object[]> playActivities)
342
+ {
343
+ var newScore = GetPooledScoreData();
344
+ var snapshot = GetPooledScoreSnapshot();
345
+ var modifiers = GetPooledGameplayModifiers();
265
346
 
266
- modifiers.comboModifiers = ComboMultiplier;
267
- modifiers.globalModifiers = GlobalMultiplier;
268
- modifiers.activePowerUps = ActivePowerups;
347
+ newScore.cumulativeScore = CurrentScore;
348
+ newScore.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
349
+ newScore.isFinal = false;
269
350
 
270
- snapshot.gameplayModifiers = modifiers;
271
- newScore.scoreSnapshot = snapshot;
351
+ snapshot.scoreTime = gameSecond;
352
+ snapshot.scoreDelta = CurrentScore - _lastCapturedScore;
353
+ snapshot.playActivities = playActivities;
354
+
355
+ modifiers.comboModifiers = ComboMultiplier;
356
+ modifiers.globalModifiers = GlobalMultiplier;
357
+ modifiers.activePowerUps = ActivePowerups;
272
358
 
273
- PendingScores.Add(newScore);
359
+ snapshot.gameplayModifiers = modifiers;
360
+ newScore.scoreSnapshot = snapshot;
361
+ PendingScores.Add(newScore);
274
362
 
275
- _lastCapturedGameSecond = currentGameSecond;
276
- _lastCapturedScore = CurrentScore;
363
+ _lastCapturedGameSecond = gameSecond;
364
+ _lastCapturedScore = CurrentScore;
365
+ }
366
+
367
+ private void HandleGameStarted()
368
+ {
369
+ if (_gameManager == null) _gameManager = HyperRuntime.GameManager;
370
+ if (!CanRecordReplayActivity()) return;
371
+ #if UNITY_EDITOR
372
+ _editorReplay.Clear();
373
+ _editorReplay.AppendLine(JsonConvert.SerializeObject(
374
+ new object[] { HyperRandom.CurrentSeed }));
375
+ _editorReplay.AppendLine(JsonConvert.SerializeObject(new object[] { "hypergamer" }));
376
+ _editorReplay.AppendLine(JsonConvert.SerializeObject(new object[] { string.Empty }));
377
+ #endif
378
+ _replayRecorder?.StartSession();
379
+ }
380
+
381
+ private void HandleGamePaused()
382
+ {
383
+ if (!CanRecordReplayActivity()) return;
384
+ _replayRecorder?.PauseSession();
385
+ }
386
+
387
+ private void HandleGameResumed()
388
+ {
389
+ if (!CanRecordReplayActivity()) return;
390
+ _replayRecorder?.ResumeSession();
391
+ }
392
+
393
+ private void HandleGameEnded()
394
+ {
395
+ if (!CanRecordReplayActivity()) return;
396
+ _replayRecorder?.EndSession();
397
+ #if UNITY_EDITOR
398
+ SaveEditorReplay();
399
+ #endif
400
+ List<object[]> remainingActivities = _replayRecorder?.Drain();
401
+ if (remainingActivities == null || remainingActivities.Count == 0) return;
402
+
403
+ if (PendingScores.Count > 0 && PendingScores[PendingScores.Count - 1].cumulativeScore == CurrentScore)
404
+ {
405
+ ScoreSnapshot latestSnapshot = PendingScores[PendingScores.Count - 1].scoreSnapshot;
406
+ if (latestSnapshot.playActivities == null)
407
+ {
408
+ latestSnapshot.playActivities = remainingActivities;
277
409
  }
410
+ else
411
+ {
412
+ latestSnapshot.playActivities.AddRange(remainingActivities);
413
+ }
414
+ return;
415
+ }
278
416
 
279
- yield return null;
417
+ CaptureScoreSnapshot(GetCurrentGameTime(), remainingActivities);
418
+ }
419
+
420
+ private bool CanRecordReplayActivity()
421
+ {
422
+ if (_gameManager == null) _gameManager = HyperRuntime.GameManager;
423
+ return _gameManager != null && _gameManager.IsBattleMode && !_gameManager.IsReplayMode;
424
+ }
425
+
426
+ #if UNITY_EDITOR
427
+ private readonly StringBuilder _editorReplay = new StringBuilder(4096);
428
+
429
+ private void CaptureEditorReplayActivity(object[] activity)
430
+ {
431
+ _editorReplay.AppendLine(JsonConvert.SerializeObject(activity));
432
+ }
433
+
434
+ private void SaveEditorReplay()
435
+ {
436
+ try
437
+ {
438
+ File.WriteAllText(ReplaySessionState.EditorReplayFilePath, _editorReplay.ToString());
439
+ HyperDebug.Log($"Replay saved to {ReplaySessionState.EditorReplayFilePath}");
440
+ }
441
+ catch (Exception exception)
442
+ {
443
+ HyperDebug.LogWarning($"Unable to save Editor replay: {exception.Message}");
280
444
  }
281
445
  }
446
+ #endif
447
+
448
+ private float GetCurrentGameTime()
449
+ {
450
+ if (_gameManager == null) _gameManager = HyperRuntime.GameManager;
451
+ if (_gameManager == null || HyperRuntime.Timer == null) return Mathf.Max(0f, _lastCapturedGameSecond);
452
+
453
+ return _gameManager.IsPracticeMode
454
+ ? HyperRuntime.Timer.CurrentTime
455
+ : _gameManager.Config.TimeLimit - HyperRuntime.Timer.CurrentTime;
456
+ }
457
+
458
+ private static double GetUnixTimeSeconds()
459
+ {
460
+ return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000d;
461
+ }
462
+
463
+ private static double GetSimulationTimeSeconds()
464
+ {
465
+ return Time.fixedTimeAsDouble;
466
+ }
282
467
 
283
468
  private ScoreData GetPooledScoreData()
284
469
  {
@@ -337,6 +522,7 @@ namespace Hyper.Internal.Managers
337
522
 
338
523
  snapshot.scoreTime = 0f;
339
524
  snapshot.scoreDelta = 0;
525
+ snapshot.playActivities = null;
340
526
 
341
527
  if (_scoreSnapshotPool.Count < SCORE_POOL_CAPACITY)
342
528
  {