using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using UnityEngine; namespace BaseGames.Core.Save { /// /// 存档管理器。 /// 完整序列化/反序列化由 Newtonsoft.Json 驱动。 /// [DefaultExecutionOrder(-900)] public class SaveManager : MonoBehaviour, ISaveableRegistry { public const int QuickSaveSlot = 98; [Header("Event Channels - Raise")] [SerializeField] private BaseGames.Core.Events.BoolEventChannelSO _onSaveIndicatorVisible; private ISaveStorage _storage; private readonly HashSet _saveables = new(); private readonly SemaphoreSlim _saveLock = new SemaphoreSlim(1, 1); private SaveData _current; private int _currentSlot = 0; public int CurrentSlot => _currentSlot; public SaveData Data => _current; public string LastCheckpointScene { get; private set; } public string LastCheckpointSpawnId { get; private set; } private static SaveManager _instance; private void Awake() { if (_instance != null) { Destroy(gameObject); return; } _instance = this; ServiceLocator.Register(this); _storage = new LocalFileStorage(); } // ── ISaveable 注册 ──────────────────────────────────────────────────── public void Register(ISaveable s) => _saveables.Add(s); public void Unregister(ISaveable s) => _saveables.Remove(s); // ── 存档 ────────────────────────────────────────────────────────────── public async Task SaveAsync(int slot = -1) { await _saveLock.WaitAsync(); try { int targetSlot = slot < 0 ? _currentSlot : slot; _current ??= new SaveData(); _onSaveIndicatorVisible?.Raise(true); var snapshot = _saveables.ToList(); foreach (var s in snapshot) s.OnSave(_current); _current.Meta.LastSaved = DateTime.UtcNow.ToString("o"); _current.Meta.SlotIndex = targetSlot; _current.Meta.SaveCount++; // 先清空 checksum,序列化并计算,再序列化含 checksum 的最终版本 // 使用 Formatting.None 减少序列化字符串体积和 GC 分配 _current.Meta.Checksum = null; string jsonForChecksum = JsonConvert.SerializeObject(_current, Formatting.None); _current.Meta.Checksum = ComputeChecksum(jsonForChecksum); string finalJson = JsonConvert.SerializeObject(_current, Formatting.None); await _storage.WriteAsync(targetSlot, finalJson); LastCheckpointScene = _current.Player?.Scene; LastCheckpointSpawnId = _current.Meta?.SavePointId; _onSaveIndicatorVisible?.Raise(false); } finally { _saveLock.Release(); } } // ── 读档 ────────────────────────────────────────────────────────────── public async Task LoadAsync(int slot) { await _saveLock.WaitAsync(); try { if (!_storage.Exists(slot)) return false; string json = await _storage.ReadAsync(slot); if (json == null) return false; SaveData loaded; try { loaded = JsonConvert.DeserializeObject(json); } catch (Exception e) { Debug.LogError($"[SaveManager] 存档解析失败 slot={slot}: {e.Message}"); return false; } if (!ValidateChecksum(loaded, json)) { Debug.LogWarning("[SaveManager] 存档 checksum 校验失败。"); // 非 SteelSoul 模式仍允许加载(只是警告) } loaded = SaveMigrator.Migrate(loaded); _current = loaded; _currentSlot = slot; var snapshot = _saveables.ToList(); foreach (var s in snapshot) s.OnLoad(_current); LastCheckpointScene = _current.Player?.Scene; LastCheckpointSpawnId = _current.Meta?.SavePointId; return true; } finally { _saveLock.Release(); } } public bool SlotExists(int slot) => _storage.Exists(slot); public IEnumerable GetExistingSlots() => _storage.GetExistingSlots(); // ── 具名数据访问器(替代直接访问 Data 属性) ────────────────────────────────── /// 判断指定收藏物 ID 是否已拾取。 public bool IsWorldCollected(string id) => _current?.World.CollectedIds.Contains(id) == true; /// 判断指定门 / 进程锁 ID 是否已开启。 public bool IsDoorOpened(string id) => _current?.World.OpenedDoors.Contains(id) == true; /// 返回当前存档中玩家最大 HP(存档未加载时返回 0)。 public int GetPlayerMaxHP() => _current?.Player.MaxHP ?? 0; // ── 快速存档(挑战房间用)──────────────────────────────────────────── public void QuickSave() => RunFireAndForget(SaveAsync(QuickSaveSlot), "QuickSave"); public void QuickLoad() => RunFireAndForget(LoadAsync(QuickSaveSlot), "QuickLoad"); /// /// 将 async Task 包裹为 async void,捕获所有异常并输出到 Log。 /// 用于无法 await 的调用点(按钮回调、MonoBehaviour 方法等)。 /// private static async void RunFireAndForget(Task task, string context) { try { await task; } catch (Exception e) { Debug.LogError($"[SaveManager] {context} 失败: {e.Message}\n{e.StackTrace}"); } } // ── 世界标志 / EventChain ───────────────────────────────────────────── public bool GetFlag(string flagId) => _current?.EventChains.WorldFlags.TryGetValue(flagId, out var v) == true && v; public void SetFlag(string flagId, bool value) { _current ??= new SaveData(); _current.EventChains.WorldFlags[flagId] = value; } public void SetChainCompleted(string chainId) { _current ??= new SaveData(); _current.EventChains.ChainStates[chainId] = "Completed"; } public IEnumerable GetCompletedChains() => _current?.EventChains.ChainStates.Keys ?? Enumerable.Empty(); public bool IsFirstClear(string challengeId) { if (_current == null) return false; if (_current.World.ChallengeFirstClears.Contains(challengeId)) return false; _current.World.ChallengeFirstClears.Add(challengeId); return true; } public bool IsBossDefeated(string bossId) => _current?.World.DefeatedBossIds.Contains(bossId) == true; // ── 存档槽摘要 ──────────────────────────────────────────────────────── public async Task GetSlotSummaryAsync(int slotIndex) { if (!_storage.Exists(slotIndex)) return null; string json = await _storage.ReadAsync(slotIndex); if (json == null) return null; try { var data = JsonConvert.DeserializeObject(json); return new SlotSummary { SlotIndex = slotIndex, Playtime = data.Meta.Playtime, LastSaved = data.Meta.LastSaved, SceneName = data.Player?.Scene, ActiveFormId = data.Player?.ActiveFormId, }; } catch { return null; } } public void CreateSlot(int slotIndex) { _currentSlot = slotIndex; _current = new SaveData(); _current.Meta.SlotIndex = slotIndex; } public async Task DeleteSlotAsync(int slotIndex) { await _storage.DeleteAsync(slotIndex); if (_currentSlot == slotIndex) _current = null; } /// /// 将紧急存档槽的数据复制到目标槽,并删除紧急存档。 /// 由 调用,确保所有 IO 操作通过统一的 ISaveStorage 进行。 /// public async Task PromoteEmergencyToSlotAsync(int targetSlot, int emergencySlot) { if (!_storage.Exists(emergencySlot)) return; string json = await _storage.ReadAsync(emergencySlot); if (json == null) return; await _storage.WriteAsync(targetSlot, json); await _storage.DeleteAsync(emergencySlot); } // ── 私有工具 ────────────────────────────────────────────────────────── private string ComputeChecksum(string json) { var keyBytes = System.Text.Encoding.UTF8.GetBytes(GetHmacKey()); using var hmac = new System.Security.Cryptography.HMACSHA256(keyBytes); var dataBytes = System.Text.Encoding.UTF8.GetBytes(json); return Convert.ToBase64String(hmac.ComputeHash(dataBytes)); } private string GetHmacKey() { // ⚠️ 不使用 deviceUniqueIdentifier——设备唯一标识在玩家换设备时会改变, // 导致所有存档 checksum 永久失效。改用游戏固定密钥。 const string gameSecret = "ZelingV2SaveIntegrity_v2_9a3f7c1b"; return gameSecret; } private bool ValidateChecksum(SaveData data, string rawJson) { if (string.IsNullOrEmpty(data?.Meta?.Checksum)) return true; var saved = data.Meta.Checksum; data.Meta.Checksum = null; string jsonNoChecksum = JsonConvert.SerializeObject(data, Formatting.None); data.Meta.Checksum = saved; return ComputeChecksum(jsonNoChecksum) == saved; } private void OnDestroy() { if (_instance == this) _instance = null; ServiceLocator.Unregister(this); } } }