gewuyou c106e53a74 fix(game): 修复 SettingsModel 的 MA0004 警告
- 修复 SettingsModel 中异步仓储加载与保存流程缺少 ConfigureAwait(false) 的 analyzer 警告

- 保持设置模型初始化、保存、应用阶段的生命周期与事件触发语义不变
2026-04-27 07:58:22 +08:00

385 lines
14 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 object _migrationMapLock = 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>
/// <exception cref="ArgumentNullException">
/// <paramref name="migration" /> 为 <see langword="null" /> 时抛出。
/// </exception>
/// <exception cref="ArgumentException">
/// 迁移声明的目标版本不大于源版本时抛出。
/// </exception>
/// <exception cref="InvalidOperationException">
/// 同一设置类型与源版本已经注册过迁移器时抛出。
/// </exception>
/// <remarks>
/// 迁移注册表与按类型缓存的版本映射需要保持一致;因此注册与 cache miss 时的缓存重建
/// 统一通过 <see cref="_migrationMapLock" /> 串行化,避免并发加载把旧快照重新写回缓存。
/// </remarks>
public ISettingsModel RegisterMigration(ISettingsMigration migration)
{
ArgumentNullException.ThrowIfNull(migration);
VersionedMigrationRunner.ValidateForwardOnlyRegistration(
migration.SettingsType.Name,
"Settings migration",
migration.FromVersion,
migration.ToVersion,
nameof(migration));
lock (_migrationMapLock)
{
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().ConfigureAwait(false);
}
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).ConfigureAwait(false);
}
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().ConfigureAwait(false);
}
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);
}
/// <summary>
/// 将已加载的设置数据迁移到当前运行时实例声明的目标版本。
/// </summary>
/// <param name="data">从仓库读取的设置数据。</param>
/// <param name="latestData">当前内存中的设置实例,其 <c>Version</c> 值代表目标版本。</param>
/// <returns>迁移后的设置数据;如果无需迁移则返回原对象。</returns>
/// <remarks>
/// 该方法按设置类型缓存迁移表,并始终以 <paramref name="latestData" /> 的版本作为目标运行时版本,
/// 避免把旧文件中的版本号误当成当前版本。具体的缺链、版本一致性与前进性校验都委托给
/// <see cref="VersionedMigrationRunner" /> 统一处理。缓存重建与迁移注册共用
/// <see cref="_migrationMapLock" />,确保运行中的初始化不会把过期迁移快照写回缓存。
/// </remarks>
private ISettingsData MigrateIfNeeded(ISettingsData data, ISettingsData latestData)
{
var type = data.GetType();
Dictionary<int, ISettingsMigration> versionMap;
lock (_migrationMapLock)
{
if (!_migrationCache.TryGetValue(type, out var cachedVersionMap))
{
// cache miss 与 RegisterMigration 共用同一把锁,避免注册新迁移后又被旧快照覆盖回缓存。
versionMap = _migrations
.Where(kv => kv.Key.type == type)
.ToDictionary(kv => kv.Key.from, kv => kv.Value);
_migrationCache[type] = versionMap;
}
else
{
versionMap = cachedVersionMap;
}
}
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;
}
}