mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-10 19:56:45 +08:00
feat(config): 添加AI-First配置系统及源生成器
- 实现YAML配置文件加载和JSON Schema校验功能 - 提供Source Generator自动生成配置类型和表包装类 - 添加VS Code插件支持配置浏览和表单编辑 - 支持跨表引用校验和开发期热重载功能 - 生成强类型的配置访问辅助方法和注册绑定 - 实现嵌套对象和对象数组的类型安全访问
This commit is contained in:
parent
eaa1e5dff4
commit
48fd8a22bb
@ -9,7 +9,7 @@ namespace GFramework.SourceGenerators.Tests.Config;
|
|||||||
public class SchemaConfigGeneratorSnapshotTests
|
public class SchemaConfigGeneratorSnapshotTests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证一个最小 monster schema 能生成配置类型和表包装。
|
/// 验证一个最小 monster schema 能生成配置类型、表包装和注册辅助。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Snapshot_SchemaConfigGenerator()
|
public async Task Snapshot_SchemaConfigGenerator()
|
||||||
@ -35,6 +35,32 @@ public class SchemaConfigGeneratorSnapshotTests
|
|||||||
bool ContainsKey(TKey key);
|
bool ContainsKey(TKey key);
|
||||||
IReadOnlyCollection<TValue> All();
|
IReadOnlyCollection<TValue> All();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface IConfigRegistry
|
||||||
|
{
|
||||||
|
IConfigTable<TKey, TValue> GetTable<TKey, TValue>(string name)
|
||||||
|
where TKey : notnull;
|
||||||
|
|
||||||
|
bool TryGetTable<TKey, TValue>(string name, out IConfigTable<TKey, TValue>? table)
|
||||||
|
where TKey : notnull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GFramework.Game.Config
|
||||||
|
{
|
||||||
|
public sealed class YamlConfigLoader
|
||||||
|
{
|
||||||
|
public YamlConfigLoader RegisterTable<TKey, TValue>(
|
||||||
|
string tableName,
|
||||||
|
string relativePath,
|
||||||
|
string schemaRelativePath,
|
||||||
|
Func<TValue, TKey> keySelector,
|
||||||
|
IEqualityComparer<TKey>? comparer = null)
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@ -130,6 +156,8 @@ public class SchemaConfigGeneratorSnapshotTests
|
|||||||
|
|
||||||
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfig.g.cs", "MonsterConfig.g.txt");
|
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfig.g.cs", "MonsterConfig.g.txt");
|
||||||
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterTable.g.cs", "MonsterTable.g.txt");
|
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterTable.g.cs", "MonsterTable.g.txt");
|
||||||
|
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfigBindings.g.cs",
|
||||||
|
"MonsterConfigBindings.g.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GFramework.Game.Config.Generated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auto-generated registration and lookup helpers for schema file 'monster.schema.json'.
|
||||||
|
/// The helper centralizes table naming, config directory, schema path, and strong-typed registry access so consumer projects do not need to duplicate the same conventions.
|
||||||
|
/// </summary>
|
||||||
|
public static class MonsterConfigBindings
|
||||||
|
{
|
||||||
|
/// <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>
|
||||||
|
/// Registers the generated config table using the schema-derived runtime conventions.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loader">The target YAML config loader.</param>
|
||||||
|
/// <param name="comparer">Optional key comparer for the generated table registration.</param>
|
||||||
|
/// <returns>The same loader instance so registration can keep chaining.</returns>
|
||||||
|
public static global::GFramework.Game.Config.YamlConfigLoader RegisterMonsterTable(
|
||||||
|
this global::GFramework.Game.Config.YamlConfigLoader loader,
|
||||||
|
global::System.Collections.Generic.IEqualityComparer<int>? comparer = null)
|
||||||
|
{
|
||||||
|
if (loader is null)
|
||||||
|
{
|
||||||
|
throw new global::System.ArgumentNullException(nameof(loader));
|
||||||
|
}
|
||||||
|
|
||||||
|
return loader.RegisterTable<int, MonsterConfig>(
|
||||||
|
TableName,
|
||||||
|
ConfigRelativePath,
|
||||||
|
SchemaRelativePath,
|
||||||
|
static config => config.Id,
|
||||||
|
comparer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the generated config table wrapper from the registry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registry">The source config registry.</param>
|
||||||
|
/// <returns>The generated strong-typed table wrapper.</returns>
|
||||||
|
/// <exception cref="global::System.ArgumentNullException">When <paramref name="registry"/> is null.</exception>
|
||||||
|
public static MonsterTable GetMonsterTable(this global::GFramework.Game.Abstractions.Config.IConfigRegistry registry)
|
||||||
|
{
|
||||||
|
if (registry is null)
|
||||||
|
{
|
||||||
|
throw new global::System.ArgumentNullException(nameof(registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MonsterTable(registry.GetTable<int, MonsterConfig>(TableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get the generated config table wrapper from the registry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="registry">The source config registry.</param>
|
||||||
|
/// <param name="table">The generated strong-typed table wrapper when lookup succeeds; otherwise null.</param>
|
||||||
|
/// <returns>True when the generated table is registered and type-compatible; otherwise false.</returns>
|
||||||
|
/// <exception cref="global::System.ArgumentNullException">When <paramref name="registry"/> is null.</exception>
|
||||||
|
public static bool TryGetMonsterTable(this global::GFramework.Game.Abstractions.Config.IConfigRegistry registry, out MonsterTable? table)
|
||||||
|
{
|
||||||
|
if (registry is null)
|
||||||
|
{
|
||||||
|
throw new global::System.ArgumentNullException(nameof(registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registry.TryGetTable<int, MonsterConfig>(TableName, out var innerTable) && innerTable is not null)
|
||||||
|
{
|
||||||
|
table = new MonsterTable(innerTable);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
table = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -41,6 +41,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
productionContext.AddSource(
|
productionContext.AddSource(
|
||||||
$"{result.Schema.TableName}.g.cs",
|
$"{result.Schema.TableName}.g.cs",
|
||||||
SourceText.From(GenerateTableClass(result.Schema), Encoding.UTF8));
|
SourceText.From(GenerateTableClass(result.Schema), Encoding.UTF8));
|
||||||
|
productionContext.AddSource(
|
||||||
|
$"{result.Schema.EntityName}ConfigBindings.g.cs",
|
||||||
|
SourceText.From(GenerateBindingsClass(result.Schema), Encoding.UTF8));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,12 +131,18 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
idProperty.TypeSpec.SchemaType));
|
idProperty.TypeSpec.SchemaType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var schemaBaseName = GetSchemaBaseName(file.Path);
|
||||||
var schema = new SchemaFileSpec(
|
var schema = new SchemaFileSpec(
|
||||||
Path.GetFileName(file.Path),
|
Path.GetFileName(file.Path),
|
||||||
|
entityName,
|
||||||
schemaObject.ClassName,
|
schemaObject.ClassName,
|
||||||
$"{entityName}Table",
|
$"{entityName}Table",
|
||||||
GeneratedNamespace,
|
GeneratedNamespace,
|
||||||
idProperty.TypeSpec.ClrType.TrimEnd('?'),
|
idProperty.TypeSpec.ClrType.TrimEnd('?'),
|
||||||
|
idProperty.PropertyName,
|
||||||
|
schemaBaseName,
|
||||||
|
schemaBaseName,
|
||||||
|
GetSchemaRelativePath(file.Path),
|
||||||
TryGetMetadataString(root, "title"),
|
TryGetMetadataString(root, "title"),
|
||||||
TryGetMetadataString(root, "description"),
|
TryGetMetadataString(root, "description"),
|
||||||
schemaObject);
|
schemaObject);
|
||||||
@ -607,6 +616,131 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
return builder.ToString().TrimEnd();
|
return builder.ToString().TrimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成运行时注册与访问辅助源码。
|
||||||
|
/// 该辅助类型把 schema 命名约定、配置目录和 schema 相对路径固化为生成代码,
|
||||||
|
/// 让消费端无需重复手写字符串常量和主键提取逻辑。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="schema">已解析的 schema 模型。</param>
|
||||||
|
/// <returns>辅助类型源码。</returns>
|
||||||
|
private static string GenerateBindingsClass(SchemaFileSpec schema)
|
||||||
|
{
|
||||||
|
var registerMethodName = $"Register{schema.EntityName}Table";
|
||||||
|
var getMethodName = $"Get{schema.EntityName}Table";
|
||||||
|
var tryGetMethodName = $"TryGet{schema.EntityName}Table";
|
||||||
|
var bindingsClassName = $"{schema.EntityName}ConfigBindings";
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("#nullable enable");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine($"namespace {schema.Namespace};");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("/// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
$"/// Auto-generated registration and lookup helpers for schema file '{schema.FileName}'.");
|
||||||
|
builder.AppendLine(
|
||||||
|
"/// The helper centralizes table naming, config directory, schema path, and strong-typed registry access so consumer projects do not need to duplicate the same conventions.");
|
||||||
|
builder.AppendLine("/// </summary>");
|
||||||
|
builder.AppendLine($"public static class {bindingsClassName}");
|
||||||
|
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(" /// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// Registers the generated config table using the schema-derived runtime conventions.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" /// <param name=\"loader\">The target YAML config loader.</param>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// <param name=\"comparer\">Optional key comparer for the generated table registration.</param>");
|
||||||
|
builder.AppendLine(" /// <returns>The same loader instance so registration can keep chaining.</returns>");
|
||||||
|
builder.AppendLine(
|
||||||
|
$" public static global::GFramework.Game.Config.YamlConfigLoader {registerMethodName}(");
|
||||||
|
builder.AppendLine(" this global::GFramework.Game.Config.YamlConfigLoader loader,");
|
||||||
|
builder.AppendLine(
|
||||||
|
$" global::System.Collections.Generic.IEqualityComparer<{schema.KeyClrType}>? comparer = null)");
|
||||||
|
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(
|
||||||
|
$" return loader.RegisterTable<{schema.KeyClrType}, {schema.ClassName}>(");
|
||||||
|
builder.AppendLine(" TableName,");
|
||||||
|
builder.AppendLine(" ConfigRelativePath,");
|
||||||
|
builder.AppendLine(" SchemaRelativePath,");
|
||||||
|
builder.AppendLine($" static config => config.{schema.KeyPropertyName},");
|
||||||
|
builder.AppendLine(" comparer);");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Gets the generated config table wrapper from the registry.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" /// <param name=\"registry\">The source config registry.</param>");
|
||||||
|
builder.AppendLine(" /// <returns>The generated strong-typed table wrapper.</returns>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// <exception cref=\"global::System.ArgumentNullException\">When <paramref name=\"registry\"/> is null.</exception>");
|
||||||
|
builder.AppendLine(
|
||||||
|
$" public static {schema.TableName} {getMethodName}(this global::GFramework.Game.Abstractions.Config.IConfigRegistry registry)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" if (registry is null)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(registry));");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(
|
||||||
|
$" return new {schema.TableName}(registry.GetTable<{schema.KeyClrType}, {schema.ClassName}>(TableName));");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Tries to get the generated config table wrapper from the registry.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" /// <param name=\"registry\">The source config registry.</param>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// <param name=\"table\">The generated strong-typed table wrapper when lookup succeeds; otherwise null.</param>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// <returns>True when the generated table is registered and type-compatible; otherwise false.</returns>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// <exception cref=\"global::System.ArgumentNullException\">When <paramref name=\"registry\"/> is null.</exception>");
|
||||||
|
builder.AppendLine(
|
||||||
|
$" public static bool {tryGetMethodName}(this global::GFramework.Game.Abstractions.Config.IConfigRegistry registry, out {schema.TableName}? table)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" if (registry is null)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(registry));");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(
|
||||||
|
$" if (registry.TryGetTable<{schema.KeyClrType}, {schema.ClassName}>(TableName, out var innerTable) && innerTable is not null)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine($" table = new {schema.TableName}(innerTable);");
|
||||||
|
builder.AppendLine(" return true;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" table = null;");
|
||||||
|
builder.AppendLine(" return false;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
return builder.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 递归生成配置对象类型。
|
/// 递归生成配置对象类型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -773,6 +907,32 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
return Path.GetFileNameWithoutExtension(fileName);
|
return Path.GetFileNameWithoutExtension(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析生成注册辅助时要使用的 schema 相对路径。
|
||||||
|
/// 生成器优先保留 `schemas/` 目录以下的相对路径,以便消费端默认约定和 MSBuild AdditionalFiles 约定保持一致。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Schema 文件路径。</param>
|
||||||
|
/// <returns>用于运行时注册的 schema 相对路径。</returns>
|
||||||
|
private static string GetSchemaRelativePath(string path)
|
||||||
|
{
|
||||||
|
var normalizedPath = path.Replace('\\', '/');
|
||||||
|
const string rootMarker = "schemas/";
|
||||||
|
const string nestedMarker = "/schemas/";
|
||||||
|
|
||||||
|
if (normalizedPath.StartsWith(rootMarker, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nestedMarkerIndex = normalizedPath.LastIndexOf(nestedMarker, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (nestedMarkerIndex >= 0)
|
||||||
|
{
|
||||||
|
return normalizedPath.Substring(nestedMarkerIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"schemas/{Path.GetFileName(path)}";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 schema 名称转换为 PascalCase 标识符。
|
/// 将 schema 名称转换为 PascalCase 标识符。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -996,19 +1156,29 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
/// 生成器级 schema 模型。
|
/// 生成器级 schema 模型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="FileName">Schema 文件名。</param>
|
/// <param name="FileName">Schema 文件名。</param>
|
||||||
|
/// <param name="EntityName">实体名基础标识。</param>
|
||||||
/// <param name="ClassName">根配置类型名。</param>
|
/// <param name="ClassName">根配置类型名。</param>
|
||||||
/// <param name="TableName">配置表包装类型名。</param>
|
/// <param name="TableName">配置表包装类型名。</param>
|
||||||
/// <param name="Namespace">目标命名空间。</param>
|
/// <param name="Namespace">目标命名空间。</param>
|
||||||
/// <param name="KeyClrType">主键 CLR 类型。</param>
|
/// <param name="KeyClrType">主键 CLR 类型。</param>
|
||||||
|
/// <param name="KeyPropertyName">生成配置类型中的主键属性名。</param>
|
||||||
|
/// <param name="TableRegistrationName">运行时注册名。</param>
|
||||||
|
/// <param name="ConfigRelativePath">配置目录相对路径。</param>
|
||||||
|
/// <param name="SchemaRelativePath">Schema 文件相对路径。</param>
|
||||||
/// <param name="Title">根标题元数据。</param>
|
/// <param name="Title">根标题元数据。</param>
|
||||||
/// <param name="Description">根描述元数据。</param>
|
/// <param name="Description">根描述元数据。</param>
|
||||||
/// <param name="RootObject">根对象模型。</param>
|
/// <param name="RootObject">根对象模型。</param>
|
||||||
private sealed record SchemaFileSpec(
|
private sealed record SchemaFileSpec(
|
||||||
string FileName,
|
string FileName,
|
||||||
|
string EntityName,
|
||||||
string ClassName,
|
string ClassName,
|
||||||
string TableName,
|
string TableName,
|
||||||
string Namespace,
|
string Namespace,
|
||||||
string KeyClrType,
|
string KeyClrType,
|
||||||
|
string KeyPropertyName,
|
||||||
|
string TableRegistrationName,
|
||||||
|
string ConfigRelativePath,
|
||||||
|
string SchemaRelativePath,
|
||||||
string? Title,
|
string? Title,
|
||||||
string? Description,
|
string? Description,
|
||||||
SchemaObjectSpec RootObject);
|
SchemaObjectSpec RootObject);
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
- JSON Schema 作为结构描述
|
- JSON Schema 作为结构描述
|
||||||
- 一对象一文件的目录组织
|
- 一对象一文件的目录组织
|
||||||
- 运行时只读查询
|
- 运行时只读查询
|
||||||
- Source Generator 生成配置类型和表包装
|
- Source Generator 生成配置类型、表包装和注册/访问辅助
|
||||||
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
||||||
|
|
||||||
## 推荐目录结构
|
## 推荐目录结构
|
||||||
@ -83,27 +83,31 @@ dropItems:
|
|||||||
|
|
||||||
## 运行时接入
|
## 运行时接入
|
||||||
|
|
||||||
当你希望加载后的配置在运行时以只读表形式暴露时,可以使用 `YamlConfigLoader` 和 `ConfigRegistry`:
|
当你希望加载后的配置在运行时以只读表形式暴露时,优先使用生成器产出的注册与访问辅助:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using GFramework.Game.Config;
|
using GFramework.Game.Config;
|
||||||
|
using GFramework.Game.Config.Generated;
|
||||||
|
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
|
|
||||||
var loader = new YamlConfigLoader("config-root")
|
var loader = new YamlConfigLoader("config-root")
|
||||||
.RegisterTable<int, MonsterConfig>(
|
.RegisterMonsterTable();
|
||||||
"monster",
|
|
||||||
"monster",
|
|
||||||
"schemas/monster.schema.json",
|
|
||||||
static config => config.Id);
|
|
||||||
|
|
||||||
await loader.LoadAsync(registry);
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
var monsterTable = registry.GetTable<int, MonsterConfig>("monster");
|
var monsterTable = registry.GetMonsterTable();
|
||||||
var slime = monsterTable.Get(1);
|
var slime = monsterTable.Get(1);
|
||||||
```
|
```
|
||||||
|
|
||||||
这个重载会先按 schema 校验,再进行反序列化和注册。
|
这组辅助会把以下约定固化到生成代码里:
|
||||||
|
|
||||||
|
- 表注册名,例如 `monster`
|
||||||
|
- 配置目录相对路径,例如 `monster`
|
||||||
|
- schema 相对路径,例如 `schemas/monster.schema.json`
|
||||||
|
- 主键提取逻辑,例如 `config => config.Id`
|
||||||
|
|
||||||
|
如果你需要自定义目录、表名或 key selector,仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
|
||||||
|
|
||||||
## 运行时校验行为
|
## 运行时校验行为
|
||||||
|
|
||||||
@ -187,7 +191,12 @@ var hotReload = loader.EnableHotReload(
|
|||||||
|
|
||||||
## 生成器接入约定
|
## 生成器接入约定
|
||||||
|
|
||||||
配置生成器会从 `*.schema.json` 生成配置类型和表包装类。
|
配置生成器会从 `*.schema.json` 生成以下代码:
|
||||||
|
|
||||||
|
- 配置类型
|
||||||
|
- 表包装类型
|
||||||
|
- `YamlConfigLoader` 注册辅助
|
||||||
|
- `IConfigRegistry` 强类型访问辅助
|
||||||
|
|
||||||
通过已打包的 Source Generator 使用时,默认会自动收集 `schemas/**/*.schema.json` 作为 `AdditionalFiles`。
|
通过已打包的 Source Generator 使用时,默认会自动收集 `schemas/**/*.schema.json` 作为 `AdditionalFiles`。
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user