app.hypergames.hypersdk 1.10.0 → 1.11.0-beta.10

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 (124) hide show
  1. package/CHANGELOG.md +97 -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 +115 -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 +208 -21
  11. package/Runtime/Internal/Managers/GameManager.cs +404 -19
  12. package/Runtime/Internal/Managers/ReplayManager.cs +118 -0
  13. package/Runtime/Internal/Managers/ReplayManager.cs.meta +11 -0
  14. package/Runtime/Internal/Managers/TimeManager.cs +78 -19
  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 +238 -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 +531 -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/ReplayPausedModal.cs +145 -0
  34. package/Runtime/Internal/UI/Modals/ReplayPausedModal.cs.meta +2 -0
  35. package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +39 -29
  36. package/Runtime/Public/AssemblyInfo.cs +3 -0
  37. package/Runtime/Public/AssemblyInfo.cs.meta +2 -0
  38. package/Runtime/Public/Core/HyperSDK.cs +321 -109
  39. package/Runtime/Public/Hyper.asmdef +2 -1
  40. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +610 -0
  41. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs.meta +2 -0
  42. package/Runtime/Public/Replay.meta +8 -0
  43. package/Runtime/Resources/Fonts/Nunito/Nunito-Bold SDF.asset +500 -457
  44. package/Runtime/Resources/Fonts/Nunito/Nunito-Light SDF.asset +528 -521
  45. package/Runtime/Resources/Materials/HyperReplayScanlines.preset +340 -0
  46. package/Runtime/Resources/Materials/HyperReplayScanlines.preset.meta +8 -0
  47. package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat +55 -0
  48. package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat.meta +8 -0
  49. package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader +150 -0
  50. package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader.meta +10 -0
  51. package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Normal.png +0 -0
  52. package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Normal.png.meta +156 -0
  53. package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Pressed.png +0 -0
  54. package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Pressed.png.meta +156 -0
  55. package/Runtime/Resources/Sprites/Replays/Fallback avatar.png +0 -0
  56. package/Runtime/Resources/Sprites/Replays/Fallback avatar.png.meta +156 -0
  57. package/Runtime/Resources/Sprites/Replays/Replay frame.png +0 -0
  58. package/Runtime/Resources/Sprites/Replays/Replay frame.png.meta +156 -0
  59. package/Runtime/Resources/Sprites/Replays/Replay modal icon.png +0 -0
  60. package/Runtime/Resources/Sprites/Replays/Replay modal icon.png.meta +156 -0
  61. package/Runtime/Resources/Sprites/Replays/Replay.png +0 -0
  62. package/Runtime/Resources/Sprites/Replays/Replay.png.meta +156 -0
  63. package/Runtime/Resources/Sprites/Replays.meta +8 -0
  64. package/Runtime/Resources/UIPrefabs/ControlBar.prefab +169 -2
  65. package/Runtime/Resources/UIPrefabs/GamePausedModal_Battle.prefab +1 -1
  66. package/Runtime/Resources/UIPrefabs/ReplayController.prefab +1388 -0
  67. package/Runtime/Resources/UIPrefabs/ReplayController.prefab.meta +7 -0
  68. package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab +90 -0
  69. package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab.meta +7 -0
  70. package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab +958 -0
  71. package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab.meta +7 -0
  72. package/Runtime/Resources/UIPrefabs/ReplayPausedModal.prefab +2194 -0
  73. package/Runtime/Resources/UIPrefabs/ReplayPausedModal.prefab.meta +7 -0
  74. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +189 -0
  75. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab.meta +7 -0
  76. package/Runtime/Resources/UIPrefabs/SnackBar.prefab +2 -2
  77. package/Runtime/Shared/AssemblyInfo.cs +5 -0
  78. package/Runtime/Shared/AssemblyInfo.cs.meta +11 -0
  79. package/Runtime/Shared/DTOs/Enums.cs +8 -3
  80. package/Runtime/Shared/DTOs/Score/ScoreSnapshot.cs +13 -1
  81. package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs +40 -0
  82. package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs.meta +11 -0
  83. package/Runtime/Shared/Replay.meta +8 -0
  84. package/Samples~/LaneRunner/LaneRunnerSample.unity +1604 -0
  85. package/Samples~/LaneRunner/LaneRunnerSample.unity.meta +7 -0
  86. package/Samples~/LaneRunner/Materials/DoubleScore.mat +86 -0
  87. package/Samples~/LaneRunner/Materials/DoubleScore.mat.meta +8 -0
  88. package/Samples~/LaneRunner/Materials/Ground.mat +84 -0
  89. package/Samples~/LaneRunner/Materials/Ground.mat.meta +8 -0
  90. package/Samples~/LaneRunner/Materials/Lane.mat +84 -0
  91. package/Samples~/LaneRunner/Materials/Lane.mat.meta +8 -0
  92. package/Samples~/LaneRunner/Materials/LaneMarker.mat +84 -0
  93. package/Samples~/LaneRunner/Materials/LaneMarker.mat.meta +8 -0
  94. package/Samples~/LaneRunner/Materials/Obstacle.mat +84 -0
  95. package/Samples~/LaneRunner/Materials/Obstacle.mat.meta +8 -0
  96. package/Samples~/LaneRunner/Materials/Player.mat +84 -0
  97. package/Samples~/LaneRunner/Materials/Player.mat.meta +8 -0
  98. package/Samples~/LaneRunner/Materials.meta +8 -0
  99. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +152 -0
  100. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab.meta +7 -0
  101. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +152 -0
  102. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab.meta +7 -0
  103. package/Samples~/LaneRunner/Prefabs.meta +8 -0
  104. package/Samples~/LaneRunner/README.md +42 -0
  105. package/Samples~/LaneRunner/README.md.meta +7 -0
  106. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs +9 -0
  107. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs.meta +2 -0
  108. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs +93 -0
  109. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs.meta +2 -0
  110. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs +169 -0
  111. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs.meta +2 -0
  112. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +112 -0
  113. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs.meta +2 -0
  114. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs +41 -0
  115. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs.meta +2 -0
  116. package/Samples~/LaneRunner/Scripts/Gameplay.meta +8 -0
  117. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs +62 -0
  118. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs.meta +2 -0
  119. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +72 -0
  120. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs.meta +2 -0
  121. package/Samples~/LaneRunner/Scripts/Hyper.meta +8 -0
  122. package/Samples~/LaneRunner/Scripts.meta +8 -0
  123. package/Tests.meta +8 -0
  124. package/package.json +8 -1
