docs(config): 添加游戏内容配置系统完整文档与集成测试

- 新增游戏内容配置系统完整使用文档,涵盖 YAML 配置、JSON Schema 结构、目录组织等
- 添加推荐的项目接入模板,包括目录结构、csproj 配置、启动帮助器和运行时读取入口
- 实现官方启动帮助器 GameConfigBootstrap 与 GameConfigBootstrapOptions 集成
- 添加强类型配置表访问、查询辅助和跨表引用功能说明
- 实现开发期热重载、运行时校验和 VS Code 工具集成指南
- 添加完整的单元测试验证生成器绑定、聚合注册和强类型访问功能
- 提供 Architecture 接入模板和热重载管理最佳实践
- 包含运行时校验行为、诊断对象和配置加载异常处理说明
This commit is contained in:
GeWuYou 2026-04-09 10:01:32 +08:00
parent 5190ba2359
commit fac2453766
2 changed files with 24 additions and 3 deletions

View File

@ -127,10 +127,21 @@ public class GeneratedConfigConsumerIntegrationTests
{ {
IncludedTableNames = new[] { ItemConfigBindings.TableName } 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 var monsterOnlyOptions = new GeneratedConfigRegistrationOptions
{ {
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain } IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
}; };
var predicateOnlyOptions = new GeneratedConfigRegistrationOptions
{
TableFilter = static metadata =>
string.Equals(metadata.TableName, MonsterConfigBindings.TableName, StringComparison.Ordinal)
};
Assert.Multiple(() => Assert.Multiple(() =>
{ {
@ -139,10 +150,14 @@ public class GeneratedConfigConsumerIntegrationTests
Assert.That(missingDomainTables, Is.Empty); Assert.That(missingDomainTables, Is.Empty);
Assert.That(itemOnlyRegistrationTables.Select(static metadata => metadata.TableName), Assert.That(itemOnlyRegistrationTables.Select(static metadata => metadata.TableName),
Is.EqualTo(new[] { ItemConfigBindings.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), Assert.That(GeneratedConfigCatalog.GetTablesForRegistration().Select(static metadata => metadata.TableName),
Is.SupersetOf(new[] { ItemConfigBindings.TableName, MonsterConfigBindings.TableName })); Is.SupersetOf(new[] { ItemConfigBindings.TableName, MonsterConfigBindings.TableName }));
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, monsterOnlyOptions), Is.True); Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, monsterOnlyOptions), Is.True);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(itemMetadata, monsterOnlyOptions), Is.False); 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); Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, options: null), Is.True);
}); });
} }

View File

@ -404,7 +404,7 @@ if (monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster))
### Architecture 推荐接入模板 ### Architecture 推荐接入模板
如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,推荐把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility 如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,并且当前宿主没有提供更上层的异步启动入口,可以把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility
```csharp ```csharp
using GFramework.Core.Architectures; 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再走生成的强类型入口 初始化完成后,业务组件可以继续通过架构上下文读取 utility再走生成的强类型入口
```csharp ```csharp
@ -590,7 +596,7 @@ var loader = new YamlConfigLoader("config-root")
}); });
``` ```
如果你想在真正调用 `RegisterAllGeneratedConfigTables(...)` 之前,先把“这次会注册哪些表”打到日志里,推荐直接复用同一份 options 做启动诊断,而不是手写一套平行筛选逻辑: 如果你想在真正调用 `RegisterAllGeneratedConfigTables(...)` 之前,先把“这次会注册哪些表”输出到日志中,推荐直接复用同一份 options 做启动诊断,而不是手写一套平行筛选逻辑:
```csharp ```csharp
var registrationOptions = new GeneratedConfigRegistrationOptions var registrationOptions = new GeneratedConfigRegistrationOptions
@ -610,7 +616,7 @@ var loader = new YamlConfigLoader("config-root")
这里的规则是: 这里的规则是:
- `IncludedConfigDomains``IncludedTableNames` 都按 `StringComparison.Ordinal` 做白名单匹配;传 `null` 或空集合表示“不限制” - `IncludedConfigDomains``IncludedTableNames` 都按 `StringComparison.Ordinal` 做白名单匹配;传 `null` 或空集合表示“不限制”
- `TableFilter` 会在上述白名单通过后执行,适合继续按 schema 路径、配置目录等元数据做更细的启动裁剪 - `TableFilter` 会在上述白名单通过后执行,适合继续按 schema 路径、配置目录等元数据做更细粒度的启动裁剪
- `GeneratedConfigCatalog.GetTablesForRegistration(...)``RegisterAllGeneratedConfigTables(...)` 复用同一套筛选规则,便于在启动日志和真实注册之间保持一致 - `GeneratedConfigCatalog.GetTablesForRegistration(...)``RegisterAllGeneratedConfigTables(...)` 复用同一套筛选规则,便于在启动日志和真实注册之间保持一致
- 未显式配置 comparer 的表,仍然使用各自 `Register{Entity}Table()` 的默认行为 - 未显式配置 comparer 的表,仍然使用各自 `Register{Entity}Table()` 的默认行为
- 需要自定义 comparer 的表,可以通过 `GeneratedConfigRegistrationOptions` 按表覆盖 - 需要自定义 comparer 的表,可以通过 `GeneratedConfigRegistrationOptions` 按表覆盖