refactor(settings): 重构设置系统和数据仓库实现

- 将音频和图形设置从 IResettable, IVersioned 迁移到 ISettingsData 接口
- 添加数据位置接口 IDataLocation 和数据位置提供者接口 IDataLocationProvider
- 修改数据仓库实现,使用数据位置替代类型进行数据操作
- 更新数据仓库的加载、保存、删除和存在检查方法以使用数据位置参数
- 重命名 IPersistentApplyAbleSettings 为 IResetApplyAbleSettings 并更新其实现
- 创建 ISettingsData 接口整合设置数据的基础功能
- 更新设置模型实现,统一管理设置数据的生命周期和应用器
- 添加版本化数据接口 IVersionedData 和可从源加载接口 ILoadableFrom
- 实现数据位置到存储键的扩展方法
- 更新数据事件类型以使用数据位置信息
- 重构设置模型的数据加载、保存和应用逻辑
- [skip ci]
This commit is contained in:
GeWuYou 2026-01-30 16:48:09 +08:00
parent 49d210b9ad
commit 970b8d3b96
27 changed files with 634 additions and 483 deletions

View File

@ -0,0 +1,19 @@
// Copyright (c) 2025 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.Core.Abstractions.data;
public interface ILoadableFrom<in T>
{
void LoadFrom(T source);
}

View File

@ -15,17 +15,4 @@ global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2_0
using System.ComponentModel;
namespace System.Runtime.CompilerServices;
/// <summary>
/// 用于标记仅初始化 setter 的特殊类型
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public static class IsExternalInit
{
}
#endif
global using System.Threading.Tasks;

View File

@ -0,0 +1,42 @@
// Copyright (c) 2025 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 GFramework.Game.Abstractions.enums;
namespace GFramework.Game.Abstractions.data;
/// <summary>
/// 数据位置接口,定义了数据存储的位置信息和相关属性
/// </summary>
public interface IDataLocation
{
/// <summary>
/// 存储键(文件路径 / redis key / db key
/// </summary>
string Key { get; }
/// <summary>
/// 存储类型Local / Remote / Database / Memory
/// </summary>
StorageKind Kind { get; }
/// <summary>
/// 命名空间/分区
/// </summary>
string? Namespace { get; }
/// <summary>
/// 扩展元数据(用于存储额外信息,如压缩、加密等)
/// </summary>
IReadOnlyDictionary<string, string>? Metadata { get; }
}

View File

@ -0,0 +1,29 @@
// Copyright (c) 2025 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 GFramework.Core.Abstractions.utility;
namespace GFramework.Game.Abstractions.data;
/// <summary>
/// 定义数据位置提供者的接口,用于获取指定类型的数据位置信息
/// </summary>
public interface IDataLocationProvider:IUtility
{
/// <summary>
/// 获取指定类型的数据位置
/// </summary>
/// <param name="type">需要获取位置信息的类型</param>
/// <returns>与指定类型关联的数据位置对象</returns>
IDataLocation GetLocation(Type type);
}

View File