@@ -4,6 +4,7 @@ using System.Collections.Generic;
4
4
  using UnityEngine;
5
5
  using UnityEngine.SceneManagement;
6
6
  using Hyper.Internal.Managers;
7
+ using Hyper.Shared;
7
8
 
8
9
  namespace Hyper.Internal
9
10
  {
@@ -19,6 +20,8 @@ namespace Hyper.Internal
19
20
 
20
21
  private static bool _isInitialized;
21
22
  private static bool _isSoftRestarting;
23
+ private static bool _replayRandomResetPending;
24
+ private static bool _replayCustomRestartPending;
22
25
  private static bool _customLoadingComplete;
23
26
  private static bool _customLoadingActive;
24
27
  private static float _customLoadingProgress;
@@ -30,6 +33,8 @@ namespace Hyper.Internal
30
33
  private static DataManager _dataManager;
31
34
  private static EventManager _eventManager;
32
35
  private static TimerManager _timerManager;
36
+ private static ReplayManager _replayManager;
37
+ private static double _fixedSimulationTimeSeconds;
33
38
 
34
39
  internal static bool IsInitialized => _isInitialized;
35
40
  internal static bool IsSoftRestarting
@@ -37,6 +42,7 @@ namespace Hyper.Internal
37
42
  get => _isSoftRestarting;
38
43
  set => _isSoftRestarting = value;
39
44
  }
45
+ internal static bool IsReplayCustomRestartPending => _replayCustomRestartPending;
40
46
  internal static bool CustomLoadingComplete
41
47
  {
42
48
  get => _customLoadingComplete;
@@ -55,6 +61,8 @@ namespace Hyper.Internal
55
61
  internal static DataManager DataManager => _dataManager;
56
62
  internal static EventManager EventManager => _eventManager;
57
63
  internal static TimerManager Timer => _timerManager;
64
+ internal static ReplayManager ReplayManager => _replayManager;
65
+ internal static double FixedSimulationTimeSeconds => _fixedSimulationTimeSeconds;
58
66
 
59
67
  internal static event Action CustomLoadingStarted
60
68
  {
@@ -114,6 +122,85 @@ namespace Hyper.Internal
114
122
  }
115
123
  }
