客户端rsa,冗余清理,bug修复,安卓build问题

This commit is contained in:
FloatGaming
2026-07-14 01:43:49 +08:00
parent fd22501f71
commit f8985d91d2
682 changed files with 4595 additions and 11216 deletions
+24 -22
View File
@@ -15,6 +15,8 @@ using TMPro;
/// </summary>
public class AllyCombatant : MonoBehaviour, ICombatant
{
private static float GameplayNow => Application.isPlaying ? GameplayClock.NowSongTime : Time.realtimeSinceStartup;
private static AllyHero_SO[] cachedAllAllyHeroes;
public int slotIndex = 0; // Documentation text normalized.
@@ -209,7 +211,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
if (Application.isPlaying && skillIconAutoFadeTime > 0f && lastSkillIconPushTime > 0f)
{
// Only start coroutine if not already running and time exceeded
if (skillIconAutoFadeCoroutine == null && Time.time - lastSkillIconPushTime >= skillIconAutoFadeTime)
if (skillIconAutoFadeCoroutine == null && GameplayNow - lastSkillIconPushTime >= skillIconAutoFadeTime)
{
// Double check visibility
bool anyVisible = false;
@@ -238,23 +240,23 @@ public class AllyCombatant : MonoBehaviour, ICombatant
TryCastOnFullMana();
}
if (_selfDamageRedirectToAdjacentExpireTime > 0f && Time.time > _selfDamageRedirectToAdjacentExpireTime)
if (_selfDamageRedirectToAdjacentExpireTime > 0f && GameplayNow > _selfDamageRedirectToAdjacentExpireTime)
{
ConsumeSelfDamageRedirectToAdjacent();
}
if (_rewriteNonMissToPerfectExpireTime > 0f && Time.time > _rewriteNonMissToPerfectExpireTime)
if (_rewriteNonMissToPerfectExpireTime > 0f && GameplayNow > _rewriteNonMissToPerfectExpireTime)
{
ClearNonMissToPerfectRewrite();
}
else if (_rewriteNonMissToPerfectExpireTime > 0f &&
Time.time <= _rewriteNonMissToPerfectExpireTime &&
GameplayNow <= _rewriteNonMissToPerfectExpireTime &&
string.IsNullOrEmpty(_rewriteNonMissToPerfectIconId) &&
iBudeffPrefabController.Instance != null)
{
// Fallback: if icon registration failed earlier (e.g. controller initialized later),
// retry during active window.
float remain = Mathf.Max(0.01f, _rewriteNonMissToPerfectExpireTime - Time.time);
float remain = Mathf.Max(0.01f, _rewriteNonMissToPerfectExpireTime - GameplayNow);
_rewriteNonMissToPerfectIconId = iBudeffPrefabController.Instance.RegisterTimedEffect(this, PlayerBudeffIconType.ot_luckyChance, 1f, remain);
iBudeffPrefabController.Instance.RefreshAllyNow(this);
}
@@ -728,7 +730,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
if (skillIconSlots == null || skillIconSlots.Length == 0) return;
// Reset auto-fade logic
lastSkillIconPushTime = Time.time;
lastSkillIconPushTime = GameplayNow;
if (skillIconAutoFadeCoroutine != null)
{
StopCoroutine(skillIconAutoFadeCoroutine);
@@ -837,7 +839,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
skillIconOccupied[i] = false;
// Wait small interval between fades
yield return new WaitForSeconds(0.15f);
yield return GameplayClock.WaitForSeconds(0.15f);
}
}
@@ -1887,7 +1889,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
}
activeBuffs.Add(buff);
_buffAppliedTimes[buff.buffId] = Application.isPlaying ? Time.time : Time.realtimeSinceStartup;
_buffAppliedTimes[buff.buffId] = GameplayNow;
// Apply reversible multipliers. attackMultiplier is applied via baseline+recalc to ensure it can be reverted.
if (buff.scoreMultiplier != 1f) scoreEfficiency *= buff.scoreMultiplier;
if (buff.attackMultiplier != 1f) RecalculateAttackFromBuffs();
@@ -2539,7 +2541,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
float expireTime = -1f;
if (duration > 0f && Application.isPlaying)
{
expireTime = Time.time + duration;
expireTime = GameplayNow + duration;
}
NextDamageRedirectState state = null;
@@ -2577,7 +2579,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
bool remove = redirector == null;
if (!remove && (redirector.IsDead || !redirector.gameObject.activeInHierarchy))
remove = true;
if (!remove && Application.isPlaying && state.expireTime > 0f && Time.time > state.expireTime)
if (!remove && Application.isPlaying && state.expireTime > 0f && GameplayNow > state.expireTime)
remove = true;
if (!remove) continue;
@@ -2703,12 +2705,12 @@ public class AllyCombatant : MonoBehaviour, ICombatant
if (string.IsNullOrWhiteSpace(buff.buffId))
{
buff.buffId = Guid.NewGuid().ToString();
_buffAppliedTimes[buff.buffId] = Application.isPlaying ? Time.time : Time.realtimeSinceStartup;
_buffAppliedTimes[buff.buffId] = GameplayNow;
}
float t = 0f;
if (!_buffAppliedTimes.TryGetValue(buff.buffId, out t))
{
t = Application.isPlaying ? Time.time : Time.realtimeSinceStartup;
t = GameplayNow;
_buffAppliedTimes[buff.buffId] = t;
}
if (latest == null || t > latestTime)
@@ -2747,7 +2749,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
{
if (!_buffAppliedTimes.TryGetValue(objectBuff.buffId, out objectBuffTime))
{
objectBuffTime = Application.isPlaying ? Time.time : Time.realtimeSinceStartup;
objectBuffTime = GameplayNow;
_buffAppliedTimes[objectBuff.buffId] = objectBuffTime;
}
}
@@ -2958,7 +2960,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
// Compensate at expiry: source +delta, target -delta, to keep net transfer behavior correct.
if (duration > 0f)
{
float remaining = Mathf.Max(0.01f, (startTime + duration) - Time.time);
float remaining = Mathf.Max(0.01f, (startTime + duration) - GameplayNow);
StartCoroutine(CompensateTransferredTimedBudeffAtExpiry(actualTarget, iconType, value, remaining));
}
return true;
@@ -2966,7 +2968,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
private IEnumerator CompensateTransferredTimedBudeffAtExpiry(AllyCombatant target, PlayerBudeffIconType iconType, float value, float waitSeconds)
{
yield return new WaitForSeconds(Mathf.Max(0.01f, waitSeconds));
yield return GameplayClock.WaitForSeconds(Mathf.Max(0.01f, waitSeconds));
if (target == null) yield break;
if (this == null) yield break;
@@ -3086,7 +3088,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
if (target.IsDead || !target.gameObject.activeInHierarchy) return;
if (!IsSelfDamageRedirectToAdjacentActive()) return;
float remain = Mathf.Max(0.01f, _selfDamageRedirectToAdjacentExpireTime - Time.time);
float remain = Mathf.Max(0.01f, _selfDamageRedirectToAdjacentExpireTime - GameplayNow);
string iconId = _selfDamageRedirectToAdjacentIconId;
// Clear source state without touching icon (icon already moved by budeff controller transfer).
@@ -3096,7 +3098,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
// Keep only one self-redirect state on target.
target.ConsumeSelfDamageRedirectToAdjacent();
target._selfDamageRedirectToAdjacentExpireTime = Time.time + remain;
target._selfDamageRedirectToAdjacentExpireTime = GameplayNow + remain;
target._selfDamageRedirectToAdjacentIconId = iconId;
}
@@ -3222,7 +3224,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
{
if (!Application.isPlaying) return;
if (duration <= 0f) duration = 0.01f;
_selfDamageRedirectToAdjacentExpireTime = Time.time + duration;
_selfDamageRedirectToAdjacentExpireTime = GameplayNow + duration;
if (!string.IsNullOrEmpty(_selfDamageRedirectToAdjacentIconId))
{
iBudeffPrefabController.Instance?.UnregisterTimedEffect(slotIndex, _selfDamageRedirectToAdjacentIconId);
@@ -3236,7 +3238,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
if (!Application.isPlaying) return;
if (duration <= 0f) duration = 0.01f;
_rewriteNonMissToPerfectExpireTime = Time.time + duration;
_rewriteNonMissToPerfectExpireTime = GameplayNow + duration;
if (!string.IsNullOrEmpty(_rewriteNonMissToPerfectIconId))
{
@@ -3252,7 +3254,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
{
if (!Application.isPlaying) return false;
if (_rewriteNonMissToPerfectExpireTime <= 0f) return false;
if (Time.time > _rewriteNonMissToPerfectExpireTime)
if (GameplayNow > _rewriteNonMissToPerfectExpireTime)
{
ClearNonMissToPerfectRewrite();
return false;
@@ -3280,7 +3282,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
{
if (!Application.isPlaying) return false;
if (IsDead) return false;
return _rewriteNonMissToPerfectExpireTime > 0f && Time.time <= _rewriteNonMissToPerfectExpireTime;
return _rewriteNonMissToPerfectExpireTime > 0f && GameplayNow <= _rewriteNonMissToPerfectExpireTime;
}
private bool IsSelfDamageRedirectToAdjacentActive()
@@ -3292,7 +3294,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
ConsumeSelfDamageRedirectToAdjacent();
return false;
}
if (Time.time > _selfDamageRedirectToAdjacentExpireTime)
if (GameplayNow > _selfDamageRedirectToAdjacentExpireTime)
{
ConsumeSelfDamageRedirectToAdjacent();
return false;
+14 -14
View File
@@ -1191,7 +1191,7 @@ public class EffectSystem : MonoBehaviour
comp.ReceiveDamage(perTick, source, false);
// No need to call TriggerQueuedPopups manually as deferPopup is false
yield return new WaitForSeconds(tickInterval);
yield return GameplayClock.WaitForSeconds(tickInterval);
elapsed += tickInterval;
}
}
@@ -1239,7 +1239,7 @@ public class EffectSystem : MonoBehaviour
comp.ReceiveHeal(perTick, source, false);
// No need to call TriggerQueuedPopups manually as deferPopup is false
yield return new WaitForSeconds(tickInterval);
yield return GameplayClock.WaitForSeconds(tickInterval);
elapsed += tickInterval;
}
}
@@ -1305,7 +1305,7 @@ public class EffectSystem : MonoBehaviour
{
ally.ModifyMana(Mathf.CeilToInt(perTick), true, true);
}
yield return new WaitForSeconds(tickInterval);
yield return GameplayClock.WaitForSeconds(tickInterval);
elapsed += tickInterval;
}
}
@@ -1321,7 +1321,7 @@ public class EffectSystem : MonoBehaviour
}
comp.ApplyBuff(buff, source);
yield return new WaitForSeconds(buff.duration);
yield return GameplayClock.WaitForSeconds(buff.duration);
comp.RemoveBuff(buff.buffId);
}
@@ -1337,7 +1337,7 @@ public class EffectSystem : MonoBehaviour
}
comp.ApplyBuff(debuff, source);
yield return new WaitForSeconds(debuff.duration);
yield return GameplayClock.WaitForSeconds(debuff.duration);
comp.RemoveBuff(debuff.buffId);
}
@@ -1348,7 +1348,7 @@ public class EffectSystem : MonoBehaviour
{
if (ally == null) yield break;
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency + delta);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (ally == null) yield break;
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - delta);
}
@@ -1369,7 +1369,7 @@ public class EffectSystem : MonoBehaviour
if (ally != null)
{
ally.damageResistance = ClampDamageResistance(ally.damageResistance + delta);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (ally == null) yield break;
ally.damageResistance = ClampDamageResistance(ally.damageResistance - delta);
yield break;
@@ -1380,7 +1380,7 @@ public class EffectSystem : MonoBehaviour
{
enemy.damageResistance = ClampDamageResistance(enemy.damageResistance + delta);
iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (enemy == null) yield break;
enemy.damageResistance = ClampDamageResistance(enemy.damageResistance - delta);
yield break;
@@ -1403,7 +1403,7 @@ public class EffectSystem : MonoBehaviour
{
if (ally == null) yield break;
ally.ModifyAttack(delta);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (ally == null) yield break;
ally.ModifyAttack(-delta);
}
@@ -1427,7 +1427,7 @@ public class EffectSystem : MonoBehaviour
if (enemy == null) yield break;
enemy.ModifyAttack(delta);
iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (enemy == null) yield break;
enemy.ModifyAttack(-delta);
}
@@ -1445,7 +1445,7 @@ public class EffectSystem : MonoBehaviour
if (ally == null) yield break;
ally.SetMaxHP(Mathf.Max(1, ally.maxHP + delta), false);
if (delta > 0) ally.SetCurrentHP(ally.currentHP + delta, false);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (ally == null) yield break;
ally.SetMaxHP(Mathf.Max(1, ally.maxHP - delta), false);
}
@@ -1464,7 +1464,7 @@ public class EffectSystem : MonoBehaviour
enemy.SetMaxHP(Mathf.Max(1, enemy.maxHP + delta), false);
if (delta > 0) enemy.ModifyHP(delta);
iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (enemy == null) yield break;
enemy.SetMaxHP(Mathf.Max(1, enemy.maxHP - delta), false);
}
@@ -1481,7 +1481,7 @@ public class EffectSystem : MonoBehaviour
{
if (ally == null) yield break;
ally.SetMaxMana(Mathf.Max(1, ally.maxMana + delta), false);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (ally == null) yield break;
ally.SetMaxMana(Mathf.Max(1, ally.maxMana - delta), false);
}
@@ -1499,7 +1499,7 @@ public class EffectSystem : MonoBehaviour
if (enemy == null) yield break;
enemy.SetMaxMana(Mathf.Max(1, enemy.maxMana + delta), false);
iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (enemy == null) yield break;
enemy.SetMaxMana(Mathf.Max(1, enemy.maxMana - delta), false);
}
+17 -17
View File
@@ -604,7 +604,7 @@ public class SkillBuilder : MonoBehaviour
else Debug.LogWarning($"ApplyBuffToTargets: target {t.name} has no ICombatant");
}
if (buff.duration > 0f)
yield return new WaitForSeconds(buff.duration);
yield return GameplayClock.WaitForSeconds(buff.duration);
else
yield break;
@@ -1010,7 +1010,7 @@ public class SkillBuilder : MonoBehaviour
private IEnumerator RemoveRefreshOnlyTimedEffectAfterDuration(string key, GameObject target, float duration)
{
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (_refreshOnlyTimedEffectStates.TryGetValue(key, out var state))
{
@@ -1212,7 +1212,7 @@ public class SkillBuilder : MonoBehaviour
if (target == null) break;
if (!ApplyRefreshOnlyOverTimeTick(def.effectType, target, perTick, source)) break;
yield return new WaitForSeconds(tickInterval);
yield return GameplayClock.WaitForSeconds(tickInterval);
elapsed += tickInterval;
}
@@ -1544,7 +1544,7 @@ ResolvedGroup:
{
string sid = !string.IsNullOrWhiteSpace(def.skillId) ? def.skillId : def.name;
string repeatKey = $"{slotIndex}:{sid}";
float now = Time.time;
float now = GameplayClock.NowSongTime;
if (_lastSkillTriggerTime.TryGetValue(repeatKey, out float last) && (now - last) <= def.repeatWindowSeconds)
{
amountTotal = def.repeatValue;
@@ -2406,7 +2406,7 @@ ResolvedGroup:
}
ally.AddScoreDirect(Mathf.Max(0, state.scorePerSecond));
yield return new WaitForSeconds(1f);
yield return GameplayClock.WaitForSeconds(1f);
}
}
@@ -2650,7 +2650,7 @@ ResolvedGroup:
{
iBudeffPrefabController.Instance?.RegisterTimedEffect(ally, PlayerBudeffIconType.ot_scoreEfficiency_down, -ally.scoreEfficiency, duration);
}
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (ally != null && !ally.IsDead && _equipTalentScoutDisagreeRestoreScoreBySlot.TryGetValue(slotIndex, out float restore))
{
ally.scoreEfficiency = Mathf.Max(0f, restore);
@@ -2760,7 +2760,7 @@ ResolvedGroup:
private IEnumerator NewIdeaExpireCoroutine(int slotIndex, float duration)
{
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (_equipNewIdeaStacksBySlot.TryGetValue(slotIndex, out int stacks))
{
_equipNewIdeaStacksBySlot[slotIndex] = Mathf.Max(0, stacks - 1);
@@ -3372,7 +3372,7 @@ ResolvedGroup:
{
LogVerbose($"[SkillBuilder] NotifyNoteHit RECEIVED: track={trackIndex} judge={judgeResult} noteType={noteType} uniqueId={uniqueNoteId}");
// cleanup old processed ids (> 10s)
float nowCleanup = Time.time;
float nowCleanup = GameplayClock.NowSongTime;
_tmpNoteIdRemoval.Clear();
var toRemove = _tmpNoteIdRemoval;
foreach (var kv in _processedNoteHitTimestamps)
@@ -3391,7 +3391,7 @@ ResolvedGroup:
}
else
{
_processedNoteHitTimestamps[uniqueNoteId] = Time.time;
_processedNoteHitTimestamps[uniqueNoteId] = nowCleanup;
}
}
@@ -3491,7 +3491,7 @@ ResolvedGroup:
// cooldown per slot+skill
string key = $"{trackIndex}:{def.skillId}";
float now = Time.time;
float now = GameplayClock.NowSongTime;
if (def.onNoteHitCooldown > 0f)
{
if (_lastOnNoteHitTriggerTime.TryGetValue(key, out float last))
@@ -3544,7 +3544,7 @@ ResolvedGroup:
while (elapsed < duration)
{
ally.ModifyMana(Mathf.CeilToInt(perTick), true, true);
yield return new WaitForSeconds(tickInterval);
yield return GameplayClock.WaitForSeconds(tickInterval);
elapsed += tickInterval;
}
}
@@ -3552,7 +3552,7 @@ ResolvedGroup:
private IEnumerator RemoveBuffAfterDuration(GameObject target, string buffId, float duration)
{
if (duration <= 0f) yield break;
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (target == null) yield break;
var ic = target.GetComponent<ICombatant>();
ic?.RemoveBuff(buffId);
@@ -3564,7 +3564,7 @@ ResolvedGroup:
{
if (ally == null) yield break;
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency + delta);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (ally == null) yield break;
ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - delta);
}
@@ -3581,7 +3581,7 @@ ResolvedGroup:
{
if (ally == null) yield break;
ally.ModifyAttack(delta);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (ally == null) yield break;
ally.ModifyAttack(-delta);
}
@@ -3599,7 +3599,7 @@ ResolvedGroup:
if (enemy == null) yield break;
enemy.ModifyAttack(delta);
iBudeffPrefabController.Instance?.RefreshEnemyNow(enemy);
yield return new WaitForSeconds(duration);
yield return GameplayClock.WaitForSeconds(duration);
if (enemy == null) yield break;
enemy.ModifyAttack(-delta);
}
@@ -3674,7 +3674,7 @@ ResolvedGroup:
if (ally != null) ally.ModifyHP(-Mathf.CeilToInt(perTick), true);
else ic?.ReceiveDamage(perTick, source);
yield return new WaitForSeconds(tickInterval);
yield return GameplayClock.WaitForSeconds(tickInterval);
elapsed += tickInterval;
}
}
@@ -3713,7 +3713,7 @@ ResolvedGroup:
{
if (ally != null) ally.ReceiveHeal(perTick, source);
else ic?.ReceiveHeal(perTick, source);
yield return new WaitForSeconds(tickInterval);
yield return GameplayClock.WaitForSeconds(tickInterval);
elapsed += tickInterval;
}
}
+6
View File
@@ -124,6 +124,12 @@ public class Player_SO : ScriptableObject
PersistEditorChanges();
}
public void SetCurrentLevel(int value)
{
player_currentLevel = Mathf.Max(0, value);
PersistEditorChanges();
}
public void SetLegacyExpBottleCount(string fieldName, int value)
{
int safeValue = Mathf.Max(0, value);
File diff suppressed because it is too large Load Diff
@@ -5,8 +5,11 @@ using UnityEngine.SceneManagement;
using UnityEngine.EventSystems;
using System.Collections.Generic;
using UnityEngine.Audio;
using DG.Tweening;
using Bansonic;
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
using GameServer.Client;
public class btmandtopController : MonoBehaviour, ICancelHandler
@@ -24,6 +27,12 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
public Text player_mmrFragment;
public Text player_rks;
public Image rksProgressImage;
[Header("currency hover")]
public Button mmrFragmentHoverButton;
public Button coinsHoverButton;
public CanvasGroup mmrFragmentHoverCanvasGroup;
public CanvasGroup coinsHoverCanvasGroup;
[Min(0.01f)] public float currencyHoverFadeDuration = 0.25f;
[Header("put prefabs here")]
public GameObject putPrefabsHere;
public GameObject putSettingsPrefabHere;
@@ -135,6 +144,8 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
private RectTransform topNavigationRoot;
private bool topNavigationGeometryDirty = true;
private int lastTopNavigationParentChildCount = -1;
private Tween mmrFragmentHoverTween;
private Tween coinsHoverTween;
private static readonly List<string> sceneHistory = new List<string>();
private static bool sceneHistoryHooked = false;
@@ -159,6 +170,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
float currentRks = PlayerRksService.GetBestOverallRks(player_SO);
UpdatePlayerRksText(currentRks);
UpdateRksProgressImage(currentRks);
SetupCurrencyHoverIndicators();
if (button_Music == null)
{
@@ -538,6 +550,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
StopCoroutine(musicPicFade);
musicPicFade = null;
}
KillCurrencyHoverTweens();
musicPicGroup = null;
if (settings_launch != null)
settings_launch.onClick.RemoveListener(instantiateSettings);
@@ -593,6 +606,107 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
}
}
private void SetupCurrencyHoverIndicators()
{
InitializeCurrencyHoverCanvasGroup(mmrFragmentHoverCanvasGroup);
InitializeCurrencyHoverCanvasGroup(coinsHoverCanvasGroup);
AttachCurrencyHoverListener(mmrFragmentHoverButton, true);
AttachCurrencyHoverListener(coinsHoverButton, false);
}
private void InitializeCurrencyHoverCanvasGroup(CanvasGroup targetGroup)
{
if (targetGroup == null)
{
return;
}
targetGroup.DOKill();
targetGroup.alpha = 0f;
targetGroup.interactable = false;
targetGroup.blocksRaycasts = false;
}
private void AttachCurrencyHoverListener(Button targetButton, bool isFragmentButton)
{
if (targetButton == null)
{
return;
}
btmandtopCurrencyHoverRelay relay = targetButton.GetComponent<btmandtopCurrencyHoverRelay>();
if (relay == null)
{
relay = targetButton.gameObject.AddComponent<btmandtopCurrencyHoverRelay>();
}
relay.Initialize(this, isFragmentButton);
}
internal void SetCurrencyHoverVisible(bool isFragmentGroup, bool visible)
{
CanvasGroup targetGroup = isFragmentGroup ? mmrFragmentHoverCanvasGroup : coinsHoverCanvasGroup;
if (targetGroup == null)
{
return;
}
Tween currentTween = isFragmentGroup ? mmrFragmentHoverTween : coinsHoverTween;
if (currentTween != null && currentTween.IsActive())
{
currentTween.Kill();
}
float duration = Mathf.Max(0.01f, currencyHoverFadeDuration);
targetGroup.interactable = false;
targetGroup.blocksRaycasts = false;
Tween nextTween = targetGroup
.DOFade(visible ? 1f : 0f, duration)
.SetEase(Ease.Linear)
.SetUpdate(true)
.SetLink(targetGroup.gameObject)
.OnComplete(() =>
{
targetGroup.alpha = visible ? 1f : 0f;
targetGroup.interactable = false;
targetGroup.blocksRaycasts = false;
});
if (isFragmentGroup)
{
mmrFragmentHoverTween = nextTween;
}
else
{
coinsHoverTween = nextTween;
}
}
private void KillCurrencyHoverTweens()
{
if (mmrFragmentHoverTween != null && mmrFragmentHoverTween.IsActive())
{
mmrFragmentHoverTween.Kill();
}
if (coinsHoverTween != null && coinsHoverTween.IsActive())
{
coinsHoverTween.Kill();
}
if (mmrFragmentHoverCanvasGroup != null)
{
mmrFragmentHoverCanvasGroup.DOKill();
}
if (coinsHoverCanvasGroup != null)
{
coinsHoverCanvasGroup.DOKill();
}
}
private void HandleCoinsChanged(int coinAmount)
{
UpdatePlayerCoinsLegacyText(coinAmount);
@@ -1580,6 +1694,12 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
public void UpdateSteamUserInfo()
{
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
// 非 Steam 平台(如 Android):无 Steamworks,显示未知来源即可。
if (steamStatusText != null)
steamStatusText.text = LocalizationService.Get("steam.status.unknown", "Unknown Server");
return;
#else
if (!SteamManager.Initialized)
{
Debug.LogWarning("[Steam] SteamManager not initialized.");
@@ -1648,6 +1768,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
if (steamStatusText != null)
steamStatusText.text = LocalizationService.Get("steam.status.unknown", "Unknown Server");
}
#endif
}
}
@@ -1678,3 +1799,39 @@ internal sealed class btmandtopPanelVisibilityRelay : MonoBehaviour
}
}
}
internal sealed class btmandtopCurrencyHoverRelay : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{
private btmandtopController owner;
private bool isFragmentGroup;
public void Initialize(btmandtopController controller, bool fragmentGroup)
{
owner = controller;
isFragmentGroup = fragmentGroup;
}
public void OnPointerEnter(PointerEventData eventData)
{
if (owner != null)
{
owner.SetCurrencyHoverVisible(isFragmentGroup, true);
}
}
public void OnPointerExit(PointerEventData eventData)
{
if (owner != null)
{
owner.SetCurrencyHoverVisible(isFragmentGroup, false);
}
}
public void OnPointerClick(PointerEventData eventData)
{
if (owner != null)
{
owner.SetCurrencyHoverVisible(isFragmentGroup, true);
}
}
}
+39 -17
View File
@@ -122,17 +122,13 @@ public class PauseManager : MonoBehaviour
{
Pause(true);
}
else
{
Pause(false);
}
}
}
// Keep existing Backspace quick-exit while paused.
if (IsPaused && Input.GetKeyDown(KeyCode.Backspace))
{
Pause(false);
ResumeImmediately(true);
StartCoroutine(LoadSceneAsync("Main_main"));
}
}
@@ -144,6 +140,7 @@ public class PauseManager : MonoBehaviour
if (IsPaused) return;
IsPaused = true;
Time.timeScale = 0f;
GameplayClock.Pause();
OnPauseStateChanged?.Invoke(true);
// Startup flows call Pause(true) before real gameplay starts.
@@ -192,11 +189,38 @@ public class PauseManager : MonoBehaviour
if (!IsPaused) return;
IsPaused = false;
Time.timeScale = 1f;
GameplayClock.Resume();
OnPauseStateChanged?.Invoke(false);
ShowOverlayAnimated(false);
}
}
private void ResumeImmediately(bool hideOverlay)
{
if (!IsPaused)
{
if (hideOverlay)
{
SetOverlayVisibleImmediate(false);
}
return;
}
IsPaused = false;
Time.timeScale = 1f;
GameplayClock.Resume();
OnPauseStateChanged?.Invoke(false);
if (hideOverlay)
{
SetOverlayVisibleImmediate(false);
}
else
{
ShowOverlayAnimated(false);
}
}
public void TogglePause()
{
if (!CanPauseFromInput()) return;
@@ -424,7 +448,11 @@ public class PauseManager : MonoBehaviour
EnsureOverlayCanvasGroup();
EnsureOverlayPanelRoot();
overlayRoot.SetActive(visible);
// Keep the overlay hierarchy active even when hidden.
// If PauseManager lives on overlayRoot or any of its children, deactivating the root
// disables this component and all future ESC input stops working.
if (!overlayRoot.activeSelf)
overlayRoot.SetActive(true);
if (overlayCanvasGroup != null)
{
overlayCanvasGroup.alpha = visible ? 1f : 0f;
@@ -530,8 +558,8 @@ public class PauseManager : MonoBehaviour
if (overlayPanelRoot != null)
overlayPanelRoot.localScale = targetScale;
if (!show)
overlayRoot.SetActive(false);
// Do not deactivate overlayRoot here; hiding via CanvasGroup is enough and keeps
// PauseManager alive for subsequent ESC presses.
overlayFadeCoroutine = null;
}
@@ -571,16 +599,13 @@ public class PauseManager : MonoBehaviour
}
// Resume chart + music.
Pause(false);
ResumeImmediately(false);
continueCoroutine = null;
}
private IEnumerator ReplayFromPauseCoroutine()
{
SetOverlayVisibleImmediate(false);
IsPaused = false;
Time.timeScale = 1f;
try { OnPauseStateChanged?.Invoke(false); } catch { }
ResumeImmediately(true);
var bmm = SceneObjectLookupCache.FindAny<BeatmapManager>();
if (bmm != null && bmm.assignedSongData != null)
@@ -596,10 +621,7 @@ public class PauseManager : MonoBehaviour
private IEnumerator ExitFromPauseCoroutine()
{
SetOverlayVisibleImmediate(false);
IsPaused = false;
Time.timeScale = 1f;
try { OnPauseStateChanged?.Invoke(false); } catch { }
ResumeImmediately(true);
string targetScene = ExitSceneName;
ArenaRoomService arenaRoomService = ArenaRoomService.Instance;
+2 -8
View File
@@ -211,7 +211,7 @@ public class WebViewLauncher : MonoBehaviour
}
return raw.TrimEnd('/');
}
return "http://47.112.187.172:8080";
return "https://game.bansonic.top";
}
string BuildFlutterLaunchArguments(string openUrl)
@@ -534,13 +534,7 @@ public class WebViewLauncher : MonoBehaviour
public void StartWebView()
{
if (webProcess == null || webProcess.HasExited)
{
LaunchWebView();
return;
}
SendLocalControlCommand("/home");
WebViewReady?.Invoke();
Bansonic.gNotice.error.display("“浮动小游戏”暂未开放,敬请期待!");
}
public void StopWebView()
@@ -20,6 +20,7 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
private readonly Dictionary<int, long> joinDateUtcTicksByHeroId = new Dictionary<int, long>();
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
@@ -78,14 +79,18 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
AllyHeroDeployLedgerPayload payload;
loadedFromSave = AllyHeroDeployLedgerStorage.TryLoad(out payload);
hasAnyRecoverableLocalState = loadedFromSave || AllyHeroDeployLedgerStorage.HasAnyRecoverableState();
RebuildFromPayload(payload);
initialized = true;
if (!loadedFromSave)
if (!loadedFromSave && !hasAnyRecoverableLocalState)
{
ResetGrowthStateToDefaults();
}
SyncAllMirrorFlags();
SaveNow();
if (loadedFromSave || !hasAnyRecoverableLocalState)
{
SaveNow();
}
}
public void ResetToDefaultsIfSaveMissing()
@@ -95,7 +100,7 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
return;
}
if (AllyHeroDeployLedgerStorage.HasExistingSaveFile())
if (hasAnyRecoverableLocalState || AllyHeroDeployLedgerStorage.HasExistingSaveFile())
{
return;
}
@@ -11,6 +11,7 @@ public static class AllyHeroDeployLedgerStorage
private const string MainFileName = ".ahd.dat";
private const string BackupFileName = ".ahd.bak";
private const string TempFileName = ".ahd.tmp";
private const string RecoverySlotKey = "ally_hero_deploy_storage";
private static string VaultDirectoryPath => Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName);
private static string MainFilePath => Path.Combine(VaultDirectoryPath, MainFileName);
@@ -19,25 +20,7 @@ public static class AllyHeroDeployLedgerStorage
public static bool HasExistingSaveFile()
{
var mainCandidates = SaveIdentityUtility.GetVaultFilePathVariants(MainFileName);
for (int i = 0; i < mainCandidates.Count; i++)
{
if (File.Exists(mainCandidates[i]))
{
return true;
}
}
var backupCandidates = SaveIdentityUtility.GetVaultFilePathVariants(BackupFileName);
for (int i = 0; i < backupCandidates.Count; i++)
{
if (File.Exists(backupCandidates[i]))
{
return true;
}
}
return false;
return HasAnyRecoverableState();
}
public static bool TryLoad(out AllyHeroDeployLedgerPayload payload)
@@ -61,9 +44,23 @@ public static class AllyHeroDeployLedgerStorage
return true;
}
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
{
TrySave(payload);
return true;
}
return false;
}
public static bool HasAnyRecoverableState()
{
return HasAnyVaultFile(MainFileName)
|| HasAnyVaultFile(BackupFileName)
|| PlayerProgressBackupService.HasAllyHeroDeployBackup()
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
public static bool TrySave(AllyHeroDeployLedgerPayload payload)
{
try
@@ -85,6 +82,7 @@ public static class AllyHeroDeployLedgerStorage
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveAllyHeroDeploy(payload);
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
return true;
}
catch (Exception ex)
@@ -177,6 +175,20 @@ public static class AllyHeroDeployLedgerStorage
return false;
}
private static bool HasAnyVaultFile(string fileName)
{
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
for (int i = 0; i < candidates.Count; i++)
{
if (File.Exists(candidates[i]))
{
return true;
}
}
return false;
}
private static string BuildEnvelopeJson(AllyHeroDeployLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using UnityEngine;
#if !UNITY_WEBGL
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
@@ -118,6 +118,34 @@ public static class DlcOwnershipService
Cache.Clear();
}
public static void ClearLocalPersistence()
{
dlcData[] allDlcs = RuntimeResourcesCache.LoadAllDlcs();
if (allDlcs != null)
{
for (int i = 0; i < allDlcs.Length; i++)
{
dlcData dlc = allDlcs[i];
if (dlc == null)
{
continue;
}
string key = dlc.GetResolvedDlcKey();
if (string.IsNullOrWhiteSpace(key))
{
continue;
}
PlayerPrefs.DeleteKey(RemoteOwnershipPrefsPrefix + key);
PlayerPrefs.DeleteKey(LocalOverridePrefsPrefix + key);
}
}
PlayerPrefs.Save();
Cache.Clear();
}
private static DlcEntitlementState ResolveEntitlement(dlcData dlc, string key)
{
if (!dlc.ShouldEnforceEntitlement())
@@ -137,7 +165,7 @@ public static class DlcOwnershipService
return new DlcEntitlementState(key, true, true, DlcEntitlementSource.LocalFlag);
}
#if !UNITY_WEBGL
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
if (dlc.steamAppId > 0 && SteamManager.Initialized)
{
try
@@ -39,7 +39,7 @@ public static class DlcRemoteManifestSyncService
}
}
private const string DefaultServerUrl = "http://47.112.187.172:8080";
private const string DefaultServerUrl = "https://game.bansonic.top";
private const string RemoteManifestFilePrefix = "remote_";
private const string ManifestApiPath = "/api/dlcs/manifests";
@@ -11,6 +11,7 @@ public sealed class DushMaterialLedger : MonoBehaviour
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
private Player_SO boundPlayerData;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
@@ -68,11 +69,23 @@ public sealed class DushMaterialLedger : MonoBehaviour
DushMaterialLedgerPayload payload;
loadedFromSave = DushMaterialLedgerStorage.TryLoad(out payload);
hasAnyRecoverableLocalState = loadedFromSave || DushMaterialLedgerStorage.HasAnyRecoverableState();
RebuildFromPayload(payload);
initialized = true;
TryRecoverFromDefaultPlayerData();
SyncMirrorCounts();
SaveNow();
if (!loadedFromSave && !hasAnyRecoverableLocalState)
{
bool recoveredFromMirror = TryRecoverFromDefaultPlayerData();
if (recoveredFromMirror)
{
hasAnyRecoverableLocalState = true;
}
}
if (loadedFromSave || !hasAnyRecoverableLocalState)
{
SaveNow();
}
}
public void AttachPlayerData(Player_SO playerData)
@@ -11,6 +11,7 @@ public static class DushMaterialLedgerStorage
private const string MainFileName = ".dsm.dat";
private const string BackupFileName = ".dsm.bak";
private const string TempFileName = ".dsm.tmp";
private const string RecoverySlotKey = "dush_material_ledger_storage";
private static string VaultDirectoryPath
{
@@ -53,9 +54,23 @@ public static class DushMaterialLedgerStorage
return true;
}
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
{
TrySave(payload);
return true;
}
return false;
}
public static bool HasAnyRecoverableState()
{
return HasAnyVaultFile(MainFileName)
|| HasAnyVaultFile(BackupFileName)
|| PlayerProgressBackupService.HasDushMaterialBackup()
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
public static bool TrySave(DushMaterialLedgerPayload payload)
{
try
@@ -77,6 +92,7 @@ public static class DushMaterialLedgerStorage
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveDushMaterial(payload);
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
return true;
}
catch (Exception ex)
@@ -169,6 +185,20 @@ public static class DushMaterialLedgerStorage
return false;
}
private static bool HasAnyVaultFile(string fileName)
{
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
for (int i = 0; i < candidates.Count; i++)
{
if (File.Exists(candidates[i]))
{
return true;
}
}
return false;
}
private static string BuildEnvelopeJson(DushMaterialLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -11,6 +11,7 @@ public static class EquipmentConsumableLedgerStorage
private const string MainFileName = ".eqc.dat";
private const string BackupFileName = ".eqc.bak";
private const string TempFileName = ".eqc.tmp";
private const string RecoverySlotKey = "equipment_consumable_ledger_storage";
private static string VaultDirectoryPath => Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName);
private static string MainFilePath => Path.Combine(VaultDirectoryPath, MainFileName);
@@ -38,9 +39,22 @@ public static class EquipmentConsumableLedgerStorage
return true;
}
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
{
TrySave(payload);
return true;
}
return false;
}
public static bool HasAnyRecoverableState()
{
return HasAnyVaultFile(MainFileName)
|| HasAnyVaultFile(BackupFileName)
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
public static bool TrySave(EquipmentConsumableLedgerPayload payload)
{
try
@@ -62,6 +76,7 @@ public static class EquipmentConsumableLedgerStorage
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveEquipmentConsumable(payload);
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
return true;
}
catch (Exception ex)
@@ -81,6 +96,20 @@ public static class EquipmentConsumableLedgerStorage
};
}
private static bool HasAnyVaultFile(string fileName)
{
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
for (int i = 0; i < candidates.Count; i++)
{
if (File.Exists(candidates[i]))
{
return true;
}
}
return false;
}
private static bool TryReadPayload(string path, out EquipmentConsumableLedgerPayload payload)
{
payload = CreateDefaultPayload();
@@ -12,6 +12,7 @@ public sealed class ExpBottleLedger : MonoBehaviour
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
private Player_SO boundPlayerData;
public bool IsReady
@@ -74,11 +75,24 @@ public sealed class ExpBottleLedger : MonoBehaviour
ExpBottleLedgerPayload payload;
loadedFromSave = ExpBottleLedgerStorage.TryLoad(out payload);
hasAnyRecoverableLocalState = loadedFromSave || ExpBottleLedgerStorage.HasAnyRecoverableState();
RebuildFromPayload(payload);
initialized = true;
TryRecoverFromDefaultPlayerData();
SyncMirrorCounts();
SaveNow();
if (!loadedFromSave && !hasAnyRecoverableLocalState)
{
bool recoveredFromMirror = TryRecoverFromDefaultPlayerData();
if (recoveredFromMirror)
{
hasAnyRecoverableLocalState = true;
}
}
if (loadedFromSave || !hasAnyRecoverableLocalState)
{
SaveNow();
}
if (OnLedgerReloaded != null)
{
OnLedgerReloaded();
@@ -11,6 +11,7 @@ public static class ExpBottleLedgerStorage
private const string MainFileName = ".xpv.dat";
private const string BackupFileName = ".xpv.bak";
private const string TempFileName = ".xpv.tmp";
private const string RecoverySlotKey = "exp_bottle_ledger_storage";
private static string VaultDirectoryPath
{
@@ -53,9 +54,23 @@ public static class ExpBottleLedgerStorage
return true;
}
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
{
TrySave(payload);
return true;
}
return false;
}
public static bool HasAnyRecoverableState()
{
return HasAnyVaultFile(MainFileName)
|| HasAnyVaultFile(BackupFileName)
|| PlayerProgressBackupService.HasExpBottleBackup()
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
public static bool TrySave(ExpBottleLedgerPayload payload)
{
try
@@ -77,6 +92,7 @@ public static class ExpBottleLedgerStorage
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveExpBottle(payload);
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
return true;
}
catch (Exception ex)
@@ -169,6 +185,20 @@ public static class ExpBottleLedgerStorage
return false;
}
private static bool HasAnyVaultFile(string fileName)
{
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
for (int i = 0; i < candidates.Count; i++)
{
if (File.Exists(candidates[i]))
{
return true;
}
}
return false;
}
private static string BuildEnvelopeJson(ExpBottleLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -0,0 +1,396 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public static class FirstRunFactoryResetService
{
private const string StoreStateLegacyPath = "storeSystem_state.json";
// 明文"已初始化"标记:不依赖 deviceUniqueIdentifier / 加密,仅表示本机曾成功初始化过。
// 用途见 TryPerformFactoryResetFallback:区分"真首启"与"存档存在但暂时读不出"。
private const string InitializedMarkerFileName = ".bansonic_initialized";
private static readonly int[] DefaultTeamHeroIds = { 30201, 30202, 30203, 30204, 30205 };
private static bool executed;
private static string InitializedMarkerPath =>
Path.Combine(Application.persistentDataPath, InitializedMarkerFileName);
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
TryPerformFactoryResetFallback();
}
private static void TryPerformFactoryResetFallback()
{
if (executed)
{
return;
}
executed = true;
if (HasAnyRecoverableLocalState())
{
// 有可恢复存档:补写标记(覆盖历史版本升级上来、尚无标记的老用户),不重置。
EnsureInitializedMarker();
return;
}
// 走到这里说明"当前读不到任何可恢复存档"。必须区分两种情况:
// A) 真正首次运行:应用出厂默认(全 0)并写标记 —— 安全。
// B) 曾经玩过、但存档暂时读不出(典型:deviceUniqueIdentifier 变化致解密/验签失败):
// 此时清零 = 数据丢失灾难。宁可保留现状等待其它恢复路径,也绝不主动抹除。
if (HasInitializedMarker())
{
Debug.LogWarning("[FactoryReset] 检测到已初始化标记,但当前读不到任何存档。" +
"为避免误删既有(暂不可读)玩家数据,跳过出厂重置。");
return;
}
Debug.LogWarning("[FactoryReset] 首次运行且无可恢复存档,应用出厂默认设置。");
ApplyFactoryReset();
EnsureInitializedMarker();
}
private static bool HasInitializedMarker()
{
try
{
return File.Exists(InitializedMarkerPath);
}
catch (Exception ex)
{
Debug.LogWarning("[FactoryReset] 读取初始化标记失败: " + ex.Message);
return false;
}
}
private static void EnsureInitializedMarker()
{
try
{
if (File.Exists(InitializedMarkerPath))
{
return;
}
File.WriteAllText(InitializedMarkerPath, DateTime.UtcNow.ToString("o"));
}
catch (Exception ex)
{
Debug.LogWarning("[FactoryReset] 写入初始化标记失败: " + ex.Message);
}
}
private static bool HasAnyRecoverableLocalState()
{
if (PlayerEconomyStorage.HasAnyRecoverableState())
{
return true;
}
if (SecureSaveVault.HasAnyRecoverableState("player_experience", "runtime", Path.Combine(Application.persistentDataPath, "player_experience.json")))
{
return true;
}
if (AllyHeroDeployLedgerStorage.HasAnyRecoverableState())
{
return true;
}
if (ExpBottleLedgerStorage.HasAnyRecoverableState())
{
return true;
}
if (DushMaterialLedgerStorage.HasAnyRecoverableState())
{
return true;
}
if (EquipmentConsumableLedgerStorage.HasAnyRecoverableState())
{
return true;
}
if (StoreOwnershipStorage.HasAnyRecoverableState())
{
return true;
}
if (SecureSaveVault.HasAnyRecoverableState("player_rks", "overall"))
{
return true;
}
if (SecureSaveVault.HasAnyRecoverableState("player_skills", "runtime"))
{
return true;
}
if (SecureSaveVault.HasAnyRecoverableState("recent_play_history", "runs_v1"))
{
return true;
}
if (SecureSaveVault.HasAnyRecoverableState("runtime_equipment", "generated_list"))
{
return true;
}
if (DailyTaskSaveService.HasAnyRecoverableState())
{
return true;
}
if (SecureSaveVault.HasAnyRecoverableState("store_state", "runtime", Path.Combine(Application.persistentDataPath, StoreStateLegacyPath)))
{
return true;
}
if (PlayerProgressBackupService.HasExistingBackupFile())
{
return true;
}
SongData[] songs = RuntimeResourcesCache.LoadAllSongs();
if (songs != null)
{
for (int i = 0; i < songs.Length; i++)
{
SongData song = songs[i];
if (song == null || song.songID <= 0)
{
continue;
}
string legacySongPath = Path.Combine(Application.persistentDataPath, $"SongData_{song.songID}.json");
if (SecureSaveVault.HasAnyRecoverableState("song_runtime", song.songID.ToString(), legacySongPath))
{
return true;
}
}
}
return false;
}
private static void ApplyFactoryReset()
{
ResetPlayerEconomyAndExperience();
ResetHeroGrowthAndLoadout();
ResetPlayerSkillAndRks();
ResetSongProgressAndRecentHistory();
ResetDailyTasks();
ResetStoreState();
ResetGeneratedEquipment();
ResetTeamSelection();
ResetSettingsAndUserPrefs();
ResetPlayerMirrorSo();
ResetDlcOwnershipFlags();
RuntimeResourcesCache.InvalidateAll();
PlayerPrefs.Save();
}
private static void ResetPlayerEconomyAndExperience()
{
Player_SO player = RuntimeResourcesCache.LoadDefaultPlayerSo();
PlayerEconomyLedger economyLedger = PlayerEconomyLedger.EnsureInstance();
economyLedger.SetCoins(0);
economyLedger.SetMaterial(0);
PlayerExperienceLedger experienceLedger = PlayerExperienceLedger.EnsureInstance();
experienceLedger.SetExperience(0);
if (player != null)
{
player.player_currentEXP = 0;
player.SetCoins(0);
player.SetMaterial(0);
player.SetURankingScore(0f);
player.SetLegacyExpBottleCount("commonExpBottle78001", 0);
player.SetLegacyExpBottleCount("mediumExpBottle78002", 0);
player.SetLegacyExpBottleCount("superiorExpBottle78003", 0);
player.SetLegacyExpBottleCount("supremeExpBottle78004", 0);
player.SetLegacyExpBottleCount("extraordinaryExpBottle78005", 0);
player.SetLegacyExpBottleCount("celestialExpBottle78006", 0);
player.SetLegacyDushMaterialCount("dushMaterial78021", 0);
player.SetLegacyDushMaterialCount("dushMaterial78022", 0);
player.SetLegacyDushMaterialCount("dushMaterial78023", 0);
player.SetLegacyDushMaterialCount("dushMaterial78024", 0);
}
}
private static void ResetHeroGrowthAndLoadout()
{
AllyHeroDeployLedger.EnsureInstance().ResetGrowthProgressOnly();
ExpBottleLedger.EnsureInstance().ResetAllToZero();
DushMaterialLedger.EnsureInstance().ResetAllToZero();
EquipmentConsumableLedger.EnsureInstance().ResetAllToZero();
AllyHero_SO.ClearAllEquippedSkills();
AllyHero_SO[] heroes = RuntimeResourcesCache.LoadAllAllyHeroes();
if (heroes == null)
{
return;
}
for (int i = 0; i < heroes.Length; i++)
{
AllyHero_SO hero = heroes[i];
if (hero == null)
{
continue;
}
hero.ClearEquippedEquipment();
hero.ClearSelectedSkin();
hero.ally_currentEXP = 0;
hero.ally_growthUnlockedTierIndex = 0;
hero.level_lock = false;
hero.ally_autoBreakthroughEnabled = false;
hero.ally_battleDeployCount = 0;
hero.ally_finishCount = 0;
hero.ally_mvpCount = 0;
hero.ally_joinDateUtcTicks = 0L;
}
}
private static void ResetPlayerSkillAndRks()
{
PlayerRksService.ClearPersistentState();
PlayerSkillService.ResetToDefaultLevelOneSkill();
}
private static void ResetSongProgressAndRecentHistory()
{
SongData[] songs = RuntimeResourcesCache.LoadAllSongs();
if (songs != null)
{
for (int i = 0; i < songs.Length; i++)
{
if (songs[i] != null)
{
songs[i].ClearSaveDataAndReload();
}
}
}
RecentPlayHistoryStore.Clear();
}
private static void ResetDailyTasks()
{
DailyTaskService.EnsureInstance().ClearPersistentState();
}
private static void ResetStoreState()
{
SecureSaveVault.Delete("store_state", "runtime", Path.Combine(Application.persistentDataPath, StoreStateLegacyPath));
StoreOwnershipLedger.EnsureInstance().ClearPersistentState();
PlayerProgressBackupService.ClearStoreOwnershipBackup();
storeItemSO[] storeItems = RuntimeResourcesCache.LoadAllStoreItems();
if (storeItems == null)
{
return;
}
for (int i = 0; i < storeItems.Length; i++)
{
storeItemSO item = storeItems[i];
if (item == null)
{
continue;
}
item.purchasedCount = 0;
item.user_has_read = false;
}
}
private static void ResetGeneratedEquipment()
{
equipSmelt.ClearPersistentState();
Bansonic.equipmentGenerator.ClearRuntimeGeneratedPersistence();
PlayerPrefs.DeleteKey("bansonic_equipment_next_id_v1");
}
private static void ResetTeamSelection()
{
for (int i = 0; i < DefaultTeamHeroIds.Length; i++)
{
int slot = i + 1;
PlayerPrefs.SetInt($"selected_heroSlot0{slot}_heroID", DefaultTeamHeroIds[i]);
PlayerPrefs.DeleteKey($"selected_heroSlot0{slot}_exp");
}
PlayerPrefs.SetInt("SelectedMainHeroID", DefaultTeamHeroIds[0]);
}
private static void ResetSettingsAndUserPrefs()
{
string[] keysToDelete =
{
"noteSpeedMultiplier",
"UserGlobalDelaySeconds",
"screenMode",
"resolutionIndex",
"customResW",
"customResH",
"frameRateIndex",
"SuperResolution",
"EnableGlobalMute",
"Volume_Main",
"Volume_NoteHit",
"Volume_MusicInGame",
"Volume_MusicOutGame",
"Volume_UI",
"EnableSyncNotePrefab",
"bansonic_language_code",
"global_chat_last_private_partner_id",
"notebook.selected_category",
"notebook.selected_father_id",
"notebook.selected_son_id",
"bansonic_online_enabled",
"before_everything_agree_ranking",
"before_everything_first_launch_completed",
"before_everything_light_warning_accepted",
"before_everything_user_info_accepted",
"selected_song_last_entered_id",
"song_select_skip_quick_enter_prompt"
};
for (int i = 0; i < keysToDelete.Length; i++)
{
PlayerPrefs.DeleteKey(keysToDelete[i]);
}
string[] keyBindingPrefixes = { "red", "green", "yellow", "purple", "blue" };
for (int i = 0; i < keyBindingPrefixes.Length; i++)
{
PlayerPrefs.DeleteKey($"KeyBinding_{keyBindingPrefixes[i]}");
}
}
private static void ResetPlayerMirrorSo()
{
Player_SO player = RuntimeResourcesCache.LoadDefaultPlayerSo();
if (player == null)
{
return;
}
player.SetCurrentLevel(0);
player.player_currentEXP = 0;
}
private static void ResetDlcOwnershipFlags()
{
DlcOwnershipService.ClearLocalPersistence();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0eee97e142264d24a89205928ea673a8
@@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
public static class LocalRecoveryMirror
{
private const string RecoveryDirectoryName = ".save_recovery";
public static void SaveJson<T>(string slotKey, T data)
{
if (string.IsNullOrWhiteSpace(slotKey) || (!typeof(T).IsValueType && (object)data == null))
{
return;
}
try
{
string json = JsonUtility.ToJson(data, false);
SaveRaw(slotKey, json);
}
catch (Exception ex)
{
Debug.LogWarning($"[LocalRecoveryMirror] SaveJson failed ({slotKey}): {ex.Message}");
}
}
public static bool TryLoadJson<T>(string slotKey, out T data)
{
data = default(T);
if (string.IsNullOrWhiteSpace(slotKey))
{
return false;
}
string json;
if (!TryLoadRaw(slotKey, out json) || string.IsNullOrWhiteSpace(json))
{
return false;
}
try
{
data = JsonUtility.FromJson<T>(json);
if (typeof(T).IsValueType)
{
return true;
}
return (object)data != null;
}
catch (Exception ex)
{
Debug.LogWarning($"[LocalRecoveryMirror] TryLoadJson failed ({slotKey}): {ex.Message}");
data = default(T);
return false;
}
}
public static void SaveRaw(string slotKey, string json)
{
if (string.IsNullOrWhiteSpace(slotKey) || json == null)
{
return;
}
try
{
string path = GetCanonicalPath(slotKey);
string directory = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
string backupPath = path + ".bak";
string tempPath = path + ".tmp";
File.WriteAllText(tempPath, json, Encoding.UTF8);
if (File.Exists(path))
{
File.Copy(path, backupPath, true);
}
File.Copy(tempPath, path, true);
File.Delete(tempPath);
}
catch (Exception ex)
{
Debug.LogWarning($"[LocalRecoveryMirror] SaveRaw failed ({slotKey}): {ex.Message}");
}
}
public static bool TryLoadRaw(string slotKey, out string json)
{
json = null;
if (string.IsNullOrWhiteSpace(slotKey))
{
return false;
}
IReadOnlyList<string> candidates = GetVariantPaths(slotKey);
for (int i = 0; i < candidates.Count; i++)
{
string path = candidates[i];
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
continue;
}
try
{
string loaded = File.ReadAllText(path, Encoding.UTF8);
if (string.IsNullOrWhiteSpace(loaded))
{
continue;
}
json = loaded;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[LocalRecoveryMirror] TryLoadRaw failed ({path}): {ex.Message}");
}
}
return false;
}
public static bool HasSlotData(string slotKey)
{
if (string.IsNullOrWhiteSpace(slotKey))
{
return false;
}
IReadOnlyList<string> candidates = GetVariantPaths(slotKey);
for (int i = 0; i < candidates.Count; i++)
{
string path = candidates[i];
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
{
return true;
}
}
return false;
}
public static void DeleteSlot(string slotKey)
{
if (string.IsNullOrWhiteSpace(slotKey))
{
return;
}
IReadOnlyList<string> candidates = GetVariantPaths(slotKey);
for (int i = 0; i < candidates.Count; i++)
{
string path = candidates[i];
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
continue;
}
try
{
File.Delete(path);
}
catch (Exception ex)
{
Debug.LogWarning($"[LocalRecoveryMirror] DeleteSlot failed ({path}): {ex.Message}");
}
}
}
private static string GetCanonicalPath(string slotKey)
{
string fileName = BuildFileName(slotKey);
return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), RecoveryDirectoryName, fileName);
}
private static IReadOnlyList<string> GetVariantPaths(string slotKey)
{
var result = new List<string>();
string fileName = BuildFileName(slotKey);
string backupFileName = fileName + ".bak";
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants();
for (int i = 0; i < roots.Count; i++)
{
string root = roots[i];
if (string.IsNullOrWhiteSpace(root))
{
continue;
}
AddDistinct(result, Path.Combine(root, RecoveryDirectoryName, fileName));
AddDistinct(result, Path.Combine(root, RecoveryDirectoryName, backupFileName));
}
return result;
}
private static string BuildFileName(string slotKey)
{
return slotKey.Trim().Replace('/', '_').Replace('\\', '_').Replace(':', '_') + ".json";
}
private static void AddDistinct(List<string> target, string value)
{
if (target == null || string.IsNullOrWhiteSpace(value))
{
return;
}
for (int i = 0; i < target.Count; i++)
{
if (string.Equals(target[i], value, StringComparison.OrdinalIgnoreCase))
{
return;
}
}
target.Add(value);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d25f7b0a830e50741b49abd6263782bc
@@ -11,6 +11,7 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
private PlayerEconomyPayload payload;
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
private Player_SO boundPlayerData;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
@@ -69,13 +70,18 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
PlayerEconomyPayload loadedPayload;
loadedFromSave = PlayerEconomyStorage.TryLoad(out loadedPayload);
hasAnyRecoverableLocalState = loadedFromSave || PlayerEconomyStorage.HasAnyRecoverableState();
payload = loadedPayload;
if (payload == null)
{
payload = PlayerEconomyStorage.CreateDefaultPayload();
}
initialized = true;
SaveNow();
if (loadedFromSave)
{
SaveNow();
}
}
public void AttachPlayerData(Player_SO playerData)
@@ -88,12 +94,17 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
InitializeIfNeeded();
boundPlayerData = playerData;
if (!loadedFromSave)
if (!loadedFromSave && !hasAnyRecoverableLocalState)
{
payload.coins = Mathf.Max(0, playerData.Coins);
payload.material = Mathf.Max(0, playerData.Material);
// 首次启动且无任何可恢复存档:用硬编码默认值(0)建立初始存档,
// 绝不读取 Player_SO 的烘焙值——该资产可能被打包时的测试数据污染,
// 直接采用会把玩家进度“种”成 999988 之类的测试数值。
payload = PlayerEconomyStorage.CreateDefaultPayload();
SyncToPlayerData();
SaveNow();
loadedFromSave = true;
hasAnyRecoverableLocalState = true;
NotifyEconomyChanged();
return;
}
@@ -217,6 +228,13 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
SaveNow();
}
public void SetMaterial(int value)
{
InitializeIfNeeded();
payload.material = Mathf.Max(0, value);
SaveNow();
}
public void SaveNow()
{
InitializeIfNeededForSave();
@@ -11,6 +11,7 @@ public static class PlayerEconomyStorage
private const string MainFileName = ".eco.dat";
private const string BackupFileName = ".eco.bak";
private const string TempFileName = ".eco.tmp";
private const string RecoverySlotKey = "player_economy_storage";
private static string VaultDirectoryPath
{
@@ -53,9 +54,23 @@ public static class PlayerEconomyStorage
return true;
}
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
{
TrySave(payload);
return true;
}
return false;
}
public static bool HasAnyRecoverableState()
{
return HasAnyVaultFile(MainFileName)
|| HasAnyVaultFile(BackupFileName)
|| PlayerProgressBackupService.HasEconomyBackup()
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
public static bool TrySave(PlayerEconomyPayload payload)
{
try
@@ -77,6 +92,7 @@ public static class PlayerEconomyStorage
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveEconomy(payload);
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
return true;
}
catch (Exception ex)
@@ -170,6 +186,20 @@ public static class PlayerEconomyStorage
return false;
}
private static bool HasAnyVaultFile(string fileName)
{
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
for (int i = 0; i < candidates.Count; i++)
{
if (File.Exists(candidates[i]))
{
return true;
}
}
return false;
}
private static string BuildEnvelopeJson(PlayerEconomyPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -19,6 +19,7 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
private PlayerExperiencePayload payload;
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
private Player_SO boundPlayerData;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
@@ -75,6 +76,8 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
return;
}
hasAnyRecoverableLocalState = SecureSaveVault.HasAnyRecoverableState("player_experience", "runtime", GetLegacySavePath())
|| PlayerProgressBackupService.HasPlayerExperienceBackup();
loadedFromSave = SecureSaveVault.TryLoadJson("player_experience", "runtime", out payload, GetLegacySavePath());
if (payload == null)
{
@@ -91,6 +94,8 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
}
}
hasAnyRecoverableLocalState = hasAnyRecoverableLocalState || loadedFromSave;
initialized = true;
}
@@ -104,11 +109,16 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
InitializeIfNeeded();
boundPlayerData = playerData;
if (!loadedFromSave)
if (!loadedFromSave && !hasAnyRecoverableLocalState)
{
payload.playerExp = Mathf.Max(0, playerData.player_currentEXP);
// 首次启动且无任何可恢复存档:用硬编码默认值(0)建立初始存档,
// 不读取 Player_SO 的烘焙经验值,避免打包测试数据污染玩家进度。
payload = CreateDefaultPayload();
SyncToPlayerData();
SaveNow();
loadedFromSave = true;
hasAnyRecoverableLocalState = true;
NotifyExperienceChanged();
return;
}
@@ -122,6 +132,13 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
return payload.playerExp;
}
public void SetExperience(int amount)
{
InitializeIfNeeded();
payload.playerExp = Mathf.Max(0, amount);
SaveNow();
}
public void AddExperience(int amount)
{
if (amount == 0)
@@ -181,6 +198,16 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
NotifyExperienceChanged();
}
public void ClearPersistentState()
{
InitializeIfNeeded();
payload = CreateDefaultPayload();
SecureSaveVault.Delete("player_experience", "runtime", GetLegacySavePath());
PlayerProgressBackupService.ClearPlayerExperienceBackup();
SyncToPlayerData();
NotifyExperienceChanged();
}
private void SyncToPlayerData()
{
if (boundPlayerData == null || payload == null)
@@ -10,6 +10,7 @@ public class PlayerProgressBackupBundle
public int version = 1;
public long savedUtcTicks;
public PlayerEconomyPayload economy;
public bool hasPlayerExperience;
public int playerExperience;
public float bestOverallRks;
public PlayerSkillSaveData playerSkill;
@@ -27,6 +28,62 @@ public static class PlayerProgressBackupService
private static bool s_cacheLoaded;
private static bool s_isWriting;
public static bool HasExistingBackupFile()
{
IReadOnlyList<string> candidates = SaveIdentityUtility.GetPersistentRootVariants();
for (int i = 0; i < candidates.Count; i++)
{
string root = candidates[i];
if (string.IsNullOrWhiteSpace(root))
{
continue;
}
if (File.Exists(Path.Combine(root, BackupFileName)))
{
return true;
}
}
return false;
}
public static bool HasEconomyBackup()
{
PlayerProgressBackupBundle bundle;
return TryLoadBundle(out bundle) && bundle != null && bundle.economy != null;
}
public static bool HasPlayerExperienceBackup()
{
PlayerProgressBackupBundle bundle;
return TryLoadBundle(out bundle) && bundle != null && (bundle.hasPlayerExperience || bundle.playerExperience > 0);
}
public static bool HasAllyHeroDeployBackup()
{
PlayerProgressBackupBundle bundle;
return TryLoadBundle(out bundle) && bundle != null && bundle.allyHeroDeploy != null;
}
public static bool HasExpBottleBackup()
{
PlayerProgressBackupBundle bundle;
return TryLoadBundle(out bundle) && bundle != null && bundle.expBottle != null;
}
public static bool HasDushMaterialBackup()
{
PlayerProgressBackupBundle bundle;
return TryLoadBundle(out bundle) && bundle != null && bundle.dushMaterial != null;
}
public static bool HasStoreOwnershipBackup()
{
PlayerProgressBackupBundle bundle;
return TryLoadBundle(out bundle) && bundle != null && bundle.storeOwnership != null;
}
public static bool TryRestoreEconomy(out PlayerEconomyPayload payload)
{
payload = null;
@@ -59,7 +116,7 @@ public static class PlayerProgressBackupService
return false;
}
if (bundle.playerExperience <= 0)
if (!bundle.hasPlayerExperience && bundle.playerExperience <= 0)
{
return false;
}
@@ -70,7 +127,11 @@ public static class PlayerProgressBackupService
public static void SavePlayerExperience(int experience)
{
UpdateBundle(bundle => bundle.playerExperience = Mathf.Max(0, experience));
UpdateBundle(bundle =>
{
bundle.hasPlayerExperience = true;
bundle.playerExperience = Mathf.Max(0, experience);
});
}
public static bool TryRestoreRks(out float rks)
@@ -229,6 +290,59 @@ public static class PlayerProgressBackupService
UpdateBundle(bundle => bundle.storeOwnership = CloneStoreOwnership(payload));
}
public static void ClearPlayerExperienceBackup()
{
UpdateBundle(bundle =>
{
bundle.hasPlayerExperience = false;
bundle.playerExperience = 0;
});
}
public static void ClearRksBackup()
{
UpdateBundle(bundle => bundle.bestOverallRks = 0f);
}
public static void ClearPlayerSkillBackup()
{
UpdateBundle(bundle => bundle.playerSkill = null);
}
public static void ClearStoreOwnershipBackup()
{
UpdateBundle(bundle => bundle.storeOwnership = null);
}
public static void ClearAllBackupData()
{
s_cachedBundle = null;
s_cacheLoaded = true;
IReadOnlyList<string> candidates = SaveIdentityUtility.GetPersistentRootVariants();
for (int i = 0; i < candidates.Count; i++)
{
string root = candidates[i];
if (string.IsNullOrWhiteSpace(root))
{
continue;
}
string path = Path.Combine(root, BackupFileName);
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (Exception ex)
{
Debug.LogWarning("[PlayerProgressBackup] Failed to delete backup '" + path + "': " + ex.Message);
}
}
}
private static void UpdateBundle(Action<PlayerProgressBackupBundle> mutator)
{
if (mutator == null || s_isWriting)
@@ -87,6 +87,7 @@ public static class PlayerRksService
loaded = true;
bestOverallRks = 0f;
SecureSaveVault.Delete(SaveCategory, SaveKey);
PlayerProgressBackupService.ClearRksBackup();
SyncPlayerSo(player);
OnRksChanged?.Invoke(bestOverallRks);
}
@@ -144,6 +144,11 @@ public sealed class PlayerSkillService : MonoBehaviour
EnsureInstance().ClearPersistentStateInternal();
}
public static void ResetToDefaultLevelOneSkill()
{
EnsureInstance().ResetToDefaultLevelOneSkillInternal();
}
private void Awake()
{
if (Instance != null && Instance != this)
@@ -423,6 +428,35 @@ public sealed class PlayerSkillService : MonoBehaviour
{
saveData = new PlayerSkillSaveData();
SecureSaveVault.Delete(SaveCategory, SaveKey);
PlayerProgressBackupService.ClearPlayerSkillBackup();
ApplySceneBindings();
}
private void ResetToDefaultLevelOneSkillInternal()
{
InitializeIfNeeded();
ResolveSkillAssetIfNeeded();
saveData = saveData ?? new PlayerSkillSaveData();
saveData.postMatchRewardCounter = 0;
saveData.skillSwitchCooldownRemainingMatches = 0;
int defaultIndex = -1;
if (registeredSkillAsset != null && registeredSkillAsset.skills != null && registeredSkillAsset.skills.Count > 0)
{
defaultIndex = 0;
for (int i = 0; i < registeredSkillAsset.skills.Count; i++)
{
userLevel_skills_SO.UserLevelSkillEntry entry = registeredSkillAsset.skills[i];
if (entry != null)
{
entry.isEnabled = i == defaultIndex;
}
}
}
saveData.selectedSkillIndex = defaultIndex;
SaveNow();
ApplySceneBindings();
}
@@ -6,6 +6,7 @@ public static class RecentPlayHistoryStore
private const string Category = "recent_play_history";
private const string Key = "runs_v1";
private const int MaxRecordCount = 100;
private const string RecoverySlotKey = "recent_play_history_runs_v1";
public static IReadOnlyList<RecentPlayRecord> GetRecords()
{
@@ -32,11 +33,13 @@ public static class RecentPlayHistoryStore
}
SecureSaveVault.SaveJson(Category, Key, payload);
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
}
public static void Clear()
{
SecureSaveVault.Delete(Category, Key);
LocalRecoveryMirror.DeleteSlot(RecoverySlotKey);
}
private static RecentPlayHistoryPayload LoadPayload()
@@ -44,7 +47,14 @@ public static class RecentPlayHistoryStore
RecentPlayHistoryPayload payload;
if (!SecureSaveVault.TryLoadJson(Category, Key, out payload) || payload == null)
{
payload = new RecentPlayHistoryPayload();
if (!LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) || payload == null)
{
payload = new RecentPlayHistoryPayload();
}
else
{
SecureSaveVault.SaveJson(Category, Key, payload);
}
}
if (payload.records == null)
@@ -19,6 +19,7 @@ public static class SecureSaveVault
{
private const string SecretSeed = "ban_total.secure_save_v2";
private const string VaultDirectoryName = ".cache_bridge";
private const string RecoveryDirectoryName = ".save_recovery";
private static bool s_dpapiInitialized;
private static bool s_dpapiSupported;
private static MethodInfo s_dpapiProtectMethod;
@@ -30,6 +31,11 @@ public static class SecureSaveVault
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
}
private static string RecoveryDirectoryPath
{
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), RecoveryDirectoryName); }
}
public static bool SaveJson<T>(string category, string key, T data, string legacyPlainPath = null)
{
if (!typeof(T).IsValueType && (object)data == null)
@@ -103,6 +109,7 @@ public static class SecureSaveVault
File.Copy(tempPath, mainPath, true);
TryHidePath(mainPath);
File.Delete(tempPath);
TrySaveRecoveryCopy(category, key, json);
DeleteLegacyPlainFile(legacyPlainPath);
return true;
}
@@ -126,6 +133,12 @@ public static class SecureSaveVault
return true;
}
if (TryLoadFromRecoveryCopies(category, key, out json))
{
SaveRawJson(category, key, json, legacyPlainPath);
return true;
}
if (!string.IsNullOrEmpty(legacyPlainPath) && File.Exists(legacyPlainPath))
{
try
@@ -236,6 +249,47 @@ public static class SecureSaveVault
return Directory.GetFiles(categoryDirectory, "*.dat", SearchOption.TopDirectoryOnly).Length;
}
public static bool HasAnyRecoverableState(string category, string key, string legacyPlainPath = null)
{
if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(key))
{
return false;
}
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants(legacyPlainPath);
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int rootIndex = 0; rootIndex < roots.Count; rootIndex++)
{
string root = roots[rootIndex];
if (string.IsNullOrWhiteSpace(root))
{
continue;
}
for (int variantIndex = 0; variantIndex < identifierVariants.Count; variantIndex++)
{
string applicationIdentifier = identifierVariants[variantIndex];
if (File.Exists(GetFilePathForRoot(root, category, key, ".dat", applicationIdentifier))
|| File.Exists(GetFilePathForRoot(root, category, key, ".bak", applicationIdentifier)))
{
return true;
}
}
}
IReadOnlyList<string> recoveryCandidates = GetRecoveryFilePathVariants(category, key);
for (int i = 0; i < recoveryCandidates.Count; i++)
{
string path = recoveryCandidates[i];
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
{
return true;
}
}
return !string.IsNullOrEmpty(legacyPlainPath) && File.Exists(legacyPlainPath);
}
private static bool TryReadEncryptedFile(string category, string key, string filePath, out string json)
{
json = null;
@@ -616,6 +670,117 @@ public static class SecureSaveVault
return Path.Combine(categoryDirectory, "." + safeKey + extension);
}
private static bool TryLoadFromRecoveryCopies(string category, string key, out string json)
{
json = null;
IReadOnlyList<string> candidates = GetRecoveryFilePathVariants(category, key);
for (int i = 0; i < candidates.Count; i++)
{
string path = candidates[i];
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
continue;
}
try
{
string loaded = File.ReadAllText(path, Encoding.UTF8);
if (string.IsNullOrWhiteSpace(loaded))
{
continue;
}
json = loaded;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] Recovery read failed ({path}): {ex.Message}");
}
}
return false;
}
private static void TrySaveRecoveryCopy(string category, string key, string json)
{
if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(key) || json == null)
{
return;
}
try
{
Directory.CreateDirectory(RecoveryDirectoryPath);
string recoveryPath = GetRecoveryFilePath(category, key);
string backupPath = recoveryPath + ".bak";
string tempPath = recoveryPath + ".tmp";
File.WriteAllText(tempPath, json, Encoding.UTF8);
if (File.Exists(recoveryPath))
{
File.Copy(recoveryPath, backupPath, true);
}
File.Copy(tempPath, recoveryPath, true);
File.Delete(tempPath);
}
catch (Exception ex)
{
Debug.LogWarning($"[SecureSaveVault] Recovery mirror save failed ({category}/{key}): {ex.Message}");
}
}
private static string GetRecoveryFilePath(string category, string key)
{
string safeName = ShortHash("recovery|" + category + "|" + key);
return Path.Combine(RecoveryDirectoryPath, safeName + ".json");
}
private static IReadOnlyList<string> GetRecoveryFilePathVariants(string category, string key)
{
var result = new List<string>();
if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(key))
{
return result;
}
string safeName = ShortHash("recovery|" + category + "|" + key) + ".json";
string safeBackupName = safeName + ".bak";
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants();
for (int i = 0; i < roots.Count; i++)
{
string root = roots[i];
if (string.IsNullOrWhiteSpace(root))
{
continue;
}
AddDistinctPath(result, Path.Combine(root, RecoveryDirectoryName, safeName));
AddDistinctPath(result, Path.Combine(root, RecoveryDirectoryName, safeBackupName));
}
return result;
}
private static void AddDistinctPath(List<string> target, string value)
{
if (target == null || string.IsNullOrWhiteSpace(value))
{
return;
}
for (int i = 0; i < target.Count; i++)
{
if (string.Equals(target[i], value, StringComparison.OrdinalIgnoreCase))
{
return;
}
}
target.Add(value);
}
private static void DeleteLegacyPlainFile(string legacyPlainPath)
{
if (string.IsNullOrEmpty(legacyPlainPath) || !File.Exists(legacyPlainPath))
@@ -11,6 +11,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
public static StoreOwnershipLedger Instance { get; private set; }
public event Action<int> OnOwnershipChanged;
private readonly Dictionary<int, StoreOwnershipEntry> entriesByItemId = new Dictionary<int, StoreOwnershipEntry>();
private readonly List<storeItemSO> cachedStoreItems = new List<storeItemSO>();
private bool initialized;
@@ -72,11 +74,12 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
initialized = true;
StoreOwnershipPayload payload;
bool loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
bool hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
RebuildFromPayload(payload);
LoadStoreItems();
bool recoveredFromMirrors = false;
if (!loadedFromSave)
if (!loadedFromSave && !hasAnyRecoverableLocalState)
{
recoveredFromMirrors = SeedFromCurrentMirrorFlags();
}
@@ -149,6 +152,7 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
ApplyEntryToItem(itemSO, entry);
GlobalAchievementService.EnsureInstance().RefreshDerivedMetrics();
SaveNow();
NotifyOwnershipChanged(itemSO.itemID);
return true;
}
@@ -187,6 +191,7 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
SyncAllMirrorFlags();
GlobalAchievementService.EnsureInstance().RefreshDerivedMetrics();
SaveNow();
NotifyOwnershipChanged(itemSO.itemID);
return true;
}
@@ -212,6 +217,14 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
StoreOwnershipStorage.TrySave(CreatePayload());
}
public void ClearPersistentState()
{
InitializeIfNeeded();
entriesByItemId.Clear();
SyncAllMirrorFlags();
StoreOwnershipStorage.TrySave(CreatePayload());
}
private void RebuildFromPayload(StoreOwnershipPayload payload)
{
entriesByItemId.Clear();
@@ -770,4 +783,14 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
}
#endif
}
private void NotifyOwnershipChanged(int itemId)
{
if (itemId <= 0 || OnOwnershipChanged == null)
{
return;
}
OnOwnershipChanged(itemId);
}
}
@@ -11,6 +11,7 @@ public static class StoreOwnershipStorage
private const string MainFileName = ".own.dat";
private const string BackupFileName = ".own.bak";
private const string TempFileName = ".own.tmp";
private const string RecoverySlotKey = "store_ownership_storage";
private static string VaultDirectoryPath
{
@@ -53,9 +54,23 @@ public static class StoreOwnershipStorage
return true;
}
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
{
TrySave(payload);
return true;
}
return false;
}
public static bool HasAnyRecoverableState()
{
return HasAnyVaultFile(MainFileName)
|| HasAnyVaultFile(BackupFileName)
|| PlayerProgressBackupService.HasStoreOwnershipBackup()
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
public static bool TrySave(StoreOwnershipPayload payload)
{
try
@@ -77,6 +92,7 @@ public static class StoreOwnershipStorage
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
PlayerProgressBackupService.SaveStoreOwnership(payload);
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
return true;
}
catch (Exception ex)
@@ -169,6 +185,20 @@ public static class StoreOwnershipStorage
return false;
}
private static bool HasAnyVaultFile(string fileName)
{
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
for (int i = 0; i < candidates.Count; i++)
{
if (File.Exists(candidates[i]))
{
return true;
}
}
return false;
}
private static string BuildEnvelopeJson(StoreOwnershipPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
@@ -6,12 +6,19 @@ public static class DailyTaskSaveService
{
private const string SaveCategory = "daily_task";
private const string SaveKey = "runtime";
private const string RecoverySlotKey = "daily_task_runtime";
private static string LegacySaveFilePath
{
get { return Path.Combine(Application.persistentDataPath, "daily_tasks.json"); }
}
public static bool HasAnyRecoverableState()
{
return SecureSaveVault.HasAnyRecoverableState(SaveCategory, SaveKey, LegacySaveFilePath)
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
public static DailyTaskSaveData Load()
{
try
@@ -22,6 +29,12 @@ public static class DailyTaskSaveService
return data ?? new DailyTaskSaveData();
}
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out data) && data != null)
{
SecureSaveVault.SaveJson(SaveCategory, SaveKey, data, LegacySaveFilePath);
return data;
}
return new DailyTaskSaveData();
}
catch (Exception ex)
@@ -41,10 +54,24 @@ public static class DailyTaskSaveService
try
{
SecureSaveVault.SaveJson(SaveCategory, SaveKey, data, LegacySaveFilePath);
LocalRecoveryMirror.SaveJson(RecoverySlotKey, data);
}
catch (Exception ex)
{
Debug.LogWarning($"[DailyTaskSaveService] Save failed: {ex.Message}");
}
}
public static void ClearPersistentState()
{
try
{
SecureSaveVault.Delete(SaveCategory, SaveKey, LegacySaveFilePath);
LocalRecoveryMirror.DeleteSlot(RecoverySlotKey);
}
catch (Exception ex)
{
Debug.LogWarning($"[DailyTaskSaveService] Clear failed: {ex.Message}");
}
}
}
@@ -333,6 +333,21 @@ public sealed class DailyTaskService : MonoBehaviour
return claimedCount;
}
public void ClearPersistentState()
{
InitializeStorageIfNeeded();
pendingEvents.Clear();
pendingOnlineDurationSeconds = 0f;
isGameplayDurationTracking = false;
gameplayDurationRealtimeStart = 0f;
saveData = new DailyTaskSaveData();
EnsureRuntimeCollections();
DailyTaskSaveService.ClearPersistentState();
NotifyTasksChanged();
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
if (string.Equals(scene.name, MainUiSceneName, StringComparison.OrdinalIgnoreCase))
@@ -5,11 +5,7 @@ using UnityEngine.Networking;
using UnityEngine.UI;
using GameServer.Client;
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
#define FRIEND_DISABLE_STEAMWORKS
#endif
#if !FRIEND_DISABLE_STEAMWORKS
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
@@ -262,7 +258,7 @@ public class friendCardPrefab : MonoBehaviour
private IEnumerator LoadSteamAvatarRoutine(string steamId)
{
#if FRIEND_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
yield break;
#else
if (!SteamManager.Initialized || string.IsNullOrWhiteSpace(steamId) || friendProfile == null || !ulong.TryParse(steamId, out ulong rawSteamId))
@@ -6,11 +6,7 @@ using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
#define FRIEND_DISABLE_STEAMWORKS
#endif
#if !FRIEND_DISABLE_STEAMWORKS
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
@@ -170,7 +166,7 @@ public class friendRequestPrefab : MonoBehaviour
private IEnumerator LoadSteamAvatarRoutine(string steamId)
{
#if FRIEND_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
yield break;
#else
if (!SteamManager.Initialized || string.IsNullOrWhiteSpace(steamId) || profileImage == null || !ulong.TryParse(steamId, out ulong rawSteamId))
@@ -6,11 +6,7 @@ using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
#define FRIEND_DISABLE_STEAMWORKS
#endif
#if !FRIEND_DISABLE_STEAMWORKS
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
@@ -162,7 +158,7 @@ public class friendSearchPrefab : MonoBehaviour
private IEnumerator LoadSteamAvatarRoutine(string steamId)
{
#if FRIEND_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
yield break;
#else
if (!SteamManager.Initialized || string.IsNullOrWhiteSpace(steamId) || profileImage == null || !ulong.TryParse(steamId, out ulong rawSteamId))
+3 -7
View File
@@ -7,11 +7,7 @@ using GameServer.Client;
using UnityEngine;
using UnityEngine.UI;
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
#define FRIEND_DISABLE_STEAMWORKS
#endif
#if !FRIEND_DISABLE_STEAMWORKS
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
@@ -765,7 +761,7 @@ public class friendSystem : MonoBehaviour
private static List<FriendCardViewData> ReadSteamFriends()
{
var result = new List<FriendCardViewData>();
#if FRIEND_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
return result;
#else
if (!SteamManager.Initialized)
@@ -837,7 +833,7 @@ public class friendSystem : MonoBehaviour
private static List<string> ReadSteamFriendIdsOnly()
{
var result = new List<string>();
#if FRIEND_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
return result;
#else
if (!SteamManager.Initialized)
@@ -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);
}
+10 -14
View File
@@ -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
+21 -16
View File
@@ -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();
}
}
+6 -6
View File
@@ -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}");
+13 -18
View File
@@ -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;
@@ -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.
@@ -6,11 +6,7 @@ using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
#define ROOMCHAT_DISABLE_STEAMWORKS
#endif
#if !ROOMCHAT_DISABLE_STEAMWORKS
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
@@ -271,7 +267,7 @@ public class playerMessagePrefab : MonoBehaviour
private static async Task<Sprite> TryGetSteamAvatarSpriteAsync(string steamId)
{
#if ROOMCHAT_DISABLE_STEAMWORKS
#if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX)
return null;
#else
if (string.IsNullOrWhiteSpace(steamId) || !SteamManager.Initialized)
+10 -7
View File
@@ -4,7 +4,9 @@ using UnityEngine;
using UnityEngine.UI;
using Bansonic;
using UnityEngine.SceneManagement;
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
using Steamworks;
#endif
#if UNITY_EDITOR
using UnityEditor;
#endif
@@ -49,6 +51,7 @@ public class userSettings : MonoBehaviour
private void UpdateUserInfoDisplay()
{
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
if (SteamManager.Initialized)
{
if (user_login_source_text != null)
@@ -70,15 +73,15 @@ public class userSettings : MonoBehaviour
string personaName = SteamFriends.GetPersonaName();
user_source_uid_text.text = string.Format("{0} ({1})", personaName, steamID.m_SteamID.ToString());
}
}
else
{
if (user_login_source_text != null)
user_login_source_text.text = LocalizationService.Get("settings.source.unknown_server", "Unknown Server");
if (user_source_uid_text != null)
user_source_uid_text.text = LocalizationService.Get("settings.source.unknown_user", "Unknown User");
return;
}
#endif
if (user_login_source_text != null)
user_login_source_text.text = LocalizationService.Get("settings.source.unknown_server", "Unknown Server");
if (user_source_uid_text != null)
user_source_uid_text.text = LocalizationService.Get("settings.source.unknown_user", "Unknown User");
}
private void OnEnable()