feat(settings): 重构设置系统架构以支持数据仓库模式

- 为 ILoadableFrom 接口添加 XML 文档注释
- 重命名 UnifiedSettingsRepository 为 UnifiedSettingsDataRepository
- 将仓库基类从 IDataRepository 替换为更具体的 ISettingsDataRepository
- 为 UnifiedSettingsDataRepository 添加完整的 XML 文档注释
- 在 SettingsModel 中使用 ISettingsDataRepository 替代 IDataRepository
- 修改 SettingsModel 中的应用器存储结构从 ConcurrentBag 到 ConcurrentDictionary
- 添加 LoadAllAsync 方法以支持批量加载所有设置数据
- 优化 SettingsModel 初始化逻辑以使用批量加载提高性能
- 为 AudioSettings、GraphicsSettings 和 LocalizationSettings 添加 LoadFrom 实现
- 将设置数据版本属性改为私有只读以防止外部修改
- 更新 SettingsSystem 接口约束以匹配新的抽象层设计
- 添加 GetApplicator 泛型方法以支持获取特定类型的应用器
- 实现 Reset 泛型方法以支持重置指定类型的设置数据
- [release ci]
This commit is contained in:
GeWuYou 2026-01-30 21:16:31 +08:00
parent 970b8d3b96
commit 7d581f07ca
11 changed files with 325 additions and 138 deletions

View File

@ -13,7 +13,15 @@
namespace GFramework.Core.Abstractions.data;
/// <summary>
/// 定义从指定类型数据源加载数据的接口
/// </summary>
/// <typeparam name="T">数据源的类型</typeparam>
public interface ILoadableFrom<in T>
{
/// <summary>
/// 从指定的数据源加载数据到当前对象
/// </summary>
/// <param name="source">用作数据源的对象类型为T</param>
void LoadFrom(T source);
}

View File

@ -29,6 +29,7 @@ public interface IDataRepository : IUtility
Task<T> LoadAsync<T>(IDataLocation location)
where T : class, IData, new();
/// <summary>
/// 异步保存数据到指定位置
/// </summary>

View File

@ -0,0 +1,34 @@
// 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.
namespace GFramework.Game.Abstractions.data;
/// <summary>
/// 定义设置数据仓库接口,用于管理应用程序设置数据的存储和检索
/// </summary>
/// <remarks>
/// 该接口继承自IDataRepository专门用于处理配置设置相关的数据操作
/// </remarks>
public interface ISettingsDataRepository : IDataRepository
{
/// <summary>
/// 异步加载所有设置项
/// </summary>
/// <returns>
/// 返回一个包含所有设置键值对的字典,其中键为设置名称,值为对应的设置数据对象
/// </returns>
/// <remarks>
/// 此方法将从数据源中异步读取所有可用的设置项,并将其组织成字典格式返回
/// </remarks>
Task<IDictionary<string, IData>> LoadAllAsync();
}

View File

@ -35,9 +35,19 @@ public interface ISettingsModel : IModel
/// <summary>
/// 注册设置应用器
/// </summary>
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口且具有无参构造函数</typeparam>
/// <param name="applicator">要注册的设置应用器</param>
/// <returns>当前设置模型实例,支持链式调用</returns>
ISettingsModel RegisterApplicator(IResetApplyAbleSettings applicator);
ISettingsModel RegisterApplicator<T>(IResetApplyAbleSettings applicator) where T : class, ISettingsData, new();
/// <summary>
/// 获取指定类型的设置应用器
/// </summary>
/// <typeparam name="T">要获取的设置应用器类型必须继承自IResetApplyAbleSettings</typeparam>
/// <returns>设置应用器实例如果不存在则返回null</returns>
T? GetApplicator<T>() where T : class, IResetApplyAbleSettings;
/// <summary>
/// 获取所有设置应用器
@ -80,8 +90,15 @@ public interface ISettingsModel : IModel
/// <returns>异步操作任务</returns>
Task ApplyAllAsync();
/// <summary>
/// 重置指定类型的设置
/// </summary>
/// <typeparam name="T">要重置的设置类型必须实现IResettable接口并具有无参构造函数</typeparam>
void Reset<T>() where T : class, ISettingsData, new();
/// <summary>
/// 重置所有设置数据与应用器
/// </summary>
void ResetAll();
}
}

