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 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);
}
}
}