116
124
 
125
+ /// <summary>
126
+ /// Restarts replay playback through HyperLoader while preserving replay session data.
127
+ /// </summary>
128
+ internal static void RestartReplaySession()
129
+ {
130
+ ReplaySessionState.ResetForWatchAgain();
131
+ RestartReplay();
132
+ }
133
+
134
+ internal static void SeekReplaySession(float targetProgress, float resumeSpeed, float seekSpeed)
135
+ {
136
+ if (!ReplaySessionState.IsActive || ReplaySessionState.HasEnded) return;
137
+
138
+ ReplaySessionState.RequestSeek(targetProgress, resumeSpeed, seekSpeed);
139
+ ReplaySessionState.ResetForSeekRestart();
140
+ RestartReplay();
141
+ }
142
+
143
+ private static void RestartReplay()
144
+ {
145
+ _isSoftRestarting = true;
146
+ Time.timeScale = 1f;
147
+
148
+ _replayCustomRestartPending = false;
149
+ _replayRandomResetPending = false;
150
+ _replayManager?.PrepareForRestart();
151
+
152
+ if (_host != null)
153
+ {
154
+ _host.StartCoroutine(RestartSessionCoroutine());
155
+ }
156
+ else
157
+ {
158
+ ClearAllDontDestroyOnLoadObjects();
159
+ SceneManager.LoadScene("HyperLoader");
160
+ }
161
+ }
162
+
163
+ internal static void ResetReplayRandomState()
164
+ {
165
+ int originalSeed = unchecked((int)ReplaySessionState.OriginalRandomSeed);
166
+ if (HyperRandom.CurrentSeed == originalSeed)
167
+ {
168
+ // Preserve references held by game code while rewinding every stream.
169
+ HyperRandom.ResetAll();
170
+ return;
171
+ }
172
+
173
+ HyperRandom.Init(ReplaySessionState.OriginalRandomSeed);
174
+ }
175
+
176
+ internal static void ApplyPendingReplayRandomReset()
177
+ {
178
+ if (!_replayRandomResetPending) return;
179
+
180
+ _replayRandomResetPending = false;
181
+ ResetReplayRandomState();
182
+ }
183
+
184
+ internal static void DeferReplayRandomReset()
185
+ {
186
+ _replayRandomResetPending = true;
187
+ }
188
+
189
+ internal static void CompleteCustomRestart()
190
+ {
191
+ if (!_replayCustomRestartPending || _host == null) return;
192
+
193
+ _replayCustomRestartPending = false;
194
+ _host.StartCoroutine(CompleteReplayCustomRestartNextFrame());
195
+ }
196
+
197
+ private static IEnumerator CompleteReplayCustomRestartNextFrame()
198
+ {
199
+ yield return null;
200
+ _gameManager?.CompleteReplayCustomRestart();
201
+ _isSoftRestarting = false;
202
+ }
203
+
117
204
  /// <summary>
118
205
  /// Coroutine-based restart that properly unloads all scenes before reloading.
119
206
  /// Handles async scene operations and ensures clean state before restart.
@@ -122,6 +209,7 @@ namespace Hyper.Internal
122
209
  {
123
210
  yield return UnloadAllScenesCoroutine();
124
211
 
212
+ Clear();
125
213
  ClearAllDontDestroyOnLoadObjects();
126
214
  SceneManager.LoadScene("HyperLoader");
127
215
  }
@@ -302,6 +390,9 @@ namespace Hyper.Internal
302
390
  _gameManager = _singletonObject.AddComponent<GameManager>();
303
391
  _gameManager.Initialize();
304
392
 
393
+ _replayManager = new ReplayManager();
394
+ _replayManager.Initialize(_eventManager);
395
+
305
396
  _timerManager = new TimerManager();
306
397
  _timerManager.Initialize();
307
398
  _timerManager.InjectEventHooks();
@@ -313,6 +404,14 @@ namespace Hyper.Internal
313
404
  _timerManager?.Update();
314
405
  }
315
406
 
407
+ internal static void FixedTick()
408
+ {
409
+ if (!_isInitialized) return;
410
+ _fixedSimulationTimeSeconds += Time.fixedDeltaTime;
411
+ _replayManager?.FixedTick();
412
+ _timerManager?.FixedTick();
413
+ }
414
+
316
415
  internal static void Clear()
