From 970b8d3b967fd4125f75137f7306d0f21c9bb69d Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:48:09 +0800 Subject: [PATCH] =?UTF-8?q?refactor(settings):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E7=B3=BB=E7=BB=9F=E5=92=8C=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将音频和图形设置从 IResettable, IVersioned 迁移到 ISettingsData 接口 - 添加数据位置接口 IDataLocation 和数据位置提供者接口 IDataLocationProvider - 修改数据仓库实现,使用数据位置替代类型进行数据操作 - 更新数据仓库的加载、保存、删除和存在检查方法以使用数据位置参数 - 重命名 IPersistentApplyAbleSettings 为 IResetApplyAbleSettings 并更新其实现 - 创建 ISettingsData 接口整合设置数据的基础功能 - 更新设置模型实现,统一管理设置数据的生命周期和应用器 - 添加版本化数据接口 IVersionedData 和可从源加载接口 ILoadableFrom - 实现数据位置到存储键的扩展方法 - 更新数据事件类型以使用数据位置信息 - 重构设置模型的数据加载、保存和应用逻辑 - [skip ci] --- .../data/ILoadableFrom.cs | 19 ++ GFramework.Game.Abstractions/GlobalUsings.cs | 15 +- .../data/IDataLocation.cs | 42 +++ .../data/IDataLocationProvider.cs | 29 ++ .../data/IDataRepository.cs | 47 ++- .../data/IVersionedData.cs | 32 ++ .../data/events/DataBatchSavedEvent.cs | 4 +- .../data/events/DataDeletedEvent.cs | 5 +- .../enums/StorageKind.cs | 47 +++ .../internals/IsExternalInit.cs | 14 +- ...Settings.cs => IResetApplyAbleSettings.cs} | 6 +- .../setting/ISettingsData.cs | 23 ++ .../setting/ISettingsModel.cs | 94 +++--- .../setting/ISettingsSection.cs | 6 +- .../setting/ISettingsSystem.cs | 2 +- .../setting/data/AudioSettings.cs | 14 +- .../setting/data/GraphicsSettings.cs | 12 +- .../setting/data/LocalizationSettings.cs | 10 +- GFramework.Game/data/DataRepository.cs | 128 +++----- GFramework.Game/data/UnifiedSettingsFile.cs | 34 +++ .../data/UnifiedSettingsRepository.cs | 214 +++++-------- .../extensions/DataLocationExtensions.cs | 32 ++ GFramework.Game/setting/SettingsModel.cs | 280 +++++++++--------- GFramework.Game/setting/SettingsSystem.cs | 2 +- .../setting/GodotAudioSettings.cs | 2 +- .../setting/GodotGraphicsSettings.cs | 2 +- .../setting/GodotLocalizationSettings.cs | 2 +- 27 files changed, 634 insertions(+), 483 deletions(-) create mode 100644 GFramework.Core.Abstractions/data/ILoadableFrom.cs create mode 100644 GFramework.Game.Abstractions/data/IDataLocation.cs create mode 100644 GFramework.Game.Abstractions/data/IDataLocationProvider.cs create mode 100644 GFramework.Game.Abstractions/data/IVersionedData.cs create mode 100644 GFramework.Game.Abstractions/enums/StorageKind.cs rename GFramework.Game.Abstractions/setting/{IPersistentApplyAbleSettings.cs => IResetApplyAbleSettings.cs} (74%) create mode 100644 GFramework.Game.Abstractions/setting/ISettingsData.cs create mode 100644 GFramework.Game/data/UnifiedSettingsFile.cs create mode 100644 GFramework.Game/extensions/DataLocationExtensions.cs diff --git a/GFramework.Core.Abstractions/data/ILoadableFrom.cs b/GFramework.Core.Abstractions/data/ILoadableFrom.cs new file mode 100644 index 0000000..5bf3e8f --- /dev/null +++ b/GFramework.Core.Abstractions/data/ILoadableFrom.cs @@ -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 +{ + void LoadFrom(T source); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/GlobalUsings.cs b/GFramework.Game.Abstractions/GlobalUsings.cs index 3ca0d0f..4d27181 100644 --- a/GFramework.Game.Abstractions/GlobalUsings.cs +++ b/GFramework.Game.Abstractions/GlobalUsings.cs @@ -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; - -/// -/// 用于标记仅初始化 setter 的特殊类型 -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public static class IsExternalInit -{ -} -#endif \ No newline at end of file +global using System.Threading.Tasks; \ No newline at end of file diff --git a/GFramework.Game.Abstractions/data/IDataLocation.cs b/GFramework.Game.Abstractions/data/IDataLocation.cs new file mode 100644 index 0000000..1f8b6a6 --- /dev/null +++ b/GFramework.Game.Abstractions/data/IDataLocation.cs @@ -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; + +/// +/// 数据位置接口,定义了数据存储的位置信息和相关属性 +/// +public interface IDataLocation +{ + /// + /// 存储键(文件路径 / redis key / db key) + /// + string Key { get; } + + /// + /// 存储类型(Local / Remote / Database / Memory) + /// + StorageKind Kind { get; } + + /// + /// 命名空间/分区 + /// + string? Namespace { get; } + + /// + /// 扩展元数据(用于存储额外信息,如压缩、加密等) + /// + IReadOnlyDictionary? Metadata { get; } +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/data/IDataLocationProvider.cs b/GFramework.Game.Abstractions/data/IDataLocationProvider.cs new file mode 100644 index 0000000..94b4a21 --- /dev/null +++ b/GFramework.Game.Abstractions/data/IDataLocationProvider.cs @@ -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; + +/// +/// 定义数据位置提供者的接口,用于获取指定类型的数据位置信息 +/// +public interface IDataLocationProvider:IUtility +{ + /// + /// 获取指定类型的数据位置 + /// + /// 需要获取位置信息的类型 + /// 与指定类型关联的数据位置对象 + IDataLocation GetLocation(Type type); +} diff --git a/GFramework.Game.Abstractions/data/IDataRepository.cs b/GFramework.Game.Abstractions/data/IDataRepository.cs index 79078b2..3012e57 100644 --- a/GFramework.Game.Abstractions/data/IDataRepository.cs +++ b/GFramework.Game.Abstractions/data/IDataRepository.cs @@ -21,45 +21,42 @@ namespace GFramework.Game.Abstractions.data; public interface IDataRepository : IUtility { /// - /// 异步加载指定类型的数据对象 + /// 异步加载指定位置的数据 /// /// 要加载的数据类型,必须实现IData接口并具有无参构造函数 - /// 返回加载的数据对象的Task - Task LoadAsync() where T : class, IData, new(); + /// 数据位置信息 + /// 返回加载的数据对象 + Task LoadAsync(IDataLocation location) + where T : class, IData, new(); /// - /// 根据类型异步加载数据 - /// - /// 要加载的数据类型 - /// 异步操作任务,返回实现IData接口的数据对象 - Task LoadAsync(Type type); - - /// - /// 异步保存指定的数据对象 + /// 异步保存数据到指定位置 /// /// 要保存的数据类型,必须实现IData接口 + /// 数据位置信息 /// 要保存的数据对象 - /// 表示异步保存操作的Task - Task SaveAsync(T data) where T : class, IData; + /// 返回异步操作任务 + Task SaveAsync(IDataLocation location, T data) + where T : class, IData; /// - /// 异步检查指定类型的数据是否存在 + /// 异步检查指定位置是否存在数据 /// - /// 要检查的数据类型,必须实现IData接口 - /// 返回表示数据是否存在布尔值的Task - Task ExistsAsync() where T : class, IData; + /// 数据位置信息 + /// 返回布尔值,表示数据是否存在 + Task ExistsAsync(IDataLocation location); /// - /// 异步删除指定类型的数据 + /// 异步删除指定位置的数据 /// - /// 要删除的数据类型,必须实现IData接口 - /// 表示异步删除操作的Task - Task DeleteAsync() where T : class, IData; + /// 数据位置信息 + /// 返回异步操作任务 + Task DeleteAsync(IDataLocation location); /// - /// 批量保存多个数据 + /// 异步批量保存多个数据项到各自的位置 /// - /// 要保存的数据列表,实现IData接口的对象集合 - /// 异步操作任务 - Task SaveAllAsync(IEnumerable dataList); + /// 包含数据位置和对应数据对象的可枚举集合 + /// 返回异步操作任务 + Task SaveAllAsync(IEnumerable<(IDataLocation location, IData data)> dataList); } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/data/IVersionedData.cs b/GFramework.Game.Abstractions/data/IVersionedData.cs new file mode 100644 index 0000000..b3a8066 --- /dev/null +++ b/GFramework.Game.Abstractions/data/IVersionedData.cs @@ -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; + +/// +/// 版本化数据接口,继承自IData接口 +/// 提供版本控制和修改时间跟踪功能 +/// +public interface IVersionedData : IData +{ + /// + /// 获取数据的版本号 + /// + /// 当前数据的版本号,用于标识数据的版本状态 + int Version { get; } + + /// + /// 获取数据最后修改的时间 + /// + /// DateTime类型的最后修改时间戳 + DateTime LastModified { get; } +} diff --git a/GFramework.Game.Abstractions/data/events/DataBatchSavedEvent.cs b/GFramework.Game.Abstractions/data/events/DataBatchSavedEvent.cs index 75e3e34..a0c00f1 100644 --- a/GFramework.Game.Abstractions/data/events/DataBatchSavedEvent.cs +++ b/GFramework.Game.Abstractions/data/events/DataBatchSavedEvent.cs @@ -16,5 +16,5 @@ namespace GFramework.Game.Abstractions.data.events; /// /// 表示数据批次保存事件的记录类型 /// -/// 包含已保存数据项的集合,实现了IData接口 -public sealed record DataBatchSavedEvent(ICollection List); \ No newline at end of file +/// 包含已保存数据项的集合,实现了IData接口 +public sealed record DataBatchSavedEvent(ICollection<(IDataLocation location, IData data)> DataList); \ No newline at end of file diff --git a/GFramework.Game.Abstractions/data/events/DataDeletedEvent.cs b/GFramework.Game.Abstractions/data/events/DataDeletedEvent.cs index a4dc90c..7e6019c 100644 --- a/GFramework.Game.Abstractions/data/events/DataDeletedEvent.cs +++ b/GFramework.Game.Abstractions/data/events/DataDeletedEvent.cs @@ -16,5 +16,6 @@ namespace GFramework.Game.Abstractions.data.events; /// /// 表示数据删除事件的记录类型 /// -/// 被删除数据的类型 -public sealed record DataDeletedEvent(Type Type); \ No newline at end of file +/// 数据位置信息,标识被删除数据的位置 +public sealed record DataDeletedEvent(IDataLocation Location); + diff --git a/GFramework.Game.Abstractions/enums/StorageKind.cs b/GFramework.Game.Abstractions/enums/StorageKind.cs new file mode 100644 index 0000000..2318d0e --- /dev/null +++ b/GFramework.Game.Abstractions/enums/StorageKind.cs @@ -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; + +/// +/// 存储类型枚举,用于标识不同的存储方式 +/// 此枚举使用 Flags 特性,支持位运算组合多个存储类型 +/// +[Flags] +public enum StorageKind +{ + /// + /// 无存储类型 + /// + None = 0, + + /// + /// 本地文件系统存储 + /// + Local = 1 << 0, + + /// + /// 内存存储 + /// + Memory = 1 << 1, + + /// + /// 远程存储 + /// + Remote = 1 << 2, + + /// + /// 数据库存储 + /// + Database = 1 << 3, +} diff --git a/GFramework.Game.Abstractions/internals/IsExternalInit.cs b/GFramework.Game.Abstractions/internals/IsExternalInit.cs index 47feac7..b433836 100644 --- a/GFramework.Game.Abstractions/internals/IsExternalInit.cs +++ b/GFramework.Game.Abstractions/internals/IsExternalInit.cs @@ -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; + +/// +/// 用于标记仅初始化 setter 的特殊类型 +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class IsExternalInit { } -#pragma warning restore S2094 \ No newline at end of file +#endif \ No newline at end of file diff --git a/GFramework.Game.Abstractions/setting/IPersistentApplyAbleSettings.cs b/GFramework.Game.Abstractions/setting/IResetApplyAbleSettings.cs similarity index 74% rename from GFramework.Game.Abstractions/setting/IPersistentApplyAbleSettings.cs rename to GFramework.Game.Abstractions/setting/IResetApplyAbleSettings.cs index 3fcd1cb..8cc4e18 100644 --- a/GFramework.Game.Abstractions/setting/IPersistentApplyAbleSettings.cs +++ b/GFramework.Game.Abstractions/setting/IResetApplyAbleSettings.cs @@ -14,7 +14,7 @@ namespace GFramework.Game.Abstractions.setting; /// -/// 可持久化的应用设置接口 -/// 同时具备数据持久化和应用逻辑能力 +/// 定义一个可重置且可应用设置的接口 +/// 该接口继承自IResettable和IApplyAbleSettings接口,组合了重置功能和应用设置功能 /// -public interface IPersistentApplyAbleSettings : IResettable, IApplyAbleSettings; \ No newline at end of file +public interface IResetApplyAbleSettings : IResettable, IApplyAbleSettings; diff --git a/GFramework.Game.Abstractions/setting/ISettingsData.cs b/GFramework.Game.Abstractions/setting/ISettingsData.cs new file mode 100644 index 0000000..736ed97 --- /dev/null +++ b/GFramework.Game.Abstractions/setting/ISettingsData.cs @@ -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; + +/// +/// 定义游戏设置数据的接口 +/// 该接口继承自IData和IResettable接口,提供数据管理和重置功能 +/// +public interface ISettingsData : IResettable, IVersionedData, ILoadableFrom; \ No newline at end of file diff --git a/GFramework.Game.Abstractions/setting/ISettingsModel.cs b/GFramework.Game.Abstractions/setting/ISettingsModel.cs index ea37d5d..c081fab 100644 --- a/GFramework.Game.Abstractions/setting/ISettingsModel.cs +++ b/GFramework.Game.Abstractions/setting/ISettingsModel.cs @@ -3,75 +3,85 @@ namespace GFramework.Game.Abstractions.setting; /// -/// 定义设置模型的接口,提供获取特定类型设置节的功能 +/// 设置模型接口: +/// - 管理 Settings Data 的生命周期 +/// - 管理并编排 Settings Applicator +/// - 管理 Settings Migration /// public interface ISettingsModel : IModel { + // ========================= + // Data + // ========================= + /// - /// 获取或创建数据设置(自动创建) + /// 获取指定类型的设置数据(唯一实例) /// - /// 设置数据的类型,必须继承自class、ISettingsData且具有无参构造函数 + /// 设置数据类型,必须继承自ISettingsData并具有无参构造函数 /// 指定类型的设置数据实例 - T GetData() where T : class, IResettable, new(); + T GetData() where T : class, ISettingsData, new(); /// - /// 尝试获取指定类型的设置节实例 + /// 获取所有已创建的设置数据 /// - /// 要获取的设置节类型 - /// 输出参数,如果成功则包含找到的设置节实例,否则为null - /// 如果找到指定类型的设置节则返回true,否则返回false - bool TryGet(Type type, out ISettingsSection section); + /// 所有已创建的设置数据集合 + IEnumerable AllData(); + + + // ========================= + // Applicator + // ========================= /// - /// 获取已注册的可应用设置 + /// 注册设置应用器 /// - /// 可应用设置的类型,必须继承自class和IApplyAbleSettings - /// 指定类型的可应用设置实例,如果不存在则返回null - T? GetApplicator() where T : class, IApplyAbleSettings; + /// 要注册的设置应用器 + /// 当前设置模型实例,支持链式调用 + ISettingsModel RegisterApplicator(IResetApplyAbleSettings applicator); /// - /// 获取所有设置数据的集合 + /// 获取所有设置应用器 /// - /// 包含所有设置数据的可枚举集合 - IEnumerable AllData(); + /// 所有设置应用器的集合 + IEnumerable AllApplicators(); + + + // ========================= + // Migration + // ========================= /// - /// 获取所有可应用设置的集合 + /// 注册设置迁移器 /// - /// 包含所有可应用设置的可枚举集合 - IEnumerable AllApplicators(); - - - /// - /// 注册可应用设置(必须手动注册) - /// - /// 可应用设置的类型,必须继承自class和IApplyAbleSettings - /// 要注册的可应用设置实例 - /// 返回当前设置模型实例,支持链式调用 - ISettingsModel RegisterApplicator(T applicator) where T : class, IApplyAbleSettings; - - /// - /// 注册设置迁移器 - /// - /// 要注册的设置迁移实例 - /// 返回当前设置模型实例,支持链式调用 + /// 要注册的设置迁移器 + /// 当前设置模型实例,支持链式调用 ISettingsModel RegisterMigration(ISettingsMigration migration); + + // ========================= + // Lifecycle + // ========================= + /// - /// 异步初始化指定类型的设置 + /// 初始化所有设置数据(加载 + 迁移) /// - /// 要初始化的设置类型数组 /// 异步操作任务 - Task InitializeAsync(params Type[] settingTypes); + Task InitializeAsync(); /// - /// 重置指定类型的设置 + /// 保存所有设置数据 /// - /// 要重置的设置类型,必须实现IResettable接口并具有无参构造函数 - void Reset() where T : class, IResettable, new(); + /// 异步操作任务 + Task SaveAllAsync(); /// - /// 重置所有设置 + /// 应用所有设置 + /// + /// 异步操作任务 + Task ApplyAllAsync(); + + /// + /// 重置所有设置数据与应用器 /// void ResetAll(); -} \ No newline at end of file +} diff --git a/GFramework.Game.Abstractions/setting/ISettingsSection.cs b/GFramework.Game.Abstractions/setting/ISettingsSection.cs index 7beb968..be480fe 100644 --- a/GFramework.Game.Abstractions/setting/ISettingsSection.cs +++ b/GFramework.Game.Abstractions/setting/ISettingsSection.cs @@ -1,9 +1,7 @@ -using GFramework.Game.Abstractions.data; - -namespace GFramework.Game.Abstractions.setting; +namespace GFramework.Game.Abstractions.setting; /// /// 表示游戏设置的一个配置节接口 /// 该接口定义了设置配置节的基本契约,用于管理游戏中的各种配置选项 /// -public interface ISettingsSection : IData; \ No newline at end of file +public interface ISettingsSection; \ No newline at end of file diff --git a/GFramework.Game.Abstractions/setting/ISettingsSystem.cs b/GFramework.Game.Abstractions/setting/ISettingsSystem.cs index 534e66d..bc61ab7 100644 --- a/GFramework.Game.Abstractions/setting/ISettingsSystem.cs +++ b/GFramework.Game.Abstractions/setting/ISettingsSystem.cs @@ -31,7 +31,7 @@ public interface ISettingsSystem : ISystem /// /// 设置类型,必须继承自class并实现IPersistentApplyAbleSettings接口 /// 表示异步操作的任务 - Task Reset() where T : class, IPersistentApplyAbleSettings, new(); + Task Reset() where T : class, IResetApplyAbleSettings, new(); /// /// 重置所有设置 diff --git a/GFramework.Game.Abstractions/setting/data/AudioSettings.cs b/GFramework.Game.Abstractions/setting/data/AudioSettings.cs index e888a67..feed789 100644 --- a/GFramework.Game.Abstractions/setting/data/AudioSettings.cs +++ b/GFramework.Game.Abstractions/setting/data/AudioSettings.cs @@ -1,11 +1,9 @@ -using GFramework.Core.Abstractions.versioning; - namespace GFramework.Game.Abstractions.setting.data; /// /// 音频设置类,用于管理游戏中的音频配置 /// -public class AudioSettings : IResettable, IVersioned +public class AudioSettings : ISettingsData { /// /// 获取或设置主音量,控制所有音频的总体音量 @@ -33,5 +31,13 @@ public class AudioSettings : IResettable, IVersioned SfxVolume = 0.8f; } + /// + /// 获取或设置设置数据的版本号 + /// public int Version { get; set; } = 1; -} \ No newline at end of file + + /// + /// 获取设置数据最后修改的时间 + /// + public DateTime LastModified { get; } = DateTime.Now; +} diff --git a/GFramework.Game.Abstractions/setting/data/GraphicsSettings.cs b/GFramework.Game.Abstractions/setting/data/GraphicsSettings.cs index 4415a2e..53c9a0a 100644 --- a/GFramework.Game.Abstractions/setting/data/GraphicsSettings.cs +++ b/GFramework.Game.Abstractions/setting/data/GraphicsSettings.cs @@ -1,11 +1,9 @@ -using GFramework.Core.Abstractions.versioning; - namespace GFramework.Game.Abstractions.setting.data; /// /// 图形设置类,用于管理游戏的图形相关配置 /// -public class GraphicsSettings : IResettable, IVersioned +public class GraphicsSettings : ISettingsData { /// /// 获取或设置是否启用全屏模式 @@ -32,5 +30,13 @@ public class GraphicsSettings : IResettable, IVersioned ResolutionHeight = 1080; } + /// + /// 获取或设置设置数据的版本号 + /// public int Version { get; set; } = 1; + + /// + /// 获取设置数据最后修改的时间 + /// + public DateTime LastModified { get; } = DateTime.Now; } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/setting/data/LocalizationSettings.cs b/GFramework.Game.Abstractions/setting/data/LocalizationSettings.cs index edd9233..2947490 100644 --- a/GFramework.Game.Abstractions/setting/data/LocalizationSettings.cs +++ b/GFramework.Game.Abstractions/setting/data/LocalizationSettings.cs @@ -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; /// /// 本地化设置类,用于管理游戏的语言本地化配置 /// 实现了ISettingsData接口提供设置数据功能,实现IVersioned接口提供版本控制功能 /// -public class LocalizationSettings : IResettable, IVersioned +public class LocalizationSettings : ISettingsData { /// /// 获取或设置当前使用的语言 @@ -39,6 +37,10 @@ public class LocalizationSettings : IResettable, IVersioned /// /// 获取或设置设置数据的版本号 /// - /// 默认版本号为1 public int Version { get; set; } = 1; + + /// + /// 获取设置数据最后修改的时间 + /// + public DateTime LastModified { get; } = DateTime.Now; } \ No newline at end of file diff --git a/GFramework.Game/data/DataRepository.cs b/GFramework.Game/data/DataRepository.cs index e4a53fd..dc2d836 100644 --- a/GFramework.Game/data/DataRepository.cs +++ b/GFramework.Game/data/DataRepository.cs @@ -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."); - /// - /// 异步加载指定类型的数据 - /// - /// 要加载的数据类型,必须实现IData接口 - /// 加载的数据对象 - public async Task LoadAsync() where T : class, IData, new() + protected override void OnInit() { - var key = GetKey(); + _storage ??= this.GetUtility()!; + } + + /// + /// 异步加载指定位置的数据 + /// + /// 数据类型,必须实现IData接口 + /// 数据位置信息 + /// 加载的数据对象 + public async Task LoadAsync(IDataLocation location) + where T : class, IData, new() + { + var key = location.ToStorageKey(); T result; // 检查存储中是否存在指定键的数据 @@ -58,43 +66,15 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options = } /// - /// 异步加载指定类型的数据(通过Type参数) + /// 异步保存数据到指定位置 /// - /// 要加载的数据类型 - /// 加载的数据对象 - public async Task 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(key); - else - result = (IData)Activator.CreateInstance(type)!; - - // 如果启用事件功能,则发送数据加载完成事件 - if (_options.EnableEvents) - this.SendEvent(new DataLoadedEvent(result)); - - return result; - } - - - /// - /// 异步保存指定类型的数据 - /// - /// 要保存的数据类型 + /// 数据类型,必须实现IData接口 + /// 数据位置信息 /// 要保存的数据对象 - public async Task SaveAsync(T data) where T : class, IData + public async Task SaveAsync(IDataLocation location, T data) + where T : class, IData { - var key = GetKey(); + var key = location.ToStorageKey(); // 自动备份 if (_options.AutoBackup && await Storage.ExistsAsync(key)) @@ -111,70 +91,38 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options = } /// - /// 检查指定类型的数据是否存在 + /// 检查指定位置的数据是否存在 /// - /// 要检查的数据类型 + /// 数据位置信息 /// 如果数据存在返回true,否则返回false - public async Task ExistsAsync() where T : class, IData - { - var key = GetKey(); - return await Storage.ExistsAsync(key); - } + public Task ExistsAsync(IDataLocation location) + => Storage.ExistsAsync(location.ToStorageKey()); /// - /// 异步删除指定类型的数据 + /// 异步删除指定位置的数据 /// - /// 要删除的数据类型 - public async Task DeleteAsync() where T : class, IData + /// 数据位置信息 + public async Task DeleteAsync(IDataLocation location) { - var key = GetKey(); + var key = location.ToStorageKey(); await Storage.DeleteAsync(key); - if (_options.EnableEvents) - this.SendEvent(new DataDeletedEvent(typeof(T))); + this.SendEvent(new DataDeletedEvent(location)); } /// - /// 批量异步保存多个数据对象 + /// 异步批量保存多个数据项 /// - /// 要保存的数据对象集合 - public async Task SaveAllAsync(IEnumerable dataList) + /// 包含数据位置和数据对象的枚举集合 + 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()!; - } - - /// - /// 根据类型生成存储键 - /// - /// 数据类型 - /// 生成的存储键 - protected virtual string GetKey() where T : IData - { - return GetKey(typeof(T)); - } - - /// - /// 根据类型生成存储键 - /// - /// 数据类型 - /// 生成的存储键 - protected virtual string GetKey(Type type) - { - var fileName = type.FullName!; - return string.IsNullOrEmpty(_options.BasePath) ? fileName : $"{_options.BasePath}/{fileName}"; - } -} \ No newline at end of file +} diff --git a/GFramework.Game/data/UnifiedSettingsFile.cs b/GFramework.Game/data/UnifiedSettingsFile.cs new file mode 100644 index 0000000..2656947 --- /dev/null +++ b/GFramework.Game/data/UnifiedSettingsFile.cs @@ -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; + +/// +/// 统一设置文件类,用于管理应用程序的配置设置 +/// 实现了版本控制接口,支持配置文件的版本管理 +/// +internal sealed class UnifiedSettingsFile:IVersioned +{ + /// + /// 配置节集合,存储不同类型的配置数据 + /// 键为配置节名称,值为配置对象 + /// + public Dictionary Sections { get; set; } = new(); + + /// + /// 配置文件版本号,用于版本控制和兼容性检查 + /// + public int Version { get; set; } +} diff --git a/GFramework.Game/data/UnifiedSettingsRepository.cs b/GFramework.Game/data/UnifiedSettingsRepository.cs index c3dffd4..0ea6ccc 100644 --- a/GFramework.Game/data/UnifiedSettingsRepository.cs +++ b/GFramework.Game/data/UnifiedSettingsRepository.cs @@ -30,7 +30,7 @@ public class UnifiedSettingsRepository( string fileName = "settings.json") : AbstractContextUtility, IDataRepository { - private readonly Dictionary _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 - // ========================= - - /// - /// 异步加载指定类型的数据 - /// - /// 要加载的数据类型,必须继承自IData接口并具有无参构造函数 - /// 加载的数据实例 - public async Task LoadAsync() where T : class, IData, new() - { - await EnsureLoadedAsync(); - - var key = GetTypeKey(typeof(T)); - - var result = _cache.TryGetValue(key, out var json) ? Serializer.Deserialize(json) : new T(); - - if (_options.EnableEvents) - this.SendEvent(new DataLoadedEvent(result)); - - return result; - } - - /// - /// 异步加载指定类型的数据(通过Type参数) - /// - /// 要加载的数据类型 - /// 加载的数据实例 - /// 当类型不符合要求时抛出异常 - public async Task 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(result)); - - return result; - } - - /// - /// 异步保存数据到存储 - /// - /// 要保存的数据类型 - /// 要保存的数据实例 - public async Task SaveAsync(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(data)); - } - - /// - /// 异步批量保存多个数据实例 - /// - /// 要保存的数据实例集合 - public async Task SaveAllAsync(IEnumerable 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)); - } - - /// - /// 检查指定类型的数据是否存在 - /// - /// 要检查的数据类型 - /// 如果存在返回true,否则返回false - public async Task ExistsAsync() where T : class, IData - { - await EnsureLoadedAsync(); - return _cache.ContainsKey(GetTypeKey(typeof(T))); - } - - /// - /// 删除指定类型的数据 - /// - /// 要删除的数据类型 - public async Task DeleteAsync() 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()!; _serializer ??= this.GetUtility()!; } + // ========================= + // IDataRepository + // ========================= + + public async Task LoadAsync(IDataLocation location) + where T : class, IData, new() + { + await EnsureLoadedAsync(); + var key = location.Key; + var result = _file!.Sections.TryGetValue(key, out var raw) ? Serializer.Deserialize(raw) : new T(); + if (_options.EnableEvents) + this.SendEvent(new DataLoadedEvent(result)); + return result; + } + + public async Task SaveAsync(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(data)); + } + + public async Task 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>(GetUnifiedKey()); - _cache.Clear(); - foreach (var (k, v) in data) - _cache[k] = v; + _file = await Storage.ReadAsync(fileName); + } + else + { + _file = new UnifiedSettingsFile { Version = 1 }; } _loaded = true; @@ -199,6 +154,7 @@ public class UnifiedSettingsRepository( } } + /// /// 将缓存中的所有数据保存到统一文件 /// @@ -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}"; } - - /// - /// 获取类型的唯一标识键 - /// - /// 要获取键的类型 - /// 类型的全名作为键 - protected virtual string GetTypeKey(Type type) - { - return type.FullName!; - } } \ No newline at end of file diff --git a/GFramework.Game/extensions/DataLocationExtensions.cs b/GFramework.Game/extensions/DataLocationExtensions.cs new file mode 100644 index 0000000..6a5b1e6 --- /dev/null +++ b/GFramework.Game/extensions/DataLocationExtensions.cs @@ -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; + +/// +/// 提供数据位置相关的扩展方法 +/// +public static class DataLocationExtensions +{ + /// + /// 将数据位置转换为存储键 + /// + /// 数据位置对象 + /// 格式化的存储键字符串,如果命名空间为空则返回键值,否则返回"命名空间/键值"格式 + public static string ToStorageKey(this IDataLocation location) + { + return string.IsNullOrEmpty(location.Namespace) ? location.Key : $"{location.Namespace}/{location.Key}"; + } +} diff --git a/GFramework.Game/setting/SettingsModel.cs b/GFramework.Game/setting/SettingsModel.cs index 8a86433..2231144 100644 --- a/GFramework.Game/setting/SettingsModel.cs +++ b/GFramework.Game/setting/SettingsModel.cs @@ -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; /// -/// 设置模型类,用于管理不同类型的应用程序设置部分 +/// 设置模型: +/// - 管理 Settings Data 的生命周期(Load / Save / Reset / Migration) +/// - 编排 Settings Applicator 的 Apply 行为 /// -public class SettingsModel(IDataRepository? repository) - : AbstractModel, ISettingsModel where TRepository : class, IDataRepository +public class SettingsModel : AbstractModel, ISettingsModel + where TRepository : class, IDataRepository { private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(SettingsModel)); - private readonly ConcurrentDictionary _applicators = new(); - private readonly ConcurrentDictionary _dataSettings = new(); - private readonly ConcurrentDictionary> _migrationCache = new(); + // ========================= + // Fields + // ========================= + + private readonly ConcurrentDictionary _data = new(); + private readonly ConcurrentBag _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> _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()!; + _locationProvider ??= this.GetUtility()!; + } + // ========================= + // Data access + // ========================= /// - /// 获取指定类型的设置数据实例,如果不存在则创建新的实例 + /// 获取指定类型的设置数据实例(唯一实例) /// - /// 设置数据类型,必须实现ISettingsData接口并提供无参构造函数 - /// 指定类型的设置数据实例 - public T GetData() where T : class, IResettable, new() + public T GetData() where T : class, ISettingsData, new() { - return (T)_dataSettings.GetOrAdd(typeof(T), _ => new T()); + return (T)_data.GetOrAdd(typeof(T), _ => new T()); } - /// - /// 获取所有设置数据的枚举集合 - /// - /// 所有设置数据的枚举集合 - public IEnumerable AllData() + + public IEnumerable AllData() { - return _dataSettings.Values; + return _data.Values; } - // ----------------------------- + // ========================= // Applicator - // ----------------------------- + // ========================= /// - /// 获取所有设置应用器的枚举集合 + /// 注册设置应用器 /// - /// 所有设置应用器的枚举集合 - public IEnumerable AllApplicators() + public ISettingsModel RegisterApplicator(IResetApplyAbleSettings applicator) { - return _applicators.Values; - } - - /// - /// 注册设置应用器到模型中 - /// - /// 设置应用器类型,必须实现IApplyAbleSettings接口 - /// 要注册的设置应用器实例 - /// 当前设置模型实例,支持链式调用 - public ISettingsModel RegisterApplicator(T applicator) - where T : class, IApplyAbleSettings - { - _applicators[typeof(T)] = applicator; + _applicators.Add(applicator); return this; } /// - /// 获取指定类型的设置应用器实例 + /// 获取所有设置应用器 /// - /// 设置应用器类型,必须实现IApplyAbleSettings接口 - /// 指定类型的设置应用器实例,如果不存在则返回null - public T? GetApplicator() where T : class, IApplyAbleSettings + public IEnumerable AllApplicators() { - return _applicators.TryGetValue(typeof(T), out var app) - ? (T)app - : null; + return _applicators; } - // ----------------------------- - // Section lookup - // ----------------------------- - - /// - /// 尝试获取指定类型的设置节 - /// - /// 要查找的设置类型 - /// 输出参数,找到的设置节实例 - /// 如果找到对应类型的设置节则返回true,否则返回false - 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 - // ----------------------------- + // ========================= - /// - /// 注册设置迁移器到模型中 - /// - /// 要注册的设置迁移器实例 - /// 当前设置模型实例,支持链式调用 public ISettingsModel RegisterMigration(ISettingsMigration migration) { _migrations[(migration.SettingsType, migration.FromVersion)] = migration; return this; } - - // ----------------------------- - // Load / Init - // ----------------------------- - - /// - /// 异步初始化设置模型,加载指定类型的设置数据 - /// - /// 要初始化的设置类型数组 - 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); - } - } - } - - /// - /// 重置指定类型的可重置对象 - /// - /// 要重置的对象类型,必须是class类型,实现IResettable接口,并具有无参构造函数 - public void Reset() where T : class, IResettable, new() - { - var data = GetData(); - data.Reset(); - } - - /// - /// 重置所有存储的数据设置对象 - /// - public void ResetAll() - { - foreach (var data in _dataSettings.Values) data.Reset(); - } - - /// - /// 如果需要的话,对设置节进行版本迁移 - /// - /// 待检查和迁移的设置节 - /// 迁移后的设置节 - 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(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 + // ========================= /// - /// 初始化方法,用于获取设置持久化服务 + /// 初始化设置模型: + /// - 加载所有已存在的 Settings Data + /// - 执行必要的迁移 /// - protected override void OnInit() + public async Task InitializeAsync() { - _repository ??= this.GetUtility()!; + 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(location); + var migrated = MigrateIfNeeded(loaded); + + // 回填数据(不替换实例) + data.LoadFrom(migrated); + } + catch (Exception ex) + { + Log.Error($"Failed to initialize settings data: {data.GetType().Name}", ex); + } + } } + + /// + /// 将所有 Settings Data 持久化 + /// + 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); + } + } + } + + /// + /// 应用所有设置 + /// + 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); + } + } + } + + /// + /// 重置所有设置 + /// + public void ResetAll() + { + foreach (var data in _data.Values) + data.Reset(); + + foreach (var applicator in _applicators) + applicator.Reset(); + } + } \ No newline at end of file diff --git a/GFramework.Game/setting/SettingsSystem.cs b/GFramework.Game/setting/SettingsSystem.cs index 4be3d77..c01322e 100644 --- a/GFramework.Game/setting/SettingsSystem.cs +++ b/GFramework.Game/setting/SettingsSystem.cs @@ -63,7 +63,7 @@ public class SettingsSystem(IDataRepository? repository) /// /// 设置类型,必须实现IPersistentApplyAbleSettings接口且具有无参构造函数 /// 异步任务 - public async Task Reset() where T : class, IPersistentApplyAbleSettings, new() + public async Task Reset() where T : class, IResetApplyAbleSettings, new() { _model.Reset(); await Apply(); diff --git a/GFramework.Godot/setting/GodotAudioSettings.cs b/GFramework.Godot/setting/GodotAudioSettings.cs index c5c3ac3..92c9b61 100644 --- a/GFramework.Godot/setting/GodotAudioSettings.cs +++ b/GFramework.Godot/setting/GodotAudioSettings.cs @@ -11,7 +11,7 @@ namespace GFramework.Godot.setting; /// 设置模型对象,提供音频设置数据访问 /// 音频总线映射对象,定义了不同音频类型的总线名称 public class GodotAudioSettings(ISettingsModel model, AudioBusMap audioBusMap) - : IPersistentApplyAbleSettings + : IResetApplyAbleSettings { /// /// 应用音频设置到Godot音频系统 diff --git a/GFramework.Godot/setting/GodotGraphicsSettings.cs b/GFramework.Godot/setting/GodotGraphicsSettings.cs index 07d0e53..9112218 100644 --- a/GFramework.Godot/setting/GodotGraphicsSettings.cs +++ b/GFramework.Godot/setting/GodotGraphicsSettings.cs @@ -8,7 +8,7 @@ namespace GFramework.Godot.setting; /// Godot图形设置应用器 /// /// 设置模型接口 -public class GodotGraphicsSettings(ISettingsModel model) : IPersistentApplyAbleSettings +public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettings { /// /// 应用图形设置到Godot引擎 diff --git a/GFramework.Godot/setting/GodotLocalizationSettings.cs b/GFramework.Godot/setting/GodotLocalizationSettings.cs index 02cdb5f..c68405b 100644 --- a/GFramework.Godot/setting/GodotLocalizationSettings.cs +++ b/GFramework.Godot/setting/GodotLocalizationSettings.cs @@ -24,7 +24,7 @@ namespace GFramework.Godot.setting; /// 设置模型 /// 本地化映射表 public class GodotLocalizationSettings(ISettingsModel model, LocalizationMap localizationMap) - : IPersistentApplyAbleSettings + : IResetApplyAbleSettings { /// /// 应用本地化设置到Godot引擎