@ -21,45 +21,42 @@ namespace GFramework.Game.Abstractions.data;
public interface IDataRepository : IUtility
{
/// <summary>
/// 异步加载指定类型的数据对象
/// 异步加载指定位置的数据
/// </summary>
/// <typeparam name="T">要加载的数据类型必须实现IData接口并具有无参构造函数</typeparam>
/// <returns>返回加载的数据对象的Task</returns>
Task<T> LoadAsync<T>() where T : class, IData, new();
/// <param name="location">数据位置信息</param>
/// <returns>返回加载的数据对象</returns>
Task<T> LoadAsync<T>(IDataLocation location)
where T : class, IData, new();
/// <summary>
/// 根据类型异步加载数据
/// </summary>
/// <param name="type">要加载的数据类型</param>
/// <returns>异步操作任务返回实现IData接口的数据对象</returns>
Task<IData> LoadAsync(Type type);
/// <summary>
/// 异步保存指定的数据对象
/// 异步保存数据到指定位置
/// </summary>
/// <typeparam name="T">要保存的数据类型必须实现IData接口</typeparam>
/// <param name="location">数据位置信息</param>
/// <param name="data">要保存的数据对象</param>
/// <returns>表示异步保存操作的Task</returns>
Task SaveAsync<T>(T data) where T : class, IData;
/// <returns>返回异步操作任务</returns>
Task SaveAsync<T>(IDataLocation location, T data)
where T : class, IData;
/// <summary>
/// 异步检查指定类型的数据是否存在
/// 异步检查指定位置是否存在数据
/// </summary>
/// <typeparam name="T">要检查的数据类型必须实现IData接口</typeparam>
/// <returns>返回表示数据是否存在布尔值的Task</returns>
Task<bool> ExistsAsync<T>() where T : class, IData;
/// <param name="location">数据位置信息</param>
/// <returns>返回布尔值,表示数据是否存在</returns>
Task<bool> ExistsAsync(IDataLocation location);
/// <summary>
/// 异步删除指定类型的数据
/// 异步删除指定位置的数据
/// </summary>
/// <typeparam name="T">要删除的数据类型必须实现IData接口</typeparam>
/// <returns>表示异步删除操作的Task</returns>
Task DeleteAsync<T>() where T : class, IData;
/// <param name="location">数据位置信息</param>
/// <returns>返回异步操作任务</returns>
Task DeleteAsync(IDataLocation location);
/// <summary>
/// 批量保存多个数据
/// 异步批量保存多个数据项到各自的位置
/// </summary>
/// <param name="dataList">要保存的数据列表实现IData接口的对象集合</param>
/// <returns>异步操作任务</returns>
Task SaveAllAsync(IEnumerable<IData> dataList);
/// <param name="dataList">包含数据位置和对应数据对象的可枚举集合</param>
/// <returns>返回异步操作任务</returns>
Task SaveAllAsync(IEnumerable<(IDataLocation location, IData data)> dataList);
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2025 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>
/// 版本化数据接口继承自IData接口
/// 提供版本控制和修改时间跟踪功能
/// </summary>
public interface IVersionedData : IData
{
/// <summary>
/// 获取数据的版本号
/// </summary>
/// <returns>当前数据的版本号,用于标识数据的版本状态</returns>
int Version { get; }
/// <summary>
/// 获取数据最后修改的时间
/// </summary>
/// <returns>DateTime类型的最后修改时间戳</returns>
DateTime LastModified { get; }
}

View File

@ -16,5 +16,5 @@ namespace GFramework.Game.Abstractions.data.events;
/// <summary>
/// 表示数据批次保存事件的记录类型
/// </summary>
/// <param name="List">包含已保存数据项的集合实现了IData接口</param>
public sealed record DataBatchSavedEvent(ICollection<IData> List);
/// <param name="DataList">包含已保存数据项的集合实现了IData接口</param>
public sealed record DataBatchSavedEvent(ICollection<(IDataLocation location, IData data)> DataList);

View File

@ -16,5 +16,6 @@ namespace GFramework.Game.Abstractions.data.events;
/// <summary>
/// 表示数据删除事件的记录类型
/// </summary>
/// <param name="Type">被删除数据的类型</param>
public sealed record DataDeletedEvent(Type Type);
/// <param name="Location">数据位置信息,标识被删除数据的位置</param>
public sealed record DataDeletedEvent(IDataLocation Location);

View File

@ -0,0 +1,47 @@
// Copyright (c) 2025 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.enums;
/// <summary>
/// 存储类型枚举,用于标识不同的存储方式
/// 此枚举使用 Flags 特性,支持位运算组合多个存储类型
/// </summary>
[Flags]
public enum StorageKind
{
/// <summary>
/// 无存储类型
/// </summary>
None = 0,
/// <summary>
/// 本地文件系统存储
/// </summary>
Local = 1 << 0,
/// <summary>
/// 内存存储
/// </summary>
Memory = 1 << 1,
/// <summary>
/// 远程存储
/// </summary>
Remote = 1 << 2,
/// <summary>
/// 数据库存储
/// </summary>
Database = 1 << 3,
}

View File

@ -2,10 +2,16 @@
// This type is required to support init-only setters and record types
// when targeting netstandard2.0 or older frameworks.
#pragma warning disable S2094 // Remove this empty class
namespace GFramework.Game.Abstractions.internals;
#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2_0
using System.ComponentModel;
internal static class IsExternalInit
namespace System.Runtime.CompilerServices;
/// <summary>
/// 用于标记仅初始化 setter 的特殊类型
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public static class IsExternalInit
{
}
#pragma warning restore S2094
#endif

View File

@ -14,7 +14,7 @@
namespace GFramework.Game.Abstractions.setting;
/// <summary>
/// 可持久化的应用设置接口
/// 同时具备数据持久化和应用逻辑能力
/// 定义一个可重置且可应用设置的接口
/// 该接口继承自IResettable和IApplyAbleSettings接口组合了重置功能和应用设置功能
/// </summary>
public interface IPersistentApplyAbleSettings : IResettable, IApplyAbleSettings;
public interface IResetApplyAbleSettings : IResettable, IApplyAbleSettings;

View File

@ -0,0 +1,23 @@
// Copyright (c) 2025 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 GFramework.Core.Abstractions.data;
using GFramework.Game.Abstractions.data;
namespace GFramework.Game.Abstractions.setting;
/// <summary>
/// 定义游戏设置数据的接口
/// 该接口继承自IData和IResettable接口提供数据管理和重置功能
/// </summary>
public interface ISettingsData : IResettable, IVersionedData, ILoadableFrom<ISettingsData>;

View File

@ -3,75 +3,85 @@
namespace GFramework.Game.Abstractions.setting;
/// <summary>
/// 定义设置模型的接口,提供获取特定类型设置节的功能
/// 设置模型接口:
/// - 管理 Settings Data 的生命周期
/// - 管理并编排 Settings Applicator
/// - 管理 Settings Migration
/// </summary>
public interface ISettingsModel : IModel
{
// =========================
// Data
// =========================
/// <summary>
/// 获取或创建数据设置(自动创建)
/// 获取指定类型的设置数据(唯一实例
/// </summary>
/// <typeparam name="T">设置数据的类型必须继承自class、ISettingsData且具有无参构造函数</typeparam>
/// <typeparam name="T">设置数据类型必须继承自ISettingsData并具有无参构造函数</typeparam>
/// <returns>指定类型的设置数据实例</returns>
T GetData<T>() where T : class, IResettable, new();
T GetData<T>() where T : class, ISettingsData, new();
/// <summary>
/// 尝试获取指定类型的设置节实例
/// 获取所有已创建的设置数据
/// </summary>
/// <param name="type">要获取的设置节类型</param>
/// <param name="section">输出参数如果成功则包含找到的设置节实例否则为null</param>
/// <returns>如果找到指定类型的设置节则返回true否则返回false</returns>
bool TryGet(Type type, out ISettingsSection section);
/// <returns>所有已创建的设置数据集合</returns>
IEnumerable<ISettingsData> AllData();
// =========================
// Applicator
// =========================
/// <summary>
/// 获取已注册的可应用设置
/// 注册设置应用器
/// </summary>
/// <typeparam name="T">可应用设置的类型必须继承自class和IApplyAbleSettings</typeparam>
/// <returns>指定类型的可应用设置实例如果不存在则返回null</returns>
T? GetApplicator<T>() where T : class, IApplyAbleSettings;
/// <param name="applicator">要注册的设置应用器</param>
/// <returns>当前设置模型实例,支持链式调用</returns>
ISettingsModel RegisterApplicator(IResetApplyAbleSettings applicator);
/// <summary>
/// 获取所有设置数据的集合
/// 获取所有设置应用器
/// </summary>
/// <returns>包含所有设置数据的可枚举集合</returns>
IEnumerable<IResettable> AllData();
/// <returns>所有设置应用器的集合</returns>
IEnumerable<IResetApplyAbleSettings> AllApplicators();
// =========================
// Migration
// =========================
/// <summary>
/// 获取所有可应用设置的集合
/// 注册设置迁移器
/// </summary>
/// <returns>包含所有可应用设置的可枚举集合</returns>
IEnumerable<IApplyAbleSettings> AllApplicators();
/// <summary>
/// 注册可应用设置(必须手动注册)
/// </summary>
/// <typeparam name="T">可应用设置的类型必须继承自class和IApplyAbleSettings</typeparam>
/// <param name="applicator">要注册的可应用设置实例</param>
/// <returns>返回当前设置模型实例,支持链式调用</returns>
ISettingsModel RegisterApplicator<T>(T applicator) where T : class, IApplyAbleSettings;
/// <summary>
/// 注册设置迁移器
/// </summary>
/// <param name="migration">要注册的设置迁移实例</param>
/// <returns>返回当前设置模型实例,支持链式调用</returns>
/// <param name="migration">要注册的设置迁移器</param>
/// <returns>当前设置模型实例,支持链式调用</returns>
ISettingsModel RegisterMigration(ISettingsMigration migration);
// =========================
// Lifecycle
// =========================
/// <summary>
/// 异步初始化指定类型的设置
/// 初始化所有设置数据(加载 + 迁移)
/// </summary>
/// <param name="settingTypes">要初始化的设置类型数组</param>
/// <returns>异步操作任务</returns>
Task InitializeAsync(params Type[] settingTypes);
Task InitializeAsync();
/// <summary>
/// 重置指定类型的设置
/// 保存所有设置数据
/// </summary>
/// <typeparam name="T">要重置的设置类型必须实现IResettable接口并具有无参构造函数</typeparam>
void Reset<T>() where T : class, IResettable, new();
/// <returns>异步操作任务</returns>
Task SaveAllAsync();
/// <summary>
/// 重置所有设置
/// 应用所有设置
/// </summary>
/// <returns>异步操作任务</returns>
Task ApplyAllAsync();
/// <summary>
/// 重置所有设置数据与应用器
/// </summary>
void ResetAll();
}
}

