using System.IO; using GFramework.Game.Config; namespace GFramework.Game.Tests.Config; /// /// 验证 YAML 配置加载器的目录扫描与注册行为。 /// [TestFixture] public class YamlConfigLoaderTests { /// /// 为每个测试创建独立临时目录,避免文件系统状态互相污染。 /// [SetUp] public void SetUp() { _rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigTests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_rootPath); } /// /// 清理测试期间创建的临时目录。 /// [TearDown] public void TearDown() { if (Directory.Exists(_rootPath)) { Directory.Delete(_rootPath, true); } } private string _rootPath = null!; /// /// 验证加载器能够扫描 YAML 文件并将结果写入注册表。 /// [Test] public async Task LoadAsync_Should_Register_Table_From_Yaml_Files() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime hp: 10 """); CreateConfigFile( "monster/goblin.yml", """ id: 2 name: Goblin hp: 30 """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", static config => config.Id); var registry = new ConfigRegistry(); await loader.LoadAsync(registry); var table = registry.GetTable("monster"); Assert.Multiple(() => { 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)); }); } /// /// 验证注册的配置目录不存在时会抛出清晰错误。 /// [Test] public void LoadAsync_Should_Throw_When_Config_Directory_Does_Not_Exist() { var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("monster")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证某个配置表加载失败时,注册表不会留下部分成功的中间状态。 /// [Test] public void LoadAsync_Should_Not_Mutate_Registry_When_A_Later_Table_Fails() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime hp: 10 """); var registry = new ConfigRegistry(); registry.RegisterTable( "existing", new InMemoryConfigTable( new[] { new ExistingConfigStub(100, "Original") }, static config => config.Id)); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", static config => config.Id) .RegisterTable("broken", "broken", static config => config.Id); Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(registry.Count, Is.EqualTo(1)); Assert.That(registry.HasTable("monster"), Is.False); Assert.That(registry.GetTable("existing").Get(100).Name, Is.EqualTo("Original")); }); } /// /// 验证非法 YAML 会被包装成带文件路径的反序列化错误。 /// [Test] public void LoadAsync_Should_Throw_With_File_Path_When_Yaml_Is_Invalid() { CreateConfigFile( "monster/slime.yaml", """ id: [1 name: Slime """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("slime.yaml")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证启用 schema 校验后,缺失必填字段会在反序列化前被拒绝。 /// [Test] public void LoadAsync_Should_Throw_When_Required_Property_Is_Missing_According_To_Schema() { CreateConfigFile( "monster/slime.yaml", """ id: 1 hp: 10 """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "hp": { "type": "integer" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("name")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证启用 schema 校验后,类型不匹配的标量字段会被拒绝。 /// [Test] public void LoadAsync_Should_Throw_When_Property_Type_Does_Not_Match_Schema() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime hp: high """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "hp": { "type": "integer" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("hp")); Assert.That(exception!.Message, Does.Contain("integer")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证启用 schema 校验后,标量 enum 限制会在运行时被拒绝。 /// [Test] public void LoadAsync_Should_Throw_When_Scalar_Value_Is_Not_Declared_In_Schema_Enum() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime rarity: epic """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name", "rarity"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "rarity": { "type": "string", "enum": ["common", "rare"] } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("common")); Assert.That(exception!.Message, Does.Contain("rare")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证启用 schema 校验后,未知字段不会再被静默忽略。 /// [Test] public void LoadAsync_Should_Throw_When_Unknown_Property_Is_Present_In_Schema_Bound_Mode() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime attackPower: 2 """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "hp": { "type": "integer" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("attackPower")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证数组字段的元素类型会按 schema 校验。 /// [Test] public void LoadAsync_Should_Throw_When_Array_Item_Type_Does_Not_Match_Schema() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime dropRates: - 1 - potion """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "dropRates": { "type": "array", "items": { "type": "integer" } } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable( "monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("dropRates")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证数组元素上的 enum 限制会按 schema 在运行时生效。 /// [Test] public void LoadAsync_Should_Throw_When_Array_Item_Is_Not_Declared_In_Schema_Enum() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime tags: - fire - poison """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string", "enum": ["fire", "ice"] } } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("fire")); Assert.That(exception!.Message, Does.Contain("ice")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证嵌套对象中的必填字段同样会按 schema 在运行时生效。 /// [Test] public void LoadAsync_Should_Throw_When_Nested_Object_Is_Missing_Required_Property() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime reward: gold: 10 """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name", "reward"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "reward": { "type": "object", "required": ["gold", "currency"], "properties": { "gold": { "type": "integer" }, "currency": { "type": "string" } } } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("reward.currency")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证对象数组中的嵌套字段也会按 schema 递归校验。 /// [Test] public void LoadAsync_Should_Throw_When_Object_Array_Item_Contains_Unknown_Property() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime phases: - wave: 1 monsterId: slime hpScale: 1.5 """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name", "phases"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "phases": { "type": "array", "items": { "type": "object", "required": ["wave", "monsterId"], "properties": { "wave": { "type": "integer" }, "monsterId": { "type": "string" } } } } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("phases[0].hpScale")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证深层对象数组中的跨表引用也会参与整批加载校验。 /// [Test] public void LoadAsync_Should_Throw_When_Nested_Object_Array_Reference_Target_Is_Missing() { CreateConfigFile( "item/potion.yaml", """ id: potion name: Potion """); CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime phases: - wave: 1 dropItemId: potion - wave: 2 dropItemId: bomb """); CreateSchemaFile( "schemas/item.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "string" }, "name": { "type": "string" } } } """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name", "phases"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "phases": { "type": "array", "items": { "type": "object", "required": ["wave", "dropItemId"], "properties": { "wave": { "type": "integer" }, "dropItemId": { "type": "string", "x-gframework-ref-table": "item" } } } } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("item", "item", "schemas/item.schema.json", static config => config.Id) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("phases[1].dropItemId")); Assert.That(exception!.Message, Does.Contain("bomb")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证绑定跨表引用 schema 时,存在的目标行可以通过加载校验。 /// [Test] public async Task LoadAsync_Should_Accept_Existing_Cross_Table_Reference() { CreateConfigFile( "item/potion.yaml", """ id: potion name: Potion """); CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime dropItemId: potion """); CreateSchemaFile( "schemas/item.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "string" }, "name": { "type": "string" } } } """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name", "dropItemId"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "dropItemId": { "type": "string", "x-gframework-ref-table": "item" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("item", "item", "schemas/item.schema.json", static config => config.Id) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); await loader.LoadAsync(registry); Assert.Multiple(() => { Assert.That(registry.GetTable("item").ContainsKey("potion"), Is.True); Assert.That(registry.GetTable("monster").Get(1).DropItemId, Is.EqualTo("potion")); }); } /// /// 验证缺失的跨表引用会阻止整批配置写入注册表。 /// [Test] public void LoadAsync_Should_Throw_When_Cross_Table_Reference_Target_Is_Missing() { CreateConfigFile( "item/slime-gel.yaml", """ id: slime_gel name: Slime Gel """); CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime dropItemId: potion """); CreateSchemaFile( "schemas/item.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "string" }, "name": { "type": "string" } } } """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name", "dropItemId"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "dropItemId": { "type": "string", "x-gframework-ref-table": "item" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("item", "item", "schemas/item.schema.json", static config => config.Id) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("dropItemId")); Assert.That(exception!.Message, Does.Contain("potion")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证跨表引用同样支持标量数组中的每个元素。 /// [Test] public void LoadAsync_Should_Throw_When_Array_Reference_Item_Is_Missing() { CreateConfigFile( "item/potion.yaml", """ id: potion name: Potion """); CreateConfigFile( "item/slime-gel.yaml", """ id: slime_gel name: Slime Gel """); CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime dropItemIds: - potion - missing_item """); CreateSchemaFile( "schemas/item.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "string" }, "name": { "type": "string" } } } """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name", "dropItemIds"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "dropItemIds": { "type": "array", "items": { "type": "string" }, "x-gframework-ref-table": "item" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("item", "item", "schemas/item.schema.json", static config => config.Id) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry)); Assert.Multiple(() => { Assert.That(exception, Is.Not.Null); Assert.That(exception!.Message, Does.Contain("dropItemIds[1]")); Assert.That(exception!.Message, Does.Contain("missing_item")); Assert.That(registry.Count, Is.EqualTo(0)); }); } /// /// 验证启用热重载后,配置文件内容变更会刷新已注册配置表。 /// [Test] public async Task EnableHotReload_Should_Update_Registered_Table_When_Config_File_Changes() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime hp: 10 """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "hp": { "type": "integer" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); await loader.LoadAsync(registry); var reloadTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var hotReload = loader.EnableHotReload( registry, onTableReloaded: tableName => reloadTaskSource.TrySetResult(tableName), debounceDelay: TimeSpan.FromMilliseconds(150)); try { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime hp: 25 """); var tableName = await WaitForTaskWithinAsync(reloadTaskSource.Task, TimeSpan.FromSeconds(5)); Assert.Multiple(() => { Assert.That(tableName, Is.EqualTo("monster")); Assert.That(registry.GetTable("monster").Get(1).Hp, Is.EqualTo(25)); }); } finally { hotReload.UnRegister(); } } /// /// 验证热重载失败时会保留旧表状态,并通过失败回调暴露诊断信息。 /// [Test] public async Task EnableHotReload_Should_Keep_Previous_Table_When_Schema_Change_Makes_Reload_Fail() { CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime hp: 10 """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "hp": { "type": "integer" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); await loader.LoadAsync(registry); var reloadFailureTaskSource = new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions .RunContinuationsAsynchronously); var hotReload = loader.EnableHotReload( registry, onTableReloadFailed: (tableName, exception) => reloadFailureTaskSource.TrySetResult((tableName, exception)), debounceDelay: TimeSpan.FromMilliseconds(150)); try { CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name", "rarity"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "hp": { "type": "integer" }, "rarity": { "type": "string" } } } """); var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5)); Assert.Multiple(() => { Assert.That(failure.TableName, Is.EqualTo("monster")); Assert.That(failure.Exception.Message, Does.Contain("rarity")); Assert.That(registry.GetTable("monster").Get(1).Hp, Is.EqualTo(10)); }); } finally { hotReload.UnRegister(); } } /// /// 验证当被引用表变更导致依赖表引用失效时,热重载会整体回滚受影响表。 /// [Test] public async Task EnableHotReload_Should_Keep_Previous_State_When_Dependency_Table_Breaks_Cross_Table_Reference() { CreateConfigFile( "item/potion.yaml", """ id: potion name: Potion """); CreateConfigFile( "monster/slime.yaml", """ id: 1 name: Slime dropItemId: potion """); CreateSchemaFile( "schemas/item.schema.json", """ { "type": "object", "required": ["id", "name"], "properties": { "id": { "type": "string" }, "name": { "type": "string" } } } """); CreateSchemaFile( "schemas/monster.schema.json", """ { "type": "object", "required": ["id", "name", "dropItemId"], "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "dropItemId": { "type": "string", "x-gframework-ref-table": "item" } } } """); var loader = new YamlConfigLoader(_rootPath) .RegisterTable("item", "item", "schemas/item.schema.json", static config => config.Id) .RegisterTable("monster", "monster", "schemas/monster.schema.json", static config => config.Id); var registry = new ConfigRegistry(); await loader.LoadAsync(registry); var reloadFailureTaskSource = new TaskCompletionSource<(string TableName, Exception Exception)>(TaskCreationOptions .RunContinuationsAsynchronously); var hotReload = loader.EnableHotReload( registry, onTableReloadFailed: (tableName, exception) => reloadFailureTaskSource.TrySetResult((tableName, exception)), debounceDelay: TimeSpan.FromMilliseconds(150)); try { CreateConfigFile( "item/potion.yaml", """ id: elixir name: Elixir """); var failure = await WaitForTaskWithinAsync(reloadFailureTaskSource.Task, TimeSpan.FromSeconds(5)); Assert.Multiple(() => { Assert.That(failure.TableName, Is.EqualTo("item")); Assert.That(failure.Exception.Message, Does.Contain("dropItemId")); Assert.That(registry.GetTable("item").ContainsKey("potion"), Is.True); Assert.That(registry.GetTable("monster").Get(1).DropItemId, Is.EqualTo("potion")); }); } finally { hotReload.UnRegister(); } } /// /// 创建测试用配置文件。 /// /// 相对根目录的文件路径。 /// 文件内容。 private void CreateConfigFile(string relativePath, string content) { var fullPath = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar)); var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } File.WriteAllText(fullPath, content); } /// /// 创建测试用 schema 文件。 /// /// 相对根目录的文件路径。 /// 文件内容。 private void CreateSchemaFile(string relativePath, string content) { CreateConfigFile(relativePath, content); } /// /// 在限定时间内等待异步任务完成,避免文件监听测试无限挂起。 /// /// 任务结果类型。 /// 要等待的任务。 /// 超时时间。 /// 任务结果。 private static async Task WaitForTaskWithinAsync(Task task, TimeSpan timeout) { var completedTask = await Task.WhenAny(task, Task.Delay(timeout)); if (!ReferenceEquals(completedTask, task)) { Assert.Fail($"Timed out after {timeout} while waiting for file watcher notification."); } return await task; } /// /// 用于 YAML 加载测试的最小怪物配置类型。 /// private sealed class MonsterConfigStub { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; /// /// 获取或设置生命值。 /// public int Hp { get; set; } } /// /// 用于数组 schema 校验测试的最小怪物配置类型。 /// private sealed class MonsterConfigIntegerArrayStub { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; /// /// 获取或设置掉落率列表。 /// public IReadOnlyList DropRates { get; set; } = Array.Empty(); } /// /// 用于嵌套对象 schema 校验测试的最小怪物配置类型。 /// private sealed class MonsterNestedConfigStub { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; /// /// 获取或设置奖励对象。 /// public RewardConfigStub Reward { get; set; } = new(); } /// /// 表示嵌套奖励对象的测试桩类型。 /// private sealed class RewardConfigStub { /// /// 获取或设置金币数量。 /// public int Gold { get; set; } /// /// 获取或设置货币类型。 /// public string Currency { get; set; } = string.Empty; } /// /// 用于对象数组 schema 校验测试的怪物配置类型。 /// private sealed class MonsterPhaseArrayConfigStub { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; /// /// 获取或设置阶段数组。 /// public IReadOnlyList Phases { get; set; } = Array.Empty(); } /// /// 表示对象数组中的阶段元素。 /// private sealed class PhaseConfigStub { /// /// 获取或设置波次编号。 /// public int Wave { get; set; } /// /// 获取或设置怪物主键。 /// public string MonsterId { get; set; } = string.Empty; } /// /// 用于深层跨表引用测试的怪物配置类型。 /// private sealed class MonsterPhaseDropConfigStub { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; /// /// 获取或设置阶段数组。 /// public List Phases { get; set; } = new(); } /// /// 表示带有掉落引用的阶段元素。 /// private sealed class PhaseDropConfigStub { /// /// 获取或设置波次编号。 /// public int Wave { get; set; } /// /// 获取或设置掉落物品主键。 /// public string DropItemId { get; set; } = string.Empty; } /// /// 用于跨表引用测试的最小物品配置类型。 /// private sealed class ItemConfigStub { /// /// 获取或设置主键。 /// public string Id { get; set; } = string.Empty; /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; } /// /// 用于单值跨表引用测试的怪物配置类型。 /// private sealed class MonsterDropConfigStub { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; /// /// 获取或设置掉落物品主键。 /// public string DropItemId { get; set; } = string.Empty; } /// /// 用于数组跨表引用测试的怪物配置类型。 /// private sealed class MonsterDropArrayConfigStub { /// /// 获取或设置主键。 /// public int Id { get; set; } /// /// 获取或设置名称。 /// public string Name { get; set; } = string.Empty; /// /// 获取或设置掉落物品主键列表。 /// public List DropItemIds { get; set; } = new(); } /// /// 用于验证注册表一致性的现有配置类型。 /// /// 配置主键。 /// 配置名称。 private sealed record ExistingConfigStub(int Id, string Name); }