Files
bansonic_beta_main/Assets/scripts/_runtimecache/PlayerExperienceLedger.cs
T
2026-07-16 23:04:59 +08:00

263 lines
6.6 KiB
C#

using System;
using System.IO;
using UnityEngine;
public sealed class PlayerExperienceLedger : MonoBehaviour
{
[Serializable]
private class PlayerExperiencePayload
{
public int version = 1;
public long lastUpdatedUtcTicks;
public int playerExp;
}
public static PlayerExperienceLedger Instance { get; private set; }
public event Action<int> OnExperienceChanged;
private PlayerExperiencePayload payload;
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
private Player_SO boundPlayerData;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static PlayerExperienceLedger EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__runtime_player_exp_bridge");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<PlayerExperienceLedger>();
return Instance;
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeIfNeeded();
TryAttachDefaultPlayerData();
}
private void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
SaveNow();
}
}
private void OnApplicationQuit()
{
SaveNow();
}
public void InitializeIfNeeded()
{
if (initialized)
{
return;
}
hasAnyRecoverableLocalState = SecureSaveVault.HasAnyRecoverableState("player_experience", "runtime", GetLegacySavePath())
|| PlayerProgressBackupService.HasPlayerExperienceBackup();
loadedFromSave = SecureSaveVault.TryLoadJson("player_experience", "runtime", out payload, GetLegacySavePath());
if (payload == null)
{
int backupExperience;
if (PlayerProgressBackupService.TryRestorePlayerExperience(out backupExperience))
{
payload = CreateDefaultPayload();
payload.playerExp = Mathf.Max(0, backupExperience);
loadedFromSave = true;
}
else
{
payload = CreateDefaultPayload();
}
}
hasAnyRecoverableLocalState = hasAnyRecoverableLocalState || loadedFromSave;
initialized = true;
}
public void AttachPlayerData(Player_SO playerData)
{
if (playerData == null)
{
return;
}
InitializeIfNeeded();
boundPlayerData = playerData;
if (!loadedFromSave && !hasAnyRecoverableLocalState)
{
// 首次启动且无任何可恢复存档:用硬编码默认值(0)建立初始存档,
// 不读取 Player_SO 的烘焙经验值,避免打包测试数据污染玩家进度。
payload = CreateDefaultPayload();
SyncToPlayerData();
SaveNow();
loadedFromSave = true;
hasAnyRecoverableLocalState = true;
NotifyExperienceChanged();
return;
}
SyncToPlayerData();
NotifyExperienceChanged();
}
public int GetExperience()
{
InitializeIfNeeded();
return payload.playerExp;
}
public void SetExperience(int amount)
{
InitializeIfNeeded();
payload.playerExp = Mathf.Max(0, amount);
SaveNow();
}
public void AddExperience(int amount)
{
if (amount == 0)
{
return;
}
InitializeIfNeeded();
long next = (long)payload.playerExp + amount;
if (next < 0)
{
next = 0;
}
else if (next > int.MaxValue)
{
next = int.MaxValue;
}
payload.playerExp = (int)next;
SaveNow();
}
public bool TryConsumeExperience(int amount)
{
if (amount <= 0)
{
return true;
}
InitializeIfNeeded();
if (payload.playerExp < amount)
{
return false;
}
payload.playerExp -= amount;
SaveNow();
return true;
}
public void SaveNow()
{
if (!initialized)
{
InitializeIfNeeded();
}
if (payload == null)
{
payload = CreateDefaultPayload();
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[PlayerExperienceLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older player experience data.");
return;
}
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
SecureSaveVault.SaveJson("player_experience", "runtime", payload, GetLegacySavePath());
PlayerProgressBackupService.SavePlayerExperience(payload.playerExp);
SyncToPlayerData();
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)
{
return;
}
boundPlayerData.player_currentEXP = Mathf.Max(0, payload.playerExp);
#if UNITY_EDITOR
if (!Application.isPlaying)
{
UnityEditor.EditorUtility.SetDirty(boundPlayerData);
}
#endif
}
private void NotifyExperienceChanged()
{
OnExperienceChanged?.Invoke(GetExperience());
}
private void TryAttachDefaultPlayerData()
{
Player_SO player = RuntimeResourcesCache.LoadDefaultPlayerSo();
if (player != null)
{
AttachPlayerData(player);
}
}
private static PlayerExperiencePayload CreateDefaultPayload()
{
return new PlayerExperiencePayload
{
version = 1,
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
playerExp = 0,
};
}
private static string GetLegacySavePath()
{
return Path.Combine(Application.persistentDataPath, "player_experience.json");
}
}