存档完善和修复

This commit is contained in:
2026-05-20 15:10:35 +08:00
parent 84528403ec
commit ec633d9b79
17 changed files with 254 additions and 67 deletions

View File

@@ -60,7 +60,7 @@ namespace BaseGames.Tests.EditMode
}
[Test]
public void Migrate_OldVersion_FillsPlayerDeathShade()
public void Migrate_OldVersion_DeathShade_RemainsNull()
{
var data = new SaveData();
data.Meta.Version = "1.0";
@@ -68,7 +68,8 @@ namespace BaseGames.Tests.EditMode
var result = SaveMigrator.Migrate(data);
Assert.IsNotNull(result.Player.DeathShade, "迁移后 DeathShade 不应为 null");
Assert.IsNull(result.Player.DeathShade,
"无遗骸时 DeathShade 应保持 null由 DeathShadeManager 按需创建");
}
[Test]
@@ -99,22 +100,18 @@ namespace BaseGames.Tests.EditMode
public void SaveData_SerializeDeserialize_PreservesPlayerData()
{
var original = new SaveData();
original.Player.CurrentHP = 55;
original.Player.MaxHP = 100;
original.Player.CurrentHP = 55;
original.Player.MaxHP = 100;
original.Player.CurrentLingZhu = 1234;
original.Player.Scene = "TestRoom";
original.Player.PosX = 3.14f;
original.Player.PosY = -2.71f;
original.Player.Scene = "TestRoom";
string json = JsonConvert.SerializeObject(original, Formatting.None);
var restored = JsonConvert.DeserializeObject<SaveData>(json);
Assert.AreEqual(original.Player.CurrentHP, restored.Player.CurrentHP);
Assert.AreEqual(original.Player.MaxHP, restored.Player.MaxHP);
Assert.AreEqual(original.Player.CurrentHP, restored.Player.CurrentHP);
Assert.AreEqual(original.Player.MaxHP, restored.Player.MaxHP);
Assert.AreEqual(original.Player.CurrentLingZhu, restored.Player.CurrentLingZhu);
Assert.AreEqual(original.Player.Scene, restored.Player.Scene);
Assert.AreEqual(original.Player.PosX, restored.Player.PosX, 0.0001f);
Assert.AreEqual(original.Player.PosY, restored.Player.PosY, 0.0001f);
Assert.AreEqual(original.Player.Scene, restored.Player.Scene);
}
[Test]

View File

@@ -71,7 +71,12 @@ namespace BaseGames.Core
_onQuestStateChanged? .Subscribe(OnQuestStateChanged) .AddTo(_subs);
}
private void OnDisable() => _subs.Clear();
private void OnDisable()
{
StopAllCoroutines();
_onCooldown = false;
_subs.Clear();
}
// ── 触发处理 ───────────────────────────────────────────────────────────

View File

@@ -66,7 +66,10 @@ namespace BaseGames.Core
Debug.LogWarning("[GameServiceRegistrar] ⚠ _eventChannelRegistry 未绑定IEventChannelRegistry 未注册。", this);
if (_saveManager)
{
_saveManager.Initialize(new BaseGames.Core.Save.LocalFileStorage());
ServiceLocator.Register<ISaveService>(new SaveServiceAdapter(_saveManager));
}
else
Debug.LogWarning("[GameServiceRegistrar] ⚠ _saveManager 未绑定ISaveService 未注册。", this);
@@ -228,5 +231,6 @@ namespace BaseGames.Core
public void SetFlag(string flagId, bool value) => _save.SetFlag(flagId, value);
public System.Collections.Generic.IEnumerable<string> GetCompletedChains() => _save.GetCompletedChains();
public void SetChainCompleted(string chainId) => _save.SetChainCompleted(chainId);
public void MarkBossDefeated(string bossId) => _save.MarkBossDefeated(bossId);
}
}

View File

@@ -0,0 +1,12 @@
namespace BaseGames.Core
{
/// <summary>
/// 提供当前玩家灵珠数量的只读接口。
/// 置于 BaseGames.Core 程序集,使 World 程序集中的组件(如 DeathShadeManager
/// 无需直接引用 BaseGames.Player 程序集即可读取灵珠值。
/// </summary>
public interface ILingZhuProvider
{
int CurrentLingZhu { get; }
}
}

