docs(config): 添加游戏内容配置系统完整文档

- 新增游戏内容配置系统详细文档,涵盖 YAML 配置、JSON Schema 结构描述
- 添加运行时只读查询、Source Generator 类型生成等功能说明
- 提供推荐目录结构、Schema 示例和 YAML 示例配置
- 添加 VS Code 插件配置浏览、校验和表单编辑功能介绍
- 提供 Godot 文本配置桥接、运行时读取模板和 Architecture 接入指南
- 说明热重载、跨表引用、查询辅助等高级功能使用方法
- 添加开发期工具和当前限制说明,提供完整的配置系统接入流程
This commit is contained in:
GeWuYou 2026-04-11 08:41:30 +08:00
parent 1c064bfe66
commit 0f1319334e
5 changed files with 83 additions and 12 deletions

View File

@ -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;
/// </summary>
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<bool> _canEnableHotReload;
private readonly IDeserializer _deserializer;
private readonly string _hotReloadUnavailableMessage;
private readonly Dictionary<string, IReadOnlyCollection<string>> _lastSuccessfulDependencies =
new(StringComparer.Ordinal);
@ -36,6 +38,27 @@ public sealed class YamlConfigLoader : IConfigLoader
/// <param name="rootPath">配置根目录。</param>
/// <exception cref="ArgumentException">当 <paramref name="rootPath" /> 为空时抛出。</exception>
public YamlConfigLoader(string rootPath)
: this(rootPath, null, null)
{
}
/// <summary>
/// 使用指定配置根目录与热重载可用性守卫创建 YAML 配置加载器。
/// </summary>
/// <param name="rootPath">配置根目录。</param>
/// <param name="canEnableHotReload">
/// 用于判断当前实例是否允许启用热重载的委托。
/// 宿主适配层可借此把额外的文件系统前置条件下沉到底层加载器,避免公开实例被绕过时启用错误监听目标。
/// </param>
/// <param name="hotReloadUnavailableMessage">
/// 当 <paramref name="canEnableHotReload" /> 返回 <see langword="false" /> 时抛出的异常消息;
/// 为空时使用默认消息。
/// </param>
/// <exception cref="ArgumentException">当 <paramref name="rootPath" /> 为空时抛出。</exception>
internal YamlConfigLoader(
string rootPath,
Func<bool>? 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<YamlTableLoadResult> loadedTables)
{
_lastSuccessfulDependencies.Clear();

View File

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("GFramework.Game.Tests")]
[assembly: InternalsVisibleTo("GFramework.Godot")]

View File

@ -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!;
/// <summary>
/// 为每个测试准备独立的资源根目录与用户目录。
/// </summary>
@ -40,10 +43,6 @@ public sealed class GodotYamlConfigLoaderTests
}
}
private string _resourceRoot = null!;
private string _testRoot = null!;
private string _userRoot = null!;
/// <summary>
/// 验证导出态会把注册过的 YAML 与 schema 文本同步到运行时缓存,再交给底层加载器。
/// </summary>
@ -113,6 +112,20 @@ public sealed class GodotYamlConfigLoaderTests
Assert.That(exception!.Message, Does.Contain("Hot reload"));
}
/// <summary>
/// 验证即使调用方拿到底层加载器实例,也不能绕过 Godot 适配层施加的热重载守卫。
/// </summary>
[Test]
public void Loader_EnableHotReload_Should_Still_Respect_Godot_HotReload_Guard()
{
var loader = CreateLoader(isEditor: false);
var exception = Assert.Throws<InvalidOperationException>(() =>
loader.Loader.EnableHotReload(new ConfigRegistry()));
Assert.That(exception!.Message, Does.Contain("Hot reload"));
}
/// <summary>
/// 验证导出态会按父目录优先同步缓存,避免父目录重置删掉先前复制到子目录的内容。
/// </summary>

View File

@ -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;
/// </summary>
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
/// 获取底层 <see cref="YamlConfigLoader" /> 实例。
/// 调用方可继续在该实例上追加注册表定义或读取注册数量。
/// </summary>
/// <remarks>
/// 该实例仅应用于补充注册表定义或检查注册状态。
/// 不要直接调用 <see cref="YamlConfigLoader.LoadAsync(GFramework.Game.Abstractions.Config.IConfigRegistry,System.Threading.CancellationToken)" />
/// 或 <see cref="YamlConfigLoader.EnableHotReload(GFramework.Game.Abstractions.Config.IConfigRegistry,YamlConfigHotReloadOptions?)" />
/// 应分别改为调用 <see cref="LoadAsync" /> 与 <see cref="EnableHotReload" />,以确保 Godot 适配层先执行缓存同步并维持
/// <see cref="CanEnableHotReload" /> 守卫。
/// </remarks>
public YamlConfigLoader Loader => _loader;
/// <summary>
@ -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);

View File

@ -355,6 +355,8 @@ await loader.LoadAsync(registry);
- 导出预设必须显式包含 `.yaml``.yml``.json``.schema.json` 等原始文本资产;否则导出包里根本没有这些文件,任何加载器都无法读取
- 只有当源根目录可直接映射到普通文件系统目录时,`EnableHotReload(...)` 才可用;如果当前实例依赖 `user://`
缓存,热重载会被拒绝,而不是制造“监听了缓存目录却不反映真实源目录”的假象
- 如果你通过 `GodotYamlConfigLoader.Loader` 继续追加表注册,请只把它当作“注册入口”使用;实际加载和热重载必须继续调用
`GodotYamlConfigLoader.LoadAsync(...)``GodotYamlConfigLoader.EnableHotReload(...)`
### 运行时读取模板