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
@@ -16,13 +16,27 @@ namespace Hyper.Internal
16
16
  {
17
17
  StartCoroutine(AnimateLoadingText());
18
18
  }
19
+
20
+ internal void SetBaseText(string value)
21
+ {
22
+ if (string.IsNullOrWhiteSpace(value)) return;
23
+
24
+ loadingBaseText = value;
25
+ if (loadingText != null)
26
+ {
27
+ loadingText.text = loadingBaseText;
28
+ }
29
+ }
19
30
 
20
31
  private IEnumerator AnimateLoadingText()
21
32
  {
22
33
  int i = 0;
23
34
  while (true)
24
35
  {
25
- loadingText.text = loadingBaseText + loadingStates[i];
36
+ if (loadingText != null)
37
+ {
38
+ loadingText.text = loadingBaseText + loadingStates[i];
39
+ }
26
40
  i = (i + 1) % loadingStates.Length;
27
41
  yield return new WaitForSecondsRealtime(dotInterval);
28
42
  }
@@ -40,6 +40,8 @@ namespace Hyper.Internal
40
40
  private TextMeshProUGUI _timerTextComponent;
41
41
  private LivesDisplay _livesDisplay;
42
42
  private HealthBarDisplay _healthBarDisplay;
43
+ private GameObject _pauseButtonRoot;
44
+ private GameObject _replayMarker;
43
45
 
44
46
  // Layout constants
45
47
  private const float CENTERED_LAYOUT_SCORE_Y = 31f;
@@ -76,7 +78,9 @@ namespace Hyper.Internal
76
78
  ConfigureLayout();
77
79
  ConfigureTheme();
78
80
 
79
- connectionStatusIcon.onClick.AddListener(ShowConnectionLostStatus);
81
+ ConfigureReplayState();
82
+
83
+ connectionStatusIcon?.onClick.AddListener(ShowConnectionLostStatus);
80
84
  }
81
85
 
82
86
  private void ConfigureControlBarBackground()
@@ -93,7 +97,7 @@ namespace Hyper.Internal
93
97
 
94
98
  private void OnDestroy()
95
99
  {
96
- connectionStatusIcon.onClick.RemoveListener(ShowConnectionLostStatus);
100
+ connectionStatusIcon?.onClick.RemoveListener(ShowConnectionLostStatus);
97
101
  }
98
102
 
99
103
  private void CacheTextComponents()
@@ -314,10 +318,29 @@ namespace Hyper.Internal
314
318
 
315
319
  private void ShowConnectionLostStatus()
316
320
  {
321
+ if (HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode) return;
317
322
  // Show the connection lost status
318
323
  _gameManager.ShowConnectionLost();
319
324
  }
320
325
 
326
+ private void ConfigureReplayState()
327
+ {
328
+ Transform[] children = GetComponentsInChildren<Transform>(true);
329
+ for (int i = 0; i < children.Length; i++)
330
+ {
331
+ if (children[i].name == "PauseButton") _pauseButtonRoot = children[i].gameObject;
332
+ else if (children[i].name == "Replay Marker") _replayMarker = children[i].gameObject;
333
+ }
334
+
335
+ bool replay = HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode;
336
+ if (_pauseButtonRoot != null) _pauseButtonRoot.SetActive(!replay);
337
+ if (_replayMarker != null)
338
+ {
339
+ _replayMarker.SetActive(replay);
340
+ }
341
+ if (connectionStatusIcon != null) connectionStatusIcon.gameObject.SetActive(!replay);
342
+ }
343
+
321
344
  public void UpdateScoreLabel()