View File

@@ -73,5 +73,8 @@ namespace BaseGames.Core
/// <summary>标记某条事件链已完成。</summary>
void SetChainCompleted(string chainId);
/// <summary>标记某个 Boss 已被击败,立即写入当前 SaveData无需等待下次存档。</summary>
void MarkBossDefeated(string bossId);
}
}

View File

@@ -28,7 +28,7 @@ namespace BaseGames.Core.Save
private void Update()
{
if (!_gameplayActive || _saveManager == null) return;
_timer += Time.deltaTime;
_timer += Time.unscaledDeltaTime;
if (_timer >= _intervalSeconds)
{
_timer = 0f;

View File

@@ -39,8 +39,15 @@ namespace BaseGames.Core.Save
if (_instance != null) { Destroy(gameObject); return; }
_instance = this;
ServiceLocator.Register<ISaveableRegistry>(this);
}
_storage = new LocalFileStorage();
/// <summary>
/// 注入存储后端。由 GameServiceRegistrar.Awake 在注册服务后立即调用。
/// 允许在测试环境替换为 InMemoryStorage 等替代实现。
/// </summary>
public void Initialize(ISaveStorage storage)
{
_storage = storage;
}
// ── ISaveable 注册 ────────────────────────────────────────────────────
@@ -105,8 +112,12 @@ namespace BaseGames.Core.Save
if (!ValidateChecksum(loaded, json))
{
if (loaded.Meta.IsSteelSoul)
{
Debug.LogError("[SaveManager] 钢铁之魂存档 checksum 校验失败,拒绝加载以防止作弊。");
return false;
}
Debug.LogWarning("[SaveManager] 存档 checksum 校验失败。");
// 非 SteelSoul 模式仍允许加载(只是警告)
}
loaded = SaveMigrator.Migrate(loaded);
@@ -179,6 +190,13 @@ namespace BaseGames.Core.Save
public bool IsBossDefeated(string bossId)
=> _current?.World.DefeatedBossIds.Contains(bossId) == true;
public void MarkBossDefeated(string bossId)
{
_current ??= new SaveData();
if (!_current.World.DefeatedBossIds.Contains(bossId))
_current.World.DefeatedBossIds.Add(bossId);
}
// ── 存档槽摘要 ────────────────────────────────────────────────────────
public async Task<SlotSummary> GetSlotSummaryAsync(int slotIndex)
{

View File

@@ -50,7 +50,6 @@ namespace BaseGames.Core.Save
[Serializable]
public class PlayerSaveData
{
public float PosX, PosY;
public string Scene;
public int CurrentHP, MaxHP;
public int CurrentLingZhu, LifetimeLingZhu;
@@ -97,7 +96,6 @@ namespace BaseGames.Core.Save
public List<string> DefeatedBossIds = new();
public List<string> CollectedIds = new();
public List<string> DestroyedObjectIds = new();
public Dictionary<string, bool> Switches = new();
public Dictionary<string, int> NpcRelations = new();
public HashSet<string> ChallengeFirstClears = new();
}
@@ -106,9 +104,9 @@ namespace BaseGames.Core.Save
[Serializable]
public class MapSaveData
{
public List<string> ExploredRooms = new(); // 踏入过的房间 ID
public List<string> MappedRooms = new(); // 完整地图信息(购买/存档点揭示)
public List<MapPin> Pins = new(); // 玩家自定义地图标记
public HashSet<string> ExploredRooms = new(); // 踏入过的房间 ID
public HashSet<string> MappedRooms = new(); // 完整地图信息(购买/存档点揭示)
public List<MapPin> Pins = new(); // 玩家自定义地图标记
}
/// <summary>玩家在地图上放置的自定义标记(架构 15_MapShopModule §1.5)。</summary>
@@ -171,7 +169,6 @@ namespace BaseGames.Core.Save
public int LingZhuEarned, LingZhuLost;
public float DistanceTraveled;
public float SpeedrunTime;
public int SaveCount;
public Dictionary<string, int> SkillUseCounts = new();
public Dictionary<string, int> DeathsByBoss = new();
}

View File

@@ -26,10 +26,6 @@ namespace BaseGames.Core.Save
data.ChallengeRooms ??= new ChallengeRoomsSaveData();
data.NGPlus = null; // 非 NG+ 模式
// Player 子字段兼容(旧版本可能未记录 DeathShade
if (data.Player != null)
data.Player.DeathShade ??= new DeathShadeSaveData();
Debug.Log($"[SaveMigrator] 从 '{v}' 迁移至 '2.0'。");
v = "2.0";
}