View File

@ -1,9 +1,7 @@
using GFramework.Game.Abstractions.data;
namespace GFramework.Game.Abstractions.setting;
namespace GFramework.Game.Abstractions.setting;
/// <summary>
/// 表示游戏设置的一个配置节接口
/// 该接口定义了设置配置节的基本契约,用于管理游戏中的各种配置选项
/// </summary>
public interface ISettingsSection : IData;
public interface ISettingsSection;

View File

@ -31,7 +31,7 @@ public interface ISettingsSystem : ISystem
/// </summary>
/// <typeparam name="T">设置类型必须继承自class并实现IPersistentApplyAbleSettings接口</typeparam>
/// <returns>表示异步操作的任务</returns>
Task Reset<T>() where T : class, IPersistentApplyAbleSettings, new();
Task Reset<T>() where T : class, IResetApplyAbleSettings, new();
/// <summary>
/// 重置所有设置

View File

@ -1,11 +1,9 @@
using GFramework.Core.Abstractions.versioning;
namespace GFramework.Game.Abstractions.setting.data;
/// <summary>
/// 音频设置类,用于管理游戏中的音频配置
/// </summary>
public class AudioSettings : IResettable, IVersioned
public class AudioSettings : ISettingsData
{
/// <summary>
/// 获取或设置主音量,控制所有音频的总体音量
@ -33,5 +31,13 @@ public class AudioSettings : IResettable, IVersioned
SfxVolume = 0.8f;
}
/// <summary>
/// 获取或设置设置数据的版本号
/// </summary>
public int Version { get; set; } = 1;
}
/// <summary>
/// 获取设置数据最后修改的时间
/// </summary>
public DateTime LastModified { get; } = DateTime.Now;
}

