app.hypergames.hypersdk 1.4.22 → 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 +7 -0
- package/Editor/Scripts/Inspectors/HyperSDKSettingsEditor.cs +48 -12
- package/Editor/Scripts/Tools/HyperBuildAssistant.cs +33 -9
- package/HyperLoader.unity +4 -4
- package/Runtime/Internal/Bootstrap/HyperLoader.cs +29 -24
- package/Runtime/Internal/Managers/BackendManager.cs +207 -40
- package/Runtime/Internal/Managers/DataManager.cs +128 -35
- 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
|
|
@@ -350,7 +365,7 @@ namespace Hyper.Internal.Managers
|
|
|
350
365
|
|
|
351
366
|
if (string.IsNullOrEmpty(_cachedSessionId))
|
|
352
367
|
{
|
|
353
|
-
HyperDebug.LogError("No valid session ID – cannot send resumeGame");
|
|
368
|
+
HyperDebug.LogError("[ResumeGame] No valid session ID – cannot send resumeGame");
|
|
354
369
|
_sendGameResumedCoroutine = null;
|
|
355
370
|
yield break;
|
|
356
371
|
}
|
|
@@ -358,7 +373,7 @@ namespace Hyper.Internal.Managers
|
|
|
358
373
|
// Don't send if socket is not connected - prevents wasting retry attempts
|
|
359
374
|
if (!isSocketConnected)
|
|
360
375
|
{
|
|
361
|
-
HyperDebug.LogWarning("Cannot send gameResumed: WebSocket not connected");
|
|
376
|
+
HyperDebug.LogWarning("[ResumeGame] Cannot send gameResumed: WebSocket not connected");
|
|
362
377
|
_sendGameResumedCoroutine = null;
|
|
363
378
|
yield break;
|
|
364
379
|
}
|
|
@@ -382,12 +397,12 @@ namespace Hyper.Internal.Managers
|
|
|
382
397
|
};
|
|
383
398
|
|
|
384
399
|
string json = JsonConvert.SerializeObject(message);
|
|
385
|
-
HyperDebug.Log($"Sending game resumed message: {json}");
|
|
400
|
+
HyperDebug.Log($"[ResumeGame] Sending game resumed message (attempt {attempts}/{maxAttempts}): {json}");
|
|
386
401
|
SendWebSocketMessage(json);
|
|
387
402
|
}
|
|
388
403
|
catch (Exception ex)
|
|
389
404
|
{
|
|
390
|
-
HyperDebug.LogError($"SendGameResumed attempt {attempts} failed: {ex.Message}");
|
|
405
|
+
HyperDebug.LogError($"[ResumeGame] SendGameResumed attempt {attempts} failed: {ex.Message}");
|
|
391
406
|
}
|
|
392
407
|
|
|
393
408
|
float elapsed = 0f;
|
|
@@ -398,6 +413,11 @@ namespace Hyper.Internal.Managers
|
|
|
398
413
|
yield return null;
|
|
399
414
|
elapsed += Time.unscaledDeltaTime;
|
|
400
415
|
}
|
|
416
|
+
|
|
417
|
+
if (!_gameResumed)
|
|
418
|
+
{
|
|
419
|
+
HyperDebug.LogWarning($"[ResumeGame] No gameResumed acknowledgement from server within {timeout} seconds (attempt {attempts}/{maxAttempts}).");
|
|
420
|
+
}
|
|
401
421
|
}
|
|
402
422
|
|
|
403
423
|
yield return new WaitForSecondsRealtime(1.5f);
|
|
@@ -463,6 +483,13 @@ namespace Hyper.Internal.Managers
|
|
|
463
483
|
|
|
464
484
|
public void OnSocketConnected()
|
|
465
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
|
+
|
|
466
493
|
// Mark connected before any follow-up actions (resumeGame, UI updates, etc.)
|
|
467
494
|
isSocketConnected = true;
|
|
468
495
|
_isManuallyClosed = false;
|
|
@@ -518,14 +545,16 @@ namespace Hyper.Internal.Managers
|
|
|
518
545
|
_sendGameStartedCoroutine = null;
|
|
519
546
|
}
|
|
520
547
|
|
|
521
|
-
|
|
548
|
+
HyperDebug.Log("[WebSocket] Received gameStarted event from server.");
|
|
549
|
+
TryUpdateCSRFToken(message, "gameStarted");
|
|
522
550
|
}
|
|
523
551
|
|
|
524
552
|
public void OnSocketGameResumed(string message)
|
|
525
553
|
{
|
|
526
554
|
_gameResumed = true;
|
|
555
|
+
HyperDebug.Log("[WebSocket] Received gameResumed event from server.");
|
|
527
556
|
|
|
528
|
-
TryUpdateCSRFToken(message);
|
|
557
|
+
TryUpdateCSRFToken(message, "gameResumed");
|
|
529
558
|
ResumeScoreSending();
|
|
530
559
|
}
|
|
531
560
|
|
|
@@ -538,21 +567,23 @@ namespace Hyper.Internal.Managers
|
|
|
538
567
|
{
|
|
539
568
|
if (_lastSentBatch != null && _lastSentBatch.Count > 0)
|
|
540
569
|
{
|
|
541
|
-
_lastScoreData = _lastSentBatch.
|
|
570
|
+
_lastScoreData = _lastSentBatch[_lastSentBatch.Count - 1];
|
|
571
|
+
HyperDebug.Log($"[Score] Server acknowledged score update. CumulativeScore={_lastScoreData.cumulativeScore}, IsFinal={_lastScoreData.isFinal}");
|
|
542
572
|
_eventManager?.InvokeWebSocketScoreSubmitted(_lastScoreData);
|
|
543
573
|
}
|
|
544
574
|
|
|
545
575
|
if (_isWaitingForFinalScoreResponse)
|
|
546
576
|
{
|
|
577
|
+
HyperDebug.Log("[Score] Final score response received from server.");
|
|
547
578
|
_finalScoreSubmitted = true;
|
|
548
579
|
_isWaitingForFinalScoreResponse = false;
|
|
549
580
|
return;
|
|
550
581
|
}
|
|
551
582
|
|
|
552
|
-
TryUpdateCSRFToken(message);
|
|
583
|
+
TryUpdateCSRFToken(message, "scoreUpdated");
|
|
553
584
|
}
|
|
554
585
|
|
|
555
|
-
private void TryUpdateCSRFToken(string json)
|
|
586
|
+
private void TryUpdateCSRFToken(string json, string context = null)
|
|
556
587
|
{
|
|
557
588
|
try
|
|
558
589
|
{
|
|
@@ -560,11 +591,17 @@ namespace Hyper.Internal.Managers
|
|
|
560
591
|
if (parsed.TryGetValue("csrfToken", out object tokenObj))
|
|
561
592
|
{
|
|
562
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"}'.");
|
|
563
600
|
}
|
|
564
601
|
}
|
|
565
602
|
catch (Exception ex)
|
|
566
603
|
{
|
|
567
|
-
HyperDebug.LogError($"Failed to parse CSRF token: {ex.Message}");
|
|
604
|
+
HyperDebug.LogError($"[CSRF] Failed to parse CSRF token from '{context ?? "unknown"}': {ex.Message}");
|
|
568
605
|
}
|
|
569
606
|
}
|
|
570
607
|
|
|
@@ -683,16 +720,60 @@ namespace Hyper.Internal.Managers
|
|
|
683
720
|
{
|
|
684
721
|
_gameManager?.GameOverModalComponent?.Init();
|
|
685
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
|
+
}
|
|
686
755
|
|
|
687
756
|
while (_currentRetryCount < MAX_RETRY_COUNT)
|
|
688
757
|
{
|
|
689
758
|
if (_hasSessionEnded)
|
|
690
759
|
{
|
|
760
|
+
LastSubmissionError = SubmissionErrorType.NetworkError;
|
|
691
761
|
_gameManager?.ShowRetryTimeExpiredModal();
|
|
692
762
|
_finalScoreSubmissionCoroutine = null;
|
|
693
763
|
yield break;
|
|
694
764
|
}
|
|
695
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
|
+
|
|
696
777
|
var finalScoreData = _dataManager?.CurrentScoreData;
|
|
697
778
|
finalScoreData.isFinal = true;
|
|
698
779
|
|
|
@@ -703,17 +784,19 @@ namespace Hyper.Internal.Managers
|
|
|
703
784
|
|
|
704
785
|
if (string.IsNullOrEmpty(encryptedScoreData))
|
|
705
786
|
{
|
|
706
|
-
|
|
787
|
+
LastSubmissionError = SubmissionErrorType.EncryptionError;
|
|
788
|
+
HyperDebug.LogError("Encryption failed [ENCRYPTION_ERROR]");
|
|
707
789
|
yield break;
|
|
708
790
|
}
|
|
709
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
|
|
710
796
|
if (_sendGameResumedCoroutine == null)
|
|
711
797
|
{
|
|
712
798
|
_sendGameResumedCoroutine = StartCoroutine(SendGameResumedCoroutine());
|
|
713
|
-
|
|
714
|
-
//? Ensure game is resumed to get new CSRF token before final score submission
|
|
715
799
|
yield return _sendGameResumedCoroutine;
|
|
716
|
-
|
|
717
800
|
_sendGameResumedCoroutine = null;
|
|
718
801
|
}
|
|
719
802
|
else
|
|
@@ -723,7 +806,24 @@ namespace Hyper.Internal.Managers
|
|
|
723
806
|
|
|
724
807
|
if (!isSocketConnected)
|
|
725
808
|
{
|
|
726
|
-
|
|
809
|
+
LastSubmissionError = SubmissionErrorType.NetworkError;
|
|
810
|
+
HyperDebug.LogError("WebSocket not connected - cannot submit final score [NETWORK_ERROR]");
|
|
811
|
+
_currentRetryCount++;
|
|
812
|
+
_gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
|
|
813
|
+
|
|
814
|
+
if (_currentRetryCount < MAX_RETRY_COUNT)
|
|
815
|
+
{
|
|
816
|
+
yield return new WaitForSecondsRealtime(1f);
|
|
817
|
+
}
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
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]");
|
|
727
827
|
_currentRetryCount++;
|
|
728
828
|
_gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
|
|
729
829
|
|
|
@@ -731,13 +831,19 @@ namespace Hyper.Internal.Managers
|
|
|
731
831
|
{
|
|
732
832
|
yield return new WaitForSecondsRealtime(1f);
|
|
733
833
|
}
|
|
834
|
+
|
|
734
835
|
continue;
|
|
735
836
|
}
|
|
736
837
|
|
|
737
838
|
// Sending final score via WebSocket
|
|
738
839
|
_finalScoreSubmitted = false;
|
|
739
840
|
_isWaitingForFinalScoreResponse = true;
|
|
740
|
-
|
|
841
|
+
|
|
842
|
+
_lastSentBatch.Clear();
|
|
843
|
+
if (finalScoreData != null)
|
|
844
|
+
{
|
|
845
|
+
_lastSentBatch.Add(finalScoreData);
|
|
846
|
+
}
|
|
741
847
|
|
|
742
848
|
bool sendFailed = false;
|
|
743
849
|
try
|
|
@@ -748,7 +854,8 @@ namespace Hyper.Internal.Managers
|
|
|
748
854
|
}
|
|
749
855
|
catch (Exception ex)
|
|
750
856
|
{
|
|
751
|
-
|
|
857
|
+
LastSubmissionError = SubmissionErrorType.NetworkError;
|
|
858
|
+
HyperDebug.LogError($"Failed to send final score via WebSocket: {ex.Message} [NETWORK_ERROR]");
|
|
752
859
|
// Keep _isWaitingForFinalScoreResponse = true since we're still in final score submission mode
|
|
753
860
|
// and will retry. We're still waiting for a final score response.
|
|
754
861
|
sendFailed = true;
|
|
@@ -804,6 +911,8 @@ namespace Hyper.Internal.Managers
|
|
|
804
911
|
// Timeout occurred, but keep _isWaitingForFinalScoreResponse = true
|
|
805
912
|
// because we're still in final score submission mode and will retry.
|
|
806
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]");
|
|
807
916
|
_currentRetryCount++;
|
|
808
917
|
_gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
|
|
809
918
|
|
|
@@ -814,6 +923,8 @@ namespace Hyper.Internal.Managers
|
|
|
814
923
|
}
|
|
815
924
|
}
|
|
816
925
|
|
|
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}");
|
|
817
928
|
_isWaitingForFinalScoreResponse = false;
|
|
818
929
|
_gameManager?.GameOverModalComponent?.AllAttemptsHaveFailed();
|
|
819
930
|
_finalScoreSubmissionCoroutine = null;
|
|
@@ -842,7 +953,10 @@ namespace Hyper.Internal.Managers
|
|
|
842
953
|
#region Score Submission (WebSocket)
|
|
843
954
|
|
|
844
955
|
private Coroutine _scoreSenderCoroutine;
|
|
845
|
-
|
|
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>();
|
|
846
960
|
private ScoreData _lastScoreData;
|
|
847
961
|
private readonly WaitForSeconds _scoreSendInterval = new WaitForSeconds(1f);
|
|
848
962
|
private bool _isWaitingForFinalScoreResponse;
|
|
@@ -872,21 +986,20 @@ namespace Hyper.Internal.Managers
|
|
|
872
986
|
{
|
|
873
987
|
while (true)
|
|
874
988
|
{
|
|
875
|
-
|
|
989
|
+
var scoresToSend = _scoreSendBuffer;
|
|
990
|
+
scoresToSend.Clear();
|
|
876
991
|
|
|
877
|
-
|
|
992
|
+
if (_dataManager?.PendingScores.Count > 0)
|
|
878
993
|
{
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
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);
|
|
885
999
|
}
|
|
886
|
-
|
|
1000
|
+
else
|
|
887
1001
|
{
|
|
888
|
-
|
|
889
|
-
scoresToSend = null;
|
|
1002
|
+
HyperDebug.Log("[Score] No pending scores to send in this tick.");
|
|
890
1003
|
}
|
|
891
1004
|
|
|
892
1005
|
yield return _scoreSendInterval;
|
|
@@ -901,14 +1014,66 @@ namespace Hyper.Internal.Managers
|
|
|
901
1014
|
// and ensures we wait for reconnection to get fresh token via SendGameResumed()
|
|
902
1015
|
if (!isSocketConnected)
|
|
903
1016
|
{
|
|
904
|
-
|
|
1017
|
+
int lastScore = scores[scores.Count - 1].cumulativeScore;
|
|
1018
|
+
HyperDebug.LogWarning($"[Score] Cannot send scores: WebSocket not connected. LastBatchCumulativeScore={lastScore} [ErrorType=NetworkError]");
|
|
905
1019
|
return;
|
|
906
1020
|
}
|
|
907
1021
|
|
|
908
1022
|
try
|
|
909
1023
|
{
|
|
910
|
-
|
|
911
|
-
|
|
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
|
+
}
|
|
912
1077
|
|
|
913
1078
|
string rawScoreJson = JsonConvert.SerializeObject(scoreData);
|
|
914
1079
|
var compressedScoreData = JsonCompressor.CompressJson(rawScoreJson);
|
|
@@ -917,25 +1082,27 @@ namespace Hyper.Internal.Managers
|
|
|
917
1082
|
|
|
918
1083
|
if (string.IsNullOrEmpty(encryptedData))
|
|
919
1084
|
{
|
|
920
|
-
|
|
1085
|
+
int lastScore = scoreData != null ? scoreData.cumulativeScore : -1;
|
|
1086
|
+
HyperDebug.LogError($"[Score] Encryption failed for per-second submission. CumulativeScore={lastScore} [ErrorType=EncryptionError]");
|
|
921
1087
|
return;
|
|
922
1088
|
}
|
|
923
1089
|
|
|
924
1090
|
var message = new WebSocketScoreMessage(_cachedSessionId, _csrfToken, encryptedData);
|
|
925
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}");
|
|
926
1095
|
SendWebSocketMessage(json);
|
|
927
|
-
|
|
928
|
-
#if UNITY_EDITOR
|
|
929
|
-
_lastScoreData = _lastSentBatch.Last();
|
|
930
|
-
_lastSentBatch.Clear();
|
|
931
|
-
#endif
|
|
932
1096
|
}
|
|
933
1097
|
catch (Exception ex)
|
|
934
1098
|
{
|
|
935
|
-
|
|
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}]");
|
|
936
1101
|
}
|
|
937
1102
|
finally
|
|
938
1103
|
{
|
|
1104
|
+
if (_dataManager == null) _dataManager = HyperRuntime.DataManager;
|
|
1105
|
+
_dataManager?.ReturnScoreDataToPool(scores);
|
|
939
1106
|
scores?.Clear();
|
|
940
1107
|
}
|
|
941
1108
|
}
|
|
@@ -44,7 +44,7 @@ namespace Hyper.Internal.Managers
|
|
|
44
44
|
public void UpdateScore(int score)
|
|
45
45
|
{
|
|
46
46
|
if (_gameManager == null) _gameManager = HyperRuntime.GameManager;
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
if (_gameManager.HasGameEnded) return;
|
|
49
49
|
|
|
50
50
|
CurrentScore = score;
|
|
@@ -56,23 +56,26 @@ namespace Hyper.Internal.Managers
|
|
|
56
56
|
|
|
57
57
|
float currentGameSecond = Mathf.Floor(currentTimestamp);
|
|
58
58
|
|
|
59
|
-
CurrentScoreData
|
|
59
|
+
// Reuse a single ScoreData graph for CurrentScoreData to avoid per-call allocations
|
|
60
|
+
if (CurrentScoreData == null)
|
|
60
61
|
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
62
|
+
CurrentScoreData = _currentScoreDataBuffer;
|
|
63
|
+
CurrentScoreData.scoreSnapshot = _currentSnapshotBuffer;
|
|
64
|
+
CurrentScoreData.scoreSnapshot.gameplayModifiers = _currentModifiersBuffer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
CurrentScoreData.cumulativeScore = CurrentScore;
|
|
68
|
+
CurrentScoreData.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
|
69
|
+
CurrentScoreData.isFinal = true;
|
|
70
|
+
|
|
71
|
+
var snapshot = CurrentScoreData.scoreSnapshot;
|
|
72
|
+
snapshot.scoreTime = currentGameSecond;
|
|
73
|
+
snapshot.scoreDelta = CurrentScore - _lastCapturedScore;
|
|
74
|
+
|
|
75
|
+
var modifiers = snapshot.gameplayModifiers;
|
|
76
|
+
modifiers.comboModifiers = ComboMultiplier;
|
|
77
|
+
modifiers.globalModifiers = GlobalMultiplier;
|
|
78
|
+
modifiers.activePowerUps = ActivePowerups;
|
|
76
79
|
|
|
77
80
|
if (score > GetBestScore())
|
|
78
81
|
{
|
|
@@ -130,6 +133,7 @@ namespace Hyper.Internal.Managers
|
|
|
130
133
|
internal List<ScoreData> PendingScores { get; private set; } = new List<ScoreData>();
|
|
131
134
|
internal TutorialData TutorialData { get; private set; }
|
|
132
135
|
internal bool HasReferencedTutorialResources { get; private set; }
|
|
136
|
+
private const int SCORE_POOL_CAPACITY = 15;
|
|
133
137
|
|
|
134
138
|
internal void ConfigurePlayerSoundSetting()
|
|
135
139
|
{
|
|
@@ -152,7 +156,7 @@ namespace Hyper.Internal.Managers
|
|
|
152
156
|
internal void StartCapturingScores()
|
|
153
157
|
{
|
|
154
158
|
if (_gameManager == null) _gameManager = HyperRuntime.GameManager;
|
|
155
|
-
|
|
159
|
+
|
|
156
160
|
if (_scoreCaptureCoroutine == null)
|
|
157
161
|
{
|
|
158
162
|
_scoreCaptureCoroutine = StartCoroutine(CaptureScoreEverySecond());
|
|
@@ -200,6 +204,16 @@ namespace Hyper.Internal.Managers
|
|
|
200
204
|
private float _lastCapturedGameSecond = -1f;
|
|
201
205
|
private int _lastCapturedScore = 0;
|
|
202
206
|
|
|
207
|
+
// Reusable object graph for CurrentScoreData to avoid allocations in UpdateScore()
|
|
208
|
+
private readonly ScoreData _currentScoreDataBuffer = new ScoreData();
|
|
209
|
+
private readonly ScoreSnapshot _currentSnapshotBuffer = new ScoreSnapshot();
|
|
210
|
+
private readonly GameplayModifiers _currentModifiersBuffer = new GameplayModifiers();
|
|
211
|
+
|
|
212
|
+
// Pools used for per-second score snapshots captured into PendingScores
|
|
213
|
+
private readonly Queue<ScoreData> _scoreDataPool = new Queue<ScoreData>();
|
|
214
|
+
private readonly Queue<ScoreSnapshot> _scoreSnapshotPool = new Queue<ScoreSnapshot>();
|
|
215
|
+
private readonly Queue<GameplayModifiers> _gameplayModifiersPool = new Queue<GameplayModifiers>();
|
|
216
|
+
|
|
203
217
|
#endregion
|
|
204
218
|
|
|
205
219
|
#region Score Capture Logic
|
|
@@ -216,23 +230,24 @@ namespace Hyper.Internal.Managers
|
|
|
216
230
|
|
|
217
231
|
if (currentGameSecond > _lastCapturedGameSecond)
|
|
218
232
|
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
// Get objects from pools to avoid allocations in this hot path
|
|
234
|
+
var newScore = GetPooledScoreData();
|
|
235
|
+
var snapshot = GetPooledScoreSnapshot();
|
|
236
|
+
var modifiers = GetPooledGameplayModifiers();
|
|
237
|
+
|
|
238
|
+
newScore.cumulativeScore = CurrentScore;
|
|
239
|
+
newScore.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
|
240
|
+
newScore.isFinal = false;
|
|
241
|
+
|
|
242
|
+
snapshot.scoreTime = currentGameSecond;
|
|
243
|
+
snapshot.scoreDelta = CurrentScore - _lastCapturedScore;
|
|
244
|
+
|
|
245
|
+
modifiers.comboModifiers = ComboMultiplier;
|
|
246
|
+
modifiers.globalModifiers = GlobalMultiplier;
|
|
247
|
+
modifiers.activePowerUps = ActivePowerups;
|
|
248
|
+
|
|
249
|
+
snapshot.gameplayModifiers = modifiers;
|
|
250
|
+
newScore.scoreSnapshot = snapshot;
|
|
236
251
|
|
|
237
252
|
PendingScores.Add(newScore);
|
|
238
253
|
|
|
@@ -244,6 +259,84 @@ namespace Hyper.Internal.Managers
|
|
|
244
259
|
}
|
|
245
260
|
}
|
|
246
261
|
|
|
262
|
+
private ScoreData GetPooledScoreData()
|
|
263
|
+
{
|
|
264
|
+
return _scoreDataPool.Count > 0 ? _scoreDataPool.Dequeue() : new ScoreData();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private ScoreSnapshot GetPooledScoreSnapshot()
|
|
268
|
+
{
|
|
269
|
+
return _scoreSnapshotPool.Count > 0 ? _scoreSnapshotPool.Dequeue() : new ScoreSnapshot();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private GameplayModifiers GetPooledGameplayModifiers()
|
|
273
|
+
{
|
|
274
|
+
return _gameplayModifiersPool.Count > 0 ? _gameplayModifiersPool.Dequeue() : new GameplayModifiers();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
internal void ReturnScoreDataToPool(List<ScoreData> scores)
|
|
278
|
+
{
|
|
279
|
+
if (scores == null) return;
|
|
280
|
+
|
|
281
|
+
for (int i = 0; i < scores.Count; i++)
|
|
282
|
+
{
|
|
283
|
+
ReturnScoreDataToPool(scores[i]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private void ReturnScoreDataToPool(ScoreData data)
|
|
288
|
+
{
|
|
289
|
+
if (data == null) return;
|
|
290
|
+
|
|
291
|
+
if (data.scoreSnapshot != null)
|
|
292
|
+
{
|
|
293
|
+
ReturnScoreSnapshotToPool(data.scoreSnapshot);
|
|
294
|
+
data.scoreSnapshot = null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
data.cumulativeScore = 0;
|
|
298
|
+
data.isFinal = false;
|
|
299
|
+
data.timestampUtc = null;
|
|
300
|
+
|
|
301
|
+
if (_scoreDataPool.Count < SCORE_POOL_CAPACITY)
|
|
302
|
+
{
|
|
303
|
+
_scoreDataPool.Enqueue(data);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private void ReturnScoreSnapshotToPool(ScoreSnapshot snapshot)
|
|
308
|
+
{
|
|
309
|
+
if (snapshot == null) return;
|
|
310
|
+
|
|
311
|
+
if (snapshot.gameplayModifiers != null)
|
|
312
|
+
{
|
|
313
|
+
ReturnGameplayModifiersToPool(snapshot.gameplayModifiers);
|
|
314
|
+
snapshot.gameplayModifiers = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
snapshot.scoreTime = 0f;
|
|
318
|
+
snapshot.scoreDelta = 0;
|
|
319
|
+
|
|
320
|
+
if (_scoreSnapshotPool.Count < SCORE_POOL_CAPACITY)
|
|
321
|
+
{
|
|
322
|
+
_scoreSnapshotPool.Enqueue(snapshot);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private void ReturnGameplayModifiersToPool(GameplayModifiers modifiers)
|
|
327
|
+
{
|
|
328
|
+
if (modifiers == null) return;
|
|
329
|
+
|
|
330
|
+
modifiers.comboModifiers = 0f;
|
|
331
|
+
modifiers.globalModifiers = 0f;
|
|
332
|
+
modifiers.activePowerUps = null;
|
|
333
|
+
|
|
334
|
+
if (_gameplayModifiersPool.Count < SCORE_POOL_CAPACITY)
|
|
335
|
+
{
|
|
336
|
+
_gameplayModifiersPool.Enqueue(modifiers);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
247
340
|
private void SetBestScore(ScoreData scoreData, string sessionId)
|
|
248
341
|
{
|
|
249
342
|
BestScoreWrapper bestScore = new BestScoreWrapper(scoreData, sessionId);
|
|
@@ -38,6 +38,9 @@ namespace Hyper.Internal
|
|
|
38
38
|
[Tooltip("Choose the start CTA overlay shown before gameplay begins. Hyper requires one of these overlays - do not add your own start buttons.")]
|
|
39
39
|
public StartCTA startCTA = StartCTA.TapToPlay;
|
|
40
40
|
|
|
41
|
+
[Tooltip("If Start CTA is set to Swipe to Play, choose the swipe direction used for the visual cue.")]
|
|
42
|
+
public SwipeDirection swipeDirection = SwipeDirection.Up;
|
|
43
|
+
|
|
41
44
|
#endregion
|
|
42
45
|
|
|
43
46
|
#region Control Bar Settings
|