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