diff --git a/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs b/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs
index 476f3dea..bd81952f 100644
--- a/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs
+++ b/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs
@@ -1,5 +1,4 @@
using System.IO;
-using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
namespace GFramework.Game.Tests.Config;
@@ -10,6 +9,8 @@ namespace GFramework.Game.Tests.Config;
[TestFixture]
public class YamlConfigLoaderTests
{
+ private string _rootPath = null!;
+
///
/// 为每个测试创建独立临时目录,避免文件系统状态互相污染。
///
@@ -32,8 +33,6 @@ public class YamlConfigLoaderTests
}
}
- private string _rootPath = null!;
-
///
/// 验证加载器能够扫描 YAML 文件并将结果写入注册表。
///
@@ -71,6 +70,68 @@ public class YamlConfigLoaderTests
});
}
+ ///
+ /// 验证加载器支持通过选项对象注册带 schema 校验的配置表。
+ ///
+ [Test]
+ public async Task RegisterTable_Should_Support_Options_Object()
+ {
+ CreateConfigFile(
+ "monster/slime.yaml",
+ """
+ id: 1
+ name: Slime
+ hp: 10
+ """);
+ CreateSchemaFile(
+ "schemas/monster.schema.json",
+ """
+ {
+ "type": "object",
+ "required": ["id", "name"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "hp": { "type": "integer" }
+ }
+ }
+ """);
+
+ var loader = new YamlConfigLoader(_rootPath)
+ .RegisterTable(
+ new YamlConfigTableRegistrationOptions(
+ "monster",
+ "monster",
+ static config => config.Id)
+ {
+ SchemaRelativePath = "schemas/monster.schema.json"
+ });
+ var registry = new ConfigRegistry();
+
+ await loader.LoadAsync(registry);
+
+ var table = registry.GetTable("monster");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(table.Count, Is.EqualTo(1));
+ Assert.That(table.Get(1).Name, Is.EqualTo("Slime"));
+ Assert.That(table.Get(1).Hp, Is.EqualTo(10));
+ });
+ }
+
+ ///
+ /// 验证加载器会拒绝空的配置表注册选项对象。
+ ///
+ [Test]
+ public void RegisterTable_Should_Throw_When_Options_Are_Null()
+ {
+ var loader = new YamlConfigLoader(_rootPath);
+
+ Assert.Throws(() =>
+ loader.RegisterTable(null!));
+ }
+
///
/// 验证注册的配置目录不存在时会抛出清晰错误。
///
@@ -999,6 +1060,78 @@ public class YamlConfigLoaderTests
}
}
+ ///
+ /// 验证热重载支持通过选项对象配置回调和防抖延迟。
+ ///
+ [Test]
+ public async Task EnableHotReload_Should_Support_Options_Object()
+ {
+ CreateConfigFile(
+ "monster/slime.yaml",
+ """
+ id: 1
+ name: Slime
+ hp: 10
+ """);
+ CreateSchemaFile(
+ "schemas/monster.schema.json",
+ """
+ {
+ "type": "object",
+ "required": ["id", "name"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "hp": { "type": "integer" }
+ }
+ }
+ """);
+
+ var loader = new YamlConfigLoader(_rootPath)
+ .RegisterTable(
+ new YamlConfigTableRegistrationOptions(
+ "monster",
+ "monster",
+ static config => config.Id)
+ {
+ SchemaRelativePath = "schemas/monster.schema.json"
+ });
+ var registry = new ConfigRegistry();
+ await loader.LoadAsync(registry);
+
+ var reloadTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var hotReload = loader.EnableHotReload(
+ registry,
+ new YamlConfigHotReloadOptions
+ {
+ OnTableReloaded = tableName => reloadTaskSource.TrySetResult(tableName),
+ DebounceDelay = TimeSpan.FromMilliseconds(150)
+ });
+
+ try
+ {
+ CreateConfigFile(
+ "monster/slime.yaml",
+ """
+ id: 1
+ name: Slime
+ hp: 25
+ """);
+
+ var tableName = await WaitForTaskWithinAsync(reloadTaskSource.Task, TimeSpan.FromSeconds(5));
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(tableName, Is.EqualTo("monster"));
+ Assert.That(registry.GetTable("monster").Get(1).Hp, Is.EqualTo(25));
+ });
+ }
+ finally
+ {
+ hotReload.UnRegister();
+ }
+ }
+
///
/// 验证热重载失败时会保留旧表状态,并通过失败回调暴露诊断信息。
///
diff --git a/GFramework.Game/Config/YamlConfigHotReloadOptions.cs b/GFramework.Game/Config/YamlConfigHotReloadOptions.cs
new file mode 100644
index 00000000..52b17006
--- /dev/null
+++ b/GFramework.Game/Config/YamlConfigHotReloadOptions.cs
@@ -0,0 +1,27 @@
+namespace GFramework.Game.Config;
+
+///
+/// 描述开发期热重载的可选行为。
+/// 该选项对象集中承载回调和防抖等可扩展参数,
+/// 以避免后续继续在
+/// 上堆叠额外重载。
+///
+public sealed class YamlConfigHotReloadOptions
+{
+ ///
+ /// 获取或设置单个配置表重载成功后的可选回调。
+ ///
+ public Action? OnTableReloaded { get; init; }
+
+ ///
+ /// 获取或设置单个配置表重载失败后的可选回调。
+ /// 当失败来自加载器本身时,异常通常为 。
+ ///
+ public Action? OnTableReloadFailed { get; init; }
+
+ ///
+ /// 获取或设置文件系统事件的防抖延迟。
+ /// 默认值为 200 毫秒,用于吸收编辑器保存时的短时间重复触发。
+ ///
+ public TimeSpan DebounceDelay { get; init; } = TimeSpan.FromMilliseconds(200);
+}
\ No newline at end of file
diff --git a/GFramework.Game/Config/YamlConfigLoader.cs b/GFramework.Game/Config/YamlConfigLoader.cs
index 5034f2cf..dd219ff2 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;
@@ -20,6 +17,8 @@ public sealed class YamlConfigLoader : IConfigLoader
private const string SchemaRelativePathCannotBeNullOrWhiteSpaceMessage =
"Schema relative path cannot be null or whitespace.";
+ private static readonly TimeSpan DefaultHotReloadDebounceDelay = TimeSpan.FromMilliseconds(200);
+
private readonly IDeserializer _deserializer;
private readonly Dictionary> _lastSuccessfulDependencies =
@@ -100,8 +99,32 @@ public sealed class YamlConfigLoader : IConfigLoader
Action? onTableReloaded = null,
Action? onTableReloadFailed = null,
TimeSpan? debounceDelay = null)
+ {
+ return EnableHotReload(
+ registry,
+ new YamlConfigHotReloadOptions
+ {
+ OnTableReloaded = onTableReloaded,
+ OnTableReloadFailed = onTableReloadFailed,
+ DebounceDelay = debounceDelay ?? DefaultHotReloadDebounceDelay
+ });
+ }
+
+ ///
+ /// 启用开发期热重载,并通过选项对象集中配置回调和防抖行为。
+ /// 该入口用于减少继续堆叠位置参数重载的需要,
+ /// 也为未来扩展过滤策略或日志钩子预留稳定形态。
+ ///
+ /// 要被热重载更新的配置注册表。
+ /// 热重载配置选项;为空时使用默认选项。
+ /// 用于停止热重载监听的注销句柄。
+ /// 当 为空时抛出。
+ public IUnRegister EnableHotReload(
+ IConfigRegistry registry,
+ YamlConfigHotReloadOptions? options)
{
ArgumentNullException.ThrowIfNull(registry);
+ options ??= new YamlConfigHotReloadOptions();
return new HotReloadSession(
_rootPath,
@@ -109,9 +132,9 @@ public sealed class YamlConfigLoader : IConfigLoader
registry,
_registrations,
_lastSuccessfulDependencies,
- onTableReloaded,
- onTableReloadFailed,
- debounceDelay ?? TimeSpan.FromMilliseconds(200));
+ options.OnTableReloaded,
+ options.OnTableReloadFailed,
+ options.DebounceDelay);
}
private void UpdateLastSuccessfulDependencies(IEnumerable loadedTables)
@@ -142,7 +165,11 @@ public sealed class YamlConfigLoader : IConfigLoader
IEqualityComparer? comparer = null)
where TKey : notnull
{
- return RegisterTableCore(tableName, relativePath, null, keySelector, comparer);
+ return RegisterTable(
+ new YamlConfigTableRegistrationOptions(tableName, relativePath, keySelector)
+ {
+ Comparer = comparer
+ });
}
///
@@ -166,7 +193,35 @@ public sealed class YamlConfigLoader : IConfigLoader
IEqualityComparer? comparer = null)
where TKey : notnull
{
- return RegisterTableCore(tableName, relativePath, schemaRelativePath, keySelector, comparer);
+ return RegisterTable(
+ new YamlConfigTableRegistrationOptions(tableName, relativePath, keySelector)
+ {
+ SchemaRelativePath = schemaRelativePath,
+ Comparer = comparer
+ });
+ }
+
+ ///
+ /// 使用选项对象注册一个 YAML 配置表定义。
+ /// 该入口集中承载配置目录、schema 路径、主键提取器和比较器,
+ /// 以避免未来继续为新增开关叠加更多重载。
+ ///
+ /// 配置主键类型。
+ /// 配置值类型。
+ /// 配置表注册选项。
+ /// 当前加载器实例,以便链式注册。
+ /// 当 为空时抛出。
+ public YamlConfigLoader RegisterTable(YamlConfigTableRegistrationOptions options)
+ where TKey : notnull
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ return RegisterTableCore(
+ options.TableName,
+ options.RelativePath,
+ options.SchemaRelativePath,
+ options.KeySelector,
+ options.Comparer);
}
private YamlConfigLoader RegisterTableCore(
diff --git a/GFramework.Game/Config/YamlConfigTableRegistrationOptions.cs b/GFramework.Game/Config/YamlConfigTableRegistrationOptions.cs
new file mode 100644
index 00000000..16f1a7b7
--- /dev/null
+++ b/GFramework.Game/Config/YamlConfigTableRegistrationOptions.cs
@@ -0,0 +1,57 @@
+namespace GFramework.Game.Config;
+
+///
+/// 描述一个 YAML 配置表注册项的参数集合。
+/// 该选项对象用于替代不断增加的位置参数重载,
+/// 让消费者在启用 schema 校验、主键比较器或未来扩展项时仍能保持调用点可读。
+///
+/// 配置主键类型。
+/// 配置值类型。
+public sealed class YamlConfigTableRegistrationOptions
+ where TKey : notnull
+{
+ ///
+ /// 使用最小必需参数创建配置表注册选项。
+ ///
+ /// 运行时配置表名称。
+ /// 相对配置根目录的子目录。
+ /// 配置项主键提取器。
+ /// 当 为 null 时抛出。
+ public YamlConfigTableRegistrationOptions(
+ string tableName,
+ string relativePath,
+ Func keySelector)
+ {
+ ArgumentNullException.ThrowIfNull(keySelector);
+
+ TableName = tableName;
+ RelativePath = relativePath;
+ KeySelector = keySelector;
+ }
+
+ ///
+ /// 获取运行时配置表名称。
+ ///
+ public string TableName { get; }
+
+ ///
+ /// 获取相对配置根目录的子目录。
+ ///
+ public string RelativePath { get; }
+
+ ///
+ /// 获取相对配置根目录的 schema 文件路径。
+ /// 当该值为空时,当前注册项不会启用 schema 校验。
+ ///
+ public string? SchemaRelativePath { get; init; }
+
+ ///
+ /// 获取配置项主键提取器。
+ ///
+ public Func KeySelector { get; }
+
+ ///
+ /// 获取可选的主键比较器。
+ ///
+ public IEqualityComparer? Comparer { get; init; }
+}
\ No newline at end of file
diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md
index 8fdc20ea..b390afd7 100644
--- a/docs/zh-CN/game/config-system.md
+++ b/docs/zh-CN/game/config-system.md
@@ -304,6 +304,23 @@ var hotReload = loader.EnableHotReload(
- 热重载失败时应优先依赖 `ConfigLoadException.Diagnostic` 做稳定日志或 UI 提示
- 如果你的项目已经有统一日志系统,建议在这里把诊断字段转成结构化日志,而不是拼接一整段字符串
+如果你后续还需要为热重载增加更多开关,推荐优先使用选项对象入口,而不是继续叠加位置参数:
+
+```csharp
+var hotReload = loader.EnableHotReload(
+ registry,
+ new YamlConfigHotReloadOptions
+ {
+ OnTableReloaded = tableName => Console.WriteLine($"Reloaded: {tableName}"),
+ OnTableReloadFailed = (tableName, exception) =>
+ {
+ var diagnostic = (exception as ConfigLoadException)?.Diagnostic;
+ Console.WriteLine($"{tableName}: {diagnostic?.FailureKind}");
+ },
+ DebounceDelay = TimeSpan.FromMilliseconds(150)
+ });
+```
+
## 运行时接入
当你希望加载后的配置在运行时以只读表形式暴露时,优先使用生成器产出的注册与访问辅助:
@@ -342,6 +359,20 @@ var schemaPath = MonsterConfigBindings.Metadata.SchemaRelativePath;
如果你需要自定义目录、表名或 key selector,仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
+如果你希望把 schema 路径、比较器以及未来扩展开关集中到一个对象里,推荐改用选项对象入口:
+
+```csharp
+var loader = new YamlConfigLoader("config-root")
+ .RegisterTable(
+ new YamlConfigTableRegistrationOptions(
+ "monster",
+ "monster",
+ static config => config.Id)
+ {
+ SchemaRelativePath = "schemas/monster.schema.json"
+ });
+```
+
## 运行时校验行为
绑定 schema 的表在加载时会拒绝以下问题: