diff --git a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs index 510ef01b..d8631e5d 100644 --- a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs +++ b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using GFramework.Core.Architectures; +using GFramework.Game.Abstractions.Config; using GFramework.Game.Config; using GFramework.Game.Config.Generated; using NUnit.Framework; @@ -10,14 +11,14 @@ using NUnit.Framework; namespace GFramework.Game.Tests.Config; /// -/// 验证在 初始化流程中可以通过聚合注册入口加载生成配置表,并通过表访问器读取数据。 +/// 验证在 初始化流程中可以通过官方配置启动帮助器加载生成配置表,并通过表访问器读取数据。 /// [TestFixture] public class ArchitectureConfigIntegrationTests { /// - /// 架构初始化期间,通过 的聚合注册入口注册生成表, - /// 并将 作为 utility 暴露给架构上下文读取。 + /// 架构初始化期间,通过 收敛生成表注册、加载与注册表暴露, + /// 并将 作为 utility 暴露给架构上下文读取。 /// [Test] public async Task ConfigLoaderCanRunDuringArchitectureInitialization() @@ -43,7 +44,7 @@ public class ArchitectureConfigIntegrationTests Assert.That(retrieved, Is.Not.Null); Assert.That(retrieved!.Get(1).Name, Is.EqualTo("Slime")); Assert.That(architecture.Registry.TryGetItemTable(out _), Is.False); - Assert.That(architecture.Context.GetUtility(), Is.SameAs(architecture.Registry)); + Assert.That(architecture.Context.GetUtility(), Is.SameAs(architecture.Registry)); }); } finally @@ -131,30 +132,43 @@ public class ArchitectureConfigIntegrationTests private sealed class ConsumerArchitecture : Architecture { - private readonly string _configRoot; + private readonly GameConfigBootstrap _bootstrap; - public ConfigRegistry Registry { get; } + public IConfigRegistry Registry => _bootstrap.Registry; public MonsterTable MonsterTable { get; private set; } = null!; public ConsumerArchitecture(string configRoot) { - _configRoot = configRoot ?? throw new ArgumentNullException(nameof(configRoot)); - Registry = new ConfigRegistry(); + if (configRoot == null) + { + throw new ArgumentNullException(nameof(configRoot)); + } + + _bootstrap = new GameConfigBootstrap( + new GameConfigBootstrapOptions + { + RootPath = configRoot, + ConfigureLoader = static loader => + loader.RegisterAllGeneratedConfigTables( + new GeneratedConfigRegistrationOptions + { + IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain } + }) + }); } protected override void OnInitialize() { RegisterUtility(Registry); - - var loader = new YamlConfigLoader(_configRoot) - .RegisterAllGeneratedConfigTables( - new GeneratedConfigRegistrationOptions - { - IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain } - }); - loader.LoadAsync(Registry).GetAwaiter().GetResult(); + _bootstrap.InitializeAsync().GetAwaiter().GetResult(); MonsterTable = Registry.GetMonsterTable(); } + + public override async ValueTask DestroyAsync() + { + _bootstrap.Dispose(); + await base.DestroyAsync(); + } } } diff --git a/GFramework.Game.Tests/Config/GameConfigBootstrapTests.cs b/GFramework.Game.Tests/Config/GameConfigBootstrapTests.cs new file mode 100644 index 00000000..a01d5ef3 --- /dev/null +++ b/GFramework.Game.Tests/Config/GameConfigBootstrapTests.cs @@ -0,0 +1,235 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using GFramework.Game.Abstractions.Config; +using GFramework.Game.Config; +using GFramework.Game.Config.Generated; +using NUnit.Framework; + +namespace GFramework.Game.Tests.Config; + +/// +/// 验证官方配置启动帮助器能够收敛注册、加载与热重载生命周期。 +/// +[TestFixture] +public class GameConfigBootstrapTests +{ + /// + /// 为每个测试准备独立的临时配置目录。 + /// + [SetUp] + public void SetUp() + { + _rootPath = Path.Combine(Path.GetTempPath(), "GFramework.GameConfigBootstrapTests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_rootPath); + } + + /// + /// 清理测试期间创建的临时目录。 + /// + [TearDown] + public void TearDown() + { + if (Directory.Exists(_rootPath)) + { + Directory.Delete(_rootPath, true); + } + } + + private string _rootPath = null!; + + /// + /// 验证启动帮助器能够加载生成表,并复用调用方显式提供的注册表实例。 + /// + [Test] + public async Task InitializeAsync_Should_Load_Generated_Config_Tables_Into_Registry() + { + CreateMonsterFiles(); + + var registry = new ConfigRegistry(); + using var bootstrap = CreateBootstrap(registry); + + await bootstrap.InitializeAsync(); + + var monsterTable = registry.GetMonsterTable(); + + Assert.Multiple(() => + { + Assert.That(bootstrap.Registry, Is.SameAs(registry)); + Assert.That(bootstrap.Loader.RegistrationCount, Is.EqualTo(1)); + Assert.That(bootstrap.IsInitialized, Is.True); + Assert.That(bootstrap.IsHotReloadEnabled, Is.False); + Assert.That(monsterTable.Get(1).Name, Is.EqualTo("Slime")); + Assert.That(monsterTable.Get(2).Hp, Is.EqualTo(30)); + }); + } + + /// + /// 验证启动帮助器可以在初始化后显式启用热重载,并将刷新结果写回共享注册表。 + /// + [Test] + public async Task StartHotReload_Should_Update_Registered_Table_When_Config_File_Changes() + { + CreateMonsterFiles(); + + using var bootstrap = CreateBootstrap(); + await bootstrap.InitializeAsync(); + + var reloadTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + bootstrap.StartHotReload( + new YamlConfigHotReloadOptions + { + OnTableReloaded = tableName => reloadTaskSource.TrySetResult(tableName), + DebounceDelay = TimeSpan.FromMilliseconds(150) + }); + + try + { + CreateFile( + "monster/slime.yaml", + """ + id: 1 + name: Slime + hp: 25 + faction: dungeon + """); + + var tableName = await WaitForTaskWithinAsync(reloadTaskSource.Task, TimeSpan.FromSeconds(5)); + var monsterTable = bootstrap.Registry.GetMonsterTable(); + + Assert.Multiple(() => + { + Assert.That(tableName, Is.EqualTo(MonsterConfigBindings.TableName)); + Assert.That(bootstrap.IsHotReloadEnabled, Is.True); + Assert.That(monsterTable.Get(1).Hp, Is.EqualTo(25)); + }); + } + finally + { + bootstrap.StopHotReload(); + } + } + + /// + /// 验证缺少加载器配置回调时会在构造阶段被拒绝,避免启动帮助器静默创建空加载流程。 + /// + [Test] + public void Constructor_Should_Throw_When_ConfigureLoader_Is_Missing() + { + var exception = Assert.Throws(() => + _ = new GameConfigBootstrap( + new GameConfigBootstrapOptions + { + RootPath = _rootPath + })); + + Assert.That(exception!.ParamName, Is.EqualTo("ConfigureLoader")); + } + + /// + /// 创建一个使用生成聚合注册入口的官方启动帮助器。 + /// + /// 可选的外部注册表;为空时使用默认注册表。 + /// 已配置但尚未初始化的启动帮助器。 + private GameConfigBootstrap CreateBootstrap(IConfigRegistry? registry = null) + { + return new GameConfigBootstrap( + new GameConfigBootstrapOptions + { + RootPath = _rootPath, + Registry = registry, + ConfigureLoader = static loader => + loader.RegisterAllGeneratedConfigTables( + new GeneratedConfigRegistrationOptions + { + IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain } + }) + }); + } + + /// + /// 在临时消费者根目录中创建测试文件。 + /// + /// 相对根目录的文件路径。 + /// 要写入的文件内容。 + private void CreateFile(string relativePath, string content) + { + var path = Path.Combine(_rootPath, relativePath.Replace('/', Path.DirectorySeparatorChar)); + var directoryPath = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + File.WriteAllText(path, content.Replace("\n", Environment.NewLine, StringComparison.Ordinal)); + } + + /// + /// 在临时目录中创建 monster schema 与 YAML 测试数据。 + /// + private void CreateMonsterFiles() + { + CreateFile( + "schemas/monster.schema.json", + """ + { + "title": "Monster Config", + "description": "Defines one monster entry for the bootstrap tests.", + "type": "object", + "required": ["id", "name", "hp", "faction"], + "properties": { + "id": { + "type": "integer", + "description": "Monster identifier." + }, + "name": { + "type": "string", + "description": "Monster display name." + }, + "hp": { + "type": "integer", + "description": "Monster base health." + }, + "faction": { + "type": "string", + "description": "Used by the bootstrap tests to validate generated queries." + } + } + } + """); + CreateFile( + "monster/slime.yaml", + """ + id: 1 + name: Slime + hp: 10 + faction: dungeon + """); + CreateFile( + "monster/goblin.yaml", + """ + id: 2 + name: Goblin + hp: 30 + faction: dungeon + """); + } + + /// + /// 在限定时间内等待异步任务完成,避免文件监听测试无限挂起。 + /// + /// 任务结果类型。 + /// 要等待的任务。 + /// 超时时间。 + /// 任务结果。 + 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; + } +} diff --git a/GFramework.Game/Config/GameConfigBootstrap.cs b/GFramework.Game/Config/GameConfigBootstrap.cs new file mode 100644 index 00000000..5ab37571 --- /dev/null +++ b/GFramework.Game/Config/GameConfigBootstrap.cs @@ -0,0 +1,183 @@ +using GFramework.Core.Abstractions.Events; +using GFramework.Game.Abstractions.Config; + +namespace GFramework.Game.Config; + +/// +/// 提供官方的 C# 配置启动帮助器。 +/// 该类型负责把配置注册表、YAML 加载器与开发期热重载句柄收敛到一个长生命周期对象中, +/// 让消费者项目可以通过一个稳定入口完成配置启动,而不是在多个脚本里重复拼装运行时细节。 +/// +public sealed class GameConfigBootstrap : IDisposable +{ + private const string ConfigureLoaderCannotBeNullMessage = "ConfigureLoader must be provided."; + private const string RootPathCannotBeNullOrWhiteSpaceMessage = "Root path cannot be null or whitespace."; + + private readonly GameConfigBootstrapOptions _options; + private IUnRegister? _hotReload; + private YamlConfigLoader? _loader; + private bool _disposed; + + /// + /// 使用指定选项创建配置启动帮助器。 + /// + /// 配置启动约定。 + /// 为空时抛出。 + /// + /// 当 为空, + /// 或 未提供时抛出。 + /// + public GameConfigBootstrap(GameConfigBootstrapOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + if (string.IsNullOrWhiteSpace(options.RootPath)) + { + throw new ArgumentException( + RootPathCannotBeNullOrWhiteSpaceMessage, + nameof(options.RootPath)); + } + + if (options.ConfigureLoader == null) + { + throw new ArgumentException( + ConfigureLoaderCannotBeNullMessage, + nameof(options.ConfigureLoader)); + } + + _options = options; + RootPath = options.RootPath; + Registry = options.Registry ?? new ConfigRegistry(); + } + + /// + /// 获取配置根目录。 + /// + public string RootPath { get; } + + /// + /// 获取当前配置生命周期共享的注册表。 + /// 默认情况下该实例由启动帮助器创建;如调用方传入自定义注册表,则返回同一个对象。 + /// + public IConfigRegistry Registry { get; } + + /// + /// 获取一个值,指示启动帮助器是否已经成功完成初次加载。 + /// + public bool IsInitialized => _loader != null; + + /// + /// 获取一个值,指示开发期热重载是否已启用。 + /// + public bool IsHotReloadEnabled => _hotReload != null; + + /// + /// 获取当前生效的 YAML 配置加载器。 + /// 只有在 成功返回后该属性才可访问。 + /// + /// 当当前实例已释放时抛出。 + /// 当启动帮助器尚未初始化成功时抛出。 + public YamlConfigLoader Loader + { + get + { + ThrowIfDisposed(); + + return _loader ?? throw new InvalidOperationException( + "The config bootstrap has not been initialized yet."); + } + } + + /// + /// 执行初次配置加载,并在需要时启动开发期热重载。 + /// 该方法只能成功调用一次,避免同一个生命周期对象在运行中被重新拼装为另一套加载约定。 + /// + /// 取消令牌。 + /// 表示异步初始化流程的任务。 + /// 当当前实例已释放时抛出。 + /// 当当前实例已经初始化成功时抛出。 + /// 当配置加载失败时抛出。 + public async Task InitializeAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + if (_loader != null) + { + throw new InvalidOperationException( + "The config bootstrap can only be initialized once per instance."); + } + + var loader = new YamlConfigLoader(RootPath); + _options.ConfigureLoader!(loader); + await loader.LoadAsync(Registry, cancellationToken); + + // 仅在初次加载完全成功后才公开加载器实例,避免上层观察到半初始化状态。 + _loader = loader; + + if (_options.EnableHotReload) + { + StartHotReload(_options.HotReloadOptions); + } + } + + /// + /// 启用开发期热重载。 + /// 该入口让调用方可以先完成一次确定性的初始加载,再按环境决定是否追加文件监听。 + /// + /// 热重载选项;为空时使用 的默认行为。 + /// 当当前实例已释放时抛出。 + /// + /// 当初始加载尚未完成,或热重载已经处于启用状态时抛出。 + /// + /// + /// 当 小于 + /// 时抛出。 + /// + public void StartHotReload(YamlConfigHotReloadOptions? options = null) + { + ThrowIfDisposed(); + + var loader = _loader ?? throw new InvalidOperationException( + "Hot reload can only be started after the initial config load succeeds."); + + if (_hotReload != null) + { + throw new InvalidOperationException("Hot reload is already enabled."); + } + + _hotReload = loader.EnableHotReload(Registry, options); + } + + /// + /// 停止开发期热重载并释放监听资源。 + /// 该方法是幂等的,允许启动层在销毁阶段无条件调用。 + /// + public void StopHotReload() + { + var hotReload = _hotReload; + _hotReload = null; + hotReload?.UnRegister(); + } + + /// + /// 停止热重载并释放当前帮助器持有的资源。 + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + StopHotReload(); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(GameConfigBootstrap)); + } + } +} diff --git a/GFramework.Game/Config/GameConfigBootstrapOptions.cs b/GFramework.Game/Config/GameConfigBootstrapOptions.cs new file mode 100644 index 00000000..bb517c14 --- /dev/null +++ b/GFramework.Game/Config/GameConfigBootstrapOptions.cs @@ -0,0 +1,41 @@ +using GFramework.Game.Abstractions.Config; + +namespace GFramework.Game.Config; + +/// +/// 描述官方配置启动帮助器的初始化约定。 +/// 该选项对象把配置根目录、表注册回调和热重载策略收敛到一个稳定入口, +/// 让消费项目不必在多个启动脚本里重复拼装加载器细节。 +/// +public sealed class GameConfigBootstrapOptions +{ + /// + /// 获取或设置配置根目录。 + /// 该路径会直接传给 作为 YAML 与 schema 的共同根目录。 + /// + public string RootPath { get; init; } = string.Empty; + + /// + /// 获取或设置用于配置 的回调。 + /// 调用方通常应在这里调用生成器产出的 RegisterAllGeneratedConfigTables(), + /// 或显式注册当前场景所需的手写表定义。 + /// + public Action? ConfigureLoader { get; init; } + + /// + /// 获取或设置要复用的配置注册表。 + /// 为空时启动帮助器会创建默认的 实例。 + /// + public IConfigRegistry? Registry { get; init; } + + /// + /// 获取或设置是否在初次加载成功后立即启用开发期热重载。 + /// + public bool EnableHotReload { get; init; } + + /// + /// 获取或设置初始化阶段启用热重载时使用的选项。 + /// 当 时,该值会被忽略。 + /// + public YamlConfigHotReloadOptions? HotReloadOptions { get; init; } +} diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md index d2f7a7f1..5409c4ad 100644 --- a/docs/zh-CN/game/config-system.md +++ b/docs/zh-CN/game/config-system.md @@ -142,7 +142,7 @@ GameProject/ 这段配置的作用: -- `GFramework.Game` 提供运行时 `YamlConfigLoader`、`ConfigRegistry` 和只读表实现 +- `GFramework.Game` 提供运行时 `YamlConfigLoader`、`ConfigRegistry`、`GameConfigBootstrap` 和只读表实现 - 三个 `ProjectReference(... OutputItemType="Analyzer")` 把生成器接进当前消费者项目 - `GeWuYou.GFramework.SourceGenerators.targets` 自动把 `schemas/**/*.schema.json` 加入 `AdditionalFiles` @@ -160,12 +160,46 @@ GameProject/ ``` -### 启动引导模板 +### 官方启动帮助器 -推荐把配置系统的初始化收敛到一个单独入口,避免把 `YamlConfigLoader` 注册逻辑散落到多个启动脚本中: +`GFramework.Game` 现在内置 `GameConfigBootstrap` 与 `GameConfigBootstrapOptions`,用于把 `ConfigRegistry`、`YamlConfigLoader`、`LoadAsync` 和热重载句柄收敛到一个正式的 C# 入口中。 + +推荐直接组合这个帮助器,而不是继续在消费者项目里复制文档模板: + +```csharp +using GFramework.Game.Abstractions.Config; +using GFramework.Game.Config; +using GFramework.Game.Config.Generated; + +var bootstrap = new GameConfigBootstrap( + new GameConfigBootstrapOptions + { + RootPath = configRootPath, + ConfigureLoader = static loader => loader.RegisterAllGeneratedConfigTables(), + EnableHotReload = true, + HotReloadOptions = new YamlConfigHotReloadOptions + { + OnTableReloaded = tableName => Console.WriteLine($"Reloaded config table: {tableName}"), + OnTableReloadFailed = static (_, exception) => + { + var diagnostic = (exception as ConfigLoadException)?.Diagnostic; + Console.WriteLine($"Config reload failed: {diagnostic?.FailureKind}"); + } + } + }); + +await bootstrap.InitializeAsync(); + +var registry = bootstrap.Registry; +var monsterTable = registry.GetMonsterTable(); +var slime = monsterTable.Get(1); + +bootstrap.Dispose(); +``` + +如果你希望把它继续包装进自己的进程级入口,也建议只包一层生命周期壳,而不是重新拼装底层加载器: ```csharp -using GFramework.Core.Abstractions.Events; using GFramework.Game.Abstractions.Config; using GFramework.Game.Config; using GFramework.Game.Config.Generated; @@ -173,58 +207,55 @@ using GFramework.Game.Config.Generated; namespace GameProject.Config; /// -/// 负责初始化游戏内容配置运行时入口。 +/// 封装当前游戏进程的配置启动生命周期。 /// -public sealed class GameConfigBootstrap : IDisposable +public sealed class GameConfigRuntime : IDisposable { - private readonly ConfigRegistry _registry = new(); - private IUnRegister? _hotReload; + private readonly GameConfigBootstrap _bootstrap; /// - /// 获取当前游戏进程共享的配置注册表。 - /// - public IConfigRegistry Registry => _registry; - - /// - /// 从指定配置根目录加载所有已注册配置表。 + /// 使用指定配置根目录创建运行时入口。 /// /// 配置根目录。 - /// 是否启用开发期热重载。 - public async Task InitializeAsync(string configRootPath, bool enableHotReload = false) + public GameConfigRuntime(string configRootPath) { - var loader = new YamlConfigLoader(configRootPath) - .RegisterAllGeneratedConfigTables(); - - await loader.LoadAsync(_registry); - - if (enableHotReload) - { - _hotReload = loader.EnableHotReload( - _registry, - onTableReloaded: tableName => Console.WriteLine($"Reloaded config table: {tableName}"), - onTableReloadFailed: static (_, exception) => - { - var diagnostic = (exception as ConfigLoadException)?.Diagnostic; - Console.WriteLine($"Config reload failed: {diagnostic?.FailureKind}"); - }); - } + _bootstrap = new GameConfigBootstrap( + new GameConfigBootstrapOptions + { + RootPath = configRootPath, + ConfigureLoader = static loader => loader.RegisterAllGeneratedConfigTables() + }); } /// - /// 停止开发期热重载并释放相关资源。 + /// 获取共享配置注册表。 + /// + public IConfigRegistry Registry => _bootstrap.Registry; + + /// + /// 执行初次配置加载。 + /// + public Task InitializeAsync() + { + return _bootstrap.InitializeAsync(); + } + + /// + /// 释放底层热重载句柄等资源。 /// public void Dispose() { - _hotReload?.UnRegister(); + _bootstrap.Dispose(); } } ``` -这段模板刻意遵循几个约定: +这个官方帮助器刻意遵循几个约定: -- 优先使用生成器产出的 `RegisterAllGeneratedConfigTables()`,把多表注册收敛为一个稳定入口 -- 由一个长生命周期对象持有 `ConfigRegistry` -- 热重载句柄和配置生命周期绑在一起,避免监听器泄漏 +- 优先通过 `ConfigureLoader` 调用生成器产出的 `RegisterAllGeneratedConfigTables()`,把多表注册收敛为一个稳定入口 +- 由 `GameConfigBootstrap` 持有 `ConfigRegistry`、`YamlConfigLoader` 和热重载句柄 +- `InitializeAsync()` 只在首次加载完整成功后才公开运行时状态,避免半初始化对象泄漏到业务层 +- 热重载既可以在初始化时自动启用,也可以在初次加载后显式调用 `StartHotReload(...)` ### 运行时读取模板 @@ -309,30 +340,38 @@ if (monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster)) ### Architecture 推荐接入模板 -如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,推荐把配置系统接到 `OnInitialize()` 阶段,并把 `ConfigRegistry` 注册为 utility: +如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,推荐把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility: ```csharp using GFramework.Core.Architectures; +using GFramework.Game.Abstractions.Config; using GFramework.Game.Config; using GFramework.Game.Config.Generated; public sealed class GameArchitecture : Architecture { - private readonly string _configRootPath; + private readonly GameConfigBootstrap _configBootstrap; public GameArchitecture(string configRootPath) { - _configRootPath = configRootPath ?? throw new ArgumentNullException(nameof(configRootPath)); + _configBootstrap = new GameConfigBootstrap( + new GameConfigBootstrapOptions + { + RootPath = configRootPath, + ConfigureLoader = static loader => loader.RegisterAllGeneratedConfigTables() + }); } protected override void OnInitialize() { - var registry = RegisterUtility(new ConfigRegistry()); + RegisterUtility(_configBootstrap.Registry); + _configBootstrap.InitializeAsync().GetAwaiter().GetResult(); + } - var loader = new YamlConfigLoader(_configRootPath) - .RegisterAllGeneratedConfigTables(); - - loader.LoadAsync(registry).GetAwaiter().GetResult(); + public override async ValueTask DestroyAsync() + { + _configBootstrap.Dispose(); + await base.DestroyAsync(); } } ``` @@ -340,35 +379,40 @@ public sealed class GameArchitecture : Architecture 初始化完成后,业务组件可以继续通过架构上下文读取 utility,再走生成的强类型入口: ```csharp -var registry = Context.GetUtility(); +var registry = Context.GetUtility(); var monsterTable = registry.GetMonsterTable(); var slime = monsterTable.Get(1); ``` 推荐遵循以下顺序: -- 先注册 `ConfigRegistry` -- 再构造并配置 `YamlConfigLoader` -- 在 `OnInitialize()` 内完成首次 `LoadAsync` +- 先构造 `GameConfigBootstrap` +- 在 `OnInitialize()` 里注册 `bootstrap.Registry` +- 再调用 `bootstrap.InitializeAsync()` 完成首次加载 +- 架构销毁时释放 `GameConfigBootstrap` - 初始化完成后只通过注册表和生成表包装访问配置 -当前阶段不建议为了配置系统额外引入新的 `IArchitectureModule` 或 service module 抽象;现有 `Architecture + ConfigRegistry + YamlConfigLoader + RegisterAllGeneratedConfigTables()` 组合已经足够作为官方推荐接入路径。 +当前阶段不建议为了配置系统额外引入新的 `IArchitectureModule` 或 service module 抽象;现有 `Architecture + GameConfigBootstrap + RegisterAllGeneratedConfigTables()` 组合已经足够作为官方推荐接入路径。 ### 热重载模板 -如果你希望把开发期热重载显式收敛为一个可选能力,建议把失败诊断一起写进模板,而不是只打印异常文本: +如果你希望把开发期热重载显式收敛为一个可选能力,推荐直接通过 `GameConfigBootstrap.StartHotReload(...)` 管理,而不是让监听句柄散落在启动层之外: ```csharp -var hotReload = loader.EnableHotReload( - registry, - onTableReloaded: tableName => Console.WriteLine($"Reloaded: {tableName}"), - onTableReloadFailed: (tableName, exception) => +await bootstrap.InitializeAsync(); + +bootstrap.StartHotReload( + new YamlConfigHotReloadOptions { - var diagnostic = (exception as ConfigLoadException)?.Diagnostic; - Console.WriteLine($"Reload failed: {tableName}"); - Console.WriteLine($"Failure kind: {diagnostic?.FailureKind}"); - Console.WriteLine($"Yaml path: {diagnostic?.YamlPath}"); - Console.WriteLine($"Display path: {diagnostic?.DisplayPath}"); + OnTableReloaded = tableName => Console.WriteLine($"Reloaded: {tableName}"), + OnTableReloadFailed = (tableName, exception) => + { + var diagnostic = (exception as ConfigLoadException)?.Diagnostic; + Console.WriteLine($"Reload failed: {tableName}"); + Console.WriteLine($"Failure kind: {diagnostic?.FailureKind}"); + Console.WriteLine($"Yaml path: {diagnostic?.YamlPath}"); + Console.WriteLine($"Display path: {diagnostic?.DisplayPath}"); + } }); ``` @@ -378,22 +422,7 @@ var hotReload = loader.EnableHotReload( - 热重载失败时应优先依赖 `ConfigLoadException.Diagnostic` 做稳定日志或 UI 提示 - 如果你的项目已经有统一日志系统,建议在这里把诊断字段转成结构化日志,而不是拼接一整段字符串 -如果你后续还需要为热重载增加更多开关,推荐优先使用选项对象入口,而不是继续叠加位置参数: - -```csharp -var hotReload = loader.EnableHotReload( - registry, - new YamlConfigHotReloadOptions - { - OnTableReloaded = tableName => Console.WriteLine($"Reloaded: {tableName}"), - OnTableReloadFailed = (tableName, exception) => - { - var diagnostic = (exception as ConfigLoadException)?.Diagnostic; - Console.WriteLine($"{tableName}: {diagnostic?.FailureKind}"); - }, - DebounceDelay = TimeSpan.FromMilliseconds(150) - }); -``` +如果你确实需要直接控制底层加载器,`YamlConfigLoader.EnableHotReload(...)` 仍然保留;但在一般启动路径下,优先让 `GameConfigBootstrap` 持有并停止监听句柄。 ## 运行时接入