View File

@ -16,9 +16,9 @@ public interface ISettingsSystem : ISystem
/// <summary>
/// 应用指定类型的设置(泛型版本)
/// </summary>
/// <typeparam name="T">设置类型必须是class且实现IApplyAbleSettings接口</typeparam>
/// <typeparam name="T">设置类型必须是class且实现IResetApplyAbleSettings接口</typeparam>
/// <returns>表示异步操作的任务</returns>
Task Apply<T>() where T : class, IApplyAbleSettings;
Task Apply<T>() where T : class, IResetApplyAbleSettings;
/// <summary>
/// 保存所有设置
@ -31,7 +31,7 @@ public interface ISettingsSystem : ISystem
/// </summary>
/// <typeparam name="T">设置类型必须继承自class并实现IPersistentApplyAbleSettings接口</typeparam>
/// <returns>表示异步操作的任务</returns>
Task Reset<T>() where T : class, IResetApplyAbleSettings, new();
Task Reset<T>() where T : class, ISettingsData, IResetApplyAbleSettings, new();
/// <summary>
/// 重置所有设置

View File

@ -34,10 +34,29 @@ public class AudioSettings : ISettingsData
/// <summary>
/// 获取或设置设置数据的版本号
/// </summary>
public int Version { get; set; } = 1;
public int Version { get; private set; } = 1;
/// <summary>
/// 获取设置数据最后修改的时间
/// </summary>
public DateTime LastModified { get; } = DateTime.Now;
}
/// <summary>
/// 从指定的数据源加载音频设置
/// </summary>
/// <param name="source">包含设置数据的源对象</param>
public void LoadFrom(ISettingsData source)
{
// 检查数据源是否为音频设置类型
if (source is not AudioSettings audioSettings)
{
return;
}
// 将源数据中的各个音量设置复制到当前对象
MasterVolume = audioSettings.MasterVolume;
BgmVolume = audioSettings.BgmVolume;
SfxVolume = audioSettings.SfxVolume;
Version = audioSettings.Version;
}
}

View File

@ -33,10 +33,29 @@ public class GraphicsSettings : ISettingsData
/// <summary>
/// 获取或设置设置数据的版本号
/// </summary>
public int Version { get; set; } = 1;
public int Version { get; private set; } = 1;
/// <summary>
/// 获取设置数据最后修改的时间
/// </summary>
public DateTime LastModified { get; } = DateTime.Now;
/// <summary>
/// 从指定的数据源加载图形设置
/// </summary>
/// <param name="source">要从中加载设置的源数据对象</param>
public void LoadFrom(ISettingsData source)
{
// 检查源数据是否为GraphicsSettings类型如果不是则直接返回
if (source is not GraphicsSettings settings)
{
return;
}
// 将源设置中的属性值复制到当前对象
Fullscreen = settings.Fullscreen;
ResolutionWidth = settings.ResolutionWidth;
ResolutionHeight = settings.ResolutionHeight;
Version = settings.Version;
}
}

View File

@ -37,10 +37,29 @@ public class LocalizationSettings : ISettingsData
/// <summary>
/// 获取或设置设置数据的版本号
/// </summary>
public int Version { get; set; } = 1;
public int Version { get; private set; } = 1;
/// <summary>
/// 获取设置数据最后修改的时间
/// </summary>
public DateTime LastModified { get; } = DateTime.Now;
/// <summary>
/// 从指定的数据源加载本地化设置
/// </summary>
/// <param name="source">要从中加载设置的源对象</param>
/// <remarks>
/// 该方法仅处理类型为LocalizationSettings的对象
/// 如果源对象不是LocalizationSettings类型则直接返回不执行任何操作
/// </remarks>
public void LoadFrom(ISettingsData source)
{
if (source is not LocalizationSettings settings)
{
return;
}
Language = settings.Language;
Version = settings.Version;
}
}

View File

@ -23,16 +23,16 @@ namespace GFramework.Game.data;
/// <summary>
/// 使用单一文件存储所有设置数据的仓库实现
/// </summary>
public class UnifiedSettingsRepository(
public class UnifiedSettingsDataRepository(
IStorage? storage,
IRuntimeTypeSerializer? serializer,
DataRepositoryOptions? options = null,
string fileName = "settings.json")
: AbstractContextUtility, IDataRepository
: AbstractContextUtility, ISettingsDataRepository
{
private UnifiedSettingsFile? _file;
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions();
private UnifiedSettingsFile? _file;
private bool _loaded;
private IRuntimeTypeSerializer? _serializer = serializer;
private IStorage? _storage = storage;
@ -45,16 +45,16 @@ public class UnifiedSettingsRepository(
private UnifiedSettingsFile File =>
_file ?? throw new InvalidOperationException("UnifiedSettingsFile not set.");
protected override void OnInit()
{
_storage ??= this.GetUtility<IStorage>()!;
_serializer ??= this.GetUtility<IRuntimeTypeSerializer>()!;
}
// =========================
// IDataRepository
// =========================
/// <summary>
/// 异步加载指定位置的数据
/// </summary>
/// <typeparam name="T">数据类型必须继承自IData接口</typeparam>
/// <param name="location">数据位置信息</param>
/// <returns>加载的数据对象</returns>
public async Task<T> LoadAsync<T>(IDataLocation location)
where T : class, IData, new()
{
@ -66,6 +66,13 @@ public class UnifiedSettingsRepository(
return result;
}
/// <summary>
/// 异步保存数据到指定位置
/// </summary>
/// <typeparam name="T">数据类型必须继承自IData接口</typeparam>
/// <param name="location">数据位置信息</param>
/// <param name="data">要保存的数据对象</param>
/// <returns>异步操作任务</returns>
public async Task SaveAsync<T>(IDataLocation location, T data)
where T : class, IData
{
@ -81,13 +88,22 @@ public class UnifiedSettingsRepository(
this.SendEvent(new DataSavedEvent<T>(data));
}
/// <summary>
/// 检查指定位置的数据是否存在
/// </summary>
/// <param name="location">数据位置信息</param>
/// <returns>如果数据存在则返回true否则返回false</returns>
public async Task<bool> ExistsAsync(IDataLocation location)
{
await EnsureLoadedAsync();
return File.Sections.ContainsKey(location.Key);
}
/// <summary>
/// 删除指定位置的数据
/// </summary>
/// <param name="location">数据位置信息</param>
/// <returns>异步操作任务</returns>
public async Task DeleteAsync(IDataLocation location)
{
await EnsureLoadedAsync();
@ -101,7 +117,11 @@ public class UnifiedSettingsRepository(
}
}
/// <summary>
/// 批量保存多个数据项到存储
/// </summary>
/// <param name="dataList">包含数据位置和数据对象的枚举集合</param>
/// <returns>异步操作任务</returns>
public async Task SaveAllAsync(
IEnumerable<(IDataLocation location, IData data)> dataList)
{
@ -120,6 +140,24 @@ public class UnifiedSettingsRepository(
this.SendEvent(new DataBatchSavedEvent(valueTuples.ToList()));
}
/// <summary>
/// 加载所有存储的数据项
/// </summary>
/// <returns>包含所有数据项的字典,键为数据位置键,值为数据对象</returns>
public async Task<IDictionary<string, IData>> LoadAllAsync()
{
await EnsureLoadedAsync();
return File.Sections.ToDictionary(
kv => kv.Key,
kv => Serializer.Deserialize<IData>(kv.Value)
);
}
protected override void OnInit()
{
_storage ??= this.GetUtility<IStorage>()!;
_serializer ??= this.GetUtility<IRuntimeTypeSerializer>()!;
}
// =========================
// Internals
@ -154,7 +192,6 @@ public class UnifiedSettingsRepository(
}
}
/// <summary>
/// 将缓存中的所有数据保存到统一文件
/// </summary>

View File

@ -14,38 +14,30 @@ namespace GFramework.Game.setting;
/// - 编排 Settings Applicator 的 Apply 行为
/// </summary>
public class SettingsModel<TRepository> : AbstractModel, ISettingsModel
where TRepository : class, IDataRepository
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 ConcurrentBag<IResetApplyAbleSettings> _applicators = new();
private readonly ConcurrentDictionary<(Type type, int from), ISettingsMigration> _migrations = new();
private readonly ConcurrentDictionary<Type, Dictionary<int, ISettingsMigration>> _migrationCache = new();
private IDataRepository? _repository;
private readonly ConcurrentDictionary<(Type type, int from), ISettingsMigration> _migrations = new();
private IDataLocationProvider? _locationProvider;
private IDataRepository Repository =>
_repository ?? throw new InvalidOperationException("IDataRepository not initialized.");
private ISettingsDataRepository? _repository;
private ISettingsDataRepository DataRepository =>
_repository ?? throw new InvalidOperationException("ISettingsDataRepository not initialized.");
private IDataLocationProvider LocationProvider =>
_locationProvider ?? throw new InvalidOperationException("IDataLocationProvider not initialized.");
// =========================
// Init
// =========================
protected override void OnInit()
{
_repository ??= this.GetUtility<TRepository>()!;
_locationProvider ??= this.GetUtility<IDataLocationProvider>()!;
}
// =========================
// Data access
// =========================
@ -57,7 +49,7 @@ public class SettingsModel<TRepository> : AbstractModel, ISettingsModel
return (T)_data.GetOrAdd(typeof(T), _ => new T());
}
public IEnumerable<ISettingsData> AllData()
{
return _data.Values;
@ -70,9 +62,10 @@ public class SettingsModel<TRepository> : AbstractModel, ISettingsModel
/// <summary>
/// 注册设置应用器
/// </summary>
public ISettingsModel RegisterApplicator(IResetApplyAbleSettings applicator)
public ISettingsModel RegisterApplicator<T>(IResetApplyAbleSettings applicator)
where T : class, ISettingsData, new()
{
_applicators.Add(applicator);
_applicators[typeof(T)] = applicator;
return this;
}
@ -81,7 +74,7 @@ public class SettingsModel<TRepository> : AbstractModel, ISettingsModel
/// </summary>
public IEnumerable<IResetApplyAbleSettings> AllApplicators()
{
return _applicators;
return _applicators.Values;
}
// =========================
@ -94,6 +87,135 @@ public class SettingsModel<TRepository> : AbstractModel, ISettingsModel
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.LoadFrom(migrated);
}
catch (Exception ex)
{
Log.Error($"Failed to initialize settings data: {data.GetType().Name}", ex);
}
}
}
/// <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);
}
}
}
/// <summary>
/// 应用所有设置
/// </summary>
public async Task ApplyAllAsync()
{
foreach (var applicator in _applicators)
{
try
{
await applicator.Value.Apply();
}
catch (Exception ex)
{
Log.Error($"Failed to apply settings: {applicator.GetType().Name}", ex);
}
}
}
/// <summary>
/// 重置指定类型的可重置对象
/// </summary>
/// <typeparam name="T">要重置的对象类型必须是class类型实现IResettable接口并具有无参构造函数</typeparam>
public void Reset<T>() where T : class, ISettingsData, new()
{
var data = GetData<T>();
data.Reset();
}
/// <summary>
/// 重置所有设置
/// </summary>
public void ResetAll()
{
foreach (var data in _data.Values)
data.Reset();
foreach (var applicator in _applicators)
applicator.Value.Reset();
}
/// <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;
}
// =========================
// Init
// =========================
protected override void OnInit()
{
_repository ??= this.GetUtility<TRepository>()!;
_locationProvider ??= this.GetUtility<IDataLocationProvider>()!;
}
private ISettingsData MigrateIfNeeded(ISettingsData data)
{
if (data is not IVersionedData versioned)
@ -119,88 +241,4 @@ public class SettingsModel<TRepository> : AbstractModel, ISettingsModel
return current;
}
// =========================
// Lifecycle
// =========================
/// <summary>
/// 初始化设置模型:
/// - 加载所有已存在的 Settings Data
/// - 执行必要的迁移
/// </summary>
public async Task InitializeAsync()
{
foreach (var data in _data.Values)
{
try
{
var type = data.GetType();
var location = LocationProvider.GetLocation(type);
if (!await Repository.ExistsAsync(location))
continue;
var loaded = await Repository.LoadAsync<ISettingsData>(location);
var migrated = MigrateIfNeeded(loaded);
// 回填数据(不替换实例)
data.LoadFrom(migrated);
}
catch (Exception ex)
{
Log.Error($"Failed to initialize settings data: {data.GetType().Name}", ex);
}
}
}
/// <summary>
/// 将所有 Settings Data 持久化
/// </summary>
public async Task SaveAllAsync()
{
foreach (var data in _data.Values)
{
try
{
var location = LocationProvider.GetLocation(data.GetType());
await Repository.SaveAsync(location, data);
}
catch (Exception ex)
{
Log.Error($"Failed to save settings data: {data.GetType().Name}", ex);
}
}
}
/// <summary>
/// 应用所有设置
/// </summary>
public async Task ApplyAllAsync()
{
foreach (var applicator in _applicators)
{
try
{
await applicator.Apply();
}
catch (Exception ex)
{
Log.Error($"Failed to apply settings: {applicator.GetType().Name}", ex);
}
}
}
/// <summary>
/// 重置所有设置
/// </summary>
public void ResetAll()
{
foreach (var data in _data.Values)
data.Reset();
foreach (var applicator in _applicators)
applicator.Reset();
}
}