View File

@ -1,11 +1,9 @@
using GFramework.Core.Abstractions.versioning;
namespace GFramework.Game.Abstractions.setting.data;
/// <summary>
/// 图形设置类,用于管理游戏的图形相关配置
/// </summary>
public class GraphicsSettings : IResettable, IVersioned
public class GraphicsSettings : ISettingsData
{
/// <summary>
/// 获取或设置是否启用全屏模式
@ -32,5 +30,13 @@ public class GraphicsSettings : IResettable, IVersioned
ResolutionHeight = 1080;
}
/// <summary>
/// 获取或设置设置数据的版本号
/// </summary>
public int Version { get; set; } = 1;
/// <summary>
/// 获取设置数据最后修改的时间
/// </summary>
public DateTime LastModified { get; } = DateTime.Now;
}

View File

@ -11,15 +11,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Core.Abstractions.versioning;
namespace GFramework.Game.Abstractions.setting.data;
/// <summary>
/// 本地化设置类,用于管理游戏的语言本地化配置
/// 实现了ISettingsData接口提供设置数据功能实现IVersioned接口提供版本控制功能
/// </summary>
public class LocalizationSettings : IResettable, IVersioned
public class LocalizationSettings : ISettingsData
{
/// <summary>
/// 获取或设置当前使用的语言
@ -39,6 +37,10 @@ public class LocalizationSettings : IResettable, IVersioned
/// <summary>
/// 获取或设置设置数据的版本号
/// </summary>
/// <value>默认版本号为1</value>
public int Version { get; set; } = 1;
/// <summary>
/// 获取设置数据最后修改的时间
/// </summary>
public DateTime LastModified { get; } = DateTime.Now;
}

View File

@ -16,6 +16,7 @@ using GFramework.Core.extensions;
using GFramework.Core.utility;
using GFramework.Game.Abstractions.data;
using GFramework.Game.Abstractions.data.events;
using GFramework.Game.extensions;
namespace GFramework.Game.data;
@ -34,14 +35,21 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
throw new InvalidOperationException(
"Failed to initialize storage. No IStorage utility found in context.");
/// <summary>
/// 异步加载指定类型的数据
/// </summary>
/// <typeparam name="T">要加载的数据类型必须实现IData接口</typeparam>
/// <returns>加载的数据对象</returns>
public async Task<T> LoadAsync<T>() where T : class, IData, new()
protected override void OnInit()
{
var key = GetKey<T>();
_storage ??= this.GetUtility<IStorage>()!;
}
/// <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()
{
var key = location.ToStorageKey();
T result;
// 检查存储中是否存在指定键的数据
@ -58,43 +66,15 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
}
/// <summary>
/// 异步加载指定类型的数据通过Type参数
/// 异步保存数据到指定位置
/// </summary>
/// <param name="type">要加载的数据类型</param>
/// <returns>加载的数据对象</returns>
public async Task<IData> LoadAsync(Type type)
{
if (!typeof(IData).IsAssignableFrom(type))
throw new ArgumentException($"{type.Name} does not implement IData");
if (!type.IsClass || type.GetConstructor(Type.EmptyTypes) == null)
throw new ArgumentException($"{type.Name} must be a class with parameterless constructor");
var key = GetKey(type);
IData result;
// 检查存储中是否存在指定键的数据
if (await Storage.ExistsAsync(key))
result = await Storage.ReadAsync<IData>(key);
else
result = (IData)Activator.CreateInstance(type)!;
// 如果启用事件功能,则发送数据加载完成事件
if (_options.EnableEvents)
this.SendEvent(new DataLoadedEvent<IData>(result));
return result;
}
/// <summary>
/// 异步保存指定类型的数据
/// </summary>
/// <typeparam name="T">要保存的数据类型</typeparam>
/// <typeparam name="T">数据类型必须实现IData接口</typeparam>
/// <param name="location">数据位置信息</param>
/// <param name="data">要保存的数据对象</param>
public async Task SaveAsync<T>(T data) where T : class, IData
public async Task SaveAsync<T>(IDataLocation location, T data)
where T : class, IData
{
var key = GetKey<T>();
var key = location.ToStorageKey();
// 自动备份
if (_options.AutoBackup && await Storage.ExistsAsync(key))
@ -111,70 +91,38 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
}
/// <summary>
/// 检查指定类型的数据是否存在
/// 检查指定位置的数据是否存在
/// </summary>
/// <typeparam name="T">要检查的数据类型</typeparam>
/// <param name="location">数据位置信息</param>
/// <returns>如果数据存在返回true否则返回false</returns>
public async Task<bool> ExistsAsync<T>() where T : class, IData
{
var key = GetKey<T>();
return await Storage.ExistsAsync(key);
}
public Task<bool> ExistsAsync(IDataLocation location)
=> Storage.ExistsAsync(location.ToStorageKey());
/// <summary>
/// 异步删除指定类型的数据
/// 异步删除指定位置的数据
/// </summary>
/// <typeparam name="T">要删除的数据类型</typeparam>
public async Task DeleteAsync<T>() where T : class, IData
/// <param name="location">数据位置信息</param>
public async Task DeleteAsync(IDataLocation location)
{
var key = GetKey<T>();
var key = location.ToStorageKey();
await Storage.DeleteAsync(key);
if (_options.EnableEvents)
this.SendEvent(new DataDeletedEvent(typeof(T)));
this.SendEvent(new DataDeletedEvent(location));
}
/// <summary>
/// 批量异步保存多个数据对象
/// 异步批量保存多个数据项
/// </summary>
/// <param name="dataList">要保存的数据对象集合</param>
public async Task SaveAllAsync(IEnumerable<IData> dataList)
/// <param name="dataList">包含数据位置和数据对象的枚举集合</param>
public async Task SaveAllAsync(IEnumerable<(IDataLocation location, IData data)> dataList)
{
var list = dataList.ToList();
foreach (var data in list)
var valueTuples = dataList.ToList();
foreach (var (location, data) in valueTuples)
{
var type = data.GetType();
var key = GetKey(type);
await Storage.WriteAsync(key, data);
await SaveAsync(location, data);
}
if (_options.EnableEvents)
this.SendEvent(new DataBatchSavedEvent(list));
this.SendEvent(new DataBatchSavedEvent(valueTuples));
}
protected override void OnInit()
{
_storage ??= this.GetUtility<IStorage>()!;
}
/// <summary>
/// 根据类型生成存储键
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <returns>生成的存储键</returns>
protected virtual string GetKey<T>() where T : IData
{
return GetKey(typeof(T));
}
/// <summary>
/// 根据类型生成存储键
/// </summary>
/// <param name="type">数据类型</param>
/// <returns>生成的存储键</returns>
protected virtual string GetKey(Type type)
{
var fileName = type.FullName!;
return string.IsNullOrEmpty(_options.BasePath) ? fileName : $"{_options.BasePath}/{fileName}";
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) 2025 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 GFramework.Core.Abstractions.versioning;
namespace GFramework.Game.data;
/// <summary>
/// 统一设置文件类,用于管理应用程序的配置设置
/// 实现了版本控制接口,支持配置文件的版本管理
/// </summary>
internal sealed class UnifiedSettingsFile:IVersioned
{
/// <summary>
/// 配置节集合,存储不同类型的配置数据
/// 键为配置节名称,值为配置对象
/// </summary>
public Dictionary<string, string> Sections { get; set; } = new();
/// <summary>
/// 配置文件版本号,用于版本控制和兼容性检查
/// </summary>
public int Version { get; set; }
}