322
345
  {
323
346
  int currentScore = HyperRuntime.DataManager.CurrentScore;
@@ -2,6 +2,7 @@ using Hyper.Internal.Managers;
2
2
  using UnityEngine;
3
3
  using UnityEngine.EventSystems;
4
4
  using UnityEngine.UI;
5
+ using Hyper.Shared;
5
6
 
6
7
  namespace Hyper.Internal
7
8
  {
@@ -31,6 +32,7 @@ namespace Hyper.Internal
31
32
 
32
33
  public void OnPointerDown(PointerEventData eventData)
33
34
  {
35
+ if (HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode) return;
34
36
  if (!_isTypeRush)
35
37
  {
36
38
  _gameManager.PauseGame();
@@ -39,10 +41,11 @@ namespace Hyper.Internal
39
41
 
40
42
  public void OnPointerUp(PointerEventData eventData)
41
43
  {
44
+ if (HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode) return;
42
45
  if (_isTypeRush)
43
46
  {
44
47
  _gameManager.PauseGame();
45
48
  }
46
49
  }
47
50
  }
48
- }
51
+ }
@@ -0,0 +1,527 @@
1
+ using System;
2
+ using System.Collections;
3
+ using Hyper.Shared;
4
+ using TMPro;
5
+ using UnityEngine;
6
+ using UnityEngine.EventSystems;
7
+ using UnityEngine.UI;
8
+
9
+ namespace Hyper.Internal
10
+ {
11
+ internal sealed class ReplayController : MonoBehaviour
12
+ {
13
+ internal const int AvatarTextureSize = 128;
14
+
15
+ [SerializeField] private Slider progressSlider;
16
+ [SerializeField] private TextMeshProUGUI replayLabel;
17
+ [SerializeField] private RawImage playerAvatar;
18
+ [SerializeField] private Button exitButton;
19
+ [SerializeField] private Button playbackSpeedButton;
20
+ [SerializeField] private TextMeshProUGUI playbackSpeedLabel;
21
+ [SerializeField] private float[] playbackSpeeds = { 1.5f, 2f, 3f, 4f, 5f, 1f };
22
+ [SerializeField, Min(1f)] private float seekPlaybackSpeed = 30f;
23
+ [SerializeField, Range(0.25f, 1f)] private float seekSnapshotScale = 0.55f;
24
+ [SerializeField] private GameObject seekLoadingOverlay;
25
+ [SerializeField] private RawImage seekSnapshotImage;
26
+
27
+ private int playbackSpeedIndex;
28
+ private ReplaySeekInput seekInput;
29
+ private bool wasSeeking;
30
+ private bool isCapturingSeek;
31
+ private float pendingSeekTarget;
32
+ private Texture2D seekSnapshotTexture;
33
+ private Image seekOverlayBackground;
34
+ private Color seekOverlayBackgroundColor;
35
+
36
+ private void Awake()
37
+ {
38
+ AutoBindReferences();
39
+ ConfigureProgressSlider();
40
+ ConfigureSeekOverlay();
41
+ ConfigureSeekSnapshot();
42
+ ApplyInitialPlaybackSpeed();
43
+ ReplaySessionState.SeekCompleted += RestorePlaybackAfterSeek;
44
+ if (exitButton != null) exitButton.onClick.AddListener(ExitReplay);
45
+ if (playbackSpeedButton != null) playbackSpeedButton.onClick.AddListener(CyclePlaybackSpeed);
46
+ }
47
+
48
+ private void Start()
49
+ {
50
+ string playerName = ReplaySessionState.PlayerName ?? string.Empty;
51
+ playerName = playerName.TrimStart('@');
52
+ if (replayLabel != null) replayLabel.text = $"Replay of @{playerName}";
53
+ if (progressSlider != null)
54
+ {
55
+ float displayedProgress = ReplaySessionState.IsSeeking
56
+ ? ReplaySessionState.SeekTarget01
57
+ : ReplaySessionState.Progress01;
58
+ progressSlider.SetValueWithoutNotify(displayedProgress);
59
+ }
60
+ UpdatePlaybackSpeedLabel(ReplaySessionState.UserPlaybackSpeed);
61
+ SetSeekingPresentation(ReplaySessionState.IsSeeking);
62
+
63
+ if (playerAvatar != null && ReplaySessionState.AvatarTexture != null)
64
+ {
65
+ playerAvatar.texture = ReplaySessionState.AvatarTexture;
66
+ }
67
+ }
68
+
69
+ private void Update()
70
+ {
71
+ bool isSeeking = ReplaySessionState.IsSeeking;
72
+ if (wasSeeking && !isSeeking && !ReplaySessionState.HasEnded)
73
+ {
74
+ RestorePlaybackAfterSeek(ReplaySessionState.PlaybackSpeed);
75
+ }
76
+
77
+ if (wasSeeking != isSeeking)
78
+ {
79
+ SetSeekingPresentation(isSeeking);
80
+ }
81
+
82
+ if (progressSlider != null && (seekInput == null || !seekInput.IsDragging))
83
+ {
84
+ float displayedProgress = isCapturingSeek
85
+ ? pendingSeekTarget
86
+ : isSeeking
87
+ ? ReplaySessionState.SeekTarget01
88
+ : ReplaySessionState.Progress01;
89
+ progressSlider.SetValueWithoutNotify(displayedProgress);
90
+ }
91
+
92
+ wasSeeking = isSeeking;
93
+ }
94
+
95
+ private void OnDestroy()
96
+ {
97
+ if (exitButton != null) exitButton.onClick.RemoveListener(ExitReplay);
98
+ if (playbackSpeedButton != null) playbackSpeedButton.onClick.RemoveListener(CyclePlaybackSpeed);
99
+ ReplaySessionState.SeekCompleted -= RestorePlaybackAfterSeek;
100
+ seekInput?.Clear();
101
+ ClearSeekSnapshot();
102
+ }
103
+
104
+ private void ApplyInitialPlaybackSpeed()
105
+ {
106
+ if (!ReplaySessionState.IsActive || playbackSpeeds == null || playbackSpeeds.Length == 0)
107
+ {
108
+ return;
109
+ }
110
+
111
+ float speed = ReplaySessionState.IsSeeking
112
+ ? ReplaySessionState.UserPlaybackSpeed
113
+ : SanitizePlaybackSpeed(playbackSpeeds[0]);
114
+ if (!ReplaySessionState.IsSeeking)
115
+ {
116
+ ReplaySessionState.SetPlaybackSpeed(speed);
117
+ Time.timeScale = speed;
118
+ }
119
+
120
+ playbackSpeedIndex = playbackSpeeds.Length > 1 ? 1 : 0;
121
+ AlignPlaybackSpeedIndex(speed);
122
+ UpdatePlaybackSpeedLabel(speed);
123
+ }
124
+
125
+ internal void SetReplayEnded()
126
+ {
127
+ if (exitButton != null) exitButton.interactable = false;
128
+ if (playbackSpeedButton != null) playbackSpeedButton.interactable = false;
129
+ if (progressSlider != null) progressSlider.interactable = false;
130
+ if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(false);
131
+ ClearSeekSnapshot();
132
+ if (progressSlider != null) progressSlider.SetValueWithoutNotify(1f);
133
+ }
134
+
135
+ internal void ResetForReplayRestart()
136
+ {
137
+ float playbackSpeed = ReplaySessionState.UserPlaybackSpeed;
138
+ if (!ReplaySessionState.IsSeeking && playbackSpeeds != null && playbackSpeeds.Length > 0)
139
+ {
140
+ playbackSpeed = SanitizePlaybackSpeed(playbackSpeeds[0]);
141
+ ReplaySessionState.SetPlaybackSpeed(playbackSpeed);
142
+ Time.timeScale = playbackSpeed;
143
+ }
144
+
145
+ isCapturingSeek = false;
146
+ pendingSeekTarget = ReplaySessionState.IsSeeking
147
+ ? ReplaySessionState.SeekTarget01
148
+ : 0f;
149
+ wasSeeking = ReplaySessionState.IsSeeking;
150
+
151
+ if (progressSlider != null)
152
+ {
153
+ progressSlider.SetValueWithoutNotify(pendingSeekTarget);
154
+ }
155
+
156
+ UpdatePlaybackSpeedLabel(playbackSpeed);
157
+ AlignPlaybackSpeedIndex(playbackSpeed);
158
+ SetSeekingPresentation(ReplaySessionState.IsSeeking);
159
+ }
160
+
161
+ private void ExitReplay()
162
+ {
163
+ if (exitButton != null) exitButton.interactable = false;
164
+ HyperRuntime.GameManager?.ExitReplayEarly();
165
+ }
166
+
167
+ private void CyclePlaybackSpeed()
168
+ {
169
+ if (!ReplaySessionState.IsActive || ReplaySessionState.HasEnded ||
170
+ playbackSpeeds == null || playbackSpeeds.Length == 0)
171
+ {
172
+ return;
173
+ }
174
+
175
+ float speed = SanitizePlaybackSpeed(playbackSpeeds[playbackSpeedIndex]);
176
+ playbackSpeedIndex = (playbackSpeedIndex + 1) % playbackSpeeds.Length;
177
+
178
+ ReplaySessionState.SetPlaybackSpeed(speed);
179
+ Time.timeScale = speed;
180
+ UpdatePlaybackSpeedLabel(speed);
181
+ }
182
+
183
+ private void SeekTo(float targetProgress)
184
+ {
185
+ if (!ReplaySessionState.IsActive || ReplaySessionState.HasEnded ||
186
+ ReplaySessionState.IsSeeking || isCapturingSeek)
187
+ {
188
+ return;
189
+ }
190
+
191
+ float resumeSpeed = SanitizePlaybackSpeed(ReplaySessionState.UserPlaybackSpeed);
192
+ float fastForwardSpeed = SanitizePlaybackSpeed(seekPlaybackSpeed);
193
+ StartCoroutine(CaptureAndSeek(targetProgress, resumeSpeed, fastForwardSpeed));
194
+ }
195
+
196
+ private IEnumerator CaptureAndSeek(float targetProgress, float resumeSpeed, float fastForwardSpeed)
197
+ {
198
+ isCapturingSeek = true;
199
+ pendingSeekTarget = Mathf.Clamp01(targetProgress);
200
+
201
+ yield return new WaitForEndOfFrame();
202
+
203
+ Texture2D capturedFrame = null;
204
+ try
205
+ {
206
+ capturedFrame = ScreenCapture.CaptureScreenshotAsTexture();
207
+ if (capturedFrame != null)
208
+ {
209
+ seekSnapshotTexture = CreateScaledSnapshot(capturedFrame, seekSnapshotScale);
210
+ }
211
+ }
212
+ catch (Exception exception)
213
+ {
214
+ HyperDebug.LogWarning($"[Replay] Unable to capture seek snapshot: {exception.Message}");
215
+ }
216
+ finally
217
+ {
218
+ if (capturedFrame != null) Destroy(capturedFrame);
219
+ }
220
+
221
+ if (seekSnapshotImage != null && seekSnapshotTexture != null)
222
+ {
223
+ seekSnapshotImage.texture = seekSnapshotTexture;
224
+ seekSnapshotImage.gameObject.SetActive(true);
225
+ SetSeekOverlayBackgroundVisible(false);
226
+ }
227
+
228
+ try
229
+ {
230
+ SetSeekingPresentation(true);
231
+ HyperRuntime.SeekReplaySession(pendingSeekTarget, resumeSpeed, fastForwardSpeed);
232
+ }
233
+ finally
234
+ {
235
+ isCapturingSeek = false;
236
+ }
237
+ }
238
+
239
+ private void RestorePlaybackAfterSeek(float playbackSpeed)
240
+ {
241
+ float speed = SanitizePlaybackSpeed(playbackSpeed);
242
+ Time.timeScale = speed;
243
+ AlignPlaybackSpeedIndex(speed);
244
+ UpdatePlaybackSpeedLabel(speed);
245
+ SetSeekingPresentation(false);
246
+ wasSeeking = false;
247
+ }
248
+
249
+ private void AlignPlaybackSpeedIndex(float speed)
250
+ {
251
+ if (playbackSpeeds == null || playbackSpeeds.Length == 0) return;
252
+
253
+ for (int i = 0; i < playbackSpeeds.Length; i++)
254
+ {
255
+ if (!Mathf.Approximately(SanitizePlaybackSpeed(playbackSpeeds[i]), speed)) continue;
256
+ playbackSpeedIndex = (i + 1) % playbackSpeeds.Length;
257
+ return;
258
+ }
259
+ }
260
+
261
+ private void SetSeekingPresentation(bool isSeeking)
262
+ {
263
+ ReplayPresentationCanvas.SetSeekingLayer(isSeeking);
264
+ if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(isSeeking);
265
+ if (progressSlider != null) progressSlider.interactable = !isSeeking && !ReplaySessionState.HasEnded;
266
+ if (playbackSpeedButton != null)
267
+ {
268
+ playbackSpeedButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
269
+ }
270
+
271
+ if (exitButton != null) exitButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
272
+ if (!isSeeking) ClearSeekSnapshot();
273
+ }
274
+
275
+ private static float SanitizePlaybackSpeed(float speed)
276
+ {
277
+ return float.IsNaN(speed) || float.IsInfinity(speed) || speed <= 0f ? 1f : speed;
278
+ }
279
+
280
+ private void UpdatePlaybackSpeedLabel(float speed)
281
+ {
282
+ if (playbackSpeedLabel == null && playbackSpeedButton != null)
283
+ {
284
+ playbackSpeedLabel = playbackSpeedButton.GetComponentInChildren<TextMeshProUGUI>(true);
285
+ }
286
+
287
+ if (playbackSpeedLabel != null)
288
+ {
289
+ playbackSpeedLabel.text = $"{speed:0.#}x";
290
+ }
291
+ }
292
+
293
+ internal static Texture2D CreateCircularAvatar(Texture2D source, int size)
294
+ {
295
+ var result = new Texture2D(size, size, TextureFormat.RGBA32, false)
296
+ {
297
+ name = "Replay Avatar (Circular)",
298
+ filterMode = FilterMode.Bilinear,
299
+ wrapMode = TextureWrapMode.Clamp
300
+ };
301
+
302
+ Color[] pixels = new Color[size * size];
303
+ float sourceAspect = source.width / (float)source.height;
304
+ float edgeFeather = 2.5f / size;
305
+
306
+ for (int y = 0; y < size; y++)
307
+ {
308
+ float v = (y + 0.5f) / size;
309
+ for (int x = 0; x < size; x++)
310
+ {
311
+ float u = (x + 0.5f) / size;
312
+ float centeredX = u - 0.5f;
313
+ float centeredY = v - 0.5f;
314
+ float distanceFromCenter = Mathf.Sqrt(centeredX * centeredX + centeredY * centeredY);
315
+ float edgeCoverage = Mathf.Clamp01((0.5f - distanceFromCenter) / edgeFeather);
316
+ edgeCoverage = edgeCoverage * edgeCoverage * (3f - 2f * edgeCoverage);
317
+
318
+ if (edgeCoverage <= 0f)
319
+ {
320
+ pixels[y * size + x] = Color.clear;
321
+ continue;
322
+ }
323
+
324
+ float sampleU = sourceAspect > 1f
325
+ ? 0.5f + centeredX / sourceAspect
326
+ : u;
327
+ float sampleV = sourceAspect < 1f
328
+ ? 0.5f + centeredY * sourceAspect
329
+ : v;
330
+ Color color = source.GetPixelBilinear(sampleU, sampleV);
331
+ color.a *= edgeCoverage;
332
+ pixels[y * size + x] = color;
333
+ }
334
+ }
335
+
336
+ result.SetPixels(pixels);
337
+ result.Apply(false, true);
338
+ return result;
339
+ }
340
+
341
+ private void AutoBindReferences()
342
+ {
343
+ if (progressSlider == null) progressSlider = GetComponentInChildren<Slider>(true);
344
+ if (exitButton == null) exitButton = GetComponentInChildren<Button>(true);
345
+
346
+ TextMeshProUGUI[] labels = GetComponentsInChildren<TextMeshProUGUI>(true);
347
+ for (int i = 0; replayLabel == null && i < labels.Length; i++)
348
+ {
349
+ if (labels[i].name == "Subtext") replayLabel = labels[i];
350
+ }
351
+
352
+ RawImage[] images = GetComponentsInChildren<RawImage>(true);
353
+ for (int i = 0; playerAvatar == null && i < images.Length; i++)
354
+ {
355
+ if (images[i].name == "Player Avatar") playerAvatar = images[i];
356
+ }
357
+ }
358
+
359
+ private void ConfigureProgressSlider()
360
+ {
361
+ if (progressSlider == null) return;
362
+
363
+ progressSlider.minValue = 0f;
364
+ progressSlider.maxValue = 1f;
365
+ progressSlider.wholeNumbers = false;
366
+ progressSlider.interactable = true;
367
+ float displayedProgress = ReplaySessionState.IsSeeking
368
+ ? ReplaySessionState.SeekTarget01
369
+ : ReplaySessionState.Progress01;
370
+ progressSlider.SetValueWithoutNotify(displayedProgress);
371
+
372
+ seekInput = progressSlider.GetComponent<ReplaySeekInput>();
373
+ if (seekInput == null) seekInput = progressSlider.gameObject.AddComponent<ReplaySeekInput>();
374
+ seekInput.Initialize(progressSlider, SeekTo);
375
+ }
376
+
377
+ private void ConfigureSeekOverlay()
378
+ {
379
+ if (seekLoadingOverlay == null)
380
+ {
381
+ Transform existing = transform.Find("ReplaySeekOverlay");
382
+ if (existing != null) seekLoadingOverlay = existing.gameObject;
383
+ }
384
+
385
+ if (seekLoadingOverlay == null)
386
+ {
387
+ GameObject prefab = Resources.Load<GameObject>("UIPrefabs/ReplaySeekOverlay");
388
+ seekLoadingOverlay = Instantiate(prefab, transform, false);
389
+ }
390
+
391
+ RectTransform rect = seekLoadingOverlay.GetComponent<RectTransform>();
392
+ if (rect != null)
393
+ {
394
+ rect.anchorMin = Vector2.zero;
395
+ rect.anchorMax = Vector2.one;
396
+ rect.offsetMin = Vector2.zero;
397
+ rect.offsetMax = Vector2.zero;
398
+ }
399
+
400
+ seekLoadingOverlay.transform.SetAsFirstSibling();
401
+ seekOverlayBackground = seekLoadingOverlay.GetComponent<Image>();
402
+ if (seekOverlayBackground != null) seekOverlayBackgroundColor = seekOverlayBackground.color;
403
+ seekLoadingOverlay.SetActive(ReplaySessionState.IsSeeking);
404
+ wasSeeking = ReplaySessionState.IsSeeking;
405
+ }
406
+
407
+ private void ConfigureSeekSnapshot()
408
+ {
409
+ if (seekSnapshotImage == null)
410
+ {
411
+ Transform existing = transform.Find("ReplaySeekSnapshot");
412
+ if (existing != null) seekSnapshotImage = existing.GetComponent<RawImage>();
413
+ }
414
+
415
+ if (seekSnapshotImage == null)
416
+ {
417
+ var snapshot = new GameObject(
418
+ "ReplaySeekSnapshot",
419
+ typeof(RectTransform),
420
+ typeof(CanvasRenderer),
421
+ typeof(RawImage));
422
+ snapshot.transform.SetParent(transform, false);
423
+ seekSnapshotImage = snapshot.GetComponent<RawImage>();
424
+ }
425
+
426
+ RectTransform rect = seekSnapshotImage.rectTransform;
427
+ rect.anchorMin = Vector2.zero;
428
+ rect.anchorMax = Vector2.one;
429
+ rect.offsetMin = Vector2.zero;
430
+ rect.offsetMax = Vector2.zero;
431
+ seekSnapshotImage.raycastTarget = false;
432
+ seekSnapshotImage.transform.SetAsFirstSibling();
433
+ if (seekLoadingOverlay != null) seekLoadingOverlay.transform.SetSiblingIndex(1);
434
+ seekSnapshotImage.gameObject.SetActive(false);
435
+ }
436
+
437
+ private static Texture2D CreateScaledSnapshot(Texture2D source, float scale)
438
+ {
439
+ float sanitizedScale = Mathf.Clamp(scale, 0.25f, 1f);
440
+ int width = Mathf.Max(1, Mathf.RoundToInt(source.width * sanitizedScale));
441
+ int height = Mathf.Max(1, Mathf.RoundToInt(source.height * sanitizedScale));
442
+ RenderTexture temporary = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32);
443
+ RenderTexture previous = RenderTexture.active;
444
+ Texture2D result = null;
445
+
446
+ try
447
+ {
448
+ Graphics.Blit(source, temporary);
449
+ RenderTexture.active = temporary;
450
+ result = new Texture2D(width, height, TextureFormat.RGB24, false)
451
+ {
452
+ name = "Replay Seek Snapshot",
453
+ filterMode = FilterMode.Bilinear,
454
+ wrapMode = TextureWrapMode.Clamp
455
+ };
456
+ result.ReadPixels(new Rect(0f, 0f, width, height), 0, 0, false);
457
+ result.Apply(false, true);
458
+ return result;
459
+ }
460
+ catch
461
+ {
462
+ if (result != null) Destroy(result);
463
+ throw;
464
+ }
465
+ finally
466
+ {
467
+ RenderTexture.active = previous;
468
+ RenderTexture.ReleaseTemporary(temporary);
469
+ }
470
+ }
471
+
472
+ private void ClearSeekSnapshot()
473
+ {
474
+ SetSeekOverlayBackgroundVisible(true);
475
+ if (seekSnapshotImage != null)
476
+ {
477
+ seekSnapshotImage.texture = null;
478
+ seekSnapshotImage.gameObject.SetActive(false);
479
+ }
480
+
481
+ if (seekSnapshotTexture == null) return;
482
+ Destroy(seekSnapshotTexture);
483
+ seekSnapshotTexture = null;
484
+ }
485
+
486
+ private void SetSeekOverlayBackgroundVisible(bool isVisible)
487
+ {
488
+ if (seekOverlayBackground == null) return;
489
+ Color color = seekOverlayBackgroundColor;
490
+ if (!isVisible) color.a = 0.5f;
491
+ seekOverlayBackground.color = color;
492
+ }
493
+ }
494
+
495
+ internal sealed class ReplaySeekInput : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
496
+ {
497
+ private Slider slider;
498
+ private Action<float> onSeekRequested;
499
+
500
+ internal bool IsDragging { get; private set; }
501
+
502
+ internal void Initialize(Slider targetSlider, Action<float> callback)
503
+ {
504
+ slider = targetSlider;
505
+ onSeekRequested = callback;
506
+ }
507
+
508
+ internal void Clear()
509
+ {
510
+ IsDragging = false;
511
+ onSeekRequested = null;
512
+ }
513
+
514
+ public void OnPointerDown(PointerEventData eventData)
515
+ {
516
+ if (slider == null || !slider.interactable) return;
517
+ IsDragging = true;
518
+ }
519
+
520
+ public void OnPointerUp(PointerEventData eventData)
521
+ {
522
+ if (!IsDragging) return;
523
+ IsDragging = false;
524
+ onSeekRequested?.Invoke(slider.normalizedValue);
525
+ }
526
+ }
527
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 488ba0c343178684c9e74294492a1fa1