317
416
  {
318
417
  if (_host == null)
@@ -323,6 +422,9 @@ namespace Hyper.Internal
323
422
 
324
423
  _timerManager = null;
325
424
 
425
+ _replayManager?.Cleanup();
426
+ _replayManager = null;
427
+
326
428
  _dataManager?.Cleanup();
327
429
  _dataManager = null;
328
430
 
@@ -339,6 +441,9 @@ namespace Hyper.Internal
339
441
  _customLoadingComplete = false;
340
442
  _customLoadingActive = false;
341
443
  _customLoadingProgress = 0f;
444
+ _replayRandomResetPending = false;
445
+ _replayCustomRestartPending = false;
446
+ _fixedSimulationTimeSeconds = 0d;
342
447
 
343
448
  if (_singletonObject != null)
344
449
  {
@@ -364,6 +469,11 @@ namespace Hyper.Internal
364
469
  {
365
470
  if (obj.scene.name == "DontDestroyOnLoad" && obj.transform.parent == null)
366
471
  {
472
+ if (ReplaySessionState.IsSeeking && ReplayPresentationCanvas.IsPresentationRoot(obj))
473
+ {
474
+ continue;
475
+ }
476
+
367
477
  UnityEngine.Object.Destroy(obj);
368
478
  }
369
479
  }
@@ -372,6 +482,7 @@ namespace Hyper.Internal
372
482
  internal static void OnHostDestroyed(HyperRuntimeHost host)
373
483
  {
374
484
  if (_host != host) return;
485
+ _replayManager?.Cleanup();
375
486
  ResetStaticState();
376
487
  }
377
488
 
@@ -385,11 +496,15 @@ namespace Hyper.Internal
385
496
  _gameManager = null;
386
497
  _backendManager = null;
387
498
  _dataManager = null;
499
+ _replayManager = null;
388
500
  _gameContent = null;
389
501
  _isInitialized = false;
390
502
  _customLoadingComplete = false;
391
503
  _customLoadingActive = false;
392
504
  _customLoadingProgress = 0f;
505
+ _replayRandomResetPending = false;
506
+ _replayCustomRestartPending = false;
507
+ _fixedSimulationTimeSeconds = 0d;
393
508
  }
394
509
  }
395
510
  }
@@ -6,6 +6,7 @@ namespace Hyper.Internal
6
6
  /// Lightweight MonoBehaviour that allows the internal runtime to participate in
7
7
  /// Unity's update loop without exposing managed objects to public assemblies.
8
8
  /// </summary>
9
+ [DefaultExecutionOrder(-10000)]
9
10
  internal sealed class HyperRuntimeHost : MonoBehaviour
10
11
  {
11
12
  private void Awake()
@@ -18,6 +19,11 @@ namespace Hyper.Internal
18
19
  HyperRuntime.Tick();
19
20
  }
20
21
 
22
+ private void FixedUpdate()
23
+ {
24
+ HyperRuntime.FixedTick();
25
+ }
26
+
21
27
  private void OnDestroy()
22
28
  {
23
29
  HyperRuntime.OnHostDestroyed(this);
@@ -27,6 +27,11 @@ namespace Hyper.Internal
27
27
  /// </summary>
28
28
  public bool isPVP => GameMode == GameMode.Battle;
29
29
 
30
+ /// <summary>
31
+ /// Returns true for modes that use battle gameplay, scenes, and timers.
32
+ /// </summary>
33
+ public bool IsBattleGameplay => GameMode == GameMode.Battle || GameMode == GameMode.Replay;
34
+
30
35
  #endregion
31
36
 
32
37
  #region Internal Fields
@@ -114,5 +119,17 @@ namespace Hyper.Internal
114
119
  {
115
120
  GameMode = GameMode.Practice;
116
121
  }
122
+
123
+ public void SetModeAsReplay()
124
+ {
125
+ GameMode = GameMode.Replay;
126
+ }
127
+
128
+ internal void SetModeFromLaunchFlags(bool isPVP, bool isReplay)
129
+ {
130
+ GameMode = isReplay
131
+ ? GameMode.Replay
132
+ : isPVP ? GameMode.Battle : GameMode.Practice;
133
+ }
117
134
  }
118
- }
135
+ }
@@ -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
  {