View File

@@ -10,7 +10,7 @@ namespace BaseGames.Player
/// 玩家数值管理组件。负责 HP、灵魂、灵气、弹簧充能、LingZhu、能力解锁与存档读写。
/// 实现 <see cref="IRewardTarget"/> 供 RewardSO 使用,避免 Quest 程序集直接依赖 Player 程序集。
/// </summary>
public class PlayerStats : MonoBehaviour, ISaveable, BaseGames.Core.IRestoreOnSave, IRewardTarget
public class PlayerStats : MonoBehaviour, ISaveable, BaseGames.Core.IRestoreOnSave, IRewardTarget, BaseGames.Core.ILingZhuProvider
{
[Header("配置")]
[SerializeField] private PlayerStatsSO _config;
@@ -36,6 +36,7 @@ namespace BaseGames.Player
public int MaxSpringCharges { get; private set; }
public int SpringKillPoints { get; private set; }
public int CurrentLingZhu { get; private set; }
private int _lifetimeLingZhu;
public bool IsInvincible => _invincibleTimer > 0f;
public bool IsAlive => CurrentHP > 0;
@@ -277,7 +278,8 @@ namespace BaseGames.Player
public void AddLingZhu(int amount)
{
if (amount <= 0) return;
CurrentLingZhu += amount;
CurrentLingZhu += amount;
_lifetimeLingZhu += amount;
_onLingZhuChanged?.Raise(CurrentLingZhu);
}
@@ -320,6 +322,7 @@ namespace BaseGames.Player
p.CurrentHP = CurrentHP;
p.MaxHP = MaxHP;
p.CurrentLingZhu = CurrentLingZhu;
p.LifetimeLingZhu = _lifetimeLingZhu;
p.AbilityFlags = (uint)_unlockedAbilities;
}
@@ -329,6 +332,7 @@ namespace BaseGames.Player
MaxHP = p.MaxHP;
CurrentHP = Mathf.Clamp(p.CurrentHP, 0, MaxHP);
CurrentLingZhu = p.CurrentLingZhu;
_lifetimeLingZhu = p.LifetimeLingZhu;
_unlockedAbilities = (AbilityType)p.AbilityFlags;
_onHPChanged?.Raise(CurrentHP);

View File

@@ -4,19 +4,19 @@ using BaseGames.Core.Save;
namespace BaseGames.Progression
{
/// <summary>
/// 由事件触发的成就条件(使用 World.Switches 中的布尔标志位)。
/// 由事件触发的成就条件(使用 EventChains.WorldFlags 中的布尔标志位)。
/// 由代码如剧情节点、NPC 交互写入标志AchievementManager 轮询检查。
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/Achievement/Condition/EventTriggered", fileName = "COND_EventTriggered_")]
public class EventTriggeredCondition : AchievementCondition
{
[Tooltip("World.Switches 中用于标记此事件发生的布尔 Key")]
[Tooltip("EventChains.WorldFlags 中用于标记此事件发生的布尔 Key")]
public string flagKey;
public override bool IsMet(SaveData save)
{
if (save?.World?.Switches == null || string.IsNullOrEmpty(flagKey)) return false;
return save.World.Switches.TryGetValue(flagKey, out bool val) && val;
if (save?.EventChains?.WorldFlags == null || string.IsNullOrEmpty(flagKey)) return false;
return save.EventChains.WorldFlags.TryGetValue(flagKey, out bool val) && val;
}
}
}

View File

