mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
docs(config): 添加游戏内容配置系统完整文档
- 新增游戏内容配置系统详细文档,涵盖 YAML 配置、JSON Schema 结构描述 - 添加运行时只读查询、Source Generator 类型生成等功能说明 - 提供推荐目录结构、Schema 示例和 YAML 示例配置 - 添加 VS Code 插件配置浏览、校验和表单编辑功能介绍 - 提供 Godot 文本配置桥接、运行时读取模板和 Architecture 接入指南 - 说明热重载、跨表引用、查询辅助等高级功能使用方法 - 添加开发期工具和当前限制说明,提供完整的配置系统接入流程
This commit is contained in:
parent
1c064bfe66
commit
0f1319334e
@ -1,8 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using GFramework.Core.Abstractions.Events;
|
|
||||||
using GFramework.Game.Abstractions.Config;
|
using GFramework.Game.Abstractions.Config;
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
using YamlDotNet.Serialization.NamingConventions;
|
|
||||||
|
|
||||||
namespace GFramework.Game.Config;
|
namespace GFramework.Game.Config;
|
||||||
|
|
||||||
@ -13,6 +10,9 @@ namespace GFramework.Game.Config;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class YamlConfigLoader : IConfigLoader
|
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 RootPathCannotBeNullOrWhiteSpaceMessage = "Root path cannot be null or whitespace.";
|
||||||
private const string TableNameCannotBeNullOrWhiteSpaceMessage = "Table name 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.";
|
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 static readonly TimeSpan DefaultHotReloadDebounceDelay = TimeSpan.FromMilliseconds(200);
|
||||||
|
|
||||||
|
private readonly Func<bool> _canEnableHotReload;
|
||||||
private readonly IDeserializer _deserializer;
|
private readonly IDeserializer _deserializer;
|
||||||
|
private readonly string _hotReloadUnavailableMessage;
|
||||||
|
|
||||||
private readonly Dictionary<string, IReadOnlyCollection<string>> _lastSuccessfulDependencies =
|
private readonly Dictionary<string, IReadOnlyCollection<string>> _lastSuccessfulDependencies =
|
||||||
new(StringComparer.Ordinal);
|
new(StringComparer.Ordinal);
|
||||||
@ -36,6 +38,27 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
/// <param name="rootPath">配置根目录。</param>
|
/// <param name="rootPath">配置根目录。</param>
|
||||||
/// <exception cref="ArgumentException">当 <paramref name="rootPath" /> 为空时抛出。</exception>
|
/// <exception cref="ArgumentException">当 <paramref name="rootPath" /> 为空时抛出。</exception>
|
||||||
public YamlConfigLoader(string rootPath)
|
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))
|
if (string.IsNullOrWhiteSpace(rootPath))
|
||||||
{
|
{
|
||||||
@ -43,6 +66,10 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
_rootPath = rootPath;
|
_rootPath = rootPath;
|
||||||
|
_canEnableHotReload = canEnableHotReload ?? (() => true);
|
||||||
|
_hotReloadUnavailableMessage = string.IsNullOrWhiteSpace(hotReloadUnavailableMessage)
|
||||||
|
? DefaultHotReloadUnavailableMessage
|
||||||
|
: hotReloadUnavailableMessage;
|
||||||
_deserializer = new DeserializerBuilder()
|
_deserializer = new DeserializerBuilder()
|
||||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||||
.IgnoreUnmatchedProperties()
|
.IgnoreUnmatchedProperties()
|
||||||
@ -136,6 +163,7 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(registry);
|
ArgumentNullException.ThrowIfNull(registry);
|
||||||
options ??= new YamlConfigHotReloadOptions();
|
options ??= new YamlConfigHotReloadOptions();
|
||||||
|
EnsureHotReloadCanBeEnabled();
|
||||||
if (options.DebounceDelay < TimeSpan.Zero)
|
if (options.DebounceDelay < TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(
|
throw new ArgumentOutOfRangeException(
|
||||||
@ -154,6 +182,19 @@ public sealed class YamlConfigLoader : IConfigLoader
|
|||||||
options.DebounceDelay);
|
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)
|
private void UpdateLastSuccessfulDependencies(IEnumerable<YamlTableLoadResult> loadedTables)
|
||||||
{
|
{
|
||||||
_lastSuccessfulDependencies.Clear();
|
_lastSuccessfulDependencies.Clear();
|
||||||
|
|||||||
4
GFramework.Game/Properties/AssemblyInfo.cs
Normal file
4
GFramework.Game/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("GFramework.Game.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("GFramework.Godot")]
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using GFramework.Game.Abstractions.Config;
|
using GFramework.Game.Abstractions.Config;
|
||||||
using GFramework.Game.Config;
|
|
||||||
using GFramework.Godot.Config;
|
using GFramework.Godot.Config;
|
||||||
|
|
||||||
namespace GFramework.Godot.Tests.Config;
|
namespace GFramework.Godot.Tests.Config;
|
||||||
@ -12,6 +11,10 @@ namespace GFramework.Godot.Tests.Config;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public sealed class GodotYamlConfigLoaderTests
|
public sealed class GodotYamlConfigLoaderTests
|
||||||
{
|
{
|
||||||
|
private string _resourceRoot = null!;
|
||||||
|
private string _testRoot = null!;
|
||||||
|
private string _userRoot = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为每个测试准备独立的资源根目录与用户目录。
|
/// 为每个测试准备独立的资源根目录与用户目录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -40,10 +43,6 @@ public sealed class GodotYamlConfigLoaderTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _resourceRoot = null!;
|
|
||||||
private string _testRoot = null!;
|
|
||||||
private string _userRoot = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证导出态会把注册过的 YAML 与 schema 文本同步到运行时缓存,再交给底层加载器。
|
/// 验证导出态会把注册过的 YAML 与 schema 文本同步到运行时缓存,再交给底层加载器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -113,6 +112,20 @@ public sealed class GodotYamlConfigLoaderTests
|
|||||||
Assert.That(exception!.Message, Does.Contain("Hot reload"));
|
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>
|
||||||
/// 验证导出态会按父目录优先同步缓存,避免父目录重置删掉先前复制到子目录的内容。
|
/// 验证导出态会按父目录优先同步缓存,避免父目录重置删掉先前复制到子目录的内容。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ using GFramework.Core.Abstractions.Events;
|
|||||||
using GFramework.Game.Abstractions.Config;
|
using GFramework.Game.Abstractions.Config;
|
||||||
using GFramework.Game.Config;
|
using GFramework.Game.Config;
|
||||||
using GFramework.Godot.Extensions;
|
using GFramework.Godot.Extensions;
|
||||||
using FileAccess = Godot.FileAccess;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Config;
|
namespace GFramework.Godot.Config;
|
||||||
|
|
||||||
@ -14,6 +13,9 @@ namespace GFramework.Godot.Config;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GodotYamlConfigLoader : IConfigLoader
|
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 GodotYamlConfigEnvironment _environment;
|
||||||
private readonly YamlConfigLoader _loader;
|
private readonly YamlConfigLoader _loader;
|
||||||
private readonly GodotYamlConfigLoaderOptions _options;
|
private readonly GodotYamlConfigLoaderOptions _options;
|
||||||
@ -80,7 +82,10 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
_options = options;
|
_options = options;
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
LoaderRootPath = ResolveLoaderRootPath();
|
LoaderRootPath = ResolveLoaderRootPath();
|
||||||
_loader = new YamlConfigLoader(LoaderRootPath);
|
_loader = new YamlConfigLoader(
|
||||||
|
LoaderRootPath,
|
||||||
|
() => CanEnableHotReload,
|
||||||
|
HotReloadUnavailableMessage);
|
||||||
options.ConfigureLoader?.Invoke(_loader);
|
options.ConfigureLoader?.Invoke(_loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +108,13 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
/// 获取底层 <see cref="YamlConfigLoader" /> 实例。
|
/// 获取底层 <see cref="YamlConfigLoader" /> 实例。
|
||||||
/// 调用方可继续在该实例上追加注册表定义或读取注册数量。
|
/// 调用方可继续在该实例上追加注册表定义或读取注册数量。
|
||||||
/// </summary>
|
/// </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;
|
public YamlConfigLoader Loader => _loader;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -170,8 +182,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
|
|
||||||
if (!CanEnableHotReload)
|
if (!CanEnableHotReload)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(HotReloadUnavailableMessage);
|
||||||
"Hot reload is only available when the source root can be accessed as a normal filesystem directory.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _loader.EnableHotReload(registry, options);
|
return _loader.EnableHotReload(registry, options);
|
||||||
|
|||||||
@ -355,6 +355,8 @@ await loader.LoadAsync(registry);
|
|||||||
- 导出预设必须显式包含 `.yaml`、`.yml`、`.json`、`.schema.json` 等原始文本资产;否则导出包里根本没有这些文件,任何加载器都无法读取
|
- 导出预设必须显式包含 `.yaml`、`.yml`、`.json`、`.schema.json` 等原始文本资产;否则导出包里根本没有这些文件,任何加载器都无法读取
|
||||||
- 只有当源根目录可直接映射到普通文件系统目录时,`EnableHotReload(...)` 才可用;如果当前实例依赖 `user://`
|
- 只有当源根目录可直接映射到普通文件系统目录时,`EnableHotReload(...)` 才可用;如果当前实例依赖 `user://`
|
||||||
缓存,热重载会被拒绝,而不是制造“监听了缓存目录却不反映真实源目录”的假象
|
缓存,热重载会被拒绝,而不是制造“监听了缓存目录却不反映真实源目录”的假象
|
||||||
|
- 如果你通过 `GodotYamlConfigLoader.Loader` 继续追加表注册,请只把它当作“注册入口”使用;实际加载和热重载必须继续调用
|
||||||
|
`GodotYamlConfigLoader.LoadAsync(...)` 与 `GodotYamlConfigLoader.EnableHotReload(...)`
|
||||||
|
|
||||||
### 运行时读取模板
|
### 运行时读取模板
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user