From bc7063fb951234dbd4ea64b253ff729b687f6afb Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 20 May 2026 18:18:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=AD=98=E6=A1=A3?= =?UTF-8?q?=E7=AE=A1=E7=90=86=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=A7=BD=E4=BD=8D=E6=91=98=E8=A6=81=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=87=8F=E5=B0=91=E4=B8=BB=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E7=AD=89=E5=BE=85=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/_Game/Scripts/Core/Save/GameSaveManager.cs | 13 ++++++++----- Assets/_Game/Scripts/Core/Save/LocalFileStorage.cs | 8 ++++++-- Assets/_Game/Scripts/Core/Save/SaveData.cs | 2 +- Assets/_Game/Scripts/Equipment/ToolSlotManager.cs | 9 +++++++++ Assets/_Game/Scripts/Player/FormController.cs | 4 +++- .../_Game/Scripts/Progression/AchievementManager.cs | 3 +++ Assets/_Game/Scripts/UI/Menus/SaveSlotController.cs | 11 +++++++++-- 7 files changed, 39 insertions(+), 11 deletions(-) diff --git a/Assets/_Game/Scripts/Core/Save/GameSaveManager.cs b/Assets/_Game/Scripts/Core/Save/GameSaveManager.cs index 80dc7f1..744745c 100644 --- a/Assets/_Game/Scripts/Core/Save/GameSaveManager.cs +++ b/Assets/_Game/Scripts/Core/Save/GameSaveManager.cs @@ -221,6 +221,9 @@ namespace BaseGames.Core.Save } // ── 存档槽摘要 ──────────────────────────────────────────────────────── + /// + /// 仅解析 Meta 和 Player 两个顶层字段,避免反序列化整个 SaveData 造成不必要的 GC 压力。 + /// public async Task GetSlotSummaryAsync(int slotIndex) { if (!_storage.Exists(slotIndex)) return null; @@ -228,14 +231,14 @@ namespace BaseGames.Core.Save if (json == null) return null; try { - var data = JsonConvert.DeserializeObject(json); + var root = Newtonsoft.Json.Linq.JObject.Parse(json); return new SlotSummary { SlotIndex = slotIndex, - Playtime = data.Meta.Playtime, - LastSaved = data.Meta.LastSaved, - SceneName = data.Player?.Scene, - ActiveFormId = data.Player?.ActiveFormId, + Playtime = root["Meta"]?["Playtime"]?.Value() ?? 0f, + LastSaved = root["Meta"]?["LastSaved"]?.Value(), + SceneName = root["Player"]?["Scene"]?.Value(), + ActiveFormId = root["Player"]?["ActiveFormId"]?.Value(), }; } catch { return null; } diff --git a/Assets/_Game/Scripts/Core/Save/LocalFileStorage.cs b/Assets/_Game/Scripts/Core/Save/LocalFileStorage.cs index b23bd44..6e41415 100644 --- a/Assets/_Game/Scripts/Core/Save/LocalFileStorage.cs +++ b/Assets/_Game/Scripts/Core/Save/LocalFileStorage.cs @@ -25,10 +25,14 @@ namespace BaseGames.Core.Save public async Task WriteAsync(int slotIndex, string json) { var path = GetPath(slotIndex); - // 先写临时文件再原子性替换,防止写入中途断电损坏存档 + // 先写临时文件再原子性替换,防止写入中途断电损坏存档。 + // File.Replace 要求 destination 必须已存在;首次存档时直接 Move。 var tmp = path + ".tmp"; await File.WriteAllTextAsync(tmp, json); - File.Replace(tmp, path, path + ".bak", ignoreMetadataErrors: true); + if (File.Exists(path)) + File.Replace(tmp, path, path + ".bak", ignoreMetadataErrors: true); + else + File.Move(tmp, path); } public async Task ReadAsync(int slotIndex) diff --git a/Assets/_Game/Scripts/Core/Save/SaveData.cs b/Assets/_Game/Scripts/Core/Save/SaveData.cs index 2e2cf69..b23b0fb 100644 --- a/Assets/_Game/Scripts/Core/Save/SaveData.cs +++ b/Assets/_Game/Scripts/Core/Save/SaveData.cs @@ -34,7 +34,7 @@ namespace BaseGames.Core.Save [Serializable] public class SaveMeta { - public string Version = "2.2"; + public string Version = SaveMigrator.CurrentVersion; public int SlotIndex; public string LastSaved; // ISO 8601 public float Playtime; diff --git a/Assets/_Game/Scripts/Equipment/ToolSlotManager.cs b/Assets/_Game/Scripts/Equipment/ToolSlotManager.cs index a55aaf8..9bd2ea3 100644 --- a/Assets/_Game/Scripts/Equipment/ToolSlotManager.cs +++ b/Assets/_Game/Scripts/Equipment/ToolSlotManager.cs @@ -82,6 +82,9 @@ namespace BaseGames.Equipment { data.Tools.ToolSlot0 = _slots[0]?.toolId; data.Tools.ToolSlot1 = _slots[1]?.toolId; + // 持久化剩余使用次数(-1 = 无限,保持原值) + data.Tools.ToolStates["Slot0_Uses"] = Newtonsoft.Json.Linq.JObject.FromObject(new { uses = _remainingUses[0] }); + data.Tools.ToolStates["Slot1_Uses"] = Newtonsoft.Json.Linq.JObject.FromObject(new { uses = _remainingUses[1] }); } public void OnLoad(SaveData data) @@ -90,6 +93,12 @@ namespace BaseGames.Equipment EquipTool(0, _toolCatalog.Find(data.Tools.ToolSlot0)); EquipTool(1, _toolCatalog.Find(data.Tools.ToolSlot1)); + + // 恢复剩余使用次数(EquipTool 会重置为 maxUses,此处覆盖还原) + if (data.Tools.ToolStates.TryGetValue("Slot0_Uses", out var uses0)) + _remainingUses[0] = uses0["uses"]?.Value() ?? _remainingUses[0]; + if (data.Tools.ToolStates.TryGetValue("Slot1_Uses", out var uses1)) + _remainingUses[1] = uses1["uses"]?.Value() ?? _remainingUses[1]; } } } diff --git a/Assets/_Game/Scripts/Player/FormController.cs b/Assets/_Game/Scripts/Player/FormController.cs index b95dbdd..fd1a55a 100644 --- a/Assets/_Game/Scripts/Player/FormController.cs +++ b/Assets/_Game/Scripts/Player/FormController.cs @@ -109,7 +109,9 @@ namespace BaseGames.Player { if (_config.forms[i]?.formId == data.Player.ActiveFormId) { - SwitchToFormByIndex(i); + // 直接赋值,不触发事件——加载时场景尚未完全就绪,订阅者(WeaponManager 等) + // 会在各自 OnEnable + Register → OnLoad 回调中自行恢复状态。 + CurrentForm = _config.forms[i]; return; } } diff --git a/Assets/_Game/Scripts/Progression/AchievementManager.cs b/Assets/_Game/Scripts/Progression/AchievementManager.cs index 0d3313f..0d379ec 100644 --- a/Assets/_Game/Scripts/Progression/AchievementManager.cs +++ b/Assets/_Game/Scripts/Progression/AchievementManager.cs @@ -97,6 +97,9 @@ namespace BaseGames.Progression if (_states.TryGetValue(kv.Key, out var state)) state.Progress = kv.Value.Percent; } + + // 加载后立即重新评估所有未解锁成就,避免离线满足条件的成就须等到下次游戏事件才触发。 + EvaluateAll(saveData); } // ── 轮询检查 ────────────────────────────────────────────────────────── diff --git a/Assets/_Game/Scripts/UI/Menus/SaveSlotController.cs b/Assets/_Game/Scripts/UI/Menus/SaveSlotController.cs index 370a355..51d2bf5 100644 --- a/Assets/_Game/Scripts/UI/Menus/SaveSlotController.cs +++ b/Assets/_Game/Scripts/UI/Menus/SaveSlotController.cs @@ -33,11 +33,18 @@ namespace BaseGames.UI.Menus { var svc = ServiceLocator.GetOrDefault(); if (svc == null) return; + + // 并行加载所有槽位摘要,减少主菜单等待时间 + var tasks = new Task[_slotUIs.Length]; + for (int i = 0; i < _slotUIs.Length; i++) + tasks[i] = svc.GetSlotSummaryAsync(i); + + var summaries = await Task.WhenAll(tasks); + for (int i = 0; i < _slotUIs.Length; i++) { if (_slotUIs[i] == null) continue; - var summary = await svc.GetSlotSummaryAsync(i); - _slotUIs[i].Refresh(summary); + _slotUIs[i].Refresh(summaries[i]); } }