app.hypergames.hypersdk 1.5.10 → 1.7.0

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 (27) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/Editor/Scripts/Tools/HyperBuildAssistant.cs +4 -3
  3. package/Editor/Scripts/Windows/HyperGameContentWindow.cs +10 -0
  4. package/Editor/Scripts/Windows/HyperSettingsWindow.cs +10 -0
  5. package/HyperLoader.unity +3 -3
  6. package/Runtime/Internal/Bootstrap/HyperLoader.cs +57 -10
  7. package/Runtime/Internal/Bootstrap/HyperProductionLoggingBootstrap.cs +19 -0
  8. package/Runtime/Internal/Bootstrap/HyperProductionLoggingBootstrap.cs.meta +11 -0
  9. package/Runtime/Internal/Core/HyperRuntime.cs +16 -0
  10. package/Runtime/Internal/Managers/BackendManager.cs +110 -82
  11. package/Runtime/Internal/Managers/DataManager.cs +0 -11
  12. package/Runtime/Internal/Managers/EventManager.cs +1 -1
  13. package/Runtime/Internal/Managers/GameManager.cs +29 -23
  14. package/Runtime/Internal/Managers/TimeManager.cs +5 -0
  15. package/Runtime/Internal/ScriptableObjects/HyperGameContentConfig.cs +8 -0
  16. package/Runtime/Internal/UI/Modals/GamePausedModal.cs +1 -8
  17. package/Runtime/Plugins/WebGL/HyperCorePlugin.jslib +26 -0
  18. package/Runtime/Public/Core/HyperSDK.cs +191 -0
  19. package/Runtime/Public/PlayerDeviceClassification.cs +41 -0
  20. package/Runtime/Public/PlayerDeviceClassification.cs.meta +2 -0
  21. package/Runtime/Resources/HyperGameContentConfig.asset +1 -0
  22. package/Runtime/Resources/UIPrefabs/GamePausedModal_Battle.prefab +1 -1
  23. package/Runtime/Resources/UIPrefabs/ScoreSubmissionFailedModal_Landscape.prefab +16 -16
  24. package/Runtime/Resources/UIPrefabs/TimeUpModal.prefab +59 -14
  25. package/Runtime/Shared/DTOs/Enums.cs +1 -1
  26. package/Runtime/Shared/Utils/HyperDebug.cs +1 -1
  27. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # [1.7.0](https://github.com/mvmhyper/hyper-sdk/compare/v1.6.0...v1.7.0) (2026-04-02)
