mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 08:44:29 +08:00
- 实现 ISaveRepository<T> 接口提供存档管理功能 - 添加 SaveRepository<T> 实现类支持槽位存档管理 - 实现数据版本迁移机制支持存档版本升级 - 添加完整的存档测试用例验证功能正确性 - 创建数据与存档系统中文文档说明使用方法 - 移除项目中不再需要的本地计划文件夹配置
285 lines
11 KiB
C#
285 lines
11 KiB
C#
// 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;
|
||
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;
|
||
using GFramework.Game.Storage;
|
||
|
||
namespace GFramework.Game.Data;
|
||
|
||
/// <summary>
|
||
/// 基于槽位的存档仓库实现
|
||
/// </summary>
|
||
/// <typeparam name="TSaveData">存档数据类型</typeparam>
|
||
public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository<TSaveData>
|
||
where TSaveData : class, IData, new()
|
||
{
|
||
private readonly SaveConfiguration _config;
|
||
private readonly Dictionary<int, ISaveMigration<TSaveData>> _migrations = new();
|
||
private readonly IStorage _rootStorage;
|
||
|
||
/// <summary>
|
||
/// 初始化存档仓库
|
||
/// </summary>
|
||
/// <param name="storage">存储实例</param>
|
||
/// <param name="config">存档配置</param>
|
||
public SaveRepository(IStorage storage, SaveConfiguration config)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(storage);
|
||
ArgumentNullException.ThrowIfNull(config);
|
||
|
||
_config = config;
|
||
_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>
|
||
/// <param name="slot">存档槽位编号</param>
|
||
/// <returns>如果存档存在返回true,否则返回false</returns>
|
||
public async Task<bool> ExistsAsync(int slot)
|
||
{
|
||
var storage = GetSlotStorage(slot);
|
||
return await storage.ExistsAsync(_config.SaveFileName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载指定槽位的存档
|
||
/// </summary>
|
||
/// <param name="slot">存档槽位编号</param>
|
||
/// <returns>存档数据对象,如果不存在则返回新实例</returns>
|
||
public async Task<TSaveData> LoadAsync(int slot)
|
||
{
|
||
var storage = GetSlotStorage(slot);
|
||
|
||
if (await storage.ExistsAsync(_config.SaveFileName))
|
||
{
|
||
var loaded = await storage.ReadAsync<TSaveData>(_config.SaveFileName);
|
||
return await MigrateIfNeededAsync(slot, storage, loaded);
|
||
}
|
||
|
||
return new TSaveData();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存存档到指定槽位
|
||
/// </summary>
|
||
/// <param name="slot">存档槽位编号</param>
|
||
/// <param name="data">要保存的存档数据</param>
|
||
public async Task SaveAsync(int slot, TSaveData data)
|
||
{
|
||
var slotPath = $"{_config.SaveSlotPrefix}{slot}";
|
||
|
||
// 确保槽位目录存在
|
||
if (!await _rootStorage.DirectoryExistsAsync(slotPath))
|
||
await _rootStorage.CreateDirectoryAsync(slotPath);
|
||
|
||
var storage = GetSlotStorage(slot);
|
||
await storage.WriteAsync(_config.SaveFileName, data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除指定槽位的存档
|
||
/// </summary>
|
||
/// <param name="slot">存档槽位编号</param>
|
||
public async Task DeleteAsync(int slot)
|
||
{
|
||
var storage = GetSlotStorage(slot);
|
||
await storage.DeleteAsync(_config.SaveFileName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 列出所有有效的存档槽位
|
||
/// </summary>
|
||
/// <returns>包含所有有效存档槽位编号的只读列表,按升序排列</returns>
|
||
public async Task<IReadOnlyList<int>> ListSlotsAsync()
|
||
{
|
||
// 列举所有槽位目录
|
||
var directories = await _rootStorage.ListDirectoriesAsync();
|
||
|
||
var slots = new List<int>();
|
||
|
||
foreach (var dirName in directories)
|
||
{
|
||
// 检查目录名是否符合槽位前缀
|
||
if (!dirName.StartsWith(_config.SaveSlotPrefix, StringComparison.Ordinal))
|
||
continue;
|
||
|
||
// 提取槽位编号
|
||
var slotStr = dirName[_config.SaveSlotPrefix.Length..];
|
||
if (!int.TryParse(slotStr, CultureInfo.InvariantCulture, out var slot))
|
||
continue;
|
||
|
||
// 直接检查存档文件是否存在,避免重复创建 ScopedStorage
|
||
var saveFilePath = $"{dirName}/{_config.SaveFileName}";
|
||
if (await _rootStorage.ExistsAsync(saveFilePath))
|
||
slots.Add(slot);
|
||
}
|
||
|
||
return slots.OrderBy(x => x).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定槽位的存储对象
|
||
/// </summary>
|
||
/// <param name="slot">存档槽位编号</param>
|
||
/// <returns>对应槽位的存储对象</returns>
|
||
private IStorage GetSlotStorage(int slot)
|
||
{
|
||
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()
|
||
{
|
||
}
|
||
}
|