app.hypergames.hypersdk 1.6.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  # [1.6.0](https://github.com/mvmhyper/hyper-sdk/compare/v1.5.10...v1.6.0) (2026-03-28)
2
9
 
3
10
 
@@ -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
  }
@@ -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);
@@ -59,32 +59,14 @@ namespace Hyper.Internal.Managers
59
59
  {
60
60
  if (!IsInGameScene || _hasGameEnded || !_hasGameStarted) return;
61
61
 
62
- // Automatically end the game when lives are exhausted
63
- // Use appropriate method based on game mode
64
- if (IsBattleMode)
65
- {
66
- EndBattleGame();
67
- }
68
- else
69
- {
70
- EndPracticeGame();
71
- }
62
+ EndGame();
72
63
  }
73
64
 
74
65
  private void HandleHealthExhausted()
75
66
  {
76
67
  if (!IsInGameScene || _hasGameEnded || !_hasGameStarted) return;
77
68
 
78
- // Automatically end the game when health is exhausted
79
- // Use appropriate method based on game mode
80
- if (IsBattleMode)
81
- {
82
- EndBattleGame();
83
- }
84
- else
85
- {
86
- EndPracticeGame();
87
- }
69
+ EndGame();
88
70
  }
89
71
 
90
72
  internal void Cleanup()
@@ -107,6 +89,22 @@ namespace Hyper.Internal.Managers
107
89
  public bool HasGameStarted => _hasGameStarted;
108
90
  public bool HasGameEnded => _hasGameEnded;
109
91
 
92
+ internal int BattleTimeRemaining =>
93
+ IsBattleMode && HyperRuntime.Timer != null
94
+ ? HyperRuntime.Timer.BattleTimeRemaining
95
+ : 0;
96
+
97
+ /// <summary>
98
+ /// Ends the current session for whichever mode is active (practice or battle).
99
+ /// </summary>
100
+ public void EndGame()
101
+ {
102
+ if (IsBattleMode)
103
+ EndBattleGame();
104
+ else
105
+ EndPracticeGame();
106
+ }
107
+
110
108
  public void EndBattleGame()
