docs(game): 添加游戏内容配置系统文档和生成器功能

- 添加了 AI-First 配置系统完整文档,涵盖 YAML 配置、JSON Schema 结构、目录组织
- 实现了 Source Generator 自动生成配置类型、表包装、单表注册和访问辅助代码
- 提供了项目级聚合注册目录和配置浏览、校验、表单编辑等工具支持
- 集成了运行时只读查询、热重载、跨表引用校验等核心功能
- 添加了 VS Code 插件支持配置浏览、raw 编辑、schema 打开和递归校验功能
This commit is contained in:
GeWuYou 2026-04-06 15:55:58 +08:00
parent 83c0c57f10
commit 4f966f9f50
5 changed files with 112 additions and 6 deletions

View File

@ -100,7 +100,9 @@ public class GeneratedConfigConsumerIntegrationTests
Assert.Multiple(() =>
{
Assert.That(GeneratedConfigCatalog.Tables.Count, Is.EqualTo(1));
Assert.That(
GeneratedConfigCatalog.Tables.Select(static metadata => metadata.TableName),
Does.Contain("monster"));
Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var catalogEntry), Is.True);
Assert.That(catalogEntry.ConfigDomain, Is.EqualTo("monster"));
Assert.That(catalogEntry.ConfigRelativePath, Is.EqualTo("monster"));

View File

@ -450,9 +450,14 @@ public class SchemaConfigGeneratorTests
Assert.Multiple(() =>
{
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigCatalog"));
Assert.That(catalogSource, Does.Contain("public sealed class GeneratedConfigRegistrationOptions"));
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("public global::System.Collections.Generic.IEqualityComparer<string>? ItemComparer { get; init; }"));
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IEqualityComparer<int>? 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(options.ItemComparer);"));
Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(options.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)"));

View File

@ -90,6 +90,17 @@ public static class GeneratedConfigCatalog
}
}
/// <summary>
/// Captures optional per-table registration overrides for the generated aggregate registration entry point.
/// </summary>
public sealed class GeneratedConfigRegistrationOptions
{
/// <summary>
/// Gets or sets the optional key comparer forwarded to MonsterConfigBindings.RegisterMonsterTable(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer<int>?) when aggregate registration runs.
/// </summary>
public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }
}
/// <summary>
/// Provides a single extension method that registers every generated config table discovered in the current consumer project.
/// </summary>
@ -109,7 +120,28 @@ public static class GeneratedConfigRegistrationExtensions
throw new global::System.ArgumentNullException(nameof(loader));
}
loader.RegisterMonsterTable();
return RegisterAllGeneratedConfigTables(loader, options: null);
}
/// <summary>
/// Registers all generated config tables while preserving optional per-table overrides such as custom key comparers.
/// </summary>
/// <param name="loader">Target YAML config loader.</param>
/// <param name="options">Optional per-table overrides for aggregate registration; when null, all tables use their default comparer behavior.</param>
/// <returns>The same loader instance after all generated table registrations have been applied.</returns>
/// <exception cref="global::System.ArgumentNullException">When <paramref name="loader"/> is null.</exception>
public static global::GFramework.Game.Config.YamlConfigLoader RegisterAllGeneratedConfigTables(
this global::GFramework.Game.Config.YamlConfigLoader loader,
GeneratedConfigRegistrationOptions? options)
{
if (loader is null)
{
throw new global::System.ArgumentNullException(nameof(loader));
}
options ??= new GeneratedConfigRegistrationOptions();
loader.RegisterMonsterTable(options.MonsterComparer);
return loader;
}
}

View File

@ -1082,6 +1082,31 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" metadata = default;");
builder.AppendLine(" return false;");
builder.AppendLine(" }");
builder.AppendLine("}");
builder.AppendLine();
builder.AppendLine("/// <summary>");
builder.AppendLine(
"/// Captures optional per-table registration overrides for the generated aggregate registration entry point.");
builder.AppendLine("/// </summary>");
builder.AppendLine("public sealed class GeneratedConfigRegistrationOptions");
builder.AppendLine("{");
for (var index = 0; index < schemas.Count; index++)
{
var schema = schemas[index];
builder.AppendLine(" /// <summary>");
builder.AppendLine(
$" /// Gets or sets the optional key comparer forwarded to {schema.EntityName}ConfigBindings.Register{schema.EntityName}Table(global::GFramework.Game.Config.YamlConfigLoader, global::System.Collections.Generic.IEqualityComparer<{schema.KeyClrType}>?) when aggregate registration runs.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public global::System.Collections.Generic.IEqualityComparer<{schema.KeyClrType}>? {schema.EntityName}Comparer {{ get; init; }}");
if (index < schemas.Count - 1)
{
builder.AppendLine();
}
}
builder.AppendLine("}");
builder.AppendLine();
builder.AppendLine("/// <summary>");
@ -1107,10 +1132,35 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(loader));");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" return RegisterAllGeneratedConfigTables(loader, options: null);");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Registers all generated config tables while preserving optional per-table overrides such as custom key comparers.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"loader\">Target YAML config loader.</param>");
builder.AppendLine(
" /// <param name=\"options\">Optional per-table overrides for aggregate registration; when null, all tables use their default comparer behavior.</param>");
builder.AppendLine(" /// <returns>The same loader instance after all generated table registrations have been applied.</returns>");
builder.AppendLine(
" /// <exception cref=\"global::System.ArgumentNullException\">When <paramref name=\"loader\"/> is null.</exception>");
builder.AppendLine(
" public static global::GFramework.Game.Config.YamlConfigLoader RegisterAllGeneratedConfigTables(");
builder.AppendLine(" this global::GFramework.Game.Config.YamlConfigLoader loader,");
builder.AppendLine(" GeneratedConfigRegistrationOptions? options)");
builder.AppendLine(" {");
builder.AppendLine(" if (loader is null)");
builder.AppendLine(" {");
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(loader));");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" options ??= new GeneratedConfigRegistrationOptions();");
builder.AppendLine();
foreach (var schema in schemas)
{
builder.AppendLine($" loader.Register{schema.EntityName}Table();");
builder.AppendLine($" loader.Register{schema.EntityName}Table(options.{schema.EntityName}Comparer);");
}
builder.AppendLine(" return loader;");

View File

@ -454,6 +454,23 @@ if (GeneratedConfigCatalog.TryGetByTableName("monster", out var metadata))
}
```
如果你需要为某些表保留自定义 key comparer也可以继续走聚合注册入口而不是被迫退回逐表手写
```csharp
var loader = new YamlConfigLoader("config-root")
.RegisterAllGeneratedConfigTables(
new GeneratedConfigRegistrationOptions
{
ItemComparer = StringComparer.OrdinalIgnoreCase
});
```
这里的规则是:
- 未显式配置 comparer 的表,仍然使用各自 `Register{Entity}Table()` 的默认行为
- 需要自定义 comparer 的表,可以通过 `GeneratedConfigRegistrationOptions` 按表覆盖
- 如果项目希望继续完全手写某张表的注册流程,逐表 `Register*Table(...)` 入口仍然保留,作为兼容逃生通道
如果你需要自定义目录、表名或 key selector仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
如果你希望把 schema 路径、比较器以及未来扩展开关集中到一个对象里,推荐改用选项对象入口: