feat(data): 添加数据仓库功能并重构设置系统接口

- 新增 DataRepository 类实现数据存储和读取功能
- 添加数据仓库配置选项类 DataRepositoryOptions
- 定义 IData 接口作为通用数据标记接口
- 实现数据加载、保存、删除等异步操作方法
- 添加数据事件系统包括加载、保存、删除等事件类型
- 将 ISettingsData 接口重命名为 IResettable 并更新相关实现
- 更新 SettingsModel 和 SettingsPersistence 使用新的接口
- 修改 SettingsBatchChangedEvent 和 SettingsBatchSavedEvent 使用 IResettable 类型
- 重构 AudioSettings、GraphicsSettings、LocalizationSettings 继承新接口
- 更新 IPersistentApplyAbleSettings 接口依赖为 IResettable
This commit is contained in:
GeWuYou 2026-01-28 20:08:34 +08:00
parent c918085ba9
commit 0b7c64fd99
19 changed files with 377 additions and 28 deletions

View File

@ -0,0 +1,40 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.data;
/// <summary>
/// 数据仓库配置选项
/// </summary>
public class DataRepositoryOptions
{
/// <summary>
/// 存储基础路径(如 "user://data/"
/// </summary>
public string BasePath { get; set; } = "";
/// <summary>
/// 键名前缀(如 "Game",生成的键为 "Game_SettingsData"
/// </summary>
public string KeyPrefix { get; set; } = "Data";
/// <summary>
/// 是否在保存时自动备份
/// </summary>
public bool AutoBackup { get; set; } = false;
/// <summary>
/// 是否启用加载/保存事件
/// </summary>
public bool EnableEvents { get; set; } = true;
}

View File

@ -0,0 +1,19 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.data;
/// <summary>
/// 通用数据标记接口
/// </summary>
public interface IData;

View File

