mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
docs(config): 添加游戏内容配置系统文档和集成测试
- 新增架构配置集成测试验证 YAML 配置加载功能 - 添加消费者项目配置生成器集成测试 - 创建完整的游戏内容配置系统中文文档 - 文档涵盖目录结构、Schema 示例、接入模板和运行时校验行为 - 提供 Architecture 推荐接入模板和热重载配置说明 - 完善 VS Code 工具功能介绍和当前限制说明
This commit is contained in:
parent
f0064e31aa
commit
83c0c57f10
@ -10,13 +10,13 @@ using NUnit.Framework;
|
|||||||
namespace GFramework.Game.Tests.Config;
|
namespace GFramework.Game.Tests.Config;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证在 <see cref="Architecture" /> 初始化流程中可以注册配置注册表、执行加载并通过生成的表访问器读取数据。
|
/// 验证在 <see cref="Architecture" /> 初始化流程中可以通过聚合注册入口加载生成配置表,并通过表访问器读取数据。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class ArchitectureConfigIntegrationTests
|
public class ArchitectureConfigIntegrationTests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 架构初始化期间,通过 <see cref="YamlConfigLoader" /> 注册生成表,
|
/// 架构初始化期间,通过 <see cref="YamlConfigLoader" /> 的聚合注册入口注册生成表,
|
||||||
/// 并将 <see cref="ConfigRegistry" /> 作为 utility 暴露给架构上下文读取。
|
/// 并将 <see cref="ConfigRegistry" /> 作为 utility 暴露给架构上下文读取。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
@ -147,7 +147,7 @@ public class ArchitectureConfigIntegrationTests
|
|||||||
RegisterUtility(Registry);
|
RegisterUtility(Registry);
|
||||||
|
|
||||||
var loader = new YamlConfigLoader(_configRoot)
|
var loader = new YamlConfigLoader(_configRoot)
|
||||||
.RegisterMonsterTable();
|
.RegisterAllGeneratedConfigTables();
|
||||||
loader.LoadAsync(Registry).GetAwaiter().GetResult();
|
loader.LoadAsync(Registry).GetAwaiter().GetResult();
|
||||||
MonsterTable = Registry.GetMonsterTable();
|
MonsterTable = Registry.GetMonsterTable();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ namespace GFramework.Game.Tests.Config;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证消费者项目通过 `schemas/**/*.schema.json` 自动拾取 schema 后,
|
/// 验证消费者项目通过 `schemas/**/*.schema.json` 自动拾取 schema 后,
|
||||||
/// 可以直接编译并使用生成的注册辅助、强类型访问入口、查询辅助与运行时加载链路。
|
/// 可以直接编译并使用生成的聚合注册辅助、强类型访问入口、查询辅助与运行时加载链路。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class GeneratedConfigConsumerIntegrationTests
|
public class GeneratedConfigConsumerIntegrationTests
|
||||||
@ -39,7 +39,7 @@ public class GeneratedConfigConsumerIntegrationTests
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证生成器自动拾取消费者项目的 schema 后,
|
/// 验证生成器自动拾取消费者项目的 schema 后,
|
||||||
/// 可以用生成的注册辅助完成加载,并通过强类型表包装访问运行时数据与查询辅助。
|
/// 可以用生成的聚合注册辅助完成加载,并通过强类型表包装访问运行时数据与查询辅助。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Project()
|
public async Task LoadAsync_Should_Support_Generated_Bindings_In_Consumer_Project()
|
||||||
@ -91,7 +91,7 @@ public class GeneratedConfigConsumerIntegrationTests
|
|||||||
|
|
||||||
var registry = new ConfigRegistry();
|
var registry = new ConfigRegistry();
|
||||||
var loader = new YamlConfigLoader(_rootPath)
|
var loader = new YamlConfigLoader(_rootPath)
|
||||||
.RegisterMonsterTable();
|
.RegisterAllGeneratedConfigTables();
|
||||||
|
|
||||||
await loader.LoadAsync(registry);
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
@ -100,6 +100,11 @@ public class GeneratedConfigConsumerIntegrationTests
|
|||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
|
Assert.That(GeneratedConfigCatalog.Tables.Count, Is.EqualTo(1));
|
||||||
|
Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var catalogEntry), Is.True);
|
||||||
|
Assert.That(catalogEntry.ConfigDomain, Is.EqualTo("monster"));
|
||||||
|
Assert.That(catalogEntry.ConfigRelativePath, Is.EqualTo("monster"));
|
||||||
|
Assert.That(catalogEntry.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
|
||||||
Assert.That(MonsterConfigBindings.ConfigDomain, Is.EqualTo("monster"));
|
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"));
|
||||||
|
|||||||
@ -168,6 +168,8 @@ public class SchemaConfigGeneratorSnapshotTests
|
|||||||
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",
|
await AssertSnapshotAsync(generatedSources, snapshotFolder, "MonsterConfigBindings.g.cs",
|
||||||
"MonsterConfigBindings.g.txt");
|
"MonsterConfigBindings.g.txt");
|
||||||
|
await AssertSnapshotAsync(generatedSources, snapshotFolder, "GeneratedConfigCatalog.g.cs",
|
||||||
|
"GeneratedConfigCatalog.g.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -212,4 +214,4 @@ public class SchemaConfigGeneratorSnapshotTests
|
|||||||
{
|
{
|
||||||
return text.Replace("\r\n", "\n", StringComparison.Ordinal).Trim();
|
return text.Replace("\r\n", "\n", StringComparison.Ordinal).Trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -351,4 +351,111 @@ public class SchemaConfigGeneratorTests
|
|||||||
Assert.That(tableSource, Does.Not.Contain("TryFindFirstByReward("));
|
Assert.That(tableSource, Does.Not.Contain("TryFindFirstByReward("));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证生成器会为当前消费者项目内的全部 schema 额外产出聚合注册入口,
|
||||||
|
/// 让 C# 启动代码可以一行注册所有生成表。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Run_Should_Generate_Project_Level_Registration_Catalog()
|
||||||
|
{
|
||||||
|
const string source = """
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Abstractions.Config
|
||||||
|
{
|
||||||
|
public interface IConfigTable
|
||||||
|
{
|
||||||
|
Type KeyType { get; }
|
||||||
|
Type ValueType { get; }
|
||||||
|
int Count { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IConfigTable<TKey, TValue> : IConfigTable
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
TValue Get(TKey key);
|
||||||
|
bool TryGet(TKey key, out TValue? value);
|
||||||
|
bool ContainsKey(TKey key);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string monsterSchema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "integer" },
|
||||||
|
"name": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
const string itemSchema = """
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["id"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"rarity": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var result = SchemaGeneratorTestDriver.Run(
|
||||||
|
source,
|
||||||
|
("monster.schema.json", monsterSchema),
|
||||||
|
("item.schema.json", itemSchema));
|
||||||
|
|
||||||
|
var generatedSources = result.Results
|
||||||
|
.Single()
|
||||||
|
.GeneratedSources
|
||||||
|
.ToDictionary(
|
||||||
|
static sourceResult => sourceResult.HintName,
|
||||||
|
static sourceResult => sourceResult.SourceText.ToString(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
|
||||||
|
Assert.That(generatedSources.TryGetValue("GeneratedConfigCatalog.g.cs", out var catalogSource), Is.True);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigCatalog"));
|
||||||
|
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("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)"));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,115 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GFramework.Game.Config.Generated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a project-level catalog for every config table generated from the current consumer project's schemas.
|
||||||
|
/// Use this entry point when you want the C# runtime bootstrap path to register all generated tables without repeating one call per schema.
|
||||||
|
/// </summary>
|
||||||
|
public static class GeneratedConfigCatalog
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Describes one generated config table so bootstrap code can enumerate generated domains without re-parsing schema files at runtime.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct TableMetadata
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes one generated table metadata entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configDomain">Logical config domain derived from the schema base name.</param>
|
||||||
|
/// <param name="tableName">Runtime registration name.</param>
|
||||||
|
/// <param name="configRelativePath">Relative YAML directory path.</param>
|
||||||
|
/// <param name="schemaRelativePath">Relative schema file path.</param>
|
||||||
|
public TableMetadata(
|
||||||
|
string configDomain,
|
||||||
|
string tableName,
|
||||||
|
string configRelativePath,
|
||||||
|
string schemaRelativePath)
|
||||||
|
{
|
||||||
|
ConfigDomain = configDomain ?? throw new global::System.ArgumentNullException(nameof(configDomain));
|
||||||
|
TableName = tableName ?? throw new global::System.ArgumentNullException(nameof(tableName));
|
||||||
|
ConfigRelativePath = configRelativePath ?? throw new global::System.ArgumentNullException(nameof(configRelativePath));
|
||||||
|
SchemaRelativePath = schemaRelativePath ?? throw new global::System.ArgumentNullException(nameof(schemaRelativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the logical config domain derived from the schema base name.
|
||||||
|
/// </summary>
|
||||||
|
public string ConfigDomain { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the runtime registration name used by <see cref="global::GFramework.Game.Config.YamlConfigLoader" />.
|
||||||
|
/// </summary>
|
||||||
|
public string TableName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the relative directory that stores YAML files for the generated config table.
|
||||||
|
/// </summary>
|
||||||
|
public string ConfigRelativePath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the relative schema file path collected by the source generator.
|
||||||
|
/// </summary>
|
||||||
|
public string SchemaRelativePath { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets metadata for every generated config table in the current consumer project.
|
||||||
|
/// </summary>
|
||||||
|
public static global::System.Collections.Generic.IReadOnlyList<TableMetadata> Tables { get; } = global::System.Array.AsReadOnly(new TableMetadata[]
|
||||||
|
{
|
||||||
|
new(
|
||||||
|
MonsterConfigBindings.Metadata.ConfigDomain,
|
||||||
|
MonsterConfigBindings.Metadata.TableName,
|
||||||
|
MonsterConfigBindings.Metadata.ConfigRelativePath,
|
||||||
|
MonsterConfigBindings.Metadata.SchemaRelativePath),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to resolve generated table metadata by runtime registration name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">Runtime registration name.</param>
|
||||||
|
/// <param name="metadata">Resolved generated table metadata when the registration name exists; otherwise the default value.</param>
|
||||||
|
/// <returns><see langword="true" /> when the registration name belongs to a generated config table; otherwise <see langword="false" />.</returns>
|
||||||
|
public static bool TryGetByTableName(string tableName, out TableMetadata metadata)
|
||||||
|
{
|
||||||
|
if (tableName is null)
|
||||||
|
{
|
||||||
|
throw new global::System.ArgumentNullException(nameof(tableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(tableName, MonsterConfigBindings.Metadata.TableName, global::System.StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
metadata = Tables[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a single extension method that registers every generated config table discovered in the current consumer project.
|
||||||
|
/// </summary>
|
||||||
|
public static class GeneratedConfigRegistrationExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers all generated config tables using schema-derived conventions so bootstrap code can stay one-line even as schemas grow.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loader">Target YAML config loader.</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)
|
||||||
|
{
|
||||||
|
if (loader is null)
|
||||||
|
{
|
||||||
|
throw new global::System.ArgumentNullException(nameof(loader));
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.RegisterMonsterTable();
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -41,6 +41,25 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
$"{result.Schema.EntityName}ConfigBindings.g.cs",
|
$"{result.Schema.EntityName}ConfigBindings.g.cs",
|
||||||
SourceText.From(GenerateBindingsClass(result.Schema), Encoding.UTF8));
|
SourceText.From(GenerateBindingsClass(result.Schema), Encoding.UTF8));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var collectedSchemas = schemaFiles.Collect();
|
||||||
|
context.RegisterSourceOutput(collectedSchemas, static (productionContext, results) =>
|
||||||
|
{
|
||||||
|
var schemas = results
|
||||||
|
.Where(static result => result.Schema is not null)
|
||||||
|
.Select(static result => result.Schema!)
|
||||||
|
.OrderBy(static schema => schema.TableRegistrationName, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (schemas.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
productionContext.AddSource(
|
||||||
|
"GeneratedConfigCatalog.g.cs",
|
||||||
|
SourceText.From(GenerateCatalogClass(schemas), Encoding.UTF8));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -940,6 +959,166 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
|
|||||||
return builder.ToString().TrimEnd();
|
return builder.ToString().TrimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成项目级聚合辅助源码。
|
||||||
|
/// 该辅助把当前消费者项目内所有有效 schema 汇总为一个统一入口,
|
||||||
|
/// 以便运行时快速完成批量注册并在需要时枚举已生成的配置域元数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="schemas">当前编译中成功解析的 schema 集合。</param>
|
||||||
|
/// <returns>聚合辅助源码。</returns>
|
||||||
|
private static string GenerateCatalogClass(IReadOnlyList<SchemaFileSpec> schemas)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("// <auto-generated />");
|
||||||
|
builder.AppendLine("#nullable enable");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine($"namespace {GeneratedNamespace};");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("/// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
"/// Provides a project-level catalog for every config table generated from the current consumer project's schemas.");
|
||||||
|
builder.AppendLine(
|
||||||
|
"/// Use this entry point when you want the C# runtime bootstrap path to register all generated tables without repeating one call per schema.");
|
||||||
|
builder.AppendLine("/// </summary>");
|
||||||
|
builder.AppendLine("public static class GeneratedConfigCatalog");
|
||||||
|
builder.AppendLine("{");
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// Describes one generated config table so bootstrap code can enumerate generated domains without re-parsing schema files at runtime.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" public readonly struct TableMetadata");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Initializes one generated table metadata entry.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" /// <param name=\"configDomain\">Logical config domain derived from the schema base name.</param>");
|
||||||
|
builder.AppendLine(" /// <param name=\"tableName\">Runtime registration name.</param>");
|
||||||
|
builder.AppendLine(" /// <param name=\"configRelativePath\">Relative YAML directory path.</param>");
|
||||||
|
builder.AppendLine(" /// <param name=\"schemaRelativePath\">Relative schema file path.</param>");
|
||||||
|
builder.AppendLine(" public TableMetadata(");
|
||||||
|
builder.AppendLine(" string configDomain,");
|
||||||
|
builder.AppendLine(" string tableName,");
|
||||||
|
builder.AppendLine(" string configRelativePath,");
|
||||||
|
builder.AppendLine(" string schemaRelativePath)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(
|
||||||
|
" ConfigDomain = configDomain ?? throw new global::System.ArgumentNullException(nameof(configDomain));");
|
||||||
|
builder.AppendLine(
|
||||||
|
" TableName = tableName ?? throw new global::System.ArgumentNullException(nameof(tableName));");
|
||||||
|
builder.AppendLine(
|
||||||
|
" ConfigRelativePath = configRelativePath ?? throw new global::System.ArgumentNullException(nameof(configRelativePath));");
|
||||||
|
builder.AppendLine(
|
||||||
|
" SchemaRelativePath = schemaRelativePath ?? throw new global::System.ArgumentNullException(nameof(schemaRelativePath));");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Gets the logical config domain derived from the schema base name.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" public string ConfigDomain { get; }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Gets the runtime registration name used by <see cref=\"global::GFramework.Game.Config.YamlConfigLoader\" />.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" public string TableName { get; }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Gets the relative directory that stores YAML files for the generated config table.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" public string ConfigRelativePath { get; }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Gets the relative schema file path collected by the source generator.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" public string SchemaRelativePath { get; }");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Gets metadata for every generated config table in the current consumer project.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" public static global::System.Collections.Generic.IReadOnlyList<TableMetadata> Tables { get; } = global::System.Array.AsReadOnly(new TableMetadata[]");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
|
||||||
|
foreach (var schema in schemas)
|
||||||
|
{
|
||||||
|
builder.AppendLine(" new(");
|
||||||
|
builder.AppendLine($" {schema.EntityName}ConfigBindings.Metadata.ConfigDomain,");
|
||||||
|
builder.AppendLine($" {schema.EntityName}ConfigBindings.Metadata.TableName,");
|
||||||
|
builder.AppendLine($" {schema.EntityName}ConfigBindings.Metadata.ConfigRelativePath,");
|
||||||
|
builder.AppendLine($" {schema.EntityName}ConfigBindings.Metadata.SchemaRelativePath),");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" });");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(" /// Tries to resolve generated table metadata by runtime registration name.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" /// <param name=\"tableName\">Runtime registration name.</param>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// <param name=\"metadata\">Resolved generated table metadata when the registration name exists; otherwise the default value.</param>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// <returns><see langword=\"true\" /> when the registration name belongs to a generated config table; otherwise <see langword=\"false\" />.</returns>");
|
||||||
|
builder.AppendLine(" public static bool TryGetByTableName(string tableName, out TableMetadata metadata)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" if (tableName is null)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(tableName));");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
|
||||||
|
for (var index = 0; index < schemas.Count; index++)
|
||||||
|
{
|
||||||
|
var schema = schemas[index];
|
||||||
|
builder.AppendLine(
|
||||||
|
$" if (string.Equals(tableName, {schema.EntityName}ConfigBindings.Metadata.TableName, global::System.StringComparison.Ordinal))");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(
|
||||||
|
$" metadata = Tables[{index.ToString(CultureInfo.InvariantCulture)}];");
|
||||||
|
builder.AppendLine(" return true;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" metadata = default;");
|
||||||
|
builder.AppendLine(" return false;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
builder.AppendLine();
|
||||||
|
builder.AppendLine("/// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
"/// Provides a single extension method that registers every generated config table discovered in the current consumer project.");
|
||||||
|
builder.AppendLine("/// </summary>");
|
||||||
|
builder.AppendLine("public static class GeneratedConfigRegistrationExtensions");
|
||||||
|
builder.AppendLine("{");
|
||||||
|
builder.AppendLine(" /// <summary>");
|
||||||
|
builder.AppendLine(
|
||||||
|
" /// Registers all generated config tables using schema-derived conventions so bootstrap code can stay one-line even as schemas grow.");
|
||||||
|
builder.AppendLine(" /// </summary>");
|
||||||
|
builder.AppendLine(" /// <param name=\"loader\">Target YAML config loader.</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(" {");
|
||||||
|
builder.AppendLine(" if (loader is null)");
|
||||||
|
builder.AppendLine(" {");
|
||||||
|
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(loader));");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine();
|
||||||
|
|
||||||
|
foreach (var schema in schemas)
|
||||||
|
{
|
||||||
|
builder.AppendLine($" loader.Register{schema.EntityName}Table();");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine(" return loader;");
|
||||||
|
builder.AppendLine(" }");
|
||||||
|
builder.AppendLine("}");
|
||||||
|
return builder.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 收集 schema 中声明的跨表引用元数据,并为生成代码分配稳定成员名。
|
/// 收集 schema 中声明的跨表引用元数据,并为生成代码分配稳定成员名。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
- 一对象一文件的目录组织
|
- 一对象一文件的目录组织
|
||||||
- 运行时只读查询
|
- 运行时只读查询
|
||||||
- Runtime / Generator / Tooling 共享支持 `minimum`、`maximum`、`minLength`、`maxLength`
|
- Runtime / Generator / Tooling 共享支持 `minimum`、`maximum`、`minLength`、`maxLength`
|
||||||
- Source Generator 生成配置类型、表包装和注册/访问辅助
|
- Source Generator 生成配置类型、表包装、单表注册/访问辅助,以及项目级聚合注册目录
|
||||||
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
- VS Code 插件提供配置浏览、raw 编辑、schema 打开、递归轻量校验和嵌套对象表单入口
|
||||||
|
|
||||||
## 推荐目录结构
|
## 推荐目录结构
|
||||||
@ -193,8 +193,7 @@ public sealed class GameConfigBootstrap : IDisposable
|
|||||||
public async Task InitializeAsync(string configRootPath, bool enableHotReload = false)
|
public async Task InitializeAsync(string configRootPath, bool enableHotReload = false)
|
||||||
{
|
{
|
||||||
var loader = new YamlConfigLoader(configRootPath)
|
var loader = new YamlConfigLoader(configRootPath)
|
||||||
.RegisterMonsterTable()
|
.RegisterAllGeneratedConfigTables();
|
||||||
.RegisterItemTable();
|
|
||||||
|
|
||||||
await loader.LoadAsync(_registry);
|
await loader.LoadAsync(_registry);
|
||||||
|
|
||||||
@ -223,7 +222,7 @@ public sealed class GameConfigBootstrap : IDisposable
|
|||||||
|
|
||||||
这段模板刻意遵循几个约定:
|
这段模板刻意遵循几个约定:
|
||||||
|
|
||||||
- 优先使用生成器产出的 `Register*Table()`,避免手写表名、路径和 key selector
|
- 优先使用生成器产出的 `RegisterAllGeneratedConfigTables()`,把多表注册收敛为一个稳定入口
|
||||||
- 由一个长生命周期对象持有 `ConfigRegistry`
|
- 由一个长生命周期对象持有 `ConfigRegistry`
|
||||||
- 热重载句柄和配置生命周期绑在一起,避免监听器泄漏
|
- 热重载句柄和配置生命周期绑在一起,避免监听器泄漏
|
||||||
|
|
||||||
@ -331,8 +330,7 @@ public sealed class GameArchitecture : Architecture
|
|||||||
var registry = RegisterUtility(new ConfigRegistry());
|
var registry = RegisterUtility(new ConfigRegistry());
|
||||||
|
|
||||||
var loader = new YamlConfigLoader(_configRootPath)
|
var loader = new YamlConfigLoader(_configRootPath)
|
||||||
.RegisterMonsterTable()
|
.RegisterAllGeneratedConfigTables();
|
||||||
.RegisterItemTable();
|
|
||||||
|
|
||||||
loader.LoadAsync(registry).GetAwaiter().GetResult();
|
loader.LoadAsync(registry).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
@ -354,7 +352,7 @@ var slime = monsterTable.Get(1);
|
|||||||
- 在 `OnInitialize()` 内完成首次 `LoadAsync`
|
- 在 `OnInitialize()` 内完成首次 `LoadAsync`
|
||||||
- 初始化完成后只通过注册表和生成表包装访问配置
|
- 初始化完成后只通过注册表和生成表包装访问配置
|
||||||
|
|
||||||
当前阶段不建议为了配置系统额外引入新的 `IArchitectureModule` 或 service module 抽象;现有 `Architecture + ConfigRegistry + YamlConfigLoader + Register*Table()` 组合已经足够作为官方推荐接入路径。
|
当前阶段不建议为了配置系统额外引入新的 `IArchitectureModule` 或 service module 抽象;现有 `Architecture + ConfigRegistry + YamlConfigLoader + RegisterAllGeneratedConfigTables()` 组合已经足够作为官方推荐接入路径。
|
||||||
|
|
||||||
### 热重载模板
|
### 热重载模板
|
||||||
|
|
||||||
@ -408,7 +406,7 @@ 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")
|
||||||
.RegisterMonsterTable();
|
.RegisterAllGeneratedConfigTables();
|
||||||
|
|
||||||
await loader.LoadAsync(registry);
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
@ -416,6 +414,11 @@ var monsterTable = registry.GetMonsterTable();
|
|||||||
var slime = monsterTable.Get(1);
|
var slime = monsterTable.Get(1);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
这里推荐把“注册全部已生成配置表”和“读取单表强类型元数据”分成两层:
|
||||||
|
|
||||||
|
- 启动层优先走 `RegisterAllGeneratedConfigTables()`,避免每新增一个 schema 都要回到启动代码继续补链式调用
|
||||||
|
- 消费层继续通过 `GetMonsterTable()`、`MonsterConfigBindings.Metadata` 这类单表入口读取强类型信息
|
||||||
|
|
||||||
这组辅助会把以下约定固化到生成代码里:
|
这组辅助会把以下约定固化到生成代码里:
|
||||||
|
|
||||||
- 配置域常量,例如 `MonsterConfigBindings.ConfigDomain`
|
- 配置域常量,例如 `MonsterConfigBindings.ConfigDomain`
|
||||||
@ -433,6 +436,24 @@ var configPath = MonsterConfigBindings.Metadata.ConfigRelativePath;
|
|||||||
var schemaPath = MonsterConfigBindings.Metadata.SchemaRelativePath;
|
var schemaPath = MonsterConfigBindings.Metadata.SchemaRelativePath;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如果你需要在启动或诊断代码里枚举当前消费者项目里有哪些生成表,也可以直接读取项目级目录:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
foreach (var metadata in GeneratedConfigCatalog.Tables)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{metadata.TableName} -> {metadata.SchemaRelativePath}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以按表名回查:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if (GeneratedConfigCatalog.TryGetByTableName("monster", out var metadata))
|
||||||
|
{
|
||||||
|
Console.WriteLine(metadata.ConfigRelativePath);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
如果你需要自定义目录、表名或 key selector,仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
|
如果你需要自定义目录、表名或 key selector,仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
|
||||||
|
|
||||||
如果你希望把 schema 路径、比较器以及未来扩展开关集中到一个对象里,推荐改用选项对象入口:
|
如果你希望把 schema 路径、比较器以及未来扩展开关集中到一个对象里,推荐改用选项对象入口:
|
||||||
@ -553,7 +574,7 @@ 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")
|
||||||
.RegisterMonsterTable();
|
.RegisterAllGeneratedConfigTables();
|
||||||
|
|
||||||
await loader.LoadAsync(registry);
|
await loader.LoadAsync(registry);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user