View File

@ -30,7 +30,7 @@ public class UnifiedSettingsRepository(
string fileName = "settings.json")
: AbstractContextUtility, IDataRepository
{
private readonly Dictionary<string, string> _cache = new();
private UnifiedSettingsFile? _file;
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions();
private bool _loaded;
@ -43,129 +43,83 @@ public class UnifiedSettingsRepository(
private IRuntimeTypeSerializer Serializer =>
_serializer ?? throw new InvalidOperationException("ISerializer not initialized.");
// =========================
// IDataRepository
// =========================
/// <summary>
/// 异步加载指定类型的数据
/// </summary>
/// <typeparam name="T">要加载的数据类型必须继承自IData接口并具有无参构造函数</typeparam>
/// <returns>加载的数据实例</returns>
public async Task<T> LoadAsync<T>() where T : class, IData, new()
{
await EnsureLoadedAsync();
var key = GetTypeKey(typeof(T));
var result = _cache.TryGetValue(key, out var json) ? Serializer.Deserialize<T>(json) : new T();
if (_options.EnableEvents)
this.SendEvent(new DataLoadedEvent<T>(result));
return result;
}
/// <summary>
/// 异步加载指定类型的数据通过Type参数
/// </summary>
/// <param name="type">要加载的数据类型</param>
/// <returns>加载的数据实例</returns>
/// <exception cref="ArgumentException">当类型不符合要求时抛出异常</exception>
public async Task<IData> LoadAsync(Type type)
{
if (!typeof(IData).IsAssignableFrom(type))
throw new ArgumentException($"{type.Name} does not implement IData");
if (!type.IsClass || type.GetConstructor(Type.EmptyTypes) == null)
throw new ArgumentException($"{type.Name} must have parameterless ctor");
await EnsureLoadedAsync();
var key = GetTypeKey(type);
IData result;
if (_cache.TryGetValue(key, out var json))
result = (IData)Serializer.Deserialize(json, type);
else
result = (IData)Activator.CreateInstance(type)!;
if (_options.EnableEvents)
this.SendEvent(new DataLoadedEvent<IData>(result));
return result;
}
/// <summary>
/// 异步保存数据到存储
/// </summary>
/// <typeparam name="T">要保存的数据类型</typeparam>
/// <param name="data">要保存的数据实例</param>
public async Task SaveAsync<T>(T data) where T : class, IData
{
await EnsureLoadedAsync();
var key = GetTypeKey(typeof(T));
_cache[key] = Serializer.Serialize(data);
await SaveUnifiedFileAsync();
if (_options.EnableEvents)
this.SendEvent(new DataSavedEvent<T>(data));
}
/// <summary>
/// 异步批量保存多个数据实例
/// </summary>
/// <param name="dataList">要保存的数据实例集合</param>
public async Task SaveAllAsync(IEnumerable<IData> dataList)
{
await EnsureLoadedAsync();
var list = dataList.ToList();
foreach (var data in list)
{
var key = GetTypeKey(data.GetType());
_cache[key] = Serializer.Serialize(data);
}
await SaveUnifiedFileAsync();
if (_options.EnableEvents)
this.SendEvent(new DataBatchSavedEvent(list));
}
/// <summary>
/// 检查指定类型的数据是否存在
/// </summary>
/// <typeparam name="T">要检查的数据类型</typeparam>
/// <returns>如果存在返回true否则返回false</returns>
public async Task<bool> ExistsAsync<T>() where T : class, IData
{
await EnsureLoadedAsync();
return _cache.ContainsKey(GetTypeKey(typeof(T)));
}
/// <summary>
/// 删除指定类型的数据
/// </summary>
/// <typeparam name="T">要删除的数据类型</typeparam>
public async Task DeleteAsync<T>() where T : class, IData
{
await EnsureLoadedAsync();
_cache.Remove(GetTypeKey(typeof(T)));
await SaveUnifiedFileAsync();
if (_options.EnableEvents)
this.SendEvent(new DataDeletedEvent(typeof(T)));
}
private UnifiedSettingsFile File =>
_file ?? throw new InvalidOperationException("UnifiedSettingsFile not set.");
protected override void OnInit()
{
_storage ??= this.GetUtility<IStorage>()!;
_serializer ??= this.GetUtility<IRuntimeTypeSerializer>()!;
}
// =========================
// IDataRepository
// =========================
public async Task<T> LoadAsync<T>(IDataLocation location)
where T : class, IData, new()
{
await EnsureLoadedAsync();
var key = location.Key;
var result = _file!.Sections.TryGetValue(key, out var raw) ? Serializer.Deserialize<T>(raw) : new T();
if (_options.EnableEvents)
this.SendEvent(new DataLoadedEvent<IData>(result));
return result;
}
public async Task SaveAsync<T>(IDataLocation location, T data)
where T : class, IData
{
await EnsureLoadedAsync();
var key = location.Key;
var serialized = Serializer.Serialize(data);
_file!.Sections[key] = serialized;
await Storage.WriteAsync(fileName, _file);
if (_options.EnableEvents)
this.SendEvent(new DataSavedEvent<T>(data));
}
public async Task<bool> ExistsAsync(IDataLocation location)
{
await EnsureLoadedAsync();
return File.Sections.ContainsKey(location.Key);
}
public async Task DeleteAsync(IDataLocation location)
{
await EnsureLoadedAsync();
if (File.Sections.Remove(location.Key))
{
await SaveUnifiedFileAsync();
if (_options.EnableEvents)
this.SendEvent(new DataDeletedEvent(location));
}
}
public async Task SaveAllAsync(
IEnumerable<(IDataLocation location, IData data)> dataList)
{
await EnsureLoadedAsync();
var valueTuples = dataList.ToList();
foreach (var (location, data) in valueTuples)
{
var serialized = Serializer.Serialize(data);
File.Sections[location.Key] = serialized;
}
await SaveUnifiedFileAsync();
if (_options.EnableEvents)
this.SendEvent(new DataBatchSavedEvent(valueTuples.ToList()));
}
// =========================
// Internals
@ -183,12 +137,13 @@ public class UnifiedSettingsRepository(
{
if (_loaded) return;
if (await Storage.ExistsAsync(GetUnifiedKey()))
if (await Storage.ExistsAsync(fileName))
{
var data = await Storage.ReadAsync<Dictionary<string, string>>(GetUnifiedKey());
_cache.Clear();
foreach (var (k, v) in data)
_cache[k] = v;
_file = await Storage.ReadAsync<UnifiedSettingsFile>(fileName);
}
else
{
_file = new UnifiedSettingsFile { Version = 1 };
}
_loaded = true;
@ -199,6 +154,7 @@ public class UnifiedSettingsRepository(
}
}
/// <summary>
/// 将缓存中的所有数据保存到统一文件
/// </summary>
@ -207,7 +163,7 @@ public class UnifiedSettingsRepository(
await _lock.WaitAsync();
try
{
await Storage.WriteAsync(GetUnifiedKey(), _cache);
await Storage.WriteAsync(GetUnifiedKey(), File);
}
finally
{
@ -223,14 +179,4 @@ public class UnifiedSettingsRepository(
{
return string.IsNullOrEmpty(_options.BasePath) ? fileName : $"{_options.BasePath}/{fileName}";
}
/// <summary>
/// 获取类型的唯一标识键
/// </summary>
/// <param name="type">要获取键的类型</param>
/// <returns>类型的全名作为键</returns>
protected virtual string GetTypeKey(Type type)
{
return type.FullName!;
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2025 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 GFramework.Game.Abstractions.data;
namespace GFramework.Game.extensions;
/// <summary>
/// 提供数据位置相关的扩展方法
/// </summary>
public static class DataLocationExtensions
{
/// <summary>
/// 将数据位置转换为存储键
/// </summary>
/// <param name="location">数据位置对象</param>
/// <returns>格式化的存储键字符串,如果命名空间为空则返回键值,否则返回"命名空间/键值"格式</returns>
public static string ToStorageKey(this IDataLocation location)
{
return string.IsNullOrEmpty(location.Namespace) ? location.Key : $"{location.Namespace}/{location.Key}";
}
}

View File

@ -1,6 +1,5 @@
using System.Collections.Concurrent;
using GFramework.Core.Abstractions.logging;
using GFramework.Core.Abstractions.versioning;
using GFramework.Core.extensions;
using GFramework.Core.logging;
using GFramework.Core.model;
@ -10,186 +9,98 @@ using GFramework.Game.Abstractions.setting;
namespace GFramework.Game.setting;
/// <summary>
/// 设置模型类,用于管理不同类型的应用程序设置部分
/// 设置模型:
/// - 管理 Settings Data 的生命周期Load / Save / Reset / Migration
/// - 编排 Settings Applicator 的 Apply 行为
/// </summary>
public class SettingsModel<TRepository>(IDataRepository? repository)
: AbstractModel, ISettingsModel where TRepository : class, IDataRepository
public class SettingsModel<TRepository> : AbstractModel, ISettingsModel
where TRepository : class, IDataRepository
{
private static readonly ILogger Log =
LoggerFactoryResolver.Provider.CreateLogger(nameof(SettingsModel<TRepository>));
private readonly ConcurrentDictionary<Type, IApplyAbleSettings> _applicators = new();
private readonly ConcurrentDictionary<Type, IResettable> _dataSettings = new();
private readonly ConcurrentDictionary<Type, Dictionary<int, ISettingsMigration>> _migrationCache = 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 IDataRepository? _repository = repository;
private IDataRepository Repository => _repository ?? throw new InvalidOperationException("Repository is not set");
private readonly ConcurrentDictionary<Type, Dictionary<int, ISettingsMigration>> _migrationCache = new();
// -----------------------------
// Data
// -----------------------------
private IDataRepository? _repository;
private IDataLocationProvider? _locationProvider;
private IDataRepository Repository =>
_repository ?? throw new InvalidOperationException("IDataRepository 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
// =========================
/// <summary>
/// 获取指定类型的设置数据实例,如果不存在则创建新的实例
/// 获取指定类型的设置数据实例(唯一实例)
/// </summary>
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口并提供无参构造函数</typeparam>
/// <returns>指定类型的设置数据实例</returns>
public T GetData<T>() where T : class, IResettable, new()
public T GetData<T>() where T : class, ISettingsData, new()
{
return (T)_dataSettings.GetOrAdd(typeof(T), _ => new T());
return (T)_data.GetOrAdd(typeof(T), _ => new T());
}
/// <summary>
/// 获取所有设置数据的枚举集合
/// </summary>
/// <returns>所有设置数据的枚举集合</returns>
public IEnumerable<IResettable> AllData()
public IEnumerable<ISettingsData> AllData()
{
return _dataSettings.Values;
return _data.Values;
}
// -----------------------------
// =========================
// Applicator
// -----------------------------
// =========================
/// <summary>
/// 获取所有设置应用器的枚举集合
/// 注册设置应用器
/// </summary>
/// <returns>所有设置应用器的枚举集合</returns>
public IEnumerable<IApplyAbleSettings> AllApplicators()
public ISettingsModel RegisterApplicator(IResetApplyAbleSettings applicator)
{
return _applicators.Values;
}
/// <summary>
/// 注册设置应用器到模型中
/// </summary>
/// <typeparam name="T">设置应用器类型必须实现IApplyAbleSettings接口</typeparam>
/// <param name="applicator">要注册的设置应用器实例</param>
/// <returns>当前设置模型实例,支持链式调用</returns>
public ISettingsModel RegisterApplicator<T>(T applicator)
where T : class, IApplyAbleSettings
{
_applicators[typeof(T)] = applicator;
_applicators.Add(applicator);
return this;
}
/// <summary>
/// 获取指定类型的设置应用器实例
/// 获取所有设置应用器
/// </summary>
/// <typeparam name="T">设置应用器类型必须实现IApplyAbleSettings接口</typeparam>
/// <returns>指定类型的设置应用器实例如果不存在则返回null</returns>
public T? GetApplicator<T>() where T : class, IApplyAbleSettings
public IEnumerable<IResetApplyAbleSettings> AllApplicators()
{
return _applicators.TryGetValue(typeof(T), out var app)
? (T)app
: null;
return _applicators;
}
// -----------------------------
// Section lookup
// -----------------------------
/// <summary>
/// 尝试获取指定类型的设置节
/// </summary>
/// <param name="type">要查找的设置类型</param>
/// <param name="section">输出参数,找到的设置节实例</param>
/// <returns>如果找到对应类型的设置节则返回true否则返回false</returns>
public bool TryGet(Type type, out ISettingsSection section)
{
if (_dataSettings.TryGetValue(type, out var data))
{
section = data;
return true;
}
if (_applicators.TryGetValue(type, out var applicator))
{
section = applicator;
return true;
}
section = null!;
return false;
}
// -----------------------------
// =========================
// Migration
// -----------------------------
// =========================
/// <summary>
/// 注册设置迁移器到模型中
/// </summary>
/// <param name="migration">要注册的设置迁移器实例</param>
/// <returns>当前设置模型实例,支持链式调用</returns>
public ISettingsModel RegisterMigration(ISettingsMigration migration)
{
_migrations[(migration.SettingsType, migration.FromVersion)] = migration;
return this;
}
// -----------------------------
// Load / Init
// -----------------------------
/// <summary>
/// 异步初始化设置模型,加载指定类型的设置数据
/// </summary>
/// <param name="settingTypes">要初始化的设置类型数组</param>
public async Task InitializeAsync(params Type[] settingTypes)
private ISettingsData MigrateIfNeeded(ISettingsData data)
{
foreach (var type in settingTypes)
{
if (!typeof(IResettable).IsAssignableFrom(type) ||
!typeof(IData).IsAssignableFrom(type))
continue;
if (data is not IVersionedData versioned)
return data;
try
{
var loaded = (ISettingsSection)await Repository.LoadAsync(type);
var migrated = MigrateIfNeeded(loaded);
_dataSettings[type] = (IResettable)migrated;
_migrationCache.TryRemove(type, out _);
}
catch (Exception ex)
{
Log.Error($"Failed to load settings for {type.Name}", ex);
}
}
}
/// <summary>
/// 重置指定类型的可重置对象
/// </summary>
/// <typeparam name="T">要重置的对象类型必须是class类型实现IResettable接口并具有无参构造函数</typeparam>
public void Reset<T>() where T : class, IResettable, new()
{
var data = GetData<T>();
data.Reset();
}
/// <summary>
/// 重置所有存储的数据设置对象
/// </summary>
public void ResetAll()
{
foreach (var data in _dataSettings.Values) data.Reset();
}
/// <summary>
/// 如果需要的话,对设置节进行版本迁移
/// </summary>
/// <param name="section">待检查和迁移的设置节</param>
/// <returns>迁移后的设置节</returns>
private ISettingsSection MigrateIfNeeded(ISettingsSection section)
{
if (section is not IVersioned versioned)
return section;
var type = section.GetType();
var current = section;
var type = data.GetType();
var current = data;
if (!_migrationCache.TryGetValue(type, out var versionMap))
{
@ -202,19 +113,94 @@ public class SettingsModel<TRepository>(IDataRepository? repository)
while (versionMap.TryGetValue(versioned.Version, out var migration))
{
current = migration.Migrate(current);
versioned = (IVersioned)current;
current = (ISettingsData)migration.Migrate(current);
versioned = current;
}
return current;
}
// =========================
// Lifecycle
// =========================
/// <summary>
/// 初始化方法,用于获取设置持久化服务
/// 初始化设置模型:
/// - 加载所有已存在的 Settings Data
/// - 执行必要的迁移
/// </summary>
protected override void OnInit()
public async Task InitializeAsync()
{
_repository ??= this.GetUtility<TRepository>()!;
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

@ -63,7 +63,7 @@ public class SettingsSystem<TRepository>(IDataRepository? repository)
/// </summary>
/// <typeparam name="T">设置类型必须实现IPersistentApplyAbleSettings接口且具有无参构造函数</typeparam>
/// <returns>异步任务</returns>
public async Task Reset<T>() where T : class, IPersistentApplyAbleSettings, new()
public async Task Reset<T>() where T : class, IResetApplyAbleSettings, new()
{
_model.Reset<T>();
await Apply<T>();

View File

@ -11,7 +11,7 @@ namespace GFramework.Godot.setting;
/// <param name="model">设置模型对象,提供音频设置数据访问</param>
/// <param name="audioBusMap">音频总线映射对象,定义了不同音频类型的总线名称</param>
public class GodotAudioSettings(ISettingsModel model, AudioBusMap audioBusMap)
: IPersistentApplyAbleSettings
: IResetApplyAbleSettings
{
/// <summary>
/// 应用音频设置到Godot音频系统

View File

@ -8,7 +8,7 @@ namespace GFramework.Godot.setting;
/// Godot图形设置应用器
/// </summary>
/// <param name="model">设置模型接口</param>
public class GodotGraphicsSettings(ISettingsModel model) : IPersistentApplyAbleSettings
public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettings
{
/// <summary>
/// 应用图形设置到Godot引擎

View File

@ -24,7 +24,7 @@ namespace GFramework.Godot.setting;
/// <param name="model">设置模型</param>
/// <param name="localizationMap">本地化映射表</param>
public class GodotLocalizationSettings(ISettingsModel model, LocalizationMap localizationMap)
: IPersistentApplyAbleSettings
: IResetApplyAbleSettings
{
/// <summary>
/// 应用本地化设置到Godot引擎