From f2900502620388d39fe6748425a0ab1469ab60ae Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 9 Apr 2026 11:27:41 +0800
Subject: [PATCH 1/4] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E6=A8=A1=E5=9D=97=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?=
=?UTF-8?q?=E5=92=8C=E6=9E=B6=E6=9E=84=E6=A8=A1=E5=9D=97=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 ArchitectureConfigIntegrationTests 验证配置模块在架构场景下的接入链路
- 添加 GameConfigModule 作为 Architecture 官方配置模块接入入口
- 实现模块生命周期管理,统一完成注册表暴露和首次加载
- 添加配置模块防止重复安装到多个架构的保护机制
- 验证配置模块在其他 utility 初始化前完成首次加载的顺序
- 更新中文文档详细说明配置系统接入模板和架构集成方式
Conflicts:
docs/zh-CN/game/config-system.md
---
.../ArchitectureConfigIntegrationTests.cs | 206 +++++++++++++++---
GFramework.Game/Config/GameConfigModule.cs | 156 +++++++++++++
docs/zh-CN/game/config-system.md | 39 ++--
3 files changed, 356 insertions(+), 45 deletions(-)
create mode 100644 GFramework.Game/Config/GameConfigModule.cs
diff --git a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
index d8631e5d..7f7948a9 100644
--- a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
+++ b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
@@ -3,6 +3,8 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GFramework.Core.Architectures;
+using GFramework.Core.Extensions;
+using GFramework.Core.Utility;
using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
using GFramework.Game.Config.Generated;
@@ -11,17 +13,28 @@ using NUnit.Framework;
namespace GFramework.Game.Tests.Config;
///
-/// 验证在 初始化流程中可以通过官方配置启动帮助器加载生成配置表,并通过表访问器读取数据。
+/// 验证 场景下的官方配置模块接入链路。
+/// 这些测试覆盖模块安装、utility 初始化顺序以及生成表访问,确保模块化入口能够替代手写 bootstrap 模板。
///
[TestFixture]
public class ArchitectureConfigIntegrationTests
{
///
- /// 架构初始化期间,通过 收敛生成表注册、加载与注册表暴露,
+ /// 清理全局架构上下文,避免测试之间残留同类型架构绑定。
+ ///
+ [SetUp]
+ [TearDown]
+ public void ResetGlobalArchitectureContext()
+ {
+ GameContext.Clear();
+ }
+
+ ///
+ /// 架构初始化期间,通过 收敛生成表注册、加载与注册表暴露,
/// 并将 作为 utility 暴露给架构上下文读取。
///
[Test]
- public async Task ConfigLoaderCanRunDuringArchitectureInitialization()
+ public async Task ConfigModuleCanRunDuringArchitectureInitialization()
{
var rootPath = CreateTempConfigRoot();
ConsumerArchitecture? architecture = null;
@@ -45,6 +58,8 @@ public class ArchitectureConfigIntegrationTests
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.ConfigModule.IsInitialized, Is.True);
+ Assert.That(architecture.ConfigModule.IsHotReloadEnabled, Is.False);
});
}
finally
@@ -58,6 +73,84 @@ public class ArchitectureConfigIntegrationTests
}
}
+ ///
+ /// 验证配置模块会在其他 utility 初始化之前完成首次加载,
+ /// 这样依赖配置的 utility 无需再自行阻塞等待配置系统完成启动。
+ ///
+ [Test]
+ public async Task ConfigModuleShould_Load_Config_Before_Dependent_Utility_Initialization()
+ {
+ var rootPath = CreateTempConfigRoot();
+ ConsumerArchitecture? architecture = null;
+ var initialized = false;
+ try
+ {
+ architecture = new ConsumerArchitecture(rootPath);
+ await architecture.InitializeAsync();
+ initialized = true;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(architecture.ProbeUtility.InitializedWithLoadedConfig, Is.True);
+ Assert.That(architecture.ProbeUtility.ObservedMonsterName, Is.EqualTo("Slime"));
+ Assert.That(architecture.ProbeUtility.ObservedDungeonMonsterCount, Is.EqualTo(2));
+ });
+ }
+ finally
+ {
+ if (architecture is not null && initialized)
+ {
+ await architecture.DestroyAsync();
+ }
+
+ DeleteDirectoryIfExists(rootPath);
+ }
+ }
+
+ ///
+ /// 验证同一个模块实例不会被重复安装到多个架构中,
+ /// 避免共享内部 bootstrap 状态导致跨架构生命周期混淆。
+ ///
+ [Test]
+ public async Task GameConfigModuleShould_Reject_Reusing_The_Same_Module_Instance()
+ {
+ var rootPath = CreateTempConfigRoot();
+ ModuleOnlyArchitecture? firstArchitecture = null;
+ var firstDestroyed = false;
+ try
+ {
+ var module = CreateModule(rootPath);
+
+ firstArchitecture = new ModuleOnlyArchitecture(module);
+ await firstArchitecture.InitializeAsync();
+ var wasInitializedBeforeDestroy = module.IsInitialized;
+ await firstArchitecture.DestroyAsync();
+ firstDestroyed = true;
+ firstArchitecture = null;
+ GameContext.Clear();
+
+ var secondArchitecture = new ModuleOnlyArchitecture(module);
+ var exception =
+ Assert.ThrowsAsync(async () => await secondArchitecture.InitializeAsync());
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(wasInitializedBeforeDestroy, Is.True);
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Message, Does.Contain("cannot be installed more than once"));
+ });
+ }
+ finally
+ {
+ if (firstArchitecture is not null && !firstDestroyed)
+ {
+ await firstArchitecture.DestroyAsync();
+ }
+
+ DeleteDirectoryIfExists(rootPath);
+ }
+ }
+
private static string CreateTempConfigRoot()
{
var rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigArchitecture", Guid.NewGuid().ToString("N"));
@@ -94,6 +187,26 @@ public class ArchitectureConfigIntegrationTests
}
}
+ ///
+ /// 创建一个使用配置模块的架构实例。
+ ///
+ /// 测试配置根目录。
+ /// 已配置的模块实例。
+ private static GameConfigModule CreateModule(string configRoot)
+ {
+ return new GameConfigModule(
+ new GameConfigBootstrapOptions
+ {
+ RootPath = configRoot,
+ ConfigureLoader = static loader =>
+ loader.RegisterAllGeneratedConfigTables(
+ new GeneratedConfigRegistrationOptions
+ {
+ IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
+ })
+ });
+ }
+
private const string MonsterSchemaJson = @"{
""title"": ""Monster Config"",
""description"": ""Defines one monster entry for the generated consumer integration test."",
@@ -132,11 +245,8 @@ public class ArchitectureConfigIntegrationTests
private sealed class ConsumerArchitecture : Architecture
{
- private readonly GameConfigBootstrap _bootstrap;
-
- public IConfigRegistry Registry => _bootstrap.Registry;
-
- public MonsterTable MonsterTable { get; private set; } = null!;
+ private readonly GameConfigModule _configModule;
+ private readonly ConfigAwareProbeUtility _probeUtility = new();
public ConsumerArchitecture(string configRoot)
{
@@ -145,30 +255,76 @@ public class ArchitectureConfigIntegrationTests
throw new ArgumentNullException(nameof(configRoot));
}
- _bootstrap = new GameConfigBootstrap(
- new GameConfigBootstrapOptions
- {
- RootPath = configRoot,
- ConfigureLoader = static loader =>
- loader.RegisterAllGeneratedConfigTables(
- new GeneratedConfigRegistrationOptions
- {
- IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
- })
- });
+ _configModule = CreateModule(configRoot);
}
+ public GameConfigModule ConfigModule => _configModule;
+
+ public IConfigRegistry Registry => _configModule.Registry;
+
+ public MonsterTable MonsterTable => Registry.GetMonsterTable();
+
+ public ConfigAwareProbeUtility ProbeUtility => _probeUtility;
+
+ ///
+ /// 在用户初始化阶段安装配置模块,并注册一个依赖配置的测试 utility,
+ /// 以验证模块会在 utility 初始化前完成首次加载。
+ ///
protected override void OnInitialize()
{
- RegisterUtility(Registry);
- _bootstrap.InitializeAsync().GetAwaiter().GetResult();
- MonsterTable = Registry.GetMonsterTable();
+ InstallModule(_configModule);
+ RegisterUtility(_probeUtility);
}
+ }
- public override async ValueTask DestroyAsync()
+ ///
+ /// 用于验证模块复用限制的最小架构。
+ ///
+ private sealed class ModuleOnlyArchitecture(GameConfigModule configModule) : Architecture
+ {
+ public GameConfigModule ConfigModule => configModule;
+
+ ///
+ /// 安装外部传入的配置模块。
+ ///
+ protected override void OnInitialize()
{
- _bootstrap.Dispose();
- await base.DestroyAsync();
+ InstallModule(configModule);
+ }
+ }
+
+ ///
+ /// 在 utility 初始化阶段直接读取配置表的探针工具。
+ /// 如果模块没有在 utility 阶段开始前完成首次加载,这个探针会在初始化时失败。
+ ///
+ private sealed class ConfigAwareProbeUtility : AbstractContextUtility
+ {
+ ///
+ /// 获取一个值,指示初始化时是否已经读取到有效配置。
+ ///
+ public bool InitializedWithLoadedConfig { get; private set; }
+
+ ///
+ /// 获取初始化期间读取到的怪物名称。
+ ///
+ public string? ObservedMonsterName { get; private set; }
+
+ ///
+ /// 获取初始化期间读取到的 dungeon 阵营怪物数量。
+ ///
+ public int ObservedDungeonMonsterCount { get; private set; }
+
+ ///
+ /// 读取架构上下文中的配置注册表并验证目标表已经可用。
+ ///
+ protected override void OnInit()
+ {
+ var registry = this.GetUtility();
+ var monsterTable = registry.GetMonsterTable();
+
+ ObservedMonsterName = monsterTable.Get(1).Name;
+ ObservedDungeonMonsterCount = monsterTable.FindByFaction("dungeon").Count();
+ InitializedWithLoadedConfig = true;
}
}
}
diff --git a/GFramework.Game/Config/GameConfigModule.cs b/GFramework.Game/Config/GameConfigModule.cs
new file mode 100644
index 00000000..087102d3
--- /dev/null
+++ b/GFramework.Game/Config/GameConfigModule.cs
@@ -0,0 +1,156 @@
+using System;
+using System.Threading;
+using GFramework.Core.Abstractions.Architectures;
+using GFramework.Core.Abstractions.Enums;
+using GFramework.Core.Architectures;
+using GFramework.Core.Utility;
+using GFramework.Game.Abstractions.Config;
+
+namespace GFramework.Game.Config;
+
+///
+/// 提供基于 的官方配置模块接入入口。
+/// 该模块负责把 挂接到架构生命周期中,统一完成注册表暴露、
+/// 首次加载以及架构销毁时的资源回收。
+///
+///
+/// 使用该模块时,推荐在 Architecture.OnInitialize() 的较早位置调用 ,
+/// 以便其他 utility、model 和 system 在各自初始化阶段都能读取到已经完成首次加载的配置表。
+/// 如果消费项目不基于 ,则继续直接使用 更合适。
+///
+public sealed class GameConfigModule : IArchitectureModule
+{
+ private readonly GameConfigBootstrap _bootstrap;
+ private readonly ModuleBootstrapLifetimeUtility _lifetimeUtility;
+ private int _installState;
+
+ ///
+ /// 使用指定的启动选项创建配置模块。
+ ///
+ /// 配置启动帮助器选项。
+ /// 当 为空时抛出。
+ ///
+ /// 当 不满足 的构造约束时抛出。
+ ///
+ public GameConfigModule(GameConfigBootstrapOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ _bootstrap = new GameConfigBootstrap(options);
+ _lifetimeUtility = new ModuleBootstrapLifetimeUtility(_bootstrap);
+ }
+
+ ///
+ /// 获取当前模块复用的配置注册表。
+ /// 该实例会在模块安装时注册为架构 utility,供其他组件通过上下文直接读取。
+ ///
+ public IConfigRegistry Registry => _bootstrap.Registry;
+
+ ///
+ /// 获取一个值,指示模块绑定的配置启动器是否已经完成首次加载。
+ ///
+ public bool IsInitialized => _bootstrap.IsInitialized;
+
+ ///
+ /// 获取一个值,指示模块绑定的开发期热重载是否已启用。
+ ///
+ public bool IsHotReloadEnabled => _bootstrap.IsHotReloadEnabled;
+
+ ///
+ /// 获取当前生效的 YAML 配置加载器。
+ /// 只有在模块完成首次加载后该属性才可访问。
+ ///
+ /// 当模块所属架构已销毁时抛出。
+ /// 当首次加载尚未成功完成时抛出。
+ public YamlConfigLoader Loader => _bootstrap.Loader;
+
+ ///
+ /// 将配置模块安装到指定架构中。
+ /// 安装后会立即暴露 ,并注册一个生命周期钩子,
+ /// 以便在 utility 初始化之前完成一次确定性的配置加载。
+ ///
+ /// 目标架构实例。
+ /// 当 为空时抛出。
+ /// 当同一个模块实例被重复安装时抛出。
+ public void Install(IArchitecture architecture)
+ {
+ ArgumentNullException.ThrowIfNull(architecture);
+
+ if (Interlocked.Exchange(ref _installState, 1) != 0)
+ {
+ throw new InvalidOperationException(
+ "The same GameConfigModule instance cannot be installed more than once.");
+ }
+
+ architecture.RegisterUtility(Registry);
+ architecture.RegisterUtility(_lifetimeUtility);
+ architecture.RegisterLifecycleHook(new BootstrapInitializationHook(_bootstrap));
+ }
+
+ ///
+ /// 在首次加载成功后显式启用开发期热重载。
+ /// 该入口与 的语义保持一致,
+ /// 供已经保留模块实例引用的架构启动层按环境决定是否追加监听。
+ ///
+ /// 热重载选项;为空时使用默认行为。
+ public void StartHotReload(YamlConfigHotReloadOptions? options = null)
+ {
+ _bootstrap.StartHotReload(options);
+ }
+
+ ///
+ /// 停止开发期热重载并释放监听资源。
+ /// 该方法是幂等的,允许架构外部的开发期开关无条件调用。
+ ///
+ public void StopHotReload()
+ {
+ _bootstrap.StopHotReload();
+ }
+
+ ///
+ /// 在 utility 初始化之前完成首次配置加载的生命周期钩子。
+ /// 这样后续 utility、model 和 system 在各自初始化阶段就能直接依赖已加载的注册表。
+ ///
+ private sealed class BootstrapInitializationHook(GameConfigBootstrap bootstrap) : IArchitectureLifecycleHook
+ {
+ ///
+ /// 在目标阶段触发配置加载。
+ ///
+ /// 当前架构阶段。
+ /// 相关架构实例;当前实现不直接使用,但保留用于接口契约一致性。
+ public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
+ {
+ if (phase != ArchitecturePhase.BeforeUtilityInit)
+ {
+ return;
+ }
+
+ // 架构生命周期钩子当前是同步接口,因此这里显式桥接到统一的 bootstrap 异步实现,
+ // 让 Architecture 模式和独立运行时模式保持同一套加载、诊断和热重载启动语义。
+ bootstrap.InitializeAsync().GetAwaiter().GetResult();
+ }
+ }
+
+ ///
+ /// 跟随架构 utility 生命周期释放底层 bootstrap 资源的薄封装。
+ /// 该 utility 本身不承担加载职责,只负责在架构销毁时停止热重载并释放监听句柄。
+ ///
+ private sealed class ModuleBootstrapLifetimeUtility(GameConfigBootstrap bootstrap) : AbstractContextUtility
+ {
+ ///
+ /// 该 utility 不需要在初始化阶段执行额外逻辑。
+ /// 首次加载已经由 在 utility 初始化前完成。
+ ///
+ protected override void OnInit()
+ {
+ }
+
+ ///
+ /// 架构销毁时释放 bootstrap 资源,确保热重载监听句柄不会泄漏到架构生命周期之外。
+ ///
+ protected override void OnDestroy()
+ {
+ bootstrap.Dispose();
+ }
+ }
+}
diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md
index cf6b7eda..5561dccd 100644
--- a/docs/zh-CN/game/config-system.md
+++ b/docs/zh-CN/game/config-system.md
@@ -168,7 +168,7 @@ GameProject/
这段配置的作用:
-- `GFramework.Game` 提供运行时 `YamlConfigLoader`、`ConfigRegistry`、`GameConfigBootstrap` 和只读表实现
+- `GFramework.Game` 提供运行时 `YamlConfigLoader`、`ConfigRegistry`、`GameConfigBootstrap`、`GameConfigModule` 和只读表实现
- 三个 `ProjectReference(... OutputItemType="Analyzer")` 把生成器接进当前消费者项目
- `GeWuYou.GFramework.SourceGenerators.targets` 自动把 `schemas/**/*.schema.json` 加入 `AdditionalFiles`
@@ -404,7 +404,7 @@ if (monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster))
### Architecture 推荐接入模板
-如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,并且当前宿主没有提供更上层的异步启动入口,可以把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility:
+如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,推荐优先使用 `GameConfigModule`,而不是在 `OnInitialize()` 里手动拼装 `GameConfigBootstrap` 的注册、加载和销毁顺序:
```csharp
using GFramework.Core.Architectures;
@@ -414,11 +414,11 @@ using GFramework.Game.Config.Generated;
public sealed class GameArchitecture : Architecture
{
- private readonly GameConfigBootstrap _configBootstrap;
+ private readonly GameConfigModule _configModule;
public GameArchitecture(string configRootPath)
{
- _configBootstrap = new GameConfigBootstrap(
+ _configModule = new GameConfigModule(
new GameConfigBootstrapOptions
{
RootPath = configRootPath,
@@ -428,14 +428,7 @@ public sealed class GameArchitecture : Architecture
protected override void OnInitialize()
{
- RegisterUtility(_configBootstrap.Registry);
- _configBootstrap.InitializeAsync().GetAwaiter().GetResult();
- }
-
- public override async ValueTask DestroyAsync()
- {
- _configBootstrap.Dispose();
- await base.DestroyAsync();
+ InstallModule(_configModule);
}
}
```
@@ -456,22 +449,28 @@ var slime = monsterTable.Get(1);
推荐遵循以下顺序:
-- 先构造 `GameConfigBootstrap`
-- 在 `OnInitialize()` 里注册 `bootstrap.Registry`
-- 再调用 `bootstrap.InitializeAsync()` 完成首次加载
-- 架构销毁时释放 `GameConfigBootstrap`
+- 先构造 `GameConfigModule`
+- 在 `OnInitialize()` 的较早位置调用 `InstallModule(_configModule)`
+- 让模块在 `BeforeUtilityInit` 阶段完成首次加载
+- 架构销毁时让模块跟随 utility 生命周期自动释放 `GameConfigBootstrap`
- 初始化完成后只通过注册表和生成表包装访问配置
-当前阶段不建议为了配置系统额外引入新的 `IArchitectureModule` 或 service module 抽象;现有 `Architecture + GameConfigBootstrap + RegisterAllGeneratedConfigTables()` 组合已经足够作为官方推荐接入路径。
+这样做的收益是:
+
+- `IConfigRegistry` 会在模块安装时立即注册为 utility,后续组件统一从上下文读取
+- 首次加载发生在 `BeforeUtilityInit`,因此依赖配置的 utility、model 和 system 在自己的初始化阶段就能直接读取表
+- 架构销毁时不再需要手写 `Dispose()` 样板来停止热重载句柄
+
+如果你仍然需要在架构外直接控制 `InitializeAsync()`、`StartHotReload(...)` 或 `StopHotReload()` 的调用时机,继续直接使用 `GameConfigBootstrap` 仍然是合适的;`GameConfigModule` 是面向 `Architecture` 宿主的官方薄封装,而不是替代底层 bootstrap。
### 热重载模板
-如果你希望把开发期热重载显式收敛为一个可选能力,推荐直接通过 `GameConfigBootstrap.StartHotReload(...)` 管理,而不是让监听句柄散落在启动层之外:
+如果你希望把开发期热重载显式收敛为一个可选能力,在 `Architecture` 场景下可以直接保留上面示例中的 `_configModule` 字段并调用 `GameConfigModule.StartHotReload(...)`;非 `Architecture` 场景则继续直接通过 `GameConfigBootstrap.StartHotReload(...)` 管理,而不是让监听句柄散落在启动层之外:
```csharp
-await bootstrap.InitializeAsync();
+await architecture.InitializeAsync();
-bootstrap.StartHotReload(
+_configModule.StartHotReload(
new YamlConfigHotReloadOptions
{
OnTableReloaded = tableName => Console.WriteLine($"Reloaded: {tableName}"),
From 13c91c88691390289d7f225b59e32d2cb949169e Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 9 Apr 2026 14:48:05 +0800
Subject: [PATCH 2/4] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E6=A8=A1=E5=9D=97=E6=9E=B6=E6=9E=84=E9=9B=86=E6=88=90?=
=?UTF-8?q?=E4=B8=8EYAML=E5=8A=A0=E8=BD=BD=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现 GameConfigBootstrap 启动帮助器,统一管理配置注册表、YAML加载器与热重载句柄
- 创建 GameConfigModule 配置模块,集成到 Architecture 生命周期中完成自动加载与资源回收
- 实现 YamlConfigLoader 基于文件目录的YAML配置加载器,支持批量加载与热重载功能
- 添加 ArchitectureConfigIntegrationTests 集成测试,验证模块安装、加载顺序与表访问
- 实现热重载防抖机制,支持开发期配置变更监听与增量更新
- 提供同步上下文桥接支持,避免Unity主线程或UI线程上的死锁问题
---
.../ArchitectureConfigIntegrationTests.cs | 208 ++++++++++++++++--
GFramework.Game/Config/GameConfigBootstrap.cs | 8 +-
GFramework.Game/Config/GameConfigModule.cs | 53 ++++-
GFramework.Game/Config/YamlConfigLoader.cs | 21 +-
.../Config/YamlConfigSchemaValidator.cs | 2 +-
5 files changed, 262 insertions(+), 30 deletions(-)
diff --git a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
index 7f7948a9..7e94d6a3 100644
--- a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
+++ b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using GFramework.Core.Architectures;
using GFramework.Core.Extensions;
@@ -78,7 +79,7 @@ public class ArchitectureConfigIntegrationTests
/// 这样依赖配置的 utility 无需再自行阻塞等待配置系统完成启动。
///
[Test]
- public async Task ConfigModuleShould_Load_Config_Before_Dependent_Utility_Initialization()
+ public async Task ConfigModuleShouldLoadConfigBeforeDependentUtilityInitialization()
{
var rootPath = CreateTempConfigRoot();
ConsumerArchitecture? architecture = null;
@@ -112,7 +113,7 @@ public class ArchitectureConfigIntegrationTests
/// 避免共享内部 bootstrap 状态导致跨架构生命周期混淆。
///
[Test]
- public async Task GameConfigModuleShould_Reject_Reusing_The_Same_Module_Instance()
+ public async Task GameConfigModuleShouldRejectReusingTheSameModuleInstance()
{
var rootPath = CreateTempConfigRoot();
ModuleOnlyArchitecture? firstArchitecture = null;
@@ -151,6 +152,99 @@ public class ArchitectureConfigIntegrationTests
}
}
+ ///
+ /// 验证配置启动帮助器在同步阻塞且存在 的线程上仍可完成初始化,
+ /// 避免架构生命周期钩子的同步桥接因为 await 捕获上下文而死锁。
+ ///
+ [Test]
+ public void GameConfigBootstrapShouldSupportSynchronousBridgeOnBlockingSynchronizationContext()
+ {
+ var rootPath = CreateTempConfigRoot();
+ GameConfigBootstrap? bootstrap = null;
+ try
+ {
+ bootstrap = CreateBootstrap(rootPath);
+
+ RunBlockingOnSynchronizationContext(
+ () => bootstrap.InitializeAsync(),
+ TimeSpan.FromSeconds(5));
+
+ var monsterTable = bootstrap.Registry.GetMonsterTable();
+ Assert.Multiple(() =>
+ {
+ Assert.That(bootstrap.IsInitialized, Is.True);
+ Assert.That(monsterTable.Get(1).Name, Is.EqualTo("Slime"));
+ Assert.That(monsterTable.FindByFaction("dungeon").Count(), Is.EqualTo(2));
+ });
+ }
+ finally
+ {
+ bootstrap?.Dispose();
+ DeleteDirectoryIfExists(rootPath);
+ }
+ }
+
+ ///
+ /// 验证模块在架构已经越过安装窗口时会拒绝安装,
+ /// 并且失败不会消耗模块实例,便于后续在新的架构上重试安装。
+ ///
+ [Test]
+ public async Task GameConfigModuleShouldRejectLateInstallationWithoutConsumingTheModuleInstance()
+ {
+ var rootPath = CreateTempConfigRoot();
+ ReadyOnlyArchitecture? readyArchitecture = null;
+ ModuleOnlyArchitecture? retryArchitecture = null;
+ var readyArchitectureInitialized = false;
+ var retryArchitectureInitialized = false;
+ try
+ {
+ var module = CreateModule(rootPath);
+
+ readyArchitecture = new ReadyOnlyArchitecture();
+ await readyArchitecture.InitializeAsync();
+ readyArchitectureInitialized = true;
+
+ var exception = Assert.Throws(() => readyArchitecture.InstallModule(module));
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Message, Does.Contain("BeforeUtilityInit"));
+ Assert.That(readyArchitecture.Context.GetUtilities(), Is.Empty);
+ Assert.That(module.IsInitialized, Is.False);
+ });
+
+ await readyArchitecture.DestroyAsync();
+ readyArchitectureInitialized = false;
+ readyArchitecture = null;
+ GameContext.Clear();
+
+ retryArchitecture = new ModuleOnlyArchitecture(module);
+ await retryArchitecture.InitializeAsync();
+ retryArchitectureInitialized = true;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(module.IsInitialized, Is.True);
+ Assert.That(retryArchitecture.Registry.GetMonsterTable().Get(1).Name, Is.EqualTo("Slime"));
+ });
+ }
+ finally
+ {
+ if (retryArchitecture is not null && retryArchitectureInitialized)
+ {
+ await retryArchitecture.DestroyAsync();
+ }
+
+ if (readyArchitecture is not null && readyArchitectureInitialized)
+ {
+ await readyArchitecture.DestroyAsync();
+ }
+
+ DeleteDirectoryIfExists(rootPath);
+ }
+ }
+
private static string CreateTempConfigRoot()
{
var rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigArchitecture", Guid.NewGuid().ToString("N"));
@@ -188,23 +282,78 @@ public class ArchitectureConfigIntegrationTests
}
///
- /// 创建一个使用配置模块的架构实例。
+ /// 在不处理消息队列的同步上下文线程上执行阻塞等待,
+ /// 用于回归验证初始化异步链不会依赖原上下文恢复 continuation。
+ ///
+ /// 要同步阻塞执行的异步操作。
+ /// 等待线程结束的超时时间。
+ private static void RunBlockingOnSynchronizationContext(Func action, TimeSpan timeout)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ Exception? capturedException = null;
+ var workerThread = new Thread(() =>
+ {
+ SynchronizationContext.SetSynchronizationContext(new NonPumpingSynchronizationContext());
+
+ try
+ {
+ action().GetAwaiter().GetResult();
+ }
+ catch (Exception exception)
+ {
+ capturedException = exception;
+ }
+ })
+ {
+ IsBackground = true
+ };
+
+ workerThread.Start();
+ if (!workerThread.Join(timeout))
+ {
+ Assert.Fail("The blocking synchronization-context bridge did not complete within the expected timeout.");
+ }
+
+ if (capturedException != null)
+ {
+ throw new AssertionException(
+ $"The blocking synchronization-context bridge failed: {capturedException}");
+ }
+ }
+
+ private static GameConfigBootstrap CreateBootstrap(string configRoot)
+ {
+ return new GameConfigBootstrap(CreateBootstrapOptions(configRoot));
+ }
+
+ ///
+ /// 创建一个使用配置模块的模块实例。
///
/// 测试配置根目录。
/// 已配置的模块实例。
private static GameConfigModule CreateModule(string configRoot)
{
- return new GameConfigModule(
- new GameConfigBootstrapOptions
- {
- RootPath = configRoot,
- ConfigureLoader = static loader =>
- loader.RegisterAllGeneratedConfigTables(
- new GeneratedConfigRegistrationOptions
- {
- IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
- })
- });
+ return new GameConfigModule(CreateBootstrapOptions(configRoot));
+ }
+
+ ///
+ /// 创建供测试复用的配置启动选项。
+ ///
+ /// 测试配置根目录。
+ /// 可用于模块或直接 bootstrap 的启动选项。
+ private static GameConfigBootstrapOptions CreateBootstrapOptions(string configRoot)
+ {
+ return new GameConfigBootstrapOptions
+ {
+ RootPath = configRoot,
+ ConfigureLoader = static loader =>
+ loader.RegisterAllGeneratedConfigTables(
+ new GeneratedConfigRegistrationOptions
+ {
+ IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
+ })
+ };
}
private const string MonsterSchemaJson = @"{
@@ -284,6 +433,8 @@ public class ArchitectureConfigIntegrationTests
{
public GameConfigModule ConfigModule => configModule;
+ public IConfigRegistry Registry => configModule.Registry;
+
///
/// 安装外部传入的配置模块。
///
@@ -293,6 +444,19 @@ public class ArchitectureConfigIntegrationTests
}
}
+ ///
+ /// 仅用于把架构推进到 Ready 阶段的空壳架构。
+ ///
+ private sealed class ReadyOnlyArchitecture : Architecture
+ {
+ ///
+ /// 该测试架构不注册任何组件,仅验证模块的安装窗口约束。
+ ///
+ protected override void OnInitialize()
+ {
+ }
+ }
+
///
/// 在 utility 初始化阶段直接读取配置表的探针工具。
/// 如果模块没有在 utility 阶段开始前完成首次加载,这个探针会在初始化时失败。
@@ -327,4 +491,20 @@ public class ArchitectureConfigIntegrationTests
InitializedWithLoadedConfig = true;
}
}
+
+ ///
+ /// 模拟一个不会主动处理 回调的阻塞线程上下文。
+ /// 如果初始化链错误地捕获该上下文,continuation 会永久悬挂,从而暴露同步桥接死锁。
+ ///
+ private sealed class NonPumpingSynchronizationContext : SynchronizationContext
+ {
+ ///
+ /// 丢弃异步投递的 continuation,模拟被同步阻塞且未泵消息的宿主线程。
+ ///
+ /// 要执行的回调。
+ /// 回调状态。
+ public override void Post(SendOrPostCallback d, object? state)
+ {
+ }
+ }
}
diff --git a/GFramework.Game/Config/GameConfigBootstrap.cs b/GFramework.Game/Config/GameConfigBootstrap.cs
index 242002be..9ffe34ec 100644
--- a/GFramework.Game/Config/GameConfigBootstrap.cs
+++ b/GFramework.Game/Config/GameConfigBootstrap.cs
@@ -1,3 +1,4 @@
+using System.Threading;
using GFramework.Core.Abstractions.Events;
using GFramework.Game.Abstractions.Config;
@@ -127,6 +128,11 @@ public sealed class GameConfigBootstrap : IDisposable
/// 该方法只能成功调用一次,避免同一个生命周期对象在运行中被重新拼装为另一套加载约定。
///
/// 取消令牌。
+ ///
+ /// 该入口会被 的同步生命周期钩子桥接调用,
+ /// 因此内部所有异步等待都必须使用 ConfigureAwait(false),避免在 Unity 主线程、
+ /// UI Dispatcher 或带 的测试线程上发生同步阻塞死锁。
+ ///
/// 表示异步初始化流程的任务。
/// 当当前实例已释放时抛出。
/// 当当前实例已经初始化成功时抛出。
@@ -152,7 +158,7 @@ public sealed class GameConfigBootstrap : IDisposable
{
var loader = new YamlConfigLoader(RootPath);
_options.ConfigureLoader!(loader);
- await loader.LoadAsync(Registry, cancellationToken);
+ await loader.LoadAsync(Registry, cancellationToken).ConfigureAwait(false);
if (_options.EnableHotReload)
{
diff --git a/GFramework.Game/Config/GameConfigModule.cs b/GFramework.Game/Config/GameConfigModule.cs
index 087102d3..4b726877 100644
--- a/GFramework.Game/Config/GameConfigModule.cs
+++ b/GFramework.Game/Config/GameConfigModule.cs
@@ -20,6 +20,10 @@ namespace GFramework.Game.Config;
///
public sealed class GameConfigModule : IArchitectureModule
{
+ private const int InstallStateNotInstalled = 0;
+ private const int InstallStateInstalling = 1;
+ private const int InstallStateInstalled = 2;
+
private readonly GameConfigBootstrap _bootstrap;
private readonly ModuleBootstrapLifetimeUtility _lifetimeUtility;
private int _installState;
@@ -76,15 +80,31 @@ public sealed class GameConfigModule : IArchitectureModule
{
ArgumentNullException.ThrowIfNull(architecture);
- if (Interlocked.Exchange(ref _installState, 1) != 0)
+ ValidateInstallationPhase(architecture);
+
+ if (Interlocked.CompareExchange(
+ ref _installState,
+ InstallStateInstalling,
+ InstallStateNotInstalled) != InstallStateNotInstalled)
{
throw new InvalidOperationException(
"The same GameConfigModule instance cannot be installed more than once.");
}
- architecture.RegisterUtility(Registry);
- architecture.RegisterUtility(_lifetimeUtility);
- architecture.RegisterLifecycleHook(new BootstrapInitializationHook(_bootstrap));
+ try
+ {
+ // 先注册生命周期钩子,确保任何“已错过 BeforeUtilityInit”的安装都会在暴露注册表之前失败,
+ // 避免架构看到永远不会完成首次加载的半安装配置入口。
+ architecture.RegisterLifecycleHook(new BootstrapInitializationHook(_bootstrap));
+ architecture.RegisterUtility(Registry);
+ architecture.RegisterUtility(_lifetimeUtility);
+ Volatile.Write(ref _installState, InstallStateInstalled);
+ }
+ catch
+ {
+ Volatile.Write(ref _installState, InstallStateNotInstalled);
+ throw;
+ }
}
///
@@ -127,7 +147,30 @@ public sealed class GameConfigModule : IArchitectureModule
// 架构生命周期钩子当前是同步接口,因此这里显式桥接到统一的 bootstrap 异步实现,
// 让 Architecture 模式和独立运行时模式保持同一套加载、诊断和热重载启动语义。
- bootstrap.InitializeAsync().GetAwaiter().GetResult();
+ bootstrap.InitializeAsync().ConfigureAwait(false).GetAwaiter().GetResult();
+ }
+ }
+
+ ///
+ /// 验证模块仍处于允许接入的生命周期窗口。
+ /// 该模块依赖 钩子完成首次加载,
+ /// 因此一旦架构已经离开 ,继续安装只会错过首载时机。
+ ///
+ /// 目标架构实例。
+ ///
+ /// 当目标架构已经开始组件初始化阶段时抛出。
+ ///
+ private static void ValidateInstallationPhase(IArchitecture architecture)
+ {
+ if (architecture is not Architecture concreteArchitecture)
+ {
+ return;
+ }
+
+ if (concreteArchitecture.CurrentPhase != ArchitecturePhase.None)
+ {
+ throw new InvalidOperationException(
+ "GameConfigModule must be installed before the architecture enters BeforeUtilityInit.");
}
}
diff --git a/GFramework.Game/Config/YamlConfigLoader.cs b/GFramework.Game/Config/YamlConfigLoader.cs
index 012caa32..15e38b1b 100644
--- a/GFramework.Game/Config/YamlConfigLoader.cs
+++ b/GFramework.Game/Config/YamlConfigLoader.cs
@@ -69,7 +69,8 @@ public sealed class YamlConfigLoader : IConfigLoader
foreach (var registration in _registrations)
{
cancellationToken.ThrowIfCancellationRequested();
- loadedTables.Add(await registration.LoadAsync(_rootPath, _deserializer, cancellationToken));
+ loadedTables.Add(
+ await registration.LoadAsync(_rootPath, _deserializer, cancellationToken).ConfigureAwait(false));
}
CrossTableReferenceValidator.Validate(registry, loadedTables);
@@ -442,7 +443,8 @@ public sealed class YamlConfigLoader : IConfigLoader
if (!string.IsNullOrEmpty(SchemaRelativePath))
{
var schemaPath = Path.Combine(rootPath, SchemaRelativePath);
- schema = await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken);
+ schema = await YamlConfigSchemaValidator.LoadAsync(Name, schemaPath, cancellationToken)
+ .ConfigureAwait(false);
referencedTableNames = schema.ReferencedTableNames;
}
@@ -463,7 +465,7 @@ public sealed class YamlConfigLoader : IConfigLoader
string yaml;
try
{
- yaml = await File.ReadAllTextAsync(file, cancellationToken);
+ yaml = await File.ReadAllTextAsync(file, cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
@@ -987,8 +989,8 @@ public sealed class YamlConfigLoader : IConfigLoader
{
try
{
- await Task.Delay(_debounceDelay, reloadTokenSource.Token);
- await ReloadTableAsync(tableName, reloadTokenSource.Token);
+ await Task.Delay(_debounceDelay, reloadTokenSource.Token).ConfigureAwait(false);
+ await ReloadTableAsync(tableName, reloadTokenSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (reloadTokenSource.IsCancellationRequested)
{
@@ -1018,7 +1020,7 @@ public sealed class YamlConfigLoader : IConfigLoader
}
var reloadLock = _reloadLocks[tableName];
- await reloadLock.WaitAsync(cancellationToken);
+ await reloadLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
@@ -1031,8 +1033,9 @@ public sealed class YamlConfigLoader : IConfigLoader
foreach (var affectedTableName in affectedTableNames)
{
cancellationToken.ThrowIfCancellationRequested();
- loadedTables.Add(await _registrations[affectedTableName].LoadAsync(_rootPath, _deserializer,
- cancellationToken));
+ loadedTables.Add(
+ await _registrations[affectedTableName].LoadAsync(_rootPath, _deserializer, cancellationToken)
+ .ConfigureAwait(false));
}
CrossTableReferenceValidator.Validate(_registry, loadedTables);
@@ -1125,4 +1128,4 @@ public sealed class YamlConfigLoader : IConfigLoader
}
}
}
-}
\ No newline at end of file
+}
diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.cs
index f435c10a..381f321c 100644
--- a/GFramework.Game/Config/YamlConfigSchemaValidator.cs
+++ b/GFramework.Game/Config/YamlConfigSchemaValidator.cs
@@ -51,7 +51,7 @@ internal static class YamlConfigSchemaValidator
string schemaText;
try
{
- schemaText = await File.ReadAllTextAsync(schemaPath, cancellationToken);
+ schemaText = await File.ReadAllTextAsync(schemaPath, cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
From eb7a8c702cde461ca60790a0728137e042436e2b Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 9 Apr 2026 15:15:10 +0800
Subject: [PATCH 3/4] =?UTF-8?q?docs(tests):=20=E4=B8=BA=E9=85=8D=E7=BD=AE?=
=?UTF-8?q?=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=E6=B7=BB=E5=8A=A0=E8=AF=A6?=
=?UTF-8?q?=E7=BB=86=E7=9A=84XML=E6=96=87=E6=A1=A3=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 为ConsumerArchitecture构造函数添加参数验证和功能说明文档
- 为ConfigModule属性添加配置模块获取方法的详细说明
- 为Registry属性添加配置注册表访问的生命周期状态说明
- 为MonsterTable属性添加怪物配置表读取的异常处理和使用约束
- 为ProbeUtility属性添加配置可见性探针的功能说明
- 为ModuleOnlyArchitecture类添加模块共享测试的相关文档
- 为配置模块和注册表属性添加生命周期状态和使用时机说明
---
.../ArchitectureConfigIntegrationTests.cs | 49 +++++++++++++++++++
1 file changed, 49 insertions(+)
diff --git a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
index 7e94d6a3..49e99af9 100644
--- a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
+++ b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
@@ -397,6 +397,11 @@ public class ArchitectureConfigIntegrationTests
private readonly GameConfigModule _configModule;
private readonly ConfigAwareProbeUtility _probeUtility = new();
+ ///
+ /// 使用指定配置根目录创建一个消费者测试架构。
+ ///
+ /// 测试配置根目录。
+ /// 当 为空时抛出。
public ConsumerArchitecture(string configRoot)
{
if (configRoot == null)
@@ -407,12 +412,43 @@ public class ArchitectureConfigIntegrationTests
_configModule = CreateModule(configRoot);
}
+ ///
+ /// 获取当前架构安装的配置模块。
+ ///
+ ///
+ /// 该模块会在 中安装,并在架构进入
+ /// 时完成首次加载。
+ /// 调用方可通过该属性观察模块初始化状态,但不应在架构初始化完成前假定加载已经成功。
+ ///
public GameConfigModule ConfigModule => _configModule;
+ ///
+ /// 获取由配置模块暴露到架构上下文中的注册表。
+ ///
+ ///
+ /// 该属性在模块安装后即可访问同一个注册表实例,但只有在模块首次加载完成后,
+ /// 其中的生成配置表读取才具备成功契约。
+ ///
public IConfigRegistry Registry => _configModule.Registry;
+ ///
+ /// 获取测试使用的怪物配置表。
+ ///
+ /// 已经从 中解析出的怪物表包装。
+ /// 当模块首次加载尚未完成时抛出。
+ ///
+ /// 该属性用于断言生成访问器在架构初始化完成后可直接读取;
+ /// 它依赖模块在 utility 初始化阶段之前已经完成首次加载。
+ ///
public MonsterTable MonsterTable => Registry.GetMonsterTable();
+ ///
+ /// 获取用于观测 utility 初始化阶段配置可见性的探针 utility。
+ ///
+ ///
+ /// 该 utility 会在 中注册,并在 utility 初始化阶段读取配置表,
+ /// 用于验证配置模块是否按约定在更早的生命周期阶段完成首载。
+ ///
public ConfigAwareProbeUtility ProbeUtility => _probeUtility;
///
@@ -431,8 +467,21 @@ public class ArchitectureConfigIntegrationTests
///
private sealed class ModuleOnlyArchitecture(GameConfigModule configModule) : Architecture
{
+ ///
+ /// 获取安装到当前测试架构中的配置模块。
+ ///
+ ///
+ /// 该属性直接暴露传入的模块实例,便于测试验证同一模块实例跨架构复用时的生命周期约束。
+ ///
public GameConfigModule ConfigModule => configModule;
+ ///
+ /// 获取当前模块共享的配置注册表。
+ ///
+ ///
+ /// 该注册表实例在模块安装后即与架构绑定,但只有在架构完成配置首载后,
+ /// 其中的强类型配置表访问才应被视为可用。
+ ///
public IConfigRegistry Registry => configModule.Registry;
///
From febf9480770b78ec86c1e4d46ae6f53ecda4f1ce Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 9 Apr 2026 16:33:02 +0800
Subject: [PATCH 4/4] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=E6=9E=B6?=
=?UTF-8?q?=E6=9E=84=E9=85=8D=E7=BD=AE=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?=
=?UTF-8?q?=E5=92=8C=E6=A8=A1=E5=9D=97=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现了 ArchitectureConfigIntegrationTests 测试类,验证配置模块在架构场景下的完整链路
- 添加了 GameConfigModule 类,提供基于 Architecture 的配置模块接入入口
- 实现了配置模块的生命周期管理,包括首次加载和热重载支持
- 集成了 BootstrapInitializationHook 确保配置在 utility 初始化前完成加载
- 添加了模块复用限制和安装窗口验证机制
- 实现了架构销毁时的资源清理和生命周期钩子注册
---
.../ArchitectureConfigIntegrationTests.cs | 37 +++++++++----------
GFramework.Game/Config/GameConfigModule.cs | 19 +++++++---
2 files changed, 30 insertions(+), 26 deletions(-)
diff --git a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
index 49e99af9..f9a2492f 100644
--- a/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
+++ b/GFramework.Game.Tests/Config/ArchitectureConfigIntegrationTests.cs
@@ -1,15 +1,11 @@
-using System;
using System.IO;
-using System.Linq;
using System.Threading;
-using System.Threading.Tasks;
using GFramework.Core.Architectures;
using GFramework.Core.Extensions;
using GFramework.Core.Utility;
using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
using GFramework.Game.Config.Generated;
-using NUnit.Framework;
namespace GFramework.Game.Tests.Config;
@@ -153,33 +149,39 @@ public class ArchitectureConfigIntegrationTests
}
///
- /// 验证配置启动帮助器在同步阻塞且存在 的线程上仍可完成初始化,
- /// 避免架构生命周期钩子的同步桥接因为 await 捕获上下文而死锁。
+ /// 验证配置模块在同步阻塞且存在 的线程上仍可完成架构初始化,
+ /// 直接覆盖 通过生命周期钩子执行同步桥接的真实路径。
///
[Test]
- public void GameConfigBootstrapShouldSupportSynchronousBridgeOnBlockingSynchronizationContext()
+ public void GameConfigModuleShouldSupportSynchronousBridgeOnBlockingSynchronizationContext()
{
var rootPath = CreateTempConfigRoot();
- GameConfigBootstrap? bootstrap = null;
+ ConsumerArchitecture? architecture = null;
+ var initialized = false;
try
{
- bootstrap = CreateBootstrap(rootPath);
+ architecture = new ConsumerArchitecture(rootPath);
RunBlockingOnSynchronizationContext(
- () => bootstrap.InitializeAsync(),
+ () => architecture.InitializeAsync(),
TimeSpan.FromSeconds(5));
+ initialized = true;
- var monsterTable = bootstrap.Registry.GetMonsterTable();
+ var monsterTable = architecture.Registry.GetMonsterTable();
Assert.Multiple(() =>
{
- Assert.That(bootstrap.IsInitialized, Is.True);
+ Assert.That(architecture.ConfigModule.IsInitialized, Is.True);
Assert.That(monsterTable.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(monsterTable.FindByFaction("dungeon").Count(), Is.EqualTo(2));
});
}
finally
{
- bootstrap?.Dispose();
+ if (architecture is not null && initialized)
+ {
+ architecture.DestroyAsync().GetAwaiter().GetResult();
+ }
+
DeleteDirectoryIfExists(rootPath);
}
}
@@ -322,14 +324,9 @@ public class ArchitectureConfigIntegrationTests
}
}
- private static GameConfigBootstrap CreateBootstrap(string configRoot)
- {
- return new GameConfigBootstrap(CreateBootstrapOptions(configRoot));
- }
-
///
- /// 创建一个使用配置模块的模块实例。
- ///
+ /// 创建一个使用配置模块的模块实例。
+ ///
/// 测试配置根目录。
/// 已配置的模块实例。
private static GameConfigModule CreateModule(string configRoot)
diff --git a/GFramework.Game/Config/GameConfigModule.cs b/GFramework.Game/Config/GameConfigModule.cs
index 4b726877..1f964e04 100644
--- a/GFramework.Game/Config/GameConfigModule.cs
+++ b/GFramework.Game/Config/GameConfigModule.cs
@@ -22,7 +22,7 @@ public sealed class GameConfigModule : IArchitectureModule
{
private const int InstallStateNotInstalled = 0;
private const int InstallStateInstalling = 1;
- private const int InstallStateInstalled = 2;
+ private const int InstallStateConsumed = 2;
private readonly GameConfigBootstrap _bootstrap;
private readonly ModuleBootstrapLifetimeUtility _lifetimeUtility;
@@ -76,6 +76,11 @@ public sealed class GameConfigModule : IArchitectureModule
/// 目标架构实例。
/// 当 为空时抛出。
/// 当同一个模块实例被重复安装时抛出。
+ ///
+ /// 生命周期阶段校验会在任何注册动作前执行,因此错过安装窗口的调用不会消耗当前模块实例。
+ /// 一旦开始向架构注册 utility 或生命周期钩子,就不存在回滚 API;因此后续任何失败都会把该模块实例视为已消耗,
+ /// 调用方必须创建新的 再重试。
+ ///
public void Install(IArchitecture architecture)
{
ArgumentNullException.ThrowIfNull(architecture);
@@ -93,16 +98,18 @@ public sealed class GameConfigModule : IArchitectureModule
try
{
- // 先注册生命周期钩子,确保任何“已错过 BeforeUtilityInit”的安装都会在暴露注册表之前失败,
- // 避免架构看到永远不会完成首次加载的半安装配置入口。
- architecture.RegisterLifecycleHook(new BootstrapInitializationHook(_bootstrap));
+ // 阶段窗口已经在前面做过无副作用校验,因此这里优先注册 utility,
+ // 让常见的容器/上下文接线失败在不可回滚的 hook 注册之前暴露出来。
architecture.RegisterUtility(Registry);
architecture.RegisterUtility(_lifetimeUtility);
- Volatile.Write(ref _installState, InstallStateInstalled);
+ architecture.RegisterLifecycleHook(new BootstrapInitializationHook(_bootstrap));
+ Volatile.Write(ref _installState, InstallStateConsumed);
}
catch
{
- Volatile.Write(ref _installState, InstallStateNotInstalled);
+ // 架构对 utility / hook 注册都不提供回滚入口,因此一旦进入注册阶段,
+ // 即使安装失败也必须禁止复用同一个模块实例,避免重复暴露共享注册表或挂上第二个 hook。
+ Volatile.Write(ref _installState, InstallStateConsumed);
throw;
}
}