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.
@@ -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; } = 4;
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
- username = _gameManager?.Config.Username,
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 && isSocketConnected)
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
- TryUpdateCSRFToken(message);
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.Last();
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 (_isFinalScoreSubmission)
575
+ if (_isWaitingForFinalScoreResponse)
534
576
  {
577
+ HyperDebug.Log("[Score] Final score response received from server.");
535
578
  _finalScoreSubmitted = true;
536
- _isFinalScoreSubmission = false;
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
- #if UNITY_WEBGL && !UNITY_EDITOR
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
- HyperDebug.LogError("Encryption failed");
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
- HyperDebug.LogError("WebSocket not connected - cannot submit final score");
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
- _isFinalScoreSubmission = true;
731
- _lastSentBatch = new List<ScoreData> { finalScoreData };
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
- HyperDebug.LogError($"Failed to send final score via WebSocket: {ex.Message}");
743
- _isFinalScoreSubmission = false;
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
- _isFinalScoreSubmission = false;
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
- _isFinalScoreSubmission = false;
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
- _isFinalScoreSubmission = false;
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
- _isFinalScoreSubmission = false;
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
- private List<ScoreData> _lastSentBatch = new List<ScoreData>();
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 _isFinalScoreSubmission;
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
- List<ScoreData> scoresToSend = null;
989
+ var scoresToSend = _scoreSendBuffer;
990
+ scoresToSend.Clear();
862
991
 
863
- try
992
+ if (_dataManager?.PendingScores.Count > 0)
864
993
  {
865
- if (_dataManager?.PendingScores.Count > 0)
866
- {
867
- scoresToSend = new List<ScoreData>(_dataManager?.PendingScores);
868
- _dataManager?.PendingScores.Clear();
869
- SendScoresOverWebSocket(scoresToSend);
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
- finally
1000
+ else
873
1001
  {
874
- scoresToSend?.Clear();
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
- _lastSentBatch = new List<ScoreData>(scores);
889
- var scoreData = scores.Last();
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
- HyperDebug.LogError("Encryption failed");
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
- HyperDebug.LogError($"Failed to send scores via WebSocket: {ex.Message}");
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
  }