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.That(MonsterConfigBindings.ConfigDomain, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.TableName, Is.EqualTo("monster"));
Assert.That(MonsterConfigBindings.ConfigRelativePath, Is.EqualTo("monster"));
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.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(table.Get(2).Hp, Is.EqualTo(30));

View File

@ -9,20 +9,51 @@ namespace GFramework.Game.Config.Generated;
/// </summary>
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>
/// Gets the runtime registration name of the generated config table.
/// </summary>
public const string TableName = "monster";
public const string TableName = Metadata.TableName;
/// <summary>
/// Gets the config directory path expected by the generated registration helper.
/// </summary>
public const string ConfigRelativePath = "monster";
public const string ConfigRelativePath = Metadata.ConfigRelativePath;
/// <summary>
/// Gets the schema file path expected by the generated registration helper.
/// </summary>
public const string SchemaRelativePath = "schemas/monster.schema.json";
public const string SchemaRelativePath = Metadata.SchemaRelativePath;
/// <summary>
/// Registers the generated config table using the schema-derived runtime conventions.
@ -40,9 +71,9 @@ public static class MonsterConfigBindings
}
return loader.RegisterTable<int, MonsterConfig>(
TableName,
ConfigRelativePath,
SchemaRelativePath,
Metadata.TableName,
Metadata.ConfigRelativePath,
Metadata.SchemaRelativePath,
static config => config.Id,
comparer);
}
@ -60,7 +91,7 @@ public static class MonsterConfigBindings
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>
@ -77,7 +108,7 @@ public static class MonsterConfigBindings
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);
return true;

View File

@ -650,22 +650,58 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine($"public static class {bindingsClassName}");
builder.AppendLine("{");
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(" /// </summary>");
builder.AppendLine(
$" public const string TableName = {SymbolDisplay.FormatLiteral(schema.TableRegistrationName, true)};");
builder.AppendLine(" public const string TableName = Metadata.TableName;");
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(" public const string ConfigRelativePath = Metadata.ConfigRelativePath;");
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(" public const string SchemaRelativePath = Metadata.SchemaRelativePath;");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
@ -688,9 +724,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine();
builder.AppendLine(
$" return loader.RegisterTable<{schema.KeyClrType}, {schema.ClassName}>(");
builder.AppendLine(" TableName,");
builder.AppendLine(" ConfigRelativePath,");
builder.AppendLine(" SchemaRelativePath,");
builder.AppendLine(" Metadata.TableName,");
builder.AppendLine(" Metadata.ConfigRelativePath,");
builder.AppendLine(" Metadata.SchemaRelativePath,");
builder.AppendLine($" static config => config.{schema.KeyPropertyName},");
builder.AppendLine(" comparer);");
builder.AppendLine(" }");
@ -711,7 +747,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
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(" /// <summary>");
@ -733,7 +769,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
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($" table = new {schema.TableName}(innerTable);");
builder.AppendLine(" return true;");

View File

@ -103,11 +103,21 @@ var slime = monsterTable.Get(1);
这组辅助会把以下约定固化到生成代码里:
- 配置域常量,例如 `MonsterConfigBindings.ConfigDomain`
- 表注册名,例如 `monster`
- 配置目录相对路径,例如 `monster`
- schema 相对路径,例如 `schemas/monster.schema.json`
- 主键提取逻辑,例如 `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(...)` 原始重载。
## 运行时校验行为