feat: 更新存档管理,添加异步存档槽管理功能,优化存档验证逻辑
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using BaseGames.Core.Events;
|
using BaseGames.Core.Events;
|
||||||
@@ -105,12 +106,20 @@ namespace BaseGames.Core
|
|||||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||||
Debug.Log($"[AutoSave] 触发:{reason}");
|
Debug.Log($"[AutoSave] 触发:{reason}");
|
||||||
#endif
|
#endif
|
||||||
// 与手动存档使用相同槽位,fire-and-forget
|
RunFireAndForget(svc.SaveAsync(svc.ActiveSlot), reason);
|
||||||
_ = svc.SaveAsync(svc.ActiveSlot);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return new WaitForSecondsRealtime(_cooldownSeconds);
|
yield return new WaitForSecondsRealtime(_cooldownSeconds);
|
||||||
_onCooldown = false;
|
_onCooldown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async void RunFireAndForget(System.Threading.Tasks.Task task, string context)
|
||||||
|
{
|
||||||
|
try { await task; }
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogError($"[AutoSave] {context} 存档失败: {e.Message}\n{e.StackTrace}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,5 +232,10 @@ namespace BaseGames.Core
|
|||||||
public System.Collections.Generic.IEnumerable<string> GetCompletedChains() => _save.GetCompletedChains();
|
public System.Collections.Generic.IEnumerable<string> GetCompletedChains() => _save.GetCompletedChains();
|
||||||
public void SetChainCompleted(string chainId) => _save.SetChainCompleted(chainId);
|
public void SetChainCompleted(string chainId) => _save.SetChainCompleted(chainId);
|
||||||
public void MarkBossDefeated(string bossId) => _save.MarkBossDefeated(bossId);
|
public void MarkBossDefeated(string bossId) => _save.MarkBossDefeated(bossId);
|
||||||
|
|
||||||
|
// ── 存档槽管理(主菜单 UI 用)─────────────────────────────────────────
|
||||||
|
public System.Threading.Tasks.Task<BaseGames.Core.Save.SlotSummary> GetSlotSummaryAsync(int slotIndex)
|
||||||
|
=> _save.GetSlotSummaryAsync(slotIndex);
|
||||||
|
public void CreateSlot(int slotIndex) => _save.CreateSlot(slotIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BaseGames.Core.Save;
|
||||||
|
|
||||||
namespace BaseGames.Core
|
namespace BaseGames.Core
|
||||||
{
|
{
|
||||||
@@ -76,5 +77,13 @@ namespace BaseGames.Core
|
|||||||
|
|
||||||
/// <summary>标记某个 Boss 已被击败,立即写入当前 SaveData(无需等待下次存档)。</summary>
|
/// <summary>标记某个 Boss 已被击败,立即写入当前 SaveData(无需等待下次存档)。</summary>
|
||||||
void MarkBossDefeated(string bossId);
|
void MarkBossDefeated(string bossId);
|
||||||
|
|
||||||
|
// ── 存档槽管理(主菜单 UI 用)────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>读取指定存档槽摘要数据用于 UI 展示;槽不存在时返回 null。</summary>
|
||||||
|
Task<SlotSummary> GetSlotSummaryAsync(int slotIndex);
|
||||||
|
|
||||||
|
/// <summary>新建存档槽(不写盘,仅初始化内存 SaveData 并设定活跃槽索引)。</summary>
|
||||||
|
void CreateSlot(int slotIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ namespace BaseGames.Core.Save
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ValidateChecksum(loaded, json))
|
if (!ValidateChecksum(loaded))
|
||||||
{
|
{
|
||||||
if (loaded.Meta.IsSteelSoul)
|
if (loaded.Meta.IsSteelSoul)
|
||||||
{
|
{
|
||||||
@@ -278,7 +278,7 @@ namespace BaseGames.Core.Save
|
|||||||
return gameSecret;
|
return gameSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ValidateChecksum(SaveData data, string rawJson)
|
private bool ValidateChecksum(SaveData data)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(data?.Meta?.Checksum)) return true;
|
if (string.IsNullOrEmpty(data?.Meta?.Checksum)) return true;
|
||||||
var saved = data.Meta.Checksum;
|
var saved = data.Meta.Checksum;
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ public class SpeedrunTimer : MonoBehaviour, ISaveable
|
|||||||
/// <summary>上一帧已显示的整秒值,展示内容未变化时跳过字符串重建。</summary>
|
/// <summary>上一帧已显示的整秒值,展示内容未变化时跳过字符串重建。</summary>
|
||||||
private int _lastDisplayedSecond = -1;
|
private int _lastDisplayedSecond = -1;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
bool show = _settings != null && _settings.ShowSpeedrunTimer;
|
bool show = _settings != null && _settings.ShowSpeedrunTimer;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
|
using BaseGames.Core;
|
||||||
using BaseGames.Core.Events;
|
using BaseGames.Core.Events;
|
||||||
using BaseGames.Core.Save;
|
using BaseGames.Core.Save;
|
||||||
|
|
||||||
@@ -14,7 +15,6 @@ namespace BaseGames.UI.Menus
|
|||||||
public class SaveSlotController : MonoBehaviour
|
public class SaveSlotController : MonoBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private SaveSlotUI[] _slotUIs; // 存档槽 UI(数量由 Inspector 决定)
|
[SerializeField] private SaveSlotUI[] _slotUIs; // 存档槽 UI(数量由 Inspector 决定)
|
||||||
[SerializeField] private GameSaveManager _saveManager;
|
|
||||||
|
|
||||||
[Header("Event Channels")]
|
[Header("Event Channels")]
|
||||||
[SerializeField] private IntEventChannelSO _onSlotConfirmed; // 携带槽索引,供 GameManager 监听
|
[SerializeField] private IntEventChannelSO _onSlotConfirmed; // 携带槽索引,供 GameManager 监听
|
||||||
@@ -22,7 +22,6 @@ namespace BaseGames.UI.Menus
|
|||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
var task = RefreshAsync();
|
var task = RefreshAsync();
|
||||||
// 捕获 async Task 异常,避免 async void 吞掉未处理异常
|
|
||||||
task.ContinueWith(t =>
|
task.ContinueWith(t =>
|
||||||
{
|
{
|
||||||
if (t.IsFaulted)
|
if (t.IsFaulted)
|
||||||
@@ -32,11 +31,12 @@ namespace BaseGames.UI.Menus
|
|||||||
|
|
||||||
private async Task RefreshAsync()
|
private async Task RefreshAsync()
|
||||||
{
|
{
|
||||||
if (_saveManager == null) return;
|
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||||||
|
if (svc == null) return;
|
||||||
for (int i = 0; i < _slotUIs.Length; i++)
|
for (int i = 0; i < _slotUIs.Length; i++)
|
||||||
{
|
{
|
||||||
if (_slotUIs[i] == null) continue;
|
if (_slotUIs[i] == null) continue;
|
||||||
var summary = await _saveManager.GetSlotSummaryAsync(i);
|
var summary = await svc.GetSlotSummaryAsync(i);
|
||||||
_slotUIs[i].Refresh(summary);
|
_slotUIs[i].Refresh(summary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,16 +44,19 @@ namespace BaseGames.UI.Menus
|
|||||||
/// <summary>选中指定槽位(新局或继续)。由 SaveSlotUI 内部按钮调用。</summary>
|
/// <summary>选中指定槽位(新局或继续)。由 SaveSlotUI 内部按钮调用。</summary>
|
||||||
public void OnSlotSelected(int slotIndex)
|
public void OnSlotSelected(int slotIndex)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= _slotUIs.Length || _saveManager == null) return;
|
if (slotIndex < 0 || slotIndex >= _slotUIs.Length) return;
|
||||||
_ = SelectSlotAsync(slotIndex);
|
_ = SelectSlotAsync(slotIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SelectSlotAsync(int slotIndex)
|
private async Task SelectSlotAsync(int slotIndex)
|
||||||
{
|
{
|
||||||
if (_saveManager.SlotExists(slotIndex))
|
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||||||
await _saveManager.LoadAsync(slotIndex);
|
if (svc == null) return;
|
||||||
|
|
||||||
|
if (svc.HasSave(slotIndex))
|
||||||
|
await svc.LoadAsync(slotIndex);
|
||||||
else
|
else
|
||||||
_saveManager.CreateSlot(slotIndex);
|
svc.CreateSlot(slotIndex);
|
||||||
|
|
||||||
_onSlotConfirmed?.Raise(slotIndex);
|
_onSlotConfirmed?.Raise(slotIndex);
|
||||||
}
|
}
|
||||||
@@ -61,13 +64,15 @@ namespace BaseGames.UI.Menus
|
|||||||
/// <summary>删除指定槽位存档并刷新 UI。由 SaveSlotUI 内部按钮调用。</summary>
|
/// <summary>删除指定槽位存档并刷新 UI。由 SaveSlotUI 内部按钮调用。</summary>
|
||||||
public void OnSlotDeleteRequested(int slotIndex)
|
public void OnSlotDeleteRequested(int slotIndex)
|
||||||
{
|
{
|
||||||
if (slotIndex < 0 || slotIndex >= _slotUIs.Length || _saveManager == null) return;
|
if (slotIndex < 0 || slotIndex >= _slotUIs.Length) return;
|
||||||
_ = DeleteAndRefreshAsync(slotIndex);
|
_ = DeleteAndRefreshAsync(slotIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteAndRefreshAsync(int slotIndex)
|
private async Task DeleteAndRefreshAsync(int slotIndex)
|
||||||
{
|
{
|
||||||
await _saveManager.DeleteSlotAsync(slotIndex);
|
var svc = ServiceLocator.GetOrDefault<ISaveService>();
|
||||||
|
if (svc == null) return;
|
||||||
|
await svc.DeleteSlotAsync(slotIndex);
|
||||||
await RefreshAsync();
|
await RefreshAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ namespace BaseGames.World
|
|||||||
/// 挂载位置:Persistent 场景的根 GameObject(与 GameServiceRegistrar 同级)。
|
/// 挂载位置:Persistent 场景的根 GameObject(与 GameServiceRegistrar 同级)。
|
||||||
/// 需在 Inspector 中绑定场景中唯一的 WorldStateRegistry ScriptableObject 资源。
|
/// 需在 Inspector 中绑定场景中唯一的 WorldStateRegistry ScriptableObject 资源。
|
||||||
///
|
///
|
||||||
/// OnSave:从 Registry 读取 Collectible / Destroyed / Flag 三个分类,写入 SaveData。
|
/// OnSave:从 Registry 读取 Collectible / Destroyed / Door / Flag 四个分类,写入 SaveData。
|
||||||
/// SavePoint 和 Door 两个分类由各自的 SaveableMonoBehaviour 独立写入,此处跳过。
|
/// 每个分类在写入前先 Clear,确保运行时删除的状态不会残留在存档里。
|
||||||
/// OnLoad:将完整的 SaveData 传递给 Registry,恢复所有分类的运行时缓存。
|
/// OnLoad:将完整的 SaveData 传递给 Registry,恢复所有分类的运行时缓存。
|
||||||
/// 这是 Registry 从空白变为"知道世界状态"的唯一时机。
|
/// 这是 Registry 从空白变为"知道世界状态"的唯一时机。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -29,6 +29,12 @@ namespace BaseGames.World
|
|||||||
foreach (var id in _registry.GetAllIds(WorldObjectCategory.Destroyed))
|
foreach (var id in _registry.GetAllIds(WorldObjectCategory.Destroyed))
|
||||||
data.World.DestroyedObjectIds.Add(id);
|
data.World.DestroyedObjectIds.Add(id);
|
||||||
|
|
||||||
|
data.World.OpenedDoors.Clear();
|
||||||
|
foreach (var id in _registry.GetAllIds(WorldObjectCategory.Door))
|
||||||
|
data.World.OpenedDoors.Add(id);
|
||||||
|
|
||||||
|
// 清除所有 Flag 后重写,确保运行时 ClearFlag() 的变更能正确落盘
|
||||||
|
data.EventChains.WorldFlags.Clear();
|
||||||
foreach (var id in _registry.GetAllIds(WorldObjectCategory.Flag))
|
foreach (var id in _registry.GetAllIds(WorldObjectCategory.Flag))
|
||||||
data.EventChains.WorldFlags[id] = true;
|
data.EventChains.WorldFlags[id] = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user