mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-10 19:56:45 +08:00
feat(game): 添加数据与存档系统核心功能
- 实现 ISaveRepository<T> 接口提供存档管理功能 - 添加 SaveRepository<T> 实现类支持槽位存档管理 - 实现数据版本迁移机制支持存档版本升级 - 添加完整的存档测试用例验证功能正确性 - 创建数据与存档系统中文文档说明使用方法 - 移除项目中不再需要的本地计划文件夹配置
This commit is contained in:
parent
397611d47c
commit
7ad80f54d3
44
GFramework.Game.Abstractions/Data/ISaveMigration.cs
Normal file
44
GFramework.Game.Abstractions/Data/ISaveMigration.cs
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2026 GeWuYou
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace GFramework.Game.Abstractions.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 定义存档数据迁移接口,用于将旧版本存档升级到较新的版本。
|
||||
/// </summary>
|
||||
/// <typeparam name="TSaveData">
|
||||
/// 存档数据类型。该类型通常需要实现 <see cref="IVersionedData" />,
|
||||
/// 以便仓库在加载时判断当前版本并串联迁移链。
|
||||
/// </typeparam>
|
||||
public interface ISaveMigration<TSaveData>
|
||||
where TSaveData : class, IData
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取迁移前的版本号。
|
||||
/// </summary>
|
||||
int FromVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取迁移后的目标版本号。
|
||||
/// </summary>
|
||||
int ToVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 将旧版本存档转换为新版本存档。
|
||||
/// </summary>
|
||||
/// <param name="oldData">待升级的旧版本存档数据。</param>
|
||||
/// <returns>迁移完成后的存档数据。</returns>
|
||||
TSaveData Migrate(TSaveData oldData);
|
||||
}
|
||||
@ -11,6 +11,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
|
||||
namespace GFramework.Game.Abstractions.Data;
|
||||
@ -22,6 +25,20 @@ namespace GFramework.Game.Abstractions.Data;
|
||||
public interface ISaveRepository<TSaveData> : IUtility
|
||||
where TSaveData : class, IData, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册存档迁移器。
|
||||
/// </summary>
|
||||
/// <param name="migration">
|
||||
/// 负责将某个旧版本存档升级到新版本的迁移器。
|
||||
/// 仅当 <typeparamref name="TSaveData" /> 实现 <see cref="IVersionedData" /> 时该功能才有效。
|
||||
/// </param>
|
||||
/// <returns>当前存档仓库实例,便于链式注册多个迁移器。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="migration" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// <typeparamref name="TSaveData" /> 未实现 <see cref="IVersionedData" />,因此无法建立版本迁移管线。
|
||||
/// </exception>
|
||||
ISaveRepository<TSaveData> RegisterMigration(ISaveMigration<TSaveData> migration);
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定槽位是否存在存档
|
||||
/// </summary>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
using GFramework.Game.Abstractions.Enums;
|
||||
|
||||
@ -14,7 +15,20 @@ internal sealed class TestSaveData : IData
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
internal sealed class TestVersionedSaveData : IVersionedData
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public int Level { get; set; }
|
||||
|
||||
public int Experience { get; set; }
|
||||
|
||||
public int Version { get; set; } = 3;
|
||||
|
||||
public DateTime LastModified { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
internal sealed class TestSimpleData : IData
|
||||
{
|
||||
public int Value { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +59,84 @@ public class PersistenceTests
|
||||
Assert.That(await repository.ExistsAsync(1), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveRepository_LoadAsync_Should_Apply_Migrations_And_Persist_Upgraded_Save()
|
||||
{
|
||||
var root = CreateTempRoot();
|
||||
using var storage = new FileStorage(root, new JsonSerializer());
|
||||
var config = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save"
|
||||
};
|
||||
|
||||
var writer = new SaveRepository<TestVersionedSaveData>(storage, config);
|
||||
await writer.SaveAsync(1, new TestVersionedSaveData
|
||||
{
|
||||
Name = "hero",
|
||||
Level = 5,
|
||||
Experience = 0,
|
||||
Version = 1
|
||||
});
|
||||
|
||||
var repository = new SaveRepository<TestVersionedSaveData>(storage, config)
|
||||
.RegisterMigration(new TestSaveMigrationV1ToV2())
|
||||
.RegisterMigration(new TestSaveMigrationV2ToV3());
|
||||
|
||||
var loaded = await repository.LoadAsync(1);
|
||||
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(loaded.Version, Is.EqualTo(3));
|
||||
Assert.That(loaded.Experience, Is.EqualTo(500));
|
||||
Assert.That(loaded.Name, Is.EqualTo("hero-v2"));
|
||||
Assert.That(persisted.Version, Is.EqualTo(3));
|
||||
Assert.That(persisted.Experience, Is.EqualTo(500));
|
||||
Assert.That(persisted.Name, Is.EqualTo("hero-v2"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveRepository_RegisterMigration_For_NonVersioned_Save_Should_Throw()
|
||||
{
|
||||
var root = CreateTempRoot();
|
||||
using var storage = new FileStorage(root, new JsonSerializer());
|
||||
var config = new SaveConfiguration();
|
||||
var repository = new SaveRepository<TestSaveData>(storage, config);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => repository.RegisterMigration(new TestNonVersionedMigration()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveRepository_LoadAsync_Should_Throw_When_Migration_Chain_Is_Incomplete()
|
||||
{
|
||||
var root = CreateTempRoot();
|
||||
using var storage = new FileStorage(root, new JsonSerializer());
|
||||
var config = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save"
|
||||
};
|
||||
|
||||
var writer = new SaveRepository<TestVersionedSaveData>(storage, config);
|
||||
await writer.SaveAsync(1, new TestVersionedSaveData
|
||||
{
|
||||
Name = "legacy",
|
||||
Level = 3,
|
||||
Experience = 0,
|
||||
Version = 1
|
||||
});
|
||||
|
||||
var repository = new SaveRepository<TestVersionedSaveData>(storage, config)
|
||||
.RegisterMigration(new TestSaveMigrationV1ToV2());
|
||||
|
||||
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.LoadAsync(1));
|
||||
Assert.That(exception!.Message, Does.Contain("from version 2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UnifiedSettingsDataRepository_RoundTripsDataAndLoadAll()
|
||||
{
|
||||
@ -92,4 +170,57 @@ public class PersistenceTests
|
||||
Assert.That(all.Keys, Contains.Item(location.Key));
|
||||
Assert.That(all[location.Key], Is.TypeOf<TestSimpleData>());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSaveMigrationV1ToV2 : ISaveMigration<TestVersionedSaveData>
|
||||
{
|
||||
public int FromVersion => 1;
|
||||
|
||||
public int ToVersion => 2;
|
||||
|
||||
public TestVersionedSaveData Migrate(TestVersionedSaveData oldData)
|
||||
{
|
||||
return new TestVersionedSaveData
|
||||
{
|
||||
Name = $"{oldData.Name}-v2",
|
||||
Level = oldData.Level,
|
||||
Experience = oldData.Level * 100,
|
||||
Version = 2,
|
||||
LastModified = oldData.LastModified
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSaveMigrationV2ToV3 : ISaveMigration<TestVersionedSaveData>
|
||||
{
|
||||
public int FromVersion => 2;
|
||||
|
||||
public int ToVersion => 3;
|
||||
|
||||
public TestVersionedSaveData Migrate(TestVersionedSaveData oldData)
|
||||
{
|
||||
return new TestVersionedSaveData
|
||||
{
|
||||
Name = oldData.Name,
|
||||
Level = oldData.Level,
|
||||
Experience = oldData.Experience,
|
||||
Version = 3,
|
||||
LastModified = oldData.LastModified
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestNonVersionedMigration : ISaveMigration<TestSaveData>
|
||||
{
|
||||
public int FromVersion => 1;
|
||||
|
||||
public int ToVersion => 2;
|
||||
|
||||
public TestSaveData Migrate(TestSaveData oldData)
|
||||
{
|
||||
return new TestSaveData
|
||||
{
|
||||
Name = oldData.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.Storage;
|
||||
using GFramework.Core.Utility;
|
||||
using GFramework.Game.Abstractions.Data;
|
||||
@ -27,6 +31,7 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
||||
where TSaveData : class, IData, new()
|
||||
{
|
||||
private readonly SaveConfiguration _config;
|
||||
private readonly Dictionary<int, ISaveMigration<TSaveData>> _migrations = new();
|
||||
private readonly IStorage _rootStorage;
|
||||
|
||||
/// <summary>
|
||||
@ -43,6 +48,32 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
||||
_rootStorage = new ScopedStorage(storage, config.SaveRoot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册存档迁移器,使仓库在加载旧版本存档时自动执行升级。
|
||||
/// </summary>
|
||||
/// <param name="migration">要注册的存档迁移器。</param>
|
||||
/// <returns>当前存档仓库实例,支持链式调用。</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="migration" /> 为 <see langword="null" />。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// <typeparamref name="TSaveData" /> 未实现 <see cref="IVersionedData" />,无法使用版本化迁移。
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentException">迁移器的目标版本不大于源版本。</exception>
|
||||
public ISaveRepository<TSaveData> RegisterMigration(ISaveMigration<TSaveData> migration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migration);
|
||||
EnsureVersionedSaveType();
|
||||
|
||||
if (migration.ToVersion <= migration.FromVersion)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Migration for {typeof(TSaveData).Name} must advance the version number.",
|
||||
nameof(migration));
|
||||
}
|
||||
|
||||
_migrations[migration.FromVersion] = migration;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定槽位是否存在存档
|
||||
/// </summary>
|
||||
@ -64,7 +95,10 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
||||
var storage = GetSlotStorage(slot);
|
||||
|
||||
if (await storage.ExistsAsync(_config.SaveFileName))
|
||||
return await storage.ReadAsync<TSaveData>(_config.SaveFileName);
|
||||
{
|
||||
var loaded = await storage.ReadAsync<TSaveData>(_config.SaveFileName);
|
||||
return await MigrateIfNeededAsync(slot, storage, loaded);
|
||||
}
|
||||
|
||||
return new TSaveData();
|
||||
}
|
||||
@ -137,10 +171,114 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
|
||||
return new ScopedStorage(_rootStorage, $"{_config.SaveSlotPrefix}{slot}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在加载旧版本存档时按注册顺序执行迁移,并在成功后自动回写升级结果。
|
||||
/// </summary>
|
||||
/// <param name="slot">当前加载的存档槽位。</param>
|
||||
/// <param name="storage">对应槽位的存储对象。</param>
|
||||
/// <param name="data">原始加载出来的存档数据。</param>
|
||||
/// <returns>迁移后的最新存档;如果无需迁移则返回原始对象。</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 当前运行时缺少必要的迁移链、读取到更高版本的存档,或迁移器返回了非法版本。
|
||||
/// </exception>
|
||||
private async Task<TSaveData> MigrateIfNeededAsync(int slot, IStorage storage, TSaveData data)
|
||||
{
|
||||
if (data is not IVersionedData versionedData)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
var latestTemplate = new TSaveData();
|
||||
if (latestTemplate is not IVersionedData latestVersionedData)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
var currentVersion = versionedData.Version;
|
||||
var targetVersion = latestVersionedData.Version;
|
||||
|
||||
if (currentVersion > targetVersion)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Save slot {slot} for {typeof(TSaveData).Name} is version {currentVersion}, " +
|
||||
$"which is newer than the current runtime version {targetVersion}.");
|
||||
}
|
||||
|
||||
if (currentVersion == targetVersion)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
EnsureVersionedSaveType();
|
||||
|
||||
var migrated = data;
|
||||
|
||||
// 迁移链按“当前版本 -> 下一个已注册迁移器”推进;任何缺口都表示运行时无法安全解释旧存档。
|
||||
while (currentVersion < targetVersion)
|
||||
{
|
||||
if (!_migrations.TryGetValue(currentVersion, out var migration))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No save migration is registered for {typeof(TSaveData).Name} from version {currentVersion}.");
|
||||
}
|
||||
|
||||
migrated = migration.Migrate(migrated) ??
|
||||
throw new InvalidOperationException(
|
||||
$"Save migration for {typeof(TSaveData).Name} from version {currentVersion} returned null.");
|
||||
|
||||
if (migrated is not IVersionedData migratedVersionedData)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Save migration for {typeof(TSaveData).Name} must return data implementing {nameof(IVersionedData)}.");
|
||||
}
|
||||
|
||||
// 显式校验迁移器声明与实际结果,避免版本号不前进导致死循环或把旧数据错误回写为“已升级”。
|
||||
if (migration.ToVersion != migratedVersionedData.Version)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Save migration for {typeof(TSaveData).Name} declared target version {migration.ToVersion} " +
|
||||
$"but returned version {migratedVersionedData.Version}.");
|
||||
}
|
||||
|
||||
if (migratedVersionedData.Version <= currentVersion)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Save migration for {typeof(TSaveData).Name} must advance beyond version {currentVersion}.");
|
||||
}
|
||||
|
||||
if (migratedVersionedData.Version > targetVersion)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Save migration for {typeof(TSaveData).Name} produced version {migratedVersionedData.Version}, " +
|
||||
$"which exceeds the current runtime version {targetVersion}.");
|
||||
}
|
||||
|
||||
currentVersion = migratedVersionedData.Version;
|
||||
}
|
||||
|
||||
await storage.WriteAsync(_config.SaveFileName, migrated);
|
||||
return migrated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当前存档类型支持基于版本号的迁移流程。
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// <typeparamref name="TSaveData" /> 未实现 <see cref="IVersionedData" />。
|
||||
/// </exception>
|
||||
private static void EnsureVersionedSaveType()
|
||||
{
|
||||
if (!typeof(IVersionedData).IsAssignableFrom(typeof(TSaveData)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{typeof(TSaveData).Name} must implement {nameof(IVersionedData)} to use save migrations.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化逻辑
|
||||
/// </summary>
|
||||
protected override void OnInit()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,9 +136,4 @@
|
||||
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>
|
||||
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="local-plan\docs\"/>
|
||||
<Folder Include="local-plan\todos\"/>
|
||||
<Folder Include="local-plan\评估\"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -56,6 +56,7 @@ public interface IDataRepository : IUtility
|
||||
public interface ISaveRepository<TSaveData> : IUtility
|
||||
where TSaveData : class, IData, new()
|
||||
{
|
||||
ISaveRepository<TSaveData> RegisterMigration(ISaveMigration<TSaveData> migration);
|
||||
Task<bool> ExistsAsync(int slot);
|
||||
Task<TSaveData> LoadAsync(int slot);
|
||||
Task SaveAsync(int slot, TSaveData data);
|
||||
@ -64,6 +65,18 @@ public interface ISaveRepository<TSaveData> : IUtility
|
||||
}
|
||||
```
|
||||
|
||||
`ISaveMigration<TSaveData>` 定义单步迁移:
|
||||
|
||||
```csharp
|
||||
public interface ISaveMigration<TSaveData>
|
||||
where TSaveData : class, IData
|
||||
{
|
||||
int FromVersion { get; }
|
||||
int ToVersion { get; }
|
||||
TSaveData Migrate(TSaveData oldData);
|
||||
}
|
||||
```
|
||||
|
||||
### 版本化数据
|
||||
|
||||
`IVersionedData` 支持数据版本管理:
|
||||
@ -265,67 +278,77 @@ public partial class AutoSaveController : IController
|
||||
|
||||
### 数据版本迁移
|
||||
|
||||
`SaveRepository<TSaveData>` 当前负责槽位存档的读取、写入、删除和列举,并没有内建“注册迁移器后自动升级存档”的统一迁移管线。
|
||||
`SaveRepository<TSaveData>` 现在支持注册正式的迁移器,并在 `LoadAsync(slot)` 时自动升级旧版本存档。
|
||||
|
||||
下面示例展示的是应用层迁移策略:加载后检查版本,调用你自己的迁移逻辑,再决定是否回写新版本数据。
|
||||
迁移规则如下:
|
||||
|
||||
- `TSaveData` 需要实现 `IVersionedData`
|
||||
- 仓库以 `new TSaveData().Version` 作为当前运行时目标版本
|
||||
- 每个迁移器负责一个 `FromVersion -> ToVersion` 跳转
|
||||
- 加载时仓库会按链路连续执行迁移,并在成功后自动回写升级后的存档
|
||||
- 如果缺少中间迁移器,或者读到了比当前运行时更高的版本,`LoadAsync` 会抛出异常,避免静默加载错误数据
|
||||
|
||||
```csharp
|
||||
// 版本 1 的数据
|
||||
public class SaveDataV1 : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public string PlayerName { get; set; }
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
// 版本 2 的数据(添加了新字段)
|
||||
public class SaveDataV2 : IVersionedData
|
||||
public sealed class SaveData : IVersionedData
|
||||
{
|
||||
// 当前运行时代码支持的最新版本
|
||||
public int Version { get; set; } = 2;
|
||||
public string PlayerName { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; } // 新增字段
|
||||
public DateTime LastPlayTime { get; set; } // 新增字段
|
||||
public int Experience { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
}
|
||||
|
||||
// 数据迁移器
|
||||
public class SaveDataMigrator
|
||||
public sealed class SaveDataMigrationV1ToV2 : ISaveMigration<SaveData>
|
||||
{
|
||||
public SaveDataV2 Migrate(SaveDataV1 oldData)
|
||||
public int FromVersion => 1;
|
||||
|
||||
public int ToVersion => 2;
|
||||
|
||||
public SaveData Migrate(SaveData oldData)
|
||||
{
|
||||
return new SaveDataV2
|
||||
return new SaveData
|
||||
{
|
||||
Version = 2,
|
||||
PlayerName = oldData.PlayerName,
|
||||
Level = oldData.Level,
|
||||
Experience = oldData.Level * 100, // 根据等级计算经验
|
||||
LastPlayTime = DateTime.Now
|
||||
Experience = oldData.Level * 100,
|
||||
LastModified = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 加载后由应用层决定是否迁移
|
||||
public async Task<SaveDataV2> LoadWithMigration(int slot)
|
||||
public sealed class SaveModule : AbstractModule
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveDataV2>>();
|
||||
var data = await saveRepo.LoadAsync(slot);
|
||||
|
||||
if (data.Version < 2)
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 需要迁移:此处调用应用层迁移器
|
||||
var oldData = data as SaveDataV1;
|
||||
var migrator = new SaveDataMigrator();
|
||||
var newData = migrator.Migrate(oldData);
|
||||
var storage = architecture.GetUtility<IStorage>();
|
||||
var saveConfig = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save"
|
||||
};
|
||||
|
||||
// 保存迁移后的数据
|
||||
await saveRepo.SaveAsync(slot, newData);
|
||||
return newData;
|
||||
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig)
|
||||
.RegisterMigration(new SaveDataMigrationV1ToV2());
|
||||
|
||||
architecture.RegisterUtility<ISaveRepository<SaveData>>(saveRepo);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
public async Task<SaveData> LoadGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 如果槽位里是 v1,仓库会自动迁移到 v2,并把新版本重新写回存储。
|
||||
return await saveRepo.LoadAsync(slot);
|
||||
}
|
||||
```
|
||||
|
||||
`ISaveMigration<TSaveData>` 接收和返回的是同一个存档类型。也就是说,框架提供的是“当前类型内的版本升级管线”,
|
||||
而不是跨 CLR 类型的双模型反序列化系统。如果旧版本缺失了新字段,反序列化会先使用当前类型的默认值,再由迁移器补齐。
|
||||
|
||||
### 使用数据仓库
|
||||
|
||||
```csharp
|
||||
@ -514,15 +537,14 @@ await saveRepo.SaveAsync(3, saveData); // 槽位 3
|
||||
### 问题:如何处理数据版本升级?
|
||||
|
||||
**解答**:
|
||||
实现 `IVersionedData` 并在加载后检查版本。当前框架不会自动为 `ISaveRepository<T>` 执行迁移,需要由业务层决定迁移规则与回写时机:
|
||||
实现 `IVersionedData`,并在仓库初始化阶段注册 `ISaveMigration<TSaveData>`。之后 `LoadAsync(slot)` 会自动执行迁移并回写:
|
||||
|
||||
```csharp
|
||||
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig)
|
||||
.RegisterMigration(new SaveDataMigrationV1ToV2())
|
||||
.RegisterMigration(new SaveDataMigrationV2ToV3());
|
||||
|
||||
var data = await saveRepo.LoadAsync(slot);
|
||||
if (data.Version < CurrentVersion)
|
||||
{
|
||||
data = MigrateData(data);
|
||||
await saveRepo.SaveAsync(slot, data);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:存档数据保存在哪里?
|
||||
|
||||
@ -196,4 +196,4 @@ public interface ISettingsMigration
|
||||
- 设置迁移是内建能力
|
||||
- 设置持久化是内建能力
|
||||
- 设置如何应用到具体引擎由 applicator 决定
|
||||
- 存档系统的迁移能力不等同于设置系统;`ISaveRepository<T>` 当前仍需要业务层自己实现迁移策略
|
||||
- 存档系统也支持内建版本迁移,但入口位于 `ISaveRepository<T>.RegisterMigration(...)`,语义是槽位存档升级而不是设置节初始化
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user