客户端rsa,冗余清理,bug修复,安卓build问题
This commit is contained in:
@@ -38,8 +38,6 @@ public class AnimationController : MonoBehaviour
|
||||
private bool holdActive = false;
|
||||
private string currentHoldColor;
|
||||
private bool isHolding = false;
|
||||
private WaitForSeconds cachedHoldWait;
|
||||
private float cachedHoldWaitSeconds = -1f;
|
||||
private static bool loggedMissingHitParticleSource;
|
||||
|
||||
// Track active particle instances for immediate cleanup
|
||||
@@ -203,7 +201,7 @@ public class AnimationController : MonoBehaviour
|
||||
|
||||
private IEnumerator RecycleFxRoutine(GameObject instance, int rentToken, float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
yield return GameplayClock.WaitForSeconds(delay);
|
||||
if (instance == null)
|
||||
yield break;
|
||||
|
||||
@@ -316,12 +314,7 @@ public class AnimationController : MonoBehaviour
|
||||
while (holdActive)
|
||||
{
|
||||
float waitSeconds = Mathf.Max(0.01f, holdParticleInterval);
|
||||
if (cachedHoldWait == null || !Mathf.Approximately(cachedHoldWaitSeconds, waitSeconds))
|
||||
{
|
||||
cachedHoldWaitSeconds = waitSeconds;
|
||||
cachedHoldWait = new WaitForSeconds(waitSeconds);
|
||||
}
|
||||
yield return cachedHoldWait;
|
||||
yield return GameplayClock.WaitForSeconds(waitSeconds);
|
||||
if (!holdActive) break;
|
||||
PlayDestroyAnimation(color);
|
||||
}
|
||||
|
||||
@@ -218,6 +218,12 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
private Coroutine playbackDelayCoroutine;
|
||||
|
||||
private void BeginGameplayClock()
|
||||
{
|
||||
GameplayClock.Reset();
|
||||
GameplayClock.StartChart(0f);
|
||||
}
|
||||
|
||||
// Public helper to reliably unmute and start music playback, respecting delay.
|
||||
public void PlayMusicWithDelay(float delaySeconds)
|
||||
{
|
||||
@@ -275,7 +281,7 @@ public class GameManager : MonoBehaviour
|
||||
{
|
||||
float clampedStartTime = ClampAudioStartTime(delaySeconds < 0f ? -delaySeconds : 0f);
|
||||
musicSource.time = clampedStartTime;
|
||||
musicSource.Play();
|
||||
musicSource.Play();
|
||||
PlaybackStarted = true;
|
||||
}
|
||||
catch { }
|
||||
@@ -1287,6 +1293,7 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
// Resume using PauseManager
|
||||
PauseManager.Instance?.Pause(false);
|
||||
BeginGameplayClock();
|
||||
|
||||
// Start spawning using the parsed beatmap
|
||||
if (beatmapManager.beatmap != null)
|
||||
@@ -1346,6 +1353,7 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
// Resume
|
||||
PauseManager.Instance?.Pause(false);
|
||||
BeginGameplayClock();
|
||||
|
||||
// Start spawning using the existing parsed beatmap
|
||||
if (beatmapManager.beatmap != null)
|
||||
@@ -1380,13 +1388,6 @@ public class GameManager : MonoBehaviour
|
||||
float delay = beatmapManager.globalDelaySeconds;
|
||||
PlayMusicWithDelay(delay);
|
||||
|
||||
// ensure overlay shows and pause logic runs after loading the clip
|
||||
var pm2 = PauseManager.Instance ?? SceneObjectLookupCache.FindAny<PauseManager>();
|
||||
if (pm2 != null)
|
||||
{
|
||||
pm2.Pause(true);
|
||||
Debug.Log("PauseManager.Pause(true) invoked after assigning musicClip in normal startup");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1444,6 +1445,7 @@ public class GameManager : MonoBehaviour
|
||||
|
||||
// Resume
|
||||
PauseManager.Instance?.Pause(false);
|
||||
BeginGameplayClock();
|
||||
|
||||
// Start spawning
|
||||
if (beatmapManager.beatmap != null)
|
||||
@@ -1473,12 +1475,6 @@ public class GameManager : MonoBehaviour
|
||||
float delay = beatmapManager.globalDelaySeconds;
|
||||
PlayMusicWithDelay(delay);
|
||||
|
||||
var pm3 = PauseManager.Instance ?? SceneObjectLookupCache.FindAny<PauseManager>();
|
||||
if (pm3 != null)
|
||||
{
|
||||
pm3.Pause(true);
|
||||
Debug.Log("PauseManager.Pause(true) invoked after assigning parsedMusicFile clip");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
using UnityEngine;
|
||||
|
||||
public static class GameplayClock
|
||||
{
|
||||
private static bool initialized;
|
||||
private static bool paused;
|
||||
private static double chartStartDspTime;
|
||||
private static double pauseStartDspTime;
|
||||
private static double accumulatedPauseSeconds;
|
||||
private static float chartStartSongTime;
|
||||
|
||||
public static bool IsInitialized => initialized;
|
||||
public static bool IsPaused => paused;
|
||||
public static float ChartStartSongTime => chartStartSongTime;
|
||||
public static double ChartStartDspTime => chartStartDspTime;
|
||||
|
||||
public static void Reset()
|
||||
{
|
||||
initialized = false;
|
||||
paused = false;
|
||||
chartStartDspTime = 0d;
|
||||
pauseStartDspTime = 0d;
|
||||
accumulatedPauseSeconds = 0d;
|
||||
chartStartSongTime = 0f;
|
||||
}
|
||||
|
||||
public static void StartChart(float initialSongTimeSeconds = 0f)
|
||||
{
|
||||
initialized = true;
|
||||
paused = false;
|
||||
chartStartDspTime = AudioSettings.dspTime;
|
||||
pauseStartDspTime = 0d;
|
||||
accumulatedPauseSeconds = 0d;
|
||||
chartStartSongTime = Mathf.Max(0f, initialSongTimeSeconds);
|
||||
}
|
||||
|
||||
public static void Pause()
|
||||
{
|
||||
if (!initialized || paused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
paused = true;
|
||||
pauseStartDspTime = AudioSettings.dspTime;
|
||||
}
|
||||
|
||||
public static void Resume()
|
||||
{
|
||||
if (!initialized || !paused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
accumulatedPauseSeconds += AudioSettings.dspTime - pauseStartDspTime;
|
||||
pauseStartDspTime = 0d;
|
||||
paused = false;
|
||||
}
|
||||
|
||||
public static float NowSongTime
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!initialized)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
double currentDsp = paused ? pauseStartDspTime : AudioSettings.dspTime;
|
||||
double elapsed = currentDsp - chartStartDspTime - accumulatedPauseSeconds;
|
||||
return chartStartSongTime + Mathf.Max(0f, (float)elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
public static float SongTimeFromDsp(double dspTime)
|
||||
{
|
||||
if (!initialized)
|
||||
{
|
||||
return chartStartSongTime;
|
||||
}
|
||||
|
||||
double effectivePause = accumulatedPauseSeconds;
|
||||
if (paused && dspTime > pauseStartDspTime)
|
||||
{
|
||||
dspTime = pauseStartDspTime;
|
||||
}
|
||||
|
||||
double elapsed = dspTime - chartStartDspTime - effectivePause;
|
||||
return chartStartSongTime + Mathf.Max(0f, (float)elapsed);
|
||||
}
|
||||
|
||||
public static float ToAbsoluteChartTime(float songTimelineTimeSeconds)
|
||||
{
|
||||
return chartStartSongTime + songTimelineTimeSeconds;
|
||||
}
|
||||
|
||||
public static System.Collections.IEnumerator WaitForSeconds(float seconds)
|
||||
{
|
||||
if (seconds <= 0f)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!initialized)
|
||||
{
|
||||
yield return new UnityEngine.WaitForSeconds(seconds);
|
||||
yield break;
|
||||
}
|
||||
|
||||
float targetTime = NowSongTime + seconds;
|
||||
while (NowSongTime < targetTime)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47a06e6eb65d9734f8cf63eb1cb59207
|
||||
@@ -312,6 +312,11 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
prevTrackHeld = false;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (controller != null)
|
||||
@@ -435,7 +440,7 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
|
||||
// Removed: CalibratePosition for delay==0f was causing Start segments to snap to incorrect positions
|
||||
// if they started moving immediately after Setup (due to activationTime <= Time.time).
|
||||
// if they started moving immediately after Setup (due to activationTime <= gameplay clock).
|
||||
// Start segments should begin at spawnPoint and move from there naturally.
|
||||
}
|
||||
|
||||
@@ -446,7 +451,7 @@ public class HoldNote : BaseNote
|
||||
var jm = cachedJudgeManager;
|
||||
var tkm = cachedTrackKeyManager;
|
||||
bool debugEnabled = JudgeManager.IsDebugEnabled;
|
||||
float now = Time.time;
|
||||
float now = GameplayClock.NowSongTime;
|
||||
|
||||
// Sample held state and derive key-down/up edges at the very top, BEFORE any
|
||||
// early return. Unity's Input.GetKeyDown/Up are global per-frame edges that
|
||||
@@ -740,7 +745,7 @@ public class HoldNote : BaseNote
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] End entered while key still held: {noteColor}, forcing Perfect");
|
||||
// Record release time as current time (player is still holding)
|
||||
releaseTime = Time.time;
|
||||
releaseTime = GameplayClock.NowSongTime;
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
|
||||
// Evaluate and judge the hold end - force Perfect since we reached the end while holding
|
||||
@@ -818,12 +823,11 @@ public class HoldNote : BaseNote
|
||||
|
||||
if (segment == NoteSegment.End)
|
||||
{
|
||||
float timeToWait = Mathf.Max(0f, scheduledEndTime - Time.time);
|
||||
float timeToWait = Mathf.Max(0f, scheduledEndTime - GameplayClock.NowSongTime);
|
||||
if (timeToWait > 0f)
|
||||
{
|
||||
// use scaled time here so pause affects this wait
|
||||
float target = Time.time + timeToWait;
|
||||
while (Time.time < target)
|
||||
float target = GameplayClock.NowSongTime + timeToWait;
|
||||
while (GameplayClock.NowSongTime < target)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
@@ -858,7 +862,7 @@ public class HoldNote : BaseNote
|
||||
|
||||
private IEnumerator DelayedReturnToPool(float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
yield return GameplayClock.WaitForSeconds(delay);
|
||||
if (gameObject.activeSelf)
|
||||
{
|
||||
ReturnToPool();
|
||||
@@ -883,7 +887,7 @@ public class HoldNote : BaseNote
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] START forced Miss: {noteColor}");
|
||||
|
||||
// Use EvaluateHoldEnd to centralize miss logic and statistics
|
||||
EvaluateHoldEnd(Time.time, true);
|
||||
EvaluateHoldEnd(GameplayClock.NowSongTime, true);
|
||||
|
||||
// Still need to show judge result and update combo for the head segment
|
||||
InputManager.Instance?.ShowJudgeResult(trackIndex, "Miss");
|
||||
@@ -897,7 +901,7 @@ public class HoldNote : BaseNote
|
||||
return;
|
||||
}
|
||||
|
||||
float pressTime = Time.time;
|
||||
float pressTime = GameplayClock.NowSongTime;
|
||||
float rawOffsetMs = (hitTime - pressTime) * 1000f;
|
||||
float offset = Mathf.Abs(pressTime - hitTime);
|
||||
string result;
|
||||
@@ -962,7 +966,7 @@ public class HoldNote : BaseNote
|
||||
JudgeManager.Instance.RegisterStartJudged(noteID, false);
|
||||
isHoldActive = false;
|
||||
|
||||
EvaluateHoldEnd(Time.time, true);
|
||||
EvaluateHoldEnd(GameplayClock.NowSongTime, true);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1035,10 +1039,10 @@ public class HoldNote : BaseNote
|
||||
|
||||
private IEnumerator DelayedReturn()
|
||||
{
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
yield return GameplayClock.WaitForSeconds(0.05f);
|
||||
if (gameObject.activeSelf)
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] DelayedReturn: returning {noteID} to pool at time={Time.time:F3}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] DelayedReturn: returning {noteID} to pool at time={GameplayClock.NowSongTime:F3}");
|
||||
ReturnToPool();
|
||||
}
|
||||
}
|
||||
@@ -1331,7 +1335,7 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
else
|
||||
{
|
||||
releaseTime = Time.time;
|
||||
releaseTime = GameplayClock.NowSongTime;
|
||||
}
|
||||
hasReleased = true;
|
||||
JudgeManager.Instance?.RegisterNoteReleased(noteID, true);
|
||||
@@ -1353,7 +1357,7 @@ public class HoldNote : BaseNote
|
||||
return;
|
||||
}
|
||||
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] ReturnToPool called for {noteID} segment={segment} time={Time.time:F3}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] ReturnToPool called for {noteID} segment={segment} time={GameplayClock.NowSongTime:F3}");
|
||||
|
||||
// CRITICAL: Always release lock before returning to pool, regardless of state
|
||||
ReleaseTrackJudgeLockSafe();
|
||||
@@ -1389,6 +1393,7 @@ public class HoldNote : BaseNote
|
||||
isJudged = false;
|
||||
isHoldActive = false;
|
||||
hasBeenHeldFromStart = false;
|
||||
prevTrackHeld = false;
|
||||
|
||||
holdStartTime = -1f;
|
||||
|
||||
@@ -1463,7 +1468,7 @@ public class HoldNote : BaseNote
|
||||
|
||||
float activation = controller.ActivationTime;
|
||||
float s = controller.CurrentSpeed;
|
||||
float elapsed = Time.time - activation;
|
||||
float elapsed = GameplayClock.NowSongTime - activation;
|
||||
Vector3 expected = spawnPointPosition;
|
||||
|
||||
if (controller.UsesAbsolutePositioning)
|
||||
|
||||
@@ -20,12 +20,12 @@ public class HoldNoteController : MonoBehaviour
|
||||
if (useAbsolutePositioning)
|
||||
{
|
||||
if (!isMoving) return;
|
||||
ApplyAbsolutePosition(Time.time);
|
||||
ApplyAbsolutePosition(GameplayClock.NowSongTime);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start moving after activation time in legacy mode
|
||||
if (!isMoving && Time.time >= activationTime)
|
||||
if (!isMoving && GameplayClock.NowSongTime >= activationTime)
|
||||
{
|
||||
isMoving = true;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public class HoldNoteController : MonoBehaviour
|
||||
/// </summary>
|
||||
public void SetSegmentDelay(float segmentDelay)
|
||||
{
|
||||
activationTime = Time.time + segmentDelay;
|
||||
activationTime = GameplayClock.NowSongTime + segmentDelay;
|
||||
isMoving = false;
|
||||
useAbsolutePositioning = false;
|
||||
visualOffset = 0f;
|
||||
@@ -79,7 +79,7 @@ public class HoldNoteController : MonoBehaviour
|
||||
isMoving = true;
|
||||
|
||||
// Snap immediately to the expected position to avoid a 1-frame pop.
|
||||
ApplyAbsolutePosition(Time.time);
|
||||
ApplyAbsolutePosition(GameplayClock.NowSongTime);
|
||||
}
|
||||
|
||||
public void StopMovement()
|
||||
|
||||
@@ -207,7 +207,7 @@ public class JudgeManager : MonoBehaviour
|
||||
return false;
|
||||
}
|
||||
s.startResolved = true;
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryResolveStart: start resolved for {noteID} at time={Time.time:F3}");
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryResolveStart: start resolved for {noteID} at time={GameplayClock.NowSongTime:F3}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ public class JudgeManager : MonoBehaviour
|
||||
return false;
|
||||
}
|
||||
s.endResolved = true;
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryResolveEnd: end resolved for {noteID} at time={Time.time:F3}");
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryResolveEnd: end resolved for {noteID} at time={GameplayClock.NowSongTime:F3}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ public class JudgeManager : MonoBehaviour
|
||||
return false;
|
||||
}
|
||||
s.skillTriggered = true;
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryTriggerSkill: skill triggered for {noteID} at time={Time.time:F3}");
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] TryTriggerSkill: skill triggered for {noteID} at time={GameplayClock.NowSongTime:F3}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ public class JudgeManager : MonoBehaviour
|
||||
public void RegisterStartJudged(string noteID, bool state)
|
||||
{
|
||||
startJudgedNotes[noteID] = state;
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterStartJudged: {noteID} = {state} at time={Time.time:F3}");
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterStartJudged: {noteID} = {state} at time={GameplayClock.NowSongTime:F3}");
|
||||
}
|
||||
|
||||
public bool IsStartJudged(string noteID)
|
||||
@@ -288,7 +288,7 @@ public class JudgeManager : MonoBehaviour
|
||||
|
||||
if (!judgeQueues[key].Contains(note))
|
||||
judgeQueues[key].Enqueue(note);
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterNote: key={key} note={note.name} time={Time.time:F3} queueSize={judgeQueues[key].Count}");
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] RegisterNote: key={key} note={note.name} time={GameplayClock.NowSongTime:F3} queueSize={judgeQueues[key].Count}");
|
||||
}
|
||||
|
||||
public void UnregisterNote(KeyCode key, Note note)
|
||||
@@ -315,7 +315,7 @@ public class JudgeManager : MonoBehaviour
|
||||
Note note = judgeQueues[key].Dequeue();
|
||||
if (note != null && !note.IsJudged())
|
||||
{
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] JudgeEarliestNote: judging {note.name} for key={key} at time={Time.time:F3}");
|
||||
if (IsDebugEnabled) Debug.Log($"[JudgeManager] JudgeEarliestNote: judging {note.name} for key={key} at time={GameplayClock.NowSongTime:F3}");
|
||||
note.Judge();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,109 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class Notes : MonoBehaviour
|
||||
{
|
||||
public int trackIndex; // Documentation text normalized.
|
||||
public float hitTime; // Documentation text normalized.
|
||||
private bool canBeJudged = false; // Documentation text normalized.
|
||||
private bool isHit = false; // Documentation text normalized.
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Documentation text normalized.
|
||||
if (canBeJudged && !isHit && Time.timeSinceLevelLoad > hitTime + 0.3f)
|
||||
{
|
||||
JudgeMiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (other.CompareTag("JudgmentLine"))
|
||||
{
|
||||
canBeJudged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerExit2D(Collider2D other)
|
||||
{
|
||||
if (other.CompareTag("JudgmentLine"))
|
||||
{
|
||||
canBeJudged = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void JudgeNote()
|
||||
{
|
||||
if (!canBeJudged || isHit) return; // Documentation text normalized.
|
||||
|
||||
float currentTime = Time.timeSinceLevelLoad;
|
||||
float timeDifference = Mathf.Abs(currentTime - hitTime);
|
||||
|
||||
if (timeDifference <= 0.05f)
|
||||
{
|
||||
JudgePerfect();
|
||||
}
|
||||
else if (timeDifference <= 0.1f)
|
||||
{
|
||||
JudgeGreat();
|
||||
}
|
||||
else if (timeDifference <= 0.2f)
|
||||
{
|
||||
JudgeGood();
|
||||
}
|
||||
else if (timeDifference <= 0.3f)
|
||||
{
|
||||
JudgeMiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void Recycle()
|
||||
{
|
||||
// Documentation text normalized.
|
||||
if (NotePool.Instance != null)
|
||||
{
|
||||
NotePool.Instance.ReturnNote(gameObject, "red"); // Documentation text normalized.
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void JudgePerfect()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Perfect!");
|
||||
isHit = true;
|
||||
Recycle();
|
||||
}
|
||||
|
||||
private void JudgeGreat()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Great!");
|
||||
isHit = true;
|
||||
Recycle();
|
||||
}
|
||||
|
||||
private void JudgeGood()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Good!");
|
||||
isHit = true;
|
||||
Recycle();
|
||||
}
|
||||
|
||||
private void JudgeMiss()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Miss!");
|
||||
isHit = true;
|
||||
Recycle();
|
||||
}
|
||||
}
|
||||
using UnityEngine;
|
||||
|
||||
[AddComponentMenu("")]
|
||||
[System.Obsolete("Legacy prototype note script. Do not use in gameplay.", false)]
|
||||
public class Notes : MonoBehaviour
|
||||
{
|
||||
public int trackIndex;
|
||||
public float hitTime;
|
||||
private bool canBeJudged = false;
|
||||
private bool isHit = false;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
enabled = false;
|
||||
Debug.LogWarning("[Notes] Legacy prototype script was enabled. It has been disabled automatically and is not part of the current gameplay pipeline.", this);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (canBeJudged && !isHit && Time.timeSinceLevelLoad > hitTime + 0.3f)
|
||||
{
|
||||
JudgeMiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (other.CompareTag("JudgmentLine"))
|
||||
{
|
||||
canBeJudged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerExit2D(Collider2D other)
|
||||
{
|
||||
if (other.CompareTag("JudgmentLine"))
|
||||
{
|
||||
canBeJudged = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void JudgeNote()
|
||||
{
|
||||
if (!canBeJudged || isHit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float currentTime = Time.timeSinceLevelLoad;
|
||||
float timeDifference = Mathf.Abs(currentTime - hitTime);
|
||||
|
||||
if (timeDifference <= 0.05f)
|
||||
{
|
||||
JudgePerfect();
|
||||
}
|
||||
else if (timeDifference <= 0.1f)
|
||||
{
|
||||
JudgeGreat();
|
||||
}
|
||||
else if (timeDifference <= 0.2f)
|
||||
{
|
||||
JudgeGood();
|
||||
}
|
||||
else if (timeDifference <= 0.3f)
|
||||
{
|
||||
JudgeMiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void Recycle()
|
||||
{
|
||||
if (NotePool.Instance != null)
|
||||
{
|
||||
NotePool.Instance.ReturnNote(gameObject, "red");
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void JudgePerfect()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Perfect!");
|
||||
isHit = true;
|
||||
Recycle();
|
||||
}
|
||||
|
||||
private void JudgeGreat()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Great!");
|
||||
isHit = true;
|
||||
Recycle();
|
||||
}
|
||||
|
||||
private void JudgeGood()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Good!");
|
||||
isHit = true;
|
||||
Recycle();
|
||||
}
|
||||
|
||||
private void JudgeMiss()
|
||||
{
|
||||
Debug.Log($"Track {trackIndex}: Miss!");
|
||||
isHit = true;
|
||||
Recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ public class Note : BaseNote
|
||||
if (!isJudged && GameConfig.autoPlayEnabled && gameObject.activeSelf)
|
||||
{
|
||||
// Only judge once the note reaches its scheduled hit time.
|
||||
if (Time.time >= hitTime)
|
||||
if (GameplayClock.NowSongTime >= hitTime)
|
||||
{
|
||||
TryAutoJudgePerfect();
|
||||
}
|
||||
@@ -226,9 +226,9 @@ public class Note : BaseNote
|
||||
}
|
||||
|
||||
// Force miss if we've passed the deadline without being judged
|
||||
if (!isJudged && Time.time > missDeadlineTime && gameObject.activeSelf)
|
||||
if (!isJudged && GameplayClock.NowSongTime > missDeadlineTime && gameObject.activeSelf)
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note] {noteColor} on track {TrackIndex} exceeded miss deadline at time {Time.time:F3}, forcing Miss");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note] {noteColor} on track {TrackIndex} exceeded miss deadline at time {GameplayClock.NowSongTime:F3}, forcing Miss");
|
||||
JudgeMiss();
|
||||
}
|
||||
}
|
||||
@@ -330,7 +330,7 @@ public class Note : BaseNote
|
||||
|
||||
hasLock = true;
|
||||
|
||||
float pressTime = Time.time;
|
||||
float pressTime = GameplayClock.NowSongTime;
|
||||
float maxWindow = (judgeConfig?.missRange ?? 0.5f);
|
||||
|
||||
// If there is a controller and the note is not inside judge zone, only allow judgment
|
||||
@@ -562,7 +562,7 @@ public class Note : BaseNote
|
||||
}
|
||||
|
||||
try { SkillBuilder.Instance?.NotifyNoteHit(TrackIndex, "Miss", SkillDefinition.NoteTypeTrigger.Tap); } catch { }
|
||||
LogTapJudge("Miss", 0f, false, false, Time.time);
|
||||
LogTapJudge("Miss", 0f, false, false, GameplayClock.NowSongTime);
|
||||
|
||||
// notify global judge manager that this note has been finally judged (miss)
|
||||
JudgeManager.Instance?.NotifyNoteJudged();
|
||||
@@ -628,7 +628,7 @@ public class Note : BaseNote
|
||||
// causes "GameObject is already being activated or deactivated" errors.
|
||||
// Instead, mark for miss and let Update handle it next frame.
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[Note.SetJudgeZone] Note {noteColor} left judge zone on track {TrackIndex}, will force Miss next frame");
|
||||
missDeadlineTime = Time.time; // Force deadline to now so Update will handle it
|
||||
missDeadlineTime = GameplayClock.NowSongTime; // Force deadline to now so Update will handle it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class NoteController : MonoBehaviour
|
||||
if (isMoving && useAbsolutePositioning)
|
||||
{
|
||||
// Use absolute positioning based on elapsed time since activation
|
||||
float elapsedSinceActivation = Time.time - activationTime;
|
||||
float elapsedSinceActivation = GameplayClock.NowSongTime - activationTime;
|
||||
float travelDistance = speed * Mathf.Max(0f, elapsedSinceActivation);
|
||||
|
||||
// Position = spawnPoint + initial offset + downward travel
|
||||
@@ -65,7 +65,7 @@ public class NoteController : MonoBehaviour
|
||||
activationTime = hitTime - travelTime;
|
||||
baseSpawnYOffset = initialYOffset;
|
||||
useAbsolutePositioning = true;
|
||||
transform.position = GetExpectedPosition(Time.time);
|
||||
transform.position = GetExpectedPosition(GameplayClock.NowSongTime);
|
||||
|
||||
if (JudgeManager.IsDebugEnabled)
|
||||
Debug.Log($"[NoteController] Configured: spawnPos={spawnPosition}, activationTime={activationTime:F3}, initialYOffset={initialYOffset:F4}");
|
||||
|
||||
@@ -89,7 +89,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
public NoteJudgeConfig judgeConfig; // Documentation text normalized.
|
||||
private Beatmap beatmap;
|
||||
private float startTime; // Documentation text normalized.
|
||||
private float startTime; // Absolute chart time anchor.
|
||||
private float bpm = 120f;
|
||||
private bool isSpawning = false;
|
||||
|
||||
@@ -247,7 +247,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
JudgeManager.Instance?.SetTotalNotes(total);
|
||||
|
||||
bpm = beatmap.bpm;
|
||||
startTime = Time.time;
|
||||
startTime = GameplayClock.ChartStartSongTime;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"歌曲开始时间: {startTime}");
|
||||
|
||||
// keep reference so we can stop spawning when doing immediate settlement
|
||||
@@ -305,14 +305,12 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
float travelTime = GetLaneTravelTimeSeconds(laneTravelTimes, note.trackIndex);
|
||||
float spawnTime = note.time - travelTime;
|
||||
float delay = spawnTime - (Time.time - startTime) + spawnOffset;
|
||||
float chartSpawnTime = startTime + spawnTime + spawnOffset;
|
||||
float delay = chartSpawnTime - GameplayClock.NowSongTime;
|
||||
|
||||
if (delay > 0)
|
||||
{
|
||||
// Use scaled-time wait so spawning is paused while Time.timeScale==0 (PauseManager pause)
|
||||
float target = Time.time + delay;
|
||||
// Wait using frames so this loop respects Time.timeScale (Time.time won't advance when paused)
|
||||
while (Time.time < target)
|
||||
while (GameplayClock.NowSongTime < chartSpawnTime)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
@@ -390,8 +388,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
Transform spawnPoint = spawnPoints[noteData.trackIndex];
|
||||
|
||||
// Calculate hit time (realtime when note should be judged)
|
||||
float rawHit = startTime + noteData.time + globalHitDelay;
|
||||
float realtimeHit = Mathf.Max(0f, rawHit);
|
||||
float chartHitTime = Mathf.Max(0f, startTime + noteData.time + globalHitDelay);
|
||||
|
||||
Vector3 initialPosition = spawnPoint.position;
|
||||
note.transform.position = initialPosition;
|
||||
@@ -403,12 +400,12 @@ public class NoteSpawner : MonoBehaviour
|
||||
if (noteScript != null)
|
||||
{
|
||||
// Setup note script with timing parameters
|
||||
noteScript.Setup(key, noteData.trackIndex, noteSpeed, realtimeHit, noteData.color, judgeConfig, noteData, isSync);
|
||||
noteScript.Setup(key, noteData.trackIndex, noteSpeed, chartHitTime, noteData.color, judgeConfig, noteData, isSync);
|
||||
|
||||
// Configure controller for absolute positioning (replaces relative Translate)
|
||||
if (noteController != null)
|
||||
{
|
||||
noteController.ConfigureAbsolutePositioning(initialPosition, realtimeHit, travelTime, 0f);
|
||||
noteController.ConfigureAbsolutePositioning(initialPosition, chartHitTime, travelTime, 0f);
|
||||
}
|
||||
|
||||
EnqueueNoteCalibration(noteController);
|
||||
@@ -447,15 +444,13 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
Transform spawnPoint = spawnPoints[noteData.trackIndex];
|
||||
// Documentation text normalized.
|
||||
float rawScheduledEnd = startTime + (noteData.time + noteData.length) + globalHitDelay;
|
||||
float scheduledEndTime = Mathf.Max(0f, rawScheduledEnd); // clamp to non-negative
|
||||
float scheduledEndTime = Mathf.Max(0f, startTime + (noteData.time + noteData.length) + globalHitDelay);
|
||||
|
||||
// Documentation text normalized.
|
||||
int holdNoteId = ++holdNoteIdCounter;
|
||||
|
||||
// base realtime for hits
|
||||
float rawBase = startTime + noteData.time + globalHitDelay;
|
||||
float baseHit = Mathf.Max(0f, rawBase);
|
||||
float baseHit = Mathf.Max(0f, startTime + noteData.time + globalHitDelay);
|
||||
|
||||
GameObject startObj = notePool.GetStartNote(noteData.color);
|
||||
if (startObj == null)
|
||||
@@ -759,7 +754,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
if (now >= job.nextTime)
|
||||
{
|
||||
Vector3 expected = job.controller.GetExpectedPosition(Time.time);
|
||||
Vector3 expected = job.controller.GetExpectedPosition(GameplayClock.NowSongTime);
|
||||
float distanceDeviation = Vector3.Distance(job.obj.transform.position, expected);
|
||||
if (distanceDeviation > calibrateTolerance)
|
||||
{
|
||||
@@ -847,10 +842,10 @@ public class NoteSpawner : MonoBehaviour
|
||||
if (JudgeManager.IsDebugEnabled)
|
||||
{
|
||||
Debug.Log($"[NoteSpawner] Settlement scheduled at chartEnd+{postChartDelay:F1}s " +
|
||||
$"(chartEnd={chartEndTime:F3}, startTime={startTime:F3}, now={Time.time:F3})");
|
||||
$"(chartEnd={chartEndTime:F3}, startTime={startTime:F3}, now={GameplayClock.NowSongTime:F3})");
|
||||
}
|
||||
|
||||
while (Time.time < settlementTime)
|
||||
while (GameplayClock.NowSongTime < settlementTime)
|
||||
yield return null;
|
||||
|
||||
ForceClearInputState(null);
|
||||
|
||||
@@ -370,7 +370,7 @@ public class TrackJudgeHitEffectController : MonoBehaviour
|
||||
if (useUnscaledTime)
|
||||
yield return new WaitForSecondsRealtime(delay);
|
||||
else
|
||||
yield return new WaitForSeconds(delay);
|
||||
yield return GameplayClock.WaitForSeconds(delay);
|
||||
|
||||
if (instance == null)
|
||||
yield break;
|
||||
|
||||
@@ -205,7 +205,7 @@ public class effectEventController : MonoBehaviour
|
||||
}
|
||||
|
||||
float speed = Mathf.Max(0f, cameraDriftSpeed);
|
||||
float time = Time.time * speed;
|
||||
float time = GameplayClock.NowSongTime * speed;
|
||||
float offsetX = (Mathf.PerlinNoise(cameraDriftSeedX, time) - 0.5f) * 2f * Mathf.Abs(cameraDriftRange.x);
|
||||
float offsetY = (Mathf.PerlinNoise(cameraDriftSeedY, time) - 0.5f) * 2f * Mathf.Abs(cameraDriftRange.y);
|
||||
|
||||
|
||||
@@ -11,11 +11,7 @@ using UnityEngine;
|
||||
using Bansonic;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
|
||||
#define ARENA_ROOM_DISABLE_STEAMWORKS
|
||||
#endif
|
||||
|
||||
#if !ARENA_ROOM_DISABLE_STEAMWORKS
|
||||
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
|
||||
using Steamworks;
|
||||
#endif
|
||||
|
||||
@@ -2205,7 +2201,7 @@ namespace GameServer.Client
|
||||
return network.SteamDisplayName ?? string.Empty;
|
||||
}
|
||||
|
||||
#if ARENA_ROOM_DISABLE_STEAMWORKS
|
||||
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
|
||||
return string.Empty;
|
||||
#else
|
||||
if (SteamManager.Initialized && ulong.TryParse(senderSteamId, out ulong parsedSteamId))
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using GameServer.Client;
|
||||
@@ -115,8 +113,9 @@ public class GameServerBridge : MonoBehaviour
|
||||
}
|
||||
|
||||
// ── HMAC 签名 ──
|
||||
// 优先用握手下发的"每会话签名密钥";无会话密钥时回退到内置静态密钥(兼容旧服务端)。
|
||||
string payload = $"{songId}|{difficulty}|{totalScore}|{chartScore}|{idolScore}";
|
||||
string hmac = ComputeHmacSha256(payload, HMAC_SECRET);
|
||||
string hmac = GameServer.Client.GameServerSession.ComputeScoreHmac(payload, HMAC_SECRET);
|
||||
|
||||
// ── 上传日志(清晰可见) ──
|
||||
Debug.Log("╔══════════════════════════════════════════════════════╗");
|
||||
@@ -169,17 +168,6 @@ public class GameServerBridge : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeHmacSha256(string payload, string secret)
|
||||
{
|
||||
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
|
||||
{
|
||||
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
|
||||
var sb = new StringBuilder(hash.Length * 2);
|
||||
foreach (byte b in hash) sb.Append(b.ToString("x2"));
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertDifficulty(int i) => i switch { 0 => "ez", 1 => "hd", 2 => "in", 3 => "im", _ => "unknown" };
|
||||
private static string GetGrade(long s) => s >= 960000 ? "SSS" : s >= 920000 ? "SS" : s >= 880000 ? "S" : s >= 820000 ? "A" : s >= 720000 ? "B" : s >= 600000 ? "C" : s >= 400000 ? "D" : "F";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GameServer.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// 客户端会话状态:保存握手返回的会话令牌与"每会话签名密钥",
|
||||
/// 并用内置的服务端 RSA 公钥验证会话密钥确实来自真实服务端(明文 HTTP 下防中间人伪造)。
|
||||
///
|
||||
/// 设计要点:
|
||||
/// - session_token 用于后续请求的 Authorization: Bearer,服务端据此绑定 steam_id,杜绝伪造他人成绩。
|
||||
/// - session_key 取代内置静态 HMAC 密钥。静态密钥内置于二进制、可被逆向提取;
|
||||
/// 会话密钥每次握手随机、仅本会话有效,即使泄露也无法复用。
|
||||
/// - server_signature 用内置公钥验签。公钥泄露无害(只能验签、不能签名)。
|
||||
/// </summary>
|
||||
public static class GameServerSession
|
||||
{
|
||||
// 服务端 RSA 公钥(SubjectPublicKeyInfo / PEM)。与服务器 keys/server_public.pem 对应。
|
||||
// 公钥内置于客户端无安全风险:只能验签、不能签名。
|
||||
// 留空时跳过验签(灰度期兼容:仍可用会话密钥,只是不做来源校验)。
|
||||
private const string ServerPublicKeyPem = @"-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0lQb5hAQO7CfTTl2EScC
|
||||
+YFt11TMEUO7n4aCwBEthWnXj8WM6M7yxbUrojmM3JcqdmaQtC59VTYIralhCtbV
|
||||
cW6ACoi25vmArzc8QcOwjUPX3ZoorVp9jrVU0eIDo2qqii2eC/OKCw7bPOoIVG5A
|
||||
ZqfI1CtmkMSDQD9lg0MDpDWWdGJfwGXAznhsERl9K2F42ZPAGU+qFEz4rTg4T+SX
|
||||
oXT5PrFs0rlCjCUqoYo8s+doiFqxZGCUalk9k2h+e+uNSyVfwEopcZOcWghKlRpi
|
||||
5VVkFpmDROIXOsVVcTeXWmwR94v6dqPztsyqkjP/S2K5GHtN7ilYFe0w0xAR84DL
|
||||
ywIDAQAB
|
||||
-----END PUBLIC KEY-----";
|
||||
|
||||
private static string _sessionToken = string.Empty;
|
||||
private static string _sessionKey = string.Empty;
|
||||
private static string _expiresAtUtc = string.Empty;
|
||||
|
||||
public static string SessionToken => _sessionToken;
|
||||
public static string SessionKey => _sessionKey;
|
||||
|
||||
/// <summary>是否持有可用(未过期)的会话令牌。</summary>
|
||||
public static bool HasValidToken
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_sessionToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_expiresAtUtc))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (DateTime.TryParse(
|
||||
_expiresAtUtc,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.AdjustToUniversal | System.Globalization.DateTimeStyles.AssumeUniversal,
|
||||
out DateTime expires))
|
||||
{
|
||||
// 提前 60 秒判定过期,避免边界请求被服务端拒绝。
|
||||
return DateTime.UtcNow < expires.AddSeconds(-60);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
_sessionToken = string.Empty;
|
||||
_sessionKey = string.Empty;
|
||||
_expiresAtUtc = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用握手响应。验签失败时不保存会话密钥(回退到 legacy 静态密钥路径)。
|
||||
/// 返回是否成功建立"经过验签的"会话。
|
||||
/// </summary>
|
||||
public static bool ApplyHandshake(HandshakeResponse resp)
|
||||
{
|
||||
if (resp == null || !resp.success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 无令牌(旧服务端):什么都不做,维持 legacy 行为。
|
||||
if (string.IsNullOrWhiteSpace(resp.session_token))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 若服务端提供了签名且客户端内置了公钥,则必须验签通过才采用会话密钥。
|
||||
bool signatureRequired = !string.IsNullOrWhiteSpace(ServerPublicKeyPem)
|
||||
&& !string.IsNullOrWhiteSpace(resp.server_signature);
|
||||
if (signatureRequired)
|
||||
{
|
||||
string payload = $"{resp.steam_id}|{resp.session_token}|{resp.session_key}|{resp.expires_at}";
|
||||
if (!VerifyServerSignature(payload, resp.server_signature))
|
||||
{
|
||||
Debug.LogWarning("[GameServerSession] 服务端签名验证失败,拒绝采用该会话密钥。");
|
||||
Clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_sessionToken = resp.session_token ?? string.Empty;
|
||||
_sessionKey = resp.session_key ?? string.Empty;
|
||||
_expiresAtUtc = resp.expires_at ?? string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算成绩 HMAC:优先用会话密钥;无会话密钥时用传入的 legacy 静态密钥回退。
|
||||
/// </summary>
|
||||
public static string ComputeScoreHmac(string payload, string legacyStaticSecret)
|
||||
{
|
||||
string secret = !string.IsNullOrWhiteSpace(_sessionKey) ? _sessionKey : legacyStaticSecret;
|
||||
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
|
||||
{
|
||||
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
|
||||
var sb = new StringBuilder(hash.Length * 2);
|
||||
foreach (byte b in hash) sb.Append(b.ToString("x2"));
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool VerifyServerSignature(string payload, string signatureB64)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] signature = Convert.FromBase64String(signatureB64);
|
||||
RSAParameters parameters = ParsePublicKeyPem(ServerPublicKeyPem);
|
||||
using (var rsa = RSA.Create())
|
||||
{
|
||||
// Unity 的 .NET Standard 2.0 / Mono 运行时没有 ImportFromPem,
|
||||
// 因此手动把 SubjectPublicKeyInfo(PEM) 解析为 RSAParameters 再导入。
|
||||
rsa.ImportParameters(parameters);
|
||||
return rsa.VerifyData(
|
||||
Encoding.UTF8.GetBytes(payload),
|
||||
signature,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[GameServerSession] 验签异常: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 最小 ASN.1/DER 解析:SubjectPublicKeyInfo(PEM) -> RSAParameters ──
|
||||
// 结构: SEQUENCE { SEQUENCE { OID rsaEncryption, NULL }, BIT STRING { SEQUENCE { INTEGER modulus, INTEGER exponent } } }
|
||||
private static RSAParameters ParsePublicKeyPem(string pem)
|
||||
{
|
||||
string base64 = ExtractPemBody(pem);
|
||||
byte[] der = Convert.FromBase64String(base64);
|
||||
|
||||
int index = 0;
|
||||
ReadSequence(der, ref index); // 外层 SEQUENCE
|
||||
SkipAlgorithmIdentifier(der, ref index); // 跳过 AlgorithmIdentifier SEQUENCE
|
||||
|
||||
// BIT STRING
|
||||
ExpectTag(der, ref index, 0x03);
|
||||
int bitStringLength = ReadLength(der, ref index);
|
||||
if (bitStringLength < 1 || der[index] != 0x00)
|
||||
{
|
||||
throw new FormatException("Unexpected BIT STRING padding in public key.");
|
||||
}
|
||||
index += 1; // 跳过 BIT STRING 的未使用位计数(0x00)
|
||||
|
||||
ReadSequence(der, ref index); // RSAPublicKey SEQUENCE
|
||||
byte[] modulus = ReadIntegerUnsigned(der, ref index);
|
||||
byte[] exponent = ReadIntegerUnsigned(der, ref index);
|
||||
|
||||
return new RSAParameters { Modulus = modulus, Exponent = exponent };
|
||||
}
|
||||
|
||||
private static string ExtractPemBody(string pem)
|
||||
{
|
||||
var sb = new StringBuilder(pem.Length);
|
||||
using (var reader = new System.IO.StringReader(pem))
|
||||
{
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
string trimmed = line.Trim();
|
||||
if (trimmed.Length == 0 || trimmed.StartsWith("-----"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sb.Append(trimmed);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void ExpectTag(byte[] data, ref int index, byte tag)
|
||||
{
|
||||
if (index >= data.Length || data[index] != tag)
|
||||
{
|
||||
throw new FormatException($"Expected ASN.1 tag 0x{tag:X2} at offset {index}.");
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
private static void ReadSequence(byte[] data, ref int index)
|
||||
{
|
||||
ExpectTag(data, ref index, 0x30);
|
||||
ReadLength(data, ref index);
|
||||
}
|
||||
|
||||
private static void SkipAlgorithmIdentifier(byte[] data, ref int index)
|
||||
{
|
||||
ExpectTag(data, ref index, 0x30);
|
||||
int length = ReadLength(data, ref index);
|
||||
index += length; // 整段 AlgorithmIdentifier 内容不需要
|
||||
}
|
||||
|
||||
private static int ReadLength(byte[] data, ref int index)
|
||||
{
|
||||
int first = data[index];
|
||||
index += 1;
|
||||
if ((first & 0x80) == 0)
|
||||
{
|
||||
return first; // 短格式
|
||||
}
|
||||
|
||||
int byteCount = first & 0x7F;
|
||||
if (byteCount == 0 || byteCount > 4)
|
||||
{
|
||||
throw new FormatException("Unsupported ASN.1 length encoding.");
|
||||
}
|
||||
|
||||
int length = 0;
|
||||
for (int i = 0; i < byteCount; i++)
|
||||
{
|
||||
length = (length << 8) | data[index];
|
||||
index += 1;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
private static byte[] ReadIntegerUnsigned(byte[] data, ref int index)
|
||||
{
|
||||
ExpectTag(data, ref index, 0x02);
|
||||
int length = ReadLength(data, ref index);
|
||||
int start = index;
|
||||
index += length;
|
||||
|
||||
// 去掉 DER 正整数为避免歧义而添加的前导 0x00 符号字节
|
||||
while (length > 1 && data[start] == 0x00)
|
||||
{
|
||||
start += 1;
|
||||
length -= 1;
|
||||
}
|
||||
|
||||
byte[] result = new byte[length];
|
||||
Array.Copy(data, start, result, 0, length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79cbd031dc2447d4789e769442806868
|
||||
@@ -49,6 +49,15 @@ namespace GameServer.Client
|
||||
[JsonProperty("steam_id")] public string steam_id;
|
||||
[JsonProperty("error_code")] public string error_code;
|
||||
[JsonProperty("message")] public string message;
|
||||
// 会话令牌:后续请求以 Authorization: Bearer 携带,服务端据此绑定 steam_id。
|
||||
[JsonProperty("session_token")] public string session_token;
|
||||
// 每会话一次性签名密钥:客户端用它对成绩做 HMAC,取代内置静态密钥。
|
||||
[JsonProperty("session_key")] public string session_key;
|
||||
// 会话过期时间(UTC ISO8601)。
|
||||
[JsonProperty("expires_at")] public string expires_at;
|
||||
// 服务端 RSA 签名(base64),客户端用内置公钥验签以确认会话密钥来自真实服务端。
|
||||
[JsonProperty("server_signature")] public string server_signature;
|
||||
[JsonProperty("sign_alg")] public string sign_alg;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -9,11 +9,7 @@ using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using Bansonic;
|
||||
|
||||
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
|
||||
#define NETWORK_DISABLE_STEAMWORKS
|
||||
#endif
|
||||
|
||||
#if !NETWORK_DISABLE_STEAMWORKS
|
||||
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
|
||||
using Steamworks;
|
||||
#endif
|
||||
|
||||
@@ -32,7 +28,7 @@ public class NetworkManager : MonoBehaviour
|
||||
public const string SteamAvatarUrlPrefix = "steam-avatar://";
|
||||
|
||||
[Header("Server")]
|
||||
[SerializeField] private string serverUrl = "http://47.112.187.172:8080";
|
||||
[SerializeField] private string serverUrl = "https://game.bansonic.top";
|
||||
|
||||
[Header("Auth")]
|
||||
public string steamId = "";
|
||||
@@ -820,6 +816,13 @@ public class NetworkManager : MonoBehaviour
|
||||
SetState(ConnectionState.Handshaking);
|
||||
|
||||
HandshakeResponse resp = await PostJson<HandshakeResponse>(BuildApiUrl("/api/handshake"), req, token);
|
||||
|
||||
// 存储会话令牌与每会话签名密钥(先用内置 RSA 公钥验签,确认来自真实服务端)。
|
||||
if (resp != null && resp.success)
|
||||
{
|
||||
GameServerSession.ApplyHandshake(resp);
|
||||
}
|
||||
|
||||
OnHandshakeResult?.Invoke(resp);
|
||||
|
||||
if (resp != null && resp.success)
|
||||
@@ -1350,7 +1353,7 @@ public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
currentSteamId = string.Empty;
|
||||
currentDisplayName = string.Empty;
|
||||
#if NETWORK_DISABLE_STEAMWORKS
|
||||
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
|
||||
if (logWarnings)
|
||||
{
|
||||
Debug.LogWarning("[NetworkManager] Steamworks is unavailable on this platform. Using current serialized network identity.");
|
||||
@@ -1396,7 +1399,7 @@ public class NetworkManager : MonoBehaviour
|
||||
private static bool TryReadSteamPersona(string targetSteamId, out string displayName)
|
||||
{
|
||||
displayName = string.Empty;
|
||||
#if NETWORK_DISABLE_STEAMWORKS
|
||||
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
|
||||
return false;
|
||||
#else
|
||||
if (string.IsNullOrWhiteSpace(targetSteamId) || !SteamManager.Initialized || !ulong.TryParse(targetSteamId, out ulong parsedSteamId))
|
||||
@@ -1426,7 +1429,7 @@ public class NetworkManager : MonoBehaviour
|
||||
|
||||
private async Task<byte[]> GetLocalSteamAvatarPngAsync()
|
||||
{
|
||||
#if NETWORK_DISABLE_STEAMWORKS
|
||||
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
|
||||
return null;
|
||||
#else
|
||||
if (!SteamManager.Initialized)
|
||||
@@ -1532,6 +1535,20 @@ public class NetworkManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyAuthHeader(UnityWebRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string sessionToken = GameServerSession.SessionToken;
|
||||
if (!string.IsNullOrWhiteSpace(sessionToken))
|
||||
{
|
||||
request.SetRequestHeader("Authorization", "Bearer " + sessionToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<T> PostJson<T>(string url, object payload, CancellationToken token)
|
||||
{
|
||||
if (OnlineModeSettings.IsLocalOnlyMode)
|
||||
@@ -1546,6 +1563,7 @@ public class NetworkManager : MonoBehaviour
|
||||
request.uploadHandler = new UploadHandlerRaw(body);
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
ApplyAuthHeader(request);
|
||||
request.timeout = 15;
|
||||
|
||||
if (VerboseLogs) Debug.Log($"[NetworkManager] HTTP POST {url}");
|
||||
@@ -1582,6 +1600,7 @@ public class NetworkManager : MonoBehaviour
|
||||
request.uploadHandler = new UploadHandlerRaw(body);
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
ApplyAuthHeader(request);
|
||||
request.timeout = 15;
|
||||
|
||||
if (VerboseLogs) Debug.Log($"[NetworkManager] HTTP PUT {url}");
|
||||
@@ -1613,6 +1632,7 @@ public class NetworkManager : MonoBehaviour
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
ApplyAuthHeader(request);
|
||||
request.timeout = 15;
|
||||
if (VerboseLogs) Debug.Log($"[NetworkManager] HTTP GET {url}");
|
||||
await SendRequestAsync(request.SendWebRequest(), token);
|
||||
|
||||
@@ -46,7 +46,7 @@ public class budeff_appr : MonoBehaviour
|
||||
private IEnumerator Animate()
|
||||
{
|
||||
float delay = Mathf.Max(0f, floatDelay);
|
||||
if (delay > 0f) yield return new WaitForSeconds(delay);
|
||||
if (delay > 0f) yield return GameplayClock.WaitForSeconds(delay);
|
||||
|
||||
float duration = Mathf.Max(0.0001f, fadeDuration);
|
||||
bool useAnchored = _rectTransform != null;
|
||||
|
||||
+8
-6
@@ -7,6 +7,8 @@ using UnityEngine.UI;
|
||||
[DefaultExecutionOrder(-1000)]
|
||||
public class iBudeffPrefabController : MonoBehaviour
|
||||
{
|
||||
private static float GameplayNow => Application.isPlaying ? GameplayClock.NowSongTime : Time.realtimeSinceStartup;
|
||||
|
||||
public static iBudeffPrefabController Instance { get; private set; }
|
||||
|
||||
[Header("budeff Icons Prefab")]
|
||||
@@ -129,7 +131,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
if (ally == null || buff == null) return;
|
||||
if (!Application.isPlaying) return;
|
||||
if (!IsTrackableBuff(buff)) return;
|
||||
RegisterEntry(ally, GetGroupForBuff(buff), buff.buffId, 0f, Time.time);
|
||||
RegisterEntry(ally, GetGroupForBuff(buff), buff.buffId, 0f, GameplayNow);
|
||||
}
|
||||
|
||||
public void NotifyBuffRemoved(AllyCombatant ally, Buff buff)
|
||||
@@ -170,7 +172,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
if (!Application.isPlaying) return;
|
||||
if (!IsTrackableEnemyBuff(buff)) return;
|
||||
Debug.LogWarning($"[iBudeffPrefabController] Enemy buff applied: enemy={enemy.name} id={enemy.GetInstanceID()} buffId={buff.buffId} desc={buff.description} atkMult={buff.attackMultiplier} healMult={buff.healReceivedMultiplier} scoreMult={buff.scoreMultiplier}");
|
||||
RegisterEnemyEntry(enemy, GetGroupForEnemyBuff(buff), buff.buffId, 0f, Time.time);
|
||||
RegisterEnemyEntry(enemy, GetGroupForEnemyBuff(buff), buff.buffId, 0f, GameplayNow);
|
||||
}
|
||||
|
||||
public void NotifyEnemyBuffRemoved(EnemyCombatant enemy, Buff buff)
|
||||
@@ -188,7 +190,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
var group = GetGroupForIconType(type);
|
||||
string id = Guid.NewGuid().ToString();
|
||||
Debug.LogWarning($"[iBudeffPrefabController] Enemy timed effect: enemy={enemy.name} id={enemy.GetInstanceID()} type={type} value={value} duration={duration} group={group}");
|
||||
RegisterEnemyEntry(enemy, group, id, value, Time.time);
|
||||
RegisterEnemyEntry(enemy, group, id, value, GameplayNow);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -206,7 +208,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
|
||||
var group = GetGroupForIconType(type);
|
||||
string id = Guid.NewGuid().ToString();
|
||||
RegisterEntry(ally, group, id, value, Time.time, duration);
|
||||
RegisterEntry(ally, group, id, value, GameplayNow, duration);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1291,7 +1293,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
{
|
||||
if (go == null || cg == null) yield break;
|
||||
cg.alpha = (i % 2 == 0) ? 1f : 0.08f;
|
||||
yield return new WaitForSeconds(step);
|
||||
yield return GameplayClock.WaitForSeconds(step);
|
||||
}
|
||||
|
||||
if (go != null && cg != null) cg.alpha = 1f;
|
||||
@@ -1310,7 +1312,7 @@ public class iBudeffPrefabController : MonoBehaviour
|
||||
{
|
||||
if (go == null || cg == null) yield break;
|
||||
cg.alpha = (i % 2 == 0) ? 0.08f : 1f;
|
||||
yield return new WaitForSeconds(step);
|
||||
yield return GameplayClock.WaitForSeconds(step);
|
||||
}
|
||||
|
||||
if (go != null && cg != null) cg.alpha = 0f;
|
||||
|
||||
@@ -229,7 +229,7 @@ public class trackFractureController : MonoBehaviour
|
||||
// float1: hold before the dissolve begins. Fragments have already started drifting during this time.
|
||||
if (fractureFadeDelay > 0f)
|
||||
{
|
||||
yield return new WaitForSeconds(fractureFadeDelay);
|
||||
yield return GameplayClock.WaitForSeconds(fractureFadeDelay);
|
||||
}
|
||||
|
||||
// float2: drive _Fade from the configured start value to the configured end value over this duration.
|
||||
|
||||
Reference in New Issue
Block a user