app.hypergames.hypersdk 1.8.2 → 1.8.4
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 +14 -0
- package/HyperLoader.unity +2 -2
- package/Runtime/Internal/Data/URLData.cs +14 -18
- package/Runtime/Internal/Managers/BackendManager.cs +300 -6
- package/Runtime/Internal/Managers/DataManager.cs +21 -0
- package/Runtime/Internal/Managers/EventManager.cs +2 -0
- package/Runtime/Internal/Managers/GameManager.cs +4 -0
- package/Runtime/Internal/UI/Components/ConnectionStatusIcon.cs +261 -16
- package/Runtime/Internal/UI/Modals/ScoreSubmissionFailedModal.cs +1 -1
- package/Runtime/Plugins/WebGL/HyperTutorialCachePlugin.jslib +4 -2
- package/Runtime/Plugins/WebGL/HyperWebSocketPlugin.jslib +5 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Group 36946.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Group 36946.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Group-1.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Group-1.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Group-2.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Group-2.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Group.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Group.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/{Icon_YesWifi.png.meta → Internet Connection/Icon_YesWifi.png.meta } +1 -1
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Vector.png +0 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection/Vector.png.meta +169 -0
- package/Runtime/Resources/Sprites/Control Bar/Internet Connection.meta +8 -0
- package/Runtime/Resources/UIPrefabs/ControlBar.prefab +735 -68
- package/package.json +1 -1
- /package/Runtime/Resources/Sprites/Control Bar/{Icon_NoWifi.png → Internet Connection/Icon_NoWifi.png} +0 -0
- /package/Runtime/Resources/Sprites/Control Bar/{Icon_NoWifi.png.meta → Internet Connection/Icon_NoWifi.png.meta} +0 -0
- /package/Runtime/Resources/Sprites/Control Bar/{Icon_YesWifi.png → Internet Connection/Icon_YesWifi.png} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.8.4](https://github.com/mvmhyper/hyper-sdk/compare/v1.8.3...v1.8.4) (2026-04-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* update SDK control bar text and Wi-Fi icon sizing ([0a1b926](https://github.com/mvmhyper/hyper-sdk/commit/0a1b9269a7fb59d78fc9786a2479605b415a684f))
|
|
7
|
+
|
|
8
|
+
## [1.8.3](https://github.com/mvmhyper/hyper-sdk/compare/v1.8.2...v1.8.3) (2026-04-10)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* WebGL reliability, scores, connection UI, and session-end audio ([adda0d9](https://github.com/mvmhyper/hyper-sdk/commit/adda0d9a688e786872c82a6429ce31b160e6b5e3))
|
|
14
|
+
|
|
1
15
|
## [1.8.2](https://github.com/mvmhyper/hyper-sdk/compare/v1.8.1...v1.8.2) (2026-04-07)
|
|
2
16
|
|
|
3
17
|
|
package/HyperLoader.unity
CHANGED
|
@@ -305,7 +305,7 @@ RectTransform:
|
|
|
305
305
|
m_AnchorMin: {x: 0, y: 1}
|
|
306
306
|
m_AnchorMax: {x: 0, y: 1}
|
|
307
307
|
m_AnchoredPosition: {x: 489.45853, y: -280.1698}
|
|
308
|
-
m_SizeDelta: {x:
|
|
308
|
+
m_SizeDelta: {x: 0, y: 239.347}
|
|
309
309
|
m_Pivot: {x: 0.5, y: 0.5}
|
|
310
310
|
--- !u!114 &751153944
|
|
311
311
|
MonoBehaviour:
|
|
@@ -840,7 +840,7 @@ GameObject:
|
|
|
840
840
|
m_Component:
|
|
841
841
|
- component: {fileID: 1064166296}
|
|
842
842
|
m_Layer: 0
|
|
843
|
-
m_Name:
|
|
843
|
+
m_Name: __HyperSDK_BuildMarker_639114620230900022
|
|
844
844
|
m_TagString: Untagged
|
|
845
845
|
m_Icon: {fileID: 0}
|
|
846
846
|
m_NavMeshLayer: 0
|
|
@@ -57,57 +57,53 @@ namespace Hyper.Internal
|
|
|
57
57
|
|
|
58
58
|
public string WorkspaceId
|
|
59
59
|
{
|
|
60
|
-
get =>
|
|
61
|
-
set => _workspaceId = value;
|
|
60
|
+
get => _workspaceId;
|
|
61
|
+
set => _workspaceId = string.Copy(value);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
public string TournamentId
|
|
65
65
|
{
|
|
66
66
|
get
|
|
67
67
|
{
|
|
68
|
-
if (string.IsNullOrEmpty(OldTournamentId) && string.IsNullOrEmpty(_tournamentId))
|
|
69
|
-
{
|
|
70
|
-
_tournamentId = Guid.NewGuid().ToString();
|
|
71
|
-
}
|
|
72
68
|
return string.IsNullOrEmpty(OldTournamentId) ? _tournamentId : OldTournamentId;
|
|
73
69
|
}
|
|
74
|
-
set => _tournamentId = value;
|
|
70
|
+
set => _tournamentId = string.Copy(value);
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
public string SessionId
|
|
78
74
|
{
|
|
79
|
-
get =>
|
|
75
|
+
get => _sessionId;
|
|
80
76
|
set => _sessionId = string.Copy(value);
|
|
81
77
|
}
|
|
82
78
|
|
|
83
79
|
public string GameId
|
|
84
80
|
{
|
|
85
|
-
get =>
|
|
86
|
-
set => _gameId = value;
|
|
81
|
+
get => _gameId;
|
|
82
|
+
set => _gameId = string.Copy(value);
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
public string GameSlug
|
|
90
86
|
{
|
|
91
87
|
get => _gameSlug;
|
|
92
|
-
set => _gameSlug = value;
|
|
88
|
+
set => _gameSlug = string.Copy(value);
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
public string EntryId
|
|
96
92
|
{
|
|
97
|
-
get =>
|
|
98
|
-
set => _entryId = value;
|
|
93
|
+
get => _entryId;
|
|
94
|
+
set => _entryId = string.Copy(value);
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
public string UserId
|
|
102
98
|
{
|
|
103
|
-
get =>
|
|
104
|
-
set => _userId = value;
|
|
99
|
+
get => _userId;
|
|
100
|
+
set => _userId = string.Copy(value);
|
|
105
101
|
}
|
|
106
102
|
|
|
107
103
|
public string EmbedUrl
|
|
108
104
|
{
|
|
109
|
-
get =>
|
|
110
|
-
set => _embedUrl = value;
|
|
105
|
+
get => _embedUrl;
|
|
106
|
+
set => _embedUrl = string.Copy(value);
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
#endregion
|
|
@@ -116,7 +112,7 @@ namespace Hyper.Internal
|
|
|
116
112
|
{
|
|
117
113
|
GameMode = GameMode.Battle;
|
|
118
114
|
}
|
|
119
|
-
|
|
115
|
+
|
|
120
116
|
public void SetModeAsPractice()
|
|
121
117
|
{
|
|
122
118
|
GameMode = GameMode.Practice;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
using Newtonsoft.Json;
|
|
2
|
+
using Newtonsoft.Json.Linq;
|
|
2
3
|
using System;
|
|
3
4
|
using System.Collections;
|
|
4
5
|
using System.Collections.Generic;
|
|
@@ -64,6 +65,11 @@ namespace Hyper.Internal.Managers
|
|
|
64
65
|
|
|
65
66
|
public SubmissionErrorType LastSubmissionError { get; private set; } = SubmissionErrorType.None;
|
|
66
67
|
|
|
68
|
+
/// <summary>
|
|
69
|
+
/// Smoothed round-trip latency (ms) from sdkPing/sdkPong, or -1 when unknown or disconnected.
|
|
70
|
+
/// </summary>
|
|
71
|
+
public int SdkSmoothedLatencyMs { get; private set; } = -1;
|
|
72
|
+
|
|
67
73
|
#endregion
|
|
68
74
|
|
|
69
75
|
#region Private Configuration
|
|
@@ -75,7 +81,7 @@ namespace Hyper.Internal.Managers
|
|
|
75
81
|
/// Only uses production URL for release builds made via Build Assistant (HYPER_RELEASE_BUILD define).
|
|
76
82
|
/// All other builds (development, editor, etc.) use the dev server URL.
|
|
77
83
|
/// </summary>
|
|
78
|
-
|
|
84
|
+
public string GetServerUrl()
|
|
79
85
|
{
|
|
80
86
|
#if HYPER_RELEASE_BUILD
|
|
81
87
|
// Only release builds from Build Assistant use production URL
|
|
@@ -100,7 +106,7 @@ namespace Hyper.Internal.Managers
|
|
|
100
106
|
private static extern string EncryptScore(string score);
|
|
101
107
|
|
|
102
108
|
[DllImport("__Internal")]
|
|
103
|
-
private static extern void FetchAndCacheTutorials(string gameSlug, string gameObject);
|
|
109
|
+
private static extern void FetchAndCacheTutorials(string serverBaseUrl, string gameSlug, string gameObject);
|
|
104
110
|
|
|
105
111
|
[DllImport("__Internal")]
|
|
106
112
|
private static extern void InitWebSocket(string serverUrl);
|
|
@@ -116,7 +122,7 @@ namespace Hyper.Internal.Managers
|
|
|
116
122
|
#else
|
|
117
123
|
private string EncryptScore(string score) => score;
|
|
118
124
|
|
|
119
|
-
private void FetchAndCacheTutorials(string gameSlug, string gameObject)
|
|
125
|
+
private void FetchAndCacheTutorials(string serverBaseUrl, string gameSlug, string gameObject)
|
|
120
126
|
{
|
|
121
127
|
_isFetchingTutorial = false;
|
|
122
128
|
_onTutorialSuccess?.Invoke();
|
|
@@ -239,6 +245,7 @@ namespace Hyper.Internal.Managers
|
|
|
239
245
|
_gameManager.Config.RandomSeed = randomSeed;
|
|
240
246
|
_gameManager.Config.SessionId = id;
|
|
241
247
|
_cachedSessionId = string.Copy(id);
|
|
248
|
+
StartSdkPingLoop();
|
|
242
249
|
|
|
243
250
|
return true;
|
|
244
251
|
}
|
|
@@ -290,6 +297,14 @@ namespace Hyper.Internal.Managers
|
|
|
290
297
|
private bool _isManuallyClosed;
|
|
291
298
|
private bool _isSocketErrorCalled;
|
|
292
299
|
|
|
300
|
+
private Coroutine _sdkPingCoroutine;
|
|
301
|
+
private long _nextSdkPingId;
|
|
302
|
+
private long? _pendingSdkPingId;
|
|
303
|
+
private float _pendingSdkPingRealtime;
|
|
304
|
+
private float _sdkPingLatencyEma = -1f;
|
|
305
|
+
private const float SdkPingLatencyEmaAlpha = 0.25f;
|
|
306
|
+
private const float SdkPingIntervalSeconds = 3f;
|
|
307
|
+
|
|
293
308
|
public void InitializeWebSocket()
|
|
294
309
|
{
|
|
295
310
|
if (_dataManager == null) _dataManager = HyperRuntime.DataManager;
|
|
@@ -313,9 +328,11 @@ namespace Hyper.Internal.Managers
|
|
|
313
328
|
|
|
314
329
|
public void StopWebSocket()
|
|
315
330
|
{
|
|
331
|
+
StopSdkPingLoop();
|
|
316
332
|
_isManuallyClosed = true;
|
|
317
333
|
isSocketConnected = false;
|
|
318
334
|
CloseWebSocket();
|
|
335
|
+
_eventManager?.InvokeBackendSocketManuallyClosedEvent();
|
|
319
336
|
}
|
|
320
337
|
|
|
321
338
|
private IEnumerator StartWebSocketInitialization()
|
|
@@ -476,6 +493,73 @@ namespace Hyper.Internal.Managers
|
|
|
476
493
|
_sendGameStartedCoroutine = null;
|
|
477
494
|
}
|
|
478
495
|
|
|
496
|
+
private void StopSdkPingLoop()
|
|
497
|
+
{
|
|
498
|
+
if (_sdkPingCoroutine != null)
|
|
499
|
+
{
|
|
500
|
+
StopCoroutine(_sdkPingCoroutine);
|
|
501
|
+
_sdkPingCoroutine = null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
_pendingSdkPingId = null;
|
|
505
|
+
_sdkPingLatencyEma = -1f;
|
|
506
|
+
SdkSmoothedLatencyMs = -1;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private void StartSdkPingLoop()
|
|
510
|
+
{
|
|
511
|
+
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
512
|
+
if (_sdkPingCoroutine != null)
|
|
513
|
+
return;
|
|
514
|
+
|
|
515
|
+
_sdkPingCoroutine = StartCoroutine(SdkPingLoopCoroutine());
|
|
516
|
+
#endif
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
520
|
+
private IEnumerator SdkPingLoopCoroutine()
|
|
521
|
+
{
|
|
522
|
+
while (true)
|
|
523
|
+
{
|
|
524
|
+
yield return new WaitForSecondsRealtime(SdkPingIntervalSeconds);
|
|
525
|
+
TrySendSdkPing();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private void TrySendSdkPing()
|
|
530
|
+
{
|
|
531
|
+
if (!isSocketConnected || string.IsNullOrEmpty(_cachedSessionId))
|
|
532
|
+
return;
|
|
533
|
+
|
|
534
|
+
if (_pendingSdkPingId.HasValue)
|
|
535
|
+
return;
|
|
536
|
+
|
|
537
|
+
long pingId = ++_nextSdkPingId;
|
|
538
|
+
_pendingSdkPingId = pingId;
|
|
539
|
+
_pendingSdkPingRealtime = Time.realtimeSinceStartup;
|
|
540
|
+
|
|
541
|
+
try
|
|
542
|
+
{
|
|
543
|
+
var message = new
|
|
544
|
+
{
|
|
545
|
+
@event = "sdkPing",
|
|
546
|
+
data = new
|
|
547
|
+
{
|
|
548
|
+
sessionId = _cachedSessionId,
|
|
549
|
+
pingId
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
string json = JsonConvert.SerializeObject(message);
|
|
554
|
+
SendWebSocketMessage(json);
|
|
555
|
+
}
|
|
556
|
+
catch (Exception ex)
|
|
557
|
+
{
|
|
558
|
+
_pendingSdkPingId = null;
|
|
559
|
+
HyperDebug.LogError($"[sdkPing] Send failed: {ex.Message}");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
#endif
|
|
479
563
|
|
|
480
564
|
#endregion
|
|
481
565
|
|
|
@@ -508,10 +592,16 @@ namespace Hyper.Internal.Managers
|
|
|
508
592
|
{
|
|
509
593
|
_eventManager?.InvokeConnectionFoundEvent();
|
|
510
594
|
}
|
|
595
|
+
|
|
596
|
+
if (!string.IsNullOrEmpty(_cachedSessionId))
|
|
597
|
+
{
|
|
598
|
+
StartSdkPingLoop();
|
|
599
|
+
}
|
|
511
600
|
}
|
|
512
601
|
|
|
513
602
|
public void OnSocketDisconnected()
|
|
514
603
|
{
|
|
604
|
+
StopSdkPingLoop();
|
|
515
605
|
isSocketConnected = false;
|
|
516
606
|
|
|
517
607
|
if (!_isManuallyClosed)
|
|
@@ -537,6 +627,7 @@ namespace Hyper.Internal.Managers
|
|
|
537
627
|
}
|
|
538
628
|
|
|
539
629
|
isSocketConnected = false;
|
|
630
|
+
StopSdkPingLoop();
|
|
540
631
|
}
|
|
541
632
|
|
|
542
633
|
public void OnSocketGameStarted(string message)
|
|
@@ -567,6 +658,38 @@ namespace Hyper.Internal.Managers
|
|
|
567
658
|
// Reserved for future use
|
|
568
659
|
}
|
|
569
660
|
|
|
661
|
+
public void OnSocketSdkPong(string message)
|
|
662
|
+
{
|
|
663
|
+
if (string.IsNullOrEmpty(message) || !_pendingSdkPingId.HasValue)
|
|
664
|
+
return;
|
|
665
|
+
|
|
666
|
+
try
|
|
667
|
+
{
|
|
668
|
+
JObject jo = JObject.Parse(message);
|
|
669
|
+
JToken token = jo["pingId"];
|
|
670
|
+
if (token == null || token.Type == JTokenType.Null)
|
|
671
|
+
return;
|
|
672
|
+
|
|
673
|
+
long pongId = token.Value<long>();
|
|
674
|
+
if (pongId != _pendingSdkPingId.Value)
|
|
675
|
+
return;
|
|
676
|
+
|
|
677
|
+
float rttMs = (Time.realtimeSinceStartup - _pendingSdkPingRealtime) * 1000f;
|
|
678
|
+
_pendingSdkPingId = null;
|
|
679
|
+
|
|
680
|
+
if (_sdkPingLatencyEma < 0f)
|
|
681
|
+
_sdkPingLatencyEma = rttMs;
|
|
682
|
+
else
|
|
683
|
+
_sdkPingLatencyEma = Mathf.Lerp(_sdkPingLatencyEma, rttMs, SdkPingLatencyEmaAlpha);
|
|
684
|
+
|
|
685
|
+
SdkSmoothedLatencyMs = Mathf.RoundToInt(_sdkPingLatencyEma);
|
|
686
|
+
}
|
|
687
|
+
catch (Exception ex)
|
|
688
|
+
{
|
|
689
|
+
HyperDebug.LogWarning($"[sdkPong] Failed to parse or apply: {ex.Message}");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
570
693
|
public void OnSocketScoreUpdated(string message)
|
|
571
694
|
{
|
|
572
695
|
if (_lastSentBatch != null && _lastSentBatch.Count > 0)
|
|
@@ -585,6 +708,12 @@ namespace Hyper.Internal.Managers
|
|
|
585
708
|
}
|
|
586
709
|
|
|
587
710
|
TryUpdateCSRFToken(message, "scoreUpdated");
|
|
711
|
+
if (!string.IsNullOrEmpty(_csrfToken))
|
|
712
|
+
{
|
|
713
|
+
_isWaitingForTokenLogEmitted = false;
|
|
714
|
+
int pendingCount = _dataManager?.PendingScores?.Count ?? 0;
|
|
715
|
+
HyperDebug.Log($"[CSRF] Fresh token received from scoreUpdated. Score sending can continue. PendingCount={pendingCount}");
|
|
716
|
+
}
|
|
588
717
|
}
|
|
589
718
|
|
|
590
719
|
private void TryUpdateCSRFToken(string json, string context = null)
|
|
@@ -849,6 +978,40 @@ namespace Hyper.Internal.Managers
|
|
|
849
978
|
yield return new WaitForSecondsRealtime(1f);
|
|
850
979
|
}
|
|
851
980
|
|
|
981
|
+
// Token-gated flow invalidates token after each send.
|
|
982
|
+
// Refresh token again before sending the final score payload.
|
|
983
|
+
if (string.IsNullOrEmpty(_csrfToken))
|
|
984
|
+
{
|
|
985
|
+
_gameResumed = false;
|
|
986
|
+
HyperDebug.Log("[CSRF] Token unavailable after backlog flush. Refreshing token before final score submit.");
|
|
987
|
+
|
|
988
|
+
if (_sendGameResumedCoroutine == null)
|
|
989
|
+
{
|
|
990
|
+
_sendGameResumedCoroutine = StartCoroutine(SendGameResumedCoroutine());
|
|
991
|
+
yield return _sendGameResumedCoroutine;
|
|
992
|
+
_sendGameResumedCoroutine = null;
|
|
993
|
+
}
|
|
994
|
+
else
|
|
995
|
+
{
|
|
996
|
+
yield return _sendGameResumedCoroutine;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (string.IsNullOrEmpty(_csrfToken))
|
|
1000
|
+
{
|
|
1001
|
+
LastSubmissionError = SubmissionErrorType.CSRFTokenError;
|
|
1002
|
+
HyperDebug.LogWarning("Final score submit blocked: CSRF token still unavailable after refresh attempt. Retrying... [CSRF_TOKEN_ERROR]");
|
|
1003
|
+
_currentRetryCount++;
|
|
1004
|
+
_gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
|
|
1005
|
+
|
|
1006
|
+
if (_currentRetryCount < MAX_RETRY_COUNT)
|
|
1007
|
+
{
|
|
1008
|
+
yield return new WaitForSecondsRealtime(1f);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
852
1015
|
// Sending final score via WebSocket
|
|
853
1016
|
_finalScoreSubmitted = false;
|
|
854
1017
|
_isWaitingForFinalScoreResponse = true;
|
|
@@ -975,6 +1138,12 @@ namespace Hyper.Internal.Managers
|
|
|
975
1138
|
private readonly WaitForSeconds _scoreSendInterval = new WaitForSeconds(1f);
|
|
976
1139
|
private bool _isWaitingForFinalScoreResponse;
|
|
977
1140
|
private bool _finalScoreSubmitted;
|
|
1141
|
+
private bool _isWaitingForTokenLogEmitted;
|
|
1142
|
+
/// <summary>
|
|
1143
|
+
/// True while a score batch is being encrypted/sent. Prevents overlapping sends and
|
|
1144
|
+
/// lets <see cref="SendScoresInBatch"/> skip a tick instead of draining pending scores mid-flight.
|
|
1145
|
+
/// </summary>
|
|
1146
|
+
private bool _scoreBatchSendInProgress;
|
|
978
1147
|
|
|
979
1148
|
public void StartSendingScores()
|
|
980
1149
|
{
|
|
@@ -1006,6 +1175,28 @@ namespace Hyper.Internal.Managers
|
|
|
1006
1175
|
{
|
|
1007
1176
|
int pendingCount = _dataManager?.PendingScores?.Count ?? 0;
|
|
1008
1177
|
HyperDebug.Log($"[Score] Socket disconnected - accumulating scores for offline batch. PendingCount={pendingCount}");
|
|
1178
|
+
_isWaitingForTokenLogEmitted = false;
|
|
1179
|
+
yield return _scoreSendInterval;
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (string.IsNullOrEmpty(_csrfToken))
|
|
1184
|
+
{
|
|
1185
|
+
int pendingCount = _dataManager?.PendingScores?.Count ?? 0;
|
|
1186
|
+
if (!_isWaitingForTokenLogEmitted)
|
|
1187
|
+
{
|
|
1188
|
+
HyperDebug.LogWarning($"[Score] CSRF token unavailable - pausing score sends and batching until token refresh. PendingCount={pendingCount}");
|
|
1189
|
+
_isWaitingForTokenLogEmitted = true;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
yield return _scoreSendInterval;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (_scoreBatchSendInProgress)
|
|
1197
|
+
{
|
|
1198
|
+
int pendingCount = _dataManager?.PendingScores?.Count ?? 0;
|
|
1199
|
+
HyperDebug.LogWarning($"[Score] Batch send in progress; skipping this tick to avoid overlapping submits. PendingCount={pendingCount}");
|
|
1009
1200
|
yield return _scoreSendInterval;
|
|
1010
1201
|
continue;
|
|
1011
1202
|
}
|
|
@@ -1028,6 +1219,7 @@ namespace Hyper.Internal.Managers
|
|
|
1028
1219
|
}
|
|
1029
1220
|
else
|
|
1030
1221
|
{
|
|
1222
|
+
_isWaitingForTokenLogEmitted = false;
|
|
1031
1223
|
HyperDebug.Log("[Score] No pending scores to send in this tick.");
|
|
1032
1224
|
}
|
|
1033
1225
|
|
|
@@ -1035,10 +1227,75 @@ namespace Hyper.Internal.Managers
|
|
|
1035
1227
|
}
|
|
1036
1228
|
}
|
|
1037
1229
|
|
|
1230
|
+
/// <summary>
|
|
1231
|
+
/// Sorts a batch in chronological order (oldest to newest) by score time.
|
|
1232
|
+
/// Null entries and entries without snapshots are pushed to the end.
|
|
1233
|
+
/// </summary>
|
|
1234
|
+
private static void SortScoresByScoreTimeAscending(List<ScoreData> scores)
|
|
1235
|
+
{
|
|
1236
|
+
if (scores == null || scores.Count <= 1) return;
|
|
1237
|
+
scores.Sort(CompareScoresByScoreTimeAscending);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
private static int CompareScoresByScoreTimeAscending(ScoreData left, ScoreData right)
|
|
1241
|
+
{
|
|
1242
|
+
if (ReferenceEquals(left, right)) return 0;
|
|
1243
|
+
if (left == null) return 1;
|
|
1244
|
+
if (right == null) return -1;
|
|
1245
|
+
|
|
1246
|
+
bool leftHasSnapshot = left.scoreSnapshot != null;
|
|
1247
|
+
bool rightHasSnapshot = right.scoreSnapshot != null;
|
|
1248
|
+
|
|
1249
|
+
if (!leftHasSnapshot && !rightHasSnapshot)
|
|
1250
|
+
{
|
|
1251
|
+
return left.cumulativeScore.CompareTo(right.cumulativeScore);
|
|
1252
|
+
}
|
|
1253
|
+
if (!leftHasSnapshot) return 1;
|
|
1254
|
+
if (!rightHasSnapshot) return -1;
|
|
1255
|
+
|
|
1256
|
+
int byTime = left.scoreSnapshot.scoreTime.CompareTo(right.scoreSnapshot.scoreTime);
|
|
1257
|
+
if (byTime != 0) return byTime;
|
|
1258
|
+
|
|
1259
|
+
return left.cumulativeScore.CompareTo(right.cumulativeScore);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1038
1262
|
private void SendScoresOverWebSocket(List<ScoreData> scores)
|
|
1039
1263
|
{
|
|
1040
1264
|
if (scores == null || scores.Count == 0) return;
|
|
1041
1265
|
|
|
1266
|
+
SortScoresByScoreTimeAscending(scores);
|
|
1267
|
+
|
|
1268
|
+
ScoreData firstSortedScore = null;
|
|
1269
|
+
ScoreData lastSortedScore = null;
|
|
1270
|
+
for (int i = 0; i < scores.Count; i++)
|
|
1271
|
+
{
|
|
1272
|
+
if (scores[i] != null)
|
|
1273
|
+
{
|
|
1274
|
+
firstSortedScore = scores[i];
|
|
1275
|
+
break;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
for (int i = scores.Count - 1; i >= 0; i--)
|
|
1279
|
+
{
|
|
1280
|
+
if (scores[i] != null)
|
|
1281
|
+
{
|
|
1282
|
+
lastSortedScore = scores[i];
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (firstSortedScore != null && lastSortedScore != null)
|
|
1288
|
+
{
|
|
1289
|
+
float firstTime = firstSortedScore.scoreSnapshot?.scoreTime ?? -1f;
|
|
1290
|
+
float lastTime = lastSortedScore.scoreSnapshot?.scoreTime ?? -1f;
|
|
1291
|
+
HyperDebug.Log(
|
|
1292
|
+
$"[Score] Sorted score batch by time ascending. " +
|
|
1293
|
+
$"FirstTime={firstTime}, LastTime={lastTime}, " +
|
|
1294
|
+
$"FirstCumulativeScore={firstSortedScore.cumulativeScore}, LastCumulativeScore={lastSortedScore.cumulativeScore}, " +
|
|
1295
|
+
$"BatchSize={scores.Count}"
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1042
1299
|
// Don't send if socket is not connected - prevents sending with expired tokens
|
|
1043
1300
|
// and ensures we wait for reconnection to get fresh token via SendGameResumed()
|
|
1044
1301
|
if (!isSocketConnected)
|
|
@@ -1048,6 +1305,27 @@ namespace Hyper.Internal.Managers
|
|
|
1048
1305
|
return;
|
|
1049
1306
|
}
|
|
1050
1307
|
|
|
1308
|
+
if (string.IsNullOrEmpty(_csrfToken))
|
|
1309
|
+
{
|
|
1310
|
+
int lastScore = scores[scores.Count - 1].cumulativeScore;
|
|
1311
|
+
HyperDebug.LogWarning($"[Score] Cannot send scores: CSRF token unavailable. LastBatchCumulativeScore={lastScore} [ErrorType=CSRFTokenError]");
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (_scoreBatchSendInProgress)
|
|
1316
|
+
{
|
|
1317
|
+
HyperDebug.LogWarning("[Score] Overlapping SendScoresOverWebSocket — re-queueing batch at front of pending.");
|
|
1318
|
+
if (_dataManager == null) _dataManager = HyperRuntime.DataManager;
|
|
1319
|
+
if (_dataManager?.PendingScores != null && scores.Count > 0)
|
|
1320
|
+
{
|
|
1321
|
+
_dataManager.PendingScores.InsertRange(0, scores);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
scores.Clear();
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
_scoreBatchSendInProgress = true;
|
|
1051
1329
|
try
|
|
1052
1330
|
{
|
|
1053
1331
|
// Clone the batch into standalone ScoreData instances
|
|
@@ -1075,12 +1353,27 @@ namespace Hyper.Internal.Managers
|
|
|
1075
1353
|
return;
|
|
1076
1354
|
}
|
|
1077
1355
|
|
|
1078
|
-
|
|
1356
|
+
string tokenUsedForSend = _csrfToken;
|
|
1357
|
+
var message = new WebSocketScoreMessage(_cachedSessionId, tokenUsedForSend, encryptedData);
|
|
1079
1358
|
string json = JsonConvert.SerializeObject(message);
|
|
1080
|
-
int csrfLength = string.IsNullOrEmpty(
|
|
1359
|
+
int csrfLength = string.IsNullOrEmpty(tokenUsedForSend) ? 0 : tokenUsedForSend.Length;
|
|
1081
1360
|
int lastCumulativeScore = scores[scores.Count - 1].cumulativeScore;
|
|
1082
1361
|
HyperDebug.Log($"[Score] Sending score batch. LastCumulativeScore={lastCumulativeScore}, BatchSize={scores.Count}, SessionId={_cachedSessionId}, CsrfLength={csrfLength}");
|
|
1083
1362
|
SendWebSocketMessage(json);
|
|
1363
|
+
|
|
1364
|
+
// Strict token-gated flow: invalidate only the token we sent. If scoreUpdated ran
|
|
1365
|
+
// re-entrantly during SendWebSocketMessage (WebGL) and refreshed _csrfToken, do not wipe it.
|
|
1366
|
+
if (string.Equals(_csrfToken, tokenUsedForSend, StringComparison.Ordinal))
|
|
1367
|
+
{
|
|
1368
|
+
_csrfToken = null;
|
|
1369
|
+
_isWaitingForTokenLogEmitted = false;
|
|
1370
|
+
HyperDebug.Log("[CSRF] Token invalidated after score batch send. Awaiting fresh token from scoreUpdated.");
|
|
1371
|
+
}
|
|
1372
|
+
else
|
|
1373
|
+
{
|
|
1374
|
+
_isWaitingForTokenLogEmitted = false;
|
|
1375
|
+
HyperDebug.Log("[CSRF] Token refreshed during batch send; preserving new token (re-entrancy-safe).");
|
|
1376
|
+
}
|
|
1084
1377
|
}
|
|
1085
1378
|
catch (Exception ex)
|
|
1086
1379
|
{
|
|
@@ -1089,6 +1382,7 @@ namespace Hyper.Internal.Managers
|
|
|
1089
1382
|
}
|
|
1090
1383
|
finally
|
|
1091
1384
|
{
|
|
1385
|
+
_scoreBatchSendInProgress = false;
|
|
1092
1386
|
if (_dataManager == null) _dataManager = HyperRuntime.DataManager;
|
|
1093
1387
|
_dataManager?.ReturnScoreDataToPool(scores);
|
|
1094
1388
|
scores?.Clear();
|
|
@@ -1210,7 +1504,7 @@ namespace Hyper.Internal.Managers
|
|
|
1210
1504
|
string gameSlug = _gameManager.Config.GameSlug;
|
|
1211
1505
|
|
|
1212
1506
|
#if UNITY_WEBGL && !UNITY_EDITOR
|
|
1213
|
-
FetchAndCacheTutorials(gameSlug, gameObject.name);
|
|
1507
|
+
FetchAndCacheTutorials(GetServerUrl(), gameSlug, gameObject.name);
|
|
1214
1508
|
#else
|
|
1215
1509
|
StartCoroutine(SimulateTutorialFetch());
|
|
1216
1510
|
#endif
|
|
@@ -134,6 +134,7 @@ namespace Hyper.Internal.Managers
|
|
|
134
134
|
internal TutorialData TutorialData { get; private set; }
|
|
135
135
|
internal bool HasReferencedTutorialResources { get; private set; }
|
|
136
136
|
private const int SCORE_POOL_CAPACITY = 15;
|
|
137
|
+
private bool _gameSessionEndAudioMuted;
|
|
137
138
|
|
|
138
139
|
internal void ConfigurePlayerSoundSetting()
|
|
139
140
|
{
|
|
@@ -141,8 +142,28 @@ namespace Hyper.Internal.Managers
|
|
|
141
142
|
if (_gameManager == null) _gameManager = HyperRuntime.GameManager;
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
/// <summary>
|
|
146
|
+
/// Mutes all game audio when a session ends (global listener). Idempotent until restored.
|
|
147
|
+
/// </summary>
|
|
148
|
+
internal void MuteAudioForGameSessionEnd()
|
|
149
|
+
{
|
|
150
|
+
if (_gameSessionEndAudioMuted) return;
|
|
151
|
+
_gameSessionEndAudioMuted = true;
|
|
152
|
+
AudioListener.volume = 0f;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// <summary>
|
|
156
|
+
/// Restores listener volume from the player's sound preference (0 or 1). Used after custom restart.
|
|
157
|
+
/// </summary>
|
|
158
|
+
internal void RestoreAudioAfterGameSessionEnd()
|
|
159
|
+
{
|
|
160
|
+
if (!_gameSessionEndAudioMuted) return;
|
|
161
|
+
SetSound(GetSound());
|
|
162
|
+
}
|
|
163
|
+
|
|
144
164
|
internal void SetSound(int sound)
|
|
145
165
|
{
|
|
166
|
+
_gameSessionEndAudioMuted = false;
|
|
146
167
|
PlayerPrefs.SetInt(HyperConstants.SOUND_KEY, sound);
|
|
147
168
|
_eventManager.InvokeSoundChangedEvent(sound == 1);
|
|
148
169
|
AudioListener.volume = sound;
|
|
@@ -40,6 +40,7 @@ namespace Hyper.Internal.Managers
|
|
|
40
40
|
internal event Action OnStartDeadlineReached;
|
|
41
41
|
internal event Action OnInternetConnectionLost;
|
|
42
42
|
internal event Action OnInternetConnectionFound;
|
|
43
|
+
internal event Action OnBackendSocketManuallyClosed;
|
|
43
44
|
internal event Action OnTutorialFetchComplete;
|
|
44
45
|
internal event Action OnTutorialResourcesReferenced;
|
|
45
46
|
internal event Action OnTutorialLoadingComplete;
|
|
@@ -81,6 +82,7 @@ namespace Hyper.Internal.Managers
|
|
|
81
82
|
internal void InvokeCustomRestartEvent() => OnCustomRestart?.Invoke();
|
|
82
83
|
internal void InvokeConnectionLostEvent() => OnInternetConnectionLost?.Invoke();
|
|
83
84
|
internal void InvokeConnectionFoundEvent() => OnInternetConnectionFound?.Invoke();
|
|
85
|
+
internal void InvokeBackendSocketManuallyClosedEvent() => OnBackendSocketManuallyClosed?.Invoke();
|
|
84
86
|
internal void InvokeTutorialLoadingCompleteEvent() => OnTutorialLoadingComplete?.Invoke();
|
|
85
87
|
internal void InvokeTutorialResourcesReferencedEvent() => OnTutorialResourcesReferenced?.Invoke();
|
|
86
88
|
internal void InvokeTutorialFetchCompleteEvent() => OnTutorialFetchComplete?.Invoke();
|
|
@@ -120,6 +120,7 @@ namespace Hyper.Internal.Managers
|
|
|
120
120
|
if (IsBattleMode) return;
|
|
121
121
|
|
|
122
122
|
_hasGameEnded = true;
|
|
123
|
+
_dataManager.MuteAudioForGameSessionEnd();
|
|
123
124
|
HyperRuntime.Timer.StopAllTimers();
|
|
124
125
|
|
|
125
126
|
if (!HyperRuntime.GameContent.IsPracticeMenu)
|
|
@@ -845,6 +846,7 @@ namespace Hyper.Internal.Managers
|
|
|
845
846
|
|
|
846
847
|
HyperRuntime.Timer.Reset();
|
|
847
848
|
HyperRuntime.DataManager.UpdateScore(0);
|
|
849
|
+
HyperRuntime.DataManager.RestoreAudioAfterGameSessionEnd();
|
|
848
850
|
|
|
849
851
|
ShowControlBarElements();
|
|
850
852
|
ShowStartingInstructionModal();
|
|
@@ -857,6 +859,7 @@ namespace Hyper.Internal.Managers
|
|
|
857
859
|
private void EndGameInternal()
|
|
858
860
|
{
|
|
859
861
|
_hasGameEnded = true;
|
|
862
|
+
_dataManager.MuteAudioForGameSessionEnd();
|
|
860
863
|
|
|
861
864
|
_eventManager.InvokeGameEndedEvent();
|
|
862
865
|
HyperRuntime.Timer.StopAllTimers();
|
|
@@ -1393,6 +1396,7 @@ namespace Hyper.Internal.Managers
|
|
|
1393
1396
|
private void ShowTimeUpModalInternal()
|
|
1394
1397
|
{
|
|
1395
1398
|
_hasGameEnded = true;
|
|
1399
|
+
_dataManager.MuteAudioForGameSessionEnd();
|
|
1396
1400
|
|
|
1397
1401
|
if (_timeUpModal == null)
|
|
1398
1402
|
{
|