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