View File

@ -1,6 +1,5 @@
using GFramework.Core.extensions;
using GFramework.Core.system;
using GFramework.Game.Abstractions.data;
using GFramework.Game.Abstractions.setting;
using GFramework.Game.setting.events;
@ -9,12 +8,9 @@ namespace GFramework.Game.setting;
/// <summary>
/// 设置系统,负责管理和应用各种设置配置
/// </summary>
public class SettingsSystem<TRepository>(IDataRepository? repository)
: AbstractSystem, ISettingsSystem where TRepository : class, IDataRepository
public class SettingsSystem : AbstractSystem, ISettingsSystem
{
private ISettingsModel _model = null!;
private IDataRepository? _repository = repository;
private IDataRepository Repository => _repository ?? throw new InvalidOperationException("Repository is not set");
/// <summary>
/// 应用所有设置配置
@ -29,9 +25,9 @@ public class SettingsSystem<TRepository>(IDataRepository? repository)
/// <summary>
/// 应用指定类型的设置配置
/// </summary>
/// <typeparam name="T">设置配置类型必须是类且实现ISettingsSection接口</typeparam>
/// <typeparam name="T">设置配置类型必须是类且实现IResetApplyAbleSettings接口</typeparam>
/// <returns>完成的任务</returns>
public Task Apply<T>() where T : class, IApplyAbleSettings
public Task Apply<T>() where T : class, IResetApplyAbleSettings
{
var applicator = _model.GetApplicator<T>();
return applicator != null
@ -45,7 +41,7 @@ public class SettingsSystem<TRepository>(IDataRepository? repository)
/// <returns>完成的任务</returns>
public async Task SaveAll()
{
await Repository.SaveAllAsync(_model.AllData());
await _model.SaveAllAsync();
}
/// <summary>
@ -63,7 +59,7 @@ public class SettingsSystem<TRepository>(IDataRepository? repository)
/// </summary>
/// <typeparam name="T">设置类型必须实现IPersistentApplyAbleSettings接口且具有无参构造函数</typeparam>
/// <returns>异步任务</returns>
public async Task Reset<T>() where T : class, IResetApplyAbleSettings, new()
public async Task Reset<T>() where T : class, ISettingsData, IResetApplyAbleSettings, new()
{
_model.Reset<T>();
await Apply<T>();
@ -76,7 +72,6 @@ public class SettingsSystem<TRepository>(IDataRepository? repository)
protected override void OnInit()
{
_model = this.GetModel<ISettingsModel>()!;
_repository ??= this.GetUtility<TRepository>()!;
}
/// <summary>