app.hypergames.hypersdk 1.10.0 → 1.11.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/Editor/Scripts/Setup/HyperSDKInstaller.cs +42 -0
- package/Editor/Scripts/Tools/HyperGameModeMenu.cs +27 -7
- package/Runtime/Internal/AssemblyInfo.cs +1 -0
- package/Runtime/Internal/Bootstrap/HyperLoader.cs +186 -8
- package/Runtime/Internal/Core/HyperRuntime.cs +82 -0
- package/Runtime/Internal/Core/HyperRuntimeHost.cs +6 -0
- package/Runtime/Internal/Data/URLData.cs +18 -1
- package/Runtime/Internal/Managers/BackendManager.cs +281 -24
- package/Runtime/Internal/Managers/DataManager.cs +207 -21
- package/Runtime/Internal/Managers/GameManager.cs +365 -17
- package/Runtime/Internal/Managers/ReplayManager.cs +117 -0
- package/Runtime/Internal/Managers/ReplayManager.cs.meta +11 -0
- package/Runtime/Internal/Managers/TimeManager.cs +54 -12
- package/Runtime/Internal/Replay/ReplayActivityRecorder.cs +182 -0
- package/Runtime/Internal/Replay/ReplayActivityRecorder.cs.meta +2 -0
- package/Runtime/Internal/Replay/ReplaySessionState.cs +181 -0
- package/Runtime/Internal/Replay/ReplaySessionState.cs.meta +2 -0
- package/Runtime/Internal/Replay.meta +8 -0
- package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +5 -0
- package/Runtime/Internal/UI/Animation/LoadingTextAnimation.cs +15 -1
- package/Runtime/Internal/UI/Components/ControlBar.cs +25 -2
- package/Runtime/Internal/UI/Components/PauseButton.cs +4 -1
- package/Runtime/Internal/UI/Components/ReplayController.cs +527 -0
- package/Runtime/Internal/UI/Components/ReplayController.cs.meta +2 -0
- package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs +89 -0
- package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs.meta +2 -0
- package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs +183 -0
- package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs.meta +2 -0
- package/Runtime/Internal/UI/Modals/LoadingFailedModal.cs +12 -0
- package/Runtime/Internal/UI/Modals/ReplayOverModal.cs +61 -0
- package/Runtime/Internal/UI/Modals/ReplayOverModal.cs.meta +2 -0
- package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +38 -29
- package/Runtime/Public/AssemblyInfo.cs +3 -0
- package/Runtime/Public/AssemblyInfo.cs.meta +2 -0
- package/Runtime/Public/Core/HyperSDK.cs +305 -109
- package/Runtime/Public/Hyper.asmdef +2 -1
- package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +555 -0
- package/Runtime/Public/Replay/ReplayPlaybackEngine.cs.meta +2 -0
- package/Runtime/Public/Replay.meta +8 -0
- package/Runtime/Resources/Fonts/Nunito/Nunito-Light SDF.asset +528 -521
- package/Runtime/Resources/Materials/HyperReplayScanlines.preset +340 -0
- package/Runtime/Resources/Materials/HyperReplayScanlines.preset.meta +8 -0
- package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat +55 -0
- package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat.meta +8 -0
- package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader +150 -0
- package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader.meta +10 -0
- package/Runtime/Resources/Sprites/Replays/Fallback avatar.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Fallback avatar.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays/Recording.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Recording.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays/Replay frame.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Replay frame.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays/Replay modal icon.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Replay modal icon.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays.meta +8 -0
- package/Runtime/Resources/UIPrefabs/ControlBar.prefab +169 -2
- package/Runtime/Resources/UIPrefabs/GamePausedModal_Battle.prefab +1 -1
- package/Runtime/Resources/UIPrefabs/ReplayController.prefab +1269 -0
- package/Runtime/Resources/UIPrefabs/ReplayController.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab +90 -0
- package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab +958 -0
- package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +189 -0
- package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/SnackBar.prefab +2 -2
- package/Runtime/Shared/AssemblyInfo.cs +5 -0
- package/Runtime/Shared/AssemblyInfo.cs.meta +11 -0
- package/Runtime/Shared/DTOs/Enums.cs +8 -3
- package/Runtime/Shared/DTOs/Score/ScoreSnapshot.cs +13 -1
- package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs +37 -0
- package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs.meta +11 -0
- package/Runtime/Shared/Replay.meta +8 -0
- package/Samples~/LaneRunner/LaneRunnerSample.unity +1603 -0
- package/Samples~/LaneRunner/LaneRunnerSample.unity.meta +7 -0
- package/Samples~/LaneRunner/Materials/DoubleScore.mat +86 -0
- package/Samples~/LaneRunner/Materials/DoubleScore.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Ground.mat +84 -0
- package/Samples~/LaneRunner/Materials/Ground.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Lane.mat +84 -0
- package/Samples~/LaneRunner/Materials/Lane.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/LaneMarker.mat +84 -0
- package/Samples~/LaneRunner/Materials/LaneMarker.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Obstacle.mat +84 -0
- package/Samples~/LaneRunner/Materials/Obstacle.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Player.mat +84 -0
- package/Samples~/LaneRunner/Materials/Player.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials.meta +8 -0
- package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +152 -0
- package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab.meta +7 -0
- package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +152 -0
- package/Samples~/LaneRunner/Prefabs/Obstacle.prefab.meta +7 -0
- package/Samples~/LaneRunner/Prefabs.meta +8 -0
- package/Samples~/LaneRunner/README.md +35 -0
- package/Samples~/LaneRunner/README.md.meta +7 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs +9 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs +93 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs +155 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +110 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs +41 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay.meta +8 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs +62 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +64 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Hyper.meta +8 -0
- package/Samples~/LaneRunner/Scripts.meta +8 -0
- package/Tests.meta +8 -0
- package/package.json +8 -1
|
@@ -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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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 =
|
|
1211
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
var snapshot = GetPooledScoreSnapshot();
|
|
257
|
-
var modifiers = GetPooledGameplayModifiers();
|
|
334
|
+
CaptureScoreSnapshot(currentGameSecond, _replayRecorder?.Drain());
|
|
335
|
+
}
|
|
258
336
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
337
|
+
yield return null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
262
340
|
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
347
|
+
newScore.cumulativeScore = CurrentScore;
|
|
348
|
+
newScore.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
|
349
|
+
newScore.isFinal = false;
|
|
269
350
|
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
359
|
+
snapshot.gameplayModifiers = modifiers;
|
|
360
|
+
newScore.scoreSnapshot = snapshot;
|
|
361
|
+
PendingScores.Add(newScore);
|
|
274
362
|
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
{
|