From 83c0c57f105ed5aea26d9f9853883a6d789ee0bf Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Mon, 6 Apr 2026 15:17:33 +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?=E6=96=87=E6=A1=A3=E5=92=8C=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增架构配置集成测试验证 YAML 配置加载功能
- 添加消费者项目配置生成器集成测试
- 创建完整的游戏内容配置系统中文文档
- 文档涵盖目录结构、Schema 示例、接入模板和运行时校验行为
- 提供 Architecture 推荐接入模板和热重载配置说明
- 完善 VS Code 工具功能介绍和当前限制说明
---
.../ArchitectureConfigIntegrationTests.cs | 6 +-
...GeneratedConfigConsumerIntegrationTests.cs | 11 +-
.../SchemaConfigGeneratorSnapshotTests.cs | 4 +-
.../Config/SchemaConfigGeneratorTests.cs | 107 +++++++++++
.../GeneratedConfigCatalog.g.txt | 115 +++++++++++
.../Config/SchemaConfigGenerator.cs | 179 ++++++++++++++++++
docs/zh-CN/game/config-system.md | 39 +++-
7 files changed, 445 insertions(+), 16 deletions(-)
create mode 100644 GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt
diff --git a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
index 95b5c5e8..6765ef03 100644
--- a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
+++ b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
@@ -10,13 +10,13 @@ using NUnit.Framework;
namespace GFramework.Game.Tests.Config;
///
-/// 验证在 初始化流程中可以注册配置注册表、执行加载并通过生成的表访问器读取数据。
+/// 验证在 初始化流程中可以通过聚合注册入口加载生成配置表,并通过表访问器读取数据。
///
[TestFixture]
public class ArchitectureConfigIntegrationTests
{
///
- /// 架构初始化期间,通过 注册生成表,
+ /// 架构初始化期间,通过 的聚合注册入口注册生成表,
/// 并将 作为 utility 暴露给架构上下文读取。
///
[Test]
@@ -147,7 +147,7 @@ public class ArchitectureConfigIntegrationTests
RegisterUtility(Registry);
var loader = new YamlConfigLoader(_configRoot)
- .RegisterMonsterTable();
+ .RegisterAllGeneratedConfigTables();
loader.LoadAsync(Registry).GetAwaiter().GetResult();
MonsterTable = Registry.GetMonsterTable();
}
diff --git a/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs b/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs
index d515788d..65289810 100644
--- a/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs
+++ b/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs
@@ -8,7 +8,7 @@ namespace GFramework.Game.Tests.Config;
///
/// 验证消费者项目通过 `schemas/**/*.schema.json` 自动拾取 schema 后,
-/// 可以直接编译并使用生成的注册辅助、强类型访问入口、查询辅助与运行时加载链路。
+/// 可以直接编译并使用生成的聚合注册辅助、强类型访问入口、查询辅助与运行时加载链路。
///
[TestFixture]
public class GeneratedConfigConsumerIntegrationTests
@@ -39,7 +39,7 @@ public class GeneratedConfigConsumerIntegrationTests
///
/// 验证生成器自动拾取消费者项目的 schema 后,
- /// 可以用生成的注册辅助完成加载,并通过强类型表包装访问运行时数据与查询辅助。
+ /// 可以用生成的聚合注册辅助完成加载,并通过强类型表包装访问运行时数据与查询辅助。
///
[Test]
public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Project()
@@ -91,7 +91,7 @@ public class GeneratedConfigConsumerIntegrationTests
var registry = new ConfigRegistry();
var loader = new YamlConfigLoader(_rootPath)
- .RegisterMonsterTable();
+ .RegisterAllGeneratedConfigTables();
await loader.LoadAsync(registry);
@@ -100,6 +100,11 @@ public class GeneratedConfigConsumerIntegrationTests
Assert.Multiple(() =>
{
+ Assert.That(GeneratedConfigCatalog.Tables.Count, Is.EqualTo(1));
+ Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var catalogEntry), Is.True);
+ Assert.That(catalogEntry.ConfigDomain, Is.EqualTo("monster"));
+ Assert.That(catalogEntry.ConfigRelativePath, Is.EqualTo("monster"));
+ Assert.That(catalogEntry.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
Assert.That(MonsterConfigBindings.ConfigDomain, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.TableName, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.ConfigRelativePath, Is.EqualTo("monster"));
diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs
index 6ff638e1..9c375432 100644
--- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs
+++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs
@@ -168,6 +168,8 @@ public class SchemaConfigGeneratorSnapshotTests
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterTable.g.cs", "MonsterTable.g.txt");
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfigBindings.g.cs",
"MonsterConfigBindings.g.txt");
+ await AssertSnapshotAsync(generatedSources, snapshotFolder, "GeneratedConfigCatalog.g.cs",
+ "GeneratedConfigCatalog.g.txt");
}
///
@@ -212,4 +214,4 @@ public class SchemaConfigGeneratorSnapshotTests
{
return text.Replace("\r\n", "\n", StringComparison.Ordinal).Trim();
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs
index 20f74ea9..1f3b24b1 100644
--- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs
+++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs
@@ -351,4 +351,111 @@ public class SchemaConfigGeneratorTests
Assert.That(tableSource, Does.Not.Contain("TryFindFirstByReward("));
});
}
+
+ ///
+ /// 验证生成器会为当前消费者项目内的全部 schema 额外产出聚合注册入口,
+ /// 让 C# 启动代码可以一行注册所有生成表。
+ ///
+ [Test]
+ public void Run_Should_Generate_Project_Level_Registration_Catalog()
+ {
+ const string source = """
+ using System;
+ using System.Collections.Generic;
+
+ namespace GFramework.Game.Abstractions.Config
+ {
+ public interface IConfigTable
+ {
+ Type KeyType { get; }
+ Type ValueType { get; }
+ int Count { get; }
+ }
+
+ public interface IConfigTable : IConfigTable
+ where TKey : notnull
+ {
+ TValue Get(TKey key);
+ bool TryGet(TKey key, out TValue? value);
+ bool ContainsKey(TKey key);
+ IReadOnlyCollection All();
+ }
+
+ public interface IConfigRegistry
+ {
+ IConfigTable GetTable(string name)
+ where TKey : notnull;
+
+ bool TryGetTable(string name, out IConfigTable? table)
+ where TKey : notnull;
+ }
+ }
+
+ namespace GFramework.Game.Config
+ {
+ public sealed class YamlConfigLoader
+ {
+ public YamlConfigLoader RegisterTable(
+ string tableName,
+ string relativePath,
+ string schemaRelativePath,
+ Func keySelector,
+ IEqualityComparer? comparer = null)
+ where TKey : notnull
+ {
+ return this;
+ }
+ }
+ }
+ """;
+
+ const string monsterSchema = """
+ {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" }
+ }
+ }
+ """;
+
+ const string itemSchema = """
+ {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "id": { "type": "string" },
+ "rarity": { "type": "string" }
+ }
+ }
+ """;
+
+ var result = SchemaGeneratorTestDriver.Run(
+ source,
+ ("monster.schema.json", monsterSchema),
+ ("item.schema.json", itemSchema));
+
+ var generatedSources = result.Results
+ .Single()
+ .GeneratedSources
+ .ToDictionary(
+ static sourceResult => sourceResult.HintName,
+ static sourceResult => sourceResult.SourceText.ToString(),
+ StringComparer.Ordinal);
+
+ Assert.That(result.Results.Single().Diagnostics, Is.Empty);
+ Assert.That(generatedSources.TryGetValue("GeneratedConfigCatalog.g.cs", out var catalogSource), Is.True);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigCatalog"));
+ Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigRegistrationExtensions"));
+ Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable();"));
+ Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable();"));
+ Assert.That(catalogSource, Does.Contain("ItemConfigBindings.Metadata.TableName"));
+ Assert.That(catalogSource, Does.Contain("MonsterConfigBindings.Metadata.TableName"));
+ Assert.That(catalogSource, Does.Contain("public static bool TryGetByTableName(string tableName, out TableMetadata metadata)"));
+ });
+ }
}
diff --git a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt
new file mode 100644
index 00000000..d62e1889
--- /dev/null
+++ b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt
@@ -0,0 +1,115 @@
+//
+#nullable enable
+
+namespace GFramework.Game.Config.Generated;
+
+///
+/// Provides a project-level catalog for every config table generated from the current consumer project's schemas.
+/// Use this entry point when you want the C# runtime bootstrap path to register all generated tables without repeating one call per schema.
+///
+public static class GeneratedConfigCatalog
+{
+ ///
+ /// Describes one generated config table so bootstrap code can enumerate generated domains without re-parsing schema files at runtime.
+ ///
+ public readonly struct TableMetadata
+ {
+ ///
+ /// Initializes one generated table metadata entry.
+ ///
+ /// Logical config domain derived from the schema base name.
+ /// Runtime registration name.
+ /// Relative YAML directory path.
+ /// Relative schema file path.
+ public TableMetadata(
+ string configDomain,
+ string tableName,
+ string configRelativePath,
+ string schemaRelativePath)
+ {
+ ConfigDomain = configDomain ?? throw new global::System.ArgumentNullException(nameof(configDomain));
+ TableName = tableName ?? throw new global::System.ArgumentNullException(nameof(tableName));
+ ConfigRelativePath = configRelativePath ?? throw new global::System.ArgumentNullException(nameof(configRelativePath));
+ SchemaRelativePath = schemaRelativePath ?? throw new global::System.ArgumentNullException(nameof(schemaRelativePath));
+ }
+
+ ///
+ /// Gets the logical config domain derived from the schema base name.
+ ///
+ public string ConfigDomain { get; }
+
+ ///
+ /// Gets the runtime registration name used by .
+ ///
+ public string TableName { get; }
+
+ ///
+ /// Gets the relative directory that stores YAML files for the generated config table.
+ ///
+ public string ConfigRelativePath { get; }
+
+ ///
+ /// Gets the relative schema file path collected by the source generator.
+ ///
+ public string SchemaRelativePath { get; }
+ }
+
+ ///
+ /// Gets metadata for every generated config table in the current consumer project.
+ ///
+ public static global::System.Collections.Generic.IReadOnlyList Tables { get; } = global::System.Array.AsReadOnly(new TableMetadata[]
+ {
+ new(
+ MonsterConfigBindings.Metadata.ConfigDomain,
+ MonsterConfigBindings.Metadata.TableName,
+ MonsterConfigBindings.Metadata.ConfigRelativePath,
+ MonsterConfigBindings.Metadata.SchemaRelativePath),
+ });
+
+ ///
+ /// Tries to resolve generated table metadata by runtime registration name.
+ ///
+ /// Runtime registration name.
+ /// Resolved generated table metadata when the registration name exists; otherwise the default value.
+ /// when the registration name belongs to a generated config table; otherwise .
+ public static bool TryGetByTableName(string tableName, out TableMetadata metadata)
+ {
+ if (tableName is null)
+ {
+ throw new global::System.ArgumentNullException(nameof(tableName));
+ }
+
+ if (string.Equals(tableName, MonsterConfigBindings.Metadata.TableName, global::System.StringComparison.Ordinal))
+ {
+ metadata = Tables[0];
+ return true;
+ }
+
+ metadata = default;
+ return false;
+ }
+}
+
+///
+/// Provides a single extension method that registers every generated config table discovered in the current consumer project.
+///
+public static class GeneratedConfigRegistrationExtensions
+{
+ ///
+ /// Registers all generated config tables using schema-derived conventions so bootstrap code can stay one-line even as schemas grow.
+ ///
+ /// Target YAML config loader.
+ /// The same loader instance after all generated table registrations have been applied.
+ /// When is null.
+ public static global::GFramework.Game.Config.YamlConfigLoader RegisterAllGeneratedConfigTables(
+ this global::GFramework.Game.Config.YamlConfigLoader loader)
+ {
+ if (loader is null)
+ {
+ throw new global::System.ArgumentNullException(nameof(loader));
+ }
+
+ loader.RegisterMonsterTable();
+ return loader;
+ }
+}
\ No newline at end of file
diff --git a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
index 50ac393a..12af7bbd 100644
--- a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
+++ b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
@@ -41,6 +41,25 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
$"{result.Schema.EntityName}ConfigBindings.g.cs",
SourceText.From(GenerateBindingsClass(result.Schema), Encoding.UTF8));
});
+
+ var collectedSchemas = schemaFiles.Collect();
+ context.RegisterSourceOutput(collectedSchemas, static (productionContext, results) =>
+ {
+ var schemas = results
+ .Where(static result => result.Schema is not null)
+ .Select(static result => result.Schema!)
+ .OrderBy(static schema => schema.TableRegistrationName, StringComparer.Ordinal)
+ .ToArray();
+
+ if (schemas.Length == 0)
+ {
+ return;
+ }
+
+ productionContext.AddSource(
+ "GeneratedConfigCatalog.g.cs",
+ SourceText.From(GenerateCatalogClass(schemas), Encoding.UTF8));
+ });
}
///
@@ -940,6 +959,166 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
return builder.ToString().TrimEnd();
}
+ ///
+ /// 生成项目级聚合辅助源码。
+ /// 该辅助把当前消费者项目内所有有效 schema 汇总为一个统一入口,
+ /// 以便运行时快速完成批量注册并在需要时枚举已生成的配置域元数据。
+ ///
+ /// 当前编译中成功解析的 schema 集合。
+ /// 聚合辅助源码。
+ private static string GenerateCatalogClass(IReadOnlyList schemas)
+ {
+ var builder = new StringBuilder();
+ builder.AppendLine("// ");
+ builder.AppendLine("#nullable enable");
+ builder.AppendLine();
+ builder.AppendLine($"namespace {GeneratedNamespace};");
+ builder.AppendLine();
+ builder.AppendLine("/// ");
+ builder.AppendLine(
+ "/// Provides a project-level catalog for every config table generated from the current consumer project's schemas.");
+ builder.AppendLine(
+ "/// Use this entry point when you want the C# runtime bootstrap path to register all generated tables without repeating one call per schema.");
+ builder.AppendLine("/// ");
+ builder.AppendLine("public static class GeneratedConfigCatalog");
+ builder.AppendLine("{");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ " /// Describes one generated config table so bootstrap code can enumerate generated domains without re-parsing schema files at runtime.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" public readonly struct TableMetadata");
+ builder.AppendLine(" {");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Initializes one generated table metadata entry.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Logical config domain derived from the schema base name.");
+ builder.AppendLine(" /// Runtime registration name.");
+ builder.AppendLine(" /// Relative YAML directory path.");
+ builder.AppendLine(" /// Relative schema file path.");
+ builder.AppendLine(" public TableMetadata(");
+ builder.AppendLine(" string configDomain,");
+ builder.AppendLine(" string tableName,");
+ builder.AppendLine(" string configRelativePath,");
+ builder.AppendLine(" string schemaRelativePath)");
+ builder.AppendLine(" {");
+ builder.AppendLine(
+ " ConfigDomain = configDomain ?? throw new global::System.ArgumentNullException(nameof(configDomain));");
+ builder.AppendLine(
+ " TableName = tableName ?? throw new global::System.ArgumentNullException(nameof(tableName));");
+ builder.AppendLine(
+ " ConfigRelativePath = configRelativePath ?? throw new global::System.ArgumentNullException(nameof(configRelativePath));");
+ builder.AppendLine(
+ " SchemaRelativePath = schemaRelativePath ?? throw new global::System.ArgumentNullException(nameof(schemaRelativePath));");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Gets the logical config domain derived from the schema base name.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" public string ConfigDomain { get; }");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Gets the runtime registration name used by .");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" public string TableName { get; }");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Gets the relative directory that stores YAML files for the generated config table.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" public string ConfigRelativePath { get; }");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Gets the relative schema file path collected by the source generator.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" public string SchemaRelativePath { get; }");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Gets metadata for every generated config table in the current consumer project.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ " public static global::System.Collections.Generic.IReadOnlyList Tables { get; } = global::System.Array.AsReadOnly(new TableMetadata[]");
+ builder.AppendLine(" {");
+
+ foreach (var schema in schemas)
+ {
+ builder.AppendLine(" new(");
+ builder.AppendLine($" {schema.EntityName}ConfigBindings.Metadata.ConfigDomain,");
+ builder.AppendLine($" {schema.EntityName}ConfigBindings.Metadata.TableName,");
+ builder.AppendLine($" {schema.EntityName}ConfigBindings.Metadata.ConfigRelativePath,");
+ builder.AppendLine($" {schema.EntityName}ConfigBindings.Metadata.SchemaRelativePath),");
+ }
+
+ builder.AppendLine(" });");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Tries to resolve generated table metadata by runtime registration name.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Runtime registration name.");
+ builder.AppendLine(
+ " /// Resolved generated table metadata when the registration name exists; otherwise the default value.");
+ builder.AppendLine(
+ " /// when the registration name belongs to a generated config table; otherwise .");
+ builder.AppendLine(" public static bool TryGetByTableName(string tableName, out TableMetadata metadata)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" if (tableName is null)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(tableName));");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+
+ for (var index = 0; index < schemas.Count; index++)
+ {
+ var schema = schemas[index];
+ builder.AppendLine(
+ $" if (string.Equals(tableName, {schema.EntityName}ConfigBindings.Metadata.TableName, global::System.StringComparison.Ordinal))");
+ builder.AppendLine(" {");
+ builder.AppendLine(
+ $" metadata = Tables[{index.ToString(CultureInfo.InvariantCulture)}];");
+ builder.AppendLine(" return true;");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ }
+
+ builder.AppendLine(" metadata = default;");
+ builder.AppendLine(" return false;");
+ builder.AppendLine(" }");
+ builder.AppendLine("}");
+ builder.AppendLine();
+ builder.AppendLine("/// ");
+ builder.AppendLine(
+ "/// Provides a single extension method that registers every generated config table discovered in the current consumer project.");
+ builder.AppendLine("/// ");
+ builder.AppendLine("public static class GeneratedConfigRegistrationExtensions");
+ builder.AppendLine("{");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ " /// Registers all generated config tables using schema-derived conventions so bootstrap code can stay one-line even as schemas grow.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Target YAML config loader.");
+ builder.AppendLine(" /// The same loader instance after all generated table registrations have been applied.");
+ builder.AppendLine(
+ " /// When is null.");
+ builder.AppendLine(
+ " public static global::GFramework.Game.Config.YamlConfigLoader RegisterAllGeneratedConfigTables(");
+ builder.AppendLine(" this global::GFramework.Game.Config.YamlConfigLoader loader)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" if (loader is null)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(loader));");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+
+ foreach (var schema in schemas)
+ {
+ builder.AppendLine($" loader.Register{schema.EntityName}Table();");
+ }
+
+ builder.AppendLine(" return loader;");
+ builder.AppendLine(" }");
+ builder.AppendLine("}");
+ return builder.ToString().TrimEnd();
+ }
+
///
/// 收集 schema 中声明的跨表引用元数据,并为生成代码分配稳定成员名。
///
diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md
index ab7d9b2d..30446960 100644
--- a/docs/zh-CN/game/config-system.md
+++ b/docs/zh-CN/game/config-system.md
@@ -13,7 +13,7 @@
- 一对象一文件的目录组织
- 运行时只读查询
- Runtime / Generator / Tooling 共享支持 `minimum`、`maximum`、`minLength`、`maxLength`
-- Source Generator 生成配置类型、表包装和注册/访问辅助
+- Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
## 推荐目录结构
@@ -193,8 +193,7 @@ public sealed class GameConfigBootstrap : IDisposable
public async Task InitializeAsync(string configRootPath, bool enableHotReload = false)
{
var loader = new YamlConfigLoader(configRootPath)
- .RegisterMonsterTable()
- .RegisterItemTable();
+ .RegisterAllGeneratedConfigTables();
await loader.LoadAsync(_registry);
@@ -223,7 +222,7 @@ public sealed class GameConfigBootstrap : IDisposable
这段模板刻意遵循几个约定:
-- 优先使用生成器产出的 `Register*Table()`,避免手写表名、路径和 key selector
+- 优先使用生成器产出的 `RegisterAllGeneratedConfigTables()`,把多表注册收敛为一个稳定入口
- 由一个长生命周期对象持有 `ConfigRegistry`
- 热重载句柄和配置生命周期绑在一起,避免监听器泄漏
@@ -331,8 +330,7 @@ public sealed class GameArchitecture : Architecture
var registry = RegisterUtility(new ConfigRegistry());
var loader = new YamlConfigLoader(_configRootPath)
- .RegisterMonsterTable()
- .RegisterItemTable();
+ .RegisterAllGeneratedConfigTables();
loader.LoadAsync(registry).GetAwaiter().GetResult();
}
@@ -354,7 +352,7 @@ var slime = monsterTable.Get(1);
- 在 `OnInitialize()` 内完成首次 `LoadAsync`
- 初始化完成后只通过注册表和生成表包装访问配置
-当前阶段不建议为了配置系统额外引入新的 `IArchitectureModule` 或 service module 抽象;现有 `Architecture + ConfigRegistry + YamlConfigLoader + Register*Table()` 组合已经足够作为官方推荐接入路径。
+当前阶段不建议为了配置系统额外引入新的 `IArchitectureModule` 或 service module 抽象;现有 `Architecture + ConfigRegistry + YamlConfigLoader + RegisterAllGeneratedConfigTables()` 组合已经足够作为官方推荐接入路径。
### 热重载模板
@@ -408,7 +406,7 @@ using GFramework.Game.Config.Generated;
var registry = new ConfigRegistry();
var loader = new YamlConfigLoader("config-root")
- .RegisterMonsterTable();
+ .RegisterAllGeneratedConfigTables();
await loader.LoadAsync(registry);
@@ -416,6 +414,11 @@ var monsterTable = registry.GetMonsterTable();
var slime = monsterTable.Get(1);
```
+这里推荐把“注册全部已生成配置表”和“读取单表强类型元数据”分成两层:
+
+- 启动层优先走 `RegisterAllGeneratedConfigTables()`,避免每新增一个 schema 都要回到启动代码继续补链式调用
+- 消费层继续通过 `GetMonsterTable()`、`MonsterConfigBindings.Metadata` 这类单表入口读取强类型信息
+
这组辅助会把以下约定固化到生成代码里:
- 配置域常量,例如 `MonsterConfigBindings.ConfigDomain`
@@ -433,6 +436,24 @@ var configPath = MonsterConfigBindings.Metadata.ConfigRelativePath;
var schemaPath = MonsterConfigBindings.Metadata.SchemaRelativePath;
```
+如果你需要在启动或诊断代码里枚举当前消费者项目里有哪些生成表,也可以直接读取项目级目录:
+
+```csharp
+foreach (var metadata in GeneratedConfigCatalog.Tables)
+{
+ Console.WriteLine($"{metadata.TableName} -> {metadata.SchemaRelativePath}");
+}
+```
+
+也可以按表名回查:
+
+```csharp
+if (GeneratedConfigCatalog.TryGetByTableName("monster", out var metadata))
+{
+ Console.WriteLine(metadata.ConfigRelativePath);
+}
+```
+
如果你需要自定义目录、表名或 key selector,仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
如果你希望把 schema 路径、比较器以及未来扩展开关集中到一个对象里,推荐改用选项对象入口:
@@ -553,7 +574,7 @@ using GFramework.Game.Config.Generated;
var registry = new ConfigRegistry();
var loader = new YamlConfigLoader("config-root")
- .RegisterMonsterTable();
+ .RegisterAllGeneratedConfigTables();
await loader.LoadAsync(registry);