diff --git a/GFramework.Game.Abstractions/data/DataRepositoryOptions.cs b/GFramework.Game.Abstractions/data/DataRepositoryOptions.cs index f8134e2..b12cd48 100644 --- a/GFramework.Game.Abstractions/data/DataRepositoryOptions.cs +++ b/GFramework.Game.Abstractions/data/DataRepositoryOptions.cs @@ -26,7 +26,7 @@ public class DataRepositoryOptions /// /// 键名前缀(如 "Game",生成的键为 "Game_SettingsData") /// - public string KeyPrefix { get; set; } = "Data"; + public string KeyPrefix { get; set; } = ""; /// /// 是否在保存时自动备份 diff --git a/GFramework.Game/data/UnifiedSettingsRepository.cs b/GFramework.Game/data/UnifiedSettingsRepository.cs new file mode 100644 index 0000000..2170c32 --- /dev/null +++ b/GFramework.Game/data/UnifiedSettingsRepository.cs @@ -0,0 +1,239 @@ +// 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.serializer; +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; + +/// +/// 使用单一文件存储所有设置数据的仓库实现 +/// +public sealed class UnifiedSettingsRepository( + IStorage? storage, + IRuntimeTypeSerializer? serializer, + DataRepositoryOptions? options = null, + string fileName = "settings.json") + : AbstractContextUtility, IDataRepository +{ + private readonly Dictionary _cache = new(); + private readonly SemaphoreSlim _lock = new(1, 1); + private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions(); + private bool _loaded; + private IRuntimeTypeSerializer? _serializer = serializer; + private IStorage? _storage = storage; + + private IStorage Storage => + _storage ?? throw new InvalidOperationException("IStorage not initialized."); + + 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))); + } + + protected override void OnInit() + { + _storage ??= this.GetUtility()!; + _serializer ??= this.GetUtility()!; + } + + // ========================= + // Internals + // ========================= + + /// + /// 确保数据已从存储中加载到缓存 + /// + private async Task EnsureLoadedAsync() + { + if (_loaded) return; + + await _lock.WaitAsync(); + try + { + if (_loaded) return; + + if (await Storage.ExistsAsync(GetUnifiedKey())) + { + var data = await Storage.ReadAsync>(GetUnifiedKey()); + _cache.Clear(); + foreach (var (k, v) in data) + _cache[k] = v; + } + + _loaded = true; + } + finally + { + _lock.Release(); + } + } + + /// + /// 将缓存中的所有数据保存到统一文件 + /// + private async Task SaveUnifiedFileAsync() + { + await _lock.WaitAsync(); + try + { + await Storage.WriteAsync(GetUnifiedKey(), _cache); + } + finally + { + _lock.Release(); + } + } + + /// + /// 获取统一文件的存储键名 + /// + /// 完整的存储键名 + private string GetUnifiedKey() + { + var name = string.IsNullOrEmpty(_options.KeyPrefix) ? fileName : $"{_options.KeyPrefix}_{fileName}"; + return string.IsNullOrEmpty(_options.BasePath) ? name : $"{_options.BasePath.TrimEnd('/')}/{name}"; + } + + /// + /// 获取类型的唯一标识键 + /// + /// 要获取键的类型 + /// 类型的全名作为键 + private static string GetTypeKey(Type type) + => type.FullName!; // ⚠️ 刻意不用 AssemblyQualifiedName +} \ No newline at end of file diff --git a/GFramework.Game/serializer/JsonSerializer.cs b/GFramework.Game/serializer/JsonSerializer.cs index 925ce31..7ded055 100644 --- a/GFramework.Game/serializer/JsonSerializer.cs +++ b/GFramework.Game/serializer/JsonSerializer.cs @@ -6,28 +6,46 @@ namespace GFramework.Game.serializer; /// /// JSON序列化器实现类,用于将对象序列化为JSON字符串或将JSON字符串反序列化为对象 /// -public sealed class JsonSerializer : ISerializer +public sealed class JsonSerializer + : IRuntimeTypeSerializer { /// - /// 将指定的对象序列化为JSON字符串 + /// 将指定类型的对象序列化为JSON字符串 /// /// 要序列化的对象类型 /// 要序列化的对象实例 /// 序列化后的JSON字符串 public string Serialize(T value) - { - return JsonConvert.SerializeObject(value); - } + => JsonConvert.SerializeObject(value); /// /// 将JSON字符串反序列化为指定类型的对象 /// - /// 要反序列化的对象类型 - /// 包含JSON数据的字符串 + /// 要反序列化的目标类型 + /// 要反序列化的JSON字符串数据 /// 反序列化后的对象实例 /// 当无法反序列化数据时抛出 public T Deserialize(string data) - { - return JsonConvert.DeserializeObject(data) ?? throw new ArgumentException("Cannot deserialize data"); - } + => JsonConvert.DeserializeObject(data) + ?? throw new ArgumentException("Cannot deserialize data"); + + /// + /// 将对象序列化为JSON字符串(使用运行时类型) + /// + /// 要序列化的对象实例 + /// 对象的运行时类型 + /// 序列化后的JSON字符串 + public string Serialize(object obj, Type type) + => JsonConvert.SerializeObject(obj, type, null); + + /// + /// 将JSON字符串反序列化为指定类型的对象(使用运行时类型) + /// + /// 要反序列化的JSON字符串数据 + /// 反序列化目标类型 + /// 反序列化后的对象实例 + /// 当无法反序列化到指定类型时抛出 + public object Deserialize(string data, Type type) + => JsonConvert.DeserializeObject(data, type) + ?? throw new ArgumentException($"Cannot deserialize to {type.Name}"); } \ No newline at end of file