2
+
3
+
4
+ ### Features
5
+
6
+ * add loader API, device classification, and landscape canvas scaling fixes ([780ac39](https://github.com/mvmhyper/hyper-sdk/commit/780ac39b2d4f4c73286614a8e6b960fa947276f2))
7
+
8
+ # [1.6.0](https://github.com/mvmhyper/hyper-sdk/compare/v1.5.10...v1.6.0) (2026-03-28)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * avoid auto game resume on reconnect while paused ([ab52983](https://github.com/mvmhyper/hyper-sdk/commit/ab52983215218c658a17a022adda5efe1185bfa1))
14
+ * forward SDK events to FE, fix overlay coverage, disable release logs, and improve editor focus behavior ([1c424ad](https://github.com/mvmhyper/hyper-sdk/commit/1c424adb8dd74ee5f1bfe20896a21547fa3a0b11))
15
+ * send final score as score array payload ([7c06086](https://github.com/mvmhyper/hyper-sdk/commit/7c060869f971aa0a39f07ebc790d7da56462d1d9))
16
+
17
+
18
+ ### Features
19
+
20
+ * batch score submission with offline backlog support ([b372643](https://github.com/mvmhyper/hyper-sdk/commit/b3726438239523119d7c13ba3b5f738dcd3b1381))
21
+
1
22
  ## [1.5.10](https://github.com/mvmhyper/hyper-sdk/compare/v1.5.9...v1.5.10) (2026-03-25)
2
23
 
3
24
 
@@ -1166,7 +1166,7 @@ namespace Hyper.Editor
1166
1166
 
1167
1167
  EditorGUILayout.BeginVertical(infoBoxStyle);
1168
1168
  {
1169
- EditorGUILayout.LabelField("Fast builds with Brotli compression for testing. Uses shorter build time, debug symbols, and full exceptions.", EditorStyles.wordWrappedMiniLabel);
1169
+ EditorGUILayout.LabelField("Fast builds with Brotli compression for testing. Uses shorter build time, debug symbols, full exceptions, and HYPER_DEVELOPMENT_BUILD so HyperSDK and Unity Debug.Log output stay available.", EditorStyles.wordWrappedMiniLabel);
1170
1170
  EditorGUILayout.Space(8);
1171
1171
 
1172
1172
  if (GUILayout.Button("Build Development", buttonStyle, GUILayout.Height(40)))
@@ -1198,7 +1198,7 @@ namespace Hyper.Editor
1198
1198
 
1199
1199
  EditorGUILayout.BeginVertical(infoBoxStyle);
1200
1200
  {
1201
- EditorGUILayout.LabelField("Optimized production build that respects your Code Optimization setting and enforces LTO. Build time may exceed 20 minutes.", EditorStyles.wordWrappedMiniLabel);
1201
+ EditorGUILayout.LabelField("Optimized production build that respects your Code Optimization setting and enforces LTO. Sets HYPER_RELEASE_BUILD so Unity Debug.Log / LogWarning / LogError are disabled in the player. Build time may exceed 20 minutes.", EditorStyles.wordWrappedMiniLabel);
1202
1202
  EditorGUILayout.Space(8);
1203
1203
 
1204
1204
  if (GUILayout.Button("Build Release", buttonStyle, GUILayout.Height(40)))
@@ -1944,6 +1944,7 @@ namespace Hyper.Editor
1944
1944
  ForceAssetRepack();
1945
1945
 
1946
1946
  SetReleaseBuildDefineSymbol(true);
1947
+ AddOptimizationSummary("Runtime: Unity debug logger disabled in player (HYPER_RELEASE_BUILD)");
1947
1948
  ApplyReleaseBuildSettings();
1948
1949
 
1949
1950
  BuildPlayerOptions buildOptions = new BuildPlayerOptions
@@ -2216,7 +2217,7 @@ namespace Hyper.Editor
2216
2217
  if (!hasReleaseDefine)
2217
2218
  {
2218
2219
  defines = string.IsNullOrEmpty(defines) ? RELEASE_DEFINE : $"{defines};{RELEASE_DEFINE}";
2219
- if (IsSDKDeveloper()) HyperDebug.LogSuccess("Set HYPER_RELEASE_BUILD define symbol for production API URL");
2220
+ if (IsSDKDeveloper()) HyperDebug.LogSuccess("Set HYPER_RELEASE_BUILD (production API URL; Unity debug logger off in player)");
2220
2221
  }
2221
2222
 
2222
2223
  // Remove HYPER_DEVELOPMENT_BUILD if present
@@ -126,6 +126,16 @@ namespace Hyper.Editor
126
126
  "• TRANSITION → optional loading/transition scene.",
127
127
  MessageType.Info);
128
128
 
129
+ if (_contentConfig.EnableCustomLoading)
130
+ {
131
+ EditorGUILayout.Space(4);
132
+ EditorGUILayout.HelpBox(
133
+ "Custom Pre-Loading is enabled. The HyperLoader bootstrap will pause after SDK " +
134
+ "initialization and wait for HyperSDK.Loader.CompleteCustomLoading() to be called " +
135
+ "before loading the game scene.",
136
+ MessageType.Info);
137
+ }
138
+
129
139
  EditorGUILayout.Space(10);
130
140
 
131
141
  _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
@@ -253,6 +253,16 @@ namespace Hyper.Editor
253
253
  "• TRANSITION → optional loading/transition scene.",
254
254
  MessageType.Info);
255
255
 
256
+ if (_contentConfig.EnableCustomLoading)
257
+ {
258
+ EditorGUILayout.Space(4);
259
+ EditorGUILayout.HelpBox(
260
+ "Custom Pre-Loading is enabled. The HyperLoader bootstrap will pause after SDK " +
261
+ "initialization and wait for HyperSDK.Loader.CompleteCustomLoading() to be called " +
262
+ "before loading the game scene.",
263
+ MessageType.Info);
264
+ }
265
+
256
266
  EditorGUILayout.Space(8);
257
267
 
258
268
  _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
package/HyperLoader.unity CHANGED
@@ -304,7 +304,7 @@ RectTransform:
304
304
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
305
305
  m_AnchorMin: {x: 0, y: 1}
306
306
  m_AnchorMax: {x: 0, y: 1}
307
- m_AnchoredPosition: {x: 489.20746, y: -280.1698}
307
+ m_AnchoredPosition: {x: 489.45853, y: -280.1698}
308
308
  m_SizeDelta: {x: 1044.9, y: 239.347}
309
309
  m_Pivot: {x: 0.5, y: 0.5}
310
310
  --- !u!114 &751153944
@@ -840,7 +840,7 @@ GameObject:
840
840
  m_Component:
841
841
  - component: {fileID: 1064166296}
842
842
  m_Layer: 0
843
- m_Name: __HyperSDK_BuildMarker_639075716235347695
843
+ m_Name: __HyperSDK_BuildMarker_639104165398939431
844
844
  m_TagString: Untagged
845
845
  m_Icon: {fileID: 0}
846
846
  m_NavMeshLayer: 0
@@ -1092,7 +1092,7 @@ RectTransform:
1092
1092
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
1093
1093
  m_AnchorMin: {x: 0, y: 1}
1094
1094
  m_AnchorMax: {x: 0, y: 1}
1095
- m_AnchoredPosition: {x: 489.20746, y: -133.2274}
1095
+ m_AnchoredPosition: {x: 489.45853, y: -133.2274}
1096
1096
  m_SizeDelta: {x: 875, y: 54.5378}
1097
1097
  m_Pivot: {x: 0.5, y: 0.5}
1098
1098
  --- !u!1 &2874543288936963705
@@ -42,6 +42,10 @@ namespace Hyper.Internal
42
42
  private GameManager _gameManager;
43
43
  private BackendManager _backendManager;
44
44
 
45
+ // Progress budget milestones (adjusted based on custom pre-loading)
46
+ private float _progressAfterInit = 0.20f;
47
+ private float _progressAfterBackend = 0.50f;
48
+ private float _progressAfterCustomLoad = 0.50f;
45
49
 
46
50
  #endregion
47
51
 
@@ -231,6 +235,12 @@ namespace Hyper.Internal
231
235
  _bootstrapProgress = 0f;
232
236
  _loadingBarSlider.value = _bootstrapProgress;
233
237
 
238
+ // Configure progress budget based on whether custom pre-loading is enabled
239
+ bool hasCustomPreLoading = _gameContentConfig != null && _gameContentConfig.EnableCustomLoading;
240
+ _progressAfterInit = 0.20f;
241
+ _progressAfterBackend = hasCustomPreLoading ? 0.45f : 0.50f;
242
+ _progressAfterCustomLoad = hasCustomPreLoading ? 0.75f : _progressAfterBackend;
243
+
234
244
  // Step 1: Initialize HyperSDK
235
245
  bool sdkInitialized = false;
236
246
  HyperRuntime.Initialize(() =>
@@ -245,12 +255,12 @@ namespace Hyper.Internal
245
255
 
246
256
  while (!sdkInitialized)
247
257
  {
248
- _bootstrapProgress = Mathf.MoveTowards(_bootstrapProgress, 0.20f, Time.deltaTime * 2f);
258
+ _bootstrapProgress = Mathf.MoveTowards(_bootstrapProgress, _progressAfterInit, Time.deltaTime * 2f);
249
259
  _loadingBarSlider.value = _bootstrapProgress;
250
260
  yield return null;
251
261
  }
252
262
 
253
- _bootstrapProgress = 0.20f;
263
+ _bootstrapProgress = _progressAfterInit;
254
264
  _loadingBarSlider.value = _bootstrapProgress;
255
265
 
256
266
  // Step 2: Handle Backend/PVP or Practice mode initialization
@@ -267,6 +277,12 @@ namespace Hyper.Internal
267
277
  InitializePracticeMode();
268
278
  }
269
279
 
280
+ // Step 2.5: Custom Pre-Loading (if enabled)
281
+ if (hasCustomPreLoading)
282
+ {
283
+ yield return WaitForCustomPreLoading();
284
+ }
285
+
270
286
  // Step 3: Load target scene asynchronously
271
287
  yield return LoadTargetScene();
272
288
  }
@@ -275,17 +291,20 @@ namespace Hyper.Internal
275
291
  {
276
292
  _targetSceneName = _gameContentConfig.GameScene?.SceneName;
277
293
 
294
+ // WebSocket milestone is midpoint between init and backend completion
295
+ float webSocketMilestone = Mathf.Lerp(_progressAfterInit, _progressAfterBackend, 0.5f);
296
+
278
297
  // Initialize WebSocket
279
298
  HyperRuntime.BackendManager.InitializeWebSocket();
280
299
 
281
300
  while (!HyperRuntime.BackendManager.isSocketConnected)
282
301
  {
283
- _bootstrapProgress = Mathf.MoveTowards(_bootstrapProgress, 0.35f, Time.deltaTime * 0.5f);
302
+ _bootstrapProgress = Mathf.MoveTowards(_bootstrapProgress, webSocketMilestone, Time.deltaTime * 0.5f);
284
303
  _loadingBarSlider.value = _bootstrapProgress;
285
304
  yield return null;
286
305
  }
287
306
 
288
- _bootstrapProgress = 0.35f;
307
+ _bootstrapProgress = webSocketMilestone;
289
308
  _loadingBarSlider.value = _bootstrapProgress;
290
309
 
291
310
  // Create session
@@ -297,12 +316,12 @@ namespace Hyper.Internal
297
316
 
298
317
  while (!sessionCreated)
299
318
  {
300
- _bootstrapProgress = Mathf.MoveTowards(_bootstrapProgress, 0.50f, Time.deltaTime * 0.5f);
319
+ _bootstrapProgress = Mathf.MoveTowards(_bootstrapProgress, _progressAfterBackend, Time.deltaTime * 0.5f);
301
320
  _loadingBarSlider.value = _bootstrapProgress;
302
321
  yield return null;
303
322
  }
304
323
 
305
- _bootstrapProgress = 0.50f;
324
+ _bootstrapProgress = _progressAfterBackend;
306
325
  _loadingBarSlider.value = _bootstrapProgress;
307
326
  }
308
327
 
@@ -322,6 +341,38 @@ namespace Hyper.Internal
322
341
  : _gameContentConfig.GameScene?.SceneName;
323
342
  }
324
343
 
344
+ private IEnumerator WaitForCustomPreLoading()
345
+ {
346
+ HyperRuntime.CustomLoadingComplete = false;
347
+ HyperRuntime.CustomLoadingProgress = 0f;
348
+
349
+ float simulatedProgress = 0f;
350
+ const float simulatedSpeed = 0.15f;
351
+ const float simulatedCap = 0.70f;
352
+
353
+ HyperDebug.Log("Custom pre-loading enabled. Waiting for HyperSDK.Loader.CompleteCustomLoading()");
354
+
355
+ while (!HyperRuntime.CustomLoadingComplete)
356
+ {
357
+ float devProgress = HyperRuntime.CustomLoadingProgress;
358
+
359
+ // Simulate progress if dev isn't reporting (auto-animate so bar doesn't look stuck)
360
+ simulatedProgress = Mathf.MoveTowards(simulatedProgress, simulatedCap, Time.unscaledDeltaTime * simulatedSpeed);
361
+
362
+ // Use whichever is higher: dev-reported or simulated
363
+ float effectiveProgress = Mathf.Max(devProgress, simulatedProgress);
364
+
365
+ _bootstrapProgress = Mathf.Lerp(_progressAfterBackend, _progressAfterCustomLoad, effectiveProgress);
366
+ _loadingBarSlider.value = _bootstrapProgress;
367
+
368
+ yield return null;
369
+ }
370
+
371
+ // Snap to end of custom loading range
372
+ _bootstrapProgress = _progressAfterCustomLoad;
373
+ _loadingBarSlider.value = _bootstrapProgress;
374
+ }
375
+
325
376
  private long GetRandomSeed()
326
377
  {
327
378
  #if UNITY_EDITOR
@@ -424,9 +475,5 @@ namespace Hyper.Internal
424
475
  }
425
476
 
426
477
  #endregion
427
-
428
- #region Utility Methods
429
-
430
- #endregion
431
478
  }
432
479
  }
@@ -0,0 +1,19 @@
1
+ using UnityEngine;
2
+
3
+ namespace Hyper.Internal
4
+ {
5
+ /// <summary>
6
+ /// Disables Unity's debug logger in production WebGL players built with HYPER_RELEASE_BUILD
7
+ /// (set by Build Assistant release builds). Development builds keep the default logger enabled.
8
+ /// </summary>
9
+ internal static class HyperProductionLoggingBootstrap
10
+ {
11
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
12
+ private static void Apply()
13
+ {
14
+ #if !UNITY_EDITOR && HYPER_RELEASE_BUILD
15
+ Debug.unityLogger.logEnabled = false;
16
+ #endif
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: b8c3f21e4d6a409c9e7f2b1d8a4c5e6f
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -19,6 +19,8 @@ namespace Hyper.Internal
19
19
 
20
20
  private static bool _isInitialized;
21
21
  private static bool _isSoftRestarting;
22
+ private static bool _customLoadingComplete;
23
+ private static float _customLoadingProgress;
22
24
  private static HyperSDKSettings _settings;
23
25
  private static HyperGameContentConfig _gameContent;
24
26
  private static GameManager _gameManager;
@@ -33,6 +35,16 @@ namespace Hyper.Internal
33
35
  get => _isSoftRestarting;
34
36
  set => _isSoftRestarting = value;
35
37
  }
38
+ internal static bool CustomLoadingComplete
39
+ {
40
+ get => _customLoadingComplete;
41
+ set => _customLoadingComplete = value;
42
+ }
43
+ internal static float CustomLoadingProgress
44
+ {
45
+ get => _customLoadingProgress;
46
+ set => _customLoadingProgress = value;
47
+ }
36
48
  internal static HyperSDKSettings Settings => _settings;
37
49
  internal static HyperGameContentConfig GameContent => _gameContent;
38
50
  internal static GameManager GameManager => _gameManager;
@@ -287,6 +299,8 @@ namespace Hyper.Internal
287
299
 
288
300
  HyperRandom.Clear();
289
301
  _gameContent = null;
302
+ _customLoadingComplete = false;
303
+ _customLoadingProgress = 0f;
290
304
 
291
305
  if (_singletonObject != null)
292
306
  {
@@ -335,6 +349,8 @@ namespace Hyper.Internal
335
349
  _dataManager = null;
336
350
  _gameContent = null;
337
351
  _isInitialized = false;
352
+ _customLoadingComplete = false;
353
+ _customLoadingProgress = 0f;
338
354
  }
339
355
  }
340
356
  }
@@ -498,7 +498,11 @@ namespace Hyper.Internal.Managers
498
498
  if (_gameManager.HasGameStarted)
499
499
  {
500
500
  _gameManager?.ShowConnectionRestored();
501
- SendGameResumed();
501
+
502
+ if (!_gameManager.IsPaused)
503
+ {
504
+ SendGameResumed();
505
+ }
502
506
  }
503
507
  else
504
508
  {
@@ -723,10 +727,6 @@ namespace Hyper.Internal.Managers
723
727
  LastSubmissionError = SubmissionErrorType.None;
724
728
 
725
729
  // Keep connection alive during game end sequence
726
- // WebSocket connections can be closed by proxies/firewalls if idle for too long (typically 30-60 seconds)
727
- // During gameplay, scores are sent every second, keeping the connection active
728
- // But at game end, we stop sending scores, so the connection might go idle
729
- // We ensure the connection is active right before submission by checking and reconnecting if needed
730
730
  if (!isSocketConnected)
731
731
  {
732
732
  HyperDebug.LogWarning("WebSocket disconnected at game end - attempting to reconnect before submission");
@@ -748,8 +748,7 @@ namespace Hyper.Internal.Managers
748
748
  }
749
749
  else
750
750
  {
751
- // Connection is active - ensure it stays active by verifying it's still connected
752
- // The act of checking and the upcoming gameResumed call will keep it active
751
+
753
752
  HyperDebug.Log("Connection active - maintaining during game end sequence");
754
753
  }
755
754
 
@@ -777,7 +776,10 @@ namespace Hyper.Internal.Managers
777
776
  var finalScoreData = _dataManager?.CurrentScoreData;
778
777
  finalScoreData.isFinal = true;
779
778
 
780
- string rawScoreJson = JsonConvert.SerializeObject(finalScoreData);
779
+ // For consistency with per-second and backlog submissions, always send
780
+ // the final score as an array of ScoreData.
781
+ var finalScoreList = new List<ScoreData>(1) { finalScoreData };
782
+ string rawScoreJson = JsonConvert.SerializeObject(finalScoreList);
781
783
  var compressedScoreData = JsonCompressor.CompressJson(rawScoreJson);
782
784
  string scoreJson = compressedScoreData.MinifiedJson;
783
785
  string encryptedScoreData = EncryptScore(scoreJson);
@@ -835,6 +837,18 @@ namespace Hyper.Internal.Managers
835
837
  continue;
836
838
  }
837
839
 
840
+ // Flush any offline backlog that accumulated while disconnected.
841
+ if (_dataManager?.PendingScores != null && _dataManager.PendingScores.Count > 0)
842
+ {
843
+ var backlog = new List<ScoreData>(_dataManager.PendingScores);
844
+ _dataManager.PendingScores.Clear();
845
+ int backlogCount = backlog.Count;
846
+ HyperDebug.Log($"[Score] Flushing {backlogCount} offline backlog score(s) before final submission.");
847
+ SendScoresOverWebSocket(backlog);
848
+
849
+ yield return new WaitForSecondsRealtime(1f);
850
+ }
851
+
838
852
  // Sending final score via WebSocket
839
853
  _finalScoreSubmitted = false;
840
854
  _isWaitingForFinalScoreResponse = true;
@@ -986,15 +1000,30 @@ namespace Hyper.Internal.Managers
986
1000
  {
987
1001
  while (true)
988
1002
  {
1003
+ // Don't drain PendingScores while offline - let them accumulate as an offline batch.
1004
+ // Scores will be flushed when the connection is restored.
1005
+ if (!isSocketConnected)
1006
+ {
1007
+ int pendingCount = _dataManager?.PendingScores?.Count ?? 0;
1008
+ HyperDebug.Log($"[Score] Socket disconnected - accumulating scores for offline batch. PendingCount={pendingCount}");
1009
+ yield return _scoreSendInterval;
1010
+ continue;
1011
+ }
1012
+
989
1013
  var scoresToSend = _scoreSendBuffer;
990
1014
  scoresToSend.Clear();
991
1015
 
992
1016
  if (_dataManager?.PendingScores.Count > 0)
993
1017
  {
994
- // Copy pending scores into our reusable buffer, then clear the source list
1018
+ // Copy pending scores into our reusable buffer, then clear the source list.
1019
+ // This may be 1 score (normal) or many (offline backlog after reconnection).
995
1020
  scoresToSend.AddRange(_dataManager?.PendingScores);
996
1021
  _dataManager?.PendingScores.Clear();
997
- HyperDebug.Log($"[Score] Preparing to send {scoresToSend.Count} score sample(s) over WebSocket. LastPendingCumulativeScore={scoresToSend[scoresToSend.Count - 1].cumulativeScore}");
1022
+ int count = scoresToSend.Count;
1023
+ HyperDebug.Log($"[Score] Preparing to send {count} score sample(s) over WebSocket. LastPendingCumulativeScore={scoresToSend[count - 1].cumulativeScore}");
1024
+
1025
+ // Send the entire batch as an array payload.
1026
+ // Normal ticks send [1 score], reconnection flushes send [N scores].
998
1027
  SendScoresOverWebSocket(scoresToSend);
999
1028
  }
1000
1029
  else
@@ -1021,77 +1050,36 @@ namespace Hyper.Internal.Managers
1021
1050
 
1022
1051
  try
1023
1052
  {
1024
- // Clone the last batch into standalone ScoreData instances
1053
+ // Clone the batch into standalone ScoreData instances
1025
1054
  // so that pooling/reset in DataManager does not zero out the values we log/use here.
1026
1055
  _lastSentBatch.Clear();
1027
1056
 
1028
- ScoreData scoreData = null;
1029
- if (scores.Count > 0)
1057
+ for (int i = 0; i < scores.Count; i++)
1030
1058
  {
1031
- // We only need the last score point for submission, but we keep the whole batch
1032
- // for events so listeners can inspect the last acknowledged score accurately.
1033
- for (int i = 0; i < scores.Count; i++)
1034
- {
1035
- var source = scores[i];
1036
- if (source == null) continue;
1037
-
1038
- var cloned = new ScoreData
1039
- {
1040
- cumulativeScore = source.cumulativeScore,
1041
- isFinal = source.isFinal,
1042
- timestampUtc = source.timestampUtc
1043
- };
1044
-
1045
- if (source.scoreSnapshot != null)
1046
- {
1047
- var snapshot = source.scoreSnapshot;
1048
- var clonedSnapshot = new ScoreSnapshot
1049
- {
1050
- scoreTime = snapshot.scoreTime,
1051
- scoreDelta = snapshot.scoreDelta
1052
- };
1053
-
1054
- if (snapshot.gameplayModifiers != null)
1055
- {
1056
- var mods = snapshot.gameplayModifiers;
1057
- var clonedMods = new GameplayModifiers
1058
- {
1059
- comboModifiers = mods.comboModifiers,
1060
- globalModifiers = mods.globalModifiers,
1061
- activePowerUps = mods.activePowerUps != null
1062
- ? new List<string>(mods.activePowerUps)
1063
- : null
1064
- };
1065
-
1066
- clonedSnapshot.gameplayModifiers = clonedMods;
1067
- }
1068
-
1069
- cloned.scoreSnapshot = clonedSnapshot;
1070
- }
1071
-
1072
- _lastSentBatch.Add(cloned);
1073
- }
1074
-
1075
- scoreData = scores[scores.Count - 1];
1059
+ var source = scores[i];
1060
+ if (source == null) continue;
1061
+ _lastSentBatch.Add(CloneScoreData(source));
1076
1062
  }
1077
1063
 
1078
- string rawScoreJson = JsonConvert.SerializeObject(scoreData);
1064
+ // Serialize the full array of scores.
1065
+ // Normal ticks: array of 1. Offline backlog flush: array of N.
1066
+ string rawScoreJson = JsonConvert.SerializeObject(scores);
1079
1067
  var compressedScoreData = JsonCompressor.CompressJson(rawScoreJson);
1080
1068
  string scoreJson = compressedScoreData.MinifiedJson;
1081
1069
  string encryptedData = EncryptScore(scoreJson);
1082
1070
 
1083
1071
  if (string.IsNullOrEmpty(encryptedData))
1084
1072
  {
1085
- int lastScore = scoreData != null ? scoreData.cumulativeScore : -1;
1086
- HyperDebug.LogError($"[Score] Encryption failed for per-second submission. CumulativeScore={lastScore} [ErrorType=EncryptionError]");
1073
+ int lastScore = scores[scores.Count - 1].cumulativeScore;
1074
+ HyperDebug.LogError($"[Score] Encryption failed for score submission. LastCumulativeScore={lastScore} [ErrorType=EncryptionError]");
1087
1075
  return;
1088
1076
  }
1089
1077
 
1090
1078
  var message = new WebSocketScoreMessage(_cachedSessionId, _csrfToken, encryptedData);
1091
1079
  string json = JsonConvert.SerializeObject(message);
1092
1080
  int csrfLength = string.IsNullOrEmpty(_csrfToken) ? 0 : _csrfToken.Length;
1093
- int cumulativeScore = scoreData != null ? scoreData.cumulativeScore : -1;
1094
- HyperDebug.Log($"[Score] Sending per-second score. CumulativeScore={cumulativeScore}, BatchSize={scores.Count}, SessionId={_cachedSessionId}, CsrfLength={csrfLength}");
1081
+ int lastCumulativeScore = scores[scores.Count - 1].cumulativeScore;
1082
+ HyperDebug.Log($"[Score] Sending score batch. LastCumulativeScore={lastCumulativeScore}, BatchSize={scores.Count}, SessionId={_cachedSessionId}, CsrfLength={csrfLength}");
1095
1083
  SendWebSocketMessage(json);
1096
1084
  }
1097
1085
  catch (Exception ex)
@@ -1107,6 +1095,51 @@ namespace Hyper.Internal.Managers
1107
1095
  }
1108
1096
  }
1109
1097
 
1098
+ /// <summary>
1099
+ /// Creates a deep clone of a ScoreData instance.
1100
+ /// Used to create standalone copies that are not affected by the DataManager's object pooling.
1101
+ /// </summary>
1102
+ private static ScoreData CloneScoreData(ScoreData source)
1103
+ {
1104
+ if (source == null) return null;
1105
+
1106
+ var cloned = new ScoreData
1107
+ {
1108
+ cumulativeScore = source.cumulativeScore,
1109
+ isFinal = source.isFinal,
1110
+ timestampUtc = source.timestampUtc
1111
+ };
1112
+
1113
+ if (source.scoreSnapshot != null)
1114
+ {
1115
+ var snapshot = source.scoreSnapshot;
1116
+ var clonedSnapshot = new ScoreSnapshot
1117
+ {
1118
+ scoreTime = snapshot.scoreTime,
1119
+ scoreDelta = snapshot.scoreDelta
1120
+ };
1121
+
1122
+ if (snapshot.gameplayModifiers != null)
1123
+ {
1124
+ var mods = snapshot.gameplayModifiers;
1125
+ var clonedMods = new GameplayModifiers
1126
+ {
1127
+ comboModifiers = mods.comboModifiers,
1128
+ globalModifiers = mods.globalModifiers,
1129
+ activePowerUps = mods.activePowerUps != null
1130
+ ? new List<string>(mods.activePowerUps)
1131
+ : null
1132
+ };
1133
+
1134
+ clonedSnapshot.gameplayModifiers = clonedMods;
1135
+ }
1136
+
1137
+ cloned.scoreSnapshot = clonedSnapshot;
1138
+ }
1139
+
1140
+ return cloned;
1141
+ }
1142
+
1110
1143
  [Serializable]
1111
1144
  private class WebSocketScoreMessage
1112
1145
  {
@@ -1244,7 +1277,7 @@ namespace Hyper.Internal.Managers
1244
1277
 
1245
1278
  string eventName = eventType switch
1246
1279
  {
1247
- ConsoleEventType.GameStart => "GameStart",
1280
+ ConsoleEventType.GameStarted => "GameStarted",
1248
1281
  ConsoleEventType.GameResumed => "GameResumed",
1249
1282
  ConsoleEventType.GamePaused => "GamePaused",
1250
1283
  ConsoleEventType.GameEnd => "GameEnd",
@@ -1255,27 +1288,22 @@ namespace Hyper.Internal.Managers
1255
1288
  _ => string.Empty
1256
1289
  };
1257
1290
 
1258
- bool includeScoreData = eventType == ConsoleEventType.GameEnd ||
1259
- eventType == ConsoleEventType.PracticeGameEnd;
1260
-
1261
- ConsoleEvent logEntry;
1291
+ bool stopGameOnLog = true;
1262
1292
 
1263
- if (includeScoreData)
1293
+ switch (eventType)
1264
1294
  {
1265
- ConsoleData consoleData = new ConsoleData
1266
- {
1267
- score = _dataManager.CurrentScore,
1268
- time = HyperRuntime.Timer.CurrentTime,
1269
- gameId = _gameManager?.Config.GameId
1270
- };
1271
-
1272
- logEntry = new ConsoleEvent { name = eventName, data = consoleData };
1273
- }
1274
- else
1275
- {
1276
- logEntry = new ConsoleEvent { name = eventName };
1295
+ case ConsoleEventType.GameStarted:
1296
+ stopGameOnLog = false;
1297
+ break;
1298
+ case ConsoleEventType.GameResumed:
1299
+ stopGameOnLog = false;
1300
+ break;
1301
+ case ConsoleEventType.GamePaused:
1302
+ stopGameOnLog = false;
1303
+ break;
1277
1304
  }
1278
1305
 
1306
+ ConsoleEvent logEntry = new ConsoleEvent { name = eventName };
1279
1307
  string jsonLog = JsonUtility.ToJson(logEntry);
1280
1308
 
1281
1309
  #if UNITY_WEBGL && !UNITY_EDITOR
@@ -1283,7 +1311,7 @@ namespace Hyper.Internal.Managers
1283
1311
  #endif
1284
1312
 
1285
1313
  #if UNITY_EDITOR
1286
- UnityEditor.EditorApplication.isPlaying = false;
1314
+ if (stopGameOnLog) UnityEditor.EditorApplication.isPlaying = false;
1287
1315
  #endif
1288
1316
  }
1289
1317
 
@@ -429,21 +429,10 @@ namespace Hyper
429
429
  {
430
430
  #region Public Data Structures
431
431
 
432
- [Serializable]
433
- public class ConsoleData
434
- {
435
- public int score;
436
- public double time;
437
- public string gameId;
438
- public string currentPlatform;
439
- public string recommendedPlatform;
440
- }
441
-
442
432
  [Serializable]
443
433
  public class ConsoleEvent
444
434
  {
445
435
  public string name;
446
- public ConsoleData data;
447
436
  }
448
437
 
449
438
  #endregion
@@ -67,7 +67,7 @@ namespace Hyper.Internal.Managers
67
67
  internal void InvokeGameStartEvent()
68
68
  {
69
69
  OnGameStart?.Invoke();
70
- HyperRuntime.BackendManager.LogEvent(ConsoleEventType.GameStart);
70
+ HyperRuntime.BackendManager.LogEvent(ConsoleEventType.GameStarted);
71
71
  }
72
72
 
73
73
  internal void InvokeSoundChangedEvent(bool sound) => OnSoundChanged?.Invoke(sound);