一些修复和优化

This commit is contained in:
FloatGaming
2026-07-20 02:22:51 +08:00
parent 818fc9a02f
commit 0a0872c4f4
49 changed files with 2008 additions and 262 deletions
@@ -86,6 +86,12 @@ public class NoteSpawner : MonoBehaviour
// optional: constants for runtime clamping (kept for internal use)
private const float SpeedMultiplierMin = 0.5f;
private const float SpeedMultiplierMax = 2f;
private const float FlowSpeedGlobalMultiplier = 1.5f;
public float EffectiveSpeedMultiplier
{
get { return Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax) * FlowSpeedGlobalMultiplier; }
}
public NoteJudgeConfig judgeConfig; // Documentation text normalized.
private Beatmap beatmap;
@@ -264,7 +270,7 @@ public class NoteSpawner : MonoBehaviour
}
// Cache parameters that don't change within the loop
float sm = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax);
float sm = EffectiveSpeedMultiplier;
float baseTravelTime = (60f / bpm) * 4f;
float baseNoteTravelTime = baseTravelTime / Mathf.Max(0.0001f, sm);
float noteSpeed = CalculateSpeed(baseNoteTravelTime);
@@ -60,6 +60,13 @@ public class groundParticularController : MonoBehaviour
[SerializeField] private float speedMultiplier = 0.1f;
[Tooltip("发射速度倍增器")]
[SerializeField] private float emissionSpeedMultiplier = 1f;
[SerializeField] private float perFrameMoveMultiplier = 0.01f;
[Header("Rotation Randomness")]
[Tooltip("If enabled, each spawned particle gets a random initial rotation offset.")]
[SerializeField] private bool enableRandomRotation = false;
[Tooltip("Random initial rotation range in degrees. Each axis is offset from -range to +range.")]
[SerializeField] private float randomRotationRangeDegrees = 0f;
[Tooltip("起点缩放抖动强度 (基于音频电平)")]
[SerializeField] private float shakeIntensity = 0.1f;
@@ -214,7 +221,7 @@ public class groundParticularController : MonoBehaviour
Random.Range(-spawnOffsetY, spawnOffsetY)
);
particle.transform.position = startPoint.TransformPoint(finalLocalPos);
particle.transform.rotation = startPoint.rotation;
particle.transform.rotation = GetSpawnRotation();
// 5. 材质均衡选取逻辑
MaterialConfig? selectedConfig = GetBalancedMaterialConfig();
@@ -251,6 +258,24 @@ public class groundParticularController : MonoBehaviour
/// <summary>
/// 轮询式均衡选取材质,确保每种材质数量大致相同
/// </summary>
private Quaternion GetSpawnRotation()
{
if (!enableRandomRotation || randomRotationRangeDegrees <= 0f || startPoint == null)
{
return startPoint != null ? startPoint.rotation : Quaternion.identity;
}
float range = Mathf.Abs(randomRotationRangeDegrees);
Vector3 randomEuler = new Vector3(
Random.Range(-range, range),
Random.Range(-range, range),
Random.Range(-range, range)
);
return startPoint.rotation * Quaternion.Euler(randomEuler);
}
private MaterialConfig? GetBalancedMaterialConfig()
{
if (particleMaterials == null || particleMaterials.Count == 0) return null;
@@ -301,13 +326,13 @@ public class groundParticularController : MonoBehaviour
float noteSpawnerSpeedMultiplier = 1.0f;
if (beatmapManager != null && beatmapManager.noteSpawner != null)
{
noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.speedMultiplier;
noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.EffectiveSpeedMultiplier;
}
float effectiveBPM = Mathf.Max(currentBPM, 60f);
float noteSpeed = (totalDist * effectiveBPM * noteSpawnerSpeedMultiplier) / 240f;
float step = noteSpeed * speedMultiplier * emissionSpeedMultiplier * Time.deltaTime;
float step = noteSpeed * speedMultiplier * emissionSpeedMultiplier * perFrameMoveMultiplier * Time.deltaTime;
if (step <= 0) step = 0.01f;
traveledDist += step;
@@ -16,10 +16,11 @@ public class GameServerBridge : MonoBehaviour
[Header("配置")]
[SerializeField] private bool autoSubmitOnSettlement = true;
private const string HMAC_SECRET = "your_secret_key_change_this_in_production";
private const string HMAC_SECRET = "1d95faf15dca25edf0342014428f7dbbd5785ba993113c687b1b86c207b000e0";
private bool _hasSubmitted = false;
private float _gameStartTime;
private int _currentRetryCount = 0;
private void Awake()
{
@@ -31,6 +32,7 @@ public class GameServerBridge : MonoBehaviour
{
_gameStartTime = Time.realtimeSinceStartup;
_hasSubmitted = false;
_currentRetryCount = 0;
if (autoSubmitOnSettlement)
settlementController.OnSettlementCompleted += OnSettlementTriggered;
}
@@ -141,24 +143,31 @@ public class GameServerBridge : MonoBehaviour
Debug.Log("╔══════════════════════════════════════════════════════╗");
Debug.Log("║ ✅✅✅ 上传成功!数据已写入服务器数据库 ✅✅✅ ║");
Debug.Log("╚══════════════════════════════════════════════════════╝");
_currentRetryCount = 0;
}
else if (result == "QUEUED")
{
Debug.LogWarning("[Bridge] ⏳ 服务器繁忙,3秒后重试...");
Debug.LogWarning("[Bridge] ⏳ 服务器繁忙,后重试...");
_hasSubmitted = false;
await Task.Delay(3000);
SubmitCurrentSettlement();
await RetrySubmission(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
}
else if (result == "OPT_OUT")
{
Debug.Log("[Bridge] 当前已关闭排行榜加入选项,未提交世界排行榜。");
_currentRetryCount = 0;
}
else
else if (result == "ERROR" || result == "REJECTED")
{
Debug.LogWarning($"╔══════════════════════════════════════════════════════╗");
Debug.LogWarning($"║ ❌ 上传失败!服务器返回: {result}");
Debug.LogWarning($"╚══════════════════════════════════════════════════════╝");
_hasSubmitted = false;
await RetrySubmission(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
}
else
{
Debug.LogWarning($"[Bridge] 未知返回状态: {result}");
_hasSubmitted = false;
}
}
catch (Exception ex)
@@ -168,6 +177,58 @@ public class GameServerBridge : MonoBehaviour
}
}
/// <summary>
/// 重试上传逻辑:3次重试,每次间隔20秒,失败后缓存到本地
/// </summary>
private async Task RetrySubmission(string songId, string difficulty, int chartScore, int idolScore,
long totalScore, string grade, double runtime, string playedAt, string hmac)
{
_currentRetryCount++;
if (_currentRetryCount >= 3)
{
Debug.LogWarning($"[Bridge] 已重试 {_currentRetryCount} 次仍失败,缓存到本地待下次启动重试");
PendingScoreCache.CacheScore(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
_currentRetryCount = 0;
return;
}
Debug.Log($"[Bridge] 等待 20 秒后进行第 {_currentRetryCount} 次重试...");
await Task.Delay(20000);
var nm = NetworkManager.Instance;
if (nm == null)
{
Debug.LogWarning("[Bridge] NetworkManager 不存在,缓存到本地");
PendingScoreCache.CacheScore(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
_currentRetryCount = 0;
return;
}
Debug.Log($"[Bridge] 开始第 {_currentRetryCount} 次重试上传...");
try
{
string result = await nm.PushSettlement(songId, difficulty, chartScore, idolScore,
totalScore, grade, runtime, playedAt, hmac);
if (result == "OK")
{
Debug.Log($"[Bridge] ✅ 第 {_currentRetryCount} 次重试成功!");
_currentRetryCount = 0;
}
else
{
Debug.LogWarning($"[Bridge] 第 {_currentRetryCount} 次重试失败: {result}");
await RetrySubmission(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
}
}
catch (Exception ex)
{
Debug.LogError($"[Bridge] 第 {_currentRetryCount} 次重试异常: {ex.Message}");
await RetrySubmission(songId, difficulty, chartScore, idolScore, totalScore, grade, runtime, playedAt, hmac);
}
}
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";
}
@@ -18,6 +18,9 @@ namespace GameServer.Client
public class NetworkManager : MonoBehaviour
{
private static readonly bool VerboseLogs = false;
private const int PendingScoreRetryAttemptsPerLaunch = 3;
private const float PendingScoreRetryDelaySeconds = 20f;
private const string LegacyScoreHmacSecret = "1d95faf15dca25edf0342014428f7dbbd5785ba993113c687b1b86c207b000e0";
public static NetworkManager Instance { get; private set; }
private static string _startupSteamId = string.Empty;
private static string _startupSteamDisplayName = string.Empty;
@@ -55,6 +58,8 @@ public class NetworkManager : MonoBehaviour
private bool _startupHandshakeCompleted;
private Task<string> _avatarUploadTask;
private bool _isRefreshingSteamIdentity;
private Coroutine _pendingScoreRetryCoroutine;
private bool _isRetryingPendingScores;
private struct CachedRemoteIdentity
{
@@ -151,6 +156,7 @@ public class NetworkManager : MonoBehaviour
}
StartCoroutine(StartupHandshakeRoutine());
_pendingScoreRetryCoroutine = StartCoroutine(RetryPendingScoresRoutine());
if (VerboseLogs) Debug.Log($"[NetworkManager] Startup handshake enabled. Settlement submit will use {BuildApiUrl("/api/submit")}");
}
@@ -163,12 +169,137 @@ public class NetworkManager : MonoBehaviour
{
_requestCts?.Cancel();
OnlineModeSettings.ModeChanged -= HandleOnlineModeChanged;
if (_pendingScoreRetryCoroutine != null)
{
StopCoroutine(_pendingScoreRetryCoroutine);
_pendingScoreRetryCoroutine = null;
}
if (!_isApplicationQuitting && Instance == this)
{
Instance = null;
}
}
private IEnumerator RetryPendingScoresRoutine()
{
if (_isRetryingPendingScores)
{
yield break;
}
_isRetryingPendingScores = true;
yield return null;
List<PendingScoreData> pendingScores = PendingScoreCache.GetPendingScores();
if (pendingScores == null || pendingScores.Count == 0)
{
_isRetryingPendingScores = false;
_pendingScoreRetryCoroutine = null;
yield break;
}
if (VerboseLogs)
{
Debug.Log($"[NetworkManager] Found {pendingScores.Count} cached score(s) to retry on startup.");
}
for (int i = 0; i < pendingScores.Count; i++)
{
if (_isApplicationQuitting || OnlineModeSettings.IsLocalOnlyMode)
{
break;
}
PendingScoreData score = pendingScores[i];
if (score == null || string.IsNullOrWhiteSpace(score.songId))
{
continue;
}
bool uploaded = false;
for (int attempt = 1; attempt <= PendingScoreRetryAttemptsPerLaunch; attempt++)
{
string hmac = BuildScoreHmac(score);
Task<string> task = PushSettlement(
score.songId,
score.difficulty,
score.chartScore,
score.idolScore,
score.totalScore,
score.grade,
score.runtimeSeconds,
score.playedAt,
hmac);
while (!task.IsCompleted)
{
if (_isApplicationQuitting)
{
_isRetryingPendingScores = false;
_pendingScoreRetryCoroutine = null;
yield break;
}
yield return null;
}
string status = "ERROR";
if (task.Status == TaskStatus.RanToCompletion && !string.IsNullOrWhiteSpace(task.Result))
{
status = task.Result;
}
else if (task.IsFaulted)
{
Debug.LogWarning($"[NetworkManager] Pending score retry faulted for songId={score.songId}: {task.Exception?.GetBaseException().Message}");
}
if (string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase))
{
PendingScoreCache.RemoveScore(score.songId, score.totalScore, score.playedAt);
uploaded = true;
break;
}
if (string.Equals(status, "OPT_OUT", StringComparison.OrdinalIgnoreCase)
|| string.Equals(status, "LOCAL_ONLY", StringComparison.OrdinalIgnoreCase))
{
if (VerboseLogs)
{
Debug.Log($"[NetworkManager] Skip retrying cached score songId={score.songId} because status={status}.");
}
break;
}
PendingScoreCache.IncrementRetryCount(score.songId, score.totalScore, score.playedAt);
if (attempt < PendingScoreRetryAttemptsPerLaunch)
{
yield return new WaitForSecondsRealtime(PendingScoreRetryDelaySeconds);
}
}
if (!uploaded && VerboseLogs)
{
Debug.Log($"[NetworkManager] Cached score kept for next launch. songId={score.songId} totalScore={score.totalScore}");
}
}
_isRetryingPendingScores = false;
_pendingScoreRetryCoroutine = null;
}
private static string BuildScoreHmac(PendingScoreData score)
{
if (score == null)
{
return string.Empty;
}
string payload = $"{score.songId}|{score.difficulty}|{score.totalScore}|{score.chartScore}|{score.idolScore}";
return GameServerSession.ComputeScoreHmac(payload, LegacyScoreHmacSecret);
}
public void Connect()
{
if (OnlineModeSettings.IsLocalOnlyMode)
@@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using UnityEngine;
namespace GameServer.Client
{
/// <summary>
/// Stores score submissions that could not be uploaded because of network issues,
/// then retries them on a later startup.
/// </summary>
public static class PendingScoreCache
{
[Serializable]
private class CachedScore
{
public string songId;
public string difficulty;
public int chartScore;
public int idolScore;
public long totalScore;
public string grade;
public double runtimeSeconds;
public string playedAt;
public string hmac;
public int retryCount;
public string cachedAt;
}
[Serializable]
private class CacheContainer
{
public List<CachedScore> scores = new List<CachedScore>();
}
private static readonly string CachePath = Path.Combine(Application.persistentDataPath, "pending_scores.json");
private static readonly object FileLock = new object();
public static void CacheScore(
string songId,
string difficulty,
int chartScore,
int idolScore,
long totalScore,
string grade,
double runtimeSeconds,
string playedAt,
string hmac)
{
lock (FileLock)
{
try
{
CacheContainer container = LoadContainer();
CachedScore existing = FindScore(container, songId, totalScore, playedAt);
if (existing != null)
{
existing.difficulty = difficulty;
existing.chartScore = chartScore;
existing.idolScore = idolScore;
existing.grade = grade;
existing.runtimeSeconds = runtimeSeconds;
existing.hmac = hmac;
if (string.IsNullOrWhiteSpace(existing.cachedAt))
{
existing.cachedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
}
else
{
container.scores.Add(new CachedScore
{
songId = songId,
difficulty = difficulty,
chartScore = chartScore,
idolScore = idolScore,
totalScore = totalScore,
grade = grade,
runtimeSeconds = runtimeSeconds,
playedAt = playedAt,
hmac = hmac,
retryCount = 0,
cachedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
});
}
SaveContainer(container);
Debug.Log($"[PendingScoreCache] Cached score locally. songId={songId} totalScore={totalScore}");
}
catch (Exception ex)
{
Debug.LogError($"[PendingScoreCache] Failed to cache score: {ex.Message}");
}
}
}
public static List<PendingScoreData> GetPendingScores()
{
lock (FileLock)
{
try
{
CacheContainer container = LoadContainer();
var result = new List<PendingScoreData>(container.scores.Count);
for (int i = 0; i < container.scores.Count; i++)
{
CachedScore cached = container.scores[i];
if (cached == null)
{
continue;
}
result.Add(new PendingScoreData
{
songId = cached.songId,
difficulty = cached.difficulty,
chartScore = cached.chartScore,
idolScore = cached.idolScore,
totalScore = cached.totalScore,
grade = cached.grade,
runtimeSeconds = cached.runtimeSeconds,
playedAt = cached.playedAt,
hmac = cached.hmac,
retryCount = cached.retryCount
});
}
return result;
}
catch (Exception ex)
{
Debug.LogError($"[PendingScoreCache] Failed to read cache: {ex.Message}");
return new List<PendingScoreData>();
}
}
}
public static void RemoveScore(string songId, long totalScore, string playedAt = null)
{
lock (FileLock)
{
try
{
CacheContainer container = LoadContainer();
int removed = container.scores.RemoveAll(score =>
score != null
&& string.Equals(score.songId, songId, StringComparison.Ordinal)
&& score.totalScore == totalScore
&& (string.IsNullOrWhiteSpace(playedAt)
|| string.Equals(score.playedAt, playedAt, StringComparison.Ordinal)));
if (removed <= 0)
{
return;
}
SaveContainer(container);
Debug.Log($"[PendingScoreCache] Removed uploaded cached score. songId={songId} totalScore={totalScore}");
}
catch (Exception ex)
{
Debug.LogError($"[PendingScoreCache] Failed to remove cached score: {ex.Message}");
}
}
}
public static void IncrementRetryCount(string songId, long totalScore, string playedAt = null)
{
lock (FileLock)
{
try
{
CacheContainer container = LoadContainer();
CachedScore score = FindScore(container, songId, totalScore, playedAt);
if (score == null)
{
return;
}
score.retryCount = Mathf.Max(0, score.retryCount) + 1;
SaveContainer(container);
}
catch (Exception ex)
{
Debug.LogError($"[PendingScoreCache] Failed to update retry count: {ex.Message}");
}
}
}
public static void ClearAll()
{
lock (FileLock)
{
try
{
if (File.Exists(CachePath))
{
File.Delete(CachePath);
Debug.Log("[PendingScoreCache] Cleared all cached scores.");
}
}
catch (Exception ex)
{
Debug.LogError($"[PendingScoreCache] Failed to clear cache: {ex.Message}");
}
}
}
public static int GetCachedCount()
{
lock (FileLock)
{
try
{
return LoadContainer().scores.Count;
}
catch
{
return 0;
}
}
}
private static CacheContainer LoadContainer()
{
if (!File.Exists(CachePath))
{
return new CacheContainer();
}
try
{
string json = File.ReadAllText(CachePath);
return JsonConvert.DeserializeObject<CacheContainer>(json) ?? new CacheContainer();
}
catch (Exception ex)
{
Debug.LogWarning($"[PendingScoreCache] Failed to read cache file, creating a fresh container: {ex.Message}");
return new CacheContainer();
}
}
private static void SaveContainer(CacheContainer container)
{
string json = JsonConvert.SerializeObject(container ?? new CacheContainer(), Formatting.Indented);
File.WriteAllText(CachePath, json);
}
private static CachedScore FindScore(CacheContainer container, string songId, long totalScore, string playedAt)
{
if (container == null || container.scores == null)
{
return null;
}
for (int i = 0; i < container.scores.Count; i++)
{
CachedScore score = container.scores[i];
if (score == null)
{
continue;
}
bool playedAtMatches = string.IsNullOrWhiteSpace(playedAt)
|| string.Equals(score.playedAt, playedAt, StringComparison.Ordinal);
if (string.Equals(score.songId, songId, StringComparison.Ordinal)
&& score.totalScore == totalScore
&& playedAtMatches)
{
return score;
}
}
return null;
}
}
[Serializable]
public class PendingScoreData
{
public string songId;
public string difficulty;
public int chartScore;
public int idolScore;
public long totalScore;
public string grade;
public double runtimeSeconds;
public string playedAt;
public string hmac;
public int retryCount;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 838755ae207aa0547ae9471fcf9d052f