@ -0,0 +1,54 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.data;
/// <summary>
/// 定义数据仓库接口,提供异步的数据加载、保存、检查存在性和删除操作
/// </summary>
public interface IDataRepository
{
/// <summary>
/// 异步加载指定类型的数据对象
/// </summary>
/// <typeparam name="T">要加载的数据类型必须实现IData接口并具有无参构造函数</typeparam>
/// <returns>返回加载的数据对象的Task</returns>
Task<T> LoadAsync<T>() where T : class, IData, new();
/// <summary>
/// 异步保存指定的数据对象
/// </summary>
/// <typeparam name="T">要保存的数据类型必须实现IData接口</typeparam>
/// <param name="data">要保存的数据对象</param>
/// <returns>表示异步保存操作的Task</returns>
Task SaveAsync<T>(T data) where T : class, IData;
/// <summary>
/// 异步检查指定类型的数据是否存在
/// </summary>
/// <typeparam name="T">要检查的数据类型必须实现IData接口</typeparam>
/// <returns>返回表示数据是否存在布尔值的Task</returns>
Task<bool> ExistsAsync<T>() where T : class, IData;
/// <summary>
/// 异步删除指定类型的数据
/// </summary>
/// <typeparam name="T">要删除的数据类型必须实现IData接口</typeparam>
/// <returns>表示异步删除操作的Task</returns>
Task DeleteAsync<T>() where T : class, IData;
/// <summary>
/// 批量保存多个数据
/// </summary>
Task SaveAllAsync(IEnumerable<IData> dataList);
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.data.events;
/// <summary>
/// 表示数据批次保存事件的记录类型
/// </summary>
/// <param name="List">包含已保存数据项的集合实现了IData接口</param>
public sealed record DataBatchSavedEvent(ICollection<IData> List);

View File

@ -0,0 +1,20 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.data.events;
/// <summary>
/// 表示数据删除事件的记录类型
/// </summary>
/// <param name="Type">被删除数据的类型</param>
public sealed record DataDeletedEvent(Type Type);

View File

@ -0,0 +1,21 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.data.events;
/// <summary>
/// 表示数据加载完成事件的泛型类
/// </summary>
/// <typeparam name="T">数据类型参数</typeparam>
/// <param name="Data">加载完成的数据对象</param>
public sealed record DataLoadedEvent<T>(T Data);

View File

@ -0,0 +1,21 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.data.events;
/// <summary>
/// 表示数据保存事件的记录类型
/// </summary>
/// <typeparam name="T">保存的数据类型</typeparam>
/// <param name="Data">保存的数据实例</param>
public sealed record DataSavedEvent<T>(T Data);

View File

@ -17,4 +17,4 @@ namespace GFramework.Game.Abstractions.setting;
/// 可持久化的应用设置接口
/// 同时具备数据持久化和应用逻辑能力
/// </summary>
public interface IPersistentApplyAbleSettings : ISettingsData, IApplyAbleSettings;
public interface IPersistentApplyAbleSettings : IResettable, IApplyAbleSettings;

View File

@ -1,9 +1,10 @@
namespace GFramework.Game.Abstractions.setting;
/// <summary>
/// 设置数据接口 - 纯数据,可自动创建
/// 可重置设置接口继承自ISettingsSection接口
/// 提供将设置重置为默认值的功能
/// </summary>
public interface ISettingsData : ISettingsSection
public interface IResettable : ISettingsSection
{
/// <summary>
/// 重置设置为默认值

View File

@ -12,7 +12,7 @@ public interface ISettingsModel : IModel
/// </summary>
/// <typeparam name="T">设置数据的类型必须继承自class、ISettingsData且具有无参构造函数</typeparam>
/// <returns>指定类型的设置数据实例</returns>
T GetData<T>() where T : class, ISettingsData, new();
T GetData<T>() where T : class, IResettable, new();
/// <summary>
/// 尝试获取指定类型的设置节实例
@ -33,7 +33,7 @@ public interface ISettingsModel : IModel
/// 获取所有设置数据的集合
/// </summary>
/// <returns>包含所有设置数据的可枚举集合</returns>
IEnumerable<ISettingsData> AllData();
IEnumerable<IResettable> AllData();
/// <summary>
/// 获取所有可应用设置的集合

View File

@ -11,25 +11,25 @@ public interface ISettingsPersistence : IContextUtility
/// <summary>
/// 异步加载指定类型的设置数据
/// </summary>
Task<T> LoadAsync<T>() where T : class, ISettingsData, new();
Task<T> LoadAsync<T>() where T : class, IResettable, new();
/// <summary>
/// 异步保存指定的设置数据
/// </summary>
Task SaveAsync<T>(T section) where T : class, ISettingsData;
Task SaveAsync<T>(T section) where T : class, IResettable;
/// <summary>
/// 异步检查指定类型的设置数据是否存在
/// </summary>
Task<bool> ExistsAsync<T>() where T : class, ISettingsData;
Task<bool> ExistsAsync<T>() where T : class, IResettable;
/// <summary>
/// 异步删除指定类型的设置数据
/// </summary>
Task DeleteAsync<T>() where T : class, ISettingsData;
Task DeleteAsync<T>() where T : class, IResettable;
/// <summary>
/// 保存所有设置数据
/// </summary>
Task SaveAllAsync(IEnumerable<ISettingsData> allData);
Task SaveAllAsync(IEnumerable<IResettable> allData);
}

View File

@ -5,7 +5,7 @@ namespace GFramework.Game.Abstractions.setting.data;
/// <summary>
/// 音频设置类,用于管理游戏中的音频配置
/// </summary>
public class AudioSettings : ISettingsData, IVersioned
public class AudioSettings : IResettable, IVersioned
{
/// <summary>
/// 获取或设置主音量,控制所有音频的总体音量

View File

@ -5,7 +5,7 @@ namespace GFramework.Game.Abstractions.setting.data;
/// <summary>
/// 图形设置类,用于管理游戏的图形相关配置
/// </summary>
public class GraphicsSettings : ISettingsData, IVersioned
public class GraphicsSettings : IResettable, IVersioned
{
/// <summary>
/// 获取或设置是否启用全屏模式

View File

@ -19,7 +19,7 @@ namespace GFramework.Game.Abstractions.setting.data;
/// 本地化设置类,用于管理游戏的语言本地化配置
/// 实现了ISettingsData接口提供设置数据功能实现IVersioned接口提供版本控制功能
/// </summary>
public class LocalizationSettings : ISettingsData, IVersioned
public class LocalizationSettings : IResettable, IVersioned
{
/// <summary>
/// 获取或设置当前使用的语言

View File

@ -0,0 +1,153 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Core.Abstractions.storage;
using GFramework.Core.extensions;
using GFramework.Core.utility;
using GFramework.Game.Abstractions.data;
using GFramework.Game.Abstractions.data.events;
namespace GFramework.Game.data;
/// <summary>
/// 数据仓库类,用于管理游戏数据的存储和读取
/// </summary>
/// <param name="storage">存储接口实例</param>
/// <param name="options">数据仓库配置选项</param>
public class DataRepository(IStorage? storage, DataRepositoryOptions? options = null)
: AbstractContextUtility, IDataRepository
{
private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions();
private IStorage? _storage = storage;
private IStorage Storage => _storage ??
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()
{
var key = GetKey<T>();
T result;
if (await Storage.ExistsAsync(key))
{
result = await Storage.ReadAsync<T>(key);
}
else
{
result = new T();
}
if (_options.EnableEvents)
this.SendEvent(new DataLoadedEvent<T>(result));
return result;
}
/// <summary>
/// 异步保存指定类型的数据
/// </summary>
/// <typeparam name="T">要保存的数据类型</typeparam>
/// <param name="data">要保存的数据对象</param>
public async Task SaveAsync<T>(T data) where T : class, IData
{
var key = GetKey<T>();
// 自动备份
if (_options.AutoBackup && await Storage.ExistsAsync(key))
{
var backupKey = $"{key}.backup";
var existing = await Storage.ReadAsync<T>(key);
await Storage.WriteAsync(backupKey, existing);
}
await Storage.WriteAsync(key, data);
if (_options.EnableEvents)
this.SendEvent(new DataSavedEvent<T>(data));
}
/// <summary>
/// 检查指定类型的数据是否存在
/// </summary>
/// <typeparam name="T">要检查的数据类型</typeparam>
/// <returns>如果数据存在返回true否则返回false</returns>
public async Task<bool> ExistsAsync<T>() where T : class, IData
{
var key = GetKey<T>();
return await Storage.ExistsAsync(key);
}
/// <summary>
/// 异步删除指定类型的数据
/// </summary>
/// <typeparam name="T">要删除的数据类型</typeparam>
public async Task DeleteAsync<T>() where T : class, IData
{
var key = GetKey<T>();
await Storage.DeleteAsync(key);
if (_options.EnableEvents)
this.SendEvent(new DataDeletedEvent(typeof(T)));
}
/// <summary>
/// 批量异步保存多个数据对象
/// </summary>
/// <param name="dataList">要保存的数据对象集合</param>
public async Task SaveAllAsync(IEnumerable<IData> dataList)
{
var list = dataList.ToList();
foreach (var data in list)
{
var type = data.GetType();
var key = GetKey(type);
await Storage.WriteAsync(key, data);
}
if (_options.EnableEvents)
this.SendEvent(new DataBatchSavedEvent(list));
}
protected override void OnInit()
{
_storage ??= this.GetUtility<IStorage>()!;
}
/// <summary>
/// 根据类型生成存储键
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <returns>生成的存储键</returns>
private string GetKey<T>() where T : IData => GetKey(typeof(T));
/// <summary>
/// 根据类型生成存储键
/// </summary>
/// <param name="type">数据类型</param>
/// <returns>生成的存储键</returns>
private string GetKey(Type type)
{
var fileName = $"{_options.KeyPrefix}_{type.Name}";
if (string.IsNullOrEmpty(_options.BasePath))
return fileName;
var basePath = _options.BasePath.TrimEnd('/');
return $"{basePath}/{fileName}";
}
}

View File

@ -16,7 +16,7 @@ public class SettingsModel : AbstractModel, ISettingsModel
{
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(SettingsModel));
private readonly ConcurrentDictionary<Type, IApplyAbleSettings> _applicators = new();
private readonly ConcurrentDictionary<Type, ISettingsData> _dataSettings = new();
private readonly ConcurrentDictionary<Type, IResettable> _dataSettings = new();
private readonly ConcurrentDictionary<Type, MethodInfo> _loadAsyncMethodCache = new();
private readonly ConcurrentDictionary<Type, Dictionary<int, ISettingsMigration>> _migrationCache = new();
private readonly ConcurrentDictionary<(Type type, int from), ISettingsMigration> _migrations = new();
@ -31,7 +31,7 @@ public class SettingsModel : AbstractModel, ISettingsModel
/// </summary>
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口并提供无参构造函数</typeparam>
/// <returns>指定类型的设置数据实例</returns>
public T GetData<T>() where T : class, ISettingsData, new()
public T GetData<T>() where T : class, IResettable, new()
{
return (T)_dataSettings.GetOrAdd(typeof(T), _ => new T());
}
@ -40,7 +40,7 @@ public class SettingsModel : AbstractModel, ISettingsModel
/// 获取所有设置数据的枚举集合
/// </summary>
/// <returns>所有设置数据的枚举集合</returns>
public IEnumerable<ISettingsData> AllData()
public IEnumerable<IResettable> AllData()
=> _dataSettings.Values;
// -----------------------------
@ -166,7 +166,7 @@ public class SettingsModel : AbstractModel, ISettingsModel
{
foreach (var type in settingTypes)
{
if (!typeof(ISettingsData).IsAssignableFrom(type) ||
if (!typeof(IResettable).IsAssignableFrom(type) ||
!type.IsClass ||
type.GetConstructor(Type.EmptyTypes) == null)
continue;
@ -182,7 +182,7 @@ public class SettingsModel : AbstractModel, ISettingsModel
var loaded = (ISettingsSection)((dynamic)task).Result;
var migrated = MigrateIfNeeded(loaded);
_dataSettings[type] = (ISettingsData)migrated;
_dataSettings[type] = (IResettable)migrated;
_migrationCache.TryRemove(type, out _);
}
catch (Exception ex)

View File

@ -18,7 +18,7 @@ public class SettingsPersistence : AbstractContextUtility, ISettingsPersistence
/// </summary>
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口</typeparam>
/// <returns>如果存在则返回存储的设置数据,否则返回新创建的实例</returns>
public async Task<T> LoadAsync<T>() where T : class, ISettingsData, new()
public async Task<T> LoadAsync<T>() where T : class, IResettable, new()
{
var key = GetKey<T>();
@ -39,7 +39,7 @@ public class SettingsPersistence : AbstractContextUtility, ISettingsPersistence
/// </summary>
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口</typeparam>
/// <param name="section">要保存的设置数据实例</param>
public async Task SaveAsync<T>(T section) where T : class, ISettingsData
public async Task SaveAsync<T>(T section) where T : class, IResettable
{
var key = GetKey<T>();
await _storage.WriteAsync(key, section);
@ -51,7 +51,7 @@ public class SettingsPersistence : AbstractContextUtility, ISettingsPersistence
/// </summary>
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口</typeparam>
/// <returns>如果存在返回true否则返回false</returns>
public async Task<bool> ExistsAsync<T>() where T : class, ISettingsData
public async Task<bool> ExistsAsync<T>() where T : class, IResettable
{
var key = GetKey<T>();
return await _storage.ExistsAsync(key);
@ -61,7 +61,7 @@ public class SettingsPersistence : AbstractContextUtility, ISettingsPersistence
/// 异步删除指定类型的设置数据
/// </summary>
/// <typeparam name="T">设置数据类型必须实现ISettingsData接口</typeparam>
public async Task DeleteAsync<T>() where T : class, ISettingsData
public async Task DeleteAsync<T>() where T : class, IResettable
{
var key = GetKey<T>();
await _storage.DeleteAsync(key);
@ -73,7 +73,7 @@ public class SettingsPersistence : AbstractContextUtility, ISettingsPersistence
/// 异步保存所有设置数据到存储中
/// </summary>
/// <param name="allData">包含所有设置数据的可枚举集合</param>
public async Task SaveAllAsync(IEnumerable<ISettingsData> allData)
public async Task SaveAllAsync(IEnumerable<IResettable> allData)
{
var dataList = allData.ToList();
foreach (var data in dataList)
@ -96,7 +96,7 @@ public class SettingsPersistence : AbstractContextUtility, ISettingsPersistence
/// </summary>
/// <typeparam name="T">设置数据类型</typeparam>
/// <returns>格式为"Settings_类型名称"的键名</returns>
private static string GetKey<T>() where T : ISettingsData
private static string GetKey<T>() where T : IResettable
{
return GetKey(typeof(T));
}

View File

@ -7,12 +7,12 @@ namespace GFramework.Game.setting.events;
/// 表示多个设置项同时发生变更的事件
/// </summary>
/// <param name="settings">发生变更的设置数据集合</param>
public class SettingsBatchChangedEvent(IEnumerable<ISettingsData> settings) : ISettingsChangedEvent
public class SettingsBatchChangedEvent(IEnumerable<IResettable> settings) : ISettingsChangedEvent
{
/// <summary>
/// 获取发生变更的具体设置数据列表
/// </summary>
public IEnumerable<ISettingsData> ChangedSettings { get; } = settings.ToList();
public IEnumerable<IResettable> ChangedSettings { get; } = settings.ToList();
/// <summary>
/// 获取设置类型对于批量变更事件固定返回ISettingsSection类型

View File

@ -6,12 +6,12 @@ namespace GFramework.Game.setting.events;
/// 表示设置批量保存事件
/// </summary>
/// <param name="settings">要保存的设置数据集合</param>
public class SettingsBatchSavedEvent(IEnumerable<ISettingsData> settings) : ISettingsChangedEvent
public class SettingsBatchSavedEvent(IEnumerable<IResettable> settings) : ISettingsChangedEvent
{
/// <summary>
/// 获取已保存的设置数据只读集合
/// </summary>
public IReadOnlyCollection<ISettingsData> SavedSettings { get; } = settings.ToList();
public IReadOnlyCollection<IResettable> SavedSettings { get; } = settings.ToList();
/// <summary>
/// 获取设置类型始终返回ISettingsSection类型