app.hypergames.hypersdk 1.9.1 → 1.11.0-beta.1

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 +37 -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 +58 -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 +350 -15
  12. package/Runtime/Internal/Managers/ReplayManager.cs +111 -0
  13. package/Runtime/Internal/Managers/ReplayManager.cs.meta +11 -0
  14. package/Runtime/Internal/Managers/TimeManager.cs +56 -14
  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/ConcaveImage.cs +147 -0
  23. package/Runtime/Internal/UI/Components/ConcaveImage.cs.meta +11 -0
  24. package/Runtime/Internal/UI/Components/ControlBar.cs +86 -6
  25. package/Runtime/Internal/UI/Components/PauseButton.cs +4 -1
  26. package/Runtime/Internal/UI/Components/ReplayController.cs +501 -0
  27. package/Runtime/Internal/UI/Components/ReplayController.cs.meta +2 -0
  28. package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs +89 -0
  29. package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs.meta +2 -0
  30. package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs +183 -0
  31. package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs.meta +2 -0
  32. package/Runtime/Internal/UI/Components/SnackBar.cs +2 -9
  33. package/Runtime/Internal/UI/Modals/LoadingFailedModal.cs +12 -0
  34. package/Runtime/Internal/UI/Modals/ReplayOverModal.cs +54 -0
  35. package/Runtime/Internal/UI/Modals/ReplayOverModal.cs.meta +2 -0
  36. package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +38 -29
  37. package/Runtime/Public/AssemblyInfo.cs +3 -0
  38. package/Runtime/Public/AssemblyInfo.cs.meta +2 -0
  39. package/Runtime/Public/Core/HyperSDK.cs +305 -109
  40. package/Runtime/Public/Hyper.asmdef +2 -1
  41. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +555 -0
  42. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs.meta +2 -0
  43. package/Runtime/Public/Replay.meta +8 -0
  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/ConcaveImage.shader +155 -0
  50. package/Runtime/Resources/Shaders/ConcaveImage.shader.meta +10 -0
  51. package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader +150 -0
  52. package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader.meta +10 -0
  53. package/Runtime/Resources/Sprites/Replays/Fallback avatar.png +0 -0
  54. package/Runtime/Resources/Sprites/Replays/Fallback avatar.png.meta +156 -0
  55. package/Runtime/Resources/Sprites/Replays/Recording.png +0 -0
  56. package/Runtime/Resources/Sprites/{Control Bar/Top_Black_Rounded_Bar_Portrait_IV.png.meta → Replays/Recording.png.meta} +16 -29
  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.meta +8 -0
  62. package/Runtime/Resources/UIPrefabs/ControlBar.prefab +389 -42
  63. package/Runtime/Resources/UIPrefabs/GamePausedModal_Battle.prefab +1 -1
  64. package/Runtime/Resources/UIPrefabs/ReplayController.prefab +1269 -0
  65. package/Runtime/Resources/UIPrefabs/ReplayController.prefab.meta +7 -0
  66. package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab +90 -0
  67. package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab.meta +7 -0
  68. package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab +958 -0
  69. package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab.meta +7 -0
  70. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +189 -0
  71. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab.meta +7 -0
  72. package/Runtime/Resources/UIPrefabs/Rounded Cutout.prefab +96 -0
  73. package/Runtime/Resources/UIPrefabs/Rounded Cutout.prefab.meta +7 -0
  74. package/Runtime/Resources/UIPrefabs/SnackBar.prefab +23 -6
  75. package/Runtime/Shared/AssemblyInfo.cs +5 -0
  76. package/Runtime/Shared/AssemblyInfo.cs.meta +11 -0
  77. package/Runtime/Shared/DTOs/Enums.cs +8 -3
  78. package/Runtime/Shared/DTOs/Score/ScoreSnapshot.cs +13 -1
  79. package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs +37 -0
  80. package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs.meta +11 -0
  81. package/Runtime/Shared/Replay.meta +8 -0
  82. package/Runtime/Shared/Utils/HyperRandom.cs +25 -5
  83. package/Samples~/LaneRunner/LaneRunnerSample.unity +1603 -0
  84. package/Samples~/LaneRunner/LaneRunnerSample.unity.meta +7 -0
  85. package/Samples~/LaneRunner/Materials/DoubleScore.mat +86 -0
  86. package/Samples~/LaneRunner/Materials/DoubleScore.mat.meta +8 -0
  87. package/Samples~/LaneRunner/Materials/Ground.mat +84 -0
  88. package/Samples~/LaneRunner/Materials/Ground.mat.meta +8 -0
  89. package/Samples~/LaneRunner/Materials/Lane.mat +84 -0
  90. package/Samples~/LaneRunner/Materials/Lane.mat.meta +8 -0
  91. package/Samples~/LaneRunner/Materials/LaneMarker.mat +84 -0
  92. package/Samples~/LaneRunner/Materials/LaneMarker.mat.meta +8 -0
  93. package/Samples~/LaneRunner/Materials/Obstacle.mat +84 -0
  94. package/Samples~/LaneRunner/Materials/Obstacle.mat.meta +8 -0
  95. package/Samples~/LaneRunner/Materials/Player.mat +84 -0
  96. package/Samples~/LaneRunner/Materials/Player.mat.meta +8 -0
  97. package/Samples~/LaneRunner/Materials.meta +8 -0
  98. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +152 -0
  99. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab.meta +7 -0
  100. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +152 -0
  101. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab.meta +7 -0
  102. package/Samples~/LaneRunner/Prefabs.meta +8 -0
  103. package/Samples~/LaneRunner/README.md +35 -0
  104. package/Samples~/LaneRunner/README.md.meta +7 -0
  105. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs +9 -0
  106. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs.meta +2 -0
  107. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs +93 -0
  108. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs.meta +2 -0
  109. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs +155 -0
  110. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs.meta +2 -0
  111. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +110 -0
  112. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs.meta +2 -0
  113. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs +41 -0
  114. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs.meta +2 -0
  115. package/Samples~/LaneRunner/Scripts/Gameplay.meta +8 -0
  116. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs +62 -0
  117. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs.meta +2 -0
  118. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +64 -0
  119. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs.meta +2 -0
  120. package/Samples~/LaneRunner/Scripts/Hyper.meta +8 -0
  121. package/Samples~/LaneRunner/Scripts.meta +8 -0
  122. package/Tests.meta +8 -0
  123. package/package.json +9 -3
  124. package/Runtime/Resources/Sprites/Control Bar/Top_Black_Rounded_Bar_Portrait_IV.png +0 -0
