app.hypergames.hypersdk 1.4.21 → 1.5.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 +14 -0
- package/Editor/Scripts/Inspectors/HyperSDKSettingsEditor.cs +48 -12
- package/Editor/Scripts/Tools/HyperBuildAssistant.cs +33 -9
- package/Editor/Scripts/Tools/HyperRandomMigrationTool.cs +122 -5
- package/HyperLoader.unity +5 -5
- package/Runtime/Internal/Bootstrap/HyperLoader.cs +29 -24
- package/Runtime/Internal/Managers/BackendManager.cs +247 -58
- package/Runtime/Internal/Managers/DataManager.cs +128 -35
- package/Runtime/Internal/Managers/GameManager.cs +0 -5
- package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +3 -0
- package/Runtime/Internal/UI/Components/ControlBar.cs +9 -1
- package/Runtime/Internal/UI/Components/LivesDisplay.cs +6 -7
- package/Runtime/Internal/UI/Components/TimerText.cs +1 -1
- package/Runtime/Internal/UI/Modals/GameOverModal.cs +6 -1
- package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +50 -20
- package/Runtime/Resources/HyperSDKSettings.asset +1 -0
- package/Runtime/Resources/UIPrefabs/StartingInstructionModal.prefab +4 -4
- package/Runtime/Shared/DTOs/Enums.cs +26 -0
- package/Runtime/Shared/Hyper.Shared.asmdef +3 -1
- package/Runtime/Shared/Utils/HyperDebug.cs +17 -7
- package/Runtime/Shared/Utils/HyperNumberFormatter.cs +67 -0
- package/Runtime/Shared/Utils/HyperNumberFormatter.cs.meta +2 -0
- package/package.json +1 -1
|
@@ -42,13 +42,28 @@ namespace Hyper.Internal.Managers
|
|
|
42
42
|
|
|
43
43
|
#region Public Properties (Internal API)
|
|
44
44
|
|
|
45
|
-
public int scoreSubmissionTimeout { get; private set; } =
|
|
45
|
+
public int scoreSubmissionTimeout { get; private set; } = 6;
|
|
46
46
|
public int CurrentRetryCount => _currentRetryCount;
|
|
47
47
|
public bool isSocketConnected { get; private set; }
|
|
48
48
|
public bool tutorialFetchSuccess { get; private set; }
|
|
49
49
|
public bool tutorialExistsButFailed { get; private set; }
|
|
50
50
|
public bool tutorialDoesNotExist => !tutorialExistsButFailed && !tutorialFetchSuccess && !_isFetchingTutorial;
|
|
51
51
|
|
|
52
|
+
/// <summary>
|
|
53
|
+
/// Type of error that occurred during score submission.
|
|
54
|
+
/// </summary>
|
|
55
|
+
public enum SubmissionErrorType
|
|
56
|
+
{
|
|
57
|
+
None,
|
|
58
|
+
NetworkError,
|
|
59
|
+
TimeoutError,
|
|
60
|
+
CSRFTokenError,
|
|
61
|
+
ServerError,
|
|
62
|
+
EncryptionError
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public SubmissionErrorType LastSubmissionError { get; private set; } = SubmissionErrorType.None;
|
|
66
|
+
|
|
52
67
|
#endregion
|
|
53
68
|
|
|
54
69
|
#region Private Configuration
|
|
@@ -156,7 +171,7 @@ namespace Hyper.Internal.Managers
|
|
|
156
171
|
entryId = _gameManager?.Config.EntryId,
|
|
157
172
|
host = _gameManager?.Config.Host,
|
|
158
173
|
time = _gameManager?.Config.TimeLimit,
|
|
159
|
-
|
|
174
|
+
userName = _gameManager?.Config.Username,
|
|
160
175
|
userId = _gameManager?.Config.UserId,
|
|
161
176
|
pvp = _gameManager?.Config.isPVP,
|
|
162
177
|
oldTournamentId = _gameManager?.Config.OldTournamentId
|
|
@@ -299,6 +314,7 @@ namespace Hyper.Internal.Managers
|
|
|
299
314
|
public void StopWebSocket()
|
|
300
315
|
{
|
|
301
316
|
_isManuallyClosed = true;
|
|
317
|
+
isSocketConnected = false;
|
|
302
318
|
CloseWebSocket();
|
|
303
319
|
}
|
|
304
320
|
|
|
@@ -349,7 +365,15 @@ namespace Hyper.Internal.Managers
|
|
|
349
365
|
|
|
350
366
|
if (string.IsNullOrEmpty(_cachedSessionId))
|
|
351
367
|
{
|
|
352
|
-
HyperDebug.LogError("No valid session ID – cannot send resumeGame");
|
|
368
|
+
HyperDebug.LogError("[ResumeGame] No valid session ID – cannot send resumeGame");
|
|
369
|
+
_sendGameResumedCoroutine = null;
|
|
370
|
+
yield break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Don't send if socket is not connected - prevents wasting retry attempts
|
|
374
|
+
if (!isSocketConnected)
|
|
375
|
+
{
|
|
376
|
+
HyperDebug.LogWarning("[ResumeGame] Cannot send gameResumed: WebSocket not connected");
|
|
353
377
|
_sendGameResumedCoroutine = null;
|
|
354
378
|
yield break;
|
|
355
379
|
}
|
|
@@ -373,12 +397,12 @@ namespace Hyper.Internal.Managers
|
|
|
373
397
|
};
|
|
374
398
|
|
|
375
399
|
string json = JsonConvert.SerializeObject(message);
|
|
376
|
-
HyperDebug.Log($"Sending game resumed message: {json}");
|
|
400
|
+
HyperDebug.Log($"[ResumeGame] Sending game resumed message (attempt {attempts}/{maxAttempts}): {json}");
|
|
377
401
|
SendWebSocketMessage(json);
|
|
378
402
|
}
|
|
379
403
|
catch (Exception ex)
|
|
380
404
|
{
|
|
381
|
-
HyperDebug.LogError($"SendGameResumed attempt {attempts} failed: {ex.Message}");
|
|
405
|
+
HyperDebug.LogError($"[ResumeGame] SendGameResumed attempt {attempts} failed: {ex.Message}");
|
|
382
406
|
}
|
|
383
407
|
|
|
384
408
|
float elapsed = 0f;
|
|
@@ -389,6 +413,11 @@ namespace Hyper.Internal.Managers
|
|
|
389
413
|
yield return null;
|
|
390
414
|
elapsed += Time.unscaledDeltaTime;
|
|
391
415
|
}
|
|
416
|
+
|
|
417
|
+
if (!_gameResumed)
|
|
418
|
+
{
|
|
419
|
+
HyperDebug.LogWarning($"[ResumeGame] No gameResumed acknowledgement from server within {timeout} seconds (attempt {attempts}/{maxAttempts}).");
|
|
420
|
+
}
|
|
392
421
|
}
|
|
393
422
|
|
|
394
423
|
yield return new WaitForSecondsRealtime(1.5f);
|
|
@@ -454,6 +483,18 @@ namespace Hyper.Internal.Managers
|
|
|
454
483
|
|
|
455
484
|
public void OnSocketConnected()
|
|
456
485
|
{
|
|
486
|
+
if (HyperRuntime.Timer.SessionTimeRemaining <= 0)
|
|
487
|
+
{
|
|
488
|
+
HyperDebug.LogWarning("Session time remaining is 0 - closing WebSocket connection");
|
|
489
|
+
CloseWebSocket();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Mark connected before any follow-up actions (resumeGame, UI updates, etc.)
|
|
494
|
+
isSocketConnected = true;
|
|
495
|
+
_isManuallyClosed = false;
|
|
496
|
+
_isSocketErrorCalled = false;
|
|
497
|
+
|
|
457
498
|
if (_gameManager.HasGameStarted)
|
|
458
499
|
{
|
|
459
500
|
_gameManager?.ShowConnectionRestored();
|
|
@@ -463,20 +504,18 @@ namespace Hyper.Internal.Managers
|
|
|
463
504
|
{
|
|
464
505
|
_eventManager?.InvokeConnectionFoundEvent();
|
|
465
506
|
}
|
|
466
|
-
|
|
467
|
-
isSocketConnected = true;
|
|
468
|
-
_isManuallyClosed = false;
|
|
469
|
-
_isSocketErrorCalled = false;
|
|
470
507
|
}
|
|
471
508
|
|
|
472
509
|
public void OnSocketDisconnected()
|
|
473
510
|
{
|
|
511
|
+
isSocketConnected = false;
|
|
512
|
+
|
|
474
513
|
if (!_isManuallyClosed)
|
|
475
514
|
{
|
|
476
515
|
_gameManager?.ShowConnectionLost();
|
|
477
516
|
}
|
|
478
517
|
|
|
479
|
-
if (!_gameManager.HasGameStarted
|
|
518
|
+
if (!_gameManager.HasGameStarted)
|
|
480
519
|
{
|
|
481
520
|
_isSocketErrorCalled = true;
|
|
482
521
|
_gameManager?.ShowLoadingFailedModal(true);
|
|
@@ -506,14 +545,16 @@ namespace Hyper.Internal.Managers
|
|
|
506
545
|
_sendGameStartedCoroutine = null;
|
|
507
546
|
}
|
|
508
547
|
|
|
509
|
-
|
|
548
|
+
HyperDebug.Log("[WebSocket] Received gameStarted event from server.");
|
|
549
|
+
TryUpdateCSRFToken(message, "gameStarted");
|
|
510
550
|
}
|
|
511
551
|
|
|
512
552
|
public void OnSocketGameResumed(string message)
|
|
513
553
|
{
|
|
514
554
|
_gameResumed = true;
|
|
555
|
+
HyperDebug.Log("[WebSocket] Received gameResumed event from server.");
|
|
515
556
|
|
|
516
|
-
TryUpdateCSRFToken(message);
|
|
557
|
+
TryUpdateCSRFToken(message, "gameResumed");
|
|
517
558
|
ResumeScoreSending();
|
|
518
559
|
}
|
|
519
560
|
|
|
@@ -526,20 +567,23 @@ namespace Hyper.Internal.Managers
|
|
|
526
567
|
{
|
|
527
568
|
if (_lastSentBatch != null && _lastSentBatch.Count > 0)
|
|
528
569
|
{
|
|
529
|
-
_lastScoreData = _lastSentBatch.
|
|
570
|
+
_lastScoreData = _lastSentBatch[_lastSentBatch.Count - 1];
|
|
571
|
+
HyperDebug.Log($"[Score] Server acknowledged score update. CumulativeScore={_lastScoreData.cumulativeScore}, IsFinal={_lastScoreData.isFinal}");
|
|
530
572
|
_eventManager?.InvokeWebSocketScoreSubmitted(_lastScoreData);
|
|
531
573
|
}
|
|
532
574
|
|
|
533
|
-
if (
|
|
575
|
+
if (_isWaitingForFinalScoreResponse)
|
|
534
576
|
{
|
|
577
|
+
HyperDebug.Log("[Score] Final score response received from server.");
|
|
535
578
|
_finalScoreSubmitted = true;
|
|
536
|
-
|
|
579
|
+
_isWaitingForFinalScoreResponse = false;
|
|
580
|
+
return;
|
|
537
581
|
}
|
|
538
582
|
|
|
539
|
-
TryUpdateCSRFToken(message);
|
|
583
|
+
TryUpdateCSRFToken(message, "scoreUpdated");
|
|
540
584
|
}
|
|
541
585
|
|
|
542
|
-
private void TryUpdateCSRFToken(string json)
|
|
586
|
+
private void TryUpdateCSRFToken(string json, string context = null)
|
|
543
587
|
{
|
|
544
588
|
try
|
|
545
589
|
{
|
|
@@ -547,11 +591,17 @@ namespace Hyper.Internal.Managers
|
|
|
547
591
|
if (parsed.TryGetValue("csrfToken", out object tokenObj))
|
|
548
592
|
{
|
|
549
593
|
_csrfToken = tokenObj.ToString();
|
|
594
|
+
int tokenLength = string.IsNullOrEmpty(_csrfToken) ? 0 : _csrfToken.Length;
|
|
595
|
+
HyperDebug.Log($"[CSRF] Token updated from '{context ?? "unknown"}'. Length={tokenLength}");
|
|
596
|
+
}
|
|
597
|
+
else
|
|
598
|
+
{
|
|
599
|
+
HyperDebug.LogWarning($"[CSRF] No csrfToken field present in message when updating from '{context ?? "unknown"}'.");
|
|
550
600
|
}
|
|
551
601
|
}
|
|
552
602
|
catch (Exception ex)
|
|
553
603
|
{
|
|
554
|
-
HyperDebug.LogError($"Failed to parse CSRF token: {ex.Message}");
|
|
604
|
+
HyperDebug.LogError($"[CSRF] Failed to parse CSRF token from '{context ?? "unknown"}': {ex.Message}");
|
|
555
605
|
}
|
|
556
606
|
}
|
|
557
607
|
|
|
@@ -663,27 +713,67 @@ namespace Hyper.Internal.Managers
|
|
|
663
713
|
|
|
664
714
|
_finalScoreSubmissionCoroutine = null;
|
|
665
715
|
|
|
666
|
-
|
|
667
|
-
_finalScoreSubmissionCoroutine = StartCoroutine(FinalScoreSubmissionCoroutine());
|
|
668
|
-
#else
|
|
669
|
-
_finalScoreSubmissionCoroutine = StartCoroutine(FinalScoreSubmissionCoroutineDemo());
|
|
670
|
-
#endif
|
|
716
|
+
SubmitScore();
|
|
671
717
|
}
|
|
672
718
|
|
|
673
719
|
private IEnumerator FinalScoreSubmissionCoroutine()
|
|
674
720
|
{
|
|
675
721
|
_gameManager?.GameOverModalComponent?.Init();
|
|
676
722
|
_currentRetryCount = 0;
|
|
723
|
+
LastSubmissionError = SubmissionErrorType.None;
|
|
724
|
+
|
|
725
|
+
// 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
|
+
if (!isSocketConnected)
|
|
731
|
+
{
|
|
732
|
+
HyperDebug.LogWarning("WebSocket disconnected at game end - attempting to reconnect before submission");
|
|
733
|
+
InitializeWebSocket();
|
|
734
|
+
|
|
735
|
+
// Wait a bit for connection to establish
|
|
736
|
+
float reconnectWait = 0f;
|
|
737
|
+
const float maxReconnectWait = 3f;
|
|
738
|
+
while (!isSocketConnected && reconnectWait < maxReconnectWait)
|
|
739
|
+
{
|
|
740
|
+
yield return new WaitForSecondsRealtime(0.1f);
|
|
741
|
+
reconnectWait += 0.1f;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (isSocketConnected)
|
|
745
|
+
{
|
|
746
|
+
HyperDebug.Log("Connection restored before score submission");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
else
|
|
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
|
|
753
|
+
HyperDebug.Log("Connection active - maintaining during game end sequence");
|
|
754
|
+
}
|
|
677
755
|
|
|
678
756
|
while (_currentRetryCount < MAX_RETRY_COUNT)
|
|
679
757
|
{
|
|
680
758
|
if (_hasSessionEnded)
|
|
681
759
|
{
|
|
760
|
+
LastSubmissionError = SubmissionErrorType.NetworkError;
|
|
682
761
|
_gameManager?.ShowRetryTimeExpiredModal();
|
|
683
762
|
_finalScoreSubmissionCoroutine = null;
|
|
684
763
|
yield break;
|
|
685
764
|
}
|
|
686
765
|
|
|
766
|
+
// Check if we received a late response during retry wait
|
|
767
|
+
// This handles the case where backend responds after timeout but before next retry
|
|
768
|
+
if (_finalScoreSubmitted)
|
|
769
|
+
{
|
|
770
|
+
LastSubmissionError = SubmissionErrorType.None;
|
|
771
|
+
_gameManager?.GameOverModalComponent?.ScoreHasBeenSubmitted();
|
|
772
|
+
StopWebSocket();
|
|
773
|
+
_finalScoreSubmissionCoroutine = null;
|
|
774
|
+
yield break;
|
|
775
|
+
}
|
|
776
|
+
|
|
687
777
|
var finalScoreData = _dataManager?.CurrentScoreData;
|
|
688
778
|
finalScoreData.isFinal = true;
|
|
689
779
|
|
|
@@ -694,17 +784,19 @@ namespace Hyper.Internal.Managers
|
|
|
694
784
|
|
|
695
785
|
if (string.IsNullOrEmpty(encryptedScoreData))
|
|
696
786
|
{
|
|
697
|
-
|
|
787
|
+
LastSubmissionError = SubmissionErrorType.EncryptionError;
|
|
788
|
+
HyperDebug.LogError("Encryption failed [ENCRYPTION_ERROR]");
|
|
698
789
|
yield break;
|
|
699
790
|
}
|
|
700
791
|
|
|
792
|
+
// Reset gameResumed flag for this attempt
|
|
793
|
+
_gameResumed = false;
|
|
794
|
+
|
|
795
|
+
// Ensure game is resumed to get new CSRF token before final score submission
|
|
701
796
|
if (_sendGameResumedCoroutine == null)
|
|
702
797
|
{
|
|
703
798
|
_sendGameResumedCoroutine = StartCoroutine(SendGameResumedCoroutine());
|
|
704
|
-
|
|
705
|
-
//? Ensure game is resumed to get new CSRF token before final score submission
|
|
706
799
|
yield return _sendGameResumedCoroutine;
|
|
707
|
-
|
|
708
800
|
_sendGameResumedCoroutine = null;
|
|
709
801
|
}
|
|
710
802
|
else
|
|
@@ -714,7 +806,8 @@ namespace Hyper.Internal.Managers
|
|
|
714
806
|
|
|
715
807
|
if (!isSocketConnected)
|
|
716
808
|
{
|
|
717
|
-
|
|
809
|
+
LastSubmissionError = SubmissionErrorType.NetworkError;
|
|
810
|
+
HyperDebug.LogError("WebSocket not connected - cannot submit final score [NETWORK_ERROR]");
|
|
718
811
|
_currentRetryCount++;
|
|
719
812
|
_gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
|
|
720
813
|
|
|
@@ -725,10 +818,32 @@ namespace Hyper.Internal.Managers
|
|
|
725
818
|
continue;
|
|
726
819
|
}
|
|
727
820
|
|
|
821
|
+
// Verify that gameResumed succeeded (CSRF token was refreshed)
|
|
822
|
+
// If _gameResumed is still false, the CSRF token wasn't refreshed and submission will fail
|
|
823
|
+
if (!_gameResumed)
|
|
824
|
+
{
|
|
825
|
+
LastSubmissionError = SubmissionErrorType.CSRFTokenError;
|
|
826
|
+
HyperDebug.LogWarning("gameResumed did not succeed - CSRF token may be stale. Retrying entire submission process (including gameResumed)... [CSRF_TOKEN_ERROR]");
|
|
827
|
+
_currentRetryCount++;
|
|
828
|
+
_gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
|
|
829
|
+
|
|
830
|
+
if (_currentRetryCount < MAX_RETRY_COUNT)
|
|
831
|
+
{
|
|
832
|
+
yield return new WaitForSecondsRealtime(1f);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
|
|
728
838
|
// Sending final score via WebSocket
|
|
729
839
|
_finalScoreSubmitted = false;
|
|
730
|
-
|
|
731
|
-
|
|
840
|
+
_isWaitingForFinalScoreResponse = true;
|
|
841
|
+
|
|
842
|
+
_lastSentBatch.Clear();
|
|
843
|
+
if (finalScoreData != null)
|
|
844
|
+
{
|
|
845
|
+
_lastSentBatch.Add(finalScoreData);
|
|
846
|
+
}
|
|
732
847
|
|
|
733
848
|
bool sendFailed = false;
|
|
734
849
|
try
|
|
@@ -739,8 +854,10 @@ namespace Hyper.Internal.Managers
|
|
|
739
854
|
}
|
|
740
855
|
catch (Exception ex)
|
|
741
856
|
{
|
|
742
|
-
|
|
743
|
-
|
|
857
|
+
LastSubmissionError = SubmissionErrorType.NetworkError;
|
|
858
|
+
HyperDebug.LogError($"Failed to send final score via WebSocket: {ex.Message} [NETWORK_ERROR]");
|
|
859
|
+
// Keep _isWaitingForFinalScoreResponse = true since we're still in final score submission mode
|
|
860
|
+
// and will retry. We're still waiting for a final score response.
|
|
744
861
|
sendFailed = true;
|
|
745
862
|
}
|
|
746
863
|
|
|
@@ -765,7 +882,8 @@ namespace Hyper.Internal.Managers
|
|
|
765
882
|
if (_hasSessionEnded)
|
|
766
883
|
{
|
|
767
884
|
_gameManager?.ShowRetryTimeExpiredModal();
|
|
768
|
-
|
|
885
|
+
// Session ended - can't retry anymore, so set to false
|
|
886
|
+
_isWaitingForFinalScoreResponse = false;
|
|
769
887
|
_finalScoreSubmissionCoroutine = null;
|
|
770
888
|
yield break;
|
|
771
889
|
}
|
|
@@ -783,13 +901,18 @@ namespace Hyper.Internal.Managers
|
|
|
783
901
|
else if (_hasSessionEnded)
|
|
784
902
|
{
|
|
785
903
|
_gameManager?.ShowRetryTimeExpiredModal();
|
|
786
|
-
|
|
904
|
+
// Session ended - can't retry anymore, so set to false
|
|
905
|
+
_isWaitingForFinalScoreResponse = false;
|
|
787
906
|
_finalScoreSubmissionCoroutine = null;
|
|
788
907
|
yield break;
|
|
789
908
|
}
|
|
790
909
|
else
|
|
791
910
|
{
|
|
792
|
-
|
|
911
|
+
// Timeout occurred, but keep _isWaitingForFinalScoreResponse = true
|
|
912
|
+
// because we're still in final score submission mode and will retry.
|
|
913
|
+
// If backend responds during retry wait, we'll catch it.
|
|
914
|
+
LastSubmissionError = SubmissionErrorType.TimeoutError;
|
|
915
|
+
HyperDebug.LogWarning($"Score submission timeout after {timeout} seconds [TIMEOUT_ERROR]");
|
|
793
916
|
_currentRetryCount++;
|
|
794
917
|
_gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
|
|
795
918
|
|
|
@@ -800,7 +923,9 @@ namespace Hyper.Internal.Managers
|
|
|
800
923
|
}
|
|
801
924
|
}
|
|
802
925
|
|
|
803
|
-
|
|
926
|
+
// All retries exhausted - log the final error type for debugging
|
|
927
|
+
HyperDebug.LogError($"Score submission failed after {MAX_RETRY_COUNT} attempts. Error type: {LastSubmissionError}");
|
|
928
|
+
_isWaitingForFinalScoreResponse = false;
|
|
804
929
|
_gameManager?.GameOverModalComponent?.AllAttemptsHaveFailed();
|
|
805
930
|
_finalScoreSubmissionCoroutine = null;
|
|
806
931
|
}
|
|
@@ -828,10 +953,13 @@ namespace Hyper.Internal.Managers
|
|
|
828
953
|
#region Score Submission (WebSocket)
|
|
829
954
|
|
|
830
955
|
private Coroutine _scoreSenderCoroutine;
|
|
831
|
-
|
|
956
|
+
// Stores standalone copies of the last batch that was sent over the socket.
|
|
957
|
+
// We explicitly clone these so they are not affected by the DataManager's object pooling.
|
|
958
|
+
private readonly List<ScoreData> _lastSentBatch = new List<ScoreData>();
|
|
959
|
+
private readonly List<ScoreData> _scoreSendBuffer = new List<ScoreData>();
|
|
832
960
|
private ScoreData _lastScoreData;
|
|
833
961
|
private readonly WaitForSeconds _scoreSendInterval = new WaitForSeconds(1f);
|
|
834
|
-
private bool
|
|
962
|
+
private bool _isWaitingForFinalScoreResponse;
|
|
835
963
|
private bool _finalScoreSubmitted;
|
|
836
964
|
|
|
837
965
|
public void StartSendingScores()
|
|
@@ -858,21 +986,20 @@ namespace Hyper.Internal.Managers
|
|
|
858
986
|
{
|
|
859
987
|
while (true)
|
|
860
988
|
{
|
|
861
|
-
|
|
989
|
+
var scoresToSend = _scoreSendBuffer;
|
|
990
|
+
scoresToSend.Clear();
|
|
862
991
|
|
|
863
|
-
|
|
992
|
+
if (_dataManager?.PendingScores.Count > 0)
|
|
864
993
|
{
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
}
|
|
994
|
+
// Copy pending scores into our reusable buffer, then clear the source list
|
|
995
|
+
scoresToSend.AddRange(_dataManager?.PendingScores);
|
|
996
|
+
_dataManager?.PendingScores.Clear();
|
|
997
|
+
HyperDebug.Log($"[Score] Preparing to send {scoresToSend.Count} score sample(s) over WebSocket. LastPendingCumulativeScore={scoresToSend[scoresToSend.Count - 1].cumulativeScore}");
|
|
998
|
+
SendScoresOverWebSocket(scoresToSend);
|
|
871
999
|
}
|
|
872
|
-
|
|
1000
|
+
else
|
|
873
1001
|
{
|
|
874
|
-
|
|
875
|
-
scoresToSend = null;
|
|
1002
|
+
HyperDebug.Log("[Score] No pending scores to send in this tick.");
|
|
876
1003
|
}
|
|
877
1004
|
|
|
878
1005
|
yield return _scoreSendInterval;
|
|
@@ -883,10 +1010,70 @@ namespace Hyper.Internal.Managers
|
|
|
883
1010
|
{
|
|
884
1011
|
if (scores == null || scores.Count == 0) return;
|
|
885
1012
|
|
|
1013
|
+
// Don't send if socket is not connected - prevents sending with expired tokens
|
|
1014
|
+
// and ensures we wait for reconnection to get fresh token via SendGameResumed()
|
|
1015
|
+
if (!isSocketConnected)
|
|
1016
|
+
{
|
|
1017
|
+
int lastScore = scores[scores.Count - 1].cumulativeScore;
|
|
1018
|
+
HyperDebug.LogWarning($"[Score] Cannot send scores: WebSocket not connected. LastBatchCumulativeScore={lastScore} [ErrorType=NetworkError]");
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
886
1022
|
try
|
|
887
1023
|
{
|
|
888
|
-
|
|
889
|
-
|
|
1024
|
+
// Clone the last batch into standalone ScoreData instances
|
|
1025
|
+
// so that pooling/reset in DataManager does not zero out the values we log/use here.
|
|
1026
|
+
_lastSentBatch.Clear();
|
|
1027
|
+
|
|
1028
|
+
ScoreData scoreData = null;
|
|
1029
|
+
if (scores.Count > 0)
|
|
1030
|
+
{
|
|
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];
|
|
1076
|
+
}
|
|
890
1077
|
|
|
891
1078
|
string rawScoreJson = JsonConvert.SerializeObject(scoreData);
|
|
892
1079
|
var compressedScoreData = JsonCompressor.CompressJson(rawScoreJson);
|
|
@@ -895,25 +1082,27 @@ namespace Hyper.Internal.Managers
|
|
|
895
1082
|
|
|
896
1083
|
if (string.IsNullOrEmpty(encryptedData))
|
|
897
1084
|
{
|
|
898
|
-
|
|
1085
|
+
int lastScore = scoreData != null ? scoreData.cumulativeScore : -1;
|
|
1086
|
+
HyperDebug.LogError($"[Score] Encryption failed for per-second submission. CumulativeScore={lastScore} [ErrorType=EncryptionError]");
|
|
899
1087
|
return;
|
|
900
1088
|
}
|
|
901
1089
|
|
|
902
1090
|
var message = new WebSocketScoreMessage(_cachedSessionId, _csrfToken, encryptedData);
|
|
903
1091
|
string json = JsonConvert.SerializeObject(message);
|
|
1092
|
+
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}");
|
|
904
1095
|
SendWebSocketMessage(json);
|
|
905
|
-
|
|
906
|
-
#if UNITY_EDITOR
|
|
907
|
-
_lastScoreData = _lastSentBatch.Last();
|
|
908
|
-
_lastSentBatch.Clear();
|
|
909
|
-
#endif
|
|
910
1096
|
}
|
|
911
1097
|
catch (Exception ex)
|
|
912
1098
|
{
|
|
913
|
-
|
|
1099
|
+
int lastScore = scores[scores.Count - 1].cumulativeScore;
|
|
1100
|
+
HyperDebug.LogError($"[Score] Failed to send scores via WebSocket. LastBatchCumulativeScore={lastScore} [ErrorType=NetworkError, Exception={ex.Message}]");
|
|
914
1101
|
}
|
|
915
1102
|
finally
|
|
916
1103
|
{
|
|
1104
|
+
if (_dataManager == null) _dataManager = HyperRuntime.DataManager;
|
|
1105
|
+
_dataManager?.ReturnScoreDataToPool(scores);
|
|
917
1106
|
scores?.Clear();
|
|
918
1107
|
}
|
|
919
1108
|
}
|