mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
feat(setting): 添加设置管理系统和Godot平台实现
- 实现了SettingsModel用于管理应用程序设置部分 - 创建了SettingsSystem用于应用各种设置配置 - 添加了AudioSettings和GraphicsSettings基础设置类 - 定义了ISettingsModel、ISettingsSystem等核心接口 - 实现了GodotAudioApplier用于应用音频设置到Godot音频系统 - 创建了GodotGraphicsSettings用于管理游戏图形显示设置 - 添加了GodotFileStorage特化文件存储实现 - 实现了Godot路径扩展方法IsUserPath、IsResPath、IsGodotPath - 添加了AudioBusMap音频总线映射配置类
This commit is contained in:
parent
5877dae8fe
commit
807dbc482e
22
GFramework.Game.Abstractions/setting/AudioSettings.cs
Normal file
22
GFramework.Game.Abstractions/setting/AudioSettings.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace GFramework.Game.Abstractions.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 音频设置类,用于管理游戏中的音频配置
|
||||
/// </summary>
|
||||
public class AudioSettings : ISettingsSection
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置主音量,控制所有音频的总体音量
|
||||
/// </summary>
|
||||
public float MasterVolume { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置背景音乐音量,控制BGM的播放音量
|
||||
/// </summary>
|
||||
public float BgmVolume { get; set; } = 0.8f;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置音效音量,控制SFX的播放音量
|
||||
/// </summary>
|
||||
public float SfxVolume { get; set; } = 0.8f;
|
||||
}
|
||||
22
GFramework.Game.Abstractions/setting/GraphicsSettings.cs
Normal file
22
GFramework.Game.Abstractions/setting/GraphicsSettings.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace GFramework.Game.Abstractions.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 图形设置类,用于管理游戏的图形相关配置
|
||||
/// </summary>
|
||||
public class GraphicsSettings : ISettingsSection
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置是否启用全屏模式
|
||||
/// </summary>
|
||||
public bool Fullscreen { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置屏幕分辨率宽度
|
||||
/// </summary>
|
||||
public int ResolutionWidth { get; set; } = 1920;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置屏幕分辨率高度
|
||||
/// </summary>
|
||||
public int ResolutionHeight { get; set; } = 1080;
|
||||
}
|
||||
14
GFramework.Game.Abstractions/setting/IApplyAbleSettings.cs
Normal file
14
GFramework.Game.Abstractions/setting/IApplyAbleSettings.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GFramework.Game.Abstractions.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 定义可应用设置的接口,继承自ISettingsSection
|
||||
/// </summary>
|
||||
public interface IApplyAbleSettings : ISettingsSection
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用当前设置到系统中
|
||||
/// </summary>
|
||||
Task Apply();
|
||||
}
|
||||
38
GFramework.Game.Abstractions/setting/ISettingsModel.cs
Normal file
38
GFramework.Game.Abstractions/setting/ISettingsModel.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Core.Abstractions.model;
|
||||
|
||||
namespace GFramework.Game.Abstractions.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 定义设置模型的接口,提供获取特定类型设置节的功能
|
||||
/// </summary>
|
||||
public interface ISettingsModel : IModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取指定类型的设置节实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置节的类型,必须是class、实现ISettingsSection接口且具有无参构造函数</typeparam>
|
||||
/// <returns>指定类型的设置节实例</returns>
|
||||
T Get<T>() where T : class, ISettingsSection, new();
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取指定类型的设置节实例
|
||||
/// </summary>
|
||||
/// <param name="type">要获取的设置节类型</param>
|
||||
/// <param name="section">输出参数,如果成功则包含找到的设置节实例,否则为null</param>
|
||||
/// <returns>如果找到指定类型的设置节则返回true,否则返回false</returns>
|
||||
bool TryGet(Type type, out ISettingsSection section);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有设置节的集合
|
||||
/// </summary>
|
||||
/// <returns>包含所有设置节的可枚举集合</returns>
|
||||
IEnumerable<ISettingsSection> All();
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个可应用的设置对象
|
||||
/// </summary>
|
||||
/// <param name="applyAble">要注册的可应用设置对象</param>
|
||||
void Register(IApplyAbleSettings applyAble);
|
||||
}
|
||||
7
GFramework.Game.Abstractions/setting/ISettingsSection.cs
Normal file
7
GFramework.Game.Abstractions/setting/ISettingsSection.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace GFramework.Game.Abstractions.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 表示游戏设置的一个配置节接口
|
||||
/// 该接口定义了设置配置节的基本契约,用于管理游戏中的各种配置选项
|
||||
/// </summary>
|
||||
public interface ISettingsSection;
|
||||
32
GFramework.Game.Abstractions/setting/ISettingsSystem.cs
Normal file
32
GFramework.Game.Abstractions/setting/ISettingsSystem.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Core.Abstractions.system;
|
||||
|
||||
namespace GFramework.Game.Abstractions.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 定义设置系统的接口,提供应用各种设置的方法
|
||||
/// </summary>
|
||||
public interface ISettingsSystem : ISystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用所有可应用的设置
|
||||
/// </summary>
|
||||
Task ApplyAll();
|
||||
|
||||
/// <summary>
|
||||
/// 应用指定类型的设置
|
||||
/// </summary>
|
||||
Task Apply(Type settingsType);
|
||||
|
||||
/// <summary>
|
||||
/// 应用指定类型的设置(泛型版本)
|
||||
/// </summary>
|
||||
Task Apply<T>() where T : class, ISettingsSection;
|
||||
|
||||
/// <summary>
|
||||
/// 批量应用多个设置类型
|
||||
/// </summary>
|
||||
Task Apply(IEnumerable<Type> settingsTypes);
|
||||
}
|
||||
67
GFramework.Game/setting/SettingsModel.cs
Normal file
67
GFramework.Game/setting/SettingsModel.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using GFramework.Core.model;
|
||||
using GFramework.Game.Abstractions.setting;
|
||||
|
||||
namespace GFramework.Game.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 设置模型类,用于管理不同类型的应用程序设置部分
|
||||
/// </summary>
|
||||
public class SettingsModel : AbstractModel, ISettingsModel
|
||||
{
|
||||
private readonly Dictionary<Type, ISettingsSection> _sections = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定类型的设置部分实例,如果不存在则创建新的实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置部分的类型,必须实现ISettingsSection接口并具有无参构造函数</typeparam>
|
||||
/// <returns>指定类型的设置部分实例</returns>
|
||||
public T Get<T>() 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取指定类型的设置部分实例
|
||||
/// </summary>
|
||||
/// <param name="type">设置部分的类型</param>
|
||||
/// <param name="section">输出参数,如果找到则返回对应的设置部分实例,否则为null</param>
|
||||
/// <returns>如果找到指定类型的设置部分则返回true,否则返回false</returns>
|
||||
public bool TryGet(Type type, out ISettingsSection section)
|
||||
=> _sections.TryGetValue(type, out section!);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有设置部分的集合
|
||||
/// </summary>
|
||||
/// <returns>包含所有设置部分的可枚举集合</returns>
|
||||
public IEnumerable<ISettingsSection> All()
|
||||
=> _sections.Values;
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个可应用的设置对象到管理器中
|
||||
/// </summary>
|
||||
/// <param name="applyAble">要注册的可应用设置对象</param>
|
||||
public void Register(IApplyAbleSettings applyAble)
|
||||
{
|
||||
// 获取传入对象的类型信息
|
||||
var type = applyAble.GetType();
|
||||
// 尝试将类型和对象添加到线程安全的字典中
|
||||
_sections.TryAdd(type, applyAble);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 初始化方法,用于执行模型的初始化逻辑
|
||||
/// </summary>
|
||||
protected override void OnInit()
|
||||
{
|
||||
}
|
||||
}
|
||||
89
GFramework.Game/setting/SettingsSystem.cs
Normal file
89
GFramework.Game/setting/SettingsSystem.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Game.Abstractions.setting;
|
||||
|
||||
namespace GFramework.Game.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 设置系统,负责管理和应用各种设置配置
|
||||
/// </summary>
|
||||
public class SettingsSystem : AbstractSystem, ISettingsSystem
|
||||
{
|
||||
private ISettingsModel _model = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 应用所有设置配置
|
||||
/// </summary>
|
||||
/// <returns>完成的任务</returns>
|
||||
public Task ApplyAll()
|
||||
{
|
||||
// 遍历所有设置配置并尝试应用
|
||||
foreach (var section in _model.All())
|
||||
{
|
||||
TryApply(section);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用指定类型的设置配置
|
||||
/// </summary>
|
||||
/// <typeparam name="T">设置配置类型,必须是类且实现ISettingsSection接口</typeparam>
|
||||
/// <returns>完成的任务</returns>
|
||||
public Task Apply<T>() where T : class, ISettingsSection
|
||||
=> Apply(typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// 应用指定类型的设置配置
|
||||
/// </summary>
|
||||
/// <param name="settingsType">设置配置类型</param>
|
||||
/// <returns>完成的任务</returns>
|
||||
public Task Apply(Type settingsType)
|
||||
{
|
||||
if (!_model.TryGet(settingsType, out var section))
|
||||
return Task.CompletedTask;
|
||||
|
||||
TryApply(section);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用指定类型集合的设置配置
|
||||
/// </summary>
|
||||
/// <param name="settingsTypes">设置配置类型集合</param>
|
||||
/// <returns>完成的任务</returns>
|
||||
public Task Apply(IEnumerable<Type> settingsTypes)
|
||||
{
|
||||
// 去重后遍历设置类型,获取并应用对应的设置配置
|
||||
foreach (var type in settingsTypes.Distinct())
|
||||
{
|
||||
if (_model.TryGet(type, out var section))
|
||||
{
|
||||
TryApply(section);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化设置系统,获取设置模型实例
|
||||
/// </summary>
|
||||
protected override void OnInit()
|
||||
{
|
||||
_model = this.GetModel<ISettingsModel>()!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试应用可应用的设置配置
|
||||
/// </summary>
|
||||
/// <param name="section">设置配置对象</param>
|
||||
private static void TryApply(ISettingsSection section)
|
||||
{
|
||||
if (section is IApplyAbleSettings applyable)
|
||||
{
|
||||
applyable.Apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="extensions\ControlExtensions.cs"/>
|
||||
<Compile Remove="setting\GodotAudioSettings.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
22
GFramework.Godot/extensions/GodotPathExtensions.cs
Normal file
22
GFramework.Godot/extensions/GodotPathExtensions.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace GFramework.Godot.extensions;
|
||||
|
||||
public static class GodotPathExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断是否是 Godot 用户数据路径(user://)
|
||||
/// </summary>
|
||||
public static bool IsUserPath(this string path)
|
||||
=> !string.IsNullOrEmpty(path) && path.StartsWith("user://");
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否是 Godot 资源路径(res://)
|
||||
/// </summary>
|
||||
public static bool IsResPath(this string path)
|
||||
=> !string.IsNullOrEmpty(path) && path.StartsWith("res://");
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否是 Godot 特殊路径(user:// 或 res://)
|
||||
/// </summary>
|
||||
public static bool IsGodotPath(this string path)
|
||||
=> path.IsUserPath() || path.IsResPath();
|
||||
}
|
||||
22
GFramework.Godot/setting/AudioBusMap.cs
Normal file
22
GFramework.Godot/setting/AudioBusMap.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace GFramework.Godot.setting;
|
||||
|
||||
/// <summary>
|
||||
/// 音频总线映射配置类,用于定义音频系统中不同类型的音频总线名称
|
||||
/// </summary>
|
||||
public sealed class AudioBusMap
|
||||
{
|
||||
/// <summary>
|
||||
/// 主音频总线名称,默认值为"Master"
|
||||
/// </summary>
|
||||
public string Master { get; init; } = "Master";
|
||||
|
||||
/// <summary>
|
||||
/// 背景音乐音频总线名称,默认值为"BGM"
|
||||
/// </summary>
|
||||
public string Bgm { get; init; } = "BGM";
|
||||
|
||||
/// <summary>
|
||||
/// 音效音频总线名称,默认值为"SFX"
|
||||
/// </summary>
|
||||
public string Sfx { get; init; } = "SFX";
|
||||
}
|
||||
46
GFramework.Godot/setting/GodotAudioApplier.cs
Normal file
46
GFramework.Godot/setting/GodotAudioApplier.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using GFramework.Game.Abstractions.setting;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.setting;
|
||||
|
||||
/// <summary>
|
||||
/// Godot音频设置应用器,用于将音频设置应用到Godot引擎的音频总线系统
|
||||
/// </summary>
|
||||
/// <param name="settings">音频设置对象,包含主音量、背景音乐音量和音效音量</param>
|
||||
/// <param name="busMap">音频总线映射对象,定义了不同音频类型的总线名称</param>
|
||||
public sealed class GodotAudioApplier(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用音频设置到Godot音频系统
|
||||
/// </summary>
|
||||
/// <returns>表示异步操作的任务</returns>
|
||||
public Task Apply()
|
||||
{
|
||||
SetBus(busMap.Master, settings.MasterVolume);
|
||||
SetBus(busMap.Bgm, settings.BgmVolume);
|
||||
SetBus(busMap.Sfx, settings.SfxVolume);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定音频总线的音量
|
||||
/// </summary>
|
||||
/// <param name="busName">音频总线名称</param>
|
||||
/// <param name="linear">线性音量值(0-1之间)</param>
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
12
GFramework.Godot/setting/GodotAudioSettings.cs
Normal file
12
GFramework.Godot/setting/GodotAudioSettings.cs
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
using GFramework.Game.Abstractions.setting;
|
||||
|
||||
namespace GFramework.Godot.setting;
|
||||
|
||||
public class GodotAudioSettings: AudioSettings,IApplyAbleSettings
|
||||
{
|
||||
public Task Apply()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
43
GFramework.Godot/setting/GodotGraphicsSettings.cs
Normal file
43
GFramework.Godot/setting/GodotGraphicsSettings.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using GFramework.Game.Abstractions.setting;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.setting;
|
||||
|
||||
/// <summary>
|
||||
/// Godot图形设置类,继承自GraphicsSettings并实现IApplyAbleSettings接口
|
||||
/// 用于管理游戏的图形显示设置,包括分辨率、全屏模式等
|
||||
/// </summary>
|
||||
public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步应用当前图形设置到游戏窗口
|
||||
/// 该方法会根据设置的分辨率、全屏状态等参数调整Godot窗口的显示属性
|
||||
/// </summary>
|
||||
/// <returns>表示异步操作的任务</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
266
GFramework.Godot/storage/GodotFileStorage.cs
Normal file
266
GFramework.Godot/storage/GodotFileStorage.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Godot 特化的文件存储实现,支持 res://、user:// 和普通文件路径
|
||||
/// 支持按 key 细粒度锁保证线程安全
|
||||
/// </summary>
|
||||
public sealed class GodotFileStorage : IStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// 每个 key 对应的锁对象
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, object> _keyLocks = new();
|
||||
|
||||
private readonly ISerializer _serializer;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 Godot 文件存储
|
||||
/// </summary>
|
||||
/// <param name="serializer">序列化器实例</param>
|
||||
public GodotFileStorage(ISerializer serializer)
|
||||
{
|
||||
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
|
||||
}
|
||||
|
||||
#region Delete
|
||||
|
||||
/// <summary>
|
||||
/// 删除指定键对应的文件
|
||||
/// </summary>
|
||||
/// <param name="key">存储键</param>
|
||||
public void Delete(string key)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// 清理路径段中的无效字符,将无效文件名字符替换为下划线
|
||||
/// </summary>
|
||||
/// <param name="segment">要清理的路径段</param>
|
||||
/// <returns>清理后的路径段</returns>
|
||||
private static string SanitizeSegment(string segment)
|
||||
=> Path.GetInvalidFileNameChars().Aggregate(segment, (current, c) => current.Replace(c, '_'));
|
||||
|
||||
/// <summary>
|
||||
/// 将存储键转换为绝对路径,处理 Godot 虚拟路径和普通文件系统路径
|
||||
/// </summary>
|
||||
/// <param name="key">存储键</param>
|
||||
/// <returns>绝对路径字符串</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定路径对应的锁对象,如果不存在则创建新的锁对象
|
||||
/// </summary>
|
||||
/// <param name="path">文件路径</param>
|
||||
/// <returns>对应路径的锁对象</returns>
|
||||
private object GetLock(string path) => _keyLocks.GetOrAdd(path, _ => new object());
|
||||
|
||||
#endregion
|
||||
|
||||
#region Exists
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定键对应的文件是否存在
|
||||
/// </summary>
|
||||
/// <param name="key">存储键</param>
|
||||
/// <returns>文件存在返回 true,否则返回 false</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步检查指定键对应的文件是否存在
|
||||
/// </summary>
|
||||
/// <param name="key">存储键</param>
|
||||
/// <returns>表示异步操作的任务,结果为布尔值表示文件是否存在</returns>
|
||||
public Task<bool> ExistsAsync(string key)
|
||||
=> Task.FromResult(Exists(key));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定键对应的序列化数据并反序列化为指定类型
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要反序列化的类型</typeparam>
|
||||
/// <param name="key">存储键</param>
|
||||
/// <returns>反序列化后的对象实例</returns>
|
||||
/// <exception cref="FileNotFoundException">当指定键对应的文件不存在时抛出</exception>
|
||||
public T Read<T>(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<T>(content);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定键对应的序列化数据,如果文件不存在则返回默认值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要反序列化的类型</typeparam>
|
||||
/// <param name="key">存储键</param>
|
||||
/// <param name="defaultValue">当文件不存在时返回的默认值</param>
|
||||
/// <returns>反序列化后的对象实例或默认值</returns>
|
||||
public T Read<T>(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<T>(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步读取指定键对应的序列化数据并反序列化为指定类型
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要反序列化的类型</typeparam>
|
||||
/// <param name="key">存储键</param>
|
||||
/// <returns>表示异步操作的任务,结果为反序列化后的对象实例</returns>
|
||||
public async Task<T> ReadAsync<T>(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<T>(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Write
|
||||
|
||||
/// <summary>
|
||||
/// 将指定对象序列化并写入到指定键对应的文件中
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要序列化的对象类型</typeparam>
|
||||
/// <param name="key">存储键</param>
|
||||
/// <param name="value">要写入的对象实例</param>
|
||||
public void Write<T>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步将指定对象序列化并写入到指定键对应的文件中
|
||||
/// </summary>
|
||||
/// <typeparam name="T">要序列化的对象类型</typeparam>
|
||||
/// <param name="key">存储键</param>
|
||||
/// <param name="value">要写入的对象实例</param>
|
||||
/// <returns>表示异步操作的任务</returns>
|
||||
public async Task WriteAsync<T>(string key, T value)
|
||||
{
|
||||
await Task.Run(() => Write(key, value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user