@@ -5,7 +5,7 @@ namespace BaseGames.Progression
{
/// <summary>
/// 无治疗通关成就条件。
/// 依赖 World.Switches 中的标志位AchievementManager 在玩家治疗时写入 noHeal_failed=true。
/// 依赖 EventChains.WorldFlags 中的标志位AchievementManager 在玩家治疗时写入 noHeal_failed=true。
/// 满足条件Boss 已击败 且 无治疗标志未被置入失败状态。
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/Achievement/Condition/NoHealRun", fileName = "COND_NoHealRun_")]
@@ -14,7 +14,7 @@ namespace BaseGames.Progression
[Tooltip("追踪的 Boss ID击败此 Boss 时检查是否治疗过)")]
public string targetBossId;
[Tooltip("Switches 中记录「已治疗」失败状态的 Key由 AchievementManager 设置)")]
[Tooltip("WorldFlags 中记录「已治疗」失败状态的 Key由 AchievementManager 设置)")]
public string healFailFlagKey;
public override bool IsMet(SaveData save)
@@ -22,7 +22,7 @@ namespace BaseGames.Progression
if (save?.World == null) return false;
// Boss 已击败 且 未触发治疗失败标志
bool bossDefeated = save.World.DefeatedBossIds.Contains(targetBossId);
bool didHeal = save.World.Switches.TryGetValue(healFailFlagKey, out bool val) && val;
bool didHeal = save.EventChains?.WorldFlags?.TryGetValue(healFailFlagKey, out bool val) == true && val;
return bossDefeated && !didHeal;
}
}

View File

@@ -1,4 +1,5 @@
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
namespace BaseGames.Progression
@@ -6,7 +7,7 @@ namespace BaseGames.Progression
/// <summary>
/// Boss 进程追踪器(架构 09_ProgressionModule §13
/// 挂载在 Boss 房间的 BossTrigger 同一对象上。
/// 监听 _onBossDefeated 事件,路由到 SaveSystem 专用频道(零耦合)
/// 监听 _onBossDefeated 事件,立即将胜利写入 SaveData 并推送到全局 ISaveService
/// </summary>
public class BossProgressTracker : MonoBehaviour
{
@@ -14,8 +15,7 @@ namespace BaseGames.Progression
[SerializeField] private string[] _unlocksProgressLockIds; // 击败后应解锁的 ProgressLock ID预留
[Header("Event Channels")]
[SerializeField] private StringEventChannelSO _onBossDefeated; // 监听(来自 BossCombat
[SerializeField] private StringEventChannelSO _onBossDefeatedForSave; // 广播 → SaveSystem
[SerializeField] private StringEventChannelSO _onBossDefeated; // 监听(来自 BossCombat
private readonly CompositeDisposable _subs = new();
@@ -25,9 +25,7 @@ namespace BaseGames.Progression
private void OnBossDefeated(string bossId)
{
if (bossId != _bossId) return;
// 通过事件频道通知 SaveSystemSaveSystem 负责写入 data.World.DefeatedBossIds
_onBossDefeatedForSave?.Raise(bossId);
ServiceLocator.GetOrDefault<ISaveService>()?.MarkBossDefeated(bossId);
}
}
}

View File

@@ -13,9 +13,9 @@ namespace BaseGames.Support.AntiSoftlock
public class HardAbilityGate : AbilityGate
{
[Header("HardAbilityGate 设置")]
[Tooltip("是否还要求物理拾取验证(需要 World.Switches 中对应 Key = true")]
[Tooltip("是否还要求物理拾取验证(需要 EventChains.WorldFlags 中对应 Key = true")]
[SerializeField] private bool _requirePhysicalValidation = false;
[Tooltip("物理拾取验证 Switch KeySaveManager.Data.World.Switches[key] == true")]
[Tooltip("物理拾取验证 Flag KeyEventChains.WorldFlags[key] == true")]
[SerializeField] private string _physicalPickupSwitchKey;
[Header("引用")]
@@ -29,8 +29,8 @@ namespace BaseGames.Support.AntiSoftlock
// 次级检查:物理拾取确认
if (string.IsNullOrEmpty(_physicalPickupSwitchKey)) return true;
var save = _saveManager != null ? _saveManager.Data : null;
if (save?.World?.Switches == null) return false;
return save.World.Switches.TryGetValue(_physicalPickupSwitchKey, out bool val) && val;
if (save?.EventChains?.WorldFlags == null) return false;
return save.EventChains.WorldFlags.TryGetValue(_physicalPickupSwitchKey, out bool val) && val;
}
}
}

View File

