feat(config): 添加AI-First配置系统及文档

- 引入YAML配置源文件支持
- 实现JSON Schema结构描述功能
- 提供一对象一文件的目录组织方式
- 添加运行时只读查询能力
- 实现Source Generator生成配置类型和表包装
- 集成VS Code插件提供配置浏览和编辑功能
- 添加开发期热重载支持
- 提供跨表引用校验机制
- 创建配置生成器约定和绑定辅助类
- 添加详细的中文文档说明
- 实现集成测试验证生成器功能
This commit is contained in:
GeWuYou 2026-04-03 19:10:23 +08:00
parent ec5153f452
commit 61cc7eaa6d
4 changed files with 103 additions and 19 deletions

View File

@ -91,9 +91,16 @@ public class GeneratedConfigConsumerIntegrationTests
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(MonsterConfigBindings.ConfigDomain, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.TableName, Is.EqualTo("monster")); Assert.That(MonsterConfigBindings.TableName, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.ConfigRelativePath, Is.EqualTo("monster")); Assert.That(MonsterConfigBindings.ConfigRelativePath, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json")); Assert.That(MonsterConfigBindings.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
Assert.That(MonsterConfigBindings.Metadata.ConfigDomain, Is.EqualTo(MonsterConfigBindings.ConfigDomain));
Assert.That(MonsterConfigBindings.Metadata.TableName, Is.EqualTo(MonsterConfigBindings.TableName));
Assert.That(MonsterConfigBindings.Metadata.ConfigRelativePath,
Is.EqualTo(MonsterConfigBindings.ConfigRelativePath));
Assert.That(MonsterConfigBindings.Metadata.SchemaRelativePath,
Is.EqualTo(MonsterConfigBindings.SchemaRelativePath));
Assert.That(table.Count, Is.EqualTo(2)); Assert.That(table.Count, Is.EqualTo(2));
Assert.That(table.Get(1).Name, Is.EqualTo("Slime")); Assert.That(table.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(table.Get(2).Hp, Is.EqualTo(30)); Assert.That(table.Get(2).Hp, Is.EqualTo(30));

View File

@ -9,20 +9,51 @@ namespace GFramework.Game.Config.Generated;
/// </summary> /// </summary>
public static class MonsterConfigBindings public static class MonsterConfigBindings
{ {
/// <summary>
/// Groups the schema-derived metadata constants so consumer code can reuse one stable entry point.
/// </summary>
public static class Metadata
{
/// <summary>
/// Gets the logical config domain derived from the schema base name. The current runtime convention keeps this value aligned with the generated table name.
/// </summary>
public const string ConfigDomain = "monster";
/// <summary>
/// Gets the runtime registration name of the generated config table.
/// </summary>
public const string TableName = "monster";
/// <summary>
/// Gets the config directory path expected by the generated registration helper.
/// </summary>
public const string ConfigRelativePath = "monster";
/// <summary>
/// Gets the schema file path expected by the generated registration helper.
/// </summary>
public const string SchemaRelativePath = "schemas/monster.schema.json";
}
/// <summary>
/// Gets the logical config domain derived from the schema base name. The current runtime convention keeps this value aligned with the generated table name.
/// </summary>
public const string ConfigDomain = Metadata.ConfigDomain;
/// <summary> /// <summary>
/// Gets the runtime registration name of the generated config table. /// Gets the runtime registration name of the generated config table.
/// </summary> /// </summary>
public const string TableName = "monster"; public const string TableName = Metadata.TableName;
/// <summary> /// <summary>
/// Gets the config directory path expected by the generated registration helper. /// Gets the config directory path expected by the generated registration helper.
/// </summary> /// </summary>
public const string ConfigRelativePath = "monster"; public const string ConfigRelativePath = Metadata.ConfigRelativePath;
/// <summary> /// <summary>
/// Gets the schema file path expected by the generated registration helper. /// Gets the schema file path expected by the generated registration helper.
/// </summary> /// </summary>
public const string SchemaRelativePath = "schemas/monster.schema.json"; public const string SchemaRelativePath = Metadata.SchemaRelativePath;
/// <summary> /// <summary>
/// Registers the generated config table using the schema-derived runtime conventions. /// Registers the generated config table using the schema-derived runtime conventions.
@ -40,9 +71,9 @@ public static class MonsterConfigBindings
} }
return loader.RegisterTable<int, MonsterConfig>( return loader.RegisterTable<int, MonsterConfig>(
TableName, Metadata.TableName,
ConfigRelativePath, Metadata.ConfigRelativePath,
SchemaRelativePath, Metadata.SchemaRelativePath,
static config => config.Id, static config => config.Id,
comparer); comparer);
} }
@ -60,7 +91,7 @@ public static class MonsterConfigBindings
throw new global::System.ArgumentNullException(nameof(registry)); throw new global::System.ArgumentNullException(nameof(registry));
} }
return new MonsterTable(registry.GetTable<int, MonsterConfig>(TableName)); return new MonsterTable(registry.GetTable<int, MonsterConfig>(Metadata.TableName));
} }
/// <summary> /// <summary>
@ -77,7 +108,7 @@ public static class MonsterConfigBindings
throw new global::System.ArgumentNullException(nameof(registry)); throw new global::System.ArgumentNullException(nameof(registry));
} }
if (registry.TryGetTable<int, MonsterConfig>(TableName, out var innerTable) && innerTable is not null) if (registry.TryGetTable<int, MonsterConfig>(Metadata.TableName, out var innerTable) && innerTable is not null)
{ {
table = new MonsterTable(innerTable); table = new MonsterTable(innerTable);
return true; return true;

View File

@ -650,22 +650,58 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine($"public static class {bindingsClassName}"); builder.AppendLine($"public static class {bindingsClassName}");
builder.AppendLine("{"); builder.AppendLine("{");
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Groups the schema-derived metadata constants so consumer code can reuse one stable entry point.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public static class Metadata");
builder.AppendLine(" {");
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Gets the logical config domain derived from the schema base name. The current runtime convention keeps this value aligned with the generated table name.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public const string ConfigDomain = {SymbolDisplay.FormatLiteral(schema.TableRegistrationName, true)};");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the runtime registration name of the generated config table.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public const string TableName = {SymbolDisplay.FormatLiteral(schema.TableRegistrationName, true)};");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Gets the config directory path expected by the generated registration helper.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public const string ConfigRelativePath = {SymbolDisplay.FormatLiteral(schema.ConfigRelativePath, true)};");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the schema file path expected by the generated registration helper.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public const string SchemaRelativePath = {SymbolDisplay.FormatLiteral(schema.SchemaRelativePath, true)};");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Gets the logical config domain derived from the schema base name. The current runtime convention keeps this value aligned with the generated table name.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public const string ConfigDomain = Metadata.ConfigDomain;");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the runtime registration name of the generated config table."); builder.AppendLine(" /// Gets the runtime registration name of the generated config table.");
builder.AppendLine(" /// </summary>"); builder.AppendLine(" /// </summary>");
builder.AppendLine( builder.AppendLine(" public const string TableName = Metadata.TableName;");
$" public const string TableName = {SymbolDisplay.FormatLiteral(schema.TableRegistrationName, true)};");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the config directory path expected by the generated registration helper."); builder.AppendLine(" /// Gets the config directory path expected by the generated registration helper.");
builder.AppendLine(" /// </summary>"); builder.AppendLine(" /// </summary>");
builder.AppendLine( builder.AppendLine(" public const string ConfigRelativePath = Metadata.ConfigRelativePath;");
$" public const string ConfigRelativePath = {SymbolDisplay.FormatLiteral(schema.ConfigRelativePath, true)};");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the schema file path expected by the generated registration helper."); builder.AppendLine(" /// Gets the schema file path expected by the generated registration helper.");
builder.AppendLine(" /// </summary>"); builder.AppendLine(" /// </summary>");
builder.AppendLine( builder.AppendLine(" public const string SchemaRelativePath = Metadata.SchemaRelativePath;");
$" public const string SchemaRelativePath = {SymbolDisplay.FormatLiteral(schema.SchemaRelativePath, true)};");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine( builder.AppendLine(
@ -688,9 +724,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(); builder.AppendLine();
builder.AppendLine( builder.AppendLine(
$" return loader.RegisterTable<{schema.KeyClrType}, {schema.ClassName}>("); $" return loader.RegisterTable<{schema.KeyClrType}, {schema.ClassName}>(");
builder.AppendLine(" TableName,"); builder.AppendLine(" Metadata.TableName,");
builder.AppendLine(" ConfigRelativePath,"); builder.AppendLine(" Metadata.ConfigRelativePath,");
builder.AppendLine(" SchemaRelativePath,"); builder.AppendLine(" Metadata.SchemaRelativePath,");
builder.AppendLine($" static config => config.{schema.KeyPropertyName},"); builder.AppendLine($" static config => config.{schema.KeyPropertyName},");
builder.AppendLine(" comparer);"); builder.AppendLine(" comparer);");
builder.AppendLine(" }"); builder.AppendLine(" }");
@ -711,7 +747,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine( builder.AppendLine(
$" return new {schema.TableName}(registry.GetTable<{schema.KeyClrType}, {schema.ClassName}>(TableName));"); $" return new {schema.TableName}(registry.GetTable<{schema.KeyClrType}, {schema.ClassName}>(Metadata.TableName));");
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
@ -733,7 +769,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine( builder.AppendLine(
$" if (registry.TryGetTable<{schema.KeyClrType}, {schema.ClassName}>(TableName, out var innerTable) && innerTable is not null)"); $" if (registry.TryGetTable<{schema.KeyClrType}, {schema.ClassName}>(Metadata.TableName, out var innerTable) && innerTable is not null)");
builder.AppendLine(" {"); builder.AppendLine(" {");
builder.AppendLine($" table = new {schema.TableName}(innerTable);"); builder.AppendLine($" table = new {schema.TableName}(innerTable);");
builder.AppendLine(" return true;"); builder.AppendLine(" return true;");

View File

@ -103,11 +103,21 @@ var slime = monsterTable.Get(1);
这组辅助会把以下约定固化到生成代码里: 这组辅助会把以下约定固化到生成代码里:
- 配置域常量,例如 `MonsterConfigBindings.ConfigDomain`
- 表注册名,例如 `monster` - 表注册名,例如 `monster`
- 配置目录相对路径,例如 `monster` - 配置目录相对路径,例如 `monster`
- schema 相对路径,例如 `schemas/monster.schema.json` - schema 相对路径,例如 `schemas/monster.schema.json`
- 主键提取逻辑,例如 `config => config.Id` - 主键提取逻辑,例如 `config => config.Id`
如果你希望把这些约定作为一个统一入口传递或复用,也可以优先读取 `MonsterConfigBindings.Metadata` 下的常量:
```csharp
var domain = MonsterConfigBindings.Metadata.ConfigDomain;
var tableName = MonsterConfigBindings.Metadata.TableName;
var configPath = MonsterConfigBindings.Metadata.ConfigRelativePath;
var schemaPath = MonsterConfigBindings.Metadata.SchemaRelativePath;
```
如果你需要自定义目录、表名或 key selector仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。 如果你需要自定义目录、表名或 key selector仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
## 运行时校验行为 ## 运行时校验行为