gewuyou 88de1235ae refactor(game): 收敛版本迁移链执行器
- 新增 internal 迁移执行器,统一 settings 与 save 的链式版本校验

- 修复 SettingsModel 重复注册、缺链回填与目标版本判定的迁移约束

- 补充 Persistence 与 SettingsModel 定向测试,并更新迁移文档和 ai-plan 跟踪
2026-04-20 09:52:37 +08:00

346 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Extensions;
using GFramework.Core.Logging;
using GFramework.Core.Model;
using GFramework.Game.Abstractions.Data;
using GFramework.Game.Abstractions.Setting;
using GFramework.Game.Internal;
using GFramework.Game.Setting.Events;
namespace GFramework.Game.Setting;
/// <summary>
/// 设置模型:
/// - 管理 Settings Data 的生命周期Load / Save / Reset / Migration
/// - 编排 Settings Applicator 的 Apply 行为
/// </summary>
public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider, TRepository? repository)
: AbstractModel, ISettingsModel
where TRepository : class, ISettingsDataRepository
{
private static readonly ILogger Log =
LoggerFactoryResolver.Provider.CreateLogger(nameof(SettingsModel<TRepository>));
private readonly ConcurrentDictionary<Type, IResetApplyAbleSettings> _applicators = new();
// =========================
// Fields
// =========================
private readonly ConcurrentDictionary<Type, ISettingsData> _data = new();
private readonly ConcurrentDictionary<Type, Dictionary<int, ISettingsMigration>> _migrationCache = new();
private readonly ConcurrentDictionary<(Type type, int from), ISettingsMigration> _migrations = new();
private volatile bool _initialized;
private IDataLocationProvider? _locationProvider = locationProvider;
private ISettingsDataRepository? _repository = repository;
private ISettingsDataRepository DataRepository =>
_repository ?? throw new InvalidOperationException("ISettingsDataRepository not initialized.");
private IDataLocationProvider LocationProvider =>
_locationProvider ?? throw new InvalidOperationException("IDataLocationProvider not initialized.");
/// <summary>
/// 获取一个布尔值,指示当前对象是否已初始化。
/// </summary>
/// <returns>如果对象已初始化则返回 true否则返回 false。</returns>
public bool IsInitialized => _initialized;
// =========================
// Data access
// =========================
/// <summary>
/// 获取指定类型的设置数据实例(唯一实例)
/// </summary>
/// <typeparam name="T">实现ISettingsData接口且具有无参构造函数的类型</typeparam>
/// <returns>指定类型的设置数据实例</returns>
public T GetData<T>() where T : class, ISettingsData, new()
{
// 使用_data字典获取或添加指定类型的实例确保唯一性
var data = (T)_data.GetOrAdd(typeof(T), _ => new T());
TryRegisterDataType(typeof(T));
return data;
}
/// <summary>
/// 获取所有设置数据实例的集合
/// </summary>
/// <returns>包含所有ISettingsData实例的可枚举集合</returns>
public IEnumerable<ISettingsData> AllData()
{
// 返回_data字典中所有值的集合
return _data.Values;
}
// =========================
// Applicator
// =========================
/// <summary>
/// 注册设置应用器
/// </summary>
public ISettingsModel RegisterApplicator<T>(T applicator)
where T : class, IResetApplyAbleSettings
{
_applicators[typeof(T)] = applicator;
_data[applicator.DataType] = applicator.Data;
TryRegisterDataType(applicator.DataType);
return this;
}
/// <summary>
/// 获取所有设置应用器的集合。
/// </summary>
/// <returns>
/// 返回一个包含所有设置应用器的可枚举集合。
/// </returns>
public IEnumerable<IResetApplyAbleSettings> AllApplicators()
{
return _applicators.Values;
}
// =========================
// Migration
// =========================
/// <summary>
/// 注册一个设置迁移对象,并将其与指定的设置类型和版本关联。
/// </summary>
/// <param name="migration">
/// 要注册的设置迁移对象,需实现 ISettingsMigration 接口。
/// </param>
/// <returns>
/// 返回当前 ISettingsModel 实例,支持链式调用。
/// </returns>
public ISettingsModel RegisterMigration(ISettingsMigration migration)
{
ArgumentNullException.ThrowIfNull(migration);
VersionedMigrationRunner.ValidateForwardOnlyRegistration(
migration.SettingsType.Name,
"Settings migration",
migration.FromVersion,
migration.ToVersion,
nameof(migration));
if (!_migrations.TryAdd((migration.SettingsType, migration.FromVersion), migration))
{
throw new InvalidOperationException(
$"Duplicate settings migration registration for {migration.SettingsType.Name} from version {migration.FromVersion}.");
}
_migrationCache.TryRemove(migration.SettingsType, out _);
return this;
}
// =========================
// Lifecycle
// =========================
/// <summary>
/// 初始化设置模型:
/// - 加载所有已存在的 Settings Data
/// - 执行必要的迁移
/// </summary>
public async Task InitializeAsync()
{
IDictionary<string, IData> allData;
try
{
allData = await DataRepository.LoadAllAsync();
}
catch (Exception ex)
{
Log.Error("Failed to load unified settings file.", ex);
return;
}
foreach (var data in _data.Values)
try
{
var type = data.GetType();
var location = LocationProvider.GetLocation(type);
if (!allData.TryGetValue(location.Key, out var raw))
continue;
if (raw is not ISettingsData loaded)
continue;
var migrated = MigrateIfNeeded(loaded, data);
// 回填(不替换实例)
data.LoadFrom(migrated);
}
catch (Exception ex)
{
Log.Error($"Failed to initialize settings data: {data.GetType().Name}", ex);
}
_initialized = true;
this.SendEvent(new SettingsInitializedEvent());
}
/// <summary>
/// 将所有 Settings Data 持久化
/// </summary>
public async Task SaveAllAsync()
{
foreach (var data in _data.Values)
try
{
var location = LocationProvider.GetLocation(data.GetType());
await DataRepository.SaveAsync(location, data);
}
catch (Exception ex)
{
Log.Error($"Failed to save settings data: {data.GetType().Name}", ex);
}
this.SendEvent(new SettingsSavedAllEvent());
}
/// <summary>
/// 应用所有设置
/// </summary>
public async Task ApplyAllAsync()
{
foreach (var applicator in _applicators)
try
{
await applicator.Value.ApplyAsync();
}
catch (Exception ex)
{
Log.Error($"Failed to apply settings: {applicator.GetType().Name}", ex);
}
this.SendEvent(new SettingsAppliedAllEvent());
}
/// <summary>
/// 重置指定类型的可重置对象
/// </summary>
/// <typeparam name="T">要重置的对象类型必须是class类型实现IResettable接口并具有无参构造函数</typeparam>
public void Reset<T>() where T : class, ISettingsData, new()
{
var data = GetData<T>();
data.Reset();
this.SendEvent(new SettingsResetEvent<T>(data));
}
/// <summary>
/// 重置所有设置
/// </summary>
public void ResetAll()
{
foreach (var data in _data.Values)
data.Reset();
foreach (var applicator in _applicators)
applicator.Value.Reset();
this.SendEvent(new SettingsResetAllEvent(_data.Values));
}
/// <summary>
/// 获取指定类型的设置应用器
/// </summary>
/// <typeparam name="T">要获取的设置应用器类型必须继承自IResetApplyAbleSettings</typeparam>
/// <returns>设置应用器实例如果不存在则返回null</returns>
public T? GetApplicator<T>() where T : class, IResetApplyAbleSettings
{
return _applicators.TryGetValue(typeof(T), out var app)
? (T)app
: null;
}
// =========================
// OnInitialize
// =========================
/// <summary>
/// 初始化函数,在对象创建时调用。该函数负责初始化数据仓库和位置提供者,
/// 并注册所有已知数据类型到数据仓库中。
/// </summary>
protected override void OnInit()
{
// 初始化数据仓库实例,如果尚未赋值则通过依赖注入获取
_repository ??= this.GetUtility<TRepository>()!;
// 初始化位置提供者实例,如果尚未赋值则通过依赖注入获取
_locationProvider ??= this.GetUtility<IDataLocationProvider>()!;
// 遍历所有已知的数据类型,为其分配位置并注册到数据仓库中
foreach (var type in _data.Keys)
{
TryRegisterDataType(type);
}
}
private void TryRegisterDataType(Type type)
{
if (_repository == null || _locationProvider == null)
{
return;
}
var location = _locationProvider.GetLocation(type);
_repository.RegisterDataType(location, type);
}
private ISettingsData MigrateIfNeeded(ISettingsData data, ISettingsData latestData)
{
var type = data.GetType();
if (!_migrationCache.TryGetValue(type, out var versionMap))
{
versionMap = _migrations
.Where(kv => kv.Key.type == type)
.ToDictionary(kv => kv.Key.from, kv => kv.Value);
_migrationCache[type] = versionMap;
}
return VersionedMigrationRunner.MigrateToTargetVersion(
data,
latestData.Version,
static settings => settings.Version,
fromVersion => versionMap.TryGetValue(fromVersion, out var migration) ? migration : null,
static migration => migration.ToVersion,
static (migration, current) => ApplySettingsMigration(migration, current),
$"{type.Name} settings",
"settings migration");
}
/// <summary>
/// 执行单步设置迁移,并验证迁移结果仍然属于已注册的设置类型。
/// </summary>
/// <param name="migration">要执行的迁移器。</param>
/// <param name="currentData">当前版本的数据。</param>
/// <returns>迁移后的设置数据。</returns>
/// <exception cref="InvalidOperationException">
/// 迁移结果不实现 <see cref="ISettingsData" />,或返回了与声明设置类型不兼容的数据时抛出。
/// </exception>
private static ISettingsData ApplySettingsMigration(ISettingsMigration migration, ISettingsData currentData)
{
var fromVersion = currentData.Version;
var migrated = migration.Migrate(currentData);
if (migrated is not ISettingsData migratedData)
{
throw new InvalidOperationException(
$"Settings migration for {migration.SettingsType.Name} from version {fromVersion} must return {nameof(ISettingsData)}.");
}
if (!migration.SettingsType.IsInstanceOfType(migratedData))
{
throw new InvalidOperationException(
$"Settings migration for {migration.SettingsType.Name} from version {fromVersion} returned incompatible data type {migratedData.GetType().Name}.");
}
return migratedData;
}
}