@@ -0,0 +1,113 @@
using UnityEngine;
using UnityEngine.SceneManagement;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
namespace BaseGames.World
{
/// <summary>
/// 管理死亡遗骸Death Shade的生命周期与持久化。
///
/// 挂载位置Persistent 场景的根 GameObjectDontDestroyOnLoad
///
/// 流程:
/// 玩家死亡 → OnPlayerDied记录死亡位置与灵珠量在当前场景生成遗骸。
/// 切换场景 → OnSceneLoaded若新场景即遗骸所在场景自动生成遗骸。
/// 回收遗骸 → OnShadeCollected清除待存储的遗骸数据。
/// 存档 → OnSave将遗骸数据null = 无遗骸)写入 SaveData.Player.DeathShade。
/// 读档 → OnLoad从 SaveData 恢复遗骸数据;实际 GameObject 在下次进入该场景时生成。
/// </summary>
public class DeathShadeManager : SaveableMonoBehaviour
{
[SerializeField] private DeathShade _shadePrefab;
[SerializeField] private VoidEventChannelSO _onPlayerDied;
[SerializeField] private StringEventChannelSO _onShadeCollected;
private DeathShadeSaveData _pendingSave;
private DeathShade _activeShade;
private readonly CompositeDisposable _subs = new();
protected override void OnEnable()
{
base.OnEnable();
_onPlayerDied? .Subscribe(OnPlayerDied) .AddTo(_subs);
_onShadeCollected?.Subscribe(OnShadeCollected).AddTo(_subs);
SceneManager.sceneLoaded += OnSceneLoaded;
}
protected override void OnDisable()
{
base.OnDisable();
_subs.Clear();
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void OnPlayerDied()
{
var playerGo = GameObject.FindWithTag("Player");
if (playerGo == null) return;
int lingZhu = playerGo.GetComponent<ILingZhuProvider>()?.CurrentLingZhu ?? 0;
var pos = (Vector2)playerGo.transform.position;
string sceneId = SceneManager.GetActiveScene().name;
if (_activeShade != null)
{
Destroy(_activeShade.gameObject);
_activeShade = null;
}
_pendingSave = new DeathShadeSaveData
{
PosX = pos.x,
PosY = pos.y,
SceneId = sceneId,
LingZhuAmount = lingZhu,
};
TrySpawnShade(sceneId);
}
private void OnSceneLoaded(Scene scene, LoadSceneMode _)
{
_activeShade = null; // 前一场景的 GameObject 已被 Unity 卸载
TrySpawnShade(scene.name);
}
private void TrySpawnShade(string sceneName)
{
if (_shadePrefab == null || _pendingSave == null) return;
if (_pendingSave.SceneId != sceneName) return;
_activeShade = Instantiate(_shadePrefab);
_activeShade.Initialize(
_pendingSave.LingZhuAmount,
_pendingSave.SceneId,
new Vector2(_pendingSave.PosX, _pendingSave.PosY));
}
private void OnShadeCollected(string _)
{
_pendingSave = null;
_activeShade = null;
}
public override void OnSave(SaveData data)
{
data.Player.DeathShade = _pendingSave;
}
public override void OnLoad(SaveData data)
{
if (_activeShade != null)
{
Destroy(_activeShade.gameObject);
_activeShade = null;
}
_pendingSave = data.Player.DeathShade;
// 遗骸 GameObject 将在 OnSceneLoaded 中按需生成
}
}
}

View File

@@ -17,8 +17,9 @@ namespace BaseGames.World
/// <summary>
/// 运行时世界状态缓存。ScriptableObject通过 [SerializeField] 注入各组件。
/// SaveManager.SaveAsync 调用 GetAllFlags()
/// SaveManager.LoadAsync 调用 LoadFromSave(data.World)。
/// WorldStateRegistrySaverISaveable负责将本对象与存档管道连接
/// SaveAsync → WorldStateRegistrySaver.OnSave → 写出全部分类到 SaveData
/// LoadAsync → WorldStateRegistrySaver.OnLoad → 调用 LoadFromSave(data) 恢复缓存。
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/World/WorldStateRegistry")]
public class WorldStateRegistry : ScriptableObject
@@ -77,37 +78,34 @@ namespace BaseGames.World
// ── Persistence ───────────────────────────────────────────────────────
/// <summary>从存档数据恢复全部状态。由 SaveManager.LoadAsync 调用。</summary>
public void LoadFromSave(WorldSaveData data)
/// <summary>
/// 从存档数据恢复全部状态。由 WorldStateRegistrySaver.OnLoad 调用。
/// </summary>
public void LoadFromSave(SaveData data)
{
_states.Clear();
if (data == null) return;
foreach (var id in data.CollectedIds) Mark(WorldObjectCategory.Collectible, id);
foreach (var id in data.ActivatedSavePoints) Mark(WorldObjectCategory.SavePoint, id);
foreach (var id in data.OpenedDoors) Mark(WorldObjectCategory.Door, id);
foreach (var id in data.DestroyedObjectIds) Mark(WorldObjectCategory.Destroyed, id);
foreach (var id in data.World.CollectedIds) Mark(WorldObjectCategory.Collectible, id);
foreach (var id in data.World.ActivatedSavePoints) Mark(WorldObjectCategory.SavePoint, id);
foreach (var id in data.World.OpenedDoors) Mark(WorldObjectCategory.Door, id);
foreach (var id in data.World.DestroyedObjectIds) Mark(WorldObjectCategory.Destroyed, id);
if (data.Switches != null)
foreach (var kv in data.Switches)
if (data.EventChains?.WorldFlags != null)
foreach (var kv in data.EventChains.WorldFlags)
if (kv.Value) Mark(WorldObjectCategory.Flag, kv.Key);
}
/// <summary>获取所有通用标记,供 SaveManager 持久化。</summary>
public HashSet<string> GetAllFlags()
/// <summary>
/// 返回指定分类中所有已标记的 ID快照副本
/// 由 WorldStateRegistrySaver.OnSave 调用,将运行时状态写入 SaveData。
/// </summary>
public IReadOnlyCollection<string> GetAllIds(WorldObjectCategory category)
{
if (_states.TryGetValue(WorldObjectCategory.Flag, out var flags))
return new HashSet<string>(flags);
return new HashSet<string>();
}
/// <summary>获取所有已摧毁对象 ID供 SaveManager 持久化。</summary>
public HashSet<string> GetAllDestroyedIds()
{
if (_states.TryGetValue(WorldObjectCategory.Destroyed, out var set))
if (_states.TryGetValue(category, out var set))
return new HashSet<string>(set);
return new HashSet<string>();
return System.Array.Empty<string>();
}
/// <summary>重置所有状态(开始新游戏时调用)。</summary>

View File

@@ -0,0 +1,42 @@
using BaseGames.Core.Save;
namespace BaseGames.World
{
/// <summary>
/// 将 WorldStateRegistry 接入 ISaveable 存档管道的桥接组件。
///
/// 挂载位置Persistent 场景的根 GameObject与 GameServiceRegistrar 同级)。
/// 需在 Inspector 中绑定场景中唯一的 WorldStateRegistry ScriptableObject 资源。
///
/// OnSave从 Registry 读取 Collectible / Destroyed / Flag 三个分类,写入 SaveData。
/// SavePoint 和 Door 两个分类由各自的 SaveableMonoBehaviour 独立写入,此处跳过。
/// OnLoad将完整的 SaveData 传递给 Registry恢复所有分类的运行时缓存。
/// 这是 Registry 从空白变为"知道世界状态"的唯一时机。
/// </summary>
public class WorldStateRegistrySaver : SaveableMonoBehaviour
{
[UnityEngine.SerializeField] private WorldStateRegistry _registry;
public override void OnSave(SaveData data)
{
if (_registry == null) return;
data.World.CollectedIds.Clear();
foreach (var id in _registry.GetAllIds(WorldObjectCategory.Collectible))
data.World.CollectedIds.Add(id);
data.World.DestroyedObjectIds.Clear();
foreach (var id in _registry.GetAllIds(WorldObjectCategory.Destroyed))
data.World.DestroyedObjectIds.Add(id);
foreach (var id in _registry.GetAllIds(WorldObjectCategory.Flag))
data.EventChains.WorldFlags[id] = true;
}
public override void OnLoad(SaveData data)
{
if (_registry == null) return;
_registry.LoadFromSave(data);
}
}
}