From 0f1319334ea84d5e1faae4397786957caf9d519c Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 11 Apr 2026 08:41:30 +0800 Subject: [PATCH] =?UTF-8?q?docs(config):=20=E6=B7=BB=E5=8A=A0=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E5=86=85=E5=AE=B9=E9=85=8D=E7=BD=AE=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增游戏内容配置系统详细文档,涵盖 YAML 配置、JSON Schema 结构描述 - 添加运行时只读查询、Source Generator 类型生成等功能说明 - 提供推荐目录结构、Schema 示例和 YAML 示例配置 - 添加 VS Code 插件配置浏览、校验和表单编辑功能介绍 - 提供 Godot 文本配置桥接、运行时读取模板和 Architecture 接入指南 - 说明热重载、跨表引用、查询辅助等高级功能使用方法 - 添加开发期工具和当前限制说明,提供完整的配置系统接入流程 --- GFramework.Game/Config/YamlConfigLoader.cs | 47 +++++++++++++++++-- GFramework.Game/Properties/AssemblyInfo.cs | 4 ++ .../Config/GodotYamlConfigLoaderTests.cs | 23 +++++++-- .../Config/GodotYamlConfigLoader.cs | 19 ++++++-- docs/zh-CN/game/config-system.md | 2 + 5 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 GFramework.Game/Properties/AssemblyInfo.cs diff --git a/GFramework.Game/Config/YamlConfigLoader.cs b/GFramework.Game/Config/YamlConfigLoader.cs index 15e38b1b..68f448f2 100644 --- a/GFramework.Game/Config/YamlConfigLoader.cs +++ b/GFramework.Game/Config/YamlConfigLoader.cs @@ -1,8 +1,5 @@ using System.Diagnostics; -using GFramework.Core.Abstractions.Events; using GFramework.Game.Abstractions.Config; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace GFramework.Game.Config; @@ -13,6 +10,9 @@ namespace GFramework.Game.Config; /// public sealed class YamlConfigLoader : IConfigLoader { + private const string DefaultHotReloadUnavailableMessage = + "Hot reload is not available for the current loader configuration."; + private const string RootPathCannotBeNullOrWhiteSpaceMessage = "Root path cannot be null or whitespace."; private const string TableNameCannotBeNullOrWhiteSpaceMessage = "Table name cannot be null or whitespace."; private const string RelativePathCannotBeNullOrWhiteSpaceMessage = "Relative path cannot be null or whitespace."; @@ -22,7 +22,9 @@ public sealed class YamlConfigLoader : IConfigLoader private static readonly TimeSpan DefaultHotReloadDebounceDelay = TimeSpan.FromMilliseconds(200); + private readonly Func _canEnableHotReload; private readonly IDeserializer _deserializer; + private readonly string _hotReloadUnavailableMessage; private readonly Dictionary> _lastSuccessfulDependencies = new(StringComparer.Ordinal); @@ -36,6 +38,27 @@ public sealed class YamlConfigLoader : IConfigLoader /// 配置根目录。 /// 为空时抛出。 public YamlConfigLoader(string rootPath) + : this(rootPath, null, null) + { + } + + /// + /// 使用指定配置根目录与热重载可用性守卫创建 YAML 配置加载器。 + /// + /// 配置根目录。 + /// + /// 用于判断当前实例是否允许启用热重载的委托。 + /// 宿主适配层可借此把额外的文件系统前置条件下沉到底层加载器,避免公开实例被绕过时启用错误监听目标。 + /// + /// + /// 当 返回 时抛出的异常消息; + /// 为空时使用默认消息。 + /// + /// 为空时抛出。 + internal YamlConfigLoader( + string rootPath, + Func? canEnableHotReload, + string? hotReloadUnavailableMessage) { if (string.IsNullOrWhiteSpace(rootPath)) { @@ -43,6 +66,10 @@ public sealed class YamlConfigLoader : IConfigLoader } _rootPath = rootPath; + _canEnableHotReload = canEnableHotReload ?? (() => true); + _hotReloadUnavailableMessage = string.IsNullOrWhiteSpace(hotReloadUnavailableMessage) + ? DefaultHotReloadUnavailableMessage + : hotReloadUnavailableMessage; _deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() @@ -136,6 +163,7 @@ public sealed class YamlConfigLoader : IConfigLoader { ArgumentNullException.ThrowIfNull(registry); options ??= new YamlConfigHotReloadOptions(); + EnsureHotReloadCanBeEnabled(); if (options.DebounceDelay < TimeSpan.Zero) { throw new ArgumentOutOfRangeException( @@ -154,6 +182,19 @@ public sealed class YamlConfigLoader : IConfigLoader options.DebounceDelay); } + private void EnsureHotReloadCanBeEnabled() + { + if (_canEnableHotReload()) + { + return; + } + + // Host adapters can attach additional filesystem constraints to the loader instance. + // Enforcing the guard here prevents callers from bypassing the adapter by invoking + // EnableHotReload directly on the exposed loader reference. + throw new InvalidOperationException(_hotReloadUnavailableMessage); + } + private void UpdateLastSuccessfulDependencies(IEnumerable loadedTables) { _lastSuccessfulDependencies.Clear(); diff --git a/GFramework.Game/Properties/AssemblyInfo.cs b/GFramework.Game/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..61807842 --- /dev/null +++ b/GFramework.Game/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("GFramework.Game.Tests")] +[assembly: InternalsVisibleTo("GFramework.Godot")] diff --git a/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs b/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs index 32b16f9a..d3c0f84c 100644 --- a/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs +++ b/GFramework.Godot.Tests/Config/GodotYamlConfigLoaderTests.cs @@ -1,7 +1,6 @@ using System.Reflection; using System.Runtime.CompilerServices; using GFramework.Game.Abstractions.Config; -using GFramework.Game.Config; using GFramework.Godot.Config; namespace GFramework.Godot.Tests.Config; @@ -12,6 +11,10 @@ namespace GFramework.Godot.Tests.Config; [TestFixture] public sealed class GodotYamlConfigLoaderTests { + private string _resourceRoot = null!; + private string _testRoot = null!; + private string _userRoot = null!; + /// /// 为每个测试准备独立的资源根目录与用户目录。 /// @@ -40,10 +43,6 @@ public sealed class GodotYamlConfigLoaderTests } } - private string _resourceRoot = null!; - private string _testRoot = null!; - private string _userRoot = null!; - /// /// 验证导出态会把注册过的 YAML 与 schema 文本同步到运行时缓存,再交给底层加载器。 /// @@ -113,6 +112,20 @@ public sealed class GodotYamlConfigLoaderTests Assert.That(exception!.Message, Does.Contain("Hot reload")); } + /// + /// 验证即使调用方拿到底层加载器实例,也不能绕过 Godot 适配层施加的热重载守卫。 + /// + [Test] + public void Loader_EnableHotReload_Should_Still_Respect_Godot_HotReload_Guard() + { + var loader = CreateLoader(isEditor: false); + + var exception = Assert.Throws(() => + loader.Loader.EnableHotReload(new ConfigRegistry())); + + Assert.That(exception!.Message, Does.Contain("Hot reload")); + } + /// /// 验证导出态会按父目录优先同步缓存,避免父目录重置删掉先前复制到子目录的内容。 /// diff --git a/GFramework.Godot/Config/GodotYamlConfigLoader.cs b/GFramework.Godot/Config/GodotYamlConfigLoader.cs index 217034fc..81b1d0d8 100644 --- a/GFramework.Godot/Config/GodotYamlConfigLoader.cs +++ b/GFramework.Godot/Config/GodotYamlConfigLoader.cs @@ -3,7 +3,6 @@ using GFramework.Core.Abstractions.Events; using GFramework.Game.Abstractions.Config; using GFramework.Game.Config; using GFramework.Godot.Extensions; -using FileAccess = Godot.FileAccess; namespace GFramework.Godot.Config; @@ -14,6 +13,9 @@ namespace GFramework.Godot.Config; /// public sealed class GodotYamlConfigLoader : IConfigLoader { + private const string HotReloadUnavailableMessage = + "Hot reload is only available when the source root can be accessed as a normal filesystem directory."; + private readonly GodotYamlConfigEnvironment _environment; private readonly YamlConfigLoader _loader; private readonly GodotYamlConfigLoaderOptions _options; @@ -80,7 +82,10 @@ public sealed class GodotYamlConfigLoader : IConfigLoader _options = options; _environment = environment; LoaderRootPath = ResolveLoaderRootPath(); - _loader = new YamlConfigLoader(LoaderRootPath); + _loader = new YamlConfigLoader( + LoaderRootPath, + () => CanEnableHotReload, + HotReloadUnavailableMessage); options.ConfigureLoader?.Invoke(_loader); } @@ -103,6 +108,13 @@ public sealed class GodotYamlConfigLoader : IConfigLoader /// 获取底层 实例。 /// 调用方可继续在该实例上追加注册表定义或读取注册数量。 /// + /// + /// 该实例仅应用于补充注册表定义或检查注册状态。 + /// 不要直接调用 + /// 或 ; + /// 应分别改为调用 ,以确保 Godot 适配层先执行缓存同步并维持 + /// 守卫。 + /// public YamlConfigLoader Loader => _loader; /// @@ -170,8 +182,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader if (!CanEnableHotReload) { - throw new InvalidOperationException( - "Hot reload is only available when the source root can be accessed as a normal filesystem directory."); + throw new InvalidOperationException(HotReloadUnavailableMessage); } return _loader.EnableHotReload(registry, options); diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md index 23f204cf..fa5b520c 100644 --- a/docs/zh-CN/game/config-system.md +++ b/docs/zh-CN/game/config-system.md @@ -355,6 +355,8 @@ await loader.LoadAsync(registry); - 导出预设必须显式包含 `.yaml`、`.yml`、`.json`、`.schema.json` 等原始文本资产;否则导出包里根本没有这些文件,任何加载器都无法读取 - 只有当源根目录可直接映射到普通文件系统目录时,`EnableHotReload(...)` 才可用;如果当前实例依赖 `user://` 缓存,热重载会被拒绝,而不是制造“监听了缓存目录却不反映真实源目录”的假象 +- 如果你通过 `GodotYamlConfigLoader.Loader` 继续追加表注册,请只把它当作“注册入口”使用;实际加载和热重载必须继续调用 + `GodotYamlConfigLoader.LoadAsync(...)` 与 `GodotYamlConfigLoader.EnableHotReload(...)` ### 运行时读取模板