@@ -82,14 +82,20 @@ namespace Hyper.Internal.Managers
82
82
  {
83
83
  Destroy(_hyperCanvasObject);
84
84
  }
85
+
86
+ if (!ReplaySessionState.IsSeeking)
87
+ {
88
+ ReplayPresentationCanvas.DestroyCurrent();
89
+ }
85
90
  }
86
91
 
87
92
  #endregion
88
93
 
89
94
  #region Public API Implementation
90
95
 
91
- public bool IsBattleMode => Config.GameMode == GameMode.Battle;
92
- public bool IsPracticeMode => !IsBattleMode;
96
+ public bool IsBattleMode => Config.IsBattleGameplay;
97
+ public bool IsPracticeMode => Config.GameMode == GameMode.Practice;
98
+ public bool IsReplayMode => Config.GameMode == GameMode.Replay;
93
99
  public bool IsPaused => _isPaused;
94
100
  public bool HasGameStarted => _hasGameStarted;
95
101
  public bool HasGameEnded => _hasGameEnded;
@@ -114,6 +120,12 @@ namespace Hyper.Internal.Managers
114
120
  {
115
121
  if (!IsBattleMode) return;
116
122
 
123
+ if (IsReplayMode)
124
+ {
125
+ EndReplayGame();
126
+ return;
127
+ }
128
+
117
129
  var timeLeft = HyperRuntime.Timer.CurrentTime;
118
130
  if (timeLeft <= 0f) return;
119
131
 
@@ -124,6 +136,12 @@ namespace Hyper.Internal.Managers
124
136
  {
125
137
  if (IsBattleMode) return;
126
138
 
139
+ if (ReplaySessionState.IsActive)
140
+ {
141
+ EndReplayGame();
142
+ return;
143
+ }
144
+
127
145
  SetGameEnded();
128
146
  HyperRuntime.Timer.StopAllTimers();
129
147
 
@@ -158,6 +176,7 @@ namespace Hyper.Internal.Managers
158
176
 
159
177
  internal void PauseGame()
160
178
  {
179
+ if (IsReplayMode) return;
161
180
  if (!_hasGameStarted || _isPaused || _hasGameEnded) return;
162
181
 
163
182
  _isPaused = true;
@@ -190,11 +209,66 @@ namespace Hyper.Internal.Managers
190
209
 
191
210
  void OnApplicationFocus(bool hasFocus)
192
211
  {
212
+ if (IsReplayMode)
213
+ {
214
+ HandleReplayFocus(hasFocus);
215
+ return;
216
+ }
193
217
  #if !UNITY_EDITOR
194
218
  HandlePauseState(!hasFocus);
195
219
  #endif
196
220
  }
197
221
 
222
+ private void HandleReplayFocus(bool hasFocus)
223
+ {
224
+ if (!_hasGameStarted || _hasGameEnded) return;
225
+
226
+ if (!hasFocus && _replayFocusResumeCoroutine != null)
227
+ {
228
+ StopCoroutine(_replayFocusResumeCoroutine);
229
+ _replayFocusResumeCoroutine = null;
230
+ }
231
+
232
+ if (!hasFocus && !_replayFocusSuspended)
233
+ {
234
+ _replayFocusSuspended = true;
235
+ _savedReplayTimeScale = Time.timeScale;
236
+ Time.timeScale = 0f;
237
+ HyperRuntime.Timer.PauseGameplayTimers();
238
+
239
+ // WebGL may stop producing frames immediately after focus loss,
240
+ // so suspend replay timing here instead of waiting for Update.
241
+ _previousTimeScale = 0f;
242
+ _eventManager.InvokeTimeScaleFrozenEvent();
243
+ HyperDebug.Log("Time scale frozen event invoked");
244
+ }
245
+ else if (hasFocus && _replayFocusSuspended)
246
+ {
247
+ if (_replayFocusResumeCoroutine == null)
248
+ {
249
+ _replayFocusResumeCoroutine = StartCoroutine(ResumeReplayAfterFocus());
250
+ }
251
+ }
252
+ }
253
+
254
+ private IEnumerator ResumeReplayAfterFocus()
255
+ {
256
+ // WebGL reports focus before Unity refreshes its unscaled clock. Keep
257
+ // gameplay and replay frozen briefly while session time continues.
258
+ yield return new WaitForSecondsRealtime(1f);
259
+
260
+ _replayFocusResumeCoroutine = null;
261
+ if (!_replayFocusSuspended || _hasGameEnded) yield break;
262
+
263
+ _replayFocusSuspended = false;
264
+ Time.timeScale = _savedReplayTimeScale > 0f ? _savedReplayTimeScale : 1f;
265
+ HyperRuntime.Timer.ResumeGameTimersAfterPause();
266
+
267
+ _previousTimeScale = Time.timeScale;
268
+ _eventManager.InvokeTimeScaleResumedEvent();
269
+ HyperDebug.Log("Time scale resumed event invoked");
270
+ }
271
+
198
272
  void HandlePauseState(bool shouldPause)
199
273
  {
200
274
  if (shouldPause && !_isPaused)
@@ -214,7 +288,7 @@ namespace Hyper.Internal.Managers
214
288
  _eventManager.InvokeTimeScaleFrozenEvent();
215
289
  HyperDebug.Log("Time scale frozen event invoked");
216
290
 
217
- if (_backendManager.isSocketConnected)
291
+ if (!IsReplayMode && _backendManager.isSocketConnected)
218
292
  {
219
293
  _dataManager.StopCapturingScores();
220
294
  _backendManager.StopSendingScores();
@@ -227,13 +301,13 @@ namespace Hyper.Internal.Managers
227
301
 
228
302
  // Always restart capturing when game resumes, even if offline,
229
303
  // so scores accumulate for the offline batch and are flushed on reconnect.
230
- if (!_hasGameEnded && _hasGameStarted)
304
+ if (!IsReplayMode && !_hasGameEnded && _hasGameStarted)
231
305
  {
232
306
  _dataManager.StartCapturingScores();
233
307
  }
234
308
 
235
309
  // Only resume server session if connected
236
- if (_backendManager.isSocketConnected && !_hasGameEnded && _hasGameStarted)
310
+ if (!IsReplayMode && _backendManager.isSocketConnected && !_hasGameEnded && _hasGameStarted)
237
311
  {
238
312
  _backendManager.SendGameResumed();
239
313
  }
@@ -336,6 +410,7 @@ namespace Hyper.Internal.Managers
336
410
 
337
411
  internal void ShowConnectionLost()
338
412
  {
413
+ if (IsReplayMode) return;
339
414
  if (!IsInGameScene) return;
340
415
 
341
416
  EnsureConnectionStatusBar();
@@ -352,6 +427,7 @@ namespace Hyper.Internal.Managers
352
427
 
353
428
  internal void ShowConnectionRestored()
354
429
  {
430
+ if (IsReplayMode) return;
355
431
  if (!IsInGameScene) return;
356
432
 
357
433
  EnsureConnectionStatusBar();
@@ -374,7 +450,7 @@ namespace Hyper.Internal.Managers
374
450
  if (!_loadingFailedModal.activeInHierarchy)
375
451
  {
376
452
  _loadingFailedModal.SetActive(true);
377
- SetBlockerActive(true);
453
+ SetBlockerActive(true, 0.05f);
378
454
  }
379
455
  _eventManager.InvokeConnectionLostEvent();
380
456
  }
@@ -389,6 +465,59 @@ namespace Hyper.Internal.Managers
389
465
  }
390
466
  }
391
467
 
468
+ internal void RetryReplayLoading()
469
+ {
470
+ if (!IsReplayMode) return;
471
+
472
+ if (_replayConnectionCheckCoroutine != null)
473
+ {
474
+ return;
475
+ }
476
+
477
+ _replayConnectionCheckCoroutine = StartCoroutine(CheckReplayConnection());
478
+ }
479
+
480
+ private IEnumerator CheckReplayConnection()
481
+ {
482
+ const float timeoutSeconds = 5f;
483
+ float startedAt = Time.realtimeSinceStartup;
484
+ bool checkInProgress = false;
485
+ bool isConnected = false;
486
+
487
+ _loadingFailedModalComponent?.ShowRetryCountdown(Mathf.CeilToInt(timeoutSeconds));
488
+
489
+ while (Time.realtimeSinceStartup - startedAt < timeoutSeconds)
490
+ {
491
+ if (!isConnected && !checkInProgress)
492
+ {
493
+ checkInProgress = true;
494
+ StartCoroutine(HyperRuntime.BackendManager.CheckInternetConnection(connected =>
495
+ {
496
+ isConnected = connected;
497
+ checkInProgress = false;
498
+ }));
499
+ }
500
+
501
+ float remaining = timeoutSeconds - (Time.realtimeSinceStartup - startedAt);
502
+ _loadingFailedModalComponent?.ShowRetryCountdown(Mathf.CeilToInt(remaining));
503
+ yield return null;
504
+ }
505
+
506
+ _replayConnectionCheckCoroutine = null;
507
+
508
+ if (isConnected)
509
+ {
510
+ // This emits connection-found, allowing the existing loader to restart
511
+ // bootstrap and fetch/validate all replay data through its normal path.
512
+ ShowLoadingFailedModal(false);
513
+ yield break;
514
+ }
515
+
516
+ // The probe may finish early or reach the five-second deadline. In either
517
+ // case restore the modal CTA state instead of waiting at 00:00.
518
+ _loadingFailedModalComponent?.AllAttemptsHaveFailed();
519
+ }
520
+
392
521
  internal void ShowScoreSubmissionFailedModal()
393
522
  {
394
523
  if (IsPortrait())
@@ -484,6 +613,12 @@ namespace Hyper.Internal.Managers
484
613
 
485
614
  internal void ShowGameOverModal()
486
615
  {
616
+ if (IsReplayMode)
617
+ {
618
+ ShowReplayOverModal();
619
+ return;
620
+ }
621
+
487
622
  HidePauseModals();
488
623
 
489
624
  if (IsBattleMode)
@@ -585,12 +720,16 @@ namespace Hyper.Internal.Managers
585
720
  #region Private State
586
721
 
587
722
  private bool _hasGameStarted;
723
+ private bool _gameStartRequested;
588
724
  private bool _isPaused;
589
725
  private bool _hasGameEnded;
726
+ private bool _replayEndHandled;
590
727
  private bool _isFrozenBeforePause;
591
728
  private bool _isPortrait;
592
729
  private float _previousTimeScale = 1f;
593
730
  private float _savedTimeScaleBeforePause = 1f;
731
+ private bool _replayFocusSuspended;
732
+ private float _savedReplayTimeScale = 1f;
594
733
 
595
734
  #endregion
596
735
 
@@ -609,6 +748,9 @@ namespace Hyper.Internal.Managers
609
748
 
610
749
  [DllImport("__Internal")]
611
750
  private static extern void MarkGameLoadedOnHTML();
751
+
752
+ [DllImport("__Internal")]
753
+ private static extern void MarkGameStartedOnHTML();
612
754
  #endif
613
755
 
614
756
  private void HandleQueryParameters()
@@ -618,6 +760,8 @@ namespace Hyper.Internal.Managers
618
760
  if (queryParamsPtr == IntPtr.Zero)
619
761
  {
620
762
  HyperDebug.LogError("Failed to get query parameters");
763
+ Config = new URLData { GameMode = GameMode.Practice };
764
+ SyncReplayContext();
621
765
  return;
622
766
  }
623
767
 
@@ -637,23 +781,32 @@ namespace Hyper.Internal.Managers
637
781
  }
638
782
  #else
639
783
  Config = HyperRuntime.Settings._settings;
784
+ #if !UNITY_EDITOR
785
+ if (Config.GameMode == GameMode.Replay)
786
+ {
787
+ Config.GameMode = GameMode.Practice;
788
+ }
789
+ #endif
790
+ SyncReplayContext();
640
791
  #endif
641
792
  }
642
793
 
643
794
  private void ParseQueryParameters()
644
795
  {
645
- SetGameModeAndPVP();
796
+ SetGameMode();
646
797
  SetBackendGameStatus();
647
798
  SetOptionalParameters();
648
799
  SetSafeAreaBounds();
800
+ SyncReplayContext();
649
801
  }
650
802
 
651
- private void SetGameModeAndPVP()
803
+ private void SetGameMode()
652
804
  {
653
- bool isPVP = QueryParamsData.TryGetValue("pvp", out string pvpValue) && pvpValue == "true";
654
- Config.GameMode = isPVP ? GameMode.Battle : GameMode.Practice;
805
+ bool isReplay = IsTrueQueryFlag("replay");
806
+ bool isPVP = IsTrueQueryFlag("pvp");
807
+ Config.SetModeFromLaunchFlags(isPVP, isReplay);
655
808
 
656
- if (isPVP && QueryParamsData.TryGetValue("time", out string timeValue))
809
+ if (Config.IsBattleGameplay && QueryParamsData.TryGetValue("time", out string timeValue))
657
810
  {
658
811
  if (float.TryParse(timeValue, out float timeLimit))
659
812
  {
@@ -662,6 +815,35 @@ namespace Hyper.Internal.Managers
662
815
  }
663
816
  }
664
817
 
818
+ private bool IsTrueQueryFlag(string key)
819
+ {
820
+ return QueryParamsData.TryGetValue(key, out string value) &&
821
+ string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
822
+ }
823
+
824
+ private void SyncReplayContext()
825
+ {
826
+ if (IsReplayMode)
827
+ {
828
+ string entryId = Config.EntryId ?? string.Empty;
829
+ bool isNewReplay = !ReplaySessionState.IsActive ||
830
+ !string.Equals(ReplaySessionState.EntryId, entryId, StringComparison.Ordinal);
831
+
832
+ if (isNewReplay)
833
+ {
834
+ ReplaySessionState.Configure(
835
+ string.Empty,
836
+ string.Empty,
837
+ (int)Config.RandomSeed,
838
+ entryId);
839
+ }
840
+ }
841
+ else
842
+ {
843
+ ReplaySessionState.Clear();
844
+ }
845
+ }
846
+
665
847
  private void SetBackendGameStatus()
666
848
  {
667
849
  string[] requiredFields = { "tournamentId", "gameId", "entryId", "host", "embedUrl" };
@@ -680,6 +862,14 @@ namespace Hyper.Internal.Managers
680
862
 
681
863
  private void SetOptionalParameters()
682
864
  {
865
+ // Replay launches only require replay=true and entryId, so capture it
866
+ // independently of the full backend PvP launch contract.
867
+ if (QueryParamsData.TryGetValue("entryId", out string entryId) &&
868
+ !string.IsNullOrWhiteSpace(entryId))
869
+ {
870
+ Config.EntryId = entryId;
871
+ }
872
+
683
873
  if (QueryParamsData.TryGetValue("gameSlug", out string gameSlug))
684
874
  Config.GameSlug = gameSlug;
685
875
 
@@ -766,6 +956,7 @@ namespace Hyper.Internal.Managers
766
956
  {
767
957
  IsInGameScene = false;
768
958
  _hasGameStarted = false;
959
+ _gameStartRequested = false;
769
960
  _hasGameEnded = false;
770
961
  _isPaused = false;
771
962
  _isFrozenBeforePause = false;
@@ -790,7 +981,9 @@ namespace Hyper.Internal.Managers
790
981
  HideGameOverModal();
791
982
  IsInGameScene = true;
792
983
  _hasGameStarted = false;
984
+ _gameStartRequested = false;
793
985
  _hasGameEnded = false;
986
+ _replayEndHandled = false;
794
987
  _isPaused = false;
795
988
  _isFrozenBeforePause = false;
796
989
  _previousTimeScale = Time.timeScale; // Sync with actual time scale on scene load
@@ -798,7 +991,7 @@ namespace Hyper.Internal.Managers
798
991
 
799
992
  private void InitializeSceneUI()
800
993
  {
801
- if (Config.isPVP)
994
+ if (IsBattleMode)
802
995
  {
803
996
  HyperRuntime.Timer.StartPreSessionTimer();
804
997
  HyperRuntime.Timer.StartFiveSecondCountdown();
@@ -810,7 +1003,44 @@ namespace Hyper.Internal.Managers
810
1003
 
811
1004
  ShowControlBarElements();
812
1005
  AddTutorialModal();
813
- ShowTutorialAnnouncementOrStartingInstruction();
1006
+ if (IsReplayMode && ReplaySessionState.IsSeeking)
1007
+ {
1008
+ StartCoroutine(StartSeekingReplayWhenReady());
1009
+ }
1010
+ else
1011
+ {
1012
+ ShowTutorialAnnouncementOrStartingInstruction();
1013
+ }
1014
+ }
1015
+
1016
+ private IEnumerator StartSeekingReplayWhenReady()
1017
+ {
1018
+ // Allow scene scripts to subscribe to OnGameStart without presenting Tap to Start.
1019
+ yield return null;
1020
+ StartGameplay();
1021
+ }
1022
+
1023
+ internal void StartGameplay()
1024
+ {
1025
+ if (_gameStartRequested || _hasGameStarted || _hasGameEnded || !IsInGameScene) return;
1026
+ _gameStartRequested = true;
1027
+
1028
+ HideStartGameDeadlineSnackBar();
1029
+ _dataManager.UpdateScore(0);
1030
+ HyperRuntime.Timer.StartActiveGameTimer();
1031
+ _dataManager.StartCapturingScores();
1032
+ _backendManager.StartSendingScores();
1033
+
1034
+ if (_backendManager.isSocketConnected)
1035
+ {
1036
+ _backendManager.SendGameStarted();
1037
+ }
1038
+
1039
+ #if UNITY_WEBGL && !UNITY_EDITOR
1040
+ MarkGameStartedOnHTML();
1041
+ #endif
1042
+ _eventManager.InvokeGameStartEvent();
1043
+ HideStartingInstructionModal();
814
1044
  }
815
1045
 
816
1046
  private IEnumerator BootstrapAndStart()
@@ -841,6 +1071,7 @@ namespace Hyper.Internal.Managers
841
1071
 
842
1072
  // Reset game state flags
843
1073
  _hasGameStarted = false;
1074
+ _gameStartRequested = false;
844
1075
  _hasGameEnded = false;
845
1076
  _isPaused = false;
846
1077
  _isFrozenBeforePause = false;
@@ -859,6 +1090,12 @@ namespace Hyper.Internal.Managers
859
1090
 
860
1091
  private void EndGameInternal()
861
1092
  {
1093
+ if (IsReplayMode)
1094
+ {
1095
+ EndReplayGame();
1096
+ return;
1097
+ }
1098
+
862
1099
  SetGameEnded();
863
1100
 
864
1101
  _eventManager.InvokeGameEndedEvent();
@@ -878,6 +1115,57 @@ namespace Hyper.Internal.Managers
878
1115
  }
879
1116
  }
880
1117
 
1118
+ private void EndReplayGame()
1119
+ {
1120
+ if (ReplaySessionState.IsSeekRestartPending) return;
1121
+
1122
+ // Time Up marks _hasGameEnded before its two-second presentation finishes.
1123
+ // Track replay finalization separately so that delayed end still runs once.
1124
+ if (_replayEndHandled) return;
1125
+
1126
+ _replayEndHandled = true;
1127
+ _hasGameEnded = true;
1128
+ _replayFocusSuspended = false;
1129
+ Time.timeScale = 1f;
1130
+ _eventManager.InvokeGameEndedEvent();
1131
+ HyperRuntime.Timer.StopAllTimers();
1132
+ _dataManager.StopCapturingScores();
1133
+ HyperRuntime.BackendManager.StopSendingScores();
1134
+ ReplaySessionState.MarkEnded();
1135
+ _replayControllerComponent?.SetReplayEnded();
1136
+ ShowReplayOverModal();
1137
+ }
1138
+
1139
+ internal void ExitReplayEarly()
1140
+ {
1141
+ if (!IsReplayMode) return;
1142
+
1143
+ if (!_replayEndHandled)
1144
+ {
1145
+ _replayEndHandled = true;
1146
+ _hasGameEnded = true;
1147
+ _eventManager.InvokeGameEndedEvent();
1148
+ }
1149
+
1150
+ Time.timeScale = 1f;
1151
+ HyperRuntime.Timer.StopAllTimers();
1152
+ _dataManager.StopCapturingScores();
1153
+ HyperRuntime.BackendManager.StopSendingScores();
1154
+ _replayControllerComponent?.SetReplayEnded();
1155
+ ReplaySessionState.Clear();
1156
+ ReplayPresentationCanvas.DestroyCurrent();
1157
+ HyperRuntime.BackendManager.LogEvent(ConsoleEventType.PracticeGameEnd);
1158
+ }
1159
+
1160
+ internal void ExitCompletedReplay()
1161
+ {
1162
+ if (!IsReplayMode) return;
1163
+
1164
+ ReplaySessionState.Clear();
1165
+ ReplayPresentationCanvas.DestroyCurrent();
1166
+ HyperRuntime.BackendManager.LogEvent(ConsoleEventType.PracticeGameEnd);
1167
+ }
1168
+
881
1169
  internal void EndGame(string key)
882
1170
  {
883
1171
  if (key == "Hyper_Time_Up")
@@ -969,6 +1257,12 @@ namespace Hyper.Internal.Managers
969
1257
  private GameObject _blockerUI;
970
1258
  private Image _blockerUIImage;
971
1259
  private GameObject _safeArea;
1260
+ private GameObject _replayInputBlocker;
1261
+ private GameObject _replayControllerObject;
1262
+ private ReplayController _replayControllerComponent;
1263
+ private GameObject _replayOverModal;
1264
+ private Coroutine _replayConnectionCheckCoroutine;
1265
+ private Coroutine _replayFocusResumeCoroutine;
972
1266
 
973
1267
  private GameObject _connectionStatusBar;
974
1268
  private GameObject _startGameDeadlineStatusBar;
@@ -1085,7 +1379,7 @@ namespace Hyper.Internal.Managers
1085
1379
  #endif
1086
1380
  }
1087
1381
 
1088
- private void SetBlockerActive(bool isActive)
1382
+ private void SetBlockerActive(bool isActive, float opacity = 0.55f)
1089
1383
  {
1090
1384
  if (_blockerUI == null) return;
1091
1385
 
@@ -1093,7 +1387,7 @@ namespace Hyper.Internal.Managers
1093
1387
 
1094
1388
  if (isActive)
1095
1389
  {
1096
- _blockerUIImage.color = new Color(0f, 0f, 0f, 0.55f);
1390
+ _blockerUIImage.color = new Color(0f, 0f, 0f, Mathf.Clamp01(opacity));
1097
1391
  int lastActiveIndex = GetLastActiveChildIndex();
1098
1392
  _blockerUI.transform.SetSiblingIndex(lastActiveIndex - 1);
1099
1393
  }
@@ -1121,6 +1415,11 @@ namespace Hyper.Internal.Managers
1121
1415
  if (_controlBarElements == null) CreateControlBarElements();
1122
1416
 
1123
1417
  _controlBarElements.SetActive(true);
1418
+
1419
+ if (IsReplayMode)
1420
+ {
1421
+ EnsureReplayPresentation();
1422
+ }
1124
1423
  }
1125
1424
 
1126
1425
  private void HideTopBarElements()
@@ -1157,6 +1456,42 @@ namespace Hyper.Internal.Managers
1157
1456
  _controlBarModal = _controlBarElements.GetComponent<ControlBar>();
1158
1457
  }
1159
1458
 
1459
+ private void EnsureReplayPresentation()
1460
+ {
1461
+ ReplayPresentationCanvas presentation = ReplayPresentationCanvas.GetOrCreate(_isPortrait);
1462
+ _replayInputBlocker = presentation.InputBlocker;
1463
+ _replayControllerObject = presentation.ControllerObject;
1464
+ _replayControllerComponent = presentation.Controller;
1465
+ EventSystem.current?.SetSelectedGameObject(null);
1466
+ }
1467
+
1468
+ private void ShowReplayOverModal()
1469
+ {
1470
+ HidePauseModals();
1471
+
1472
+ if (_replayOverModal == null)
1473
+ {
1474
+ if (_canvas == null) CreateCanvas();
1475
+ GameObject prefab = Resources.Load<GameObject>("UIPrefabs/ReplayOverModal");
1476
+ if (prefab == null)
1477
+ {
1478
+ HyperDebug.LogWarning("[GameManager] ReplayOverModal prefab not found.");
1479
+ return;
1480
+ }
1481
+
1482
+ _replayOverModal = InstantiateModal(prefab);
1483
+ if (_replayOverModal.GetComponent<ReplayOverModal>() == null)
1484
+ {
1485
+ _replayOverModal.AddComponent<ReplayOverModal>();
1486
+ }
1487
+ }
1488
+
1489
+ _replayOverModal.SetActive(true);
1490
+ _replayOverModal.transform.SetAsLastSibling();
1491
+ SetBlockerActive(true);
1492
+ _replayOverModal.transform.SetAsLastSibling();
1493
+ }
1494
+
1160
1495
  private void EnsureConnectionStatusBar()
1161
1496
  {
1162
1497
  if (_connectionStatusBar != null) return;
@@ -0,0 +1,111 @@
1
+ using Hyper.Shared;
2
+
3
+ namespace Hyper.Internal.Managers
4
+ {
5
+ /// <summary>
6
+ /// Owns replay playback for the active SDK session. Games only subscribe to
7
+ /// typed activity callbacks; loading, timing, progress, suspension and ending
8
+ /// are handled here.
9
+ /// </summary>
10
+ internal sealed class ReplayManager
11
+ {
12
+ private IReplayPlaybackEngine _playback;
13
+ private EventManager _eventManager;
14
+ private bool _endingGame;
15
+
16
+ internal void Initialize(EventManager eventManager)
17
+ {
18
+ _eventManager = eventManager;
19
+ _eventManager.OnGameStart += HandleGameStart;
20
+ _eventManager.OnGameEnded += HandleGameEnded;
21
+ _eventManager.OnTimeScaleFrozen += HandleTimeScaleFrozen;
22
+ _eventManager.OnTimeScaleResumed += HandleTimeScaleResumed;
23
+ }
24
+
25
+ internal void AttachPlayback(IReplayPlaybackEngine playback)
26
+ {
27
+ if (ReferenceEquals(_playback, playback)) return;
28
+ DetachPlayback();
29
+ _playback = playback;
30
+ if (_playback == null) return;
31
+
32
+ _playback.OnGameEnded += HandlePlaybackEnded;
33
+ }
34
+
35
+ internal void FixedTick()
36
+ {
37
+ if (_playback == null || !_playback.IsPlaying) return;
38
+ _playback.Tick();
39
+ ReplaySessionState.SetProgress(_playback.Progress01);
40
+ }
41
+
42
+ internal void Cleanup()
43
+ {
44
+ if (_eventManager != null)
45
+ {
46
+ _eventManager.OnGameStart -= HandleGameStart;
47
+ _eventManager.OnGameEnded -= HandleGameEnded;
48
+ _eventManager.OnTimeScaleFrozen -= HandleTimeScaleFrozen;
49
+ _eventManager.OnTimeScaleResumed -= HandleTimeScaleResumed;
50
+ }
51
+
52
+ DetachPlayback();
53
+ _eventManager = null;
54
+ }
55
+
56
+ private void HandleGameStart()
57
+ {
58
+ if (HyperRuntime.GameManager == null || !HyperRuntime.GameManager.IsReplayMode ||
59
+ !ReplaySessionState.HasReplayData || _playback == null)
60
+ {
61
+ return;
62
+ }
63
+
64
+ if (ReplaySessionState.IsSeeking)
65
+ {
66
+ ReplaySessionState.NotifySeekPlaybackStarted();
67
+ UnityEngine.Time.timeScale = ReplaySessionState.PlaybackSpeed;
68
+ }
69
+
70
+ _playback.Begin(ReplaySessionState.ReplayNdjson);
71
+ }
72
+
73
+ private void HandleGameEnded()
74
+ {
75
+ if (_endingGame) return;
76
+ _playback?.Stop();
77
+ }
78
+
79
+ private void HandleTimeScaleFrozen()
80
+ {
81
+ if (HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode)
82
+ _playback?.Suspend();
83
+ }
84
+
85
+ private void HandleTimeScaleResumed()
86
+ {
87
+ if (HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode)
88
+ _playback?.Resume();
89
+ }
90
+
91
+ private void HandlePlaybackEnded()
92
+ {
93
+ ReplaySessionState.MarkEnded();
94
+
95
+ GameManager gameManager = HyperRuntime.GameManager;
96
+ if (gameManager == null || gameManager.HasGameEnded) return;
97
+
98
+ _endingGame = true;
99
+ gameManager.EndGame();
100
+ _endingGame = false;
101
+ }
102
+
103
+ private void DetachPlayback()
104
+ {
105
+ if (_playback == null) return;
106
+ _playback.Stop();
107
+ _playback.OnGameEnded -= HandlePlaybackEnded;
108
+ _playback = null;
109
+ }
110
+ }
111
+ }