From 807dbc482e025e27408af4500fde87dd90b6f87a Mon Sep 17 00:00:00 2001
From: GwWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Mon, 12 Jan 2026 21:07:27 +0800
Subject: [PATCH] =?UTF-8?q?feat(setting):=20=E6=B7=BB=E5=8A=A0=E8=AE=BE?=
=?UTF-8?q?=E7=BD=AE=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F=E5=92=8CGodot?=
=?UTF-8?q?=E5=B9=B3=E5=8F=B0=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现了SettingsModel用于管理应用程序设置部分
- 创建了SettingsSystem用于应用各种设置配置
- 添加了AudioSettings和GraphicsSettings基础设置类
- 定义了ISettingsModel、ISettingsSystem等核心接口
- 实现了GodotAudioApplier用于应用音频设置到Godot音频系统
- 创建了GodotGraphicsSettings用于管理游戏图形显示设置
- 添加了GodotFileStorage特化文件存储实现
- 实现了Godot路径扩展方法IsUserPath、IsResPath、IsGodotPath
- 添加了AudioBusMap音频总线映射配置类
---
.../setting/AudioSettings.cs | 22 ++
.../setting/GraphicsSettings.cs | 22 ++
.../setting/IApplyAbleSettings.cs | 14 +
.../setting/ISettingsModel.cs | 38 +++
.../setting/ISettingsSection.cs | 7 +
.../setting/ISettingsSystem.cs | 32 +++
GFramework.Game/setting/SettingsModel.cs | 67 +++++
GFramework.Game/setting/SettingsSystem.cs | 89 ++++++
GFramework.Godot/GFramework.Godot.csproj | 1 +
.../extensions/GodotPathExtensions.cs | 22 ++
GFramework.Godot/setting/AudioBusMap.cs | 22 ++
GFramework.Godot/setting/GodotAudioApplier.cs | 46 +++
.../setting/GodotAudioSettings.cs | 12 +
.../setting/GodotGraphicsSettings.cs | 43 +++
GFramework.Godot/storage/GodotFileStorage.cs | 266 ++++++++++++++++++
15 files changed, 703 insertions(+)
create mode 100644 GFramework.Game.Abstractions/setting/AudioSettings.cs
create mode 100644 GFramework.Game.Abstractions/setting/GraphicsSettings.cs
create mode 100644 GFramework.Game.Abstractions/setting/IApplyAbleSettings.cs
create mode 100644 GFramework.Game.Abstractions/setting/ISettingsModel.cs
create mode 100644 GFramework.Game.Abstractions/setting/ISettingsSection.cs
create mode 100644 GFramework.Game.Abstractions/setting/ISettingsSystem.cs
create mode 100644 GFramework.Game/setting/SettingsModel.cs
create mode 100644 GFramework.Game/setting/SettingsSystem.cs
create mode 100644 GFramework.Godot/extensions/GodotPathExtensions.cs
create mode 100644 GFramework.Godot/setting/AudioBusMap.cs
create mode 100644 GFramework.Godot/setting/GodotAudioApplier.cs
create mode 100644 GFramework.Godot/setting/GodotAudioSettings.cs
create mode 100644 GFramework.Godot/setting/GodotGraphicsSettings.cs
create mode 100644 GFramework.Godot/storage/GodotFileStorage.cs
diff --git a/GFramework.Game.Abstractions/setting/AudioSettings.cs b/GFramework.Game.Abstractions/setting/AudioSettings.cs
new file mode 100644
index 0000000..280b424
--- /dev/null
+++ b/GFramework.Game.Abstractions/setting/AudioSettings.cs
@@ -0,0 +1,22 @@
+namespace GFramework.Game.Abstractions.setting;
+
+///
+/// 音频设置类,用于管理游戏中的音频配置
+///
+public class AudioSettings : ISettingsSection
+{
+ ///
+ /// 获取或设置主音量,控制所有音频的总体音量
+ ///
+ public float MasterVolume { get; set; } = 1.0f;
+
+ ///
+ /// 获取或设置背景音乐音量,控制BGM的播放音量
+ ///
+ public float BgmVolume { get; set; } = 0.8f;
+
+ ///
+ /// 获取或设置音效音量,控制SFX的播放音量
+ ///
+ public float SfxVolume { get; set; } = 0.8f;
+}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/setting/GraphicsSettings.cs b/GFramework.Game.Abstractions/setting/GraphicsSettings.cs
new file mode 100644
index 0000000..4f85685
--- /dev/null
+++ b/GFramework.Game.Abstractions/setting/GraphicsSettings.cs
@@ -0,0 +1,22 @@
+namespace GFramework.Game.Abstractions.setting;
+
+///
+/// 图形设置类,用于管理游戏的图形相关配置
+///
+public class GraphicsSettings : ISettingsSection
+{
+ ///
+ /// 获取或设置是否启用全屏模式
+ ///
+ public bool Fullscreen { get; set; } = false;
+
+ ///
+ /// 获取或设置屏幕分辨率宽度
+ ///
+ public int ResolutionWidth { get; set; } = 1920;
+
+ ///
+ /// 获取或设置屏幕分辨率高度
+ ///
+ public int ResolutionHeight { get; set; } = 1080;
+}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/setting/IApplyAbleSettings.cs b/GFramework.Game.Abstractions/setting/IApplyAbleSettings.cs
new file mode 100644
index 0000000..5f74b21
--- /dev/null
+++ b/GFramework.Game.Abstractions/setting/IApplyAbleSettings.cs
@@ -0,0 +1,14 @@
+using System.Threading.Tasks;
+
+namespace GFramework.Game.Abstractions.setting;
+
+///
+/// 定义可应用设置的接口,继承自ISettingsSection
+///
+public interface IApplyAbleSettings : ISettingsSection
+{
+ ///
+ /// 应用当前设置到系统中
+ ///
+ Task Apply();
+}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/setting/ISettingsModel.cs b/GFramework.Game.Abstractions/setting/ISettingsModel.cs
new file mode 100644
index 0000000..30895f3
--- /dev/null
+++ b/GFramework.Game.Abstractions/setting/ISettingsModel.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using GFramework.Core.Abstractions.model;
+
+namespace GFramework.Game.Abstractions.setting;
+
+///
+/// 定义设置模型的接口,提供获取特定类型设置节的功能
+///
+public interface ISettingsModel : IModel
+{
+ ///
+ /// 获取指定类型的设置节实例
+ ///
+ /// 设置节的类型,必须是class、实现ISettingsSection接口且具有无参构造函数
+ /// 指定类型的设置节实例
+ T Get() where T : class, ISettingsSection, new();
+
+ ///
+ /// 尝试获取指定类型的设置节实例
+ ///
+ /// 要获取的设置节类型
+ /// 输出参数,如果成功则包含找到的设置节实例,否则为null
+ /// 如果找到指定类型的设置节则返回true,否则返回false
+ bool TryGet(Type type, out ISettingsSection section);
+
+ ///
+ /// 获取所有设置节的集合
+ ///
+ /// 包含所有设置节的可枚举集合
+ IEnumerable All();
+
+ ///
+ /// 注册一个可应用的设置对象
+ ///
+ /// 要注册的可应用设置对象
+ void Register(IApplyAbleSettings applyAble);
+}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/setting/ISettingsSection.cs b/GFramework.Game.Abstractions/setting/ISettingsSection.cs
new file mode 100644
index 0000000..a6b8916
--- /dev/null
+++ b/GFramework.Game.Abstractions/setting/ISettingsSection.cs
@@ -0,0 +1,7 @@
+namespace GFramework.Game.Abstractions.setting;
+
+///
+/// 表示游戏设置的一个配置节接口
+/// 该接口定义了设置配置节的基本契约,用于管理游戏中的各种配置选项
+///
+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
new file mode 100644
index 0000000..9912c84
--- /dev/null
+++ b/GFramework.Game.Abstractions/setting/ISettingsSystem.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using GFramework.Core.Abstractions.system;
+
+namespace GFramework.Game.Abstractions.setting;
+
+///
+/// 定义设置系统的接口,提供应用各种设置的方法
+///
+public interface ISettingsSystem : ISystem
+{
+ ///
+ /// 应用所有可应用的设置
+ ///
+ Task ApplyAll();
+
+ ///
+ /// 应用指定类型的设置
+ ///
+ Task Apply(Type settingsType);
+
+ ///
+ /// 应用指定类型的设置(泛型版本)
+ ///
+ Task Apply() where T : class, ISettingsSection;
+
+ ///
+ /// 批量应用多个设置类型
+ ///
+ Task Apply(IEnumerable settingsTypes);
+}
\ No newline at end of file
diff --git a/GFramework.Game/setting/SettingsModel.cs b/GFramework.Game/setting/SettingsModel.cs
new file mode 100644
index 0000000..5d35785
--- /dev/null
+++ b/GFramework.Game/setting/SettingsModel.cs
@@ -0,0 +1,67 @@
+using GFramework.Core.model;
+using GFramework.Game.Abstractions.setting;
+
+namespace GFramework.Game.setting;
+
+///
+/// 设置模型类,用于管理不同类型的应用程序设置部分
+///
+public class SettingsModel : AbstractModel, ISettingsModel
+{
+ private readonly Dictionary _sections = new();
+
+ ///
+ /// 获取指定类型的设置部分实例,如果不存在则创建新的实例
+ ///
+ /// 设置部分的类型,必须实现ISettingsSection接口并具有无参构造函数
+ /// 指定类型的设置部分实例
+ public T Get() where T : class, ISettingsSection, new()
+ {
+ var type = typeof(T);
+
+ // 尝试从字典中获取已存在的设置部分实例
+ if (_sections.TryGetValue(type, out var existing))
+ return (T)existing;
+
+ // 创建新的设置部分实例并存储到字典中
+ var created = new T();
+ _sections[type] = created;
+ return created;
+ }
+
+ ///
+ /// 尝试获取指定类型的设置部分实例
+ ///
+ /// 设置部分的类型
+ /// 输出参数,如果找到则返回对应的设置部分实例,否则为null
+ /// 如果找到指定类型的设置部分则返回true,否则返回false
+ public bool TryGet(Type type, out ISettingsSection section)
+ => _sections.TryGetValue(type, out section!);
+
+ ///
+ /// 获取所有设置部分的集合
+ ///
+ /// 包含所有设置部分的可枚举集合
+ public IEnumerable All()
+ => _sections.Values;
+
+ ///
+ /// 注册一个可应用的设置对象到管理器中
+ ///
+ /// 要注册的可应用设置对象
+ public void Register(IApplyAbleSettings applyAble)
+ {
+ // 获取传入对象的类型信息
+ var type = applyAble.GetType();
+ // 尝试将类型和对象添加到线程安全的字典中
+ _sections.TryAdd(type, applyAble);
+ }
+
+
+ ///
+ /// 初始化方法,用于执行模型的初始化逻辑
+ ///
+ protected override void OnInit()
+ {
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Game/setting/SettingsSystem.cs b/GFramework.Game/setting/SettingsSystem.cs
new file mode 100644
index 0000000..3c07c9d
--- /dev/null
+++ b/GFramework.Game/setting/SettingsSystem.cs
@@ -0,0 +1,89 @@
+using GFramework.Core.extensions;
+using GFramework.Core.system;
+using GFramework.Game.Abstractions.setting;
+
+namespace GFramework.Game.setting;
+
+///
+/// 设置系统,负责管理和应用各种设置配置
+///
+public class SettingsSystem : AbstractSystem, ISettingsSystem
+{
+ private ISettingsModel _model = null!;
+
+ ///
+ /// 应用所有设置配置
+ ///
+ /// 完成的任务
+ public Task ApplyAll()
+ {
+ // 遍历所有设置配置并尝试应用
+ foreach (var section in _model.All())
+ {
+ TryApply(section);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// 应用指定类型的设置配置
+ ///
+ /// 设置配置类型,必须是类且实现ISettingsSection接口
+ /// 完成的任务
+ public Task Apply() where T : class, ISettingsSection
+ => Apply(typeof(T));
+
+ ///
+ /// 应用指定类型的设置配置
+ ///
+ /// 设置配置类型
+ /// 完成的任务
+ public Task Apply(Type settingsType)
+ {
+ if (!_model.TryGet(settingsType, out var section))
+ return Task.CompletedTask;
+
+ TryApply(section);
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// 应用指定类型集合的设置配置
+ ///
+ /// 设置配置类型集合
+ /// 完成的任务
+ public Task Apply(IEnumerable settingsTypes)
+ {
+ // 去重后遍历设置类型,获取并应用对应的设置配置
+ foreach (var type in settingsTypes.Distinct())
+ {
+ if (_model.TryGet(type, out var section))
+ {
+ TryApply(section);
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// 初始化设置系统,获取设置模型实例
+ ///
+ protected override void OnInit()
+ {
+ _model = this.GetModel()!;
+ }
+
+ ///
+ /// 尝试应用可应用的设置配置
+ ///
+ /// 设置配置对象
+ private static void TryApply(ISettingsSection section)
+ {
+ if (section is IApplyAbleSettings applyable)
+ {
+ applyable.Apply();
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Godot/GFramework.Godot.csproj b/GFramework.Godot/GFramework.Godot.csproj
index 3a0e22a..df05907 100644
--- a/GFramework.Godot/GFramework.Godot.csproj
+++ b/GFramework.Godot/GFramework.Godot.csproj
@@ -19,6 +19,7 @@
+
diff --git a/GFramework.Godot/extensions/GodotPathExtensions.cs b/GFramework.Godot/extensions/GodotPathExtensions.cs
new file mode 100644
index 0000000..97f0edf
--- /dev/null
+++ b/GFramework.Godot/extensions/GodotPathExtensions.cs
@@ -0,0 +1,22 @@
+namespace GFramework.Godot.extensions;
+
+public static class GodotPathExtensions
+{
+ ///
+ /// 判断是否是 Godot 用户数据路径(user://)
+ ///
+ public static bool IsUserPath(this string path)
+ => !string.IsNullOrEmpty(path) && path.StartsWith("user://");
+
+ ///
+ /// 判断是否是 Godot 资源路径(res://)
+ ///
+ public static bool IsResPath(this string path)
+ => !string.IsNullOrEmpty(path) && path.StartsWith("res://");
+
+ ///
+ /// 判断是否是 Godot 特殊路径(user:// 或 res://)
+ ///
+ public static bool IsGodotPath(this string path)
+ => path.IsUserPath() || path.IsResPath();
+}
\ No newline at end of file
diff --git a/GFramework.Godot/setting/AudioBusMap.cs b/GFramework.Godot/setting/AudioBusMap.cs
new file mode 100644
index 0000000..6b9ddc0
--- /dev/null
+++ b/GFramework.Godot/setting/AudioBusMap.cs
@@ -0,0 +1,22 @@
+namespace GFramework.Godot.setting;
+
+///
+/// 音频总线映射配置类,用于定义音频系统中不同类型的音频总线名称
+///
+public sealed class AudioBusMap
+{
+ ///
+ /// 主音频总线名称,默认值为"Master"
+ ///
+ public string Master { get; init; } = "Master";
+
+ ///
+ /// 背景音乐音频总线名称,默认值为"BGM"
+ ///
+ public string Bgm { get; init; } = "BGM";
+
+ ///
+ /// 音效音频总线名称,默认值为"SFX"
+ ///
+ public string Sfx { get; init; } = "SFX";
+}
\ No newline at end of file
diff --git a/GFramework.Godot/setting/GodotAudioApplier.cs b/GFramework.Godot/setting/GodotAudioApplier.cs
new file mode 100644
index 0000000..1302cad
--- /dev/null
+++ b/GFramework.Godot/setting/GodotAudioApplier.cs
@@ -0,0 +1,46 @@
+using GFramework.Game.Abstractions.setting;
+using Godot;
+
+namespace GFramework.Godot.setting;
+
+///
+/// Godot音频设置应用器,用于将音频设置应用到Godot引擎的音频总线系统
+///
+/// 音频设置对象,包含主音量、背景音乐音量和音效音量
+/// 音频总线映射对象,定义了不同音频类型的总线名称
+public sealed class GodotAudioApplier(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
+{
+ ///
+ /// 应用音频设置到Godot音频系统
+ ///
+ /// 表示异步操作的任务
+ public Task Apply()
+ {
+ SetBus(busMap.Master, settings.MasterVolume);
+ SetBus(busMap.Bgm, settings.BgmVolume);
+ SetBus(busMap.Sfx, settings.SfxVolume);
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// 设置指定音频总线的音量
+ ///
+ /// 音频总线名称
+ /// 线性音量值(0-1之间)
+ private static void SetBus(string busName, float linear)
+ {
+ // 获取音频总线索引
+ var idx = AudioServer.GetBusIndex(busName);
+ if (idx < 0)
+ {
+ GD.PushWarning($"Audio bus not found: {busName}");
+ return;
+ }
+
+ // 将线性音量转换为分贝并设置到音频总线
+ AudioServer.SetBusVolumeDb(
+ idx,
+ Mathf.LinearToDb(Mathf.Clamp(linear, 0.0001f, 1f))
+ );
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Godot/setting/GodotAudioSettings.cs b/GFramework.Godot/setting/GodotAudioSettings.cs
new file mode 100644
index 0000000..dc968e0
--- /dev/null
+++ b/GFramework.Godot/setting/GodotAudioSettings.cs
@@ -0,0 +1,12 @@
+
+using GFramework.Game.Abstractions.setting;
+
+namespace GFramework.Godot.setting;
+
+public class GodotAudioSettings: AudioSettings,IApplyAbleSettings
+{
+ public Task Apply()
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Godot/setting/GodotGraphicsSettings.cs b/GFramework.Godot/setting/GodotGraphicsSettings.cs
new file mode 100644
index 0000000..4c3c34e
--- /dev/null
+++ b/GFramework.Godot/setting/GodotGraphicsSettings.cs
@@ -0,0 +1,43 @@
+using GFramework.Game.Abstractions.setting;
+using Godot;
+
+namespace GFramework.Godot.setting;
+
+///
+/// Godot图形设置类,继承自GraphicsSettings并实现IApplyAbleSettings接口
+/// 用于管理游戏的图形显示设置,包括分辨率、全屏模式等
+///
+public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings
+{
+ ///
+ /// 异步应用当前图形设置到游戏窗口
+ /// 该方法会根据设置的分辨率、全屏状态等参数调整Godot窗口的显示属性
+ ///
+ /// 表示异步操作的任务
+ public async Task Apply()
+ {
+ var size = new Vector2I(ResolutionWidth, ResolutionHeight);
+
+ // 直接调用DisplayServer API,不使用异步或延迟
+ // 1. 设置边框标志
+ DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, Fullscreen);
+
+ // 2. 设置窗口模式
+ DisplayServer.WindowSetMode(
+ Fullscreen ? DisplayServer.WindowMode.ExclusiveFullscreen : DisplayServer.WindowMode.Windowed
+ );
+
+ // 3. 窗口化下设置尺寸和位置
+ if (!Fullscreen)
+ {
+ DisplayServer.WindowSetSize(size);
+ // 居中窗口
+ var screen = DisplayServer.GetPrimaryScreen();
+ var screenSize = DisplayServer.ScreenGetSize(screen);
+ var pos = (screenSize - size) / 2;
+ DisplayServer.WindowSetPosition(pos);
+ }
+
+ await Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Godot/storage/GodotFileStorage.cs b/GFramework.Godot/storage/GodotFileStorage.cs
new file mode 100644
index 0000000..6f8cde3
--- /dev/null
+++ b/GFramework.Godot/storage/GodotFileStorage.cs
@@ -0,0 +1,266 @@
+using System.Collections.Concurrent;
+using System.Text;
+using GFramework.Core.Abstractions.storage;
+using GFramework.Game.Abstractions.serializer;
+using GFramework.Godot.extensions;
+using FileAccess = Godot.FileAccess;
+
+namespace GFramework.Godot.storage;
+
+///
+/// Godot 特化的文件存储实现,支持 res://、user:// 和普通文件路径
+/// 支持按 key 细粒度锁保证线程安全
+///
+public sealed class GodotFileStorage : IStorage
+{
+ ///
+ /// 每个 key 对应的锁对象
+ ///
+ private readonly ConcurrentDictionary _keyLocks = new();
+
+ private readonly ISerializer _serializer;
+
+ ///
+ /// 初始化 Godot 文件存储
+ ///
+ /// 序列化器实例
+ public GodotFileStorage(ISerializer serializer)
+ {
+ _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
+ }
+
+ #region Delete
+
+ ///
+ /// 删除指定键对应的文件
+ ///
+ /// 存储键
+ public void Delete(string key)
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// 清理路径段中的无效字符,将无效文件名字符替换为下划线
+ ///
+ /// 要清理的路径段
+ /// 清理后的路径段
+ private static string SanitizeSegment(string segment)
+ => Path.GetInvalidFileNameChars().Aggregate(segment, (current, c) => current.Replace(c, '_'));
+
+ ///
+ /// 将存储键转换为绝对路径,处理 Godot 虚拟路径和普通文件系统路径
+ ///
+ /// 存储键
+ /// 绝对路径字符串
+ private static string ToAbsolutePath(string key)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ throw new ArgumentException("Storage key cannot be empty", nameof(key));
+
+ key = key.Replace('\\', '/');
+
+ if (key.Contains(".."))
+ throw new ArgumentException("Storage key cannot contain '..'", nameof(key));
+
+ // Godot 虚拟路径直接使用 FileAccess 支持
+ if (key.IsGodotPath())
+ return key;
+
+ // 普通文件系统路径
+ var segments = key.Split('/', StringSplitOptions.RemoveEmptyEntries)
+ .Select(SanitizeSegment)
+ .ToArray();
+
+ if (segments.Length == 0)
+ throw new ArgumentException("Invalid storage key", nameof(key));
+
+ var dir = Path.Combine(segments[..^1]);
+ var fileName = segments[^1];
+
+ if (!string.IsNullOrEmpty(dir))
+ Directory.CreateDirectory(dir);
+
+ return Path.Combine(dir, fileName);
+ }
+
+ ///
+ /// 获取指定路径对应的锁对象,如果不存在则创建新的锁对象
+ ///
+ /// 文件路径
+ /// 对应路径的锁对象
+ private object GetLock(string path) => _keyLocks.GetOrAdd(path, _ => new object());
+
+ #endregion
+
+ #region Exists
+
+ ///
+ /// 检查指定键对应的文件是否存在
+ ///
+ /// 存储键
+ /// 文件存在返回 true,否则返回 false
+ public bool Exists(string key)
+ {
+ var path = ToAbsolutePath(key);
+ var keyLock = GetLock(path);
+
+ lock (keyLock)
+ {
+ if (!path.IsGodotPath()) return File.Exists(path);
+ using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
+ return file != null;
+ }
+ }
+
+ ///
+ /// 异步检查指定键对应的文件是否存在
+ ///
+ /// 存储键
+ /// 表示异步操作的任务,结果为布尔值表示文件是否存在
+ public Task ExistsAsync(string key)
+ => Task.FromResult(Exists(key));
+
+ #endregion
+
+ #region Read
+
+ ///
+ /// 读取指定键对应的序列化数据并反序列化为指定类型
+ ///
+ /// 要反序列化的类型
+ /// 存储键
+ /// 反序列化后的对象实例
+ /// 当指定键对应的文件不存在时抛出
+ public T Read(string key)
+ {
+ var path = ToAbsolutePath(key);
+ var keyLock = GetLock(path);
+
+ lock (keyLock)
+ {
+ string content;
+
+ if (path.IsGodotPath())
+ {
+ using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
+ if (file == null) throw new FileNotFoundException($"Storage key not found: {key}", path);
+ content = file.GetAsText();
+ }
+ else
+ {
+ if (!File.Exists(path))
+ throw new FileNotFoundException($"Storage key not found: {key}", path);
+ content = File.ReadAllText(path, Encoding.UTF8);
+ }
+
+ return _serializer.Deserialize(content);
+ }
+ }
+
+ ///
+ /// 读取指定键对应的序列化数据,如果文件不存在则返回默认值
+ ///
+ /// 要反序列化的类型
+ /// 存储键
+ /// 当文件不存在时返回的默认值
+ /// 反序列化后的对象实例或默认值
+ public T Read(string key, T defaultValue)
+ {
+ var path = ToAbsolutePath(key);
+ var keyLock = GetLock(path);
+
+ lock (keyLock)
+ {
+ if (path.IsGodotPath() && !FileAccess.FileExists(path) || !path.IsGodotPath() && !File.Exists(path))
+ return defaultValue;
+
+ return Read(key);
+ }
+ }
+
+ ///
+ /// 异步读取指定键对应的序列化数据并反序列化为指定类型
+ ///
+ /// 要反序列化的类型
+ /// 存储键
+ /// 表示异步操作的任务,结果为反序列化后的对象实例
+ public async Task ReadAsync(string key)
+ {
+ var path = ToAbsolutePath(key);
+ var keyLock = GetLock(path);
+
+ return await Task.Run(() =>
+ {
+ lock (keyLock)
+ {
+ string content;
+
+ if (path.IsGodotPath())
+ {
+ using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
+ if (file == null) throw new FileNotFoundException($"Storage key not found: {key}", path);
+ content = file.GetAsText();
+ }
+ else
+ {
+ if (!File.Exists(path))
+ throw new FileNotFoundException($"Storage key not found: {key}", path);
+ content = File.ReadAllText(path, Encoding.UTF8);
+ }
+
+ return _serializer.Deserialize(content);
+ }
+ });
+ }
+
+ #endregion
+
+ #region Write
+
+ ///
+ /// 将指定对象序列化并写入到指定键对应的文件中
+ ///
+ /// 要序列化的对象类型
+ /// 存储键
+ /// 要写入的对象实例
+ public void Write(string key, T value)
+ {
+ var path = ToAbsolutePath(key);
+ var keyLock = GetLock(path);
+
+ lock (keyLock)
+ {
+ var content = _serializer.Serialize(value);
+ if (path.IsGodotPath())
+ {
+ using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
+ if (file == null) throw new IOException($"Cannot write file: {path}");
+ file.StoreString(content);
+ }
+ else
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ File.WriteAllText(path, content, Encoding.UTF8);
+ }
+ }
+ }
+
+ ///
+ /// 异步将指定对象序列化并写入到指定键对应的文件中
+ ///
+ /// 要序列化的对象类型
+ /// 存储键
+ /// 要写入的对象实例
+ /// 表示异步操作的任务
+ public async Task WriteAsync(string key, T value)
+ {
+ await Task.Run(() => Write(key, value));
+ }
+
+ #endregion
+}
\ No newline at end of file