mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
refactor(settings): 重构设置系统和数据仓库实现
- 将音频和图形设置从 IResettable, IVersioned 迁移到 ISettingsData 接口 - 添加数据位置接口 IDataLocation 和数据位置提供者接口 IDataLocationProvider - 修改数据仓库实现,使用数据位置替代类型进行数据操作 - 更新数据仓库的加载、保存、删除和存在检查方法以使用数据位置参数 - 重命名 IPersistentApplyAbleSettings 为 IResetApplyAbleSettings 并更新其实现 - 创建 ISettingsData 接口整合设置数据的基础功能 - 更新设置模型实现,统一管理设置数据的生命周期和应用器 - 添加版本化数据接口 IVersionedData 和可从源加载接口 ILoadableFrom - 实现数据位置到存储键的扩展方法 - 更新数据事件类型以使用数据位置信息 - 重构设置模型的数据加载、保存和应用逻辑 - [skip ci]
This commit is contained in:
parent
49d210b9ad
commit
970b8d3b96
19
GFramework.Core.Abstractions/data/ILoadableFrom.cs
Normal file
19
GFramework.Core.Abstractions/data/ILoadableFrom.cs
Normal 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);
|
||||
}
|
||||
@ -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;
|
||||
42
GFramework.Game.Abstractions/data/IDataLocation.cs
Normal file
42
GFramework.Game.Abstractions/data/IDataLocation.cs
Normal 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; }
|
||||
}
|
||||
29
GFramework.Game.Abstractions/data/IDataLocationProvider.cs
Normal file
29
GFramework.Game.Abstractions/data/IDataLocationProvider.cs
Normal 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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
32
GFramework.Game.Abstractions/data/IVersionedData.cs
Normal file
32
GFramework.Game.Abstractions/data/IVersionedData.cs
Normal 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; }
|
||||
}
|
||||
@ -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);
|
||||
@ -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);
|
||||
|
||||
|
||||
47
GFramework.Game.Abstractions/enums/StorageKind.cs
Normal file
47
GFramework.Game.Abstractions/enums/StorageKind.cs
Normal 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,
|
||||
}
|
||||
@ -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
|
||||
@ -14,7 +14,7 @@
|
||||
namespace GFramework.Game.Abstractions.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 可持久化的应用设置接口
|
||||
/// 同时具备数据持久化和应用逻辑能力
|
||||
/// 定义一个可重置且可应用设置的接口
|
||||
/// 该接口继承自IResettable和IApplyAbleSettings接口,组合了重置功能和应用设置功能
|
||||
/// </summary>
|
||||
public interface IPersistentApplyAbleSettings : IResettable, IApplyAbleSettings;
|
||||
public interface IResetApplyAbleSettings : IResettable, IApplyAbleSettings;
|
||||
23
GFramework.Game.Abstractions/setting/ISettingsData.cs
Normal file
23
GFramework.Game.Abstractions/setting/ISettingsData.cs
Normal 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>;
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
/// 重置所有设置
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
GFramework.Game/data/UnifiedSettingsFile.cs
Normal file
34
GFramework.Game/data/UnifiedSettingsFile.cs
Normal 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; }
|
||||
}
|
||||
@ -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!;
|
||||
}
|
||||
}
|
||||
32
GFramework.Game/extensions/DataLocationExtensions.cs
Normal file
32
GFramework.Game/extensions/DataLocationExtensions.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>();
|
||||
|
||||
@ -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音频系统
|
||||
|
||||
@ -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引擎
|
||||
|
||||
@ -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引擎
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user