diff --git a/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs b/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs
index b3c726eb..b44a1cd5 100644
--- a/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs
+++ b/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs
@@ -111,6 +111,57 @@ public class GeneratedConfigConsumerIntegrationTests
});
}
+ ///
+ /// 验证项目级生成目录既能按配置域枚举元数据,也能直接复用聚合注册筛选规则产出启动诊断视图。
+ ///
+ [Test]
+ public void GeneratedConfigCatalog_Should_Expose_Domain_And_Registration_Diagnostic_Views()
+ {
+ Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var monsterMetadata), Is.True);
+ Assert.That(GeneratedConfigCatalog.TryGetByTableName("item", out var itemMetadata), Is.True);
+
+ var monsterDomainTables = GeneratedConfigCatalog.GetTablesInConfigDomain(MonsterConfigBindings.ConfigDomain);
+ var missingDomainTables = GeneratedConfigCatalog.GetTablesInConfigDomain("missing");
+ var itemOnlyRegistrationTables = GeneratedConfigCatalog.GetTablesForRegistration(
+ new GeneratedConfigRegistrationOptions
+ {
+ IncludedTableNames = new[] { ItemConfigBindings.TableName }
+ });
+ var predicateOnlyRegistrationTables = GeneratedConfigCatalog.GetTablesForRegistration(
+ new GeneratedConfigRegistrationOptions
+ {
+ TableFilter = static metadata =>
+ string.Equals(metadata.TableName, MonsterConfigBindings.TableName, StringComparison.Ordinal)
+ });
+ var monsterOnlyOptions = new GeneratedConfigRegistrationOptions
+ {
+ IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
+ };
+ var predicateOnlyOptions = new GeneratedConfigRegistrationOptions
+ {
+ TableFilter = static metadata =>
+ string.Equals(metadata.TableName, MonsterConfigBindings.TableName, StringComparison.Ordinal)
+ };
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(monsterDomainTables.Select(static metadata => metadata.TableName),
+ Is.EqualTo(new[] { MonsterConfigBindings.TableName }));
+ Assert.That(missingDomainTables, Is.Empty);
+ Assert.That(itemOnlyRegistrationTables.Select(static metadata => metadata.TableName),
+ Is.EqualTo(new[] { ItemConfigBindings.TableName }));
+ Assert.That(predicateOnlyRegistrationTables.Select(static metadata => metadata.TableName),
+ Is.EqualTo(new[] { MonsterConfigBindings.TableName }));
+ Assert.That(GeneratedConfigCatalog.GetTablesForRegistration().Select(static metadata => metadata.TableName),
+ Is.SupersetOf(new[] { ItemConfigBindings.TableName, MonsterConfigBindings.TableName }));
+ Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, monsterOnlyOptions), Is.True);
+ Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(itemMetadata, monsterOnlyOptions), Is.False);
+ Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, predicateOnlyOptions), Is.True);
+ Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(itemMetadata, predicateOnlyOptions), Is.False);
+ Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, options: null), Is.True);
+ });
+ }
+
///
/// 验证聚合注册入口可以通过生成配置域、表名集合和自定义谓词收敛多表项目的启动粒度。
///
diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs
index 24e53bd2..fc645f88 100644
--- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs
+++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs
@@ -877,17 +877,26 @@ public class SchemaConfigGeneratorTests
"public global::System.Collections.Generic.IEqualityComparer? MonsterComparer { get; init; }"));
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
+ Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(effectiveOptions.ItemComparer);"));
Assert.That(catalogSource,
- Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options))"));
- Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(options.ItemComparer);"));
- Assert.That(catalogSource,
- Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[1], options))"));
- Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(options.MonsterComparer);"));
+ Does.Contain(
+ "if (GeneratedConfigCatalog.MatchesRegistrationOptions(GeneratedConfigCatalog.Tables[1], effectiveOptions))"));
+ Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(effectiveOptions.MonsterComparer);"));
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)"));
- Assert.That(catalogSource, Does.Contain("private static bool ShouldRegisterTable("));
+ Assert.That(catalogSource,
+ Does.Contain(
+ "public static global::System.Collections.Generic.IReadOnlyList GetTablesInConfigDomain(string configDomain)"));
+ Assert.That(catalogSource,
+ Does.Contain(
+ "public static global::System.Collections.Generic.IReadOnlyList GetTablesForRegistration(GeneratedConfigRegistrationOptions? options = null)"));
+ Assert.That(catalogSource,
+ Does.Contain("public static bool MatchesRegistrationOptions("));
+ Assert.That(catalogSource,
+ Does.Contain(
+ "if (GeneratedConfigCatalog.MatchesRegistrationOptions(GeneratedConfigCatalog.Tables[0], effectiveOptions))"));
Assert.That(catalogSource, Does.Contain("private static bool MatchesOptionalAllowList("));
});
}
diff --git a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt
index e5b79994..97983970 100644
--- a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt
+++ b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt
@@ -88,6 +88,106 @@ public static class GeneratedConfigCatalog
metadata = default;
return false;
}
+
+ ///
+ /// Resolves the generated table metadata entries that belong to the specified logical config domain.
+ ///
+ /// Logical config domain derived from the schema base name.
+ /// A deterministic metadata snapshot for the requested config domain, or an empty list when no generated table belongs to that domain.
+ /// When is null.
+ public static global::System.Collections.Generic.IReadOnlyList GetTablesInConfigDomain(string configDomain)
+ {
+ if (configDomain is null)
+ {
+ throw new global::System.ArgumentNullException(nameof(configDomain));
+ }
+
+ var matchedTables = new global::System.Collections.Generic.List();
+ foreach (var metadata in Tables)
+ {
+ if (string.Equals(metadata.ConfigDomain, configDomain, global::System.StringComparison.Ordinal))
+ {
+ matchedTables.Add(metadata);
+ }
+ }
+
+ return matchedTables.Count == 0 ? global::System.Array.Empty() : matchedTables.ToArray();
+ }
+
+ ///
+ /// Resolves the generated table metadata entries that aggregate registration would currently include under the supplied filters.
+ ///
+ /// Optional aggregate registration filters and comparer overrides. When null, every generated table remains eligible.
+ /// A deterministic metadata snapshot in the same order used by .
+ public static global::System.Collections.Generic.IReadOnlyList GetTablesForRegistration(GeneratedConfigRegistrationOptions? options = null)
+ {
+ var matchedTables = new global::System.Collections.Generic.List();
+ foreach (var metadata in Tables)
+ {
+ if (MatchesRegistrationOptions(metadata, options))
+ {
+ matchedTables.Add(metadata);
+ }
+ }
+
+ return matchedTables.Count == 0 ? global::System.Array.Empty() : matchedTables.ToArray();
+ }
+
+ ///
+ /// Evaluates whether one generated table metadata entry remains eligible under the supplied aggregate registration filters.
+ ///
+ /// Generated table metadata under evaluation.
+ /// Optional aggregate registration filters and comparer overrides. When null, the metadata entry is always eligible.
+ /// when the generated table would be included by aggregate registration; otherwise .
+ public static bool MatchesRegistrationOptions(
+ TableMetadata metadata,
+ GeneratedConfigRegistrationOptions? options)
+ {
+ if (options is null)
+ {
+ return true;
+ }
+
+ // Apply cheap generated allow-lists before invoking the optional caller predicate so startup diagnostics stay aligned with real registration.
+ if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))
+ {
+ return false;
+ }
+
+ if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))
+ {
+ return false;
+ }
+
+ return options.TableFilter?.Invoke(metadata) ?? true;
+ }
+
+ ///
+ /// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.
+ ///
+ /// Optional caller-supplied allow-list.
+ /// Generated metadata value being evaluated.
+ /// when the value should remain eligible for registration; otherwise .
+ private static bool MatchesOptionalAllowList(
+ global::System.Collections.Generic.IReadOnlyCollection? allowedValues,
+ string candidate)
+ {
+ if (allowedValues is null || allowedValues.Count == 0)
+ {
+ return true;
+ }
+
+ foreach (var allowedValue in allowedValues)
+ {
+ if (allowedValue is not null &&
+ string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
///
@@ -154,64 +254,13 @@ public static class GeneratedConfigRegistrationExtensions
throw new global::System.ArgumentNullException(nameof(loader));
}
- options ??= new GeneratedConfigRegistrationOptions();
+ var effectiveOptions = options ?? new GeneratedConfigRegistrationOptions();
- if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options))
+ if (GeneratedConfigCatalog.MatchesRegistrationOptions(GeneratedConfigCatalog.Tables[0], effectiveOptions))
{
- loader.RegisterMonsterTable(options.MonsterComparer);
+ loader.RegisterMonsterTable(effectiveOptions.MonsterComparer);
}
return loader;
}
-
- ///
- /// Applies the generated registration filters in a deterministic order so bootstrap code can narrow aggregate registration without hand-writing per-table calls.
- ///
- /// Generated table metadata under consideration.
- /// Aggregate registration options supplied by the caller.
- /// when the generated table should be registered; otherwise .
- private static bool ShouldRegisterTable(
- GeneratedConfigCatalog.TableMetadata metadata,
- GeneratedConfigRegistrationOptions options)
- {
- // Apply cheap generated allow-lists before invoking the optional caller predicate so startup filtering stays predictable.
- if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))
- {
- return false;
- }
-
- if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))
- {
- return false;
- }
-
- return options.TableFilter?.Invoke(metadata) ?? true;
- }
-
- ///
- /// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.
- ///
- /// Optional caller-supplied allow-list.
- /// Generated metadata value being evaluated.
- /// when the value should remain eligible for registration; otherwise .
- private static bool MatchesOptionalAllowList(
- global::System.Collections.Generic.IReadOnlyCollection? allowedValues,
- string candidate)
- {
- if (allowedValues is null || allowedValues.Count == 0)
- {
- return true;
- }
-
- foreach (var allowedValue in allowedValues)
- {
- if (allowedValue is not null &&
- string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))
- {
- return true;
- }
- }
-
- return false;
- }
}
diff --git a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
index e60c1892..ca249109 100644
--- a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
+++ b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
@@ -1240,6 +1240,125 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" metadata = default;");
builder.AppendLine(" return false;");
builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ " /// Resolves the generated table metadata entries that belong to the specified logical config domain.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Logical config domain derived from the schema base name.");
+ builder.AppendLine(
+ " /// A deterministic metadata snapshot for the requested config domain, or an empty list when no generated table belongs to that domain.");
+ builder.AppendLine(
+ " /// When is null.");
+ builder.AppendLine(
+ " public static global::System.Collections.Generic.IReadOnlyList GetTablesInConfigDomain(string configDomain)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" if (configDomain is null)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(configDomain));");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" var matchedTables = new global::System.Collections.Generic.List();");
+ builder.AppendLine(" foreach (var metadata in Tables)");
+ builder.AppendLine(" {");
+ builder.AppendLine(
+ " if (string.Equals(metadata.ConfigDomain, configDomain, global::System.StringComparison.Ordinal))");
+ builder.AppendLine(" {");
+ builder.AppendLine(" matchedTables.Add(metadata);");
+ builder.AppendLine(" }");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(
+ " return matchedTables.Count == 0 ? global::System.Array.Empty() : matchedTables.ToArray();");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ " /// Resolves the generated table metadata entries that aggregate registration would currently include under the supplied filters.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ " /// Optional aggregate registration filters and comparer overrides. When null, every generated table remains eligible.");
+ builder.AppendLine(
+ " /// A deterministic metadata snapshot in the same order used by .");
+ builder.AppendLine(
+ " public static global::System.Collections.Generic.IReadOnlyList GetTablesForRegistration(GeneratedConfigRegistrationOptions? options = null)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" var matchedTables = new global::System.Collections.Generic.List();");
+ builder.AppendLine(" foreach (var metadata in Tables)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" if (MatchesRegistrationOptions(metadata, options))");
+ builder.AppendLine(" {");
+ builder.AppendLine(" matchedTables.Add(metadata);");
+ builder.AppendLine(" }");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(
+ " return matchedTables.Count == 0 ? global::System.Array.Empty() : matchedTables.ToArray();");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ " /// Evaluates whether one generated table metadata entry remains eligible under the supplied aggregate registration filters.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Generated table metadata under evaluation.");
+ builder.AppendLine(
+ " /// Optional aggregate registration filters and comparer overrides. When null, the metadata entry is always eligible.");
+ builder.AppendLine(
+ " /// when the generated table would be included by aggregate registration; otherwise .");
+ builder.AppendLine(" public static bool MatchesRegistrationOptions(");
+ builder.AppendLine(" TableMetadata metadata,");
+ builder.AppendLine(" GeneratedConfigRegistrationOptions? options)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" if (options is null)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" return true;");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(
+ " // Apply cheap generated allow-lists before invoking the optional caller predicate so startup diagnostics stay aligned with real registration.");
+ builder.AppendLine(
+ " if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))");
+ builder.AppendLine(" {");
+ builder.AppendLine(" return false;");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))");
+ builder.AppendLine(" {");
+ builder.AppendLine(" return false;");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" return options.TableFilter?.Invoke(metadata) ?? true;");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" /// ");
+ builder.AppendLine(
+ " /// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.");
+ builder.AppendLine(" /// ");
+ builder.AppendLine(" /// Optional caller-supplied allow-list.");
+ builder.AppendLine(" /// Generated metadata value being evaluated.");
+ builder.AppendLine(
+ " /// when the value should remain eligible for registration; otherwise .");
+ builder.AppendLine(" private static bool MatchesOptionalAllowList(");
+ builder.AppendLine(" global::System.Collections.Generic.IReadOnlyCollection? allowedValues,");
+ builder.AppendLine(" string candidate)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" if (allowedValues is null || allowedValues.Count == 0)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" return true;");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" foreach (var allowedValue in allowedValues)");
+ builder.AppendLine(" {");
+ builder.AppendLine(" if (allowedValue is not null &&");
+ builder.AppendLine(
+ " string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))");
+ builder.AppendLine(" {");
+ builder.AppendLine(" return true;");
+ builder.AppendLine(" }");
+ builder.AppendLine(" }");
+ builder.AppendLine();
+ builder.AppendLine(" return false;");
+ builder.AppendLine(" }");
builder.AppendLine("}");
builder.AppendLine();
builder.AppendLine("/// ");
@@ -1340,83 +1459,23 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(loader));");
builder.AppendLine(" }");
builder.AppendLine();
- builder.AppendLine(" options ??= new GeneratedConfigRegistrationOptions();");
+ builder.AppendLine(" var effectiveOptions = options ?? new GeneratedConfigRegistrationOptions();");
builder.AppendLine();
for (var index = 0; index < schemas.Count; index++)
{
var schema = schemas[index];
builder.AppendLine(
- $" if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[{index.ToString(CultureInfo.InvariantCulture)}], options))");
+ $" if (GeneratedConfigCatalog.MatchesRegistrationOptions(GeneratedConfigCatalog.Tables[{index.ToString(CultureInfo.InvariantCulture)}], effectiveOptions))");
builder.AppendLine(" {");
builder.AppendLine(
- $" loader.Register{schema.EntityName}Table(options.{schema.EntityName}Comparer);");
+ $" loader.Register{schema.EntityName}Table(effectiveOptions.{schema.EntityName}Comparer);");
builder.AppendLine(" }");
builder.AppendLine();
}
builder.AppendLine(" return loader;");
builder.AppendLine(" }");
- builder.AppendLine();
- builder.AppendLine(" /// ");
- builder.AppendLine(
- " /// Applies the generated registration filters in a deterministic order so bootstrap code can narrow aggregate registration without hand-writing per-table calls.");
- builder.AppendLine(" /// ");
- builder.AppendLine(" /// Generated table metadata under consideration.");
- builder.AppendLine(
- " /// Aggregate registration options supplied by the caller.");
- builder.AppendLine(
- " /// when the generated table should be registered; otherwise .");
- builder.AppendLine(
- " private static bool ShouldRegisterTable(");
- builder.AppendLine(" GeneratedConfigCatalog.TableMetadata metadata,");
- builder.AppendLine(" GeneratedConfigRegistrationOptions options)");
- builder.AppendLine(" {");
- builder.AppendLine(
- " // Apply cheap generated allow-lists before invoking the optional caller predicate so startup filtering stays predictable.");
- builder.AppendLine(
- " if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))");
- builder.AppendLine(" {");
- builder.AppendLine(" return false;");
- builder.AppendLine(" }");
- builder.AppendLine();
- builder.AppendLine(" if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))");
- builder.AppendLine(" {");
- builder.AppendLine(" return false;");
- builder.AppendLine(" }");
- builder.AppendLine();
- builder.AppendLine(" return options.TableFilter?.Invoke(metadata) ?? true;");
- builder.AppendLine(" }");
- builder.AppendLine();
- builder.AppendLine(" /// ");
- builder.AppendLine(
- " /// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.");
- builder.AppendLine(" /// ");
- builder.AppendLine(" /// Optional caller-supplied allow-list.");
- builder.AppendLine(" /// Generated metadata value being evaluated.");
- builder.AppendLine(
- " /// when the value should remain eligible for registration; otherwise .");
- builder.AppendLine(" private static bool MatchesOptionalAllowList(");
- builder.AppendLine(" global::System.Collections.Generic.IReadOnlyCollection? allowedValues,");
- builder.AppendLine(" string candidate)");
- builder.AppendLine(" {");
- builder.AppendLine(" if (allowedValues is null || allowedValues.Count == 0)");
- builder.AppendLine(" {");
- builder.AppendLine(" return true;");
- builder.AppendLine(" }");
- builder.AppendLine();
- builder.AppendLine(" foreach (var allowedValue in allowedValues)");
- builder.AppendLine(" {");
- builder.AppendLine(" if (allowedValue is not null &&");
- builder.AppendLine(
- " string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))");
- builder.AppendLine(" {");
- builder.AppendLine(" return true;");
- builder.AppendLine(" }");
- builder.AppendLine(" }");
- builder.AppendLine();
- builder.AppendLine(" return false;");
- builder.AppendLine(" }");
builder.AppendLine("}");
return builder.ToString().TrimEnd();
}
diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md
index 09395cec..cf6b7eda 100644
--- a/docs/zh-CN/game/config-system.md
+++ b/docs/zh-CN/game/config-system.md
@@ -404,7 +404,7 @@ if (monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster))
### Architecture 推荐接入模板
-如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,推荐把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility:
+如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,并且当前宿主没有提供更上层的异步启动入口,可以把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility:
```csharp
using GFramework.Core.Architectures;
@@ -440,6 +440,12 @@ public sealed class GameArchitecture : Architecture
}
```
+这个模板里的 `.GetAwaiter().GetResult()` 只是“同步 `OnInitialize()` 到异步配置加载”的桥接写法,不应被理解为无条件推荐:
+
+- 如果宿主已经提供异步组合根、启动器或更早的异步初始化阶段,优先在那里直接 `await _configBootstrap.InitializeAsync()`
+- 只有在 `Architecture` 只暴露同步 `OnInitialize()`,且当前线程不存在需要恢复的 `SynchronizationContext` 时,才适合使用这类同步桥接
+- 在 UI 线程、ASP.NET Classic 等存在活动 `SynchronizationContext` 的环境中,不要直接阻塞等待异步初始化;应把配置初始化前移到异步入口,或改为由不受该上下文约束的启动线程完成
+
初始化完成后,业务组件可以继续通过架构上下文读取 utility,再走生成的强类型入口:
```csharp
@@ -547,6 +553,15 @@ if (GeneratedConfigCatalog.TryGetByTableName("monster", out var metadata))
}
```
+如果你希望先按配置域聚合出一组候选表,再决定是否进入启动链路,也可以直接查询目录:
+
+```csharp
+foreach (var metadata in GeneratedConfigCatalog.GetTablesInConfigDomain(MonsterConfigBindings.ConfigDomain))
+{
+ Console.WriteLine(metadata.TableName);
+}
+```
+
如果你需要为某些表保留自定义 key comparer,也可以继续走聚合注册入口,而不是被迫退回逐表手写:
```csharp
@@ -581,10 +596,28 @@ var loader = new YamlConfigLoader("config-root")
});
```
+如果你想在真正调用 `RegisterAllGeneratedConfigTables(...)` 之前,先把“这次会注册哪些表”输出到日志中,推荐直接复用同一份 options 做启动诊断,而不是手写一套平行筛选逻辑:
+
+```csharp
+var registrationOptions = new GeneratedConfigRegistrationOptions
+{
+ IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
+};
+
+foreach (var metadata in GeneratedConfigCatalog.GetTablesForRegistration(registrationOptions))
+{
+ Console.WriteLine($"Registering {metadata.TableName}");
+}
+
+var loader = new YamlConfigLoader("config-root")
+ .RegisterAllGeneratedConfigTables(registrationOptions);
+```
+
这里的规则是:
- `IncludedConfigDomains` 与 `IncludedTableNames` 都按 `StringComparison.Ordinal` 做白名单匹配;传 `null` 或空集合表示“不限制”
-- `TableFilter` 会在上述白名单通过后执行,适合继续按 schema 路径、配置目录等元数据做更细的启动裁剪
+- `TableFilter` 会在上述白名单通过后执行,适合继续按 schema 路径、配置目录等元数据做更细粒度的启动裁剪
+- `GeneratedConfigCatalog.GetTablesForRegistration(...)` 与 `RegisterAllGeneratedConfigTables(...)` 复用同一套筛选规则,便于在启动日志和真实注册之间保持一致
- 未显式配置 comparer 的表,仍然使用各自 `Register{Entity}Table()` 的默认行为
- 需要自定义 comparer 的表,可以通过 `GeneratedConfigRegistrationOptions` 按表覆盖
- 当前 `ConfigDomain` 约定仍与生成表名保持一致,但建议优先引用 `*ConfigBindings.ConfigDomain`,为后续更细的分组策略保留稳定入口