111
109
  {
112
110
  if (!IsBattleMode) return;
@@ -222,6 +220,14 @@ namespace Hyper.Internal.Managers
222
220
  _eventManager.InvokeTimeScaleResumedEvent();
223
221
  HyperDebug.Log("Time scale resumed event invoked");
224
222
 
223
+ // Always restart capturing when game resumes, even if offline,
224
+ // so scores accumulate for the offline batch and are flushed on reconnect.
225
+ if (!_hasGameEnded && _hasGameStarted)
226
+ {
227
+ _dataManager.StartCapturingScores();
228
+ }
229
+
230
+ // Only resume server session if connected
225
231
  if (_backendManager.isSocketConnected && !_hasGameEnded && _hasGameStarted)
226
232
  {
227
233
  _backendManager.SendGameResumed();
@@ -993,7 +999,7 @@ namespace Hyper.Internal.Managers
993
999
  _canvasScaler = _canvas.GetComponent<CanvasScaler>();
994
1000
  _canvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
995
1001
  _canvasScaler.referenceResolution = new Vector2(1080, 1920);
996
- _canvasScaler.matchWidthOrHeight = 0.5f;
1002
+ _canvasScaler.matchWidthOrHeight = IsPortrait() ? 0.43f : 0.73f;
997
1003
 
998
1004
  CreateSafeArea();
999
1005
  CreateBlockerUI();
@@ -68,6 +68,11 @@ namespace Hyper.Internal.Managers
68
68
  ? (int)_battleTimer
69
69
  : (int)_practiceTimer;
70
70
 
71
+ /// <summary>
72
+ /// Match countdown seconds (floored, clamped at zero). Same source as battle branch of <see cref="CurrentTime"/>.
73
+ /// </summary>
74
+ internal int BattleTimeRemaining => Mathf.Max(0, (int)_battleTimer);
75
+
71
76
  public int SessionTimeRemaining => (int)_sessionTimer;
72
77
 
73
78
  private GameManager _gameManager;
@@ -49,6 +49,14 @@ namespace Hyper.Internal
49
49
 
50
50
  #endregion
51
51
 
52
+ #region Custom Pre-Loading
53
+
54
+ [Header("Custom Pre-Loading")]
55
+ [Tooltip("Enable if your game needs to perform custom work (e.g. downloading assets, fetching remote config) after SDK initialization but before the game scene loads. When enabled, the bootstrap will pause and wait for HyperSDK.Loader.CompleteCustomLoading() to be called.")]
56
+ public bool EnableCustomLoading = false;
57
+
58
+ #endregion
59
+
52
60
  #region Game Posters & Hints
53
61
 
54
62
  [Space]
@@ -96,16 +96,9 @@ namespace Hyper.Internal
96
96
 
97
97
  public void ForfeitGame()
98
98
  {
99
+ HyperRuntime.GameManager.EndGame();
99
100
  if (!IsPVPMode)
100
- {
101
- HyperRuntime.GameManager.EndPracticeGame();
102
101
  _gameManager.LoadPracticeMenuScene();
103
- }
104
-
105
- else
106
- {
107
- HyperRuntime.GameManager.EndBattleGame();
108
- }
109
102
  }
110
103
 
111
104
  public void ToggleSound()
@@ -56,6 +56,32 @@ mergeInto(LibraryManager.library, {
56
56
  window.location.reload();
57
57
  },
58
58
 
59
+ HyperIsMobileUserAgent: function () {
60
+ var ua = navigator.userAgent || '';
61
+ if (/Android|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop|Mobile/i.test(ua)) {
62
+ return 1;
63
+ }
64
+ return 0;
65
+ },
66
+
67
+ // Large screen.* vs small inner viewport: typical of desktop Chrome DevTools device toolbar (not a guarantee).
68
+ HyperIsLikelyDesktopDevtoolsMobileViewport: function () {
69
+ try {
70
+ var sw = window.screen.width;
71
+ var sh = window.screen.height;
72
+ var screenLong = Math.max(sw, sh);
73
+ var iw = window.innerWidth;
74
+ var ih = window.innerHeight;
75
+ var innerLong = Math.max(iw, ih);
76
+ if (screenLong >= 1280 && innerLong <= 900 && screenLong > innerLong * 1.35) {
77
+ return 1;
78
+ }
79
+ return 0;
80
+ } catch (e) {
81
+ return 0;
82
+ }
83
+ },
84
+
59
85
  getDeviceScreenResolution: function () {
60
86
  // Get the device screen width and height
61
87
  const width = window.screen.width;
@@ -15,6 +15,7 @@ namespace Hyper
15
15
  private static GameAPIAdapter _gameAdapter;
16
16
  private static DataAPIAdapter _dataAdapter;
17
17
  private static EventAPIAdapter _eventAdapter;
18
+ private static LoaderAPIAdapter _loaderAdapter;
18
19
 
19
20
  /// <summary>
20
21
  /// Returns true if the SDK runtime has been initialized.
@@ -89,11 +90,33 @@ namespace Hyper
89
90
  }
90
91
  }
91
92
 
93
+ /// <summary>
94
+ /// Access loader control for custom pre-loading workflows.
95
+ /// Use when EnableCustomLoading is enabled in HyperGameContentConfig.
96
+ /// </summary>
97
+ public static ILoaderAPI Loader
98
+ {
99
+ get
100
+ {
101
+ if (_loaderAdapter == null)
102
+ {
103
+ _loaderAdapter = new LoaderAPIAdapter();
104
+ }
105
+ return _loaderAdapter;
106
+ }
107
+ }
108
+
92
109
  /// <summary>
93
110
  /// Current Hyper SDK version string.
94
111
  /// </summary>
95
112
  public static string Version => HyperSDKVersion.Version;
96
113
 
114
+ /// <summary>True if the player is treated as mobile (heuristic). In the Unity Editor, follows the Device Simulator when active.</summary>
115
+ public static bool PlayerDeviceIsMobile => PlayerDeviceClassification.PlayerDeviceIsMobile;
116
+
117
+ /// <summary>True if the player is treated as PC/desktop (opposite of <see cref="PlayerDeviceIsMobile"/>).</summary>
118
+ public static bool PlayerDeviceIsPC => PlayerDeviceClassification.PlayerDeviceIsPC;
119
+
97
120
  private static void EnsureAdaptersReady()
98
121
  {
99
122
  if (_gameAdapter == null)
@@ -121,32 +144,106 @@ namespace Hyper
121
144
  /// </summary>
122
145
  public interface IGameAPI
123
146
  {
147
+ /// <summary>
148
+ /// True when the current mode is Battle (PvP or timed battle).
149
+ /// </summary>
124
150
  bool IsBattleMode { get; }
151
+ /// <summary>
152
+ /// True when the current mode is Practice.
153
+ /// </summary>
125
154
  bool IsPracticeMode { get; }
155
+ /// <summary>
156
+ /// True if the game is currently paused by the SDK.
157
+ /// </summary>
126
158
  bool IsPaused { get; }
159
+ /// <summary>
160
+ /// True once gameplay has begun (post countdown/start).
161
+ /// </summary>
127
162
  bool HasGameStarted { get; }
163
+ /// <summary>
164
+ /// True after the current game session has ended.
165
+ /// </summary>
128
166
  bool HasGameEnded { get; }
129
167
 
168
+ /// <summary>
169
+ /// Seconds remaining on the PvP match clock. Zero in practice mode or when not in a timed battle.
170
+ /// </summary>
171
+ int BattleTimeRemaining { get; }
172
+
173
+ /// <summary>
174
+ /// Ends the current session for whichever mode is active (practice or battle).
175
+ /// </summary>
176
+ void EndGame();
177
+
178
+ [Obsolete("Use EndGame() instead; it ends practice or battle based on the current mode.")]
130
179
  void EndBattleGame();
180
+
181
+ [Obsolete("Use EndGame() instead; it ends practice or battle based on the current mode.")]
131
182
  void EndPracticeGame();
183
+
184
+ /// <summary>
185
+ /// Closes the practice menu and enters gameplay (only relevant when practice scene is a menu).
186
+ /// </summary>
132
187
  void ExitPracticeMenu();
133
188
 
134
189
  // Lives Management (only available when ControlBarLayout is HeartsLeft)
190
+ /// <summary>
191
+ /// Initialize the lives system with a maximum and optional initial value.
192
+ /// </summary>
135
193
  void InitializeLives(int maxLives, int initialLives = -1);
194
+ /// <summary>
195
+ /// Set the current number of lives.
196
+ /// </summary>
136
197
  void SetLives(int lives);
198
+ /// <summary>
199
+ /// Increment lives by one.
200
+ /// </summary>
137
201
  void AddLife();
202
+ /// <summary>
203
+ /// Decrement lives by one.
204
+ /// </summary>
138
205
  void RemoveLife();
206
+ /// <summary>
207
+ /// Get the current number of lives.
208
+ /// </summary>
139
209
  int GetCurrentLives();
210
+ /// <summary>
211
+ /// Get the configured maximum number of lives.
212
+ /// </summary>
140
213
  int GetMaxLives();
141
214
 
142
215
  // Health Management (only available when ControlBarLayout is HealthBarGreenLeft)
216
+ /// <summary>
217
+ /// Initialize the health system with a maximum and optional initial value.
218
+ /// </summary>
143
219
  void InitializeHealth(float maxHealth, float initialHealth = -1f, bool isNormalized = false);
220
+ /// <summary>
221
+ /// Set the current health (absolute units).
222
+ /// </summary>
144
223
  void SetHealth(float health);
224
+ /// <summary>
225
+ /// Set the current health as a normalized value in [0,1].
226
+ /// </summary>
145
227
  void SetHealthNormalized(float normalizedHealth);
228
+ /// <summary>
229
+ /// Add to the current health by an absolute amount.
230
+ /// </summary>
146
231
  void AddHealth(float amount);
232
+ /// <summary>
233
+ /// Remove from the current health by an absolute amount.
234
+ /// </summary>
147
235
  void RemoveHealth(float amount);
236
+ /// <summary>
237
+ /// Get the current health (absolute units).
238
+ /// </summary>
148
239
  float GetCurrentHealth();
240
+ /// <summary>
241
+ /// Get the maximum health (absolute units).
242
+ /// </summary>
149
243
  float GetMaxHealth();
244
+ /// <summary>
245
+ /// Get the current health normalized to [0,1].
246
+ /// </summary>
150
247
  float GetHealthNormalized();
151
248
  }
152
249
 
@@ -155,34 +252,119 @@ namespace Hyper
155
252
  /// </summary>
156
253
  public interface IDataAPI
157
254
  {
255
+ /// <summary>
256
+ /// Current in-session score value.
257
+ /// </summary>
158
258
  int CurrentScore { get; }
159
259
 
260
+ /// <summary>
261
+ /// Overwrite the current score with a new value.
262
+ /// </summary>
160
263
  void UpdateScore(int score);
264
+ /// <summary>
265
+ /// Set the combo multiplier applied to scoring.
266
+ /// </summary>
161
267
  void SetComboMultiplier(float value);
268
+ /// <summary>
269
+ /// Set a global multiplier applied to scoring.
270
+ /// </summary>
162
271
  void SetGlobalMultiplier(float value);
272
+ /// <summary>
273
+ /// Register that a powerup is active by type identifier.
274
+ /// </summary>
163
275
  void AddPowerup(string powerupType);
276
+ /// <summary>
277
+ /// Remove an active powerup by type identifier.
278
+ /// </summary>
164
279
  void RemovePowerup(string powerupType);
280
+ /// <summary>
281
+ /// Retrieve the best/high score recorded for the player.
282
+ /// </summary>
165
283
  int GetBestScore();
166
284
  }
167
285
 
286
+ /// <summary>
287
+ /// Public API for custom pre-loading control during the HyperLoader bootstrap.
288
+ /// </summary>
289
+ public interface ILoaderAPI
290
+ {
291
+ /// <summary>
292
+ /// Report progress of your custom loading (0.0 to 1.0).
293
+ /// This feeds into the loading bar so players see smooth progress during your custom work.
294
+ /// Optional — if not called, the loader will simulate progress automatically.
295
+ /// </summary>
296
+ void ReportCustomLoadingProgress(float progress);
297
+
298
+ /// <summary>
299
+ /// Signal that your custom pre-loading is complete.
300
+ /// The HyperLoader bootstrap will resume and load the game scene.
301
+ /// Must be called when EnableCustomLoading is true in HyperGameContentConfig.
302
+ /// </summary>
303
+ void CompleteCustomLoading();
304
+ }
305
+
168
306
  /// <summary>
169
307
  /// Public API for SDK events.
170
308
  /// </summary>
171
309
  public interface IEventAPI
172
310
  {
311
+ /// <summary>
312
+ /// Fired when gameplay begins.
313
+ /// </summary>
173
314
  event Action OnGameStart;
315
+ /// <summary>
316
+ /// Fired when the match timer reaches zero (battle mode).
317
+ /// </summary>
174
318
  event Action OnTimeUp;
319
+ /// <summary>
320
+ /// Fired when the game is paused by the SDK.
321
+ /// </summary>
175
322
  event Action OnGamePaused;
323
+ /// <summary>
324
+ /// Fired when the game resumes from a paused state.
325
+ /// </summary>
176
326
  event Action OnGameResumed;
327
+ /// <summary>
328
+ /// Fired when the current game session ends.
329
+ /// </summary>
177
330
  event Action OnGameEnded;
331
+ /// <summary>
332
+ /// Fired when the SDK sound setting changes (true = enabled).
333
+ /// </summary>
178
334
  event Action<bool> OnSoundChanged;
335
+ /// <summary>
336
+ /// Fired when lives reach zero (when lives UI is in use).
337
+ /// </summary>
179
338
  event Action OnLivesExhausted;
339
+ /// <summary>
340
+ /// Fired when health reaches zero (when health UI is in use).
341
+ /// </summary>
180
342
  event Action OnHealthExhausted;
343
+ /// <summary>
344
+ /// Fired when a custom restart flow is requested by the SDK.
345
+ /// </summary>
181
346
  event Action OnCustomRestart;
182
347
  }
183
348
 
184
349
  #region Adapter Implementations
185
350
 
351
+ /// <summary>
352
+ /// Bridges the public loader API to the internal HyperRuntime custom loading state.
353
+ /// </summary>
354
+ internal sealed class LoaderAPIAdapter : ILoaderAPI
355
+ {
356
+ public void ReportCustomLoadingProgress(float progress)
357
+ {
358
+ HyperRuntime.CustomLoadingProgress = Mathf.Clamp01(progress);
359
+ }
360
+
361
+ public void CompleteCustomLoading()
362
+ {
363
+ HyperRuntime.CustomLoadingComplete = true;
364
+ HyperDebug.Log("Custom pre-loading complete. Bootstrap resuming");
365
+ }
366
+ }
367
+
186
368
  /// <summary>
187
369
  /// Bridges the public game API to the internal GameManager without exposing it to consumers.
188
370
  /// </summary>
@@ -196,8 +378,17 @@ namespace Hyper
196
378
  public bool HasGameStarted => Manager != null && Manager.HasGameStarted;
197
379
  public bool HasGameEnded => Manager != null && Manager.HasGameEnded;
198
380
 
381
+ public int BattleTimeRemaining => Manager?.BattleTimeRemaining ?? 0;
382
+
383
+ /// <inheritdoc/>
384
+ public void EndGame() => Manager?.EndGame();
385
+
386
+ [Obsolete("Use EndGame() instead; it ends practice or battle based on the current mode.")]
199
387
  public void EndBattleGame() => Manager?.EndBattleGame();
388
+
389
+ [Obsolete("Use EndGame() instead; it ends practice or battle based on the current mode.")]
200
390
  public void EndPracticeGame() => Manager?.EndPracticeGame();
391
+
201
392
  public void ExitPracticeMenu() => Manager?.ExitPracticeMenu();
202
393
 
203
394
  public void InitializeLives(int maxLives, int initialLives = -1) => Manager?.InitializeLives(maxLives, initialLives);
@@ -0,0 +1,41 @@
1
+ using System.Runtime.InteropServices;
2
+ using Hyper.Shared;
3
+ using UnityEngine;
4
+
5
+ namespace Hyper
6
+ {
7
+ internal static class PlayerDeviceClassification
8
+ {
9
+ internal static bool PlayerDeviceIsMobile => Resolve() == DevicePlatform.Mobile;
10
+
11
+ internal static bool PlayerDeviceIsPC => !PlayerDeviceIsMobile;
12
+
13
+ private static DevicePlatform Resolve()
14
+ {
15
+ #if UNITY_EDITOR
16
+ // Unity Device Simulator: Device.Application matches the simulated device; Application does not.
17
+ return UnityEngine.Device.Application.isMobilePlatform ? DevicePlatform.Mobile : DevicePlatform.PC;
18
+ #elif UNITY_WEBGL && !UNITY_EDITOR
19
+ if (HyperIsLikelyDesktopDevtoolsMobileViewport() != 0)
20
+ return DevicePlatform.PC;
21
+ if (Application.isMobilePlatform || HyperIsMobileUserAgent() != 0)
22
+ return DevicePlatform.Mobile;
23
+ return DevicePlatform.PC;
24
+ #else
25
+ return Application.isMobilePlatform ? DevicePlatform.Mobile : DevicePlatform.PC;
26
+ #endif
27
+ }
28
+
29
+ #if UNITY_WEBGL && !UNITY_EDITOR
30
+ [DllImport("__Internal")]
31
+ private static extern int HyperIsMobileUserAgent();
32
+
33
+ [DllImport("__Internal")]
34
+ private static extern int HyperIsLikelyDesktopDevtoolsMobileViewport();
35
+ #else
36
+ private static int HyperIsMobileUserAgent() => 0;
37
+
38
+ private static int HyperIsLikelyDesktopDevtoolsMobileViewport() => 0;
39
+ #endif
40
+ }
41
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 6462d3b83a3aab741899fa52596c2263
@@ -25,4 +25,5 @@ MonoBehaviour:
25
25
  sceneGUID:
26
26
  scenePath:
27
27
  sceneName:
28
+ EnableCustomLoading: 0
28
29
  gamePosterSprite: {fileID: 0}
@@ -53,7 +53,7 @@ namespace Hyper.Shared
53
53
  /// <summary>
54
54
  /// Fired when the game starts.
55
55
  /// </summary>
56
- GameStart,
56
+ GameStarted,
57
57
 
58
58
  /// <summary>
59
59
  /// Fired when the game is resumed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app.hypergames.hypersdk",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "displayName": "HyperSDK",
5
5
  "description": "Official Unity SDK for Hyper: multiplayer battles, leaderboards, deterministic RNG, session management, tutorials, and WebGL optimizations for production-ready games.",
6
6
  "unity": "6000.0",
@@ -24,9 +24,9 @@
24
24
  "email": "info@mvm.gg",
25
25
  "url": "https://mvm.gg/"
26
26
  },
27
- "licensesUrl": "https://mvm.gg/",
28
- "changelogUrl": "https://mvm.gg/",
29
- "documentationUrl": "https://mvm.gg/",
27
+ "licensesUrl": "https://github.com/mvmhyper/hyper-sdk/blob/releaseTests/LICENSE.md",
28
+ "changelogUrl": "https://github.com/mvmhyper/hyper-sdk/blob/releaseTests/CHANGELOG.md",
29
+ "documentationUrl": "https://docs-getting-started-updates.hyper-sdk-documentation.pages.dev/",
30
30
  "repository": {
31
31
  "type": "git",
32
32
  "url": "https://github.com/mvmhyper/hyper-sdk.git",