Compare commits

..

37 Commits

Author SHA1 Message Date
gewuyou
f0064e31aa
Merge pull request #187 from GeWuYou/feat/data-repository-persistence
docs(data): 添加数据与存档系统文档并实现数据仓库功能
2026-04-06 13:01:31 +08:00
GeWuYou
60526a8a98 docs(game): 添加数据与存档系统文档和持久化测试
- 新增数据与存档系统完整文档,包含核心概念、基本用法和高级功能
- 实现文件存储、槽位存档仓库和统一设置仓库的持久化行为测试
- 覆盖存档迁移、备份恢复、批量操作等关键功能的测试用例
- 添加数据版本控制和自动备份机制的详细说明
- 提供完整的 API 接口文档和使用示例代码
2026-04-06 12:52:06 +08:00
gewuyou
77f67c8a9c
Merge pull request #186 from GeWuYou/feat/add-game-content-config-with-source-generator
docs(config): 添加游戏内容配置系统文档和集成测试
2026-04-06 12:40:32 +08:00
GeWuYou
7fad6772e0 feat(generator): 添加配置架构集成测试和JSON模式配置生成器
- 添加 ArchitectureConfigIntegrationTests 测试验证配置注册表初始化功能
- 实现 SchemaConfigGenerator 源代码生成器解析JSON schema文件
- 生成配置类型类、配置表包装器和运行时绑定辅助代码
- 支持嵌套对象、对象数组、标量数组的类型安全访问
- 提供强类型的配置表查询方法如 FindByProperty 等
- 实现跨表引用元数据生成和验证机制
2026-04-06 12:25:11 +08:00
GeWuYou
7114a76377 docs(data): 添加数据与存档系统文档并实现数据仓库功能
- 新增数据与存档系统详细文档,涵盖核心概念和使用方法
- 实现 DataRepository 类提供统一数据持久化接口
- 添加 DataRepositoryOptions 配置选项支持备份和事件功能
- 实现完整的数据仓库测试用例验证持久化行为
- 支持多槽位存档管理和版本化数据迁移功能
- 提供批量数据操作和事件通知机制
- 实现自动备份功能防止数据丢失
- 支持聚合设置仓库统一管理多个配置项
2026-04-06 12:21:28 +08:00
GeWuYou
2b30e859e9 docs(config): 添加游戏内容配置系统文档和集成测试
- 新增游戏内容配置系统完整文档,包含 YAML 配置、JSON Schema 结构描述
- 添加推荐目录结构和配置示例,支持怪物、物品、技能等静态内容管理
- 实现 Source Generator 自动生成配置类型、表包装和注册访问辅助功能
- 集成 VS Code 插件提供配置浏览、raw 编辑、schema 打开和校验功能
- 添加生成查询辅助,为顶层标量字段生成 FindBy* 与 TryFindFirstBy* 方法
- 实现开发期热重载功能,支持配置文件修改后自动刷新运行时表
- 添加跨表引用校验,支持 x-gframework-ref-table 声明的引用关系检查
- 新增集成测试验证生成器自动拾取 schema 并支持强类型访问入口
- 添加 IsExternalInit 类型支持低版本 .NET 框架的 init-only setter 功能
2026-04-06 11:42:34 +08:00
gewuyou
fb3bf49a12
Merge pull request #185 from GeWuYou/feat/game-save-data-system
feat(game): 添加数据与存档系统核心功能
2026-04-06 11:39:17 +08:00
GeWuYou
25e4965817 feat(game): 添加存档仓库实现和持久化测试
- 实现基于槽位的存档仓库功能,支持存档的保存、加载、删除和列举操作
- 添加存档版本迁移机制,支持自动升级旧版本存档数据
- 实现文件存储和统一设置仓库的基础持久化功能
- 添加完整的单元测试覆盖存档仓库的各种使用场景
- 创建测试工具类和测试数据模型用于验证持久化行为
- 添加项目AI代理行为规范文档,确保代码质量和一致性
2026-04-06 11:33:35 +08:00
GeWuYou
7ad80f54d3 feat(game): 添加数据与存档系统核心功能
- 实现 ISaveRepository<T> 接口提供存档管理功能
- 添加 SaveRepository<T> 实现类支持槽位存档管理
- 实现数据版本迁移机制支持存档版本升级
- 添加完整的存档测试用例验证功能正确性
- 创建数据与存档系统中文文档说明使用方法
- 移除项目中不再需要的本地计划文件夹配置
2026-04-06 11:09:53 +08:00
gewuyou
397611d47c
Merge pull request #184 from GeWuYou/docs/godot-architecture-and-patterns
docs(godot): add architecture integration and design pattern docs
2026-04-06 10:19:13 +08:00
GeWuYou
aab0995f49 docs(godot): add architecture integration and design pattern docs
- Add documentation for Godot architecture integration, including AbstractArchitecture,
  ArchitectureAnchor, and IGodotModule
- Describe basic usage: architecture creation, initialization, and anchor usage
- Provide advanced examples: module system, lifecycle hooks, and hot-reload support
- Document ContextAware usage for accessing architecture within nodes
- Include best practices for multi-architecture setups and common pitfalls
- Add architecture design patterns guide (MVC, MVVM, Command, etc.)
- Cover event-driven, DI, and service locator patterns with examples

Note:
- Normalize line endings to LF to fix inconsistent diffs caused by CRLF/LF mismatch
- No functional code changes
2026-04-06 10:12:59 +08:00
gewuyou
c62893d0c2
Merge pull request #183 from GeWuYou/chore/add-editorconfig-gitattributes
chore(config): 添加编辑器配置和 Git 属性设置
2026-04-06 09:47:02 +08:00
GeWuYou
5a1232274e chore(config): 添加编辑器配置和 Git 属性设置
- 配置 .editorconfig 文件统一代码风格
- 设置 UTF-8 编码和 LF 行尾
- 为解决方案文件和批处理脚本配置 CRLF 行尾
- 配置 .gitattributes 统一文本文件行尾规范化
- 设置二进制文件不进行行尾转换
- 指定各类源码文件使用 LF 行尾
2026-04-06 09:41:42 +08:00
gewuyou
e513ecc284
Merge pull request #174 from GeWuYou/feat/architecture-base-and-bootstrapper
Feat/architecture base and bootstrapper
2026-04-06 08:44:14 +08:00
GeWuYou
e3ea364b29 docs(core): 添加架构详解和核心框架文档
- 新增 Architecture 架构详解文档,涵盖设计目标、核心组件、生命周期管理
- 添加 GFramework.Core 核心框架文档,包含五层架构、快速开始、组件联动等内容
- 新增架构组件激活器实现,支持类型注册路径的实例创建能力
- 添加架构销毁器,统一处理可销毁对象的登记与释放
- 实现架构生命周期管理器,负责阶段转换和组件初始化销毁
2026-04-06 08:33:56 +08:00
gewuyou
6f17e2c437
Merge pull request #175 from GeWuYou/feat/ai-first-config-system
Feat/ai first config system
2026-04-06 07:59:48 +08:00
GeWuYou
7da35c00b2 feat(config): 添加基于文件目录的YAML配置加载器
- 实现YamlConfigLoader类,支持从文件目录加载YAML配置
- 提供RegisterTable方法注册配置表定义,支持schema校验
- 添加LoadAsync异步加载功能,支持批量加载配置表
- 实现EnableHotReload方法,支持开发期配置热重载
- 添加跨表引用校验功能,确保配置依赖关系正确性
- 支持YAML文件和YML文件格式,自动识别文件扩展名
- 提供配置表主键提取器和比较器自定义功能
- 实现文件变更监听和防抖机制,避免频繁重载
- 支持配置目录和schema文件路径的灵活配置
- 提供详细的加载异常信息和诊断支持
2026-04-06 07:53:46 +08:00
GeWuYou
a416e093ee feat(config): 添加基于YAML的配置加载器实现
- 实现YamlConfigLoader类,支持从文件目录加载YAML配置
- 提供RegisterTable方法支持配置表定义注册
- 实现热重载功能,监听文件变更并自动重新加载
- 支持schema校验,拒绝未知字段和类型错误
- 实现跨表引用校验,确保配置一致性
- 添加YamlConfigTableRegistrationOptions选项类
- 支持防抖机制避免频繁重载
- 提供详细的错误诊断信息
2026-04-06 07:37:59 +08:00
gewuyou
e67cfd4808
Merge pull request #176 from GeWuYou/feat/coroutine-core-and-godot-integration
Feat/coroutine core and godot integration
2026-04-06 07:23:05 +08:00
GeWuYou
d21370787b test(godot): 添加Godot测试项目并配置CI流水线
- 在CI工作流中添加GFramework.Godot.Tests项目的测试执行
- 配置dotnet test命令运行Godot测试套件
- 添加TRX格式的日志输出和测试结果目录设置
- 更新Godot模块的全局引用配置
- 在Timing.cs中添加必要的命名空间引用
- 在GodotTimeSourceTests.cs中添加NUnit框架引用
2026-04-06 00:37:32 +08:00
GeWuYou
2053451185 feat(coroutine): 添加协程调度器和相关功能实现
- 实现了 CoroutineScheduler 类,支持协程的运行、暂停、恢复和终止管理
- 添加了协程槽位管理机制 CoroutineSlot,用于跟踪单个协程状态
- 实现了协程的优先级、标签和分组功能,支持批量操作
- 集成了等待指令系统,包括 WaitForSecondsRealtime、WaitForFixedUpdate 等
- 添加了协程统计功能和异常处理机制
- 实现了 Godot 平台的时间源适配器 GodotTimeSource
- 创建了协程调度器的高级功能测试用例
- 添加了 Timing 节点用于在 Godot 引擎中管理协程生命周期
2026-04-06 00:33:37 +08:00
GeWuYou
6cac882fb4 refactor(coroutine): 简化协程调度器中的执行阶段访问
- 移除私有字段 _executionStage,直接使用构造函数参数 executionStage
- 更新 ExecutionStage 属性实现,直接返回构造函数参数
- 修改协程元数据设置时使用参数而非私有字段
- 调整等待指令判断逻辑,直接比较参数值
2026-04-05 15:21:51 +08:00
GeWuYou
ccffb121b3 fix(coroutine): 修复协程完成状态处理中的异常情况
- 为 CoroutineCompletionStatus 枚举添加默认分支处理
- 抛出 ArgumentOutOfRangeException 以处理不支持的协程完成状态
- 防止因未知状态值导致的运行时错误
- 提高协程调度器的健壮性和错误处理能力
2026-04-05 15:18:29 +08:00
GeWuYou
03346fbfe7 docs(coroutine): 更新协程系统文档
- 重构 Core 协程系统文档,优化概述和核心概念说明
- 新增 Godot 协程系统集成文档
- 添加协程系统使用教程
- 更新等待指令说明,包括时间、条件、Task 和事件等待
- 补充协程控制、快照查询和生命周期管理相关内容
- 修正代码示例和 API 使用说明
2026-04-05 15:06:53 +08:00
GeWuYou
1c41c57d72 feat(coroutine): 添加协程系统核心组件与Godot集成
- 实现CoroutineMetadata类存储协程元数据信息
- 创建CoroutineScheduler协程调度器管理协程生命周期
- 添加CoroutineSlot类管理单个协程执行状态
- 实现GodotTimeSource时间源支持缩放和真实时间
- 添加Timing类提供Godot协程管理功能
- 实现CoroutineNodeExtensions扩展方法支持节点生命周期管理
- 支持协程分组、标签、优先级等功能
- 提供协程暂停、恢复、终止等控制接口
- 实现协程统计和快照功能
- 添加等待指令处理机制支持多种等待类型
2026-04-05 15:06:35 +08:00
GeWuYou
34a333a0c1 fix(generator): 解决重复引用路径生成成员名称不稳定问题
- 使用 TryGetValue 方法替代 ContainsKey 检查避免多次访问字典
- 重用跟踪的重复计数确保重复引用路径保持生成的成员名称稳定
- 修复重复计数递增逻辑确保正确的序号生成
- 简化重复计数器的使用方式提高代码可读性
- 移除文件末尾多余空行保持代码整洁
2026-04-05 13:44:31 +08:00
GeWuYou
8c9fbb39b2 feat(config): 添加YAML配置加载器依赖注入支持
- 引入GFramework.Core.Abstractions.Events命名空间
- 添加YamlDotNet反序列化库相关引用
- 注册配置抽象层依赖到YAML配置加载器
- 更新测试文件中的命名空间引用以匹配新架构
2026-04-05 13:34:50 +08:00
GeWuYou
4ad89b10b2 chore(dependencies): 添加 Microsoft.Extensions.DependencyInjection 引用
- 在 ArchitectureComponentRegistry.cs 中添加 Microsoft.Extensions.DependencyInjection 引用
- 在 ArchitectureComponentRegistryBehaviorTests.cs 中添加 Microsoft.Extensions.DependencyInjection 引用
2026-04-05 13:28:32 +08:00
GeWuYou
8f6c453c1a refactor(architectures): 添加依赖注入服务引用
- 在 Architecture.cs 中引入 Microsoft.Extensions.DependencyInjection 命名空间
- 在 ArchitectureComponentActivator.cs 中引入 Microsoft.Extensions.DependencyInjection 命名空间
- 为架构组件激活器增加依赖注入支持
- 统一架构层的服务注册机制
2026-04-05 13:18:42 +08:00
GeWuYou
f7e05d19cc docs(core): 添加核心架构与CQRS模式详细文档
- 新增Architecture架构详解文档,包含设计目标、生命周期管理和组件注册等内容
- 新增CQRS与Mediator模式文档,详细介绍命令查询职责分离的实现方式
- 新增核心框架概述文档,提供完整的架构图和快速入门指南
- 完善架构组件联动说明,涵盖初始化、命令执行和事件传播流程
- 补充最佳实践指导,明确分层职责和通信方式选择原则
2026-04-05 11:02:00 +08:00
GeWuYou
bb2b3a0c60 refactor(arch): 重构架构组件注册机制
- 将组件注册逻辑提取到独立的 ArchitectureComponentRegistry 类中
- 实现 ArchitectureComponentActivator 来支持注册阶段实例化组件
- 更新系统和模型注册文档说明,明确在注册阶段创建实例并参与初始化
- 修复类型注册在架构准备就绪后的行为一致性问题
- 添加完整的组件注册行为测试覆盖实例注册、工厂注册和生命周期管理
- 优化依赖注入解析机制,支持构造函数依赖和单例缓存
2026-04-05 11:01:50 +08:00
GeWuYou
01dc1523a5 feat(architecture): 添加架构核心组件和依赖注入容器实现
- 实现 Architecture 基类,提供系统、模型、工具等组件的注册与管理功能
- 添加 ArchitectureComponentRegistry 组件注册管理器,负责管理系统、模型、工具的注册
- 实现 ArchitectureLifecycle 生命周期管理器,管理架构阶段转换和组件初始化
- 添加 ArchitectureModules 模块管理器,负责架构模块安装和中介行为注册
- 实现 MicrosoftDiContainer 依赖注入容器,包装 Microsoft.Extensions.DependencyInjection
- 提供完整的组件生命周期管理,支持同步和异步初始化模式
- 添加架构阶段管理和组件注册验证机制
2026-04-05 10:52:21 +08:00
GeWuYou
16fae83f70 feat(architectures): 添加架构基类和初始化编排功能
- 实现 Architecture 基类,提供系统、模型、工具等组件的注册与管理功能
- 添加 ArchitectureBootstrapper 协调器,负责初始化期间的基础设施准备工作
- 实现生命周期管理、组件注册管理和模块管理功能
- 提供同步和异步初始化方法,支持不同场景下的架构初始化需求
- 添加架构上下文绑定和 IOC 容器冻结功能,确保运行时依赖图稳定
- 实现架构生命周期钩子注册和阶段变更事件功能
- 添加完整的单元测试验证初始化编排流程的正确性
2026-04-05 10:03:48 +08:00
GeWuYou
7fda40de42 feat(game): 添加游戏内容配置系统实现
- 实现基于 YAML 的配置加载器支持
- 添加 JSON Schema 结构验证功能
- 实现一对象一文件的目录组织方式
- 提供运行时只读查询接口
- 添加 Source Generator 生成配置类型和表包装
- 实现 VS Code 插件配置浏览和编辑功能
- 添加开发期热重载支持
- 实现跨表引用校验机制
- 提供完整的配置系统文档说明
2026-04-03 22:58:05 +08:00
GeWuYou
ecf2309e11 docs(game): 添加游戏内容配置系统文档
- 介绍面向静态游戏内容的 AI-First 配表方案
- 说明配置系统管理怪物、物品、技能、任务等静态内容数据
- 描述 YAML 作为配置源文件和 JSON Schema 作为结构描述的支持
- 展示推荐的目录结构和 Schema 示例
- 提供完整的接入模板包括 csproj 配置、启动引导和运行时读取
- 详述运行时校验行为和跨表引用机制
- 说明开发期热重载功能和 VS Code 工具集成
- 列出当前限制和独立 Config Studio 评估结论
2026-04-03 22:01:10 +08:00
GeWuYou
ec4e2edeab feat(config): 添加AI-First游戏内容配置系统
- 实现YAML配置文件与JSON Schema结构描述支持
- 提供一对象一文件的目录组织方式
- 集成Source Generator生成配置类型和表包装代码
- 添加VS Code插件支持配置浏览和表单编辑功能
- 实现运行时只读查询和开发期热重载机制
- 支持跨表引用校验和轻量元数据复用
- 添加配置加载异常诊断和批量编辑入口
2026-04-03 21:17:39 +08:00
GeWuYou
61cc7eaa6d feat(config): 添加AI-First配置系统及文档
- 引入YAML配置源文件支持
- 实现JSON Schema结构描述功能
- 提供一对象一文件的目录组织方式
- 添加运行时只读查询能力
- 实现Source Generator生成配置类型和表包装
- 集成VS Code插件提供配置浏览和编辑功能
- 添加开发期热重载支持
- 提供跨表引用校验机制
- 创建配置生成器约定和绑定辅助类
- 添加详细的中文文档说明
- 实现集成测试验证生成器功能
2026-04-03 19:10:23 +08:00
94 changed files with 28214 additions and 22237 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
[*.sln]
end_of_line = crlf
[*.bat]
end_of_line = crlf
[*.cmd]
end_of_line = crlf
[*.ps1]
end_of_line = crlf

35
.gitattributes vendored Normal file
View File

@ -0,0 +1,35 @@
# Keep repository text normalized to LF unless a file format is known to require CRLF.
* text=auto eol=lf
# Solution and Windows-native scripts are more interoperable when they keep CRLF in the working tree.
*.sln text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Source, config, scripts, and documentation stay LF across WSL and Windows editors.
*.sh text eol=lf
*.cs text eol=lf
*.csproj text eol=lf
*.props text eol=lf
*.targets text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.md text eol=lf
*.ts text eol=lf
*.js text eol=lf
*.mts text eol=lf
*.vue text eol=lf
*.css text eol=lf
# Common binary assets should never be line-normalized.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.zip binary
*.dll binary
*.so binary
*.pdb binary

View File

@ -156,6 +156,11 @@ jobs:
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \ --logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
--results-directory TestResults & --results-directory TestResults &
dotnet test GFramework.Godot.Tests \
-c Release \
--no-build \
--logger "trx;LogFileName=godot-$RANDOM.trx" \
--results-directory TestResults &
# 等待所有后台测试完成 # 等待所有后台测试完成
wait wait

View File

@ -193,6 +193,14 @@ bash scripts/validate-csharp-naming.sh
- If a framework abstraction changes meaning or intended usage, update the explanatory comments in code as part of the - If a framework abstraction changes meaning or intended usage, update the explanatory comments in code as part of the
same change. same change.
### Task Tracking
- When working from a tracked implementation plan, contributors MUST update the corresponding tracking document under
`local-plan/todos/` in the same change.
- Tracking updates MUST reflect completed work, newly discovered issues, validation results, and the next recommended
recovery point.
- Completing code changes without updating the active tracking document is considered incomplete work.
### Repository Documentation ### Repository Documentation
- Update the relevant `README.md` or `docs/` page when behavior, setup steps, architecture guidance, or user-facing - Update the relevant `README.md` or `docs/` page when behavior, setup steps, architecture guidance, or user-facing

View File

@ -74,7 +74,9 @@ public interface IArchitecture : IAsyncInitializable, IAsyncDestroyable, IInitia
/// <summary> /// <summary>
/// 注册中介行为管道 /// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑 /// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 既支持实现 <c>IPipelineBehavior&lt;,&gt;</c> 的开放泛型行为类型,
/// 也支持绑定到单一请求/响应对的封闭行为类型。
/// </summary> /// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam> /// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
void RegisterMediatorBehavior<TBehavior>() void RegisterMediatorBehavior<TBehavior>()

View File

@ -0,0 +1,28 @@
namespace GFramework.Core.Abstractions.Coroutine;
/// <summary>
/// 表示协程的最终完成结果。
/// </summary>
public enum CoroutineCompletionStatus
{
/// <summary>
/// 调度器无法确认该句柄的最终结果。
/// 这通常意味着句柄无效,或者句柄对应的历史结果已经不可用。
/// </summary>
Unknown,
/// <summary>
/// 协程自然执行结束。
/// </summary>
Completed,
/// <summary>
/// 协程被外部终止、清空或取消令牌中断。
/// </summary>
Cancelled,
/// <summary>
/// 协程在推进过程中抛出了异常。
/// </summary>
Faulted
}

View File

@ -0,0 +1,29 @@
namespace GFramework.Core.Abstractions.Coroutine;
/// <summary>
/// 表示协程调度器当前所处的执行阶段。
/// </summary>
/// <remarks>
/// 某些等待指令具有阶段语义,例如 <c>WaitForFixedUpdate</c> 和 <c>WaitForEndOfFrame</c>。
/// 宿主应为这些语义提供匹配的调度器阶段,否则这类等待不会自然完成。
/// </remarks>
public enum CoroutineExecutionStage
{
/// <summary>
/// 默认更新阶段。
/// 普通时间等待、下一帧等待以及大多数条件等待都会在该阶段推进。
/// </summary>
Update,
/// <summary>
/// 固定更新阶段。
/// 仅与固定步相关的等待指令会在该阶段完成。
/// </summary>
FixedUpdate,
/// <summary>
/// 帧结束阶段。
/// 仅与帧尾或延迟执行相关的等待指令会在该阶段完成。
/// </summary>
EndOfFrame
}

View File

@ -1,20 +1,20 @@
// IsExternalInit.cs // IsExternalInit.cs
// This type is required to support init-only setters and record types // This type is required to support init-only setters and record types
// when targeting netstandard2.0 or older frameworks. // when targeting netstandard2.0 or older frameworks.
#if !NET5_0_OR_GREATER #if !NET5_0_OR_GREATER
using System.ComponentModel; using System.ComponentModel;
// ReSharper disable CheckNamespace // ReSharper disable CheckNamespace
namespace System.Runtime.CompilerServices; namespace System.Runtime.CompilerServices;
/// <summary> /// <summary>
/// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。 /// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。
/// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。 /// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit internal static class IsExternalInit
{ {
} }
#endif #endif

View File

@ -0,0 +1,717 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 验证 Architecture 通过 <c>ArchitectureComponentRegistry</c> 暴露出的组件注册行为。
/// 这些测试覆盖实例注册、工厂注册、上下文注入、生命周期初始化和 Ready 后注册约束,
/// 用于保护组件注册器在继续重构后的既有契约。
/// </summary>
[TestFixture]
public class ArchitectureComponentRegistryBehaviorTests
{
/// <summary>
/// 初始化日志工厂和全局上下文状态。
/// </summary>
[SetUp]
public void SetUp()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
GameContext.Clear();
}
/// <summary>
/// 清理测试过程中绑定到全局表的架构上下文。
/// </summary>
[TearDown]
public void TearDown()
{
GameContext.Clear();
}
/// <summary>
/// 验证系统实例注册会注入上下文并参与生命周期初始化。
/// </summary>
[Test]
public async Task RegisterSystem_Instance_Should_Set_Context_And_Initialize_System()
{
var system = new TrackingSystem();
var architecture = new RegistryTestArchitecture(target => target.RegisterSystem(system));
await architecture.InitializeAsync();
Assert.Multiple(() =>
{
Assert.That(system.GetContext(), Is.SameAs(architecture.Context));
Assert.That(system.InitializeCallCount, Is.EqualTo(1));
Assert.That(architecture.Context.GetSystem<TrackingSystem>(), Is.SameAs(system));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证模型实例注册会注入上下文并参与生命周期初始化。
/// </summary>
[Test]
public async Task RegisterModel_Instance_Should_Set_Context_And_Initialize_Model()
{
var model = new TrackingModel();
var architecture = new RegistryTestArchitecture(target => target.RegisterModel(model));
await architecture.InitializeAsync();
Assert.Multiple(() =>
{
Assert.That(model.GetContext(), Is.SameAs(architecture.Context));
Assert.That(model.InitializeCallCount, Is.EqualTo(1));
Assert.That(architecture.Context.GetModel<TrackingModel>(), Is.SameAs(model));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证上下文工具注册会注入上下文并参与生命周期初始化。
/// </summary>
[Test]
public async Task RegisterUtility_Instance_Should_Set_Context_For_ContextUtility()
{
var utility = new TrackingContextUtility();
var architecture = new RegistryTestArchitecture(target => target.RegisterUtility(utility));
await architecture.InitializeAsync();
Assert.Multiple(() =>
{
Assert.That(utility.GetContext(), Is.SameAs(architecture.Context));
Assert.That(utility.InitializeCallCount, Is.EqualTo(1));
Assert.That(architecture.Context.GetUtility<TrackingContextUtility>(), Is.SameAs(utility));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证普通工具的工厂注册会在首次解析时创建单例并执行创建回调。
/// </summary>
[Test]
public async Task RegisterUtility_Type_Should_Create_Singleton_And_Invoke_Callback()
{
FactoryCreatedUtility? callbackInstance = null;
var architecture = new RegistryTestArchitecture(target =>
target.RegisterUtility<FactoryCreatedUtility>(created => callbackInstance = created));
await architecture.InitializeAsync();
var first = architecture.Context.GetUtility<FactoryCreatedUtility>();
var second = architecture.Context.GetUtility<FactoryCreatedUtility>();
Assert.Multiple(() =>
{
Assert.That(callbackInstance, Is.SameAs(first));
Assert.That(second, Is.SameAs(first));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证系统类型注册会在初始化期间物化实例、注入构造函数依赖并执行创建回调。
/// </summary>
[Test]
public async Task RegisterSystem_Type_Should_Create_Instance_During_Initialization()
{
var dependency = new ConstructorDependency("system-dependency");
FactoryCreatedSystem? callbackInstance = null;
var architecture = new RegistryTestArchitecture(
target => target.RegisterSystem<FactoryCreatedSystem>(created => callbackInstance = created),
services => services.AddSingleton(dependency));
await architecture.InitializeAsync();
var resolved = architecture.Context.GetSystem<FactoryCreatedSystem>();
Assert.Multiple(() =>
{
Assert.That(callbackInstance, Is.Not.Null);
Assert.That(resolved, Is.SameAs(callbackInstance));
Assert.That(resolved.Dependency, Is.SameAs(dependency));
Assert.That(resolved.GetContext(), Is.SameAs(architecture.Context));
Assert.That(resolved.InitializeCallCount, Is.EqualTo(1));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证模型类型注册会在初始化期间物化实例、注入构造函数依赖并执行创建回调。
/// </summary>
[Test]
public async Task RegisterModel_Type_Should_Create_Instance_During_Initialization()
{
var dependency = new ConstructorDependency("model-dependency");
FactoryCreatedModel? callbackInstance = null;
var architecture = new RegistryTestArchitecture(
target => target.RegisterModel<FactoryCreatedModel>(created => callbackInstance = created),
services => services.AddSingleton(dependency));
await architecture.InitializeAsync();
var resolved = architecture.Context.GetModel<FactoryCreatedModel>();
Assert.Multiple(() =>
{
Assert.That(callbackInstance, Is.Not.Null);
Assert.That(resolved, Is.SameAs(callbackInstance));
Assert.That(resolved.Dependency, Is.SameAs(dependency));
Assert.That(resolved.GetContext(), Is.SameAs(architecture.Context));
Assert.That(resolved.InitializeCallCount, Is.EqualTo(1));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证预冻结阶段通过实现类型注册的单例依赖会在同一轮组件激活中复用同一个实例。
/// 该回归测试用于保护 <see cref="ArchitectureComponentActivator" /> 的共享单例缓存,避免系统和模型分别创建重复单例。
/// </summary>
[Test]
public async Task RegisterSystem_And_Model_Type_Should_Reuse_ImplementationType_Singleton_During_Activation()
{
var counter = new DependencyConstructionCounter();
var architecture = new RegistryTestArchitecture(
target =>
{
target.RegisterSystem<ImplementationTypeDependencySystem>();
target.RegisterModel<ImplementationTypeDependencyModel>();
},
services =>
{
services.AddSingleton(counter);
services.AddSingleton<ImplementationTypeSharedDependency>();
});
await architecture.InitializeAsync();
var system = architecture.Context.GetSystem<ImplementationTypeDependencySystem>();
var model = architecture.Context.GetModel<ImplementationTypeDependencyModel>();
Assert.Multiple(() =>
{
Assert.That(counter.CreationCount, Is.EqualTo(1));
Assert.That(system.Dependency, Is.SameAs(model.Dependency));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证预冻结阶段通过工厂注册的单例依赖会在同一轮组件激活中复用同一个实例。
/// 该回归测试覆盖 <c>ImplementationFactory</c> 描述符路径,避免用户工厂在初始化时被重复调用。
/// </summary>
[Test]
public async Task RegisterSystem_And_Model_Type_Should_Reuse_Factory_Singleton_During_Activation()
{
var creationCount = 0;
var architecture = new RegistryTestArchitecture(
target =>
{
target.RegisterSystem<FactoryDependencySystem>();
target.RegisterModel<FactoryDependencyModel>();
},
services =>
{
services.AddSingleton(_ =>
{
creationCount++;
return new FactorySharedDependency();
});
});
await architecture.InitializeAsync();
var system = architecture.Context.GetSystem<FactoryDependencySystem>();
var model = architecture.Context.GetModel<FactoryDependencyModel>();
Assert.Multiple(() =>
{
Assert.That(creationCount, Is.EqualTo(1));
Assert.That(system.Dependency, Is.SameAs(model.Dependency));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证 Ready 阶段后不允许继续注册 Utility保持与系统和模型一致的约束。
/// </summary>
[Test]
public async Task RegisterUtility_After_Ready_Should_Throw_InvalidOperationException()
{
var architecture = new RegistryTestArchitecture(_ => { });
await architecture.InitializeAsync();
Assert.That(
() => architecture.RegisterUtility(new TrackingContextUtility()),
Throws.InvalidOperationException.With.Message.EqualTo(
"Cannot register utility after Architecture is Ready"));
await architecture.DestroyAsync();
}
/// <summary>
/// 用于测试组件注册行为的最小架构实现。
/// </summary>
private sealed class RegistryTestArchitecture : Architecture
{
private readonly Action<IServiceCollection>? _configurator;
private readonly Action<RegistryTestArchitecture> _registrationAction;
/// <summary>
/// 创建一个可选地附带服务配置逻辑的测试架构。
/// </summary>
/// <param name="registrationAction">初始化阶段执行的组件注册逻辑。</param>
/// <param name="configurator">初始化前执行的服务配置逻辑。</param>
public RegistryTestArchitecture(
Action<RegistryTestArchitecture> registrationAction,
Action<IServiceCollection>? configurator = null)
{
_registrationAction = registrationAction;
_configurator = configurator;
}
/// <summary>
/// 返回测试注入的服务配置逻辑。
/// </summary>
public override Action<IServiceCollection>? Configurator => _configurator;
/// <summary>
/// 在初始化阶段执行测试注入的注册逻辑。
/// </summary>
protected override void OnInitialize()
{
_registrationAction(this);
}
}
/// <summary>
/// 记录初始化与上下文注入情况的测试系统。
/// </summary>
private sealed class TrackingSystem : ISystem
{
private IArchitectureContext _context = null!;
/// <summary>
/// 获取系统初始化调用次数。
/// </summary>
public int InitializeCallCount { get; private set; }
/// <summary>
/// 记录初始化调用。
/// </summary>
public void Initialize()
{
InitializeCallCount++;
}
/// <summary>
/// 该测试系统不关心阶段变更。
/// </summary>
/// <param name="phase">当前架构阶段。</param>
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
/// <summary>
/// 存储注入的架构上下文。
/// </summary>
/// <param name="context">架构上下文。</param>
public void SetContext(IArchitectureContext context)
{
_context = context;
}
/// <summary>
/// 返回当前持有的架构上下文。
/// </summary>
public IArchitectureContext GetContext()
{
return _context;
}
/// <summary>
/// 该测试系统没有额外销毁逻辑。
/// </summary>
public void Destroy()
{
}
}
/// <summary>
/// 记录初始化与上下文注入情况的测试模型。
/// </summary>
private sealed class TrackingModel : IModel
{
private IArchitectureContext _context = null!;
/// <summary>
/// 获取模型初始化调用次数。
/// </summary>
public int InitializeCallCount { get; private set; }
/// <summary>
/// 记录初始化调用。
/// </summary>
public void Initialize()
{
InitializeCallCount++;
}
/// <summary>
/// 该测试模型不关心阶段变更。
/// </summary>
/// <param name="phase">当前架构阶段。</param>
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
/// <summary>
/// 存储注入的架构上下文。
/// </summary>
/// <param name="context">架构上下文。</param>
public void SetContext(IArchitectureContext context)
{
_context = context;
}
/// <summary>
/// 返回当前持有的架构上下文。
/// </summary>
public IArchitectureContext GetContext()
{
return _context;
}
}
/// <summary>
/// 记录初始化与上下文注入情况的测试上下文工具。
/// </summary>
private sealed class TrackingContextUtility : IContextUtility
{
private IArchitectureContext _context = null!;
/// <summary>
/// 获取工具初始化调用次数。
/// </summary>
public int InitializeCallCount { get; private set; }
/// <summary>
/// 记录初始化调用。
/// </summary>
public void Initialize()
{
InitializeCallCount++;
}
/// <summary>
/// 存储注入的架构上下文。
/// </summary>
/// <param name="context">架构上下文。</param>
public void SetContext(IArchitectureContext context)
{
_context = context;
}
/// <summary>
/// 返回当前持有的架构上下文。
/// </summary>
public IArchitectureContext GetContext()
{
return _context;
}
/// <summary>
/// 该测试工具没有额外销毁逻辑。
/// </summary>
public void Destroy()
{
}
}
/// <summary>
/// 用于验证普通工厂注册路径的简单工具。
/// </summary>
private sealed class FactoryCreatedUtility : IUtility
{
}
/// <summary>
/// 用于验证构造函数依赖注入的简单依赖对象。
/// </summary>
private sealed class ConstructorDependency(string name)
{
/// <summary>
/// 获取依赖对象名称。
/// </summary>
public string Name { get; } = name;
}
/// <summary>
/// 用于验证系统类型注册路径的工厂创建系统。
/// </summary>
private sealed class FactoryCreatedSystem(ConstructorDependency dependency) : ISystem
{
private IArchitectureContext _context = null!;
/// <summary>
/// 获取构造函数注入的依赖对象。
/// </summary>
public ConstructorDependency Dependency { get; } = dependency;
/// <summary>
/// 获取初始化调用次数。
/// </summary>
public int InitializeCallCount { get; private set; }
public void Initialize()
{
InitializeCallCount++;
}
public void Destroy()
{
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
/// <summary>
/// 用于验证模型类型注册路径的工厂创建模型。
/// </summary>
private sealed class FactoryCreatedModel(ConstructorDependency dependency) : IModel
{
private IArchitectureContext _context = null!;
/// <summary>
/// 获取构造函数注入的依赖对象。
/// </summary>
public ConstructorDependency Dependency { get; } = dependency;
/// <summary>
/// 获取初始化调用次数。
/// </summary>
public int InitializeCallCount { get; private set; }
public void Initialize()
{
InitializeCallCount++;
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
/// <summary>
/// 统计实现类型单例在预冻结激活阶段的构造次数。
/// </summary>
private sealed class DependencyConstructionCounter
{
/// <summary>
/// 获取共享依赖被构造的次数。
/// </summary>
public int CreationCount { get; private set; }
/// <summary>
/// 记录一次新的依赖构造。
/// </summary>
public void RecordCreation()
{
CreationCount++;
}
}
/// <summary>
/// 用于覆盖 ImplementationType 单例描述符路径的共享依赖。
/// </summary>
private sealed class ImplementationTypeSharedDependency
{
/// <summary>
/// 创建共享依赖并记录构造次数。
/// </summary>
/// <param name="counter">用于统计构造次数的计数器。</param>
public ImplementationTypeSharedDependency(DependencyConstructionCounter counter)
{
counter.RecordCreation();
}
}
/// <summary>
/// 用于覆盖 ImplementationType 单例复用路径的测试系统。
/// </summary>
private sealed class ImplementationTypeDependencySystem(ImplementationTypeSharedDependency dependency) : ISystem
{
private IArchitectureContext _context = null!;
/// <summary>
/// 获取构造函数注入的共享依赖。
/// </summary>
public ImplementationTypeSharedDependency Dependency { get; } = dependency;
public void Initialize()
{
}
public void Destroy()
{
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
/// <summary>
/// 用于覆盖 ImplementationType 单例复用路径的测试模型。
/// </summary>
private sealed class ImplementationTypeDependencyModel(ImplementationTypeSharedDependency dependency) : IModel
{
private IArchitectureContext _context = null!;
/// <summary>
/// 获取构造函数注入的共享依赖。
/// </summary>
public ImplementationTypeSharedDependency Dependency { get; } = dependency;
public void Initialize()
{
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
/// <summary>
/// 用于覆盖 ImplementationFactory 单例描述符路径的共享依赖。
/// </summary>
private sealed class FactorySharedDependency
{
}
/// <summary>
/// 用于覆盖 ImplementationFactory 单例复用路径的测试系统。
/// </summary>
private sealed class FactoryDependencySystem(FactorySharedDependency dependency) : ISystem
{
private IArchitectureContext _context = null!;
/// <summary>
/// 获取构造函数注入的共享依赖。
/// </summary>
public FactorySharedDependency Dependency { get; } = dependency;
public void Initialize()
{
}
public void Destroy()
{
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
/// <summary>
/// 用于覆盖 ImplementationFactory 单例复用路径的测试模型。
/// </summary>
private sealed class FactoryDependencyModel(FactorySharedDependency dependency) : IModel
{
private IArchitectureContext _context = null!;
/// <summary>
/// 获取构造函数注入的共享依赖。
/// </summary>
public FactorySharedDependency Dependency { get; } = dependency;
public void Initialize()
{
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
}

View File

@ -0,0 +1,188 @@
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Architectures;
using GFramework.Core.Environment;
using GFramework.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 验证 <see cref="Architecture" /> 初始化编排流程的单元测试。
/// 这些测试覆盖环境初始化、服务准备、上下文绑定和自定义服务配置的时序,
/// 以确保核心协调器在拆分后仍保持既有行为。
/// </summary>
[TestFixture]
public class ArchitectureInitializationPipelineTests
{
/// <summary>
/// 为每个测试准备独立的日志工厂和游戏上下文状态。
/// </summary>
[SetUp]
public void SetUp()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
GameContext.Clear();
}
/// <summary>
/// 清理测试期间注册的全局游戏上下文,避免跨测试污染。
/// </summary>
[TearDown]
public void TearDown()
{
GameContext.Clear();
}
/// <summary>
/// 验证异步初始化会在执行用户初始化逻辑之前准备环境、服务和上下文。
/// </summary>
[Test]
public async Task InitializeAsync_Should_Prepare_Runtime_Before_OnInitialize()
{
var environment = new TrackingEnvironment();
var marker = new BootstrapMarker();
var architecture = new InitializationPipelineTestArchitecture(environment, marker);
await architecture.InitializeAsync();
AssertRuntimePrepared(architecture, environment, marker);
await architecture.DestroyAsync();
}
/// <summary>
/// 验证同步初始化路径复用同一套基础设施准备流程。
/// </summary>
[Test]
public async Task Initialize_Should_Prepare_Runtime_Before_OnInitialize()
{
var environment = new TrackingEnvironment();
var marker = new BootstrapMarker();
var architecture = new InitializationPipelineTestArchitecture(environment, marker);
architecture.Initialize();
AssertRuntimePrepared(architecture, environment, marker);
await architecture.DestroyAsync();
}
/// <summary>
/// 断言初始化阶段所需的运行时准备工作都已经完成。
/// </summary>
/// <param name="architecture">待验证的测试架构实例。</param>
/// <param name="environment">测试使用的环境对象。</param>
/// <param name="marker">通过服务配置委托注册的标记服务。</param>
private static void AssertRuntimePrepared(
InitializationPipelineTestArchitecture architecture,
TrackingEnvironment environment,
BootstrapMarker marker)
{
Assert.Multiple(() =>
{
Assert.That(architecture.ObservedEnvironmentInitialized, Is.True);
Assert.That(architecture.ObservedConfiguredServiceAvailable, Is.True);
Assert.That(architecture.ObservedEventBusAvailable, Is.True);
Assert.That(architecture.ObservedContextWasBound, Is.True);
Assert.That(architecture.ObservedEnvironmentRegistered, Is.True);
Assert.That(architecture.Context.GetEnvironment(), Is.SameAs(environment));
Assert.That(architecture.Context.GetService<BootstrapMarker>(), Is.SameAs(marker));
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.Ready));
});
}
/// <summary>
/// 跟踪初始化期间关键可观察状态的测试架构。
/// </summary>
private sealed class InitializationPipelineTestArchitecture : Architecture
{
private readonly TrackingEnvironment _environment;
private readonly BootstrapMarker _marker;
/// <summary>
/// 使用可观察环境和标记服务创建测试架构。
/// </summary>
/// <param name="environment">用于验证初始化时序的环境对象。</param>
/// <param name="marker">用于验证服务钩子执行结果的标记服务。</param>
public InitializationPipelineTestArchitecture(
TrackingEnvironment environment,
BootstrapMarker marker)
: base(environment: environment)
{
_environment = environment;
_marker = marker;
}
/// <summary>
/// 记录用户初始化逻辑执行时环境是否已经准备完成。
/// </summary>
public bool ObservedEnvironmentInitialized { get; private set; }
/// <summary>
/// 记录自定义服务是否已在用户初始化前注册到容器。
/// </summary>
public bool ObservedConfiguredServiceAvailable { get; private set; }
/// <summary>
/// 记录内置事件总线是否已在用户初始化前可用。
/// </summary>
public bool ObservedEventBusAvailable { get; private set; }
/// <summary>
/// 记录当前上下文是否已在用户初始化前绑定到全局游戏上下文表。
/// </summary>
public bool ObservedContextWasBound { get; private set; }
/// <summary>
/// 记录环境对象是否已在用户初始化前注册到架构上下文。
/// </summary>
public bool ObservedEnvironmentRegistered { get; private set; }
/// <summary>
/// 为容器注册测试标记服务,用于验证初始化前的服务钩子是否执行。
/// </summary>
public override Action<IServiceCollection>? Configurator => services => services.AddSingleton(_marker);
/// <summary>
/// 在用户初始化逻辑中采集运行时准备状态。
/// </summary>
protected override void OnInitialize()
{
ObservedEnvironmentInitialized = _environment.InitializeCallCount == 1;
ObservedConfiguredServiceAvailable = ReferenceEquals(Context.GetService<BootstrapMarker>(), _marker);
ObservedEventBusAvailable = Context.GetService<IEventBus>() is not null;
ObservedContextWasBound = ReferenceEquals(GameContext.GetByType(GetType()), Context);
ObservedEnvironmentRegistered = ReferenceEquals(Context.GetEnvironment(), _environment);
}
}
/// <summary>
/// 用于验证环境初始化是否发生的测试环境。
/// </summary>
private sealed class TrackingEnvironment : EnvironmentBase
{
/// <summary>
/// 获取测试环境名称。
/// </summary>
public override string Name { get; } = "Tracking";
/// <summary>
/// 获取环境初始化调用次数。
/// </summary>
public int InitializeCallCount { get; private set; }
/// <summary>
/// 记录环境初始化次数。
/// </summary>
public override void Initialize()
{
InitializeCallCount++;
}
}
/// <summary>
/// 通过服务配置委托注册到容器的测试标记对象。
/// </summary>
private sealed class BootstrapMarker
{
}
}

View File

@ -0,0 +1,463 @@
using System.Reflection;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Lifecycle;
using GFramework.Core.Abstractions.Model;
using GFramework.Core.Abstractions.Systems;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 验证 Architecture 生命周期行为的集成测试。
/// 这些测试覆盖阶段流转、失败状态传播和逆序销毁规则,
/// 用于保护拆分后的生命周期管理、阶段协调与销毁协调行为。
/// </summary>
[TestFixture]
public class ArchitectureLifecycleBehaviorTests
{
/// <summary>
/// 为每个测试准备独立的日志工厂和全局上下文状态。
/// </summary>
[SetUp]
public void SetUp()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
GameContext.Clear();
}
/// <summary>
/// 清理测试注册到全局上下文表的架构上下文。
/// </summary>
[TearDown]
public void TearDown()
{
GameContext.Clear();
}
/// <summary>
/// 验证初始化流程会按既定顺序推进所有生命周期阶段。
/// </summary>
[Test]
public async Task InitializeAsync_Should_Enter_Expected_Phases_In_Order()
{
var architecture = new PhaseTrackingArchitecture();
await architecture.InitializeAsync();
Assert.That(architecture.PhaseHistory, Is.EqualTo(new[]
{
ArchitecturePhase.BeforeUtilityInit,
ArchitecturePhase.AfterUtilityInit,
ArchitecturePhase.BeforeModelInit,
ArchitecturePhase.AfterModelInit,
ArchitecturePhase.BeforeSystemInit,
ArchitecturePhase.AfterSystemInit,
ArchitecturePhase.Ready
}));
await architecture.DestroyAsync();
}
/// <summary>
/// 验证用户初始化失败时,等待 Ready 的任务会失败并进入 FailedInitialization 阶段。
/// </summary>
[Test]
public async Task InitializeAsync_When_OnInitialize_Throws_Should_Mark_FailedInitialization()
{
var architecture = new PhaseTrackingArchitecture(() => throw new InvalidOperationException("boom"));
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await architecture.InitializeAsync());
Assert.That(exception, Is.Not.Null);
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.FailedInitialization));
Assert.ThrowsAsync<InvalidOperationException>(async () => await architecture.WaitUntilReadyAsync());
}
/// <summary>
/// 验证销毁流程会按注册逆序释放组件,并推进 Destroying/Destroyed 阶段。
/// </summary>
[Test]
public async Task DestroyAsync_Should_Destroy_Components_In_Reverse_Registration_Order()
{
var destroyOrder = new List<string>();
var architecture = new DestroyOrderArchitecture(destroyOrder);
await architecture.InitializeAsync();
await architecture.DestroyAsync();
Assert.Multiple(() =>
{
Assert.That(destroyOrder, Is.EqualTo(new[] { "system", "model", "utility" }));
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.Destroyed));
Assert.That(architecture.PhaseHistory[^2..], Is.EqualTo(new[]
{
ArchitecturePhase.Destroying,
ArchitecturePhase.Destroyed
}));
});
}
/// <summary>
/// 验证初始化失败后仍然允许执行销毁流程。
/// 该回归测试用于保护 FailedInitialization → Destroying 的合法迁移,避免失败路径上的组件与容器泄漏。
/// </summary>
[Test]
public async Task DestroyAsync_After_FailedInitialization_Should_Cleanup_And_Enter_Destroyed()
{
var destroyOrder = new List<string>();
var architecture = new FailingInitializationArchitecture(destroyOrder);
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await architecture.InitializeAsync());
Assert.That(exception, Is.Not.Null);
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.FailedInitialization));
await architecture.DestroyAsync();
Assert.Multiple(() =>
{
Assert.That(destroyOrder, Is.EqualTo(new[] { "model", "utility" }));
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.Destroyed));
Assert.That(architecture.PhaseHistory[^3..], Is.EqualTo(new[]
{
ArchitecturePhase.FailedInitialization,
ArchitecturePhase.Destroying,
ArchitecturePhase.Destroyed
}));
});
}
/// <summary>
/// 验证 Destroyed 阶段会在容器清空前广播给容器内的阶段监听器。
/// 该回归测试保护销毁尾声的阶段通知,确保依赖最终阶段信号的服务仍能收到 Destroyed。
/// </summary>
[Test]
public async Task DestroyAsync_Should_Notify_Container_Phase_Listeners_About_Destroyed_Before_Clear()
{
var listener = new TrackingPhaseListener();
var architecture = new ListenerTrackingArchitecture(listener);
await architecture.InitializeAsync();
await architecture.DestroyAsync();
Assert.That(listener.ObservedPhases[^2..], Is.EqualTo(new[]
{
ArchitecturePhase.Destroying,
ArchitecturePhase.Destroyed
}));
}
/// <summary>
/// 验证启用 AllowLateRegistration 时,生命周期层会立即初始化后注册的组件,而不是继续沿用初始化期的拒绝策略。
/// 由于公共架构 API 在 Ready 之后会先触发容器限制,此回归测试直接覆盖生命周期协作者的对齐逻辑。
/// </summary>
[Test]
public async Task
RegisterLifecycleComponent_After_Initialization_Should_Initialize_Immediately_When_LateRegistration_Is_Enabled()
{
var architecture = new AllowLateRegistrationArchitecture();
await architecture.InitializeAsync();
var lateComponent = new LateRegisteredInitializableComponent();
architecture.RegisterLateComponentForTesting(lateComponent);
Assert.That(lateComponent.InitializeCallCount, Is.EqualTo(1));
await architecture.DestroyAsync();
}
/// <summary>
/// 记录阶段流转的可配置测试架构。
/// </summary>
private sealed class PhaseTrackingArchitecture : Architecture
{
private readonly Action? _onInitializeAction;
/// <summary>
/// 创建一个可选地在用户初始化阶段执行自定义逻辑的测试架构。
/// </summary>
/// <param name="onInitializeAction">用户初始化时执行的测试回调。</param>
public PhaseTrackingArchitecture(Action? onInitializeAction = null)
{
_onInitializeAction = onInitializeAction;
PhaseChanged += phase => PhaseHistory.Add(phase);
}
/// <summary>
/// 获取架构经历过的阶段列表。
/// </summary>
public List<ArchitecturePhase> PhaseHistory { get; } = [];
/// <summary>
/// 执行测试注入的初始化逻辑。
/// </summary>
protected override void OnInitialize()
{
_onInitializeAction?.Invoke();
}
}
/// <summary>
/// 在初始化时注册可销毁组件的测试架构。
/// </summary>
private sealed class DestroyOrderArchitecture : Architecture
{
private readonly List<string> _destroyOrder;
/// <summary>
/// 创建用于验证销毁顺序的测试架构。
/// </summary>
/// <param name="destroyOrder">记录组件销毁顺序的列表。</param>
public DestroyOrderArchitecture(List<string> destroyOrder)
{
_destroyOrder = destroyOrder;
PhaseChanged += phase => PhaseHistory.Add(phase);
}
/// <summary>
/// 获取架构经历过的阶段列表。
/// </summary>
public List<ArchitecturePhase> PhaseHistory { get; } = [];
/// <summary>
/// 注册会记录销毁顺序的 Utility、Model 和 System。
/// </summary>
protected override void OnInitialize()
{
RegisterUtility(new TrackingDestroyableUtility(_destroyOrder));
RegisterModel(new TrackingDestroyableModel(_destroyOrder));
RegisterSystem(new TrackingDestroyableSystem(_destroyOrder));
}
}
/// <summary>
/// 在初始化阶段注册可销毁组件并随后抛出异常的测试架构。
/// </summary>
private sealed class FailingInitializationArchitecture : Architecture
{
private readonly List<string> _destroyOrder;
/// <summary>
/// 创建用于验证失败后销毁行为的测试架构。
/// </summary>
/// <param name="destroyOrder">记录失败后清理顺序的列表。</param>
public FailingInitializationArchitecture(List<string> destroyOrder)
{
_destroyOrder = destroyOrder;
PhaseChanged += phase => PhaseHistory.Add(phase);
}
/// <summary>
/// 获取架构经历过的阶段列表。
/// </summary>
public List<ArchitecturePhase> PhaseHistory { get; } = [];
/// <summary>
/// 注册可销毁组件后故意抛出异常,模拟初始化失败场景。
/// </summary>
protected override void OnInitialize()
{
RegisterUtility(new TrackingDestroyableUtility(_destroyOrder));
RegisterModel(new TrackingDestroyableModel(_destroyOrder));
throw new InvalidOperationException("boom");
}
}
/// <summary>
/// 通过配置器把阶段监听器注册到容器中的测试架构。
/// </summary>
private sealed class ListenerTrackingArchitecture(TrackingPhaseListener listener) : Architecture
{
/// <summary>
/// 保持对监听器的引用,以便配置器在初始化前把同一实例注册到容器。
/// </summary>
public override Action<IServiceCollection>? Configurator =>
services => services.AddSingleton<IArchitecturePhaseListener>(listener);
/// <summary>
/// 该测试不需要额外组件注册。
/// </summary>
protected override void OnInitialize()
{
}
}
/// <summary>
/// 启用 AllowLateRegistration 的测试架构。
/// 该架构暴露生命周期协作者供回归测试验证内部注册策略对齐。
/// </summary>
private sealed class AllowLateRegistrationArchitecture : Architecture
{
/// <summary>
/// 使用允许后注册的配置创建测试架构。
/// </summary>
public AllowLateRegistrationArchitecture()
: base(new ArchitectureConfiguration
{
ArchitectureProperties = new()
{
AllowLateRegistration = true,
StrictPhaseValidation = true
}
})
{
}
/// <summary>
/// 该测试不需要初始组件。
/// </summary>
protected override void OnInitialize()
{
}
/// <summary>
/// 通过反射调用内部生命周期协作者的注册逻辑,以便覆盖无法通过公共 API 直接到达的后注册初始化路径。
/// </summary>
/// <param name="component">要登记到生命周期中的后注册组件。</param>
public void RegisterLateComponentForTesting(object component)
{
var field = typeof(Architecture).GetField(
"_lifecycle",
BindingFlags.Instance | BindingFlags.NonPublic);
var lifecycle = field?.GetValue(this) ??
throw new InvalidOperationException("Architecture lifecycle field was not found.");
var registerMethod = lifecycle.GetType().GetMethod(nameof(RegisterLateComponentForTesting)) ??
lifecycle.GetType().GetMethod("RegisterLifecycleComponent") ??
throw new InvalidOperationException(
"Architecture lifecycle registration method was not found.");
registerMethod.Invoke(lifecycle, [component]);
}
}
/// <summary>
/// 用于验证逆序销毁的上下文工具。
/// </summary>
private sealed class TrackingDestroyableUtility(List<string> destroyOrder) : IContextUtility
{
private IArchitectureContext _context = null!;
public void Initialize()
{
}
public void Destroy()
{
destroyOrder.Add("utility");
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
/// <summary>
/// 记录容器阶段通知顺序的监听器。
/// </summary>
private sealed class TrackingPhaseListener : IArchitecturePhaseListener
{
/// <summary>
/// 获取监听到的阶段列表。
/// </summary>
public List<ArchitecturePhase> ObservedPhases { get; } = [];
/// <summary>
/// 记录收到的阶段通知。
/// </summary>
/// <param name="phase">当前阶段。</param>
public void OnArchitecturePhase(ArchitecturePhase phase)
{
ObservedPhases.Add(phase);
}
}
/// <summary>
/// 记录即时初始化次数的后注册测试组件。
/// </summary>
private sealed class LateRegisteredInitializableComponent : IInitializable
{
/// <summary>
/// 获取组件被即时初始化的次数。
/// </summary>
public int InitializeCallCount { get; private set; }
/// <summary>
/// 记录一次初始化调用。
/// </summary>
public void Initialize()
{
InitializeCallCount++;
}
}
/// <summary>
/// 用于验证逆序销毁的模型。
/// </summary>
private sealed class TrackingDestroyableModel(List<string> destroyOrder) : IModel, IDestroyable
{
private IArchitectureContext _context = null!;
public void Destroy()
{
destroyOrder.Add("model");
}
public void Initialize()
{
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
/// <summary>
/// 用于验证逆序销毁的系统。
/// </summary>
private sealed class TrackingDestroyableSystem(List<string> destroyOrder) : ISystem
{
private IArchitectureContext _context = null!;
public void Initialize()
{
}
public void Destroy()
{
destroyOrder.Add("system");
}
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
public void SetContext(IArchitectureContext context)
{
_context = context;
}
public IArchitectureContext GetContext()
{
return _context;
}
}
}

View File

@ -0,0 +1,188 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Utility;
using GFramework.Core.Architectures;
using GFramework.Core.Logging;
using Mediator;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Tests.Architectures;
/// <summary>
/// 验证 Architecture 通过 <c>ArchitectureModules</c> 暴露出的模块安装与 Mediator 行为注册能力。
/// 这些测试覆盖模块安装回调和中介管道行为接入,确保模块管理器仍然保持可观察行为不变。
/// </summary>
[TestFixture]
public class ArchitectureModulesBehaviorTests
{
/// <summary>
/// 初始化日志工厂和全局上下文状态。
/// </summary>
[SetUp]
public void SetUp()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
GameContext.Clear();
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
}
/// <summary>
/// 清理测试过程中写入的全局上下文状态。
/// </summary>
[TearDown]
public void TearDown()
{
GameContext.Clear();
TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount = 0;
}
/// <summary>
/// 验证安装模块时会把当前架构实例传给模块,并允许模块在安装阶段注册组件。
/// </summary>
[Test]
public async Task InstallModule_Should_Invoke_Module_Install_With_Current_Architecture()
{
var module = new TrackingArchitectureModule();
var architecture = new ModuleTestArchitecture(target => target.InstallModule(module));
await architecture.InitializeAsync();
Assert.Multiple(() =>
{
Assert.That(module.InstalledArchitecture, Is.SameAs(architecture));
Assert.That(module.InstallCallCount, Is.EqualTo(1));
Assert.That(architecture.Context.GetUtility<InstalledByModuleUtility>(), Is.Not.Null);
});
await architecture.DestroyAsync();
}
/// <summary>
/// 验证注册的 Mediator 行为会参与请求管道执行。
/// </summary>
[Test]
public async Task RegisterMediatorBehavior_Should_Apply_Pipeline_Behavior_To_Request()
{
var architecture = new ModuleTestArchitecture(target =>
target.RegisterMediatorBehavior<TrackingPipelineBehavior<ModuleBehaviorRequest, string>>());
await architecture.InitializeAsync();
var response = await architecture.Context.SendRequestAsync(new ModuleBehaviorRequest());
Assert.Multiple(() =>
{
Assert.That(response, Is.EqualTo("handled"));
Assert.That(TrackingPipelineBehavior<ModuleBehaviorRequest, string>.InvocationCount, Is.EqualTo(1));
});
await architecture.DestroyAsync();
}
/// <summary>
/// 用于测试模块行为的最小架构实现。
/// </summary>
private sealed class ModuleTestArchitecture(Action<ModuleTestArchitecture> registrationAction) : Architecture
{
/// <summary>
/// 打开 Mediator 服务注册,以便测试中介行为接入。
/// </summary>
public override Action<IServiceCollection>? Configurator =>
services => services.AddMediator(options => { options.ServiceLifetime = ServiceLifetime.Singleton; });
/// <summary>
/// 在初始化阶段执行测试注入的模块注册逻辑。
/// </summary>
protected override void OnInitialize()
{
registrationAction(this);
}
}
/// <summary>
/// 记录模块安装调用情况的测试模块。
/// </summary>
private sealed class TrackingArchitectureModule : IArchitectureModule
{
/// <summary>
/// 获取模块安装调用次数。
/// </summary>
public int InstallCallCount { get; private set; }
/// <summary>
/// 获取最近一次接收到的架构实例。
/// </summary>
public IArchitecture? InstalledArchitecture { get; private set; }
/// <summary>
/// 记录安装调用,并在安装阶段注册一个工具验证调用链可用。
/// </summary>
/// <param name="architecture">目标架构实例。</param>
public void Install(IArchitecture architecture)
{
InstallCallCount++;
InstalledArchitecture = architecture;
architecture.RegisterUtility(new InstalledByModuleUtility());
}
}
/// <summary>
/// 由测试模块安装时注册的简单工具。
/// </summary>
private sealed class InstalledByModuleUtility : IUtility
{
}
}
/// <summary>
/// 用于验证管道行为注册是否生效的测试请求。
/// </summary>
public sealed class ModuleBehaviorRequest : IRequest<string>
{
}
/// <summary>
/// 处理测试请求的处理器。
/// </summary>
public sealed class ModuleBehaviorRequestHandler : IRequestHandler<ModuleBehaviorRequest, string>
{
/// <summary>
/// 返回固定结果,便于聚焦验证管道行为是否执行。
/// </summary>
/// <param name="request">请求实例。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>固定响应内容。</returns>
public ValueTask<string> Handle(ModuleBehaviorRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult("handled");
}
}
/// <summary>
/// 记录请求通过管道次数的测试行为。
/// </summary>
/// <typeparam name="TRequest">请求类型。</typeparam>
/// <typeparam name="TResponse">响应类型。</typeparam>
public sealed class TrackingPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
/// <summary>
/// 获取当前测试进程中该请求类型对应的行为触发次数。
/// </summary>
public static int InvocationCount { get; set; }
/// <summary>
/// 记录一次行为执行,然后继续执行下一个处理器。
/// </summary>
/// <param name="message">当前请求消息。</param>
/// <param name="next">下一个处理委托。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>下游处理器的响应结果。</returns>
public async ValueTask<TResponse> Handle(
TRequest message,
MessageHandlerDelegate<TRequest, TResponse> next,
CancellationToken cancellationToken)
{
InvocationCount++;
return await next(message, cancellationToken);
}
}

View File

@ -0,0 +1,233 @@
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine;
using GFramework.Core.Coroutine.Instructions;
namespace GFramework.Core.Tests.Coroutine;
/// <summary>
/// 协程调度器增强行为测试。
/// </summary>
[TestFixture]
public sealed class CoroutineSchedulerAdvancedTests
{
/// <summary>
/// 验证 WaitForSecondsRealtime 使用独立的真实时间源推进。
/// </summary>
[Test]
public void WaitForSecondsRealtime_Should_Use_Realtime_TimeSource_When_Provided()
{
var scaledTime = new FakeTimeSource();
var realtimeTime = new FakeTimeSource();
var scheduler = new CoroutineScheduler(
scaledTime,
realtimeTimeSource: realtimeTime);
var completed = false;
IEnumerator<IYieldInstruction> Coroutine()
{
yield return new WaitForSecondsRealtime(1.0);
completed = true;
}
scheduler.Run(Coroutine());
scaledTime.Advance(0.1);
realtimeTime.Advance(0.4);
scheduler.Update();
Assert.That(completed, Is.False);
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1));
scaledTime.Advance(0.1);
realtimeTime.Advance(0.6);
scheduler.Update();
Assert.That(completed, Is.True);
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(0));
}
/// <summary>
/// 验证固定更新等待指令仅在固定阶段调度器中推进。
/// </summary>
[Test]
public void WaitForFixedUpdate_Should_Only_Advance_On_FixedUpdate_Scheduler()
{
var defaultTime = new FakeTimeSource();
var fixedTime = new FakeTimeSource();
var defaultScheduler = new CoroutineScheduler(defaultTime);
var fixedScheduler = new CoroutineScheduler(
fixedTime,
executionStage: CoroutineExecutionStage.FixedUpdate);
var defaultCompleted = false;
var fixedCompleted = false;
IEnumerator<IYieldInstruction> DefaultCoroutine()
{
yield return new WaitForFixedUpdate();
defaultCompleted = true;
}
IEnumerator<IYieldInstruction> FixedCoroutine()
{
yield return new WaitForFixedUpdate();
fixedCompleted = true;
}
defaultScheduler.Run(DefaultCoroutine());
fixedScheduler.Run(FixedCoroutine());
defaultTime.Advance(0.1);
fixedTime.Advance(0.1);
defaultScheduler.Update();
fixedScheduler.Update();
Assert.That(defaultCompleted, Is.False);
Assert.That(defaultScheduler.ActiveCoroutineCount, Is.EqualTo(1));
Assert.That(fixedCompleted, Is.True);
Assert.That(fixedScheduler.ActiveCoroutineCount, Is.EqualTo(0));
}
/// <summary>
/// 验证取消令牌会在下一次调度循环中终止协程并记录结果。
/// </summary>
[Test]
public async Task CancellationToken_Should_Cancel_Coroutine_On_Next_Update()
{
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
using var cancellationTokenSource = new CancellationTokenSource();
IEnumerator<IYieldInstruction> Coroutine()
{
yield return new Delay(10);
}
var handle = scheduler.Run(Coroutine(), cancellationToken: cancellationTokenSource.Token);
cancellationTokenSource.Cancel();
timeSource.Advance(0.1);
scheduler.Update();
var status = await scheduler.WaitForCompletionAsync(handle);
Assert.That(scheduler.IsCoroutineAlive(handle), Is.False);
Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Cancelled));
}
/// <summary>
/// 验证调度器可以暴露活跃协程快照。
/// </summary>
[Test]
public void TryGetSnapshot_Should_Return_Current_Waiting_Instruction_And_Stage()
{
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(
timeSource,
executionStage: CoroutineExecutionStage.EndOfFrame);
IEnumerator<IYieldInstruction> Coroutine()
{
yield return new WaitForEndOfFrame();
}
var handle = scheduler.Run(Coroutine(), tag: "ui", group: "frame-end");
var found = scheduler.TryGetSnapshot(handle, out var snapshot);
Assert.That(found, Is.True);
Assert.That(snapshot.Handle, Is.EqualTo(handle));
Assert.That(snapshot.Tag, Is.EqualTo("ui"));
Assert.That(snapshot.Group, Is.EqualTo("frame-end"));
Assert.That(snapshot.IsWaiting, Is.True);
Assert.That(snapshot.WaitingInstructionType, Is.EqualTo(typeof(WaitForEndOfFrame)));
Assert.That(snapshot.ExecutionStage, Is.EqualTo(CoroutineExecutionStage.EndOfFrame));
}
/// <summary>
/// 验证异常结束的协程会记录为 Faulted。
/// </summary>
[Test]
public async Task WaitForCompletionAsync_Should_Return_Faulted_For_Failing_Coroutine()
{
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
IEnumerator<IYieldInstruction> Coroutine()
{
yield return new WaitOneFrame();
throw new InvalidOperationException("boom");
#pragma warning disable CS0162
yield break;
#pragma warning restore CS0162
}
var handle = scheduler.Run(Coroutine());
timeSource.Advance(0.1);
scheduler.Update();
timeSource.Advance(0.1);
scheduler.Update();
var status = await scheduler.WaitForCompletionAsync(handle);
Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Faulted));
}
/// <summary>
/// 验证完成状态缓存有固定上限,避免无限增长。
/// </summary>
[Test]
public void CompletionStatusHistory_Should_Be_Bounded()
{
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
var handles = new List<CoroutineHandle>();
IEnumerator<IYieldInstruction> ImmediateCoroutine()
{
yield break;
}
for (var i = 0; i < 1100; i++)
{
handles.Add(scheduler.Run(ImmediateCoroutine()));
}
Assert.That(scheduler.TryGetCompletionStatus(handles[0], out _), Is.False);
Assert.That(scheduler.TryGetCompletionStatus(handles[^1], out var latestStatus), Is.True);
Assert.That(latestStatus, Is.EqualTo(CoroutineCompletionStatus.Completed));
}
/// <summary>
/// 验证作为首个等待指令的 WaitForCoroutine 会立即启动子协程,并沿用父协程取消令牌。
/// </summary>
[Test]
public async Task WaitForCoroutine_Should_Start_Child_During_Prewarm_And_Propagate_Cancellation()
{
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
using var cancellationTokenSource = new CancellationTokenSource();
IEnumerator<IYieldInstruction> ChildCoroutine()
{
yield return new Delay(10);
}
IEnumerator<IYieldInstruction> ParentCoroutine()
{
yield return new WaitForCoroutine(ChildCoroutine());
}
var handle = scheduler.Run(ParentCoroutine(), cancellationToken: cancellationTokenSource.Token);
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(2));
cancellationTokenSource.Cancel();
timeSource.Advance(0.1);
scheduler.Update();
var status = await scheduler.WaitForCompletionAsync(handle);
Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Cancelled));
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(0));
}
}

View File

@ -16,6 +16,7 @@ namespace GFramework.Core.Architectures;
/// 专注于生命周期管理、初始化流程控制和架构阶段转换。 /// 专注于生命周期管理、初始化流程控制和架构阶段转换。
/// ///
/// 重构说明:此类已重构为协调器模式,将职责委托给专门的管理器: /// 重构说明:此类已重构为协调器模式,将职责委托给专门的管理器:
/// - ArchitectureBootstrapper: 初始化基础设施编排
/// - ArchitectureLifecycle: 生命周期管理 /// - ArchitectureLifecycle: 生命周期管理
/// - ArchitectureComponentRegistry: 组件注册管理 /// - ArchitectureComponentRegistry: 组件注册管理
/// - ArchitectureModules: 模块管理 /// - ArchitectureModules: 模块管理
@ -37,19 +38,25 @@ public abstract class Architecture : IArchitecture
IArchitectureServices? services = null, IArchitectureServices? services = null,
IArchitectureContext? context = null) IArchitectureContext? context = null)
{ {
Configuration = configuration ?? new ArchitectureConfiguration(); var resolvedConfiguration = configuration ?? new ArchitectureConfiguration();
Environment = environment ?? new DefaultEnvironment(); var resolvedEnvironment = environment ?? new DefaultEnvironment();
Services = services ?? new ArchitectureServices(); var resolvedServices = services ?? new ArchitectureServices();
_context = context; _context = context;
// 初始化 Logger // 初始化 Logger
LoggerFactoryResolver.Provider = Configuration.LoggerProperties.LoggerFactoryProvider; LoggerFactoryResolver.Provider = resolvedConfiguration.LoggerProperties.LoggerFactoryProvider;
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name); _logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
// 初始化管理器 // 初始化管理器
_lifecycle = new ArchitectureLifecycle(this, Configuration, Services, _logger); _bootstrapper = new ArchitectureBootstrapper(GetType(), resolvedEnvironment, resolvedServices, _logger);
_componentRegistry = new ArchitectureComponentRegistry(this, Configuration, Services, _lifecycle, _logger); _lifecycle = new ArchitectureLifecycle(this, resolvedConfiguration, resolvedServices, _logger);
_modules = new ArchitectureModules(this, Services, _logger); _componentRegistry = new ArchitectureComponentRegistry(
this,
resolvedConfiguration,
resolvedServices,
_lifecycle,
_logger);
_modules = new ArchitectureModules(this, resolvedServices, _logger);
} }
#endregion #endregion
@ -70,21 +77,6 @@ public abstract class Architecture : IArchitecture
#region Properties #region Properties
/// <summary>
/// 获取架构配置对象
/// </summary>
private IArchitectureConfiguration Configuration { get; }
/// <summary>
/// 获取环境配置对象
/// </summary>
private IEnvironment Environment { get; }
/// <summary>
/// 获取服务管理器
/// </summary>
private IArchitectureServices Services { get; }
/// <summary> /// <summary>
/// 当前架构的阶段 /// 当前架构的阶段
/// </summary> /// </summary>
@ -129,6 +121,11 @@ public abstract class Architecture : IArchitecture
/// </summary> /// </summary>
private IArchitectureContext? _context; private IArchitectureContext? _context;
/// <summary>
/// 初始化基础设施编排器
/// </summary>
private readonly ArchitectureBootstrapper _bootstrapper;
/// <summary> /// <summary>
/// 生命周期管理器 /// 生命周期管理器
/// </summary> /// </summary>
@ -150,7 +147,8 @@ public abstract class Architecture : IArchitecture
/// <summary> /// <summary>
/// 注册中介行为管道 /// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑 /// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 可以传入开放泛型行为类型,也可以传入绑定到特定请求的封闭行为类型。
/// </summary> /// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam> /// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
@ -184,7 +182,7 @@ public abstract class Architecture : IArchitecture
} }
/// <summary> /// <summary>
/// 注册系统类型,由 DI 容器自动创建实例 /// 注册系统类型,由当前服务集合自动创建实例并接入本轮初始化
/// </summary> /// </summary>
/// <typeparam name="T">系统类型</typeparam> /// <typeparam name="T">系统类型</typeparam>
/// <param name="onCreated">可选的实例创建后回调</param> /// <param name="onCreated">可选的实例创建后回调</param>
@ -205,7 +203,7 @@ public abstract class Architecture : IArchitecture
} }
/// <summary> /// <summary>
/// 注册模型类型,由 DI 容器自动创建实例 /// 注册模型类型,由当前服务集合自动创建实例并接入本轮初始化
/// </summary> /// </summary>
/// <typeparam name="T">模型类型</typeparam> /// <typeparam name="T">模型类型</typeparam>
/// <param name="onCreated">可选的实例创建后回调</param> /// <param name="onCreated">可选的实例创建后回调</param>
@ -284,32 +282,7 @@ public abstract class Architecture : IArchitecture
/// <param name="asyncMode">是否启用异步模式</param> /// <param name="asyncMode">是否启用异步模式</param>
private async Task InitializeInternalAsync(bool asyncMode) private async Task InitializeInternalAsync(bool asyncMode)
{ {
// === 基础环境初始化 === _context = await _bootstrapper.PrepareForInitializationAsync(_context, Configurator, asyncMode);
Environment.Initialize();
// 注册内置服务模块
Services.ModuleManager.RegisterBuiltInModules(Services.Container);
// 将 Environment 注册到容器
if (!Services.Container.Contains<IEnvironment>())
Services.Container.RegisterPlurality(Environment);
// 初始化架构上下文
_context ??= new ArchitectureContext(Services.Container);
GameContext.Bind(GetType(), _context);
// 为服务设置上下文
Services.SetContext(_context);
if (Configurator is null)
{
_logger.Debug("Mediator-based cqrs will not take effect without the service setter configured!");
}
// 执行服务钩子
Services.Container.ExecuteServicesHook(Configurator);
// 初始化服务模块
await Services.ModuleManager.InitializeAllAsync(asyncMode);
// === 用户 OnInitialize === // === 用户 OnInitialize ===
_logger.Debug("Calling user OnInitialize()"); _logger.Debug("Calling user OnInitialize()");
@ -320,9 +293,7 @@ public abstract class Architecture : IArchitecture
await _lifecycle.InitializeAllComponentsAsync(asyncMode); await _lifecycle.InitializeAllComponentsAsync(asyncMode);
// === 初始化完成阶段 === // === 初始化完成阶段 ===
Services.Container.Freeze(); _bootstrapper.CompleteInitialization();
_logger.Info("IOC container frozen");
_lifecycle.MarkAsReady(); _lifecycle.MarkAsReady();
_logger.Info($"Architecture {GetType().Name} is ready - all components initialized"); _logger.Info($"Architecture {GetType().Name} is ready - all components initialized");
} }

View File

@ -0,0 +1,118 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Environment;
using GFramework.Core.Abstractions.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Architectures;
/// <summary>
/// 协调架构初始化期间的基础设施准备工作。
/// 该类型将环境初始化、服务模块启动、上下文绑定和服务容器配置从 <see cref="Architecture" /> 中拆出,
/// 使核心架构类只保留生命周期入口和公共 API 协调职责。
/// </summary>
internal sealed class ArchitectureBootstrapper(
Type architectureType,
IEnvironment environment,
IArchitectureServices services,
ILogger logger)
{
/// <summary>
/// 在执行用户 <c>OnInitialize</c> 之前准备架构运行时。
/// 该流程必须保证环境、内置服务、上下文和服务钩子已经可用,
/// 因为用户初始化逻辑通常会立即访问事件总线、查询执行器或环境对象。
/// </summary>
/// <param name="existingContext">调用方已经提供的上下文;如果为空则创建默认上下文。</param>
/// <param name="configurator">可选的容器配置委托,用于接入 Mediator 等扩展服务。</param>
/// <param name="asyncMode">是否以异步模式初始化服务模块。</param>
/// <returns>已绑定到当前架构类型的架构上下文。</returns>
public async Task<IArchitectureContext> PrepareForInitializationAsync(
IArchitectureContext? existingContext,
Action<IServiceCollection>? configurator,
bool asyncMode)
{
InitializeEnvironment();
RegisterBuiltInModules();
EnsureEnvironmentRegistered();
var context = EnsureContext(existingContext);
ConfigureServices(context, configurator);
await InitializeServiceModulesAsync(asyncMode);
return context;
}
/// <summary>
/// 完成用户组件初始化之后的收尾工作。
/// 冻结容器可以阻止 Ready 阶段之后的意外服务注册,保持运行时依赖图稳定。
/// </summary>
public void CompleteInitialization()
{
services.Container.Freeze();
logger.Info("IOC container frozen");
}
/// <summary>
/// 初始化运行环境,使环境对象在后续服务构建和用户初始化前进入可用状态。
/// </summary>
private void InitializeEnvironment()
{
environment.Initialize();
}
/// <summary>
/// 注册框架内置服务模块。
/// 该步骤必须先于执行服务钩子,以便容器具备 CQRS 和事件总线等基础服务。
/// </summary>
private void RegisterBuiltInModules()
{
services.ModuleManager.RegisterBuiltInModules(services.Container);
}
/// <summary>
/// 确保环境对象可以通过架构容器解析。
/// 如果调用方已经预先注册了自定义环境实例,则保留现有绑定,避免覆盖外部配置。
/// </summary>
private void EnsureEnvironmentRegistered()
{
if (!services.Container.Contains<IEnvironment>())
services.Container.RegisterPlurality(environment);
}
/// <summary>
/// 获取本次初始化使用的架构上下文,并将其绑定到全局游戏上下文表。
/// 绑定发生在用户初始化之前,确保组件在注册阶段即可通过架构类型解析上下文。
/// </summary>
/// <param name="existingContext">外部提供的上下文。</param>
/// <returns>实际用于本次初始化的上下文实例。</returns>
private IArchitectureContext EnsureContext(IArchitectureContext? existingContext)
{
var context = existingContext ?? new ArchitectureContext(services.Container);
GameContext.Bind(architectureType, context);
return context;
}
/// <summary>
/// 为服务容器设置上下文并执行扩展配置钩子。
/// 这一步统一承接 Mediator 等容器扩展的接入点,避免 <see cref="Architecture" /> 直接操作容器细节。
/// </summary>
/// <param name="context">当前架构上下文。</param>
/// <param name="configurator">可选的服务集合配置委托。</param>
private void ConfigureServices(IArchitectureContext context, Action<IServiceCollection>? configurator)
{
services.SetContext(context);
if (configurator is null)
logger.Debug("Mediator-based cqrs will not take effect without the service setter configured!");
services.Container.ExecuteServicesHook(configurator);
}
/// <summary>
/// 初始化所有服务模块。
/// 该过程在用户注册系统、模型和工具之前完成,避免组件在初始化期间访问未准备好的服务。
/// </summary>
/// <param name="asyncMode">是否允许异步初始化服务模块。</param>
private async Task InitializeServiceModulesAsync(bool asyncMode)
{
await services.ModuleManager.InitializeAllAsync(asyncMode);
}
}

View File

@ -0,0 +1,106 @@
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using Microsoft.Extensions.DependencyInjection;
namespace GFramework.Core.Architectures;
/// <summary>
/// 为架构组件的类型注册路径提供实例创建能力。
/// 该类型在容器冻结前基于当前服务集合和已注册实例进行激活,
/// 使 <see cref="ArchitectureComponentRegistry" /> 可以在注册阶段就物化 System / Model
/// 避免它们在 Ready 之后首次解析时才参与生命周期而导致状态不一致。
/// </summary>
internal sealed class ArchitectureComponentActivator(
IIocContainer container,
ILogger logger)
{
/// <summary>
/// 预冻结阶段的单例实例缓存。
/// 该缓存跨越整个架构组件激活周期共享,确保多个组件在同一轮初始化中解析到同一个单例描述时不会重复创建实例。
/// </summary>
private readonly Dictionary<Type, object?> _singletonCache = [];
/// <summary>
/// 根据当前容器状态创建组件实例。
/// 激活过程优先复用已经注册到容器中的实例,再按服务描述解析实现类型或工厂方法,
/// 以兼容构造函数依赖于框架服务、用户实例服务和先前注册组件的场景。
/// </summary>
/// <typeparam name="TComponent">要创建的组件类型。</typeparam>
/// <returns>创建完成的组件实例。</returns>
public TComponent CreateInstance<TComponent>() where TComponent : class
{
var activationProvider = new RegistrationServiceProvider(container, logger, _singletonCache);
return ActivatorUtilities.CreateInstance<TComponent>(activationProvider);
}
/// <summary>
/// 面向组件注册的轻量级服务提供者。
/// 该实现只覆盖预冻结阶段需要的解析能力,避免引入完整容器冻结过程。
/// </summary>
private sealed class RegistrationServiceProvider(
IIocContainer container,
ILogger logger,
Dictionary<Type, object?> singletonCache) : IServiceProvider
{
/// <summary>
/// 共享的单例缓存。
/// 该缓存由外层 activator 统一持有,从而把单例复用范围提升到整个组件注册批次,而不是单次实例创建调用。
/// </summary>
private readonly Dictionary<Type, object?> _singletonCache = singletonCache;
/// <summary>
/// 从当前服务集合中解析指定类型的服务。
/// 解析顺序为:已注册实例 → 服务描述实例/工厂/实现类型 → 可直接实例化的具体类型。
/// </summary>
/// <param name="serviceType">请求解析的服务类型。</param>
/// <returns>解析到的服务实例;若无法解析则返回 null。</returns>
public object? GetService(Type serviceType)
{
if (serviceType == typeof(IServiceProvider))
return this;
var existingInstance = container.Get(serviceType);
if (existingInstance != null)
return existingInstance;
var descriptor =
container.GetServicesUnsafe.LastOrDefault(candidate => candidate.ServiceType == serviceType);
if (descriptor != null)
return ResolveDescriptor(serviceType, descriptor);
if (!serviceType.IsAbstract && !serviceType.IsInterface)
return ActivatorUtilities.CreateInstance(this, serviceType);
logger.Trace($"Activation provider could not resolve {serviceType.Name}");
return null;
}
/// <summary>
/// 根据服务描述解析实例,并对单例描述进行缓存。
/// 这样可以保证同一类型在一次组件注册流程中只创建一次依赖实例。
/// </summary>
/// <param name="requestedType">请求的服务类型。</param>
/// <param name="descriptor">命中的服务描述。</param>
/// <returns>解析到的实例。</returns>
private object? ResolveDescriptor(Type requestedType, ServiceDescriptor descriptor)
{
if (descriptor.Lifetime == ServiceLifetime.Singleton &&
_singletonCache.TryGetValue(requestedType, out var cached))
return cached;
object? resolved = descriptor switch
{
{ ImplementationInstance: not null } => descriptor.ImplementationInstance,
{ ImplementationFactory: not null } => descriptor.ImplementationFactory(this),
{ ImplementationType: not null } => ActivatorUtilities.CreateInstance(this,
descriptor.ImplementationType),
_ => null
};
if (descriptor.Lifetime == ServiceLifetime.Singleton && resolved != null)
_singletonCache[requestedType] = resolved;
return resolved;
}
}
}

View File

@ -20,6 +20,8 @@ internal sealed class ArchitectureComponentRegistry(
ArchitectureLifecycle lifecycle, ArchitectureLifecycle lifecycle,
ILogger logger) ILogger logger)
{ {
private readonly ArchitectureComponentActivator _activator = new(services.Container, logger);
#region Validation #region Validation
/// <summary> /// <summary>
@ -63,7 +65,8 @@ internal sealed class ArchitectureComponentRegistry(
} }
/// <summary> /// <summary>
/// 注册系统类型,由 DI 容器自动创建实例 /// 注册系统类型,并在注册阶段由当前服务集合立即创建实例。
/// 这样可以确保该系统参与当前架构初始化批次,而不是等到 Ready 之后首次解析时才延迟创建。
/// </summary> /// </summary>
/// <typeparam name="T">系统类型</typeparam> /// <typeparam name="T">系统类型</typeparam>
/// <param name="onCreated">可选的实例创建后回调</param> /// <param name="onCreated">可选的实例创建后回调</param>
@ -72,21 +75,12 @@ internal sealed class ArchitectureComponentRegistry(
ValidateRegistration("system"); ValidateRegistration("system");
logger.Debug($"Registering system type: {typeof(T).Name}"); logger.Debug($"Registering system type: {typeof(T).Name}");
services.Container.RegisterFactory<T>(sp => // 类型注册路径在注册阶段就物化实例,确保组件能参与当前初始化批次。
{ var system = _activator.CreateInstance<T>();
// 1. DI 创建实例 system.SetContext(architecture.Context);
var system = ActivatorUtilities.CreateInstance<T>(sp); lifecycle.RegisterLifecycleComponent(system);
onCreated?.Invoke(system);
// 2. 框架默认处理 services.Container.RegisterPlurality(system);
system.SetContext(architecture.Context);
lifecycle.RegisterLifecycleComponent(system);
// 3. 用户自定义处理(钩子)
onCreated?.Invoke(system);
logger.Debug($"System created: {typeof(T).Name}");
return system;
});
logger.Info($"System type registered: {typeof(T).Name}"); logger.Info($"System type registered: {typeof(T).Name}");
} }
@ -118,7 +112,8 @@ internal sealed class ArchitectureComponentRegistry(
} }
/// <summary> /// <summary>
/// 注册模型类型,由 DI 容器自动创建实例 /// 注册模型类型,并在注册阶段由当前服务集合立即创建实例。
/// 这样可以确保该模型参与当前架构初始化批次,而不是等到 Ready 之后首次解析时才延迟创建。
/// </summary> /// </summary>
/// <typeparam name="T">模型类型</typeparam> /// <typeparam name="T">模型类型</typeparam>
/// <param name="onCreated">可选的实例创建后回调</param> /// <param name="onCreated">可选的实例创建后回调</param>
@ -127,18 +122,11 @@ internal sealed class ArchitectureComponentRegistry(
ValidateRegistration("model"); ValidateRegistration("model");
logger.Debug($"Registering model type: {typeof(T).Name}"); logger.Debug($"Registering model type: {typeof(T).Name}");
services.Container.RegisterFactory<T>(sp => var model = _activator.CreateInstance<T>();
{ model.SetContext(architecture.Context);
var model = ActivatorUtilities.CreateInstance<T>(sp); lifecycle.RegisterLifecycleComponent(model);
model.SetContext(architecture.Context); onCreated?.Invoke(model);
lifecycle.RegisterLifecycleComponent(model); services.Container.RegisterPlurality(model);
// 用户自定义钩子
onCreated?.Invoke(model);
logger.Debug($"Model created: {typeof(T).Name}");
return model;
});
logger.Info($"Model type registered: {typeof(T).Name}"); logger.Info($"Model type registered: {typeof(T).Name}");
} }
@ -155,6 +143,7 @@ internal sealed class ArchitectureComponentRegistry(
/// <returns>注册成功的工具实例</returns> /// <returns>注册成功的工具实例</returns>
public TUtility RegisterUtility<TUtility>(TUtility utility) where TUtility : IUtility public TUtility RegisterUtility<TUtility>(TUtility utility) where TUtility : IUtility
{ {
ValidateRegistration("utility");
logger.Debug($"Registering utility: {typeof(TUtility).Name}"); logger.Debug($"Registering utility: {typeof(TUtility).Name}");
// 处理上下文工具类型的设置和生命周期管理 // 处理上下文工具类型的设置和生命周期管理
@ -177,6 +166,7 @@ internal sealed class ArchitectureComponentRegistry(
/// <param name="onCreated">可选的实例创建后回调</param> /// <param name="onCreated">可选的实例创建后回调</param>
public void RegisterUtility<T>(Action<T>? onCreated = null) where T : class, IUtility public void RegisterUtility<T>(Action<T>? onCreated = null) where T : class, IUtility
{ {
ValidateRegistration("utility");
logger.Debug($"Registering utility type: {typeof(T).Name}"); logger.Debug($"Registering utility type: {typeof(T).Name}");
services.Container.RegisterFactory<T>(sp => services.Container.RegisterFactory<T>(sp =>

View File

@ -0,0 +1,112 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Lifecycle;
using GFramework.Core.Abstractions.Logging;
namespace GFramework.Core.Architectures;
/// <summary>
/// 统一处理架构内可销毁对象的登记与释放。
/// 该类型封装逆序销毁、异常隔离和服务模块清理规则,
/// 让 <see cref="ArchitectureLifecycle" /> 可以专注于初始化流程本身。
/// </summary>
internal sealed class ArchitectureDisposer(
IArchitectureServices services,
ILogger logger)
{
/// <summary>
/// 保留注册顺序的可销毁对象列表。
/// 销毁时按逆序遍历,以尽量匹配组件间的依赖方向。
/// </summary>
private readonly List<object> _disposables = [];
/// <summary>
/// 用于去重的可销毁对象集合。
/// </summary>
private readonly HashSet<object> _disposableSet = [];
/// <summary>
/// 注册一个需要参与架构销毁流程的对象。
/// 只有实现 <see cref="IDestroyable" /> 或 <see cref="IAsyncDestroyable" /> 的对象会被跟踪。
/// </summary>
/// <param name="component">待检查的组件实例。</param>
public void Register(object component)
{
if (component is not (IDestroyable or IAsyncDestroyable))
return;
if (!_disposableSet.Add(component))
return;
_disposables.Add(component);
logger.Trace($"Registered {component.GetType().Name} for destruction");
}
/// <summary>
/// 执行架构销毁流程。
/// 该方法会根据当前阶段决定是否进入 Destroying/Destroyed 阶段,并负责服务模块与容器清理。
/// </summary>
/// <param name="currentPhase">销毁开始前的架构阶段。</param>
/// <param name="enterPhase">用于推进架构阶段的回调。</param>
public async ValueTask DestroyAsync(ArchitecturePhase currentPhase, Action<ArchitecturePhase> enterPhase)
{
if (currentPhase is ArchitecturePhase.Destroying or ArchitecturePhase.Destroyed)
{
logger.Warn("Architecture destroy called but already in destroying/destroyed state");
return;
}
if (currentPhase == ArchitecturePhase.None)
{
logger.Debug("Architecture destroy called but never initialized, cleaning up registered components");
await CleanupComponentsAsync();
return;
}
logger.Info("Starting architecture destruction");
enterPhase(ArchitecturePhase.Destroying);
await CleanupComponentsAsync();
await services.ModuleManager.DestroyAllAsync();
// Destroyed 广播依赖容器中的阶段监听器,必须在清空容器前完成。
enterPhase(ArchitecturePhase.Destroyed);
services.Container.Clear();
logger.Info("Architecture destruction completed");
}
/// <summary>
/// 逆序销毁当前已注册的所有可销毁组件。
/// 单个组件失败不会中断后续清理,避免在销毁阶段留下半清理状态。
/// </summary>
private async ValueTask CleanupComponentsAsync()
{
logger.Info($"Destroying {_disposables.Count} disposable components");
for (var i = _disposables.Count - 1; i >= 0; i--)
{
var component = _disposables[i];
try
{
logger.Debug($"Destroying component: {component.GetType().Name}");
if (component is IAsyncDestroyable asyncDestroyable)
{
await asyncDestroyable.DestroyAsync();
}
else if (component is IDestroyable destroyable)
{
destroyable.Destroy();
}
}
catch (Exception ex)
{
logger.Error($"Error destroying {component.GetType().Name}", ex);
}
}
_disposables.Clear();
_disposableSet.Clear();
}
}

View File

@ -27,11 +27,7 @@ internal sealed class ArchitectureLifecycle(
/// <returns>注册的钩子实例</returns> /// <returns>注册的钩子实例</returns>
public IArchitectureLifecycleHook RegisterLifecycleHook(IArchitectureLifecycleHook hook) public IArchitectureLifecycleHook RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{ {
if (CurrentPhase >= ArchitecturePhase.Ready && !configuration.ArchitectureProperties.AllowLateRegistration) return _phaseCoordinator.RegisterLifecycleHook(hook);
throw new InvalidOperationException(
"Cannot register lifecycle hook after architecture is Ready");
_lifecycleHooks.Add(hook);
return hook;
} }
#endregion #endregion
@ -44,31 +40,37 @@ internal sealed class ArchitectureLifecycle(
/// <param name="component">要注册的组件</param> /// <param name="component">要注册的组件</param>
public void RegisterLifecycleComponent(object component) public void RegisterLifecycleComponent(object component)
{ {
// 处理初始化
if (component is IInitializable initializable) if (component is IInitializable initializable)
{ {
if (!_initialized) if (_initialized)
{ {
// 原子去重HashSet.Add 返回 true 表示添加成功(之前不存在) if (!configuration.ArchitectureProperties.AllowLateRegistration)
if (_pendingInitializableSet.Add(initializable)) throw new InvalidOperationException("Cannot initialize component after Architecture is Ready");
{
_pendingInitializableList.Add(initializable); InitializeLateRegisteredComponent(initializable);
logger.Trace($"Added {component.GetType().Name} to pending initialization queue");
}
} }
else
else if (_pendingInitializableSet.Add(initializable))
{ {
throw new InvalidOperationException( _pendingInitializableList.Add(initializable);
"Cannot initialize component after Architecture is Ready"); logger.Trace($"Added {component.GetType().Name} to pending initialization queue");
} }
} }
// 处理销毁(支持 IDestroyable 或 IAsyncDestroyable _disposer.Register(component);
if (component is not (IDestroyable or IAsyncDestroyable)) return; }
// 原子去重HashSet.Add 返回 true 表示添加成功(之前不存在)
if (!_disposableSet.Add(component)) return; #endregion
_disposables.Add(component);
logger.Trace($"Registered {component.GetType().Name} for destruction"); #region Phase Management
/// <summary>
/// 进入指定的架构阶段,并执行相应的生命周期管理操作
/// </summary>
/// <param name="next">要进入的下一个架构阶段</param>
public void EnterPhase(ArchitecturePhase next)
{
_phaseCoordinator.EnterPhase(next);
} }
#endregion #endregion
@ -88,19 +90,15 @@ internal sealed class ArchitectureLifecycle(
private readonly List<IInitializable> _pendingInitializableList = []; private readonly List<IInitializable> _pendingInitializableList = [];
/// <summary> /// <summary>
/// 可销毁组件的去重集合(支持 IDestroyable 和 IAsyncDestroyable /// 架构阶段协调器
/// </summary> /// </summary>
private readonly HashSet<object> _disposableSet = []; private readonly ArchitecturePhaseCoordinator _phaseCoordinator =
new(architecture, configuration, services, logger);
/// <summary> /// <summary>
/// 存储所有需要销毁的组件(统一管理,保持注册逆序销毁) /// 架构销毁协调器
/// </summary> /// </summary>
private readonly List<object> _disposables = []; private readonly ArchitectureDisposer _disposer = new(services, logger);
/// <summary>
/// 生命周期感知对象列表
/// </summary>
private readonly List<IArchitectureLifecycleHook> _lifecycleHooks = [];
/// <summary> /// <summary>
/// 标记架构是否已初始化完成 /// 标记架构是否已初始化完成
@ -114,7 +112,7 @@ internal sealed class ArchitectureLifecycle(
/// <summary> /// <summary>
/// 当前架构的阶段 /// 当前架构的阶段
/// </summary> /// </summary>
public ArchitecturePhase CurrentPhase { get; private set; } public ArchitecturePhase CurrentPhase => _phaseCoordinator.CurrentPhase;
/// <summary> /// <summary>
/// 获取一个布尔值,指示当前架构是否处于就绪状态 /// 获取一个布尔值,指示当前架构是否处于就绪状态
@ -129,87 +127,10 @@ internal sealed class ArchitectureLifecycle(
/// <summary> /// <summary>
/// 阶段变更事件(用于测试和扩展) /// 阶段变更事件(用于测试和扩展)
/// </summary> /// </summary>
public event Action<ArchitecturePhase>? PhaseChanged; public event Action<ArchitecturePhase>? PhaseChanged
#endregion
#region Phase Management
/// <summary>
/// 进入指定的架构阶段,并执行相应的生命周期管理操作
/// </summary>
/// <param name="next">要进入的下一个架构阶段</param>
/// <exception cref="InvalidOperationException">当阶段转换不被允许时抛出异常</exception>
public void EnterPhase(ArchitecturePhase next)
{ {
// 验证阶段转换 add => _phaseCoordinator.PhaseChanged += value;
ValidatePhaseTransition(next); remove => _phaseCoordinator.PhaseChanged -= value;
// 执行阶段转换
var previousPhase = CurrentPhase;
CurrentPhase = next;
if (previousPhase != next)
logger.Info($"Architecture phase changed: {previousPhase} -> {next}");
// 通知阶段变更
NotifyPhase(next);
NotifyPhaseAwareObjects(next);
// 触发阶段变更事件(用于测试和扩展)
PhaseChanged?.Invoke(next);
}
/// <summary>
/// 验证阶段转换是否合法
/// </summary>
/// <param name="next">目标阶段</param>
/// <exception cref="InvalidOperationException">当阶段转换不合法时抛出</exception>
private void ValidatePhaseTransition(ArchitecturePhase next)
{
// 不需要严格验证,直接返回
if (!configuration.ArchitectureProperties.StrictPhaseValidation)
return;
// FailedInitialization 可以从任何阶段转换,直接返回
if (next == ArchitecturePhase.FailedInitialization)
return;
// 检查转换是否在允许列表中
if (ArchitectureConstants.PhaseTransitions.TryGetValue(CurrentPhase, out var allowed) &&
allowed.Contains(next))
return;
// 转换不合法,抛出异常
var errorMsg = $"Invalid phase transition: {CurrentPhase} -> {next}";
logger.Fatal(errorMsg);
throw new InvalidOperationException(errorMsg);
}
/// <summary>
/// 通知所有架构阶段感知对象阶段变更
/// </summary>
/// <param name="phase">新阶段</param>
private void NotifyPhaseAwareObjects(ArchitecturePhase phase)
{
foreach (var obj in services.Container.GetAll<IArchitecturePhaseListener>())
{
logger.Trace($"Notifying phase-aware object {obj.GetType().Name} of phase change to {phase}");
obj.OnArchitecturePhase(phase);
}
}
/// <summary>
/// 通知所有生命周期钩子当前阶段变更
/// </summary>
/// <param name="phase">当前架构阶段</param>
private void NotifyPhase(ArchitecturePhase phase)
{
foreach (var hook in _lifecycleHooks)
{
hook.OnPhase(phase, architecture);
logger.Trace($"Notifying lifecycle hook {hook.GetType().Name} of phase {phase}");
}
} }
#endregion #endregion
@ -302,6 +223,18 @@ internal sealed class ArchitectureLifecycle(
component.Initialize(); component.Initialize();
} }
/// <summary>
/// 立即初始化在常规初始化批次完成后新增的组件。
/// 当启用 <c>AllowLateRegistration</c> 时,生命周期层需要和注册层保持一致,
/// 让新增组件在注册当下完成同步初始化,而不是停留在未初始化状态。
/// </summary>
/// <param name="component">后注册的可初始化组件。</param>
private void InitializeLateRegisteredComponent(IInitializable component)
{
logger.Debug($"Initializing late-registered component: {component.GetType().Name}");
component.Initialize();
}
#endregion #endregion
#region Destruction #region Destruction
@ -311,72 +244,7 @@ internal sealed class ArchitectureLifecycle(
/// </summary> /// </summary>
public async ValueTask DestroyAsync() public async ValueTask DestroyAsync()
{ {
// 检查当前阶段,如果已经处于销毁或已销毁状态则直接返回 await _disposer.DestroyAsync(CurrentPhase, EnterPhase);
if (CurrentPhase >= ArchitecturePhase.Destroying)
{
logger.Warn("Architecture destroy called but already in destroying/destroyed state");
return;
}
// 如果从未初始化None 阶段),只清理已注册的组件,不进行阶段转换
if (CurrentPhase == ArchitecturePhase.None)
{
logger.Debug("Architecture destroy called but never initialized, cleaning up registered components");
await CleanupComponentsAsync();
return;
}
// 进入销毁阶段
logger.Info("Starting architecture destruction");
EnterPhase(ArchitecturePhase.Destroying);
// 清理所有组件
await CleanupComponentsAsync();
// 销毁服务模块
await services.ModuleManager.DestroyAllAsync();
services.Container.Clear();
// 进入已销毁阶段
EnterPhase(ArchitecturePhase.Destroyed);
logger.Info("Architecture destruction completed");
}
/// <summary>
/// 清理所有已注册的可销毁组件
/// </summary>
private async ValueTask CleanupComponentsAsync()
{
// 销毁所有实现了 IAsyncDestroyable 或 IDestroyable 的组件(按注册逆序销毁)
logger.Info($"Destroying {_disposables.Count} disposable components");
for (var i = _disposables.Count - 1; i >= 0; i--)
{
var component = _disposables[i];
try
{
logger.Debug($"Destroying component: {component.GetType().Name}");
// 优先使用异步销毁
if (component is IAsyncDestroyable asyncDestroyable)
{
await asyncDestroyable.DestroyAsync();
}
else if (component is IDestroyable destroyable)
{
destroyable.Destroy();
}
}
catch (Exception ex)
{
logger.Error($"Error destroying {component.GetType().Name}", ex);
// 继续销毁其他组件,不会因为一个组件失败而中断
}
}
_disposables.Clear();
_disposableSet.Clear();
} }
/// <summary> /// <summary>

View File

@ -14,7 +14,8 @@ internal sealed class ArchitectureModules(
{ {
/// <summary> /// <summary>
/// 注册中介行为管道 /// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑 /// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 支持开放泛型行为类型和针对单一请求的封闭行为类型。
/// </summary> /// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam> /// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class

View File

@ -0,0 +1,116 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Logging;
namespace GFramework.Core.Architectures;
/// <summary>
/// 负责架构阶段流转的验证与通知。
/// 该类型集中管理阶段值、生命周期钩子和阶段监听器,避免 <see cref="ArchitectureLifecycle" />
/// 同时承担阶段广播与组件初始化队列管理两类职责。
/// </summary>
internal sealed class ArchitecturePhaseCoordinator(
IArchitecture architecture,
IArchitectureConfiguration configuration,
IArchitectureServices services,
ILogger logger)
{
private readonly List<IArchitectureLifecycleHook> _lifecycleHooks = [];
/// <summary>
/// 获取当前架构阶段。
/// </summary>
public ArchitecturePhase CurrentPhase { get; private set; }
/// <summary>
/// 在架构阶段变更时触发。
/// 该事件用于测试和扩展场景,保持现有公共行为不变。
/// </summary>
public event Action<ArchitecturePhase>? PhaseChanged;
/// <summary>
/// 注册一个生命周期钩子。
/// 就绪后是否允许追加注册由架构配置控制,以保证阶段回调的一致性。
/// </summary>
/// <param name="hook">要注册的生命周期钩子。</param>
/// <returns>原样返回注册的钩子实例,便于链式调用或测试断言。</returns>
public IArchitectureLifecycleHook RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{
if (CurrentPhase >= ArchitecturePhase.Ready && !configuration.ArchitectureProperties.AllowLateRegistration)
throw new InvalidOperationException("Cannot register lifecycle hook after architecture is Ready");
_lifecycleHooks.Add(hook);
return hook;
}
/// <summary>
/// 进入指定阶段并广播给所有阶段消费者。
/// 顺序保持为“更新阶段值 → 生命周期钩子 → 容器中的阶段监听器 → 外部事件”,
/// 以兼容既有调用约定。
/// </summary>
/// <param name="next">目标阶段。</param>
public void EnterPhase(ArchitecturePhase next)
{
ValidatePhaseTransition(next);
var previousPhase = CurrentPhase;
CurrentPhase = next;
if (previousPhase != next)
logger.Info($"Architecture phase changed: {previousPhase} -> {next}");
NotifyLifecycleHooks(next);
NotifyPhaseListeners(next);
PhaseChanged?.Invoke(next);
}
/// <summary>
/// 根据配置验证阶段迁移是否合法。
/// 在关闭严格校验时直接放行,以兼容对阶段流转有特殊需求的宿主。
/// </summary>
/// <param name="next">目标阶段。</param>
/// <exception cref="InvalidOperationException">当迁移不在允许集合中时抛出。</exception>
private void ValidatePhaseTransition(ArchitecturePhase next)
{
if (!configuration.ArchitectureProperties.StrictPhaseValidation)
return;
if (next == ArchitecturePhase.FailedInitialization)
return;
if (ArchitectureConstants.PhaseTransitions.TryGetValue(CurrentPhase, out var allowed) && allowed.Contains(next))
return;
var errorMessage = $"Invalid phase transition: {CurrentPhase} -> {next}";
logger.Fatal(errorMessage);
throw new InvalidOperationException(errorMessage);
}
/// <summary>
/// 通知所有生命周期钩子当前阶段已变更。
/// 生命周期钩子通常承载注册表装配等框架扩展逻辑,因此优先于普通阶段监听器执行。
/// </summary>
/// <param name="phase">当前阶段。</param>
private void NotifyLifecycleHooks(ArchitecturePhase phase)
{
foreach (var hook in _lifecycleHooks)
{
hook.OnPhase(phase, architecture);
logger.Trace($"Notifying lifecycle hook {hook.GetType().Name} of phase {phase}");
}
}
/// <summary>
/// 通知容器中的阶段监听器当前阶段已变更。
/// 这些对象通常是系统、模型或工具本身,依赖容器解析保证通知范围与运行时实例一致。
/// </summary>
/// <param name="phase">当前阶段。</param>
private void NotifyPhaseListeners(ArchitecturePhase phase)
{
foreach (var listener in services.Container.GetAll<IArchitecturePhaseListener>())
{
logger.Trace($"Notifying phase-aware object {listener.GetType().Name} of phase change to {phase}");
listener.OnArchitecturePhase(phase);
}
}
}

View File

@ -7,6 +7,12 @@ namespace GFramework.Core.Coroutine;
/// </summary> /// </summary>
internal class CoroutineMetadata internal class CoroutineMetadata
{ {
/// <summary>
/// 协程所属调度器的执行阶段。
/// 该值用于诊断等待语义是否与当前宿主阶段匹配。
/// </summary>
public CoroutineExecutionStage ExecutionStage;
/// <summary> /// <summary>
/// 协程的分组标识符,用于批量管理协程 /// 协程的分组标识符,用于批量管理协程
/// </summary> /// </summary>

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,18 @@ namespace GFramework.Core.Coroutine;
/// </summary> /// </summary>
internal sealed class CoroutineSlot internal sealed class CoroutineSlot
{ {
/// <summary>
/// 由外部取消令牌创建的注册。
/// 调度器在协程结束时必须释放该注册,避免泄漏取消回调。
/// </summary>
public CancellationTokenRegistration CancellationRegistration;
/// <summary>
/// 创建该协程时传入的取消令牌。
/// 当协程启动子协程时,会把同一个取消令牌继续传递下去,以保持父子协程的取消语义一致。
/// </summary>
public CancellationToken CancellationToken;
/// <summary> /// <summary>
/// 协程枚举器,包含协程的执行逻辑 /// 协程枚举器,包含协程的执行逻辑
/// </summary> /// </summary>

View File

@ -0,0 +1,29 @@
using GFramework.Core.Abstractions.Coroutine;
namespace GFramework.Core.Coroutine;
/// <summary>
/// 表示某个活跃协程在调度器中的只读运行快照。
/// </summary>
/// <param name="Handle">协程句柄。</param>
/// <param name="State">当前协程状态。</param>
/// <param name="Priority">当前协程优先级。</param>
/// <param name="Tag">可选标签。</param>
/// <param name="Group">可选分组。</param>
/// <param name="StartTimeMs">协程启动时间,单位为毫秒。</param>
/// <param name="IsWaiting">当前是否正被等待指令阻塞。</param>
/// <param name="WaitingInstructionType">
/// 当前等待指令的具体类型。
/// 若协程当前未处于等待状态,则该值为 <see langword="null" />。
/// </param>
/// <param name="ExecutionStage">所属调度器的执行阶段。</param>
public readonly record struct CoroutineSnapshot(
CoroutineHandle Handle,
CoroutineState State,
CoroutinePriority Priority,
string? Tag,
string? Group,
double StartTimeMs,
bool IsWaiting,
Type? WaitingInstructionType,
CoroutineExecutionStage ExecutionStage);

View File

@ -311,7 +311,9 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// <summary> /// <summary>
/// 注册中介行为管道 /// 注册中介行为管道
/// 用于配置Mediator框架的行为拦截和处理逻辑 /// 用于配置Mediator框架的行为拦截和处理逻辑。
/// 同时支持开放泛型行为类型和已闭合的具体行为类型,
/// 以兼容通用行为和针对单一请求的专用行为两种注册方式。
/// </summary> /// </summary>
/// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam> /// <typeparam name="TBehavior">行为类型,必须是引用类型</typeparam>
public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class public void RegisterMediatorBehavior<TBehavior>() where TBehavior : class
@ -321,12 +323,35 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
{ {
ThrowIfFrozen(); ThrowIfFrozen();
GetServicesUnsafe.AddSingleton( var behaviorType = typeof(TBehavior);
typeof(IPipelineBehavior<,>),
typeof(TBehavior)
);
_logger.Debug($"Mediator behavior registered: {typeof(TBehavior).Name}"); if (behaviorType.IsGenericTypeDefinition)
{
GetServicesUnsafe.AddSingleton(typeof(IPipelineBehavior<,>), behaviorType);
}
else
{
var pipelineInterfaces = behaviorType
.GetInterfaces()
.Where(type => type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(IPipelineBehavior<,>))
.ToList();
if (pipelineInterfaces.Count == 0)
{
var errorMessage = $"{behaviorType.Name} does not implement IPipelineBehavior<,>";
_logger.Error(errorMessage);
throw new InvalidOperationException(errorMessage);
}
// 为每个已闭合的管道接口建立显式映射,支持针对特定请求/响应的专用行为。
foreach (var pipelineInterface in pipelineInterfaces)
{
GetServicesUnsafe.AddSingleton(pipelineInterface, behaviorType);
}
}
_logger.Debug($"Mediator behavior registered: {behaviorType.Name}");
} }
finally finally
{ {

View File

@ -1,57 +1,57 @@
using System; using System;
using System.Text; using System.Text;
using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Logging;
namespace GFramework.Core.Logging.Formatters; namespace GFramework.Core.Logging.Formatters;
/// <summary> /// <summary>
/// 默认日志格式化器,保持与现有格式兼容 /// 默认日志格式化器,保持与现有格式兼容
/// </summary> /// </summary>
public sealed class DefaultLogFormatter : ILogFormatter public sealed class DefaultLogFormatter : ILogFormatter
{ {
private static readonly string[] LevelStrings = private static readonly string[] LevelStrings =
[ [
"TRACE ", "TRACE ",
"DEBUG ", "DEBUG ",
"INFO ", "INFO ",
"WARNING", "WARNING",
"ERROR ", "ERROR ",
"FATAL " "FATAL "
]; ];
/// <summary> /// <summary>
/// 将日志条目格式化为默认格式 /// 将日志条目格式化为默认格式
/// </summary> /// </summary>
/// <param name="entry">日志条目</param> /// <param name="entry">日志条目</param>
/// <returns>格式化后的日志字符串</returns> /// <returns>格式化后的日志字符串</returns>
public string Format(LogEntry entry) public string Format(LogEntry entry)
{ {
var timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"); var timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff");
var levelStr = LevelStrings[(int)entry.Level]; var levelStr = LevelStrings[(int)entry.Level];
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append('[').Append(timestamp).Append("] ") sb.Append('[').Append(timestamp).Append("] ")
.Append(levelStr).Append(" [") .Append(levelStr).Append(" [")
.Append(entry.LoggerName).Append("] ") .Append(entry.LoggerName).Append("] ")
.Append(entry.Message); .Append(entry.Message);
// 添加结构化属性 // 添加结构化属性
var properties = entry.GetAllProperties(); var properties = entry.GetAllProperties();
if (properties.Count > 0) if (properties.Count > 0)
{ {
sb.Append(" |"); sb.Append(" |");
foreach (var prop in properties) foreach (var prop in properties)
{ {
sb.Append(' ').Append(prop.Key).Append('=').Append(prop.Value); sb.Append(' ').Append(prop.Key).Append('=').Append(prop.Value);
} }
} }
// 添加异常信息 // 添加异常信息
if (entry.Exception != null) if (entry.Exception != null)
{ {
sb.Append(global::System.Environment.NewLine).Append(entry.Exception); sb.Append(global::System.Environment.NewLine).Append(entry.Exception);
} }
return sb.ToString(); return sb.ToString();
} }
} }

View File

@ -1,177 +1,177 @@
# GFramework.Ecs.Arch - C# 标准集成方式 # GFramework.Ecs.Arch - C# 标准集成方式
## 为什么不使用 ModuleInitializer ## 为什么不使用 ModuleInitializer
`ModuleInitializer` 在 C# 中主要用于: `ModuleInitializer` 在 C# 中主要用于:
1. **应用程序代码** - 初始化应用程序级别的状态 1. **应用程序代码** - 初始化应用程序级别的状态
2. **高级源生成器** - 编译时代码生成 2. **高级源生成器** - 编译时代码生成
对于**库Library**来说,使用 `ModuleInitializer` 有以下问题: 对于**库Library**来说,使用 `ModuleInitializer` 有以下问题:
- ❌ 不符合 .NET 生态习惯 - ❌ 不符合 .NET 生态习惯
- ❌ 缺乏显式控制 - ❌ 缺乏显式控制
- ❌ 难以测试和调试 - ❌ 难以测试和调试
- ❌ 可能导致意外的副作用 - ❌ 可能导致意外的副作用
- ❌ 违反"显式优于隐式"原则 - ❌ 违反"显式优于隐式"原则
## C# 中的标准做法 ## C# 中的标准做法
### 1. 依赖注入扩展方法(推荐) ### 1. 依赖注入扩展方法(推荐)
这是 .NET 生态中最标准的做法,类似于: 这是 .NET 生态中最标准的做法,类似于:
- ASP.NET Core: `services.AddMvc()` - ASP.NET Core: `services.AddMvc()`
- Entity Framework: `services.AddDbContext()` - Entity Framework: `services.AddDbContext()`
- SignalR: `services.AddSignalR()` - SignalR: `services.AddSignalR()`
**优点:** **优点:**
- ✅ 符合 .NET 生态习惯 - ✅ 符合 .NET 生态习惯
- ✅ 显式、可控 - ✅ 显式、可控
- ✅ 易于测试 - ✅ 易于测试
- ✅ 支持配置 - ✅ 支持配置
- ✅ 支持链式调用 - ✅ 支持链式调用
### 2. 使用方式 ### 2. 使用方式
#### 基本用法 #### 基本用法
```csharp ```csharp
public class MyArchitecture : Architecture public class MyArchitecture : Architecture
{ {
protected override void OnConfigure() protected override void OnConfigure()
{ {
// 显式添加 Arch ECS 支持 // 显式添加 Arch ECS 支持
Services.AddArch(); Services.AddArch();
} }
} }
``` ```
#### 带配置的用法 #### 带配置的用法
```csharp ```csharp
public class MyArchitecture : Architecture public class MyArchitecture : Architecture
{ {
protected override void OnConfigure() protected override void OnConfigure()
{ {
Services.AddArch(options => Services.AddArch(options =>
{ {
options.WorldCapacity = 2000; options.WorldCapacity = 2000;
options.EnableStatistics = true; options.EnableStatistics = true;
options.Priority = 50; options.Priority = 50;
}); });
} }
} }
``` ```
#### 容器级别的用法 #### 容器级别的用法
```csharp ```csharp
var container = new MicrosoftDiContainer(); var container = new MicrosoftDiContainer();
container.AddArch(options => container.AddArch(options =>
{ {
options.WorldCapacity = 1000; options.WorldCapacity = 1000;
}); });
``` ```
### 3. 对比其他方案 ### 3. 对比其他方案
#### Spring Boot StarterJava #### Spring Boot StarterJava
```java ```java
// Spring Boot 使用自动配置 // Spring Boot 使用自动配置
@SpringBootApplication @SpringBootApplication
public class Application { public class Application {
// 自动扫描并加载 starter // 自动扫描并加载 starter
} }
``` ```
#### ASP.NET CoreC# #### ASP.NET CoreC#
```csharp ```csharp
// .NET 使用显式注册 // .NET 使用显式注册
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddSwagger(); builder.Services.AddSwagger();
``` ```
**C# 更倾向于显式注册**,因为: **C# 更倾向于显式注册**,因为:
1. 更清晰的依赖关系 1. 更清晰的依赖关系
2. 更好的 IDE 支持 2. 更好的 IDE 支持
3. 更容易调试和测试 3. 更容易调试和测试
4. 避免"魔法"行为 4. 避免"魔法"行为
### 4. 其他常见模式 ### 4. 其他常见模式
#### 4.1 Builder 模式 #### 4.1 Builder 模式
```csharp ```csharp
var architecture = new ArchitectureBuilder() var architecture = new ArchitectureBuilder()
.AddArch() .AddArch()
.AddLogging() .AddLogging()
.Build(); .Build();
``` ```
#### 4.2 Options 模式 #### 4.2 Options 模式
```csharp ```csharp
services.Configure<ArchOptions>(options => services.Configure<ArchOptions>(options =>
{ {
options.WorldCapacity = 2000; options.WorldCapacity = 2000;
}); });
``` ```
#### 4.3 静态工厂方法 #### 4.3 静态工厂方法
```csharp ```csharp
var module = ArchEcsModule.Create(options => var module = ArchEcsModule.Create(options =>
{ {
options.WorldCapacity = 1000; options.WorldCapacity = 1000;
}); });
``` ```
## 迁移指南 ## 迁移指南
### 从 ModuleInitializer 迁移 ### 从 ModuleInitializer 迁移
**之前(自动注册):** **之前(自动注册):**
```csharp ```csharp
// 只需引入包,自动注册 // 只需引入包,自动注册
// 无需任何代码 // 无需任何代码
``` ```
**现在(显式注册):** **现在(显式注册):**
```csharp ```csharp
public class MyArchitecture : Architecture public class MyArchitecture : Architecture
{ {
protected override void OnConfigure() protected override void OnConfigure()
{ {
Services.AddArch(); Services.AddArch();
} }
} }
``` ```
### 优势 ### 优势
1. **更清晰** - 一眼就能看出使用了哪些模块 1. **更清晰** - 一眼就能看出使用了哪些模块
2. **更可控** - 可以决定何时、如何注册 2. **更可控** - 可以决定何时、如何注册
3. **更灵活** - 可以传递配置参数 3. **更灵活** - 可以传递配置参数
4. **更标准** - 符合 .NET 生态习惯 4. **更标准** - 符合 .NET 生态习惯
## 总结 ## 总结
C# 生态更倾向于**显式优于隐式**的设计哲学,因此: C# 生态更倾向于**显式优于隐式**的设计哲学,因此:
- ✅ **推荐**:使用扩展方法 `AddArch()` - ✅ **推荐**:使用扩展方法 `AddArch()`
- ❌ **不推荐**:使用 `ModuleInitializer` - ❌ **不推荐**:使用 `ModuleInitializer`
这样的设计: 这样的设计:
1. 符合 .NET 生态习惯 1. 符合 .NET 生态习惯
2. 提供更好的开发体验 2. 提供更好的开发体验
3. 更容易理解和维护 3. 更容易理解和维护
4. 避免 CA2255 警告 4. 避免 CA2255 警告

View File

@ -1,314 +1,314 @@
# GFramework.Ecs.Arch - 从自动注册到显式注册的迁移 # GFramework.Ecs.Arch - 从自动注册到显式注册的迁移
## 变更原因 ## 变更原因
### 问题CA2255 警告 ### 问题CA2255 警告
``` ```
warning CA2255: The 'ModuleInitializer' attribute is only intended to be used warning CA2255: The 'ModuleInitializer' attribute is only intended to be used
in application code or advanced source generator scenarios in application code or advanced source generator scenarios
``` ```
### 为什么 ModuleInitializer 不适合库? ### 为什么 ModuleInitializer 不适合库?
在 C# 生态中,`ModuleInitializer` 主要用于: 在 C# 生态中,`ModuleInitializer` 主要用于:
1. **应用程序代码** - 初始化应用程序级别的状态 1. **应用程序代码** - 初始化应用程序级别的状态
2. **高级源生成器** - 编译时代码生成 2. **高级源生成器** - 编译时代码生成
对于**库Library**来说,使用 `ModuleInitializer` 有以下问题: 对于**库Library**来说,使用 `ModuleInitializer` 有以下问题:
- ❌ 不符合 .NET 生态习惯 - ❌ 不符合 .NET 生态习惯
- ❌ 缺乏显式控制 - ❌ 缺乏显式控制
- ❌ 难以测试和调试 - ❌ 难以测试和调试
- ❌ 可能导致意外的副作用 - ❌ 可能导致意外的副作用
- ❌ 违反"显式优于隐式"原则 - ❌ 违反"显式优于隐式"原则
- ❌ 触发 CA2255 警告 - ❌ 触发 CA2255 警告
## C# 标准做法对比 ## C# 标准做法对比
### Spring BootJava- 自动配置 ### Spring BootJava- 自动配置
```java ```java
@SpringBootApplication @SpringBootApplication
public class Application { public class Application {
// 自动扫描并加载 starter // 自动扫描并加载 starter
} }
``` ```
### ASP.NET CoreC#- 显式注册 ### ASP.NET CoreC#- 显式注册
```csharp ```csharp
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); // 显式注册 builder.Services.AddControllers(); // 显式注册
builder.Services.AddSwagger(); // 显式注册 builder.Services.AddSwagger(); // 显式注册
``` ```
**C# 更倾向于显式注册**,因为: **C# 更倾向于显式注册**,因为:
1. ✅ 更清晰的依赖关系 1. ✅ 更清晰的依赖关系
2. ✅ 更好的 IDE 支持 2. ✅ 更好的 IDE 支持
3. ✅ 更容易调试和测试 3. ✅ 更容易调试和测试
4. ✅ 避免"魔法"行为 4. ✅ 避免"魔法"行为
## 变更内容 ## 变更内容
### 1. 移除 ModuleInitializer ### 1. 移除 ModuleInitializer
**之前:** **之前:**
```csharp ```csharp
// ArchModuleInitializer.cs // ArchModuleInitializer.cs
[ModuleInitializer] [ModuleInitializer]
public static void Initialize() public static void Initialize()
{ {
ArchitectureModuleRegistry.Register(() => new ArchEcsModule(enabled: true)); ArchitectureModuleRegistry.Register(() => new ArchEcsModule(enabled: true));
} }
``` ```
**现在:** **现在:**
```csharp ```csharp
// 文件已删除 // 文件已删除
``` ```
### 2. 新增扩展方法 ### 2. 新增扩展方法
**新增:** **新增:**
```csharp ```csharp
// ArchExtensions.cs // ArchExtensions.cs
public static class ArchExtensions public static class ArchExtensions
{ {
/// <summary> /// <summary>
/// 添加 Arch ECS 支持到架构服务中 /// 添加 Arch ECS 支持到架构服务中
/// </summary> /// </summary>
public static IArchitectureServices AddArch( public static IArchitectureServices AddArch(
this IArchitectureServices services, this IArchitectureServices services,
Action<ArchOptions>? configure = null) Action<ArchOptions>? configure = null)
{ {
var options = new ArchOptions(); var options = new ArchOptions();
configure?.Invoke(options); configure?.Invoke(options);
ArchitectureModuleRegistry.Register(() => new ArchEcsModule(enabled: true)); ArchitectureModuleRegistry.Register(() => new ArchEcsModule(enabled: true));
return services; return services;
} }
/// <summary> /// <summary>
/// 添加 Arch ECS 支持到 IoC 容器中 /// 添加 Arch ECS 支持到 IoC 容器中
/// </summary> /// </summary>
public static IIocContainer AddArch( public static IIocContainer AddArch(
this IIocContainer container, this IIocContainer container,
Action<ArchOptions>? configure = null) Action<ArchOptions>? configure = null)
{ {
var options = new ArchOptions(); var options = new ArchOptions();
configure?.Invoke(options); configure?.Invoke(options);
ArchitectureModuleRegistry.Register(() => new ArchEcsModule(enabled: true)); ArchitectureModuleRegistry.Register(() => new ArchEcsModule(enabled: true));
return container; return container;
} }
} }
``` ```
### 3. 更新使用方式 ### 3. 更新使用方式
#### 之前(自动注册) #### 之前(自动注册)
```csharp ```csharp
// 只需引入包,自动注册 // 只需引入包,自动注册
// 无需任何代码 // 无需任何代码
``` ```
#### 现在(显式注册) #### 现在(显式注册)
```csharp ```csharp
public class MyArchitecture : Architecture public class MyArchitecture : Architecture
{ {
protected override void OnConfigure() protected override void OnConfigure()
{ {
// 显式注册 // 显式注册
Services.AddArch(); Services.AddArch();
} }
} }
``` ```
#### 带配置 #### 带配置
```csharp ```csharp
public class MyArchitecture : Architecture public class MyArchitecture : Architecture
{ {
protected override void OnConfigure() protected override void OnConfigure()
{ {
Services.AddArch(options => Services.AddArch(options =>
{ {
options.WorldCapacity = 2000; options.WorldCapacity = 2000;
options.EnableStatistics = true; options.EnableStatistics = true;
options.Priority = 50; options.Priority = 50;
}); });
} }
} }
``` ```
### 4. 更新测试 ### 4. 更新测试
**之前:** **之前:**
```csharp ```csharp
[Test] [Test]
public void ArchEcsModule_Should_Be_Auto_Registered() public void ArchEcsModule_Should_Be_Auto_Registered()
{ {
// 手动触发模块初始化器 // 手动触发模块初始化器
ArchModuleInitializer.Initialize(); ArchModuleInitializer.Initialize();
var services = new ArchitectureServices(); var services = new ArchitectureServices();
services.ModuleManager.RegisterBuiltInModules(...); services.ModuleManager.RegisterBuiltInModules(...);
// ... // ...
} }
``` ```
**现在:** **现在:**
```csharp ```csharp
[Test] [Test]
public void ArchEcsModule_Should_Be_Explicitly_Registered() public void ArchEcsModule_Should_Be_Explicitly_Registered()
{ {
var services = new ArchitectureServices(); var services = new ArchitectureServices();
// 显式注册 // 显式注册
services.AddArch(); services.AddArch();
services.ModuleManager.RegisterBuiltInModules(...); services.ModuleManager.RegisterBuiltInModules(...);
// ... // ...
} }
``` ```
## 优势对比 ## 优势对比
### 自动注册ModuleInitializer ### 自动注册ModuleInitializer
- ❌ 触发 CA2255 警告 - ❌ 触发 CA2255 警告
- ❌ 不符合 .NET 生态习惯 - ❌ 不符合 .NET 生态习惯
- ❌ 缺乏显式控制 - ❌ 缺乏显式控制
- ❌ 难以测试 - ❌ 难以测试
- ❌ "魔法"行为 - ❌ "魔法"行为
### 显式注册(扩展方法) ### 显式注册(扩展方法)
- ✅ 无警告 - ✅ 无警告
- ✅ 符合 .NET 生态习惯 - ✅ 符合 .NET 生态习惯
- ✅ 显式、可控 - ✅ 显式、可控
- ✅ 易于测试 - ✅ 易于测试
- ✅ 支持配置 - ✅ 支持配置
- ✅ 支持链式调用 - ✅ 支持链式调用
- ✅ 更好的 IDE 支持 - ✅ 更好的 IDE 支持
## 迁移指南 ## 迁移指南
### 对于现有用户 ### 对于现有用户
如果你之前使用自动注册方式,需要进行以下更改: 如果你之前使用自动注册方式,需要进行以下更改:
**步骤 1更新包** **步骤 1更新包**
```bash ```bash
dotnet update package GeWuYou.GFramework.Ecs.Arch dotnet update package GeWuYou.GFramework.Ecs.Arch
``` ```
**步骤 2添加显式注册** **步骤 2添加显式注册**
```csharp ```csharp
public class MyArchitecture : Architecture public class MyArchitecture : Architecture
{ {
protected override void OnConfigure() protected override void OnConfigure()
{ {
// 添加这一行 // 添加这一行
Services.AddArch(); Services.AddArch();
} }
} }
``` ```
**步骤 3可选添加配置** **步骤 3可选添加配置**
```csharp ```csharp
Services.AddArch(options => Services.AddArch(options =>
{ {
options.WorldCapacity = 2000; options.WorldCapacity = 2000;
options.EnableStatistics = true; options.EnableStatistics = true;
}); });
``` ```
## 验证结果 ## 验证结果
### 构建验证 ✅ ### 构建验证 ✅
```bash ```bash
dotnet build GFramework.sln dotnet build GFramework.sln
# Build succeeded. 39 Warning(s), 0 Error(s) # Build succeeded. 39 Warning(s), 0 Error(s)
# 无 CA2255 警告 # 无 CA2255 警告
``` ```
### 测试验证 ✅ ### 测试验证 ✅
```bash ```bash
dotnet test --filter "ExplicitRegistrationTests" dotnet test --filter "ExplicitRegistrationTests"
# Pas 4, Failed: 0, Total: 4 # Pas 4, Failed: 0, Total: 4
``` ```
**测试用例:** **测试用例:**
1. ✅ `ArchEcsModule_Should_Be_Explicitly_Registered` - 验证显式注册 1. ✅ `ArchEcsModule_Should_Be_Explicitly_Registered` - 验证显式注册
2. ✅ `World_Should_Be_Registered_In_Container` - 验证 World 注册 2. ✅ `World_Should_Be_Registered_In_Container` - 验证 World 注册
3. ✅ `AddArch_Should_Accept_Configuration` - 验证配置支持 3. ✅ `AddArch_Should_Accept_Configuration` - 验证配置支持
4. ✅ `Container_AddArch_Should_Work` - 验证容器级别注册 4. ✅ `Container_AddArch_Should_Work` - 验证容器级别注册
## 参考资料 ## 参考资料
### .NET 生态中的类似实现 ### .NET 生态中的类似实现
1. **ASP.NET Core** 1. **ASP.NET Core**
```csharp ```csharp
services.AddMvc(); services.AddMvc();
services.AddControllers(); services.AddControllers();
``` ```
2. **Entity Framework Core** 2. **Entity Framework Core**
```csharp ```csharp
services.AddDbContext<MyContext>(); services.AddDbContext<MyContext>();
``` ```
3. **SignalR** 3. **SignalR**
```csharp ```csharp
services.AddSignalR(); services.AddSignalR();
``` ```
4. **Swagger** 4. **Swagger**
```csharp ```csharp
services.AddSwaggerGen(); services.AddSwaggerGen();
``` ```
### 相关文档 ### 相关文档
-N_PATTERN.md](INTEGRATION_PATTERN.md) - 集成模式详解 -N_PATTERN.md](INTEGRATION_PATTERN.md) - 集成模式详解
- [README.md](README.md) - 使用指南 - [README.md](README.md) - 使用指南
- [CA2255 规则说明](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2255) - [CA2255 规则说明](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2255)
## 总结 ## 总结
通过从 `ModuleInitializer` 迁移到显式注册扩展方法: 通过从 `ModuleInitializer` 迁移到显式注册扩展方法:
1. ✅ **消除警告** - 不再触发 CA2255 1. ✅ **消除警告** - 不再触发 CA2255
2. ✅ **符合习惯** - 遵循 .NET 生态标准 2. ✅ **符合习惯** - 遵循 .NET 生态标准
3. ✅ **更好控制** - 显式、可配置 3. ✅ **更好控制** - 显式、可配置
4. ✅ **易于测试** - 清晰的测试边界 4. ✅ **易于测试** - 清晰的测试边界
5. ✅ **更好体验** - IDE 支持、链式调用 5. ✅ **更好体验** - IDE 支持、链式调用
这是一个**破坏性变更**,但带来了更好的开发体验和更符合 .NET 生态的设计。 这是一个**破坏性变更**,但带来了更好的开发体验和更符合 .NET 生态的设计。
--- ---
**变更日期:** 2026-03-08 **变更日期:** 2026-03-08
**影响范围:** 所有使用 GFramework.Ecs.Arch 的项目 **影响范围:** 所有使用 GFramework.Ecs.Arch 的项目
**迁移难度:** 低(只需添加一行代码) **迁移难度:** 低(只需添加一行代码)

View File

@ -14,22 +14,47 @@
namespace GFramework.Game.Abstractions.Data; namespace GFramework.Game.Abstractions.Data;
/// <summary> /// <summary>
/// 数据仓库配置选项 /// 数据仓库配置选项
/// </summary> /// </summary>
/// <remarks>
/// 该选项描述的是仓库层的公开行为契约,而不是某一种固定的落盘格式。
/// 因此不同实现可以分别使用“每项单文件”或“统一聚合文件”存储,只要对外遵守同一套备份与事件语义:
/// <list type="bullet">
/// <item>
/// <description>
/// <see cref="AutoBackup" /> 在覆盖已有持久化数据前保留上一份可恢复快照。对于聚合型仓库,备份粒度是整个统一文件。
/// </description>
/// </item>
/// <item>
/// <description>
/// <see cref="EnableEvents" /> 仅控制公开仓库操作产生的事件;内部缓存预热、迁移回写或批量保存中的子步骤不会额外发出单项事件。
/// </description>
/// </item>
/// </list>
/// </remarks>
public class DataRepositoryOptions public class DataRepositoryOptions
{ {
/// <summary> /// <summary>
/// 存储基础路径(如 "user://data/" /// 获取或设置仓库使用的基础存储路径。
/// </summary> /// </summary>
/// <remarks>
/// 具体实现会在该路径下组织自己的键空间。调用方应将其视为仓库级根目录,而不是具体文件名。
/// </remarks>
public string BasePath { get; set; } = ""; public string BasePath { get; set; } = "";
/// <summary> /// <summary>
/// 是否在保存时自动备份 /// 获取或设置是否在覆盖已有持久化数据前自动创建备份。
/// </summary> /// </summary>
/// <remarks>
/// 该选项只影响覆盖写入;首次写入不会生成备份。聚合型仓库会为统一文件创建单份备份,而不是为内部 section 分别备份。
/// </remarks>
public bool AutoBackup { get; set; } = false; public bool AutoBackup { get; set; } = false;
/// <summary> /// <summary>
/// 是否启用加载/保存事件 /// 获取或设置是否启用仓库层加载、保存、删除与批量保存事件。
/// </summary> /// </summary>
/// <remarks>
/// 当该值为 <see langword="true" /> 时,<c>SaveAllAsync</c> 只会发出批量事件,不会重复发出每个条目的单项保存事件。
/// </remarks>
public bool EnableEvents { get; set; } = true; public bool EnableEvents { get; set; } = true;
} }

View File

@ -0,0 +1,44 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
namespace GFramework.Game.Abstractions.Data;
/// <summary>
/// 定义存档数据迁移接口,用于将旧版本存档升级到较新的版本。
/// </summary>
/// <typeparam name="TSaveData">
/// 存档数据类型。该类型通常需要实现 <see cref="IVersionedData" />
/// 以便仓库在加载时判断当前版本并串联迁移链。
/// </typeparam>
public interface ISaveMigration<TSaveData>
where TSaveData : class, IData
{
/// <summary>
/// 获取迁移前的版本号。
/// </summary>
int FromVersion { get; }
/// <summary>
/// 获取迁移后的目标版本号。
/// </summary>
int ToVersion { get; }
/// <summary>
/// 将旧版本存档转换为新版本存档。
/// </summary>
/// <param name="oldData">待升级的旧版本存档数据。</param>
/// <returns>迁移完成后的存档数据。</returns>
TSaveData Migrate(TSaveData oldData);
}

View File

@ -11,6 +11,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Utility; using GFramework.Core.Abstractions.Utility;
namespace GFramework.Game.Abstractions.Data; namespace GFramework.Game.Abstractions.Data;
@ -22,6 +25,20 @@ namespace GFramework.Game.Abstractions.Data;
public interface ISaveRepository<TSaveData> : IUtility public interface ISaveRepository<TSaveData> : IUtility
where TSaveData : class, IData, new() where TSaveData : class, IData, new()
{ {
/// <summary>
/// 注册存档迁移器。
/// </summary>
/// <param name="migration">
/// 负责将某个旧版本存档升级到新版本的迁移器。
/// 仅当 <typeparamref name="TSaveData" /> 实现 <see cref="IVersionedData" /> 时该功能才有效。
/// </param>
/// <returns>当前存档仓库实例,便于链式注册多个迁移器。</returns>
/// <exception cref="ArgumentNullException"><paramref name="migration" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// <typeparamref name="TSaveData" /> 未实现 <see cref="IVersionedData" />,因此无法建立版本迁移管线。
/// </exception>
ISaveRepository<TSaveData> RegisterMigration(ISaveMigration<TSaveData> migration);
/// <summary> /// <summary>
/// 检查指定槽位是否存在存档 /// 检查指定槽位是否存在存档
/// </summary> /// </summary>

View File

@ -1,20 +1,20 @@
// IsExternalInit.cs // IsExternalInit.cs
// This type is required to support init-only setters and record types // This type is required to support init-only setters and record types
// when targeting netstandard2.0 or older frameworks. // when targeting netstandard2.0 or older frameworks.
#if !NET5_0_OR_GREATER #if !NET5_0_OR_GREATER
using System.ComponentModel; using System.ComponentModel;
// ReSharper disable CheckNamespace // ReSharper disable CheckNamespace
namespace System.Runtime.CompilerServices; namespace System.Runtime.CompilerServices;
/// <summary> /// <summary>
/// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。 /// 提供一个占位符类型,用于支持 C# 9.0 的 init 访问器功能。
/// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。 /// 该类型在 .NET 5.0 及更高版本中已内置,因此仅在较低版本的 .NET 中定义。
/// </summary> /// </summary>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit internal static class IsExternalInit
{ {
} }
#endif #endif

View File

@ -0,0 +1,155 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GFramework.Core.Architectures;
using GFramework.Game.Config;
using GFramework.Game.Config.Generated;
using NUnit.Framework;
namespace GFramework.Game.Tests.Config;
/// <summary>
/// 验证在 <see cref="Architecture" /> 初始化流程中可以注册配置注册表、执行加载并通过生成的表访问器读取数据。
/// </summary>
[TestFixture]
public class ArchitectureConfigIntegrationTests
{
/// <summary>
/// 架构初始化期间,通过 <see cref="YamlConfigLoader" /> 注册生成表,
/// 并将 <see cref="ConfigRegistry" /> 作为 utility 暴露给架构上下文读取。
/// </summary>
[Test]
public async Task ConfigLoaderCanRunDuringArchitectureInitialization()
{
var rootPath = CreateTempConfigRoot();
ConsumerArchitecture? architecture = null;
var initialized = false;
try
{
architecture = new ConsumerArchitecture(rootPath);
await architecture.InitializeAsync();
initialized = true;
var table = architecture.MonsterTable;
Assert.Multiple(() =>
{
Assert.That(table.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(table.Get(2).Hp, Is.EqualTo(30));
Assert.That(table.FindByFaction("dungeon").Select(static config => config.Name),
Is.EquivalentTo(new[] { "Slime", "Goblin" }));
Assert.That(architecture.Registry.TryGetMonsterTable(out var retrieved), Is.True);
Assert.That(retrieved, Is.Not.Null);
Assert.That(retrieved!.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(architecture.Context.GetUtility<ConfigRegistry>(), Is.SameAs(architecture.Registry));
});
}
finally
{
if (architecture is not null && initialized)
{
await architecture.DestroyAsync();
}
DeleteDirectoryIfExists(rootPath);
}
}
private static string CreateTempConfigRoot()
{
var rootPath = Path.Combine(Path.GetTempPath(), "GFramework.ConfigArchitecture", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(rootPath);
Directory.CreateDirectory(Path.Combine(rootPath, "schemas"));
Directory.CreateDirectory(Path.Combine(rootPath, "monster"));
File.WriteAllText(Path.Combine(rootPath, "schemas", "monster.schema.json"), MonsterSchemaJson);
File.WriteAllText(Path.Combine(rootPath, "monster", "slime.yaml"), MonsterSlimeYaml);
File.WriteAllText(Path.Combine(rootPath, "monster", "goblin.yaml"), MonsterGoblinYaml);
return rootPath;
}
/// <summary>
/// 最佳努力尝试删除临时目录。
/// </summary>
private static void DeleteDirectoryIfExists(string path)
{
if (!Directory.Exists(path))
{
return;
}
try
{
Directory.Delete(path, true);
}
catch (IOException)
{
// Ignored: cleanup is best effort and should not fail the test.
}
catch (UnauthorizedAccessException)
{
// Ignored: cleanup is best effort and can transiently fail when files are still being released.
}
}
private const string MonsterSchemaJson = @"{
""title"": ""Monster Config"",
""description"": ""Defines one monster entry for the generated consumer integration test."",
""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 integration test to validate generated non-unique queries.""
}
}
}";
private const string MonsterSlimeYaml =
"id: 1\nname: Slime\nhp: 10\nfaction: dungeon\n";
private const string MonsterGoblinYaml =
"id: 2\nname: Goblin\nhp: 30\nfaction: dungeon\n";
private sealed class ConsumerArchitecture : Architecture
{
private readonly string _configRoot;
public ConfigRegistry Registry { get; }
public MonsterTable MonsterTable { get; private set; } = null!;
public ConsumerArchitecture(string configRoot)
{
_configRoot = configRoot ?? throw new ArgumentNullException(nameof(configRoot));
Registry = new ConfigRegistry();
}
protected override void OnInitialize()
{
RegisterUtility(Registry);
var loader = new YamlConfigLoader(_configRoot)
.RegisterMonsterTable();
loader.LoadAsync(Registry).GetAwaiter().GetResult();
MonsterTable = Registry.GetMonsterTable();
}
}
}

View File

@ -1,4 +1,6 @@
using System;
using System.IO; using System.IO;
using System.Linq;
using GFramework.Game.Config; using GFramework.Game.Config;
using GFramework.Game.Config.Generated; using GFramework.Game.Config.Generated;
@ -6,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
@ -37,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()
@ -49,7 +51,7 @@ public class GeneratedConfigConsumerIntegrationTests
"title": "Monster Config", "title": "Monster Config",
"description": "Defines one monster entry for the end-to-end consumer integration test.", "description": "Defines one monster entry for the end-to-end consumer integration test.",
"type": "object", "type": "object",
"required": ["id", "name", "hp"], "required": ["id", "name", "hp", "faction"],
"properties": { "properties": {
"id": { "id": {
"type": "integer", "type": "integer",
@ -62,6 +64,10 @@ public class GeneratedConfigConsumerIntegrationTests
"hp": { "hp": {
"type": "integer", "type": "integer",
"description": "Monster base health." "description": "Monster base health."
},
"faction": {
"type": "string",
"description": "Used by the integration test to validate generated non-unique queries."
} }
} }
} }
@ -72,6 +78,7 @@ public class GeneratedConfigConsumerIntegrationTests
id: 1 id: 1
name: Slime name: Slime
hp: 10 hp: 10
faction: dungeon
"""); """);
CreateFile( CreateFile(
"monster/goblin.yaml", "monster/goblin.yaml",
@ -79,6 +86,7 @@ public class GeneratedConfigConsumerIntegrationTests
id: 2 id: 2
name: Goblin name: Goblin
hp: 30 hp: 30
faction: dungeon
"""); """);
var registry = new ConfigRegistry(); var registry = new ConfigRegistry();
@ -88,15 +96,35 @@ public class GeneratedConfigConsumerIntegrationTests
await loader.LoadAsync(registry); await loader.LoadAsync(registry);
var table = registry.GetMonsterTable(); var table = registry.GetMonsterTable();
var dungeonMonsters = table.FindByFaction("dungeon");
Assert.Multiple(() => Assert.Multiple(() =>
{ {
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"));
Assert.That(MonsterConfigBindings.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json")); Assert.That(MonsterConfigBindings.SchemaRelativePath, Is.EqualTo("schemas/monster.schema.json"));
Assert.That(MonsterConfigBindings.Metadata.ConfigDomain, Is.EqualTo(MonsterConfigBindings.ConfigDomain));
Assert.That(MonsterConfigBindings.Metadata.TableName, Is.EqualTo(MonsterConfigBindings.TableName));
Assert.That(MonsterConfigBindings.Metadata.ConfigRelativePath,
Is.EqualTo(MonsterConfigBindings.ConfigRelativePath));
Assert.That(MonsterConfigBindings.Metadata.SchemaRelativePath,
Is.EqualTo(MonsterConfigBindings.SchemaRelativePath));
Assert.That(MonsterConfigBindings.References.All, Is.Empty);
Assert.That(MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out _), Is.False);
Assert.That(table.Count, Is.EqualTo(2)); Assert.That(table.Count, Is.EqualTo(2));
Assert.That(table.Get(1).Name, Is.EqualTo("Slime")); Assert.That(table.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(table.Get(2).Hp, Is.EqualTo(30)); Assert.That(table.Get(2).Hp, Is.EqualTo(30));
Assert.That(table.FindByName("Slime").Select(static config => config.Id), Is.EqualTo(new[] { 1 }));
Assert.That(dungeonMonsters.Select(static config => config.Name), Is.EquivalentTo(new[] { "Slime", "Goblin" }));
Assert.That(table.TryFindFirstByName("Goblin", out var goblin), Is.True);
Assert.That(goblin, Is.Not.Null);
Assert.That(goblin!.Id, Is.EqualTo(2));
Assert.That(table.TryFindFirstByFaction("dungeon", out var firstDungeonMonster), Is.True);
Assert.That(firstDungeonMonster, Is.Not.Null);
Assert.That(firstDungeonMonster!.Name, Is.AnyOf("Slime", "Goblin"));
Assert.That(table.TryFindFirstByFaction("forest", out var missingMonster), Is.False);
Assert.That(missingMonster, Is.Null);
Assert.That(registry.TryGetMonsterTable(out var generatedTable), Is.True); Assert.That(registry.TryGetMonsterTable(out var generatedTable), Is.True);
Assert.That(generatedTable, Is.Not.Null); Assert.That(generatedTable, Is.Not.Null);
Assert.That(generatedTable!.All().Select(static config => config.Name), Assert.That(generatedTable!.All().Select(static config => config.Name),
@ -122,4 +150,4 @@ public class GeneratedConfigConsumerIntegrationTests
File.WriteAllText(path, content.Replace("\n", Environment.NewLine, StringComparison.Ordinal)); File.WriteAllText(path, content.Replace("\n", Environment.NewLine, StringComparison.Ordinal));
} }
} }

View File

@ -71,6 +71,68 @@ public class YamlConfigLoaderTests
}); });
} }
/// <summary>
/// 验证加载器支持通过选项对象注册带 schema 校验的配置表。
/// </summary>
[Test]
public async Task RegisterTable_Should_Support_Options_Object()
{
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(
new YamlConfigTableRegistrationOptions<int, MonsterConfigStub>(
"monster",
"monster",
static config => config.Id)
{
SchemaRelativePath = "schemas/monster.schema.json"
});
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var table = registry.GetTable<int, MonsterConfigStub>("monster");
Assert.Multiple(() =>
{
Assert.That(table.Count, Is.EqualTo(1));
Assert.That(table.Get(1).Name, Is.EqualTo("Slime"));
Assert.That(table.Get(1).Hp, Is.EqualTo(10));
});
}
/// <summary>
/// 验证加载器会拒绝空的配置表注册选项对象。
/// </summary>
[Test]
public void RegisterTable_Should_Throw_When_Options_Are_Null()
{
var loader = new YamlConfigLoader(_rootPath);
Assert.Throws<ArgumentNullException>(() =>
loader.RegisterTable<int, MonsterConfigStub>(null!));
}
/// <summary> /// <summary>
/// 验证注册的配置目录不存在时会抛出清晰错误。 /// 验证注册的配置目录不存在时会抛出清晰错误。
/// </summary> /// </summary>
@ -999,6 +1061,98 @@ public class YamlConfigLoaderTests
} }
} }
/// <summary>
/// 验证热重载支持通过选项对象配置回调和防抖延迟。
/// </summary>
[Test]
public async Task EnableHotReload_Should_Support_Options_Object()
{
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(
new YamlConfigTableRegistrationOptions<int, MonsterConfigStub>(
"monster",
"monster",
static config => config.Id)
{
SchemaRelativePath = "schemas/monster.schema.json"
});
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
var reloadTaskSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var hotReload = loader.EnableHotReload(
registry,
new YamlConfigHotReloadOptions
{
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<int, MonsterConfigStub>("monster").Get(1).Hp, Is.EqualTo(25));
});
}
finally
{
hotReload.UnRegister();
}
}
/// <summary>
/// 验证热重载会在启动前拒绝负的防抖延迟,避免后台延迟任务才暴露参数错误。
/// </summary>
[Test]
public void EnableHotReload_Should_Throw_When_Debounce_Delay_Is_Negative()
{
var loader = new YamlConfigLoader(_rootPath);
var registry = new ConfigRegistry();
var exception = Assert.Throws<ArgumentOutOfRangeException>(() =>
loader.EnableHotReload(
registry,
new YamlConfigHotReloadOptions
{
DebounceDelay = TimeSpan.FromMilliseconds(-1)
}));
Assert.That(exception!.ParamName, Is.EqualTo("options"));
}
/// <summary> /// <summary>
/// 验证热重载失败时会保留旧表状态,并通过失败回调暴露诊断信息。 /// 验证热重载失败时会保留旧表状态,并通过失败回调暴露诊断信息。
/// </summary> /// </summary>

View File

@ -0,0 +1,46 @@
using GFramework.Game.Config;
namespace GFramework.Game.Tests.Config;
/// <summary>
/// 验证 YAML 配置表注册选项会在构造阶段建立最小不变量,避免非法路径状态继续向后传播。
/// </summary>
[TestFixture]
public class YamlConfigTableRegistrationOptionsTests
{
/// <summary>
/// 验证构造函数会拒绝空的或仅空白字符的表名。
/// </summary>
/// <param name="tableName">待验证的表名。</param>
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void Constructor_Should_Throw_When_Table_Name_Is_Null_Or_Whitespace(string? tableName)
{
var exception = Assert.Throws<ArgumentException>(() =>
_ = new YamlConfigTableRegistrationOptions<int, string>(
tableName!,
"monster",
static config => config.Length));
Assert.That(exception!.ParamName, Is.EqualTo("tableName"));
}
/// <summary>
/// 验证构造函数会拒绝空的或仅空白字符的相对目录路径。
/// </summary>
/// <param name="relativePath">待验证的相对目录路径。</param>
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void Constructor_Should_Throw_When_Relative_Path_Is_Null_Or_Whitespace(string? relativePath)
{
var exception = Assert.Throws<ArgumentException>(() =>
_ = new YamlConfigTableRegistrationOptions<int, string>(
"monster",
relativePath!,
static config => config.Length));
Assert.That(exception!.ParamName, Is.EqualTo("relativePath"));
}
}

View File

@ -1,20 +1,114 @@
using System;
using GFramework.Game.Abstractions.Data; using GFramework.Game.Abstractions.Data;
using GFramework.Game.Abstractions.Enums; using GFramework.Game.Abstractions.Enums;
namespace GFramework.Game.Tests.Data; namespace GFramework.Game.Tests.Data;
internal sealed record TestDataLocation( /// <summary>
string Key, /// 为持久化测试提供稳定的测试数据位置实现。
StorageKinds Kinds = StorageKinds.Local, /// </summary>
string? Namespace = null, internal sealed class TestDataLocation : IDataLocation
IReadOnlyDictionary<string, string>? Metadata = null) : IDataLocation; {
/// <summary>
/// 初始化测试数据位置。
/// </summary>
/// <param name="key">测试使用的存储键。</param>
/// <param name="kinds">测试使用的存储类型。</param>
/// <param name="namespaceValue">测试使用的命名空间。</param>
/// <param name="metadata">附加测试元数据。</param>
public TestDataLocation(
string key,
StorageKinds kinds = StorageKinds.Local,
string? namespaceValue = null,
IReadOnlyDictionary<string, string>? metadata = null)
{
Key = key;
Kinds = kinds;
Namespace = namespaceValue;
Metadata = metadata;
}
/// <summary>
/// 获取测试数据对应的存储键。
/// </summary>
public string Key { get; }
/// <summary>
/// 获取测试数据使用的存储类型。
/// </summary>
public StorageKinds Kinds { get; }
/// <summary>
/// 获取测试数据使用的命名空间。
/// </summary>
public string? Namespace { get; }
/// <summary>
/// 获取附加到测试位置上的元数据。
/// </summary>
public IReadOnlyDictionary<string, string>? Metadata { get; }
}
/// <summary>
/// 为基础存档仓库测试提供的简单存档模型。
/// </summary>
internal sealed class TestSaveData : IData internal sealed class TestSaveData : IData
{ {
/// <summary>
/// 获取或设置测试存档中的名称字段。
/// </summary>
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
} }
/// <summary>
/// 为存档迁移测试提供的版本化存档模型。
/// </summary>
internal sealed class TestVersionedSaveData : IVersionedData
{
/// <summary>
/// 获取或设置测试存档中的名称字段。
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 获取或设置测试存档中的等级字段。
/// </summary>
public int Level { get; set; }
/// <summary>
/// 获取或设置测试存档中的经验字段。
/// </summary>
public int Experience { get; set; }
/// <summary>
/// 获取或设置当前测试存档的版本号。
/// </summary>
public int Version { get; set; } = 3;
/// <summary>
/// 获取或设置测试存档的最后修改时间。
/// </summary>
public DateTime LastModified { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// 为通用持久化测试提供的简单数据模型。
/// </summary>
internal sealed class TestSimpleData : IData internal sealed class TestSimpleData : IData
{ {
/// <summary>
/// 获取或设置测试数据中的整数值。
/// </summary>
public int Value { get; set; } public int Value { get; set; }
} }
/// <summary>
/// 为批量持久化测试提供的另一种数据模型,用于验证运行时类型不会在接口路径上退化。
/// </summary>
internal sealed class TestNamedData : IData
{
/// <summary>
/// 获取或设置测试数据中的名称值。
/// </summary>
public string Name { get; set; } = string.Empty;
}

View File

@ -1,11 +1,21 @@
using System.IO; using System.IO;
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Abstractions.Storage;
using GFramework.Core.Architectures;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Game.Abstractions.Data; using GFramework.Game.Abstractions.Data;
using GFramework.Game.Abstractions.Data.Events;
using GFramework.Game.Data; using GFramework.Game.Data;
using GFramework.Game.Serializer; using GFramework.Game.Serializer;
using GFramework.Game.Storage; using GFramework.Game.Storage;
namespace GFramework.Game.Tests.Data; namespace GFramework.Game.Tests.Data;
/// <summary>
/// 覆盖文件存储、槽位存档仓库和统一设置仓库的持久化行为测试。
/// </summary>
[TestFixture] [TestFixture]
public class PersistenceTests public class PersistenceTests
{ {
@ -16,6 +26,10 @@ public class PersistenceTests
return path; return path;
} }
/// <summary>
/// 验证文件存储能够持久化数据,并拒绝包含路径逃逸的非法键。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test] [Test]
public async Task FileStorage_PersistsDataAndRejectsIllegalKeys() public async Task FileStorage_PersistsDataAndRejectsIllegalKeys()
{ {
@ -31,6 +45,10 @@ public class PersistenceTests
Assert.ThrowsAsync<ArgumentException>(async () => await storage.WriteAsync("../escape", new TestSimpleData())); Assert.ThrowsAsync<ArgumentException>(async () => await storage.WriteAsync("../escape", new TestSimpleData()));
} }
/// <summary>
/// 验证槽位存档仓库的保存、加载、列举和删除行为。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test] [Test]
public async Task SaveRepository_ManagesSlots() public async Task SaveRepository_ManagesSlots()
{ {
@ -59,6 +77,121 @@ public class PersistenceTests
Assert.That(await repository.ExistsAsync(1), Is.False); Assert.That(await repository.ExistsAsync(1), Is.False);
} }
/// <summary>
/// 验证存档仓库在加载旧版本数据时会执行迁移链并回写升级后的最新版本。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task SaveRepository_LoadAsync_Should_Apply_Migrations_And_Persist_Upgraded_Save()
{
var root = CreateTempRoot();
using var storage = new FileStorage(root, new JsonSerializer());
var config = new SaveConfiguration
{
SaveRoot = "saves",
SaveSlotPrefix = "slot_",
SaveFileName = "save"
};
var writer = new SaveRepository<TestVersionedSaveData>(storage, config);
await writer.SaveAsync(1, new TestVersionedSaveData
{
Name = "hero",
Level = 5,
Experience = 0,
Version = 1
});
var repository = new SaveRepository<TestVersionedSaveData>(storage, config)
.RegisterMigration(new TestSaveMigrationV1ToV2())
.RegisterMigration(new TestSaveMigrationV2ToV3());
var loaded = await repository.LoadAsync(1);
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save");
Assert.Multiple(() =>
{
Assert.That(loaded.Version, Is.EqualTo(3));
Assert.That(loaded.Experience, Is.EqualTo(500));
Assert.That(loaded.Name, Is.EqualTo("hero-v2"));
Assert.That(persisted.Version, Is.EqualTo(3));
Assert.That(persisted.Experience, Is.EqualTo(500));
Assert.That(persisted.Name, Is.EqualTo("hero-v2"));
});
}
/// <summary>
/// 验证非版本化存档类型不能注册迁移器,避免构建无效迁移管线。
/// </summary>
/// <exception cref="InvalidOperationException">当存档类型未实现 <see cref="IVersionedData" /> 时抛出。</exception>
[Test]
public void SaveRepository_RegisterMigration_For_NonVersioned_Save_Should_Throw()
{
var root = CreateTempRoot();
using var storage = new FileStorage(root, new JsonSerializer());
var config = new SaveConfiguration();
var repository = new SaveRepository<TestSaveData>(storage, config);
Assert.Throws<InvalidOperationException>(() => repository.RegisterMigration(new TestNonVersionedMigration()));
}
/// <summary>
/// 验证同一源版本不能重复注册迁移器,避免迁移链配置被静默覆盖。
/// </summary>
/// <exception cref="InvalidOperationException">当同一源版本重复注册迁移器时抛出。</exception>
[Test]
public void SaveRepository_RegisterMigration_Should_Reject_Duplicate_FromVersion()
{
var root = CreateTempRoot();
using var storage = new FileStorage(root, new JsonSerializer());
var config = new SaveConfiguration();
var repository = new SaveRepository<TestVersionedSaveData>(storage, config);
repository.RegisterMigration(new TestSaveMigrationV1ToV2());
var exception = Assert.Throws<InvalidOperationException>(
() => repository.RegisterMigration(new TestDuplicateSaveMigrationV1ToV2()));
Assert.That(exception!.Message, Does.Contain("Duplicate save migration registration"));
}
/// <summary>
/// 验证当迁移链缺少中间版本时,加载旧存档会明确失败而不是静默返回不完整数据。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
/// <exception cref="InvalidOperationException">当从旧版本到当前版本的迁移链不完整时抛出。</exception>
[Test]
public async Task SaveRepository_LoadAsync_Should_Throw_When_Migration_Chain_Is_Incomplete()
{
var root = CreateTempRoot();
using var storage = new FileStorage(root, new JsonSerializer());
var config = new SaveConfiguration
{
SaveRoot = "saves",
SaveSlotPrefix = "slot_",
SaveFileName = "save"
};
var writer = new SaveRepository<TestVersionedSaveData>(storage, config);
await writer.SaveAsync(1, new TestVersionedSaveData
{
Name = "legacy",
Level = 3,
Experience = 0,
Version = 1
});
var repository = new SaveRepository<TestVersionedSaveData>(storage, config)
.RegisterMigration(new TestSaveMigrationV1ToV2());
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.LoadAsync(1));
Assert.That(exception!.Message, Does.Contain("from version 2"));
}
/// <summary>
/// 验证统一设置仓库能够保存、重新加载并批量读取已注册的设置数据。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test] [Test]
public async Task UnifiedSettingsDataRepository_RoundTripsDataAndLoadAll() public async Task UnifiedSettingsDataRepository_RoundTripsDataAndLoadAll()
{ {
@ -68,8 +201,7 @@ public class PersistenceTests
var repo = new UnifiedSettingsDataRepository( var repo = new UnifiedSettingsDataRepository(
storage, storage,
serializer, serializer,
new DataRepositoryOptions { EnableEvents = false }, new DataRepositoryOptions { EnableEvents = false });
"settings.json");
var location = new TestDataLocation("settings/choice"); var location = new TestDataLocation("settings/choice");
repo.RegisterDataType(location, typeof(TestSimpleData)); repo.RegisterDataType(location, typeof(TestSimpleData));
@ -81,8 +213,7 @@ public class PersistenceTests
var repo2 = new UnifiedSettingsDataRepository( var repo2 = new UnifiedSettingsDataRepository(
storage2, storage2,
serializer, serializer,
new DataRepositoryOptions { EnableEvents = false }, new DataRepositoryOptions { EnableEvents = false });
"settings.json");
repo2.RegisterDataType(location, typeof(TestSimpleData)); repo2.RegisterDataType(location, typeof(TestSimpleData));
var loaded = await repo2.LoadAsync<TestSimpleData>(location); var loaded = await repo2.LoadAsync<TestSimpleData>(location);
@ -92,4 +223,605 @@ public class PersistenceTests
Assert.That(all.Keys, Contains.Item(location.Key)); Assert.That(all.Keys, Contains.Item(location.Key));
Assert.That(all[location.Key], Is.TypeOf<TestSimpleData>()); Assert.That(all[location.Key], Is.TypeOf<TestSimpleData>());
} }
}
/// <summary>
/// 验证通用数据仓库在覆盖已有数据时会创建备份文件,并保留覆盖前的旧值。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task DataRepository_SaveAsync_Should_Create_Backup_When_Overwriting_Existing_Data()
{
var root = CreateTempRoot();
using var storage = new FileStorage(root, new JsonSerializer(), ".json");
var repository = new DataRepository(
storage,
new DataRepositoryOptions
{
AutoBackup = true,
EnableEvents = false
});
var location = new TestDataLocation("options", namespaceValue: "profile");
await repository.SaveAsync(location, new TestSimpleData { Value = 1 });
await repository.SaveAsync(location, new TestSimpleData { Value = 2 });
var current = await repository.LoadAsync<TestSimpleData>(location);
var backup = await storage.ReadAsync<TestSimpleData>("profile/options.backup");
Assert.Multiple(() =>
{
Assert.That(current.Value, Is.EqualTo(2));
Assert.That(backup.Value, Is.EqualTo(1));
});
}
/// <summary>
/// 验证通用数据仓库的批量保存只发送批量事件,不重复发送单项保存事件。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task DataRepository_SaveAllAsync_Should_Emit_Only_Batch_Event()
{
var root = CreateTempRoot();
using var storage = new FileStorage(root, new JsonSerializer(), ".json");
var repository = new DataRepository(
storage,
new DataRepositoryOptions
{
AutoBackup = false,
EnableEvents = true
});
var context = CreateEventContext();
((IContextAware)repository).SetContext(context);
var location1 = new TestDataLocation("graphics", namespaceValue: "settings");
var location2 = new TestDataLocation("audio", namespaceValue: "settings");
var savedEventCount = 0;
var batchEventCount = 0;
context.RegisterEvent<DataSavedEvent<TestSimpleData>>(_ => savedEventCount++);
context.RegisterEvent<DataBatchSavedEvent>(_ => batchEventCount++);
await repository.SaveAllAsync(
[
(location1, new TestSimpleData { Value = 10 }),
(location2, new TestSimpleData { Value = 20 })
]);
Assert.Multiple(() =>
{
Assert.That(savedEventCount, Is.Zero);
Assert.That(batchEventCount, Is.EqualTo(1));
});
}
/// <summary>
/// 验证批量覆盖已有数据时仍会按每个条目的运行时类型执行备份与回写,而不会退化为 <see cref="IData" />。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task DataRepository_SaveAllAsync_Should_Preserve_Runtime_Types_When_Overwriting_Existing_Data()
{
var root = CreateTempRoot();
using var storage = new FileStorage(root, new JsonSerializer(), ".json");
var repository = new DataRepository(
storage,
new DataRepositoryOptions
{
AutoBackup = true,
EnableEvents = false
});
var numberLocation = new TestDataLocation("graphics", namespaceValue: "settings");
var textLocation = new TestDataLocation("profile", namespaceValue: "settings");
await repository.SaveAllAsync(
[
(numberLocation, new TestSimpleData { Value = 1 }),
(textLocation, new TestNamedData { Name = "old-name" })
]);
await repository.SaveAllAsync(
[
(numberLocation, new TestSimpleData { Value = 2 }),
(textLocation, new TestNamedData { Name = "new-name" })
]);
var currentNumber = await repository.LoadAsync<TestSimpleData>(numberLocation);
var currentText = await repository.LoadAsync<TestNamedData>(textLocation);
var backupNumber = await storage.ReadAsync<TestSimpleData>("settings/graphics.backup");
var backupText = await storage.ReadAsync<TestNamedData>("settings/profile.backup");
Assert.Multiple(() =>
{
Assert.That(currentNumber.Value, Is.EqualTo(2));
Assert.That(currentText.Name, Is.EqualTo("new-name"));
Assert.That(backupNumber.Value, Is.EqualTo(1));
Assert.That(backupText.Name, Is.EqualTo("old-name"));
});
}
/// <summary>
/// 验证统一设置仓库在批量覆盖时会为整个聚合文件创建备份,并只发送批量事件。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task UnifiedSettingsDataRepository_SaveAllAsync_Should_Create_Backup_And_Emit_Only_Batch_Event()
{
var root = CreateTempRoot();
var location1 = new TestDataLocation("settings/graphics");
var location2 = new TestDataLocation("settings/audio");
using (var seedStorage = new FileStorage(root, new JsonSerializer(), ".json"))
{
var seedRepository = new UnifiedSettingsDataRepository(
seedStorage,
new JsonSerializer(),
new DataRepositoryOptions
{
AutoBackup = true,
EnableEvents = false
});
seedRepository.RegisterDataType(location1, typeof(TestSimpleData));
seedRepository.RegisterDataType(location2, typeof(TestSimpleData));
await seedRepository.SaveAsync(location1, new TestSimpleData { Value = 1 });
}
using var storage = new FileStorage(root, new JsonSerializer(), ".json");
var repository = new UnifiedSettingsDataRepository(
storage,
new JsonSerializer(),
new DataRepositoryOptions
{
AutoBackup = true,
EnableEvents = true
});
repository.RegisterDataType(location1, typeof(TestSimpleData));
repository.RegisterDataType(location2, typeof(TestSimpleData));
var context = CreateEventContext();
((IContextAware)repository).SetContext(context);
var savedEventCount = 0;
var batchEventCount = 0;
context.RegisterEvent<DataSavedEvent<TestSimpleData>>(_ => savedEventCount++);
context.RegisterEvent<DataBatchSavedEvent>(_ => batchEventCount++);
await repository.SaveAllAsync(
[
(location1, new TestSimpleData { Value = 2 }),
(location2, new TestSimpleData { Value = 3 })
]);
var current = await repository.LoadAsync<TestSimpleData>(location1);
var backupJson = await File.ReadAllTextAsync(Path.Combine(root, "settings.json.backup.json"));
Assert.Multiple(() =>
{
Assert.That(current.Value, Is.EqualTo(2));
Assert.That(savedEventCount, Is.Zero);
Assert.That(batchEventCount, Is.EqualTo(1));
Assert.That(backupJson, Does.Contain("settings/graphics"));
Assert.That(backupJson, Does.Contain("\\\"Value\\\":1"));
});
}
/// <summary>
/// 验证统一设置仓库在删除某个 section 时会回写聚合文件,并保留删除前的统一文件备份。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task UnifiedSettingsDataRepository_DeleteAsync_Should_Persist_Deletion_And_Create_Backup()
{
var root = CreateTempRoot();
var location1 = new TestDataLocation("settings/graphics");
var location2 = new TestDataLocation("settings/audio");
using (var storage = new FileStorage(root, new JsonSerializer(), ".json"))
{
var repository = new UnifiedSettingsDataRepository(
storage,
new JsonSerializer(),
new DataRepositoryOptions
{
AutoBackup = true,
EnableEvents = false
});
repository.RegisterDataType(location1, typeof(TestSimpleData));
repository.RegisterDataType(location2, typeof(TestSimpleData));
await repository.SaveAllAsync(
[
(location1, new TestSimpleData { Value = 7 }),
(location2, new TestSimpleData { Value = 11 })
]);
}
using var verifyStorage = new FileStorage(root, new JsonSerializer(), ".json");
var verifyRepository = new UnifiedSettingsDataRepository(
verifyStorage,
new JsonSerializer(),
new DataRepositoryOptions
{
AutoBackup = true,
EnableEvents = false
});
verifyRepository.RegisterDataType(location1, typeof(TestSimpleData));
verifyRepository.RegisterDataType(location2, typeof(TestSimpleData));
await verifyRepository.DeleteAsync(location2);
var remaining = await verifyRepository.LoadAsync<TestSimpleData>(location1);
var removedExists = await verifyRepository.ExistsAsync(location2);
var backupJson = await File.ReadAllTextAsync(Path.Combine(root, "settings.json.backup.json"));
Assert.Multiple(() =>
{
Assert.That(remaining.Value, Is.EqualTo(7));
Assert.That(removedExists, Is.False);
Assert.That(backupJson, Does.Contain("settings/audio"));
Assert.That(backupJson, Does.Contain("\\\"Value\\\":11"));
});
}
/// <summary>
/// 验证统一设置仓库在保存提交失败时不会污染内存缓存,并且失败修改不会泄漏到后续无关保存。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task UnifiedSettingsDataRepository_SaveAsync_When_Persist_Fails_Should_Keep_Cache_Consistent()
{
var root = CreateTempRoot();
var primaryLocation = new TestDataLocation("settings/graphics");
var secondaryLocation = new TestDataLocation("settings/audio");
using (var seedStorage = new FileStorage(root, new JsonSerializer(), ".json"))
{
var seedRepository = new UnifiedSettingsDataRepository(
seedStorage,
new JsonSerializer(),
new DataRepositoryOptions { EnableEvents = false });
seedRepository.RegisterDataType(primaryLocation, typeof(TestSimpleData));
seedRepository.RegisterDataType(secondaryLocation, typeof(TestSimpleData));
await seedRepository.SaveAsync(primaryLocation, new TestSimpleData { Value = 1 });
}
using var innerStorage = new FileStorage(root, new JsonSerializer(), ".json");
var throwingStorage = new ToggleWriteFailureStorage(innerStorage, "settings.json");
var repository = new UnifiedSettingsDataRepository(
throwingStorage,
new JsonSerializer(),
new DataRepositoryOptions { EnableEvents = false });
repository.RegisterDataType(primaryLocation, typeof(TestSimpleData));
repository.RegisterDataType(secondaryLocation, typeof(TestSimpleData));
throwingStorage.ThrowOnWrite = true;
Assert.ThrowsAsync<InvalidOperationException>(
async () => await repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 99 }));
var cachedAfterFailure = await repository.LoadAsync<TestSimpleData>(primaryLocation);
Assert.That(cachedAfterFailure.Value, Is.EqualTo(1));
throwingStorage.ThrowOnWrite = false;
await repository.SaveAsync(secondaryLocation, new TestSimpleData { Value = 7 });
using var verifyStorage = new FileStorage(root, new JsonSerializer(), ".json");
var verifyRepository = new UnifiedSettingsDataRepository(
verifyStorage,
new JsonSerializer(),
new DataRepositoryOptions { EnableEvents = false });
verifyRepository.RegisterDataType(primaryLocation, typeof(TestSimpleData));
verifyRepository.RegisterDataType(secondaryLocation, typeof(TestSimpleData));
var persistedPrimary = await verifyRepository.LoadAsync<TestSimpleData>(primaryLocation);
var persistedSecondary = await verifyRepository.LoadAsync<TestSimpleData>(secondaryLocation);
Assert.Multiple(() =>
{
Assert.That(persistedPrimary.Value, Is.EqualTo(1));
Assert.That(persistedSecondary.Value, Is.EqualTo(7));
});
}
/// <summary>
/// 验证统一设置仓库在删除提交失败时不会把未提交删除留在缓存里,也不会泄漏到后续保存。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task UnifiedSettingsDataRepository_DeleteAsync_When_Persist_Fails_Should_Keep_Cache_Consistent()
{
var root = CreateTempRoot();
var primaryLocation = new TestDataLocation("settings/graphics");
var secondaryLocation = new TestDataLocation("settings/audio");
using (var seedStorage = new FileStorage(root, new JsonSerializer(), ".json"))
{
var seedRepository = new UnifiedSettingsDataRepository(
seedStorage,
new JsonSerializer(),
new DataRepositoryOptions { EnableEvents = false });
seedRepository.RegisterDataType(primaryLocation, typeof(TestSimpleData));
seedRepository.RegisterDataType(secondaryLocation, typeof(TestSimpleData));
await seedRepository.SaveAllAsync(
[
(primaryLocation, new TestSimpleData { Value = 3 }),
(secondaryLocation, new TestSimpleData { Value = 5 })
]);
}
using var innerStorage = new FileStorage(root, new JsonSerializer(), ".json");
var throwingStorage = new ToggleWriteFailureStorage(innerStorage, "settings.json");
var repository = new UnifiedSettingsDataRepository(
throwingStorage,
new JsonSerializer(),
new DataRepositoryOptions { EnableEvents = false });
repository.RegisterDataType(primaryLocation, typeof(TestSimpleData));
repository.RegisterDataType(secondaryLocation, typeof(TestSimpleData));
throwingStorage.ThrowOnWrite = true;
Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.DeleteAsync(secondaryLocation));
Assert.That(await repository.ExistsAsync(secondaryLocation), Is.True);
throwingStorage.ThrowOnWrite = false;
await repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 9 });
using var verifyStorage = new FileStorage(root, new JsonSerializer(), ".json");
var verifyRepository = new UnifiedSettingsDataRepository(
verifyStorage,
new JsonSerializer(),
new DataRepositoryOptions { EnableEvents = false });
verifyRepository.RegisterDataType(primaryLocation, typeof(TestSimpleData));
verifyRepository.RegisterDataType(secondaryLocation, typeof(TestSimpleData));
var persistedPrimary = await verifyRepository.LoadAsync<TestSimpleData>(primaryLocation);
var persistedSecondary = await verifyRepository.LoadAsync<TestSimpleData>(secondaryLocation);
Assert.Multiple(() =>
{
Assert.That(persistedPrimary.Value, Is.EqualTo(9));
Assert.That(persistedSecondary.Value, Is.EqualTo(5));
});
}
/// <summary>
/// 验证统一设置仓库在启用事件时,只为显式仓库操作发送加载、保存、批量保存和删除事件。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task UnifiedSettingsDataRepository_WithEvents_Should_Emit_Only_Public_Operation_Events()
{
var root = CreateTempRoot();
using var storage = new FileStorage(root, new JsonSerializer(), ".json");
var repository = new UnifiedSettingsDataRepository(
storage,
new JsonSerializer(),
new DataRepositoryOptions
{
AutoBackup = true,
EnableEvents = true
});
var context = CreateEventContext();
((IContextAware)repository).SetContext(context);
var location1 = new TestDataLocation("settings/graphics");
var location2 = new TestDataLocation("settings/audio");
repository.RegisterDataType(location1, typeof(TestSimpleData));
repository.RegisterDataType(location2, typeof(TestSimpleData));
var loadedEventCount = 0;
var savedEventCount = 0;
var batchEventCount = 0;
var deletedEventCount = 0;
context.RegisterEvent<DataLoadedEvent<TestSimpleData>>(_ => loadedEventCount++);
context.RegisterEvent<DataSavedEvent<TestSimpleData>>(_ => savedEventCount++);
context.RegisterEvent<DataBatchSavedEvent>(_ => batchEventCount++);
context.RegisterEvent<DataDeletedEvent>(_ => deletedEventCount++);
_ = await repository.LoadAsync<TestSimpleData>(location1);
await repository.SaveAsync(location1, new TestSimpleData { Value = 5 });
await repository.SaveAllAsync(
[
(location1, new TestSimpleData { Value = 6 }),
(location2, new TestSimpleData { Value = 7 })
]);
await repository.DeleteAsync(location2);
Assert.Multiple(() =>
{
Assert.That(loadedEventCount, Is.EqualTo(1));
Assert.That(savedEventCount, Is.EqualTo(1));
Assert.That(batchEventCount, Is.EqualTo(1));
Assert.That(deletedEventCount, Is.EqualTo(1));
});
}
/// <summary>
/// 创建带事件总线的真实架构上下文,供上下文感知仓库测试使用。
/// </summary>
/// <returns>可用于发送和监听事件的架构上下文。</returns>
private static ArchitectureContext CreateEventContext()
{
var container = new MicrosoftDiContainer();
container.Register<IEventBus>(new EventBus());
container.Freeze();
return new ArchitectureContext(container);
}
private sealed class TestSaveMigrationV1ToV2 : ISaveMigration<TestVersionedSaveData>
{
public int FromVersion => 1;
public int ToVersion => 2;
public TestVersionedSaveData Migrate(TestVersionedSaveData oldData)
{
return new TestVersionedSaveData
{
Name = $"{oldData.Name}-v2",
Level = oldData.Level,
Experience = oldData.Level * 100,
Version = 2,
LastModified = oldData.LastModified
};
}
}
private sealed class TestSaveMigrationV2ToV3 : ISaveMigration<TestVersionedSaveData>
{
public int FromVersion => 2;
public int ToVersion => 3;
public TestVersionedSaveData Migrate(TestVersionedSaveData oldData)
{
return new TestVersionedSaveData
{
Name = oldData.Name,
Level = oldData.Level,
Experience = oldData.Experience,
Version = 3,
LastModified = oldData.LastModified
};
}
}
private sealed class TestDuplicateSaveMigrationV1ToV2 : ISaveMigration<TestVersionedSaveData>
{
public int FromVersion => 1;
public int ToVersion => 2;
public TestVersionedSaveData Migrate(TestVersionedSaveData oldData)
{
return new TestVersionedSaveData
{
Name = $"{oldData.Name}-duplicate",
Level = oldData.Level,
Experience = oldData.Experience,
Version = 2,
LastModified = oldData.LastModified
};
}
}
private sealed class TestNonVersionedMigration : ISaveMigration<TestSaveData>
{
public int FromVersion => 1;
public int ToVersion => 2;
public TestSaveData Migrate(TestSaveData oldData)
{
return new TestSaveData
{
Name = oldData.Name
};
}
}
/// <summary>
/// 为统一设置仓库失败场景测试提供可切换的写入失败包装器。
/// </summary>
private sealed class ToggleWriteFailureStorage(IStorage innerStorage, string failingKey) : IStorage
{
/// <summary>
/// 获取或设置是否在目标键写入时主动抛出异常。
/// </summary>
public bool ThrowOnWrite { get; set; }
/// <inheritdoc />
public bool Exists(string key)
{
return innerStorage.Exists(key);
}
/// <inheritdoc />
public Task<bool> ExistsAsync(string key)
{
return innerStorage.ExistsAsync(key);
}
/// <inheritdoc />
public T Read<T>(string key)
{
return innerStorage.Read<T>(key);
}
/// <inheritdoc />
public T Read<T>(string key, T defaultValue)
{
return innerStorage.Read(key, defaultValue);
}
/// <inheritdoc />
public Task<T> ReadAsync<T>(string key)
{
return innerStorage.ReadAsync<T>(key);
}
/// <inheritdoc />
public void Write<T>(string key, T value)
{
ThrowIfNeeded(key);
innerStorage.Write(key, value);
}
/// <inheritdoc />
public Task WriteAsync<T>(string key, T value)
{
ThrowIfNeeded(key);
return innerStorage.WriteAsync(key, value);
}
/// <inheritdoc />
public void Delete(string key)
{
innerStorage.Delete(key);
}
/// <inheritdoc />
public Task DeleteAsync(string key)
{
return innerStorage.DeleteAsync(key);
}
/// <inheritdoc />
public Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "")
{
return innerStorage.ListDirectoriesAsync(path);
}
/// <inheritdoc />
public Task<IReadOnlyList<string>> ListFilesAsync(string path = "")
{
return innerStorage.ListFilesAsync(path);
}
/// <inheritdoc />
public Task<bool> DirectoryExistsAsync(string path)
{
return innerStorage.DirectoryExistsAsync(path);
}
/// <inheritdoc />
public Task CreateDirectoryAsync(string path)
{
return innerStorage.CreateDirectoryAsync(path);
}
/// <summary>
/// 在启用失败开关且命中目标键时抛出一致的写入失败异常。
/// </summary>
/// <param name="key">当前正在写入的存储键。</param>
private void ThrowIfNeeded(string key)
{
if (ThrowOnWrite && string.Equals(key, failingKey, StringComparison.Ordinal))
{
throw new InvalidOperationException("Simulated unified settings write failure.");
}
}
}
}

View File

@ -0,0 +1,351 @@
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Enums;
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Architectures;
using GFramework.Core.Events;
using GFramework.Core.Ioc;
using GFramework.Game.Abstractions.Setting;
using GFramework.Game.Setting;
using GFramework.Game.Setting.Events;
namespace GFramework.Game.Tests.Setting;
/// <summary>
/// 覆盖 <see cref="SettingsSystem" /> 的系统层语义,确保系统对模型编排、事件发送和重置流程保持稳定。
/// </summary>
[TestFixture]
public sealed class SettingsSystemTests
{
/// <summary>
/// 验证 <see cref="SettingsSystem.ApplyAll" /> 会尝试应用全部 applicator并为成功与失败结果分别发送事件。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task ApplyAll_Should_Apply_All_Applicators_And_Publish_Result_Events()
{
var successfulApplicator = new PrimaryTestSettings();
var failingApplicator = new SecondaryTestSettings(throwOnApply: true);
var model = new FakeSettingsModel(successfulApplicator, failingApplicator);
var context = CreateContext(model);
var system = CreateSystem(context);
var applyingEventCount = 0;
var appliedEventCount = 0;
var failedEventCount = 0;
context.RegisterEvent<SettingsApplyingEvent<ISettingsSection>>(_ => applyingEventCount++);
context.RegisterEvent<SettingsAppliedEvent<ISettingsSection>>(eventData =>
{
appliedEventCount++;
if (!eventData.Success)
{
failedEventCount++;
}
});
await system.ApplyAll();
Assert.Multiple(() =>
{
Assert.That(successfulApplicator.ApplyCount, Is.EqualTo(1));
Assert.That(failingApplicator.ApplyCount, Is.EqualTo(1));
Assert.That(applyingEventCount, Is.EqualTo(2));
Assert.That(appliedEventCount, Is.EqualTo(2));
Assert.That(failedEventCount, Is.EqualTo(1));
});
}
/// <summary>
/// 验证 <see cref="SettingsSystem.SaveAll" /> 会直接委托给模型层统一保存。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task SaveAll_Should_Delegate_To_Model()
{
var model = new FakeSettingsModel(new PrimaryTestSettings());
var system = CreateSystem(CreateContext(model));
await system.SaveAll();
Assert.That(model.SaveAllCallCount, Is.EqualTo(1));
}
/// <summary>
/// 验证 <see cref="SettingsSystem.ResetAll" /> 会先委托模型统一重置,再重新应用全部 applicator。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task ResetAll_Should_Reset_Model_And_Reapply_All_Applicators()
{
var primaryApplicator = new PrimaryTestSettings();
var secondaryApplicator = new SecondaryTestSettings();
var model = new FakeSettingsModel(primaryApplicator, secondaryApplicator);
var system = CreateSystem(CreateContext(model));
await system.ResetAll();
Assert.Multiple(() =>
{
Assert.That(model.ResetAllCallCount, Is.EqualTo(1));
Assert.That(primaryApplicator.ApplyCount, Is.EqualTo(1));
Assert.That(secondaryApplicator.ApplyCount, Is.EqualTo(1));
});
}
/// <summary>
/// 验证 <see cref="SettingsSystem.Reset{T}" /> 会重置目标数据类型,并只重新应用对应的 applicator。
/// </summary>
/// <returns>表示异步测试完成的任务。</returns>
[Test]
public async Task Reset_Should_Reset_Target_Data_And_Reapply_Target_Applicator()
{
var primaryApplicator = new PrimaryTestSettings();
var secondaryApplicator = new SecondaryTestSettings();
var model = new FakeSettingsModel(primaryApplicator, secondaryApplicator);
var system = CreateSystem(CreateContext(model));
await system.Reset<PrimaryTestSettings>();
Assert.Multiple(() =>
{
Assert.That(model.ResetTypes, Is.EquivalentTo(new[] { typeof(PrimaryTestSettings) }));
Assert.That(primaryApplicator.ApplyCount, Is.EqualTo(1));
Assert.That(secondaryApplicator.ApplyCount, Is.Zero);
});
}
/// <summary>
/// 创建带事件总线和设置模型的真实架构上下文。
/// </summary>
/// <param name="model">测试使用的设置模型。</param>
/// <returns>可供系统解析依赖与发送事件的上下文。</returns>
private static ArchitectureContext CreateContext(ISettingsModel model)
{
var container = new MicrosoftDiContainer();
container.Register<IEventBus>(new EventBus());
container.Register<ISettingsModel>(model);
container.Freeze();
return new ArchitectureContext(container);
}
/// <summary>
/// 创建并初始化绑定到指定上下文的设置系统。
/// </summary>
/// <param name="context">系统运行所需的架构上下文。</param>
/// <returns>已完成初始化的设置系统实例。</returns>
private static SettingsSystem CreateSystem(IArchitectureContext context)
{
var system = new SettingsSystem();
((IContextAware)system).SetContext(context);
system.Initialize();
return system;
}
/// <summary>
/// 用于系统层测试的简化设置模型,记录系统对模型的调用行为。
/// </summary>
private sealed class FakeSettingsModel : ISettingsModel
{
private readonly IReadOnlyDictionary<Type, IResetApplyAbleSettings> _applicators;
/// <summary>
/// 初始化测试模型,并注册参与测试的 applicator 集合。
/// </summary>
/// <param name="applicators">测试使用的 applicator。</param>
public FakeSettingsModel(params IResetApplyAbleSettings[] applicators)
{
_applicators = applicators.ToDictionary(applicator => applicator.GetType());
}
/// <summary>
/// 获取保存全量设置的调用次数。
/// </summary>
public int SaveAllCallCount { get; private set; }
/// <summary>
/// 获取重置全部设置的调用次数。
/// </summary>
public int ResetAllCallCount { get; private set; }
/// <summary>
/// 获取被请求重置的设置数据类型列表。
/// </summary>
public List<Type> ResetTypes { get; } = [];
/// <inheritdoc />
public bool IsInitialized => true;
/// <inheritdoc />
public void SetContext(IArchitectureContext context)
{
}
/// <inheritdoc />
public IArchitectureContext GetContext()
{
throw new NotSupportedException("Fake settings model does not expose a context.");
}
/// <inheritdoc />
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
/// <inheritdoc />
public void Initialize()
{
}
/// <inheritdoc />
public T GetData<T>() where T : class, ISettingsData, new()
{
return new T();
}
/// <inheritdoc />
public IEnumerable<ISettingsData> AllData()
{
return [];
}
/// <inheritdoc />
public ISettingsModel RegisterApplicator<T>(T applicator) where T : class, IResetApplyAbleSettings
{
return this;
}
/// <inheritdoc />
public T? GetApplicator<T>() where T : class, IResetApplyAbleSettings
{
return _applicators.TryGetValue(typeof(T), out var applicator) ? (T)applicator : null;
}
/// <inheritdoc />
public IEnumerable<IResetApplyAbleSettings> AllApplicators()
{
return _applicators.Values;
}
/// <inheritdoc />
public ISettingsModel RegisterMigration(ISettingsMigration migration)
{
return this;
}
/// <inheritdoc />
public Task InitializeAsync()
{
return Task.CompletedTask;
}
/// <inheritdoc />
public Task SaveAllAsync()
{
SaveAllCallCount++;
return Task.CompletedTask;
}
/// <inheritdoc />
public Task ApplyAllAsync()
{
return Task.CompletedTask;
}
/// <inheritdoc />
public void Reset<T>() where T : class, ISettingsData, new()
{
ResetTypes.Add(typeof(T));
}
/// <inheritdoc />
public void ResetAll()
{
ResetAllCallCount++;
}
}
/// <summary>
/// 为系统层测试提供的最小设置数据实现。
/// </summary>
private abstract class TestSettingsBase : ISettingsData, IResetApplyAbleSettings
{
/// <inheritdoc />
public int Version { get; set; } = 1;
/// <inheritdoc />
public DateTime LastModified { get; } = DateTime.UtcNow;
/// <summary>
/// 获取测试用的数值字段,用于确认重置与加载行为。
/// </summary>
public int Value { get; private set; }
/// <summary>
/// 获取应用操作被调用的次数。
/// </summary>
public int ApplyCount { get; private set; }
/// <inheritdoc />
public ISettingsData Data => this;
/// <inheritdoc />
public Type DataType => GetType();
/// <summary>
/// 获取或设置是否在应用时抛出异常。
/// </summary>
protected bool ThrowOnApply { get; init; }
/// <inheritdoc />
public void Reset()
{
Value = 0;
}
/// <inheritdoc />
public void LoadFrom(ISettingsData source)
{
if (source is TestSettingsBase data)
{
Value = data.Value;
Version = data.Version;
}
}
/// <inheritdoc />
public async Task Apply()
{
ApplyCount++;
await Task.Yield();
if (ThrowOnApply)
{
throw new InvalidOperationException("Test applicator failed.");
}
}
}
/// <summary>
/// 代表主设置分支的测试设置对象。
/// </summary>
private sealed class PrimaryTestSettings : TestSettingsBase
{
}
/// <summary>
/// 代表第二个设置分支的测试设置对象,可选择在应用时失败。
/// </summary>
private sealed class SecondaryTestSettings : TestSettingsBase
{
/// <summary>
/// 初始化第二个测试设置对象。
/// </summary>
/// <param name="throwOnApply">是否在应用时抛出异常。</param>
public SecondaryTestSettings(bool throwOnApply = false)
{
ThrowOnApply = throwOnApply;
}
}
}

View File

@ -5,7 +5,8 @@
"required": [ "required": [
"id", "id",
"name", "name",
"hp" "hp",
"faction"
], ],
"properties": { "properties": {
"id": { "id": {
@ -19,6 +20,10 @@
"hp": { "hp": {
"type": "integer", "type": "integer",
"description": "Monster base health." "description": "Monster base health."
},
"faction": {
"type": "string",
"description": "Used by integration tests to validate generated non-unique queries."
} }
} }
} }

View File

@ -0,0 +1,27 @@
namespace GFramework.Game.Config;
/// <summary>
/// 描述开发期热重载的可选行为。
/// 该选项对象集中承载回调和防抖等可扩展参数,
/// 以避免后续继续在 <see cref="YamlConfigLoader.EnableHotReload(GFramework.Game.Abstractions.Config.IConfigRegistry,YamlConfigHotReloadOptions?)" />
/// 上堆叠额外重载。
/// </summary>
public sealed class YamlConfigHotReloadOptions
{
/// <summary>
/// 获取或设置单个配置表重载成功后的可选回调。
/// </summary>
public Action<string>? OnTableReloaded { get; init; }
/// <summary>
/// 获取或设置单个配置表重载失败后的可选回调。
/// 当失败来自加载器本身时,异常通常为 <see cref="GFramework.Game.Abstractions.Config.ConfigLoadException" />。
/// </summary>
public Action<string, Exception>? OnTableReloadFailed { get; init; }
/// <summary>
/// 获取或设置文件系统事件的防抖延迟。
/// 默认值为 200 毫秒,用于吸收编辑器保存时的短时间重复触发。
/// </summary>
public TimeSpan DebounceDelay { get; init; } = TimeSpan.FromMilliseconds(200);
}

View File

@ -20,6 +20,8 @@ public sealed class YamlConfigLoader : IConfigLoader
private const string SchemaRelativePathCannotBeNullOrWhiteSpaceMessage = private const string SchemaRelativePathCannotBeNullOrWhiteSpaceMessage =
"Schema relative path cannot be null or whitespace."; "Schema relative path cannot be null or whitespace.";
private static readonly TimeSpan DefaultHotReloadDebounceDelay = TimeSpan.FromMilliseconds(200);
private readonly IDeserializer _deserializer; private readonly IDeserializer _deserializer;
private readonly Dictionary<string, IReadOnlyCollection<string>> _lastSuccessfulDependencies = private readonly Dictionary<string, IReadOnlyCollection<string>> _lastSuccessfulDependencies =
@ -95,13 +97,50 @@ public sealed class YamlConfigLoader : IConfigLoader
/// <param name="debounceDelay">防抖延迟;为空时默认使用 200 毫秒。</param> /// <param name="debounceDelay">防抖延迟;为空时默认使用 200 毫秒。</param>
/// <returns>用于停止热重载监听的注销句柄。</returns> /// <returns>用于停止热重载监听的注销句柄。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="registry" /> 为空时抛出。</exception> /// <exception cref="ArgumentNullException">当 <paramref name="registry" /> 为空时抛出。</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// 当显式提供的 <paramref name="debounceDelay" /> 小于 <see cref="TimeSpan.Zero" /> 时抛出。
/// </exception>
public IUnRegister EnableHotReload( public IUnRegister EnableHotReload(
IConfigRegistry registry, IConfigRegistry registry,
Action<string>? onTableReloaded = null, Action<string>? onTableReloaded = null,
Action<string, Exception>? onTableReloadFailed = null, Action<string, Exception>? onTableReloadFailed = null,
TimeSpan? debounceDelay = null) TimeSpan? debounceDelay = null)
{
return EnableHotReload(
registry,
new YamlConfigHotReloadOptions
{
OnTableReloaded = onTableReloaded,
OnTableReloadFailed = onTableReloadFailed,
DebounceDelay = debounceDelay ?? DefaultHotReloadDebounceDelay
});
}
/// <summary>
/// 启用开发期热重载,并通过选项对象集中配置回调和防抖行为。
/// 该入口用于减少继续堆叠位置参数重载的需要,
/// 也为未来扩展过滤策略或日志钩子预留稳定形态。
/// </summary>
/// <param name="registry">要被热重载更新的配置注册表。</param>
/// <param name="options">热重载配置选项;为空时使用默认选项。</param>
/// <returns>用于停止热重载监听的注销句柄。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="registry" /> 为空时抛出。</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// 当 <paramref name="options" /> 的 <see cref="YamlConfigHotReloadOptions.DebounceDelay" /> 小于
/// <see cref="TimeSpan.Zero" /> 时抛出。
/// </exception>
public IUnRegister EnableHotReload(
IConfigRegistry registry,
YamlConfigHotReloadOptions? options)
{ {
ArgumentNullException.ThrowIfNull(registry); ArgumentNullException.ThrowIfNull(registry);
options ??= new YamlConfigHotReloadOptions();
if (options.DebounceDelay < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(options),
"DebounceDelay must be greater than or equal to zero.");
}
return new HotReloadSession( return new HotReloadSession(
_rootPath, _rootPath,
@ -109,9 +148,9 @@ public sealed class YamlConfigLoader : IConfigLoader
registry, registry,
_registrations, _registrations,
_lastSuccessfulDependencies, _lastSuccessfulDependencies,
onTableReloaded, options.OnTableReloaded,
onTableReloadFailed, options.OnTableReloadFailed,
debounceDelay ?? TimeSpan.FromMilliseconds(200)); options.DebounceDelay);
} }
private void UpdateLastSuccessfulDependencies(IEnumerable<YamlTableLoadResult> loadedTables) private void UpdateLastSuccessfulDependencies(IEnumerable<YamlTableLoadResult> loadedTables)
@ -135,6 +174,10 @@ public sealed class YamlConfigLoader : IConfigLoader
/// <param name="keySelector">配置项主键提取器。</param> /// <param name="keySelector">配置项主键提取器。</param>
/// <param name="comparer">可选主键比较器。</param> /// <param name="comparer">可选主键比较器。</param>
/// <returns>当前加载器实例,以便链式注册。</returns> /// <returns>当前加载器实例,以便链式注册。</returns>
/// <exception cref="ArgumentException">
/// 当 <paramref name="tableName" /> 或 <paramref name="relativePath" /> 为 null、空字符串或空白字符串时抛出。
/// </exception>
/// <exception cref="ArgumentNullException">当 <paramref name="keySelector" /> 为 null 时抛出。</exception>
public YamlConfigLoader RegisterTable<TKey, TValue>( public YamlConfigLoader RegisterTable<TKey, TValue>(
string tableName, string tableName,
string relativePath, string relativePath,
@ -142,7 +185,11 @@ public sealed class YamlConfigLoader : IConfigLoader
IEqualityComparer<TKey>? comparer = null) IEqualityComparer<TKey>? comparer = null)
where TKey : notnull where TKey : notnull
{ {
return RegisterTableCore(tableName, relativePath, null, keySelector, comparer); return RegisterTable(
new YamlConfigTableRegistrationOptions<TKey, TValue>(tableName, relativePath, keySelector)
{
Comparer = comparer
});
} }
/// <summary> /// <summary>
@ -158,6 +205,11 @@ public sealed class YamlConfigLoader : IConfigLoader
/// <param name="keySelector">配置项主键提取器。</param> /// <param name="keySelector">配置项主键提取器。</param>
/// <param name="comparer">可选主键比较器。</param> /// <param name="comparer">可选主键比较器。</param>
/// <returns>当前加载器实例,以便链式注册。</returns> /// <returns>当前加载器实例,以便链式注册。</returns>
/// <exception cref="ArgumentException">
/// 当 <paramref name="tableName" />、<paramref name="relativePath" /> 或 <paramref name="schemaRelativePath" />
/// 为 null、空字符串或空白字符串时抛出。
/// </exception>
/// <exception cref="ArgumentNullException">当 <paramref name="keySelector" /> 为 null 时抛出。</exception>
public YamlConfigLoader RegisterTable<TKey, TValue>( public YamlConfigLoader RegisterTable<TKey, TValue>(
string tableName, string tableName,
string relativePath, string relativePath,
@ -166,7 +218,40 @@ public sealed class YamlConfigLoader : IConfigLoader
IEqualityComparer<TKey>? comparer = null) IEqualityComparer<TKey>? comparer = null)
where TKey : notnull where TKey : notnull
{ {
return RegisterTableCore(tableName, relativePath, schemaRelativePath, keySelector, comparer); return RegisterTable(
new YamlConfigTableRegistrationOptions<TKey, TValue>(tableName, relativePath, keySelector)
{
SchemaRelativePath = schemaRelativePath,
Comparer = comparer
});
}
/// <summary>
/// 使用选项对象注册一个 YAML 配置表定义。
/// 该入口集中承载配置目录、schema 路径、主键提取器和比较器,
/// 以避免未来继续为新增开关叠加更多重载。
/// </summary>
/// <typeparam name="TKey">配置主键类型。</typeparam>
/// <typeparam name="TValue">配置值类型。</typeparam>
/// <param name="options">配置表注册选项。</param>
/// <returns>当前加载器实例,以便链式注册。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="options" /> 为空时抛出。</exception>
/// <exception cref="ArgumentException">
/// 当 <paramref name="options" /> 内的 <see cref="YamlConfigTableRegistrationOptions{TKey, TValue}.TableName" />、
/// <see cref="YamlConfigTableRegistrationOptions{TKey, TValue}.RelativePath" /> 或
/// <see cref="YamlConfigTableRegistrationOptions{TKey, TValue}.SchemaRelativePath" /> 为 null、空字符串或空白字符串时抛出。
/// </exception>
public YamlConfigLoader RegisterTable<TKey, TValue>(YamlConfigTableRegistrationOptions<TKey, TValue> options)
where TKey : notnull
{
ArgumentNullException.ThrowIfNull(options);
return RegisterTableCore(
options.TableName,
options.RelativePath,
options.SchemaRelativePath,
options.KeySelector,
options.Comparer);
} }
private YamlConfigLoader RegisterTableCore<TKey, TValue>( private YamlConfigLoader RegisterTableCore<TKey, TValue>(

View File

@ -0,0 +1,73 @@
namespace GFramework.Game.Config;
/// <summary>
/// 描述一个 YAML 配置表注册项的参数集合。
/// 该选项对象用于替代不断增加的位置参数重载,
/// 让消费者在启用 schema 校验、主键比较器或未来扩展项时仍能保持调用点可读。
/// </summary>
/// <typeparam name="TKey">配置主键类型。</typeparam>
/// <typeparam name="TValue">配置值类型。</typeparam>
public sealed class YamlConfigTableRegistrationOptions<TKey, TValue>
where TKey : notnull
{
private const string TableNameCannotBeNullOrWhiteSpaceMessage = "Table name cannot be null or whitespace.";
private const string RelativePathCannotBeNullOrWhiteSpaceMessage = "Relative path cannot be null or whitespace.";
/// <summary>
/// 使用最小必需参数创建配置表注册选项。
/// </summary>
/// <param name="tableName">运行时配置表名称。</param>
/// <param name="relativePath">相对配置根目录的子目录。</param>
/// <param name="keySelector">配置项主键提取器。</param>
/// <exception cref="ArgumentException">
/// 当 <paramref name="tableName" /> 或 <paramref name="relativePath" /> 为 null、空字符串或空白字符串时抛出。
/// </exception>
/// <exception cref="ArgumentNullException">当 <paramref name="keySelector" /> 为 null 时抛出。</exception>
public YamlConfigTableRegistrationOptions(
string tableName,
string relativePath,
Func<TValue, TKey> keySelector)
{
if (string.IsNullOrWhiteSpace(tableName))
{
throw new ArgumentException(TableNameCannotBeNullOrWhiteSpaceMessage, nameof(tableName));
}
if (string.IsNullOrWhiteSpace(relativePath))
{
throw new ArgumentException(RelativePathCannotBeNullOrWhiteSpaceMessage, nameof(relativePath));
}
ArgumentNullException.ThrowIfNull(keySelector);
TableName = tableName;
RelativePath = relativePath;
KeySelector = keySelector;
}
/// <summary>
/// 获取运行时配置表名称。
/// </summary>
public string TableName { get; }
/// <summary>
/// 获取相对配置根目录的子目录。
/// </summary>
public string RelativePath { get; }
/// <summary>
/// 获取相对配置根目录的 schema 文件路径。
/// 当该值为空时,当前注册项不会启用 schema 校验。
/// </summary>
public string? SchemaRelativePath { get; init; }
/// <summary>
/// 获取配置项主键提取器。
/// </summary>
public Func<TValue, TKey> KeySelector { get; }
/// <summary>
/// 获取可选的主键比较器。
/// </summary>
public IEqualityComparer<TKey>? Comparer { get; init; }
}

View File

@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
using System.Reflection;
using GFramework.Core.Abstractions.Storage; using GFramework.Core.Abstractions.Storage;
using GFramework.Core.Extensions; using GFramework.Core.Extensions;
using GFramework.Core.Utility; using GFramework.Core.Utility;
@ -21,13 +22,17 @@ using GFramework.Game.Extensions;
namespace GFramework.Game.Data; namespace GFramework.Game.Data;
/// <summary> /// <summary>
/// 数据仓库类,用于管理游戏数据的存储和读取 /// 数据仓库类,用于管理游戏数据的存储和读取
/// </summary> /// </summary>
/// <param name="storage">存储接口实例</param> /// <param name="storage">存储接口实例</param>
/// <param name="options">数据仓库配置选项</param> /// <param name="options">数据仓库配置选项</param>
public class DataRepository(IStorage? storage, DataRepositoryOptions? options = null) public class DataRepository(IStorage? storage, DataRepositoryOptions? options = null)
: AbstractContextUtility, IDataRepository : AbstractContextUtility, IDataRepository
{ {
private static readonly MethodInfo SaveCoreGenericMethod =
typeof(DataRepository).GetMethod(nameof(SaveCoreAsync), BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException($"Method {nameof(SaveCoreAsync)} not found.");
private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions(); private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions();
private IStorage? _storage = storage; private IStorage? _storage = storage;
@ -65,20 +70,7 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
public async Task SaveAsync<T>(IDataLocation location, T data) public async Task SaveAsync<T>(IDataLocation location, T data)
where T : class, IData where T : class, IData
{ {
var key = location.ToStorageKey(); await SaveCoreAsync(location, data, emitSavedEvent: true);
// 自动备份
if (_options.AutoBackup && await Storage.ExistsAsync(key))
{
var backupKey = $"{key}.backup";
var existing = await Storage.ReadAsync<T>(key);
await Storage.WriteAsync(backupKey, existing);
}
await Storage.WriteAsync(key, data);
if (_options.EnableEvents)
this.SendEvent(new DataSavedEvent<T>(data));
} }
/// <summary> /// <summary>
@ -98,6 +90,12 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
public async Task DeleteAsync(IDataLocation location) public async Task DeleteAsync(IDataLocation location)
{ {
var key = location.ToStorageKey(); var key = location.ToStorageKey();
if (!await Storage.ExistsAsync(key))
{
return;
}
await Storage.DeleteAsync(key); await Storage.DeleteAsync(key);
if (_options.EnableEvents) if (_options.EnableEvents)
this.SendEvent(new DataDeletedEvent(location)); this.SendEvent(new DataDeletedEvent(location));
@ -110,7 +108,13 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
public async Task SaveAllAsync(IEnumerable<(IDataLocation location, IData data)> dataList) public async Task SaveAllAsync(IEnumerable<(IDataLocation location, IData data)> dataList)
{ {
var valueTuples = dataList.ToList(); var valueTuples = dataList.ToList();
foreach (var (location, data) in valueTuples) await SaveAsync(location, data);
// 批量保存对订阅者而言应视为一次显式提交,因此这里复用底层保存逻辑,
// 但抑制逐项 DataSavedEvent避免监听器对同一批次收到重复语义的事件。
foreach (var (location, data) in valueTuples)
{
await SaveCoreUntypedAsync(location, data, emitSavedEvent: false);
}
if (_options.EnableEvents) if (_options.EnableEvents)
this.SendEvent(new DataBatchSavedEvent(valueTuples)); this.SendEvent(new DataBatchSavedEvent(valueTuples));
@ -123,4 +127,56 @@ public class DataRepository(IStorage? storage, DataRepositoryOptions? options =
{ {
_storage ??= this.GetUtility<IStorage>()!; _storage ??= this.GetUtility<IStorage>()!;
} }
}
/// <summary>
/// 执行单项保存的共享流程,并根据调用入口决定是否发送单项保存事件。
/// </summary>
/// <typeparam name="T">数据类型。</typeparam>
/// <param name="location">目标数据位置。</param>
/// <param name="data">要保存的数据对象。</param>
/// <param name="emitSavedEvent">是否在成功写入后发送单项保存事件。</param>
private async Task SaveCoreAsync<T>(IDataLocation location, T data, bool emitSavedEvent)
where T : class, IData
{
var key = location.ToStorageKey();
await BackupIfNeededAsync<T>(key);
await Storage.WriteAsync(key, data);
if (emitSavedEvent && _options.EnableEvents)
{
this.SendEvent(new DataSavedEvent<T>(data));
}
}
/// <summary>
/// 在覆盖旧值前为当前存储键创建备份。
/// </summary>
/// <param name="key">即将被覆盖的存储键。</param>
private async Task BackupIfNeededAsync<T>(string key)
where T : class, IData
{
if (!_options.AutoBackup || !await Storage.ExistsAsync(key))
{
return;
}
var backupKey = $"{key}.backup";
var existing = await Storage.ReadAsync<T>(key);
await Storage.WriteAsync(backupKey, existing);
}
/// <summary>
/// 使用数据对象的运行时类型执行保存流程,避免批量保存时因为编译期类型退化为 <see cref="IData" /> 而破坏备份反序列化。
/// </summary>
/// <param name="location">目标数据位置。</param>
/// <param name="data">要保存的数据对象。</param>
/// <param name="emitSavedEvent">是否发送单项保存事件。</param>
private Task SaveCoreUntypedAsync(IDataLocation location, IData data, bool emitSavedEvent)
{
ArgumentNullException.ThrowIfNull(data);
var closedMethod = SaveCoreGenericMethod.MakeGenericMethod(data.GetType());
return (Task)closedMethod.Invoke(this, [location, data, emitSavedEvent])!;
}
}

View File

@ -11,7 +11,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using GFramework.Core.Abstractions.Storage; using GFramework.Core.Abstractions.Storage;
using GFramework.Core.Utility; using GFramework.Core.Utility;
using GFramework.Game.Abstractions.Data; using GFramework.Game.Abstractions.Data;
@ -27,6 +31,8 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
where TSaveData : class, IData, new() where TSaveData : class, IData, new()
{ {
private readonly SaveConfiguration _config; private readonly SaveConfiguration _config;
private readonly Dictionary<int, ISaveMigration<TSaveData>> _migrations = new();
private readonly object _migrationsLock = new();
private readonly IStorage _rootStorage; private readonly IStorage _rootStorage;
/// <summary> /// <summary>
@ -43,6 +49,47 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
_rootStorage = new ScopedStorage(storage, config.SaveRoot); _rootStorage = new ScopedStorage(storage, config.SaveRoot);
} }
/// <summary>
/// 注册存档迁移器,使仓库在加载旧版本存档时自动执行升级。
/// </summary>
/// <param name="migration">要注册的存档迁移器。</param>
/// <returns>当前存档仓库实例,支持链式调用。</returns>
/// <exception cref="ArgumentNullException"><paramref name="migration" /> 为 <see langword="null" />。</exception>
/// <exception cref="InvalidOperationException">
/// <typeparamref name="TSaveData" /> 未实现 <see cref="IVersionedData" />,无法使用版本化迁移。
/// 或者同一个源版本已经注册过迁移器,导致迁移链配置存在歧义。
/// </exception>
/// <exception cref="ArgumentException">迁移器的目标版本不大于源版本。</exception>
/// <remarks>
/// 迁移注册表是可变共享状态。注册与加载可以并发发生,因此所有访问都通过 <see cref="_migrationsLock" />
/// 串行化,避免读写竞争和“部分可见”的迁移链。
/// </remarks>
public ISaveRepository<TSaveData> RegisterMigration(ISaveMigration<TSaveData> migration)
{
ArgumentNullException.ThrowIfNull(migration);
EnsureVersionedSaveType();
if (migration.ToVersion <= migration.FromVersion)
{
throw new ArgumentException(
$"Migration for {typeof(TSaveData).Name} must advance the version number.",
nameof(migration));
}
lock (_migrationsLock)
{
if (_migrations.ContainsKey(migration.FromVersion))
{
throw new InvalidOperationException(
$"Duplicate save migration registration for {typeof(TSaveData).Name} from version {migration.FromVersion}.");
}
_migrations.Add(migration.FromVersion, migration);
}
return this;
}
/// <summary> /// <summary>
/// 检查指定槽位是否存在存档 /// 检查指定槽位是否存在存档
/// </summary> /// </summary>
@ -64,7 +111,10 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
var storage = GetSlotStorage(slot); var storage = GetSlotStorage(slot);
if (await storage.ExistsAsync(_config.SaveFileName)) if (await storage.ExistsAsync(_config.SaveFileName))
return await storage.ReadAsync<TSaveData>(_config.SaveFileName); {
var loaded = await storage.ReadAsync<TSaveData>(_config.SaveFileName);
return await MigrateIfNeededAsync(slot, storage, loaded);
}
return new TSaveData(); return new TSaveData();
} }
@ -137,10 +187,121 @@ public class SaveRepository<TSaveData> : AbstractContextUtility, ISaveRepository
return new ScopedStorage(_rootStorage, $"{_config.SaveSlotPrefix}{slot}"); return new ScopedStorage(_rootStorage, $"{_config.SaveSlotPrefix}{slot}");
} }
/// <summary>
/// 在加载旧版本存档时按注册顺序执行迁移,并在成功后自动回写升级结果。
/// </summary>
/// <param name="slot">当前加载的存档槽位。</param>
/// <param name="storage">对应槽位的存储对象。</param>
/// <param name="data">原始加载出来的存档数据。</param>
/// <returns>迁移后的最新存档;如果无需迁移则返回原始对象。</returns>
/// <exception cref="InvalidOperationException">
/// 当前运行时缺少必要的迁移链、读取到更高版本的存档,或迁移器返回了非法版本。
/// </exception>
private async Task<TSaveData> MigrateIfNeededAsync(int slot, IStorage storage, TSaveData data)
{
if (data is not IVersionedData versionedData)
{
return data;
}
var latestTemplate = new TSaveData();
if (latestTemplate is not IVersionedData latestVersionedData)
{
return data;
}
var currentVersion = versionedData.Version;
var targetVersion = latestVersionedData.Version;
if (currentVersion > targetVersion)
{
throw new InvalidOperationException(
$"Save slot {slot} for {typeof(TSaveData).Name} is version {currentVersion}, " +
$"which is newer than the current runtime version {targetVersion}.");
}
if (currentVersion == targetVersion)
{
return data;
}
EnsureVersionedSaveType();
var migrated = data;
// 迁移链按“当前版本 -> 下一个已注册迁移器”推进;任何缺口都表示运行时无法安全解释旧存档。
// 读取迁移表时使用同一把锁,保证并发注册不会让加载线程看到不一致的链路状态。
while (currentVersion < targetVersion)
{
ISaveMigration<TSaveData>? migration;
lock (_migrationsLock)
{
_migrations.TryGetValue(currentVersion, out migration);
}
if (migration is null)
{
throw new InvalidOperationException(
$"No save migration is registered for {typeof(TSaveData).Name} from version {currentVersion}.");
}
migrated = migration.Migrate(migrated) ??
throw new InvalidOperationException(
$"Save migration for {typeof(TSaveData).Name} from version {currentVersion} returned null.");
if (migrated is not IVersionedData migratedVersionedData)
{
throw new InvalidOperationException(
$"Save migration for {typeof(TSaveData).Name} must return data implementing {nameof(IVersionedData)}.");
}
// 显式校验迁移器声明与实际结果,避免版本号不前进导致死循环或把旧数据错误回写为“已升级”。
if (migration.ToVersion != migratedVersionedData.Version)
{
throw new InvalidOperationException(
$"Save migration for {typeof(TSaveData).Name} declared target version {migration.ToVersion} " +
$"but returned version {migratedVersionedData.Version}.");
}
if (migratedVersionedData.Version <= currentVersion)
{
throw new InvalidOperationException(
$"Save migration for {typeof(TSaveData).Name} must advance beyond version {currentVersion}.");
}
if (migratedVersionedData.Version > targetVersion)
{
throw new InvalidOperationException(
$"Save migration for {typeof(TSaveData).Name} produced version {migratedVersionedData.Version}, " +
$"which exceeds the current runtime version {targetVersion}.");
}
currentVersion = migratedVersionedData.Version;
}
await storage.WriteAsync(_config.SaveFileName, migrated);
return migrated;
}
/// <summary>
/// 验证当前存档类型支持基于版本号的迁移流程。
/// </summary>
/// <exception cref="InvalidOperationException">
/// <typeparamref name="TSaveData" /> 未实现 <see cref="IVersionedData" />。
/// </exception>
private static void EnsureVersionedSaveType()
{
if (!typeof(IVersionedData).IsAssignableFrom(typeof(TSaveData)))
{
throw new InvalidOperationException(
$"{typeof(TSaveData).Name} must implement {nameof(IVersionedData)} to use save migrations.");
}
}
/// <summary> /// <summary>
/// 初始化逻辑 /// 初始化逻辑
/// </summary> /// </summary>
protected override void OnInit() protected override void OnInit()
{ {
} }
} }

View File

@ -21,8 +21,13 @@ using GFramework.Game.Abstractions.Data.Events;
namespace GFramework.Game.Data; namespace GFramework.Game.Data;
/// <summary> /// <summary>
/// 使用单一文件存储所有设置数据的仓库实现 /// 使用单一文件存储所有设置数据的仓库实现
/// </summary> /// </summary>
/// <remarks>
/// 该仓库通过内存缓存聚合所有设置 section并在公开的保存或删除操作发生时整文件回写。
/// 虽然底层不是“一项一个文件”,但它仍遵循 <see cref="DataRepositoryOptions" /> 定义的统一契约:
/// 启用自动备份时,覆盖写入前会为整个统一文件创建单份备份;批量保存只发出批量事件,不重复发出单项保存事件。
/// </remarks>
public class UnifiedSettingsDataRepository( public class UnifiedSettingsDataRepository(
IStorage? storage, IStorage? storage,
IRuntimeTypeSerializer? serializer, IRuntimeTypeSerializer? serializer,
@ -66,7 +71,7 @@ public class UnifiedSettingsDataRepository(
var key = location.Key; var key = location.Key;
var result = _file!.Sections.TryGetValue(key, out var raw) ? Serializer.Deserialize<T>(raw) : new T(); var result = _file!.Sections.TryGetValue(key, out var raw) ? Serializer.Deserialize<T>(raw) : new T();
if (_options.EnableEvents) if (_options.EnableEvents)
this.SendEvent(new DataLoadedEvent<IData>(result)); this.SendEvent(new DataLoadedEvent<T>(result));
return result; return result;
} }
@ -81,21 +86,11 @@ public class UnifiedSettingsDataRepository(
where T : class, IData where T : class, IData
{ {
await EnsureLoadedAsync(); await EnsureLoadedAsync();
await _lock.WaitAsync(); await MutateAndPersistAsync(file => file.Sections[location.Key] = Serializer.Serialize(data));
try
{
var key = location.Key;
var serialized = Serializer.Serialize(data);
_file!.Sections[key] = serialized; if (_options.EnableEvents)
await Storage.WriteAsync(UnifiedKey, _file);
if (_options.EnableEvents)
this.SendEvent(new DataSavedEvent<T>(data));
}
finally
{ {
_lock.Release(); this.SendEvent(new DataSavedEvent<T>(data));
} }
} }
@ -118,13 +113,30 @@ public class UnifiedSettingsDataRepository(
public async Task DeleteAsync(IDataLocation location) public async Task DeleteAsync(IDataLocation location)
{ {
await EnsureLoadedAsync(); await EnsureLoadedAsync();
var removed = false;
if (File.Sections.Remove(location.Key)) await _lock.WaitAsync();
try
{ {
await SaveUnifiedFileAsync(); var currentFile = File;
var nextFile = CloneFile(currentFile);
removed = nextFile.Sections.Remove(location.Key);
if (!removed)
{
return;
}
if (_options.EnableEvents) await WriteUnifiedFileCoreAsync(currentFile, nextFile);
this.SendEvent(new DataDeletedEvent(location)); _file = nextFile;
}
finally
{
_lock.Release();
}
if (removed && _options.EnableEvents)
{
this.SendEvent(new DataDeletedEvent(location));
} }
} }
@ -139,16 +151,17 @@ public class UnifiedSettingsDataRepository(
await EnsureLoadedAsync(); await EnsureLoadedAsync();
var valueTuples = dataList.ToList(); var valueTuples = dataList.ToList();
foreach (var (location, data) in valueTuples)
{
var serialized = Serializer.Serialize(data);
File.Sections[location.Key] = serialized;
}
await SaveUnifiedFileAsync(); await MutateAndPersistAsync(file =>
{
foreach (var (location, data) in valueTuples)
{
file.Sections[location.Key] = Serializer.Serialize(data);
}
});
if (_options.EnableEvents) if (_options.EnableEvents)
this.SendEvent(new DataBatchSavedEvent(valueTuples.ToList())); this.SendEvent(new DataBatchSavedEvent(valueTuples));
} }
/// <summary> /// <summary>
@ -226,12 +239,19 @@ public class UnifiedSettingsDataRepository(
/// <summary> /// <summary>
/// 将缓存中的所有数据保存到统一文件 /// 将缓存中的所有数据保存到统一文件
/// </summary> /// </summary>
private async Task SaveUnifiedFileAsync() private async Task MutateAndPersistAsync(Action<UnifiedSettingsFile> mutation)
{ {
await _lock.WaitAsync(); await _lock.WaitAsync();
try try
{ {
await Storage.WriteAsync(UnifiedKey, _file); var currentFile = File;
var nextFile = CloneFile(currentFile);
// 先在副本上计算“下一份已提交状态”,只有底层持久化成功后才交换缓存,
// 这样即使备份或写入失败,也不会把未提交修改留在内存快照里。
mutation(nextFile);
await WriteUnifiedFileCoreAsync(currentFile, nextFile);
_file = nextFile;
} }
finally finally
{ {
@ -240,11 +260,47 @@ public class UnifiedSettingsDataRepository(
} }
/// <summary> /// <summary>
/// 获取统一文件的存储键名 /// 将当前缓存快照写回底层存储,并在需要时创建整个文件的备份。
/// </summary> /// </summary>
/// <returns>完整的存储键名</returns> /// <remarks>
/// 该方法要求调用方已经持有 <see cref="_lock" />,以保证“读取当前快照 -> 写入备份 -> 提交新快照”的原子提交顺序。
/// 只有在该方法成功返回后,调用方才应交换内存中的 <see cref="_file" /> 引用。
/// </remarks>
/// <param name="currentFile">当前已提交的统一文件快照。</param>
/// <param name="nextFile">即将提交的新统一文件快照。</param>
private async Task WriteUnifiedFileCoreAsync(UnifiedSettingsFile currentFile, UnifiedSettingsFile nextFile)
{
if (_options.AutoBackup && await Storage.ExistsAsync(UnifiedKey))
{
var backupKey = $"{UnifiedKey}.backup";
await Storage.WriteAsync(backupKey, currentFile);
}
await Storage.WriteAsync(UnifiedKey, nextFile);
}
/// <summary>
/// 复制当前统一文件快照,确保未提交修改不会污染内存中的已提交状态。
/// </summary>
/// <param name="source">要复制的统一文件快照。</param>
/// <returns>包含独立 section 字典的新快照。</returns>
private static UnifiedSettingsFile CloneFile(UnifiedSettingsFile source)
{
ArgumentNullException.ThrowIfNull(source);
return new UnifiedSettingsFile
{
Version = source.Version,
Sections = new Dictionary<string, string>(source.Sections, source.Sections.Comparer)
};
}
/// <summary>
/// 获取统一文件的存储键名。
/// </summary>
/// <returns>完整的存储键名。</returns>
protected virtual string GetUnifiedKey() protected virtual string GetUnifiedKey()
{ {
return string.IsNullOrEmpty(_options.BasePath) ? fileName : $"{_options.BasePath}/{fileName}"; return string.IsNullOrEmpty(_options.BasePath) ? fileName : $"{_options.BasePath}/{fileName}";
} }
} }

View File

@ -0,0 +1,73 @@
using System.Collections.Generic;
using GFramework.Godot.Coroutine;
using NUnit.Framework;
namespace GFramework.Godot.Tests.Coroutine;
/// <summary>
/// GodotTimeSource 的单元测试。
/// </summary>
[TestFixture]
public sealed class GodotTimeSourceTests
{
/// <summary>
/// 验证增量模式会直接累加传入的 delta。
/// </summary>
[Test]
public void Update_Should_Accumulate_Delta_When_Using_Delta_Mode()
{
var values = new Queue<double>([0.1, 0.2]);
var timeSource = new GodotTimeSource(() => values.Dequeue());
timeSource.Update();
Assert.That(timeSource.DeltaTime, Is.EqualTo(0.1).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(0.1).Within(0.0001));
timeSource.Update();
Assert.That(timeSource.DeltaTime, Is.EqualTo(0.2).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(0.3).Within(0.0001));
}
/// <summary>
/// 验证绝对时间模式会根据前后两次采样计算 delta。
/// </summary>
[Test]
public void Update_Should_Calculate_Delta_When_Using_Absolute_Time_Mode()
{
var values = new Queue<double>([1.0, 1.25, 2.0]);
var timeSource = new GodotTimeSource(() => values.Dequeue(), useAbsoluteTime: true);
timeSource.Update();
Assert.That(timeSource.DeltaTime, Is.EqualTo(0).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(1.0).Within(0.0001));
timeSource.Update();
Assert.That(timeSource.DeltaTime, Is.EqualTo(0.25).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(1.25).Within(0.0001));
timeSource.Update();
Assert.That(timeSource.DeltaTime, Is.EqualTo(0.75).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(2.0).Within(0.0001));
}
/// <summary>
/// 验证绝对时间源在回拨时仍保持单调,不会把 CurrentTime 拉回去。
/// </summary>
[Test]
public void Update_Should_Keep_Absolute_Time_Monotonic_When_Provider_Goes_Backwards()
{
var values = new Queue<double>([5.0, 4.0, 6.5]);
var timeSource = new GodotTimeSource(() => values.Dequeue(), useAbsoluteTime: true);
timeSource.Update();
timeSource.Update();
Assert.That(timeSource.DeltaTime, Is.EqualTo(0).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(5.0).Within(0.0001));
timeSource.Update();
Assert.That(timeSource.DeltaTime, Is.EqualTo(1.5).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(6.5).Within(0.0001));
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0"/>
<PackageReference Include="NUnit" Version="4.5.1"/>
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Godot\GFramework.Godot.csproj"/>
</ItemGroup>
</Project>

View File

@ -35,6 +35,25 @@ public static class CoroutineNodeExtensions
return Timing.RunCoroutine(coroutine, segment, tag); return Timing.RunCoroutine(coroutine, segment, tag);
} }
/// <summary>
/// 以指定节点作为生命周期所有者运行协程。
/// </summary>
/// <param name="owner">拥有该协程生命周期的节点。</param>
/// <param name="coroutine">要启动的协程枚举器。</param>
/// <param name="segment">协程运行的时间段。</param>
/// <param name="tag">协程标签。</param>
/// <param name="cancellationToken">可选取消令牌。</param>
/// <returns>返回协程句柄。</returns>
public static CoroutineHandle RunCoroutine(
this Node owner,
IEnumerator<IYieldInstruction> coroutine,
Segment segment = Segment.Process,
string? tag = null,
CancellationToken cancellationToken = default)
{
return Timing.RunOwnedCoroutine(owner, coroutine, segment, tag, cancellationToken);
}
/// <summary> /// <summary>
/// 让协程在指定节点被销毁时自动取消。 /// 让协程在指定节点被销毁时自动取消。
/// </summary> /// </summary>

View File

@ -3,40 +3,80 @@
namespace GFramework.Godot.Coroutine; namespace GFramework.Godot.Coroutine;
/// <summary> /// <summary>
/// Godot时间源实现用于提供基于Godot引擎的时间信息 /// Godot 时间源实现,用于为协程调度器提供缩放时间或真实时间数据。
/// </summary> /// </summary>
/// <param name="getDeltaFunc">获取增量时间的函数委托</param> /// <param name="timeProvider">
public class GodotTimeSource(Func<double> getDeltaFunc) : ITimeSource /// 时间提供函数。
/// 在默认模式下该函数返回“本帧增量”;在绝对时间模式下该函数返回“当前绝对时间(秒)”。
/// </param>
/// <param name="useAbsoluteTime">
/// 是否把 <paramref name="timeProvider" /> 返回值解释为绝对时间。
/// 启用后,<see cref="Update" /> 会通过相邻两次读数计算 <see cref="DeltaTime" />。
/// </param>
public sealed class GodotTimeSource(Func<double> timeProvider, bool useAbsoluteTime = false) : ITimeSource
{ {
private readonly Func<double> _getDeltaFunc = getDeltaFunc ?? throw new ArgumentNullException(nameof(getDeltaFunc)); private readonly Func<double> _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
private bool _initialized;
private double _lastAbsoluteTime;
/// <summary> /// <summary>
/// 获取当前累计时间 /// 获取当前累计时间
/// </summary> /// </summary>
public double CurrentTime { get; private set; } public double CurrentTime { get; private set; }
/// <summary> /// <summary>
/// 获取上一帧的时间增量 /// 获取上一帧的时间增量
/// </summary> /// </summary>
public double DeltaTime { get; private set; } public double DeltaTime { get; private set; }
/// <summary> /// <summary>
/// 更新时间源,计算新的增量时间和累计时间 /// 更新时间源,计算新的时间增量与累计时间。
/// </summary> /// </summary>
public void Update() public void Update()
{ {
// 调用外部提供的函数获取当前帧的时间增量 var value = _timeProvider();
DeltaTime = _getDeltaFunc(); if (useAbsoluteTime)
// 累加到总时间中 {
if (!_initialized)
{
_initialized = true;
_lastAbsoluteTime = value;
CurrentTime = value;
DeltaTime = 0;
return;
}
// 对绝对时间源做单调钳制,避免 provider 回拨后把 CurrentTime 也拉回去。
var nextTime = Math.Max(value, _lastAbsoluteTime);
DeltaTime = nextTime - _lastAbsoluteTime;
_lastAbsoluteTime = nextTime;
CurrentTime = nextTime;
return;
}
DeltaTime = value;
CurrentTime += DeltaTime; CurrentTime += DeltaTime;
} }
/// <summary> /// <summary>
/// 重置时间源到初始状态 /// 创建基于 Godot 单调时钟的真实时间源。
/// </summary>
/// <returns>返回一个不受场景暂停与时间缩放影响的时间源实例。</returns>
public static GodotTimeSource CreateRealtime()
{
return new GodotTimeSource(
() => Time.GetTicksUsec() / 1_000_000.0,
useAbsoluteTime: true);
}
/// <summary>
/// 重置时间源到初始状态。
/// </summary> /// </summary>
public void Reset() public void Reset()
{ {
CurrentTime = 0; CurrentTime = 0;
DeltaTime = 0; DeltaTime = 0;
_initialized = false;
_lastAbsoluteTime = 0;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -132,7 +132,8 @@ public class SchemaConfigGeneratorSnapshotTests
"type": "string", "type": "string",
"description": "Monster reference id.", "description": "Monster reference id.",
"minLength": 2, "minLength": 2,
"maxLength": 32 "maxLength": 32,
"x-gframework-ref-table": "monster"
} }
} }
} }

View File

@ -91,4 +91,264 @@ public class SchemaConfigGeneratorTests
Assert.That(diagnostic.GetMessage(), Does.Contain("array<array>")); Assert.That(diagnostic.GetMessage(), Does.Contain("array<array>"));
}); });
} }
}
/// <summary>
/// 验证 schema 字段名无法映射为合法 C# 标识符时会直接给出诊断,而不是生成不可编译代码。
/// </summary>
/// <param name="schemaKey">会映射为非法 C# 标识符的 schema key。</param>
/// <param name="generatedIdentifier">当前命名规范化逻辑生成出的非法标识符。</param>
[TestCase("drop$item", "Drop$item")]
[TestCase("1-phase", "1Phase")]
public void Run_Should_Report_Diagnostic_When_Schema_Key_Maps_To_Invalid_CSharp_Identifier(
string schemaKey,
string generatedIdentifier)
{
const string source = """
namespace TestApp
{
public sealed class Dummy
{
}
}
""";
var schema = $$"""
{
"type": "object",
"required": ["id", "{{schemaKey}}"],
"properties": {
"id": { "type": "integer" },
"{{schemaKey}}": { "type": "string" }
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
var diagnostic = result.Results.Single().Diagnostics.Single();
Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_006"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain(schemaKey));
Assert.That(diagnostic.GetMessage(), Does.Contain(generatedIdentifier));
});
}
/// <summary>
/// 验证引用元数据成员名在不同路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。
/// </summary>
[Test]
public void Run_Should_Assign_Globally_Unique_Reference_Metadata_Member_Names()
{
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 schema = """
{
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "integer" },
"drop-items": {
"type": "array",
"items": { "type": "string" },
"x-gframework-ref-table": "item"
},
"drop_items": {
"type": "array",
"items": { "type": "string" },
"x-gframework-ref-table": "item"
},
"dropItems1": {
"type": "string",
"x-gframework-ref-table": "item"
}
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
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("MonsterConfigBindings.g.cs", out var bindingsSource), Is.True);
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems ="));
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems1 ="));
Assert.That(bindingsSource, Does.Contain("public static readonly ReferenceMetadata DropItems11 ="));
}
/// <summary>
/// 验证生成器只为顶层非主键标量字段生成轻量查询辅助,
/// 避免把数组、对象和引用字段误生成为查询 API。
/// </summary>
[Test]
public void Run_Should_Generate_Query_Helpers_Only_For_Top_Level_Scalar_Properties()
{
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 schema = """
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"hp": { "type": "integer" },
"dropItems": {
"type": "array",
"items": { "type": "string" }
},
"targetId": {
"type": "string",
"x-gframework-ref-table": "monster"
},
"reward": {
"type": "object",
"properties": {
"gold": { "type": "integer" }
}
}
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
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("MonsterTable.g.cs", out var tableSource), Is.True);
Assert.Multiple(() =>
{
Assert.That(tableSource, Does.Contain("FindByName(string value)"));
Assert.That(tableSource, Does.Contain("TryFindFirstByName(string value, out MonsterConfig? result)"));
Assert.That(tableSource, Does.Contain("FindByHp(int? value)"));
Assert.That(tableSource, Does.Contain("TryFindFirstByHp(int? value, out MonsterConfig? result)"));
Assert.That(tableSource, Does.Not.Contain("FindById("));
Assert.That(tableSource, Does.Not.Contain("FindByDropItems("));
Assert.That(tableSource, Does.Not.Contain("FindByTargetId("));
Assert.That(tableSource, Does.Not.Contain("FindByReward("));
Assert.That(tableSource, Does.Not.Contain("TryFindFirstById("));
Assert.That(tableSource, Does.Not.Contain("TryFindFirstByDropItems("));
Assert.That(tableSource, Does.Not.Contain("TryFindFirstByTargetId("));
Assert.That(tableSource, Does.Not.Contain("TryFindFirstByReward("));
});
}
}

View File

@ -117,6 +117,7 @@ public sealed partial class MonsterConfig
/// <remarks> /// <remarks>
/// Schema property path: 'phases[].monsterId'. /// Schema property path: 'phases[].monsterId'.
/// Constraints: minLength = 2, maxLength = 32. /// Constraints: minLength = 2, maxLength = 32.
/// References config table: 'monster'.
/// Generated default initializer: = string.Empty; /// Generated default initializer: = string.Empty;
/// </remarks> /// </remarks>
public string MonsterId { get; set; } = string.Empty; public string MonsterId { get; set; } = string.Empty;

View File

@ -9,20 +9,157 @@ namespace GFramework.Game.Config.Generated;
/// </summary> /// </summary>
public static class MonsterConfigBindings public static class MonsterConfigBindings
{ {
/// <summary>
/// Describes one schema property that declares <c>x-gframework-ref-table</c> metadata.
/// </summary>
public readonly struct ReferenceMetadata
{
/// <summary>
/// Initializes one generated cross-table reference descriptor.
/// </summary>
/// <param name="displayPath">Schema property path.</param>
/// <param name="referencedTableName">Referenced runtime table name.</param>
/// <param name="valueSchemaType">Schema scalar type used by the reference value.</param>
/// <param name="isCollection">Whether the property stores multiple reference keys.</param>
public ReferenceMetadata(
string displayPath,
string referencedTableName,
string valueSchemaType,
bool isCollection)
{
DisplayPath = displayPath ?? throw new global::System.ArgumentNullException(nameof(displayPath));
ReferencedTableName = referencedTableName ?? throw new global::System.ArgumentNullException(nameof(referencedTableName));
ValueSchemaType = valueSchemaType ?? throw new global::System.ArgumentNullException(nameof(valueSchemaType));
IsCollection = isCollection;
}
/// <summary>
/// Gets the schema property path such as <c>dropItems</c> or <c>phases[].monsterId</c>.
/// </summary>
public string DisplayPath { get; }
/// <summary>
/// Gets the runtime registration name of the referenced config table.
/// </summary>
public string ReferencedTableName { get; }
/// <summary>
/// Gets the schema scalar type used by the referenced key value.
/// </summary>
public string ValueSchemaType { get; }
/// <summary>
/// Gets a value indicating whether the property stores multiple reference keys.
/// </summary>
public bool IsCollection { get; }
}
/// <summary>
/// Groups the schema-derived metadata constants so consumer code can reuse one stable entry point.
/// </summary>
public static class Metadata
{
/// <summary>
/// Gets the logical config domain derived from the schema base name. The current runtime convention keeps this value aligned with the generated table name.
/// </summary>
public const string ConfigDomain = "monster";
/// <summary>
/// Gets the runtime registration name of the generated config table.
/// </summary>
public const string TableName = "monster";
/// <summary>
/// Gets the config directory path expected by the generated registration helper.
/// </summary>
public const string ConfigRelativePath = "monster";
/// <summary>
/// Gets the schema file path expected by the generated registration helper.
/// </summary>
public const string SchemaRelativePath = "schemas/monster.schema.json";
}
/// <summary>
/// Gets the logical config domain derived from the schema base name. The current runtime convention keeps this value aligned with the generated table name.
/// </summary>
public const string ConfigDomain = Metadata.ConfigDomain;
/// <summary> /// <summary>
/// Gets the runtime registration name of the generated config table. /// Gets the runtime registration name of the generated config table.
/// </summary> /// </summary>
public const string TableName = "monster"; public const string TableName = Metadata.TableName;
/// <summary> /// <summary>
/// Gets the config directory path expected by the generated registration helper. /// Gets the config directory path expected by the generated registration helper.
/// </summary> /// </summary>
public const string ConfigRelativePath = "monster"; public const string ConfigRelativePath = Metadata.ConfigRelativePath;
/// <summary> /// <summary>
/// Gets the schema file path expected by the generated registration helper. /// Gets the schema file path expected by the generated registration helper.
/// </summary> /// </summary>
public const string SchemaRelativePath = "schemas/monster.schema.json"; public const string SchemaRelativePath = Metadata.SchemaRelativePath;
/// <summary>
/// Exposes generated metadata for schema properties that declare <c>x-gframework-ref-table</c>.
/// </summary>
public static class References
{
/// <summary>
/// Gets generated reference metadata for schema property path 'dropItems'.
/// </summary>
public static readonly ReferenceMetadata DropItems = new(
"dropItems",
"item",
"string",
true);
/// <summary>
/// Gets generated reference metadata for schema property path 'phases[].monsterId'.
/// </summary>
public static readonly ReferenceMetadata PhasesItemsMonsterId = new(
"phases[].monsterId",
"monster",
"string",
false);
/// <summary>
/// Gets all generated cross-table reference descriptors for the current schema.
/// </summary>
public static global::System.Collections.Generic.IReadOnlyList<ReferenceMetadata> All { get; } = global::System.Array.AsReadOnly(new ReferenceMetadata[]
{
DropItems,
PhasesItemsMonsterId,
});
/// <summary>
/// Tries to resolve generated reference metadata by schema property path.
/// </summary>
/// <param name="displayPath">Schema property path.</param>
/// <param name="metadata">Resolved generated reference metadata when the path is known; otherwise the default value.</param>
/// <returns>True when the schema property path has generated cross-table metadata; otherwise false.</returns>
public static bool TryGetByDisplayPath(string displayPath, out ReferenceMetadata metadata)
{
if (displayPath is null)
{
throw new global::System.ArgumentNullException(nameof(displayPath));
}
if (string.Equals(displayPath, "dropItems", global::System.StringComparison.Ordinal))
{
metadata = DropItems;
return true;
}
if (string.Equals(displayPath, "phases[].monsterId", global::System.StringComparison.Ordinal))
{
metadata = PhasesItemsMonsterId;
return true;
}
metadata = default;
return false;
}
}
/// <summary> /// <summary>
/// Registers the generated config table using the schema-derived runtime conventions. /// Registers the generated config table using the schema-derived runtime conventions.
@ -40,9 +177,9 @@ public static class MonsterConfigBindings
} }
return loader.RegisterTable<int, MonsterConfig>( return loader.RegisterTable<int, MonsterConfig>(
TableName, Metadata.TableName,
ConfigRelativePath, Metadata.ConfigRelativePath,
SchemaRelativePath, Metadata.SchemaRelativePath,
static config => config.Id, static config => config.Id,
comparer); comparer);
} }
@ -60,7 +197,7 @@ public static class MonsterConfigBindings
throw new global::System.ArgumentNullException(nameof(registry)); throw new global::System.ArgumentNullException(nameof(registry));
} }
return new MonsterTable(registry.GetTable<int, MonsterConfig>(TableName)); return new MonsterTable(registry.GetTable<int, MonsterConfig>(Metadata.TableName));
} }
/// <summary> /// <summary>
@ -77,7 +214,7 @@ public static class MonsterConfigBindings
throw new global::System.ArgumentNullException(nameof(registry)); throw new global::System.ArgumentNullException(nameof(registry));
} }
if (registry.TryGetTable<int, MonsterConfig>(TableName, out var innerTable) && innerTable is not null) if (registry.TryGetTable<int, MonsterConfig>(Metadata.TableName, out var innerTable) && innerTable is not null)
{ {
table = new MonsterTable(innerTable); table = new MonsterTable(innerTable);
return true; return true;

View File

@ -52,4 +52,102 @@ public sealed partial class MonsterTable : global::GFramework.Game.Abstractions.
{ {
return _inner.All(); return _inner.All();
} }
/// <summary>
/// Finds all config entries whose property 'name' equals the supplied value.
/// </summary>
/// <param name="value">The property value to match.</param>
/// <returns>A read-only snapshot containing every matching config entry.</returns>
/// <remarks>
/// The generated helper performs a deterministic linear scan over <see cref="All"/> so it stays compatible with runtime hot reload and does not require secondary index infrastructure.
/// </remarks>
public global::System.Collections.Generic.IReadOnlyList<MonsterConfig> FindByName(string value)
{
var matches = new global::System.Collections.Generic.List<MonsterConfig>();
// Scan the current table snapshot on demand so generated helpers stay aligned with reloadable runtime data.
foreach (var candidate in All())
{
if (global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(candidate.Name, value))
{
matches.Add(candidate);
}
}
return matches.Count == 0 ? global::System.Array.Empty<MonsterConfig>() : matches.AsReadOnly();
}
/// <summary>
/// Tries to find the first config entry whose property 'name' equals the supplied value.
/// </summary>
/// <param name="value">The property value to match.</param>
/// <param name="result">The first matching config entry when lookup succeeds; otherwise <see langword="null" />.</param>
/// <returns><see langword="true" /> when a matching config entry is found; otherwise <see langword="false" />.</returns>
/// <remarks>
/// The generated helper walks the same snapshot exposed by <see cref="All"/> and returns the first match in iteration order.
/// </remarks>
public bool TryFindFirstByName(string value, out MonsterConfig? result)
{
// Keep the search path allocation-free for the first-match case by exiting as soon as one entry matches.
foreach (var candidate in All())
{
if (global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(candidate.Name, value))
{
result = candidate;
return true;
}
}
result = null;
return false;
}
/// <summary>
/// Finds all config entries whose property 'hp' equals the supplied value.
/// </summary>
/// <param name="value">The property value to match.</param>
/// <returns>A read-only snapshot containing every matching config entry.</returns>
/// <remarks>
/// The generated helper performs a deterministic linear scan over <see cref="All"/> so it stays compatible with runtime hot reload and does not require secondary index infrastructure.
/// </remarks>
public global::System.Collections.Generic.IReadOnlyList<MonsterConfig> FindByHp(int? value)
{
var matches = new global::System.Collections.Generic.List<MonsterConfig>();
// Scan the current table snapshot on demand so generated helpers stay aligned with reloadable runtime data.
foreach (var candidate in All())
{
if (global::System.Collections.Generic.EqualityComparer<int?>.Default.Equals(candidate.Hp, value))
{
matches.Add(candidate);
}
}
return matches.Count == 0 ? global::System.Array.Empty<MonsterConfig>() : matches.AsReadOnly();
}
/// <summary>
/// Tries to find the first config entry whose property 'hp' equals the supplied value.
/// </summary>
/// <param name="value">The property value to match.</param>
/// <param name="result">The first matching config entry when lookup succeeds; otherwise <see langword="null" />.</param>
/// <returns><see langword="true" /> when a matching config entry is found; otherwise <see langword="false" />.</returns>
/// <remarks>
/// The generated helper walks the same snapshot exposed by <see cref="All"/> and returns the first match in iteration order.
/// </remarks>
public bool TryFindFirstByHp(int? value, out MonsterConfig? result)
{
// Keep the search path allocation-free for the first-match case by exiting as soon as one entry matches.
foreach (var candidate in All())
{
if (global::System.Collections.Generic.EqualityComparer<int?>.Default.Equals(candidate.Hp, value))
{
result = candidate;
return true;
}
}
result = null;
return false;
}
} }

View File

@ -23,6 +23,7 @@
GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_006 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic

View File

@ -253,7 +253,10 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
var title = TryGetMetadataString(property.Value, "title"); var title = TryGetMetadataString(property.Value, "title");
var description = TryGetMetadataString(property.Value, "description"); var description = TryGetMetadataString(property.Value, "description");
var refTableName = TryGetMetadataString(property.Value, "x-gframework-ref-table"); var refTableName = TryGetMetadataString(property.Value, "x-gframework-ref-table");
var propertyName = ToPascalCase(property.Name); if (!TryBuildPropertyIdentifier(filePath, displayPath, property.Name, out var propertyName, out var diagnostic))
{
return ParsedPropertyResult.FromDiagnostic(diagnostic!);
}
switch (schemaType) switch (schemaType)
{ {
@ -557,6 +560,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
private static string GenerateTableClass(SchemaFileSpec schema) private static string GenerateTableClass(SchemaFileSpec schema)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
var queryableProperties = CollectQueryableProperties(schema).ToArray();
builder.AppendLine("// <auto-generated />"); builder.AppendLine("// <auto-generated />");
builder.AppendLine("#nullable enable"); builder.AppendLine("#nullable enable");
builder.AppendLine(); builder.AppendLine();
@ -617,6 +621,15 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" {"); builder.AppendLine(" {");
builder.AppendLine(" return _inner.All();"); builder.AppendLine(" return _inner.All();");
builder.AppendLine(" }"); builder.AppendLine(" }");
foreach (var property in queryableProperties)
{
builder.AppendLine();
AppendFindByPropertyMethod(builder, schema, property);
builder.AppendLine();
AppendTryFindFirstByPropertyMethod(builder, schema, property);
}
builder.AppendLine("}"); builder.AppendLine("}");
return builder.ToString().TrimEnd(); return builder.ToString().TrimEnd();
} }
@ -634,6 +647,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
var getMethodName = $"Get{schema.EntityName}Table"; var getMethodName = $"Get{schema.EntityName}Table";
var tryGetMethodName = $"TryGet{schema.EntityName}Table"; var tryGetMethodName = $"TryGet{schema.EntityName}Table";
var bindingsClassName = $"{schema.EntityName}ConfigBindings"; var bindingsClassName = $"{schema.EntityName}ConfigBindings";
var referenceSpecs = CollectReferenceSpecs(schema.RootObject).ToArray();
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />"); builder.AppendLine("// <auto-generated />");
@ -650,22 +664,202 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine($"public static class {bindingsClassName}"); builder.AppendLine($"public static class {bindingsClassName}");
builder.AppendLine("{"); builder.AppendLine("{");
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Describes one schema property that declares <c>x-gframework-ref-table</c> metadata.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public readonly struct ReferenceMetadata");
builder.AppendLine(" {");
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Initializes one generated cross-table reference descriptor.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"displayPath\">Schema property path.</param>");
builder.AppendLine(" /// <param name=\"referencedTableName\">Referenced runtime table name.</param>");
builder.AppendLine(
" /// <param name=\"valueSchemaType\">Schema scalar type used by the reference value.</param>");
builder.AppendLine(
" /// <param name=\"isCollection\">Whether the property stores multiple reference keys.</param>");
builder.AppendLine(" public ReferenceMetadata(");
builder.AppendLine(" string displayPath,");
builder.AppendLine(" string referencedTableName,");
builder.AppendLine(" string valueSchemaType,");
builder.AppendLine(" bool isCollection)");
builder.AppendLine(" {");
builder.AppendLine(
" DisplayPath = displayPath ?? throw new global::System.ArgumentNullException(nameof(displayPath));");
builder.AppendLine(
" ReferencedTableName = referencedTableName ?? throw new global::System.ArgumentNullException(nameof(referencedTableName));");
builder.AppendLine(
" ValueSchemaType = valueSchemaType ?? throw new global::System.ArgumentNullException(nameof(valueSchemaType));");
builder.AppendLine(" IsCollection = isCollection;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Gets the schema property path such as <c>dropItems</c> or <c>phases[].monsterId</c>.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public string DisplayPath { get; }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the runtime registration name of the referenced config table.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public string ReferencedTableName { get; }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the schema scalar type used by the referenced key value.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public string ValueSchemaType { get; }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Gets a value indicating whether the property stores multiple reference keys.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public bool IsCollection { get; }");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Groups the schema-derived metadata constants so consumer code can reuse one stable entry point.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public static class Metadata");
builder.AppendLine(" {");
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Gets the logical config domain derived from the schema base name. The current runtime convention keeps this value aligned with the generated table name.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public const string ConfigDomain = {SymbolDisplay.FormatLiteral(schema.TableRegistrationName, true)};");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the runtime registration name of the generated config table.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public const string TableName = {SymbolDisplay.FormatLiteral(schema.TableRegistrationName, true)};");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Gets the config directory path expected by the generated registration helper.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public const string ConfigRelativePath = {SymbolDisplay.FormatLiteral(schema.ConfigRelativePath, true)};");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the schema file path expected by the generated registration helper.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public const string SchemaRelativePath = {SymbolDisplay.FormatLiteral(schema.SchemaRelativePath, true)};");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Gets the logical config domain derived from the schema base name. The current runtime convention keeps this value aligned with the generated table name.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public const string ConfigDomain = Metadata.ConfigDomain;");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the runtime registration name of the generated config table."); builder.AppendLine(" /// Gets the runtime registration name of the generated config table.");
builder.AppendLine(" /// </summary>"); builder.AppendLine(" /// </summary>");
builder.AppendLine( builder.AppendLine(" public const string TableName = Metadata.TableName;");
$" public const string TableName = {SymbolDisplay.FormatLiteral(schema.TableRegistrationName, true)};");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the config directory path expected by the generated registration helper."); builder.AppendLine(" /// Gets the config directory path expected by the generated registration helper.");
builder.AppendLine(" /// </summary>"); builder.AppendLine(" /// </summary>");
builder.AppendLine( builder.AppendLine(" public const string ConfigRelativePath = Metadata.ConfigRelativePath;");
$" public const string ConfigRelativePath = {SymbolDisplay.FormatLiteral(schema.ConfigRelativePath, true)};");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the schema file path expected by the generated registration helper."); builder.AppendLine(" /// Gets the schema file path expected by the generated registration helper.");
builder.AppendLine(" /// </summary>"); builder.AppendLine(" /// </summary>");
builder.AppendLine(" public const string SchemaRelativePath = Metadata.SchemaRelativePath;");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine( builder.AppendLine(
$" public const string SchemaRelativePath = {SymbolDisplay.FormatLiteral(schema.SchemaRelativePath, true)};"); " /// Exposes generated metadata for schema properties that declare <c>x-gframework-ref-table</c>.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public static class References");
builder.AppendLine(" {");
foreach (var referenceSpec in referenceSpecs)
{
builder.AppendLine(" /// <summary>");
builder.AppendLine(
$" /// Gets generated reference metadata for schema property path '{EscapeXmlDocumentation(referenceSpec.DisplayPath)}'.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
$" public static readonly ReferenceMetadata {referenceSpec.MemberName} = new(");
builder.AppendLine(
$" {SymbolDisplay.FormatLiteral(referenceSpec.DisplayPath, true)},");
builder.AppendLine(
$" {SymbolDisplay.FormatLiteral(referenceSpec.ReferencedTableName, true)},");
builder.AppendLine(
$" {SymbolDisplay.FormatLiteral(referenceSpec.ValueSchemaType, true)},");
builder.AppendLine(
$" {(referenceSpec.IsCollection ? "true" : "false")});");
builder.AppendLine();
}
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Gets all generated cross-table reference descriptors for the current schema.");
builder.AppendLine(" /// </summary>");
if (referenceSpecs.Length == 0)
{
builder.AppendLine(
" public static global::System.Collections.Generic.IReadOnlyList<ReferenceMetadata> All { get; } = global::System.Array.Empty<ReferenceMetadata>();");
}
else
{
builder.AppendLine(
" public static global::System.Collections.Generic.IReadOnlyList<ReferenceMetadata> All { get; } = global::System.Array.AsReadOnly(new ReferenceMetadata[]");
builder.AppendLine(" {");
foreach (var referenceSpec in referenceSpecs)
{
builder.AppendLine($" {referenceSpec.MemberName},");
}
builder.AppendLine(" });");
}
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Tries to resolve generated reference metadata by schema property path.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"displayPath\">Schema property path.</param>");
builder.AppendLine(
" /// <param name=\"metadata\">Resolved generated reference metadata when the path is known; otherwise the default value.</param>");
builder.AppendLine(
" /// <returns>True when the schema property path has generated cross-table metadata; otherwise false.</returns>");
builder.AppendLine(
" public static bool TryGetByDisplayPath(string displayPath, out ReferenceMetadata metadata)");
builder.AppendLine(" {");
builder.AppendLine(" if (displayPath is null)");
builder.AppendLine(" {");
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(displayPath));");
builder.AppendLine(" }");
builder.AppendLine();
if (referenceSpecs.Length == 0)
{
builder.AppendLine(" metadata = default;");
builder.AppendLine(" return false;");
}
else
{
foreach (var referenceSpec in referenceSpecs)
{
builder.AppendLine(
$" if (string.Equals(displayPath, {SymbolDisplay.FormatLiteral(referenceSpec.DisplayPath, true)}, global::System.StringComparison.Ordinal))");
builder.AppendLine(" {");
builder.AppendLine($" metadata = {referenceSpec.MemberName};");
builder.AppendLine(" return true;");
builder.AppendLine(" }");
}
builder.AppendLine();
builder.AppendLine(" metadata = default;");
builder.AppendLine(" return false;");
}
builder.AppendLine(" }");
builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
builder.AppendLine( builder.AppendLine(
@ -688,9 +882,9 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(); builder.AppendLine();
builder.AppendLine( builder.AppendLine(
$" return loader.RegisterTable<{schema.KeyClrType}, {schema.ClassName}>("); $" return loader.RegisterTable<{schema.KeyClrType}, {schema.ClassName}>(");
builder.AppendLine(" TableName,"); builder.AppendLine(" Metadata.TableName,");
builder.AppendLine(" ConfigRelativePath,"); builder.AppendLine(" Metadata.ConfigRelativePath,");
builder.AppendLine(" SchemaRelativePath,"); builder.AppendLine(" Metadata.SchemaRelativePath,");
builder.AppendLine($" static config => config.{schema.KeyPropertyName},"); builder.AppendLine($" static config => config.{schema.KeyPropertyName},");
builder.AppendLine(" comparer);"); builder.AppendLine(" comparer);");
builder.AppendLine(" }"); builder.AppendLine(" }");
@ -711,7 +905,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine( builder.AppendLine(
$" return new {schema.TableName}(registry.GetTable<{schema.KeyClrType}, {schema.ClassName}>(TableName));"); $" return new {schema.TableName}(registry.GetTable<{schema.KeyClrType}, {schema.ClassName}>(Metadata.TableName));");
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(" /// <summary>"); builder.AppendLine(" /// <summary>");
@ -733,7 +927,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine( builder.AppendLine(
$" if (registry.TryGetTable<{schema.KeyClrType}, {schema.ClassName}>(TableName, out var innerTable) && innerTable is not null)"); $" if (registry.TryGetTable<{schema.KeyClrType}, {schema.ClassName}>(Metadata.TableName, out var innerTable) && innerTable is not null)");
builder.AppendLine(" {"); builder.AppendLine(" {");
builder.AppendLine($" table = new {schema.TableName}(innerTable);"); builder.AppendLine($" table = new {schema.TableName}(innerTable);");
builder.AppendLine(" return true;"); builder.AppendLine(" return true;");
@ -746,6 +940,206 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
return builder.ToString().TrimEnd(); return builder.ToString().TrimEnd();
} }
/// <summary>
/// 收集 schema 中声明的跨表引用元数据,并为生成代码分配稳定成员名。
/// </summary>
/// <param name="rootObject">根对象模型。</param>
/// <returns>生成期引用元数据集合。</returns>
private static IEnumerable<GeneratedReferenceSpec> CollectReferenceSpecs(SchemaObjectSpec rootObject)
{
var nextSuffixByBaseMemberName = new Dictionary<string, int>(StringComparer.Ordinal);
var allocatedMemberNames = new HashSet<string>(StringComparer.Ordinal);
foreach (var referenceSeed in EnumerateReferenceSeeds(rootObject.Properties))
{
var baseMemberName = BuildReferenceMemberName(referenceSeed.DisplayPath);
var memberName = baseMemberName;
if (!allocatedMemberNames.Add(memberName))
{
// Track globally allocated member names because a suffixed duplicate from one path can collide
// with the unsuffixed base name produced by a later, different path.
var duplicateCount = nextSuffixByBaseMemberName.TryGetValue(baseMemberName, out var nextSuffix)
? nextSuffix + 1
: 1;
memberName = $"{baseMemberName}{duplicateCount.ToString(CultureInfo.InvariantCulture)}";
while (!allocatedMemberNames.Add(memberName))
{
duplicateCount++;
memberName = $"{baseMemberName}{duplicateCount.ToString(CultureInfo.InvariantCulture)}";
}
nextSuffixByBaseMemberName[baseMemberName] = duplicateCount;
}
else
{
nextSuffixByBaseMemberName[baseMemberName] = 0;
}
yield return new GeneratedReferenceSpec(
memberName,
referenceSeed.DisplayPath,
referenceSeed.ReferencedTableName,
referenceSeed.ValueSchemaType,
referenceSeed.IsCollection);
}
}
/// <summary>
/// 收集适合生成轻量查询辅助的根级标量字段。
/// 当前实现故意限定在顶层非主键标量字段,避免把嵌套结构、数组或引用语义提前固化为运行时契约。
/// </summary>
/// <param name="schema">生成器级 schema 模型。</param>
/// <returns>可生成查询辅助的属性集合。</returns>
private static IEnumerable<SchemaPropertySpec> CollectQueryableProperties(SchemaFileSpec schema)
{
foreach (var property in schema.RootObject.Properties)
{
if (property.TypeSpec.Kind != SchemaNodeKind.Scalar)
{
continue;
}
if (!string.IsNullOrWhiteSpace(property.TypeSpec.RefTableName))
{
continue;
}
if (string.Equals(property.PropertyName, schema.KeyPropertyName, StringComparison.Ordinal))
{
continue;
}
yield return property;
}
}
/// <summary>
/// 生成按字段匹配全部结果的轻量查询辅助。
/// </summary>
/// <param name="builder">输出缓冲区。</param>
/// <param name="schema">生成器级 schema 模型。</param>
/// <param name="property">要生成查询辅助的字段模型。</param>
private static void AppendFindByPropertyMethod(
StringBuilder builder,
SchemaFileSpec schema,
SchemaPropertySpec property)
{
builder.AppendLine(" /// <summary>");
builder.AppendLine(
$" /// Finds all config entries whose property '{EscapeXmlDocumentation(property.DisplayPath)}' equals the supplied value.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"value\">The property value to match.</param>");
builder.AppendLine(" /// <returns>A read-only snapshot containing every matching config entry.</returns>");
builder.AppendLine(" /// <remarks>");
builder.AppendLine(
" /// The generated helper performs a deterministic linear scan over <see cref=\"All\"/> so it stays compatible with runtime hot reload and does not require secondary index infrastructure.");
builder.AppendLine(" /// </remarks>");
builder.AppendLine(
$" public global::System.Collections.Generic.IReadOnlyList<{schema.ClassName}> FindBy{property.PropertyName}({property.TypeSpec.ClrType} value)");
builder.AppendLine(" {");
builder.AppendLine(
$" var matches = new global::System.Collections.Generic.List<{schema.ClassName}>();");
builder.AppendLine();
builder.AppendLine(
" // Scan the current table snapshot on demand so generated helpers stay aligned with reloadable runtime data.");
builder.AppendLine(" foreach (var candidate in All())");
builder.AppendLine(" {");
builder.AppendLine(
$" if (global::System.Collections.Generic.EqualityComparer<{property.TypeSpec.ClrType}>.Default.Equals(candidate.{property.PropertyName}, value))");
builder.AppendLine(" {");
builder.AppendLine(" matches.Add(candidate);");
builder.AppendLine(" }");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(
$" return matches.Count == 0 ? global::System.Array.Empty<{schema.ClassName}>() : matches.AsReadOnly();");
builder.AppendLine(" }");
}
/// <summary>
/// 生成按字段匹配首个结果的轻量查询辅助。
/// </summary>
/// <param name="builder">输出缓冲区。</param>
/// <param name="schema">生成器级 schema 模型。</param>
/// <param name="property">要生成查询辅助的字段模型。</param>
private static void AppendTryFindFirstByPropertyMethod(
StringBuilder builder,
SchemaFileSpec schema,
SchemaPropertySpec property)
{
builder.AppendLine(" /// <summary>");
builder.AppendLine(
$" /// Tries to find the first config entry whose property '{EscapeXmlDocumentation(property.DisplayPath)}' equals the supplied value.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"value\">The property value to match.</param>");
builder.AppendLine(
" /// <param name=\"result\">The first matching config entry when lookup succeeds; otherwise <see langword=\"null\" />.</param>");
builder.AppendLine(" /// <returns><see langword=\"true\" /> when a matching config entry is found; otherwise <see langword=\"false\" />.</returns>");
builder.AppendLine(" /// <remarks>");
builder.AppendLine(
" /// The generated helper walks the same snapshot exposed by <see cref=\"All\"/> and returns the first match in iteration order.");
builder.AppendLine(" /// </remarks>");
builder.AppendLine(
$" public bool TryFindFirstBy{property.PropertyName}({property.TypeSpec.ClrType} value, out {schema.ClassName}? result)");
builder.AppendLine(" {");
builder.AppendLine(
" // Keep the search path allocation-free for the first-match case by exiting as soon as one entry matches.");
builder.AppendLine(" foreach (var candidate in All())");
builder.AppendLine(" {");
builder.AppendLine(
$" if (global::System.Collections.Generic.EqualityComparer<{property.TypeSpec.ClrType}>.Default.Equals(candidate.{property.PropertyName}, value))");
builder.AppendLine(" {");
builder.AppendLine(" result = candidate;");
builder.AppendLine(" return true;");
builder.AppendLine(" }");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" result = null;");
builder.AppendLine(" return false;");
builder.AppendLine(" }");
}
/// <summary>
/// 递归枚举对象树中所有带 ref-table 元数据的字段。
/// </summary>
/// <param name="properties">对象属性集合。</param>
/// <returns>原始引用字段信息。</returns>
private static IEnumerable<GeneratedReferenceSeed> EnumerateReferenceSeeds(
IEnumerable<SchemaPropertySpec> properties)
{
foreach (var property in properties)
{
if (!string.IsNullOrWhiteSpace(property.TypeSpec.RefTableName))
{
yield return new GeneratedReferenceSeed(
property.DisplayPath,
property.TypeSpec.RefTableName!,
property.TypeSpec.Kind == SchemaNodeKind.Array
? property.TypeSpec.ItemTypeSpec?.SchemaType ?? property.TypeSpec.SchemaType
: property.TypeSpec.SchemaType,
property.TypeSpec.Kind == SchemaNodeKind.Array);
}
if (property.TypeSpec.NestedObject is not null)
{
foreach (var nestedReference in EnumerateReferenceSeeds(property.TypeSpec.NestedObject.Properties))
{
yield return nestedReference;
}
}
if (property.TypeSpec.ItemTypeSpec?.NestedObject is not null)
{
foreach (var nestedReference in EnumerateReferenceSeeds(property.TypeSpec.ItemTypeSpec.NestedObject
.Properties))
{
yield return nestedReference;
}
}
}
}
/// <summary> /// <summary>
/// 递归生成配置对象类型。 /// 递归生成配置对象类型。
/// </summary> /// </summary>
@ -910,6 +1304,40 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine($"{indent}/// </remarks>"); builder.AppendLine($"{indent}/// </remarks>");
} }
/// <summary>
/// 将 schema 字段名转换并验证为生成代码可直接使用的属性标识符。
/// 生成器会在这里拒绝无法映射为合法 C# 标识符的外部输入,避免生成源码后才在编译阶段失败。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="displayPath">逻辑字段路径。</param>
/// <param name="schemaName">Schema 原始字段名。</param>
/// <param name="propertyName">生成后的属性名。</param>
/// <param name="diagnostic">字段名非法时生成的诊断。</param>
/// <returns>是否成功生成合法属性标识符。</returns>
private static bool TryBuildPropertyIdentifier(
string filePath,
string displayPath,
string schemaName,
out string propertyName,
out Diagnostic? diagnostic)
{
propertyName = ToPascalCase(schemaName);
if (SyntaxFacts.IsValidIdentifier(propertyName))
{
diagnostic = null;
return true;
}
diagnostic = Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidGeneratedIdentifier,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
displayPath,
schemaName,
propertyName);
return false;
}
/// <summary> /// <summary>
/// 从 schema 文件路径提取实体基础名。 /// 从 schema 文件路径提取实体基础名。
/// </summary> /// </summary>
@ -968,6 +1396,28 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
return tokens.Length == 0 ? "Config" : string.Concat(tokens); return tokens.Length == 0 ? "Config" : string.Concat(tokens);
} }
/// <summary>
/// 将 schema 字段路径转换为可用于生成引用元数据成员的 PascalCase 标识符。
/// </summary>
/// <param name="displayPath">Schema 字段路径。</param>
/// <returns>稳定的成员名。</returns>
private static string BuildReferenceMemberName(string displayPath)
{
var segments = displayPath.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
var builder = new StringBuilder();
foreach (var segment in segments)
{
var normalizedSegment = segment
.Replace("[]", "Items")
.Replace("[", " ")
.Replace("]", " ");
builder.Append(ToPascalCase(normalizedSegment));
}
return builder.Length == 0 ? "Reference" : builder.ToString();
}
/// <summary> /// <summary>
/// 为 AdditionalFiles 诊断创建文件位置。 /// 为 AdditionalFiles 诊断创建文件位置。
/// </summary> /// </summary>
@ -1335,6 +1785,34 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
SchemaObjectSpec? NestedObject, SchemaObjectSpec? NestedObject,
SchemaTypeSpec? ItemTypeSpec); SchemaTypeSpec? ItemTypeSpec);
/// <summary>
/// 生成代码前的跨表引用字段种子信息。
/// </summary>
/// <param name="DisplayPath">Schema 字段路径。</param>
/// <param name="ReferencedTableName">目标表名称。</param>
/// <param name="ValueSchemaType">引用值的标量 schema 类型。</param>
/// <param name="IsCollection">是否为数组引用。</param>
private sealed record GeneratedReferenceSeed(
string DisplayPath,
string ReferencedTableName,
string ValueSchemaType,
bool IsCollection);
/// <summary>
/// 已分配稳定成员名的生成期跨表引用信息。
/// </summary>
/// <param name="MemberName">生成到绑定类中的成员名。</param>
/// <param name="DisplayPath">Schema 字段路径。</param>
/// <param name="ReferencedTableName">目标表名称。</param>
/// <param name="ValueSchemaType">引用值的标量 schema 类型。</param>
/// <param name="IsCollection">是否为数组引用。</param>
private sealed record GeneratedReferenceSpec(
string MemberName,
string DisplayPath,
string ReferencedTableName,
string ValueSchemaType,
bool IsCollection);
/// <summary> /// <summary>
/// 属性解析结果包装。 /// 属性解析结果包装。
/// </summary> /// </summary>
@ -1364,4 +1842,4 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
Object, Object,
Array Array
} }
} }

View File

@ -63,4 +63,15 @@ public static class ConfigSchemaDiagnostics
SourceGeneratorsConfigCategory, SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error, DiagnosticSeverity.Error,
true); true);
/// <summary>
/// schema 字段名无法安全映射为 C# 标识符。
/// </summary>
public static readonly DiagnosticDescriptor InvalidGeneratedIdentifier = new(
"GF_ConfigSchema_006",
"Config schema property name cannot be converted to a valid C# identifier",
"Property '{1}' in schema file '{0}' uses schema key '{2}', which generates invalid C# identifier '{3}'",
SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error,
true);
} }

View File

@ -62,6 +62,7 @@
<None Remove="GFramework.SourceGenerators.Attributes\**"/> <None Remove="GFramework.SourceGenerators.Attributes\**"/>
<None Remove="Godot\**"/> <None Remove="Godot\**"/>
<None Remove="GFramework.Game.Tests\**"/> <None Remove="GFramework.Game.Tests\**"/>
<None Remove="GFramework.Godot.Tests\**"/>
</ItemGroup> </ItemGroup>
<!-- 聚合核心模块 --> <!-- 聚合核心模块 -->
<ItemGroup> <ItemGroup>
@ -102,6 +103,7 @@
<Compile Remove="GFramework.SourceGenerators.Attributes\**"/> <Compile Remove="GFramework.SourceGenerators.Attributes\**"/>
<Compile Remove="Godot\**"/> <Compile Remove="Godot\**"/>
<Compile Remove="GFramework.Game.Tests\**"/> <Compile Remove="GFramework.Game.Tests\**"/>
<Compile Remove="GFramework.Godot.Tests\**"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Remove="GFramework.Core\**"/> <EmbeddedResource Remove="GFramework.Core\**"/>
@ -128,14 +130,10 @@
<EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/> <EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/>
<EmbeddedResource Remove="Godot\**"/> <EmbeddedResource Remove="Godot\**"/>
<EmbeddedResource Remove="GFramework.Game.Tests\**"/> <EmbeddedResource Remove="GFramework.Game.Tests\**"/>
<EmbeddedResource Remove="GFramework.Godot.Tests\**"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/> <AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md"/> <AdditionalFiles Remove="AnalyzerReleases.Unshipped.md"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="local-plan\docs\"/>
<Folder Include="local-plan\todos\"/>
<Folder Include="local-plan\评估\"/>
</ItemGroup>
</Project> </Project>

View File

@ -36,6 +36,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GF
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGenerators.Tests", "GFramework.Godot.SourceGenerators.Tests\GFramework.Godot.SourceGenerators.Tests.csproj", "{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGenerators.Tests", "GFramework.Godot.SourceGenerators.Tests\GFramework.Godot.SourceGenerators.Tests.csproj", "{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.Tests", "GFramework.Godot.Tests\GFramework.Godot.Tests.csproj", "{576119E2-13D0-4ACF-A012-D01C320E8BF3}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -262,6 +264,18 @@ Global
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x64.Build.0 = Release|Any CPU {E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x64.Build.0 = Release|Any CPU
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.ActiveCfg = Release|Any CPU {E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.ActiveCfg = Release|Any CPU
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.Build.0 = Release|Any CPU {E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.Build.0 = Release|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x64.ActiveCfg = Debug|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x64.Build.0 = Debug|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x86.ActiveCfg = Debug|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x86.Build.0 = Debug|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|Any CPU.Build.0 = Release|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.ActiveCfg = Release|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.Build.0 = Release|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.ActiveCfg = Release|Any CPU
{576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

File diff suppressed because it is too large Load Diff

View File

@ -1,491 +1,491 @@
# 最佳实践 # 最佳实践
本文档总结了使用 GFramework 的最佳实践和设计模式。 本文档总结了使用 GFramework 的最佳实践和设计模式。
## 架构设计 ## 架构设计
### 1. 清晰的职责分离 ### 1. 清晰的职责分离
**原则**:每一层都有明确的职责,不要混淆。 **原则**:每一层都有明确的职责,不要混淆。
```csharp ```csharp
// ✅ 正确的职责分离 // ✅ 正确的职责分离
public class PlayerModel : AbstractModel public class PlayerModel : AbstractModel
{ {
// Model只存储数据 // Model只存储数据
public BindableProperty<int> Health { get; } = new(100); public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> Score { get; } = new(0); public BindableProperty<int> Score { get; } = new(0);
} }
public class CombatSystem : AbstractSystem public class CombatSystem : AbstractSystem
{ {
// System处理业务逻辑 // System处理业务逻辑
protected override void OnInit() protected override void OnInit()
{ {
this.RegisterEvent<AttackEvent>(OnAttack); this.RegisterEvent<AttackEvent>(OnAttack);
} }
private void OnAttack(AttackEvent e) private void OnAttack(AttackEvent e)
{ {
var player = this.GetModel<PlayerModel>(); var player = this.GetModel<PlayerModel>();
player.Health.Value -= e.Damage; player.Health.Value -= e.Damage;
} }
} }
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware] [ContextAware]
public partial class PlayerController : IController public partial class PlayerController : IController
{ {
// Controller连接 UI 和逻辑 // Controller连接 UI 和逻辑
public void Initialize() public void Initialize()
{ {
var player = this.GetModel<PlayerModel>(); var player = this.GetModel<PlayerModel>();
player.Health.RegisterWithInitValue(OnHealthChanged); player.Health.RegisterWithInitValue(OnHealthChanged);
} }
private void OnHealthChanged(int health) private void OnHealthChanged(int health)
{ {
UpdateHealthDisplay(health); UpdateHealthDisplay(health);
} }
} }
``` ```
### 2. 事件驱动设计 ### 2. 事件驱动设计
**原则**:使用事件解耦组件,避免直接调用。 **原则**:使用事件解耦组件,避免直接调用。
```csharp ```csharp
// ❌ 紧耦合 // ❌ 紧耦合
public class SystemA : AbstractSystem public class SystemA : AbstractSystem
{ {
private void OnEvent(EventA e) private void OnEvent(EventA e)
{ {
var systemB = this.GetSystem<SystemB>(); var systemB = this.GetSystem<SystemB>();
systemB.DoSomething(); // 直接调用 systemB.DoSomething(); // 直接调用
} }
} }
// ✅ 松耦合 // ✅ 松耦合
public class SystemA : AbstractSystem public class SystemA : AbstractSystem
{ {
private void OnEvent(EventA e) private void OnEvent(EventA e)
{ {
this.SendEvent(new EventB()); // 发送事件 this.SendEvent(new EventB()); // 发送事件
} }
} }
public class SystemB : AbstractSystem public class SystemB : AbstractSystem
{ {
protected override void OnInit() protected override void OnInit()
{ {
this.RegisterEvent<EventB>(OnEventB); this.RegisterEvent<EventB>(OnEventB);
} }
} }
``` ```
### 3. 命令查询分离 ### 3. 命令查询分离
**原则**明确区分修改状态Command和查询状态Query **原则**明确区分修改状态Command和查询状态Query
```csharp ```csharp
// ✅ 正确的 CQRS // ✅ 正确的 CQRS
public class MovePlayerCommand : AbstractCommand public class MovePlayerCommand : AbstractCommand
{ {
public Vector2 Direction { get; set; } public Vector2 Direction { get; set; }
protected override void OnDo() protected override void OnDo()
{ {
// 修改状态 // 修改状态
this.SendEvent(new PlayerMovedEvent { Direction = Direction }); this.SendEvent(new PlayerMovedEvent { Direction = Direction });
} }
} }
public class GetPlayerPositionQuery : AbstractQuery<Vector2> public class GetPlayerPositionQuery : AbstractQuery<Vector2>
{ {
protected override Vector2 OnDo() protected override Vector2 OnDo()
{ {
// 只查询,不修改 // 只查询,不修改
return this.GetModel<PlayerModel>().Position.Value; return this.GetModel<PlayerModel>().Position.Value;
} }
} }
``` ```
## 代码组织 ## 代码组织
### 1. 项目结构 ### 1. 项目结构
``` ```
GameProject/ GameProject/
├── Models/ ├── Models/
│ ├── PlayerModel.cs │ ├── PlayerModel.cs
│ ├── GameStateModel.cs │ ├── GameStateModel.cs
│ └── InventoryModel.cs │ └── InventoryModel.cs
├── Systems/ ├── Systems/
│ ├── CombatSystem.cs │ ├── CombatSystem.cs
│ ├── InventorySystem.cs │ ├── InventorySystem.cs
│ └── GameLogicSystem.cs │ └── GameLogicSystem.cs
├── Commands/ ├── Commands/
│ ├── AttackCommand.cs │ ├── AttackCommand.cs
│ ├── MoveCommand.cs │ ├── MoveCommand.cs
│ └── UseItemCommand.cs │ └── UseItemCommand.cs
├── Queries/ ├── Queries/
│ ├── GetPlayerHealthQuery.cs │ ├── GetPlayerHealthQuery.cs
│ └── GetInventoryItemsQuery.cs │ └── GetInventoryItemsQuery.cs
├── Events/ ├── Events/
│ ├── PlayerDiedEvent.cs │ ├── PlayerDiedEvent.cs
│ ├── ItemUsedEvent.cs │ ├── ItemUsedEvent.cs
│ └── EnemyDamagedEvent.cs │ └── EnemyDamagedEvent.cs
├── Controllers/ ├── Controllers/
│ ├── PlayerController.cs │ ├── PlayerController.cs
│ └── UIController.cs │ └── UIController.cs
├── Utilities/ ├── Utilities/
│ ├── StorageUtility.cs │ ├── StorageUtility.cs
│ └── MathUtility.cs │ └── MathUtility.cs
└── GameArchitecture.cs └── GameArchitecture.cs
``` ```
### 2. 命名规范 ### 2. 命名规范
```csharp ```csharp
// Models使用 Model 后缀 // Models使用 Model 后缀
public class PlayerModel : AbstractModel { } public class PlayerModel : AbstractModel { }
public class GameStateModel : AbstractModel { } public class GameStateModel : AbstractModel { }
// Systems使用 System 后缀 // Systems使用 System 后缀
public class CombatSystem : AbstractSystem { } public class CombatSystem : AbstractSystem { }
public class InventorySystem : AbstractSystem { } public class InventorySystem : AbstractSystem { }
// Commands使用 Command 后缀 // Commands使用 Command 后缀
public class AttackCommand : AbstractCommand { } public class AttackCommand : AbstractCommand { }
public class MoveCommand : AbstractCommand { } public class MoveCommand : AbstractCommand { }
// Queries使用 Query 后缀 // Queries使用 Query 后缀
public class GetPlayerHealthQuery : AbstractQuery<int> { } public class GetPlayerHealthQuery : AbstractQuery<int> { }
public class GetInventoryItemsQuery : AbstractQuery<List<Item>> { } public class GetInventoryItemsQuery : AbstractQuery<List<Item>> { }
// Events使用 Event 后缀 // Events使用 Event 后缀
public class PlayerDiedEvent : IEvent { } public class PlayerDiedEvent : IEvent { }
public class ItemUsedEvent : IEvent { } public class ItemUsedEvent : IEvent { }
// Controllers使用 Controller 后缀 // Controllers使用 Controller 后缀
public class PlayerController : IController { } public class PlayerController : IController { }
// Utilities使用 Utility 后缀 // Utilities使用 Utility 后缀
public class StorageUtility : IUtility { } public class StorageUtility : IUtility { }
``` ```
## 内存管理 ## 内存管理
### 1. 正确的注销管理 ### 1. 正确的注销管理
```csharp ```csharp
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware] [ContextAware]
public partial class MyController : IController public partial class MyController : IController
{ {
private IUnRegisterList _unregisterList = new UnRegisterList(); private IUnRegisterList _unregisterList = new UnRegisterList();
public void Initialize() public void Initialize()
{ {
var model = this.GetModel<PlayerModel>(); var model = this.GetModel<PlayerModel>();
// 注册事件并添加到注销列表 // 注册事件并添加到注销列表
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied) this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied)
.AddToUnregisterList(_unregisterList); .AddToUnregisterList(_unregisterList);
// 注册属性监听并添加到注销列表 // 注册属性监听并添加到注销列表
model.Health.Register(OnHealthChanged) model.Health.Register(OnHealthChanged)
.AddToUnregisterList(_unregisterList); .AddToUnregisterList(_unregisterList);
} }
public void Cleanup() public void Cleanup()
{ {
// 统一注销所有监听器 // 统一注销所有监听器
_unregisterList.UnRegisterAll(); _unregisterList.UnRegisterAll();
} }
private void OnPlayerDied(PlayerDiedEvent e) { } private void OnPlayerDied(PlayerDiedEvent e) { }
private void OnHealthChanged(int health) { } private void OnHealthChanged(int health) { }
} }
``` ```
### 2. 生命周期管理 ### 2. 生命周期管理
```csharp ```csharp
public class GameManager public class GameManager
{ {
private GameArchitecture _architecture; private GameArchitecture _architecture;
public void StartGame() public void StartGame()
{ {
_architecture = new GameArchitecture(); _architecture = new GameArchitecture();
_architecture.Initialize(); _architecture.Initialize();
} }
public void EndGame() public void EndGame()
{ {
// 销毁架构,自动清理所有组件 // 销毁架构,自动清理所有组件
_architecture.Destroy(); _architecture.Destroy();
_architecture = null; _architecture = null;
} }
} }
``` ```
## 性能优化 ## 性能优化
### 1. 缓存组件引用 ### 1. 缓存组件引用
```csharp ```csharp
// ❌ 低效:每次都查询 // ❌ 低效:每次都查询
public void Update() public void Update()
{ {
var model = this.GetModel<PlayerModel>(); var model = this.GetModel<PlayerModel>();
model.Health.Value -= 1; model.Health.Value -= 1;
} }
// ✅ 高效:缓存引用 // ✅ 高效:缓存引用
private PlayerModel _playerModel; private PlayerModel _playerModel;
public void Initialize() public void Initialize()
{ {
_playerModel = this.GetModel<PlayerModel>(); _playerModel = this.GetModel<PlayerModel>();
} }
public void Update() public void Update()
{ {
_playerModel.Health.Value -= 1; _playerModel.Health.Value -= 1;
} }
``` ```
### 2. 避免频繁的事件创建 ### 2. 避免频繁的事件创建
```csharp ```csharp
// ❌ 低效:每帧创建新事件 // ❌ 低效:每帧创建新事件
public void Update() public void Update()
{ {
this.SendEvent(new UpdateEvent()); // 频繁分配内存 this.SendEvent(new UpdateEvent()); // 频繁分配内存
} }
// ✅ 高效:复用事件或使用对象池 // ✅ 高效:复用事件或使用对象池
private UpdateEvent _updateEvent = new UpdateEvent(); private UpdateEvent _updateEvent = new UpdateEvent();
public void Update() public void Update()
{ {
this.SendEvent(_updateEvent); this.SendEvent(_updateEvent);
} }
``` ```
### 3. 异步处理重操作 ### 3. 异步处理重操作
```csharp ```csharp
public class LoadDataCommand : AbstractCommand public class LoadDataCommand : AbstractCommand
{ {
protected override async void OnDo() protected override async void OnDo()
{ {
// 异步加载数据,不阻塞主线程 // 异步加载数据,不阻塞主线程
var data = await LoadDataAsync(); var data = await LoadDataAsync();
this.SendEvent(new DataLoadedEvent { Data = data }); this.SendEvent(new DataLoadedEvent { Data = data });
} }
private async Task<Data> LoadDataAsync() private async Task<Data> LoadDataAsync()
{ {
return await Task.Run(() => return await Task.Run(() =>
{ {
// 耗时操作 // 耗时操作
return new Data(); return new Data();
}); });
} }
} }
``` ```
## 测试 ## 测试
### 1. 单元测试 ### 1. 单元测试
```csharp ```csharp
[TestFixture] [TestFixture]
public class CombatSystemTests public class CombatSystemTests
{ {
private GameArchitecture _architecture; private GameArchitecture _architecture;
private PlayerModel _playerModel; private PlayerModel _playerModel;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_architecture = new TestArchitecture(); _architecture = new TestArchitecture();
_architecture.Initialize(); _architecture.Initialize();
_playerModel = _architecture.GetModel<PlayerModel>(); _playerModel = _architecture.GetModel<PlayerModel>();
} }
[TearDown] [TearDown]
public void Teardown() public void Teardown()
{ {
_architecture.Destroy(); _architecture.Destroy();
} }
[Test] [Test]
public void PlayerTakeDamage_ReducesHealth() public void PlayerTakeDamage_ReducesHealth()
{ {
_playerModel.Health.Value = 100; _playerModel.Health.Value = 100;
_architecture.SendEvent(new DamageEvent { Amount = 10 }); _architecture.SendEvent(new DamageEvent { Amount = 10 });
Assert.AreEqual(90, _playerModel.Health.Value); Assert.AreEqual(90, _playerModel.Health.Value);
} }
[Test] [Test]
public void PlayerDies_WhenHealthReachesZero() public void PlayerDies_WhenHealthReachesZero()
{ {
_playerModel.Health.Value = 10; _playerModel.Health.Value = 10;
_architecture.SendEvent(new DamageEvent { Amount = 10 }); _architecture.SendEvent(new DamageEvent { Amount = 10 });
Assert.AreEqual(0, _playerModel.Health.Value); Assert.AreEqual(0, _playerModel.Health.Value);
} }
} }
``` ```
### 2. 集成测试 ### 2. 集成测试
```csharp ```csharp
[TestFixture] [TestFixture]
public class GameFlowTests public class GameFlowTests
{ {
private GameArchitecture _architecture; private GameArchitecture _architecture;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_architecture = new GameArchitecture(); _architecture = new GameArchitecture();
_architecture.Initialize(); _architecture.Initialize();
} }
[Test] [Test]
public void CompleteGameFlow() public void CompleteGameFlow()
{ {
// 初始化 // 初始化
var player = _architecture.GetModel<PlayerModel>(); var player = _architecture.GetModel<PlayerModel>();
Assert.AreEqual(100, player.Health.Value); Assert.AreEqual(100, player.Health.Value);
// 执行操作 // 执行操作
_architecture.SendCommand(new AttackCommand { Damage = 20 }); _architecture.SendCommand(new AttackCommand { Damage = 20 });
// 验证结果 // 验证结果
Assert.AreEqual(80, player.Health.Value); Assert.AreEqual(80, player.Health.Value);
} }
} }
``` ```
## 文档 ## 文档
### 1. 代码注释 ### 1. 代码注释
```csharp ```csharp
/// <summary> /// <summary>
/// 玩家模型,存储玩家的所有状态数据 /// 玩家模型,存储玩家的所有状态数据
/// </summary> /// </summary>
public class PlayerModel : AbstractModel public class PlayerModel : AbstractModel
{ {
/// <summary> /// <summary>
/// 玩家的生命值,使用 BindableProperty 实现响应式更新 /// 玩家的生命值,使用 BindableProperty 实现响应式更新
/// </summary> /// </summary>
public BindableProperty<int> Health { get; } = new(100); public BindableProperty<int> Health { get; } = new(100);
protected override void OnInit() protected override void OnInit()
{ {
// 监听生命值变化,当生命值为 0 时发送死亡事件 // 监听生命值变化,当生命值为 0 时发送死亡事件
Health.Register(hp => Health.Register(hp =>
{ {
if (hp <= 0) if (hp <= 0)
this.SendEvent(new PlayerDiedEvent()); this.SendEvent(new PlayerDiedEvent());
}); });
} }
} }
``` ```
### 2. 架构文档 ### 2. 架构文档
为你的项目编写架构文档,说明: 为你的项目编写架构文档,说明:
- 主要的 Model、System、Command、Query - 主要的 Model、System、Command、Query
- 关键事件流 - 关键事件流
- 组件间的通信方式 - 组件间的通信方式
- 扩展点和插件机制 - 扩展点和插件机制
## 常见陷阱 ## 常见陷阱
### 1. 在 Model 中包含业务逻辑 ### 1. 在 Model 中包含业务逻辑
```csharp ```csharp
// ❌ 错误 // ❌ 错误
public class PlayerModel : AbstractModel public class PlayerModel : AbstractModel
{ {
public void TakeDamage(int damage) public void TakeDamage(int damage)
{ {
Health.Value -= damage; Health.Value -= damage;
if (Health.Value <= 0) if (Health.Value <= 0)
Die(); Die();
} }
} }
// ✅ 正确 // ✅ 正确
public class CombatSystem : AbstractSystem public class CombatSystem : AbstractSystem
{ {
private void OnDamage(DamageEvent e) private void OnDamage(DamageEvent e)
{ {
var player = this.GetModel<PlayerModel>(); var player = this.GetModel<PlayerModel>();
player.Health.Value -= e.Amount; player.Health.Value -= e.Amount;
} }
} }
``` ```
### 2. 忘记注销监听器 ### 2. 忘记注销监听器
```csharp ```csharp
// ❌ 错误:可能导致内存泄漏 // ❌ 错误:可能导致内存泄漏
public void Initialize() public void Initialize()
{ {
this.RegisterEvent<Event1>(OnEvent1); // 未注销 this.RegisterEvent<Event1>(OnEvent1); // 未注销
} }
// ✅ 正确 // ✅ 正确
private IUnRegisterList _unregisterList = new UnRegisterList(); private IUnRegisterList _unregisterList = new UnRegisterList();
public void Initialize() public void Initialize()
{ {
this.RegisterEvent<Event1>(OnEvent1) this.RegisterEvent<Event1>(OnEvent1)
.AddToUnregisterList(_unregisterList); .AddToUnregisterList(_unregisterList);
} }
public void Cleanup() public void Cleanup()
{ {
_unregisterList.UnRegisterAll(); _unregisterList.UnRegisterAll();
} }
``` ```
### 3. 直接调用其他系统 ### 3. 直接调用其他系统
```csharp ```csharp
// ❌ 错误:紧耦合 // ❌ 错误:紧耦合
public class SystemA : AbstractSystem public class SystemA : AbstractSystem
{ {
private void OnEvent(EventA e) private void OnEvent(EventA e)
{ {
var systemB = this.GetSystem<SystemB>(); var systemB = this.GetSystem<SystemB>();
systemB.DoSomething(); systemB.DoSomething();
} }
} }
// ✅ 正确:使用事件解耦 // ✅ 正确:使用事件解耦
public class SystemA : AbstractSystem public class SystemA : AbstractSystem
{ {
private void OnEvent(EventA e) private void OnEvent(EventA e)
{ {
this.SendEvent(new EventB()); this.SendEvent(new EventB());
} }
} }
``` ```
--- ---
遵循这些最佳实践将帮助你构建可维护、高效、可扩展的应用程序。 遵循这些最佳实践将帮助你构建可维护、高效、可扩展的应用程序。

File diff suppressed because it is too large Load Diff

View File

@ -1,230 +1,239 @@
# Architecture 架构详解 # Architecture 架构详解
> 深入了解 GFramework 的核心架构设计和实现 > 深入了解 GFramework 的核心架构设计和实现
## 目录 ## 目录
- [概述](#概述) - [概述](#概述)
- [架构设计](#架构设计) - [架构设计](#架构设计)
- [生命周期管理](#生命周期管理) - [生命周期管理](#生命周期管理)
- [组件注册](#组件注册) - [组件注册](#组件注册)
- [模块系统](#模块系统) - [模块系统](#模块系统)
- [最佳实践](#最佳实践) - [最佳实践](#最佳实践)
- [API 参考](#api-参考) - [API 参考](#api-参考)
## 概述 ## 概述
Architecture 是 GFramework 的核心类,负责管理整个应用的生命周期、组件注册和模块管理。从 v1.1.0 开始,Architecture Architecture 是 GFramework 的核心类,负责管理整个应用的生命周期、组件注册和模块管理。从 v1.1.0 开始,Architecture
采用模块化设计,将职责分离到专门的管理器中。 采用模块化设计,将职责分离到专门的协作者中。
### 设计目标 > 命名约定:
> - `ArchitectureServices` 是公开的基础服务入口,负责容器、事件总线、命令执行器、查询执行器和服务模块管理
- **单一职责**: 每个管理器只负责一个明确的功能 > - `ArchitectureComponentRegistry` 是内部组件注册器,专门负责 System / Model / Utility 的注册与生命周期接入
- **类型安全**: 基于泛型的组件获取和注册 > - 两者不是同一层职责,不要混用
- **生命周期管理**: 自动的初始化和销毁机制
- **可扩展性**: 支持模块和钩子扩展 ### 设计目标
- **向后兼容**: 保持公共 API 稳定
- **单一职责**: 每个管理器只负责一个明确的功能
### 核心组件 - **类型安全**: 基于泛型的组件获取和注册
- **生命周期管理**: 自动的初始化和销毁机制
``` - **可扩展性**: 支持模块和钩子扩展
Architecture (核心协调器) - **向后兼容**: 保持公共 API 稳定
├── ArchitectureLifecycle (生命周期管理)
├── ArchitectureComponentRegistry (组件注册) ### 核心组件
└── ArchitectureModules (模块管理)
``` ```
Architecture (核心协调器)
## 架构设计 ├── ArchitectureBootstrapper (初始化基础设施编排)
├── ArchitectureLifecycle (生命周期管理)
### 设计模式 ├── ArchitectureComponentRegistry (组件注册)
└── ArchitectureModules (模块管理)
Architecture 采用以下设计模式: ```
1. **组合模式 (Composition)**: Architecture 组合三个管理器 ## 架构设计
2. **委托模式 (Delegation)**: 方法调用委托给专门的管理器
3. **协调器模式 (Coordinator)**: Architecture 作为协调器统一对外接口 ### 设计模式
### 类图 Architecture 采用以下设计模式:
``` 1. **组合模式 (Composition)**: Architecture 组合多个内部协作者
┌─────────────────────────────────────────────────────┐ 2. **委托模式 (Delegation)**: 方法调用委托给专门的管理器
│ Architecture │ 3. **协调器模式 (Coordinator)**: Architecture 作为协调器统一对外接口
│ - _lifecycle: ArchitectureLifecycle │
│ - _componentRegistry: ArchitectureComponentRegistry│ ### 类图
│ - _modules: ArchitectureModules │
│ - _logger: ILogger │ ```
│ │ ┌─────────────────────────────────────────────────────┐
│ + RegisterSystem<T>() │ │ Architecture │
│ + RegisterModel<T>() │ │ - _bootstrapper: ArchitectureBootstrapper │
│ + RegisterUtility<T>() │ │ - _lifecycle: ArchitectureLifecycle │
│ + InstallModule() │ │ - _componentRegistry: ArchitectureComponentRegistry│
│ + InitializeAsync() │ │ - _modules: ArchitectureModules │
│ + DestroyAsync() │ │ - _logger: ILogger │
│ + event PhaseChanged │ │ │
└─────────────────────────────────────────────────────┘ │ + RegisterSystem<T>() │
│ │ │ │ + RegisterModel<T>() │
│ │ │ │ + RegisterUtility<T>() │
▼ ▼ ▼ │ + InstallModule() │
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ + InitializeAsync() │
│ Lifecycle │ │ ComponentRegistry│ │ Modules │ │ + DestroyAsync() │
│ │ │ │ │ │ │ + event PhaseChanged │
│ - 阶段管理 │ │ - System 注册 │ │ - 模块安装 │ └─────────────────────────────────────────────────────┘
│ - 钩子管理 │ │ - Model 注册 │ │ - 行为注册 │ │ │ │ │
│ - 初始化 │ │ - Utility 注册 │ │ │ │ │ │ │
│ - 销毁 │ │ - 生命周期注册 │ │ │ ▼ ▼ ▼ ▼
└──────────────┘ └──────────────────┘ └──────────────┘ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
``` │ Bootstrapper │ │ Lifecycle │ │ComponentReg. │ │ Modules │
│ │ │ │ │ │ │ │
### 构造函数初始化 │ - 环境初始化 │ │ - 阶段管理 │ │ - System 注册│ │ - 模块安装 │
│ - 服务准备 │ │ - 钩子管理 │ │ - Model 注册 │ │ - 行为注册 │
从 v1.1.0 开始,所有管理器在构造函数中初始化: │ - 上下文绑定 │ │ - 组件初始化 │ │ - Utility 注册│ │ │
│ - 容器冻结 │ │ - 就绪/销毁协调 │ │ - 生命周期接入│ │ │
```csharp └──────────────┘ └──────────────────┘ └──────────────┘ └──────────────┘
protected Architecture( ```
IArchitectureConfiguration? configuration = null,
IEnvironment? environment = null, ### 构造函数初始化
IArchitectureServices? services = null,
IArchitectureContext? context = null) 从 v1.1.0 开始,所有管理器在构造函数中初始化:
{
Configuration = configuration ?? new ArchitectureConfiguration(); ```csharp
Environment = environment ?? new DefaultEnvironment(); protected Architecture(
Services = services ?? new ArchitectureServices(); IArchitectureConfiguration? configuration = null,
_context = context; IEnvironment? environment = null,
IArchitectureServices? services = null,
// 初始化 Logger IArchitectureContext? context = null)
LoggerFactoryResolver.Provider = Configuration.LoggerProperties.LoggerFactoryProvider; {
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name); var resolvedConfiguration = configuration ?? new ArchitectureConfiguration();
var resolvedEnvironment = environment ?? new DefaultEnvironment();
// 初始化管理器 var resolvedServices = services ?? new ArchitectureServices();
_lifecycle = new ArchitectureLifecycle(this, Configuration, Services, _logger); _context = context;
_componentRegistry = new ArchitectureComponentRegistry(this, Configuration, Services, _lifecycle, _logger);
_modules = new ArchitectureModules(this, Services, _logger); // 初始化 Logger
} LoggerFactoryResolver.Provider = resolvedConfiguration.LoggerProperties.LoggerFactoryProvider;
``` _logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
**优势**: // 初始化协作者
_bootstrapper = new ArchitectureBootstrapper(GetType(), resolvedEnvironment, resolvedServices, _logger);
- 消除 `null!` 断言,提高代码安全性 _lifecycle = new ArchitectureLifecycle(this, resolvedConfiguration, resolvedServices, _logger);
- 对象在构造后立即可用 _componentRegistry = new ArchitectureComponentRegistry(this, resolvedConfiguration, resolvedServices, _lifecycle, _logger);
- 符合"构造即完整"原则 _modules = new ArchitectureModules(this, resolvedServices, _logger);
- 可以在 InitializeAsync 之前访问事件 }
```
## 生命周期管理
**优势**:
### 架构阶段
- 消除 `null!` 断言,提高代码安全性
Architecture 定义了 11 个生命周期阶段: - 对象在构造后立即可用
- 符合"构造即完整"原则
| 阶段 | 说明 | 触发时机 | - 可以在 InitializeAsync 之前访问事件
|------------------------|--------------|------------------|
| `None` | 初始状态 | 构造函数完成后 | ## 生命周期管理
| `BeforeUtilityInit` | Utility 初始化前 | 开始初始化 Utility |
| `AfterUtilityInit` | Utility 初始化后 | 所有 Utility 初始化完成 | ### 架构阶段
| `BeforeModelInit` | Model 初始化前 | 开始初始化 Model |
| `AfterModelInit` | Model 初始化后 | 所有 Model 初始化完成 | Architecture 定义了 11 个生命周期阶段:
| `BeforeSystemInit` | System 初始化前 | 开始初始化 System |
| `AfterSystemInit` | System 初始化后 | 所有 System 初始化完成 | | 阶段 | 说明 | 触发时机 |
| `Ready` | 就绪状态 | 所有组件初始化完成 | |------------------------|--------------|------------------|
| `Destroying` | 销毁中 | 开始销毁 | | `None` | 初始状态 | 构造函数完成后 |
| `Destroyed` | 已销毁 | 销毁完成 | | `BeforeUtilityInit` | Utility 初始化前 | 开始初始化 Utility |
| `FailedInitialization` | 初始化失败 | 初始化过程中发生异常 | | `AfterUtilityInit` | Utility 初始化后 | 所有 Utility 初始化完成 |
| `BeforeModelInit` | Model 初始化前 | 开始初始化 Model |
### 阶段转换 | `AfterModelInit` | Model 初始化后 | 所有 Model 初始化完成 |
| `BeforeSystemInit` | System 初始化前 | 开始初始化 System |
``` | `AfterSystemInit` | System 初始化后 | 所有 System 初始化完成 |
正常流程: | `Ready` | 就绪状态 | 所有组件初始化完成 |
None → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit | `Destroying` | 销毁中 | 开始销毁 |
→ BeforeSystemInit → AfterSystemInit → Ready → Destroying → Destroyed | `Destroyed` | 已销毁 | 销毁完成 |
| `FailedInitialization` | 初始化失败 | 初始化过程中发生异常 |
异常流程:
Any → FailedInitialization ### 阶段转换
```
```
### 阶段事件 正常流程:
None → BeforeUtilityInit → AfterUtilityInit → BeforeModelInit → AfterModelInit
可以通过 `PhaseChanged` 事件监听阶段变化: → BeforeSystemInit → AfterSystemInit → Ready → Destroying → Destroyed
```csharp 异常流程:
public class MyArchitecture : Architecture Any → FailedInitialization
{ ```
protected override void OnInitialize()
{ ### 阶段事件
// 监听阶段变化
PhaseChanged += phase => 可以通过 `PhaseChanged` 事件监听阶段变化:
{
Console.WriteLine($"Phase changed to: {phase}"); ```csharp
}; public class MyArchitecture : Architecture
} {
} protected override void OnInitialize()
``` {
// 监听阶段变化
### 生命周期钩子 PhaseChanged += phase =>
{
实现 `IArchitectureLifecycleHook` 接口可以在阶段变化时执行自定义逻辑: Console.WriteLine($"Phase changed to: {phase}");
};
```csharp }
public class MyLifecycleHook : IArchitectureLifecycleHook }
{ ```
public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
{ ### 生命周期钩子
switch (phase)
{ 实现 `IArchitectureLifecycleHook` 接口可以在阶段变化时执行自定义逻辑:
case ArchitecturePhase.Ready:
Console.WriteLine("Architecture is ready!"); ```csharp
break; public class MyLifecycleHook : IArchitectureLifecycleHook
case ArchitecturePhase.Destroying: {
Console.WriteLine("Architecture is being destroyed!"); public void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
break; {
} switch (phase)
} {
} case ArchitecturePhase.Ready:
Console.WriteLine("Architecture is ready!");
// 注册钩子 break;
architecture.RegisterLifecycleHook(new MyLifecycleHook()); case ArchitecturePhase.Destroying:
``` Console.WriteLine("Architecture is being destroyed!");
break;
### 初始化流程 }
}
``` }
1. 创建 Architecture 实例
└─> 构造函数初始化管理器 // 注册钩子
architecture.RegisterLifecycleHook(new MyLifecycleHook());
2. 调用 InitializeAsync() 或 Initialize() ```
├─> 初始化环境 (Environment.Initialize())
├─> 注册内置服务模块 ### 初始化流程
├─> 初始化架构上下文
├─> 执行服务钩子 ```
├─> 初始化服务模块 1. 创建 Architecture 实例
├─> 调用 OnInitialize() (用户注册组件) └─> 构造函数初始化管理器
├─> 初始化所有组件
│ ├─> BeforeUtilityInit → 初始化 Utility → AfterUtilityInit 2. 调用 InitializeAsync() 或 Initialize()
│ ├─> BeforeModelInit → 初始化 Model → AfterModelInit ├─> ArchitectureBootstrapper 准备基础设施
│ └─> BeforeSystemInit → 初始化 System → AfterSystemInit │ ├─> 初始化环境 (Environment.Initialize())
├─> 冻结 IoC 容器 │ ├─> 注册内置服务模块
└─> 进入 Ready 阶段 │ ├─> 初始化架构上下文并绑定 GameContext
│ ├─> 执行服务钩子
3. 等待就绪 (可选) │ └─> 初始化服务模块
└─> await architecture.WaitUntilReadyAsync() ├─> 调用 OnInitialize() (用户注册组件)
``` ├─> 初始化所有组件
│ ├─> BeforeUtilityInit → 初始化 Utility → AfterUtilityInit
### 销毁流程 │ ├─> BeforeModelInit → 初始化 Model → AfterModelInit
│ └─> BeforeSystemInit → 初始化 System → AfterSystemInit
``` ├─> CompleteInitialization() 冻结 IoC 容器
1. 调用 DestroyAsync() 或 Destroy() └─> 进入 Ready 阶段
├─> 检查当前阶段 (如果是 None 或已销毁则直接返回)
├─> 进入 Destroying 阶段 3. 等待就绪 (可选)
├─> 逆序销毁所有组件 └─> await architecture.WaitUntilReadyAsync()
│ ├─> 优先调用 IAsyncDestroyable.DestroyAsync() ```
│ └─> 否则调用 IDestroyable.Destroy()
├─> 销毁服务模块 ### 销毁流程
├─> 清空 IoC 容器
└─> 进入 Destroyed 阶段 ```
``` 1. 调用 DestroyAsync() 或 Destroy()
├─> 检查当前阶段 (如果是 None 或已销毁则直接返回)
--- ├─> 进入 Destroying 阶段
├─> 逆序销毁所有组件
**版本**: 1.1.0 │ ├─> 优先调用 IAsyncDestroyable.DestroyAsync()
**更新日期**: 2026-03-17 │ └─> 否则调用 IDestroyable.Destroy()
**相关文档**: ├─> 销毁服务模块
├─> 进入 Destroyed 阶段
- [核心框架概述](./index.md) └─> 清空 IoC 容器
```
---
**版本**: 1.1.0
**更新日期**: 2026-03-17
**相关文档**:
- [核心框架概述](./index.md)

View File

@ -1,123 +1,123 @@
# Command 包使用说明 # Command 包使用说明
## 概述 ## 概述
Command 包实现了命令模式Command Pattern用于封装用户操作和业务逻辑。通过命令模式可以将请求封装为对象实现操作的参数化、队列化、日志记录、撤销等功能。 Command 包实现了命令模式Command Pattern用于封装用户操作和业务逻辑。通过命令模式可以将请求封装为对象实现操作的参数化、队列化、日志记录、撤销等功能。
命令系统是 GFramework CQRS 架构的重要组成部分,与事件系统和查询系统协同工作,实现完整的业务逻辑处理流程。 命令系统是 GFramework CQRS 架构的重要组成部分,与事件系统和查询系统协同工作,实现完整的业务逻辑处理流程。
## 核心接口 ## 核心接口
### ICommand ### ICommand
无返回值命令接口,定义了命令的基本契约。 无返回值命令接口,定义了命令的基本契约。
**核心方法:** **核心方法:**
```csharp ```csharp
void Execute(); // 执行命令 void Execute(); // 执行命令
``` ```
### ICommand`<TResult>` ### ICommand`<TResult>`
带返回值的命令接口,用于需要返回执行结果的命令。 带返回值的命令接口,用于需要返回执行结果的命令。
**核心方法:** **核心方法:**
```csharp ```csharp
TResult Execute(); // 执行命令并返回结果 TResult Execute(); // 执行命令并返回结果
``` ```
## 核心类 ## 核心类
### AbstractCommand ### AbstractCommand
无返回值命令的抽象基类,提供了命令的基础实现。它继承自 ContextAwareBase具有上下文感知能力。 无返回值命令的抽象基类,提供了命令的基础实现。它继承自 ContextAwareBase具有上下文感知能力。
**核心方法:** **核心方法:**
```csharp ```csharp
void ICommand.Execute(); // 实现 ICommand 接口 void ICommand.Execute(); // 实现 ICommand 接口
protected abstract void OnExecute(); // 抽象执行方法,由子类实现 protected abstract void OnExecute(); // 抽象执行方法,由子类实现
``` ```
**使用示例:** **使用示例:**
```csharp ```csharp
// 定义一个无返回值的基础命令 // 定义一个无返回值的基础命令
public class SimpleCommand : AbstractCommand public class SimpleCommand : AbstractCommand
{ {
protected override void OnExecute() protected override void OnExecute()
{ {
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
playerModel.Health.Value = playerModel.MaxHealth.Value; playerModel.Health.Value = playerModel.MaxHealth.Value;
this.SendEvent(new PlayerHealthRestoredEvent()); this.SendEvent(new PlayerHealthRestoredEvent());
} }
} }
// 使用命令 // 使用命令
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware] [ContextAware]
public partial class GameController : IController public partial class GameController : IController
{ {
public void OnRestoreHealthButtonClicked() public void OnRestoreHealthButtonClicked()
{ {
this.SendCommand(new SimpleCommand()); this.SendCommand(new SimpleCommand());
} }
} }
``` ```
### AbstractCommand`<TResult>` ### AbstractCommand`<TResult>`
无输入参数但带返回值的命令基类。 无输入参数但带返回值的命令基类。
**核心方法:** **核心方法:**
```csharp ```csharp
TResult ICommand<TResult>.Execute(); // 实现 ICommand<TResult> 接口 TResult ICommand<TResult>.Execute(); // 实现 ICommand<TResult> 接口
protected abstract TResult OnExecute(); // 抽象执行方法,由子类实现 protected abstract TResult OnExecute(); // 抽象执行方法,由子类实现
``` ```
**使用示例:** **使用示例:**
```csharp ```csharp
// 定义一个无输入但有返回值的命令 // 定义一个无输入但有返回值的命令
public class GetPlayerHealthQuery : AbstractCommand<int> public class GetPlayerHealthQuery : AbstractCommand<int>
{ {
protected override int OnExecute() protected override int OnExecute()
{ {
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
return playerModel.Health.Value; return playerModel.Health.Value;
} }
} }
// 使用命令 // 使用命令
public class UISystem : AbstractSystem public class UISystem : AbstractSystem
{ {
protected override void OnInit() protected override void OnInit()
{ {
this.RegisterEvent<UpdateUIEvent>(OnUpdateUI); this.RegisterEvent<UpdateUIEvent>(OnUpdateUI);
} }
private void OnUpdateUI(UpdateUIEvent e) private void OnUpdateUI(UpdateUIEvent e)
{ {
var health = this.SendCommand(new GetPlayerHealthQuery()); var health = this.SendCommand(new GetPlayerHealthQuery());
Console.WriteLine($"Player health: {health}"); Console.WriteLine($"Player health: {health}");
} }
} }
``` ```
## 命令的生命周期 ## 命令的生命周期
1. **创建命令**:实例化命令对象,传入必要的参数 1. **创建命令**:实例化命令对象,传入必要的参数
2. **执行命令**:调用 `Execute()` 方法,内部委托给 `OnExecute()` 2. **执行命令**:调用 `Execute()` 方法,内部委托给 `OnExecute()`
3. **返回结果**:对于带返回值的命令,返回执行结果 3. **返回结果**:对于带返回值的命令,返回执行结果
4. **命令销毁**:命令执行完毕后可以被垃圾回收 4. **命令销毁**:命令执行完毕后可以被垃圾回收
**注意事项:** **注意事项:**
- 命令应该是无状态的,执行完即可丢弃 - 命令应该是无状态的,执行完即可丢弃
- 避免在命令中保存长期引用 - 避免在命令中保存长期引用
- 命令执行应该是原子操作 - 命令执行应该是原子操作
@ -145,331 +145,331 @@ public sealed class DamagePlayerCommand(int amount) : AbstractCommand
- Store 负责统一归约状态变化 - Store 负责统一归约状态变化
完整示例见 [`state-management`](./state-management)。 完整示例见 [`state-management`](./state-management)。
## CommandBus - 命令总线 ## CommandBus - 命令总线
### 功能说明 ### 功能说明
`CommandBus` 是命令执行的核心组件,负责发送和执行命令。 `CommandBus` 是命令执行的核心组件,负责发送和执行命令。
**主要方法:** **主要方法:**
```csharp ```csharp
void Send(ICommand command); // 发送无返回值命令 void Send(ICommand command); // 发送无返回值命令
TResult Send<TResult>(ICommand<TResult> command); // 发送带返回值命令 TResult Send<TResult>(ICommand<TResult> command); // 发送带返回值命令
``` ```
**特点:** **特点:**
- 统一的命令执行入口 - 统一的命令执行入口
- 支持同步命令执行 - 支持同步命令执行
- 与架构上下文集成 - 与架构上下文集成
### 使用示例 ### 使用示例
```csharp ```csharp
// 通过架构获取命令总线 // 通过架构获取命令总线
var commandBus = architecture.Context.CommandBus; var commandBus = architecture.Context.CommandBus;
// 发送无返回值命令 // 发送无返回值命令
commandBus.Send(new StartGameCommand(1, "Player1")); commandBus.Send(new StartGameCommand(1, "Player1"));
// 发送带返回值命令 // 发送带返回值命令
var damage = commandBus.Send(new CalculateDamageCommand(100, 50)); var damage = commandBus.Send(new CalculateDamageCommand(100, 50));
``` ```
## 命令基类变体 ## 命令基类变体
框架提供了多种命令基类以满足不同需求: 框架提供了多种命令基类以满足不同需求:
### AbstractCommand`<TInput>` ### AbstractCommand`<TInput>`
带输入参数的无返回值命令类。通过 `ICommandInput` 接口传递参数。 带输入参数的无返回值命令类。通过 `ICommandInput` 接口传递参数。
**核心方法:** **核心方法:**
```csharp ```csharp
void ICommand.Execute(); // 实现 ICommand 接口 void ICommand.Execute(); // 实现 ICommand 接口
protected abstract void OnExecute(TInput input); // 抽象执行方法,接收输入参数 protected abstract void OnExecute(TInput input); // 抽象执行方法,接收输入参数
``` ```
**使用示例:** **使用示例:**
```csharp ```csharp
// 定义输入对象 // 定义输入对象
public class StartGameInput : ICommandInput public class StartGameInput : ICommandInput
{ {
public int LevelId { get; set; } public int LevelId { get; set; }
public string PlayerName { get; set; } public string PlayerName { get; set; }
} }
// 定义命令 // 定义命令
public class StartGameCommand : AbstractCommand<StartGameInput> public class StartGameCommand : AbstractCommand<StartGameInput>
{ {
protected override void OnExecute(StartGameInput input) protected override void OnExecute(StartGameInput input)
{ {
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
var gameModel = this.GetModel<GameModel>(); var gameModel = this.GetModel<GameModel>();
playerModel.PlayerName.Value = input.PlayerName; playerModel.PlayerName.Value = input.PlayerName;
gameModel.CurrentLevel.Value = input.LevelId; gameModel.CurrentLevel.Value = input.LevelId;
gameModel.GameState.Value = GameState.Playing; gameModel.GameState.Value = GameState.Playing;
this.SendEvent(new GameStartedEvent()); this.SendEvent(new GameStartedEvent());
} }
} }
// 使用命令 // 使用命令
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware] [ContextAware]
public partial class GameController : IController public partial class GameController : IController
{ {
public void OnStartButtonClicked() public void OnStartButtonClicked()
{ {
var input = new StartGameInput { LevelId = 1, PlayerName = "Player1" }; var input = new StartGameInput { LevelId = 1, PlayerName = "Player1" };
this.SendCommand(new StartGameCommand { Input = input }); this.SendCommand(new StartGameCommand { Input = input });
} }
} }
``` ```
### AbstractCommand`<TInput, TResult>` ### AbstractCommand`<TInput, TResult>`
既带输入参数又带返回值的命令类。 既带输入参数又带返回值的命令类。
**核心方法:** **核心方法:**
```csharp ```csharp
TResult ICommand<TResult>.Execute(); // 实现 ICommand<TResult> 接口 TResult ICommand<TResult>.Execute(); // 实现 ICommand<TResult> 接口
protected abstract TResult OnExecute(TInput input); // 抽象执行方法,接收输入参数 protected abstract TResult OnExecute(TInput input); // 抽象执行方法,接收输入参数
``` ```
**使用示例:** **使用示例:**
```csharp ```csharp
// 定义输入对象 // 定义输入对象
public class CalculateDamageInput : ICommandInput public class CalculateDamageInput : ICommandInput
{ {
public int AttackerAttackPower { get; set; } public int AttackerAttackPower { get; set; }
public int DefenderDefense { get; set; } public int DefenderDefense { get; set; }
} }
// 定义命令 // 定义命令
public class CalculateDamageCommand : AbstractCommand<CalculateDamageInput, int> public class CalculateDamageCommand : AbstractCommand<CalculateDamageInput, int>
{ {
protected override int OnExecute(CalculateDamageInput input) protected override int OnExecute(CalculateDamageInput input)
{ {
var config = this.GetModel<GameConfigModel>(); var config = this.GetModel<GameConfigModel>();
var baseDamage = input.AttackerAttackPower - input.DefenderDefense; var baseDamage = input.AttackerAttackPower - input.DefenderDefense;
var finalDamage = Math.Max(1, baseDamage * config.DamageMultiplier); var finalDamage = Math.Max(1, baseDamage * config.DamageMultiplier);
return (int)finalDamage; return (int)finalDamage;
} }
} }
// 使用命令 // 使用命令
public class CombatSystem : AbstractSystem public class CombatSystem : AbstractSystem
{ {
protected override void OnInit() { } protected override void OnInit() { }
public void Attack(Character attacker, Character defender) public void Attack(Character attacker, Character defender)
{ {
var input = new CalculateDamageInput var input = new CalculateDamageInput
{ {
AttackerAttackPower = attacker.AttackPower, AttackerAttackPower = attacker.AttackPower,
DefenderDefense = defender.Defense DefenderDefense = defender.Defense
}; };
var damage = this.SendCommand(new CalculateDamageCommand { Input = input }); var damage = this.SendCommand(new CalculateDamageCommand { Input = input });
defender.Health -= damage; defender.Health -= damage;
this.SendEvent(new DamageDealtEvent(attacker, defender, damage)); this.SendEvent(new DamageDealtEvent(attacker, defender, damage));
} }
} }
``` ```
### AbstractAsyncCommand`<TInput>` ### AbstractAsyncCommand`<TInput>`
支持异步执行的带输入参数的无返回值命令基类。 支持异步执行的带输入参数的无返回值命令基类。
**核心方法:** **核心方法:**
```csharp ```csharp
Task IAsyncCommand.ExecuteAsync(); // 实现异步命令接口 Task IAsyncCommand.ExecuteAsync(); // 实现异步命令接口
protected abstract Task OnExecuteAsync(TInput input); // 抽象异步执行方法 protected abstract Task OnExecuteAsync(TInput input); // 抽象异步执行方法
``` ```
### AbstractAsyncCommand`<TInput, TResult>` ### AbstractAsyncCommand`<TInput, TResult>`
支持异步执行的既带输入参数又带返回值的命令基类。 支持异步执行的既带输入参数又带返回值的命令基类。
**核心方法:** **核心方法:**
```csharp ```csharp
Task<TResult> IAsyncCommand<TResult>.ExecuteAsync(); // 实现异步命令接口 Task<TResult> IAsyncCommand<TResult>.ExecuteAsync(); // 实现异步命令接口
protected abstract Task<TResult> OnExecuteAsync(TInput input); // 抽象异步执行方法 protected abstract Task<TResult> OnExecuteAsync(TInput input); // 抽象异步执行方法
``` ```
**使用示例:** **使用示例:**
```csharp ```csharp
// 定义输入对象 // 定义输入对象
public class LoadSaveDataInput : ICommandInput public class LoadSaveDataInput : ICommandInput
{ {
public string SaveSlot { get; set; } public string SaveSlot { get; set; }
} }
// 定义异步命令 // 定义异步命令
public class LoadSaveDataCommand : AbstractAsyncCommand<LoadSaveDataInput, SaveData> public class LoadSaveDataCommand : AbstractAsyncCommand<LoadSaveDataInput, SaveData>
{ {
protected override async Task<SaveData> OnExecuteAsync(LoadSaveDataInput input) protected override async Task<SaveData> OnExecuteAsync(LoadSaveDataInput input)
{ {
var storage = this.GetUtility<IStorageUtility>(); var storage = this.GetUtility<IStorageUtility>();
return await storage.LoadSaveDataAsync(input.SaveSlot); return await storage.LoadSaveDataAsync(input.SaveSlot);
} }
} }
// 使用异步命令 // 使用异步命令
public class SaveSystem : AbstractSystem public class SaveSystem : AbstractSystem
{ {
protected override void OnInit() protected override void OnInit()
{ {
this.RegisterEvent<LoadGameRequestEvent>(OnLoadGameRequest); this.RegisterEvent<LoadGameRequestEvent>(OnLoadGameRequest);
} }
private async void OnLoadGameRequest(LoadGameRequestEvent e) private async void OnLoadGameRequest(LoadGameRequestEvent e)
{ {
var input = new LoadSaveDataInput { SaveSlot = e.SaveSlot }; var input = new LoadSaveDataInput { SaveSlot = e.SaveSlot };
var saveData = await this.SendCommandAsync(new LoadSaveDataCommand { Input = input }); var saveData = await this.SendCommandAsync(new LoadSaveDataCommand { Input = input });
if (saveData != null) if (saveData != null)
{ {
this.SendEvent(new GameLoadedEvent { SaveData = saveData }); this.SendEvent(new GameLoadedEvent { SaveData = saveData });
} }
} }
} }
``` ```
## 命令处理器执行 ## 命令处理器执行
所有发送给命令总线的命令最终都会通过 `CommandExecutor` 来执行: 所有发送给命令总线的命令最终都会通过 `CommandExecutor` 来执行:
```csharp ```csharp
public class CommandExecutor public class CommandExecutor
{ {
public static void Execute(ICommand command) public static void Execute(ICommand command)
{ {
command.Execute(); command.Execute();
} }
public static TResult Execute<TResult>(ICommand<TResult> command) public static TResult Execute<TResult>(ICommand<TResult> command)
{ {
return command.Execute(); return command.Execute();
} }
} }
``` ```
**特点:** **特点:**
- 提供统一的命令执行机制 - 提供统一的命令执行机制
- 支持同步和异步命令执行 - 支持同步和异步命令执行
- 可以扩展添加中间件逻辑 - 可以扩展添加中间件逻辑
## 使用场景 ## 使用场景
### 1. 用户交互操作 ### 1. 用户交互操作
```csharp ```csharp
public class SaveGameCommand : AbstractCommand public class SaveGameCommand : AbstractCommand
{ {
private readonly string _saveSlot; private readonly string _saveSlot;
public SaveGameCommand(string saveSlot) public SaveGameCommand(string saveSlot)
{ {
_saveSlot = saveSlot; _saveSlot = saveSlot;
} }
protected override void OnExecute() protected override void OnExecute()
{ {
var saveSystem = this.GetSystem<SaveSystem>(); var saveSystem = this.GetSystem<SaveSystem>();
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
saveSystem.SavePlayerData(playerModel, _saveSlot); saveSystem.SavePlayerData(playerModel, _saveSlot);
this.SendEvent(new GameSavedEvent(_saveSlot)); this.SendEvent(new GameSavedEvent(_saveSlot));
} }
} }
``` ```
### 2. 业务流程控制 ### 2. 业务流程控制
```csharp ```csharp
public class LoadLevelCommand : AbstractCommand public class LoadLevelCommand : AbstractCommand
{ {
private readonly int _levelId; private readonly int _levelId;
public LoadLevelCommand(int levelId) public LoadLevelCommand(int levelId)
{ {
_levelId = levelId; _levelId = levelId;
} }
protected override void OnExecute() protected override void OnExecute()
{ {
var levelSystem = this.GetSystem<LevelSystem>(); var levelSystem = this.GetSystem<LevelSystem>();
var uiSystem = this.GetSystem<UISystem>(); var uiSystem = this.GetSystem<UISystem>();
// 显示加载界面 // 显示加载界面
uiSystem.ShowLoadingScreen(); uiSystem.ShowLoadingScreen();
// 加载关卡 // 加载关卡
levelSystem.LoadLevel(_levelId); levelSystem.LoadLevel(_levelId);
// 发送事件 // 发送事件
this.SendEvent(new LevelLoadedEvent(_levelId)); this.SendEvent(new LevelLoadedEvent(_levelId));
} }
} }
``` ```
## 最佳实践 ## 最佳实践
1. **保持命令原子性**:一个命令应该完成一个完整的业务操作 1. **保持命令原子性**:一个命令应该完成一个完整的业务操作
2. **命令无状态**:命令不应该保存长期状态,执行完即可丢弃 2. **命令无状态**:命令不应该保存长期状态,执行完即可丢弃
3. **参数通过构造函数传递**:命令需要的参数应在创建时传入 3. **参数通过构造函数传递**:命令需要的参数应在创建时传入
4. **避免命令嵌套**:命令内部尽量不要发送其他命令,使用事件通信 4. **避免命令嵌套**:命令内部尽量不要发送其他命令,使用事件通信
5. **合理使用返回值**:只在确实需要返回结果时使用带返回值的命令 5. **合理使用返回值**:只在确实需要返回结果时使用带返回值的命令
6. **命令命名规范**:使用动词+名词形式,如 `StartGameCommand``SavePlayerCommand` 6. **命令命名规范**:使用动词+名词形式,如 `StartGameCommand``SavePlayerCommand`
7. **单一职责原则**:每个命令只负责一个特定的业务操作 7. **单一职责原则**:每个命令只负责一个特定的业务操作
8. **使用异步命令**:对于需要长时间执行的操作,使用异步命令避免阻塞 8. **使用异步命令**:对于需要长时间执行的操作,使用异步命令避免阻塞
9. **命令验证**:在命令执行前验证输入参数的有效性 9. **命令验证**:在命令执行前验证输入参数的有效性
10. **错误处理**:在命令中适当处理异常情况 10. **错误处理**:在命令中适当处理异常情况
## 命令模式优势 ## 命令模式优势
### 1. 可扩展性 ### 1. 可扩展性
- 命令可以被序列化和存储 - 命令可以被序列化和存储
- 支持命令队列和批处理 - 支持命令队列和批处理
- 便于实现撤销/重做功能 - 便于实现撤销/重做功能
### 2. 可测试性 ### 2. 可测试性
- 命令逻辑独立,易于单元测试 - 命令逻辑独立,易于单元测试
- 可以模拟命令执行结果 - 可以模拟命令执行结果
- 支持行为驱动开发 - 支持行为驱动开发
### 3. 可维护性 ### 3. 可维护性
- 业务逻辑集中管理 - 业务逻辑集中管理
- 降低组件间耦合度 - 降低组件间耦合度
- 便于重构和扩展 - 便于重构和扩展
## 相关包 ## 相关包
- [`architecture`](./architecture.md) - 架构核心,负责命令的分发和执行 - [`architecture`](./architecture.md) - 架构核心,负责命令的分发和执行
- [`extensions`](./extensions.md) - 提供 `SendCommand()` 扩展方法 - [`extensions`](./extensions.md) - 提供 `SendCommand()` 扩展方法
- [`query`](./query.md) - 查询模式,用于数据查询 - [`query`](./query.md) - 查询模式,用于数据查询
- [`events`](./events.md) - 事件系统,命令执行后的通知机制 - [`events`](./events.md) - 事件系统,命令执行后的通知机制
- [`system`](./system.md) - 业务系统,命令的主要执行者 - [`system`](./system.md) - 业务系统,命令的主要执行者
- [`model`](./model.md) - 数据模型,命令操作的数据 - [`model`](./model.md) - 数据模型,命令操作的数据
--- ---
**许可证**Apache 2.0 **许可证**Apache 2.0

File diff suppressed because it is too large Load Diff

View File

@ -1,61 +1,95 @@
--- ---
title: 协程系统 title: 协程系统
description: 协程系统提供基于 IEnumerator<IYieldInstruction>调度、等待和组合能力可与事件、Task、命令与查询集成 description: 基于 IEnumerator<IYieldInstruction>协程调度系统支持时间等待、阶段等待、Task 桥接、事件等待与运行时快照查询
--- ---
# 协程系统 # 协程系统
## 概述 ## 概述
GFramework 的 Core 协程系统基于 `IEnumerator<IYieldInstruction>` 构建,通过 `CoroutineScheduler` `GFramework.Core.Coroutine` 提供一个宿主无关的协程内核。它围绕 `CoroutineScheduler` 工作,统一处理:
统一推进协程执行。它适合处理分帧逻辑、时间等待、条件等待、Task 桥接,以及事件驱动的异步流程。
协程系统主要由以下部分组成: - `IEnumerator<IYieldInstruction>` 形式的协程推进
- 时间等待、条件等待、Task 等待与事件等待
- 标签、分组、暂停、恢复与终止
- 取消令牌、完成状态查询与运行快照
- 调度阶段语义,例如默认更新、固定更新和帧结束
- `CoroutineScheduler`:负责运行、更新和控制协程 Core 协程本身不依赖任何具体引擎;阶段语义是否真实成立,取决于宿主是否为调度器提供了匹配的执行阶段。
- `CoroutineHandle`:用于标识协程实例并控制其状态
- `IYieldInstruction`:定义等待行为的统一接口
- `Instructions`:内置等待指令集合
- `CoroutineHelper`:提供常用等待与生成器辅助方法
- `Extensions`:提供 Task、组合、命令、查询和 Mediator 场景下的扩展方法
## 核心概念 ## CoroutineScheduler
### CoroutineScheduler ### 基础创建
`CoroutineScheduler` 是协程系统的核心调度器。构造时需要提供 `ITimeSource`,调度器会在每次 `Update()` 时读取时间增量并推进所有活跃协程。
```csharp ```csharp
using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine; using GFramework.Core.Coroutine;
ITimeSource timeSource = /* 你的时间源实现 */; ITimeSource scaledTimeSource = /* 游戏时间 */;
var scheduler = new CoroutineScheduler(timeSource); ITimeSource realtimeTimeSource = /* 真实时间,可选 */;
var handle = scheduler.Run(MyCoroutine()); var scheduler = new CoroutineScheduler(
scaledTimeSource,
realtimeTimeSource: realtimeTimeSource,
executionStage: CoroutineExecutionStage.Update);
// 在你的主循环中推进协程 var handle = scheduler.Run(MyCoroutine(), tag: "bootstrap", group: "loading");
// 在宿主主循环中推进协程
scheduler.Update(); scheduler.Update();
``` ```
如果需要统计信息,可以启用构造函数的 `enableStatistics` 参数。 构造参数中最重要的两个语义是:
### CoroutineHandle - `realtimeTimeSource`
- 如果提供,`WaitForSecondsRealtime` 会使用它的 `DeltaTime`
- 如果不提供,实时等待会退化为使用默认时间源
- `executionStage`
- `Update`:默认阶段
- `FixedUpdate`:固定步阶段
- `EndOfFrame`:帧结束阶段
`CoroutineHandle` 用于引用具体协程,并配合调度器进行控制: ### 控制与完成状态
```csharp ```csharp
var handle = scheduler.Run(MyCoroutine(), tag: "gameplay", group: "battle"); using var cts = new CancellationTokenSource();
if (scheduler.IsCoroutineAlive(handle)) var handle = scheduler.Run(
{ LoadResources(),
scheduler.Pause(handle); tag: "loading",
scheduler.Resume(handle); group: "bootstrap",
scheduler.Kill(handle); cancellationToken: cts.Token);
}
scheduler.Pause(handle);
scheduler.Resume(handle);
scheduler.Kill(handle);
var completionStatus = await scheduler.WaitForCompletionAsync(handle);
``` ```
### IYieldInstruction 协程的最终结果由 `CoroutineCompletionStatus` 表示:
- `Completed`
- `Cancelled`
- `Faulted`
- `Unknown`
### 快照与可观测性
```csharp
if (scheduler.TryGetSnapshot(handle, out var snapshot))
{
Console.WriteLine(snapshot.State);
Console.WriteLine(snapshot.WaitingInstructionType);
Console.WriteLine(snapshot.ExecutionStage);
}
var allSnapshots = scheduler.GetActiveSnapshots();
```
快照适合做诊断、调试面板和运行中状态检查。
## IYieldInstruction
协程通过 `yield return IYieldInstruction` 表达等待逻辑: 协程通过 `yield return IYieldInstruction` 表达等待逻辑:
@ -67,91 +101,40 @@ public interface IYieldInstruction
} }
``` ```
## 基本用法
### 创建简单协程
```csharp
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions;
public IEnumerator<IYieldInstruction> SimpleCoroutine()
{
Console.WriteLine("开始");
yield return new Delay(2.0);
Console.WriteLine("2 秒后");
yield return new WaitOneFrame();
Console.WriteLine("下一帧");
}
```
### 使用 CoroutineHelper
`CoroutineHelper` 提供了一组常用等待和生成器辅助方法:
```csharp
using GFramework.Core.Coroutine;
public IEnumerator<IYieldInstruction> HelperCoroutine()
{
yield return CoroutineHelper.WaitForSeconds(1.5);
yield return CoroutineHelper.WaitForOneFrame();
yield return CoroutineHelper.WaitForFrames(10);
yield return CoroutineHelper.WaitUntil(() => isReady);
yield return CoroutineHelper.WaitWhile(() => isLoading);
}
```
除了直接返回等待指令,`CoroutineHelper` 也可以直接生成可运行的协程枚举器:
```csharp
scheduler.Run(CoroutineHelper.DelayedCall(2.0, () => Console.WriteLine("延迟执行")));
scheduler.Run(CoroutineHelper.RepeatCall(1.0, 5, () => Console.WriteLine("重复执行")));
using var cts = new CancellationTokenSource();
scheduler.Run(CoroutineHelper.RepeatCallForever(1.0, () => Console.WriteLine("持续执行"), cts.Token));
```
### 控制协程状态
```csharp
var handle = scheduler.Run(LoadResources(), tag: "loading", group: "bootstrap");
scheduler.Pause(handle);
scheduler.Resume(handle);
scheduler.Kill(handle);
scheduler.KillByTag("loading");
scheduler.PauseGroup("bootstrap");
scheduler.ResumeGroup("bootstrap");
scheduler.KillGroup("bootstrap");
var cleared = scheduler.Clear();
```
## 常用等待指令 ## 常用等待指令
### 时间与帧 ### 时间与帧
```csharp ```csharp
yield return new Delay(1.0); yield return new Delay(1.0);
yield return new WaitForSecondsScaled(1.0);
yield return new WaitForSecondsRealtime(1.0); yield return new WaitForSecondsRealtime(1.0);
yield return new WaitOneFrame(); yield return new WaitOneFrame();
yield return new WaitForNextFrame(); yield return new WaitForNextFrame();
yield return new WaitForFrames(5); yield return new WaitForFrames(5);
yield return new WaitForEndOfFrame();
yield return new WaitForFixedUpdate(); yield return new WaitForFixedUpdate();
yield return new WaitForEndOfFrame();
``` ```
语义说明:
- `Delay``WaitForSecondsScaled`
- 使用调度器默认时间源推进
- `WaitForSecondsRealtime`
- 优先使用调度器的 `realtimeTimeSource`
- `WaitForFixedUpdate`
- 仅在 `CoroutineExecutionStage.FixedUpdate` 调度器中推进
- `WaitForEndOfFrame`
- 仅在 `CoroutineExecutionStage.EndOfFrame` 调度器中推进
如果宿主没有提供匹配阶段,这类阶段型等待不会自然完成。
### 条件等待 ### 条件等待
```csharp ```csharp
yield return new WaitUntil(() => health > 0); yield return new WaitUntil(() => health > 0);
yield return new WaitWhile(() => isLoading); yield return new WaitWhile(() => isLoading);
yield return new WaitForPredicate(() => hp >= maxHp); yield return new WaitForPredicate(() => hp >= maxHp);
yield return new WaitForPredicate(() => isBusy, waitForTrue: false);
yield return new WaitUntilOrTimeout(() => connected, timeoutSeconds: 5.0); yield return new WaitUntilOrTimeout(() => connected, timeoutSeconds: 5.0);
yield return new WaitForConditionChange(() => isPaused, waitForTransitionTo: true); yield return new WaitForConditionChange(() => isPaused, waitForTransitionTo: true);
``` ```
@ -159,27 +142,14 @@ yield return new WaitForConditionChange(() => isPaused, waitForTransitionTo: tru
### Task 桥接 ### Task 桥接
```csharp ```csharp
using System.Threading.Tasks;
using GFramework.Core.Coroutine.Extensions; using GFramework.Core.Coroutine.Extensions;
Task loadTask = LoadDataAsync(); Task loadTask = LoadDataAsync();
yield return loadTask.AsCoroutineInstruction(); yield return loadTask.AsCoroutineInstruction();
var handle = scheduler.StartTaskAsCoroutine(LoadDataAsync());
``` ```
也可以将 `Task` 转成协程枚举器后直接交给调度器:
```csharp
var coroutine = LoadDataAsync().ToCoroutineEnumerator();
var handle1 = scheduler.Run(coroutine);
var handle2 = scheduler.StartTaskAsCoroutine(LoadDataAsync());
```
- `AsCoroutineInstruction()` 适合已经处在某个协程内部,只需要在当前位置等待 `Task` 完成的场景。
- `ToCoroutineEnumerator()` 适合需要把 `Task` 先转换成 `IEnumerator<IYieldInstruction>`,再传给 `scheduler.Run(...)`
`Sequence(...)` 或其他只接受协程枚举器的 API。
- `StartTaskAsCoroutine()` 适合已经持有 `CoroutineScheduler`,并希望把 `Task` 直接作为一个顶层协程启动的场景。
### 等待事件 ### 等待事件
```csharp ```csharp
@ -188,236 +158,52 @@ using GFramework.Core.Coroutine.Instructions;
public IEnumerator<IYieldInstruction> WaitForEventExample(IEventBus eventBus) public IEnumerator<IYieldInstruction> WaitForEventExample(IEventBus eventBus)
{ {
using var waitEvent = new WaitForEvent<PlayerDiedEvent>(eventBus); using var wait = new WaitForEvent<PlayerJoinedEvent>(eventBus);
yield return waitEvent;
var eventData = waitEvent.EventData;
Console.WriteLine($"玩家 {eventData!.PlayerId} 死亡");
}
```
为事件等待附加超时:
```csharp
public IEnumerator<IYieldInstruction> WaitForEventWithTimeoutExample(IEventBus eventBus)
{
using var waitEvent = new WaitForEvent<PlayerJoinedEvent>(eventBus);
var timeoutWait = new WaitForEventWithTimeout<PlayerJoinedEvent>(waitEvent, 5.0f);
yield return timeoutWait;
if (timeoutWait.IsTimeout)
Console.WriteLine("等待超时");
else
Console.WriteLine($"玩家加入: {timeoutWait.EventData!.PlayerName}");
}
```
等待两个事件中的任意一个:
```csharp
public IEnumerator<IYieldInstruction> WaitForEitherEvent(IEventBus eventBus)
{
using var wait = new WaitForMultipleEvents<PlayerReadyEvent, PlayerQuitEvent>(eventBus);
yield return wait; yield return wait;
if (wait.TriggeredBy == 1) Console.WriteLine(wait.EventData?.PlayerName);
Console.WriteLine($"Ready: {wait.FirstEventData}");
else
Console.WriteLine($"Quit: {wait.SecondEventData}");
} }
``` ```
### 协程组合 ## CoroutineHelper
等待子协程完成: `CoroutineHelper` 提供一组常用简写:
```csharp
yield return CoroutineHelper.WaitForSeconds(1.5);
yield return CoroutineHelper.WaitForOneFrame();
yield return CoroutineHelper.WaitForFrames(10);
yield return CoroutineHelper.WaitUntil(() => isReady);
yield return CoroutineHelper.WaitWhile(() => isLoading);
```
也可以直接生成可运行的协程枚举器:
```csharp
scheduler.Run(CoroutineHelper.DelayedCall(2.0, () => Console.WriteLine("延迟执行")));
scheduler.Run(CoroutineHelper.RepeatCall(1.0, 5, () => Console.WriteLine("重复执行")));
```
## 协程组合
```csharp ```csharp
public IEnumerator<IYieldInstruction> ParentCoroutine() public IEnumerator<IYieldInstruction> ParentCoroutine()
{ {
Console.WriteLine("父协程开始");
yield return new WaitForCoroutine(ChildCoroutine()); yield return new WaitForCoroutine(ChildCoroutine());
Console.WriteLine("子协程完成");
} }
private IEnumerator<IYieldInstruction> ChildCoroutine() private IEnumerator<IYieldInstruction> ChildCoroutine()
{ {
yield return CoroutineHelper.WaitForSeconds(1.0); yield return new Delay(1.0);
Console.WriteLine("子协程执行");
} }
``` ```
等待多个句柄全部完成: 如果需要等待多个顶层协程句柄,可以结合 `WaitForAllCoroutines``ParallelCoroutines(...)` 使用。
```csharp ## 建议
public IEnumerator<IYieldInstruction> WaitForMultipleCoroutines(CoroutineScheduler scheduler)
{
var handles = new List<CoroutineHandle>
{
scheduler.Run(LoadTexture()),
scheduler.Run(LoadAudio()),
scheduler.Run(LoadModel())
};
yield return new WaitForAllCoroutines(scheduler, handles); - 普通游戏时间等待优先使用 `Delay``WaitForSecondsScaled`
- 只有宿主提供真实时间源时再使用 `WaitForSecondsRealtime`
Console.WriteLine("所有资源加载完成"); - 只有宿主显式区分阶段时才使用 `WaitForFixedUpdate``WaitForEndOfFrame`
} - 需要对接生命周期或外部取消时,优先传入 `CancellationToken`
``` - 需要诊断线上状态时,优先使用 `TryGetSnapshot(...)``GetActiveSnapshots()`
### 进度等待
```csharp
public IEnumerator<IYieldInstruction> LoadingWithProgress()
{
yield return CoroutineHelper.WaitForProgress(
duration: 3.0,
onProgress: progress => Console.WriteLine($"加载进度: {progress * 100:F0}%"));
}
```
## 扩展方法
### 组合扩展
`CoroutineComposeExtensions` 提供链式顺序组合能力:
```csharp
using GFramework.Core.Coroutine.Extensions;
var chained =
LoadConfig()
.Then(() => Console.WriteLine("配置加载完成"))
.Then(StartBattle());
scheduler.Run(chained);
```
### 协程生成扩展
`CoroutineExtensions` 提供了一些常用的协程生成器:
```csharp
using GFramework.Core.Coroutine.Extensions;
var delayed = CoroutineExtensions.ExecuteAfter(2.0, () => Console.WriteLine("延迟执行"));
var repeated = CoroutineExtensions.RepeatEvery(1.0, () => Console.WriteLine("tick"), count: 5);
var progress = CoroutineExtensions.WaitForSecondsWithProgress(3.0, p => Console.WriteLine(p));
scheduler.Run(delayed);
scheduler.Run(repeated);
scheduler.Run(progress);
```
顺序或并行组合多个协程:
```csharp
var sequence = CoroutineExtensions.Sequence(LoadConfig(), LoadScene(), StartBattle());
scheduler.Run(sequence);
var parallel = scheduler.ParallelCoroutines(LoadTexture(), LoadAudio(), LoadModel());
scheduler.Run(parallel);
```
### Task 扩展
`TaskCoroutineExtensions` 提供了三类扩展:
- `AsCoroutineInstruction()`:把 `Task` / `Task<T>` 包装成等待指令
- `ToCoroutineEnumerator()`:把 `Task` / `Task<T>` 转成协程枚举器
- `StartTaskAsCoroutine()`:直接通过调度器启动 Task 协程
### 命令、查询与 Mediator 扩展
这些扩展都定义在 `GFramework.Core.Coroutine.Extensions` 命名空间中。
### 命令协程
```csharp
using GFramework.Core.Coroutine.Extensions;
public IEnumerator<IYieldInstruction> ExecuteCommand(IContextAware contextAware)
{
yield return contextAware.SendCommandCoroutineWithErrorHandler(
new LoadSceneCommand(),
ex => Console.WriteLine(ex.Message));
}
```
如果命令执行后需要等待事件:
```csharp
public IEnumerator<IYieldInstruction> ExecuteCommandAndWaitEvent(IContextAware contextAware)
{
yield return contextAware.SendCommandAndWaitEventCoroutine<LoadSceneCommand, SceneLoadedEvent>(
new LoadSceneCommand(),
evt => Console.WriteLine($"场景加载完成: {evt.SceneName}"),
timeout: 5.0f);
}
```
### 查询协程
`SendQueryCoroutine` 会同步执行查询,并通过回调返回结果:
```csharp
public IEnumerator<IYieldInstruction> QueryPlayer(IContextAware contextAware)
{
yield return contextAware.SendQueryCoroutine<GetPlayerDataQuery, PlayerData>(
new GetPlayerDataQuery { PlayerId = 1 },
playerData => Console.WriteLine($"玩家名称: {playerData.Name}"));
}
```
### Mediator 协程
如果项目使用 `Mediator.IMediator`,还可以使用 `MediatorCoroutineExtensions`
```csharp
public IEnumerator<IYieldInstruction> ExecuteMediatorCommand(IContextAware contextAware)
{
yield return contextAware.SendCommandCoroutine(
new SaveArchiveCommand(),
ex => Console.WriteLine(ex.Message));
}
```
## 异常处理
调度器会在协程抛出未捕获异常时触发 `OnCoroutineException`
```csharp
scheduler.OnCoroutineException += (handle, exception) =>
{
Console.WriteLine($"协程 {handle} 异常: {exception.Message}");
};
```
如果协程等待的是 `Task`,也可以通过 `WaitForTask` / `WaitForTask<T>` 检查任务异常。
## 常见问题
### 协程什么时候执行?
协程在调度器的 `Update()` 中推进。调度器每次更新都会先更新 `ITimeSource`,再推进所有活跃协程。
### 协程是多线程的吗?
不是。协程本身仍由调用 `Update()` 的线程推进,通常用于主线程上的分帧流程控制。
### `Delay``CoroutineHelper.WaitForSeconds()` 有什么区别?
两者表达的是同一类等待语义。`CoroutineHelper.WaitForSeconds()` 只是 `Delay` 的辅助构造方法。
### 如何等待异步方法?
在现有协程里等待 `Task` 时,优先使用 `yield return task.AsCoroutineInstruction()`;如果要把 `Task` 单独交给调度器启动,使用
`scheduler.StartTaskAsCoroutine(task)`;如果中间还需要传给只接受协程枚举器的 API则先调用 `task.ToCoroutineEnumerator()`
## 相关文档
- [事件系统](/zh-CN/core/events)
- [CQRS](/zh-CN/core/cqrs)
- [协程系统教程](/zh-CN/tutorials/coroutine-tutorial)

View File

@ -216,15 +216,20 @@ public class GameArchitecture : Architecture
{ {
protected override void Init() protected override void Init()
{ {
// 注册 Mediator 行为 // 注册通用开放泛型行为
RegisterMediatorBehavior<LoggingBehavior>(); RegisterMediatorBehavior<LoggingBehavior<,>>();
RegisterMediatorBehavior<PerformanceBehavior>(); RegisterMediatorBehavior<PerformanceBehavior<,>>();
// 处理器会自动通过依赖注入注册 // 处理器会自动通过依赖注入注册
} }
} }
``` ```
`RegisterMediatorBehavior<TBehavior>()` 同时支持两种形式:
- 开放泛型行为,例如 `LoggingBehavior<,>`,用于匹配所有请求
- 封闭行为类型,例如某个只服务于单一请求的 `SpecialBehavior`
## 高级用法 ## 高级用法
### Request请求 ### Request请求

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
# Property 包使用说明 # Property 包使用说明
## 概述 ## 概述
Property 包提供了可绑定属性BindableProperty的实现支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。 Property 包提供了可绑定属性BindableProperty的实现支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。
BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。 BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。
@ -9,137 +9,137 @@ BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事
> 对于简单字段和局部 UI 绑定,`BindableProperty<T>` 仍然是首选方案。 > 对于简单字段和局部 UI 绑定,`BindableProperty<T>` 仍然是首选方案。
> 如果你需要统一管理复杂状态树、通过 action / reducer 演进状态,或复用局部状态选择器, > 如果你需要统一管理复杂状态树、通过 action / reducer 演进状态,或复用局部状态选择器,
> 请同时参考 [`state-management`](./state-management)。 > 请同时参考 [`state-management`](./state-management)。
## 核心接口 ## 核心接口
### IReadonlyBindableProperty`<T>` ### IReadonlyBindableProperty`<T>`
只读可绑定属性接口,提供属性值的读取和变更监听功能。 只读可绑定属性接口,提供属性值的读取和变更监听功能。
**核心成员:** **核心成员:**
```csharp ```csharp
// 获取属性值 // 获取属性值
T Value { get; } T Value { get; }
// 注册监听(不立即触发回调) // 注册监听(不立即触发回调)
IUnRegister Register(Action<T> onValueChanged); IUnRegister Register(Action<T> onValueChanged);
// 注册监听并立即触发回调传递当前值 // 注册监听并立即触发回调传递当前值
IUnRegister RegisterWithInitValue(Action<T> action); IUnRegister RegisterWithInitValue(Action<T> action);
// 取消监听 // 取消监听
void UnRegister(Action<T> onValueChanged); void UnRegister(Action<T> onValueChanged);
``` ```
### IBindableProperty`<T>` ### IBindableProperty`<T>`
可绑定属性接口,继承自只读接口,增加了修改能力。 可绑定属性接口,继承自只读接口,增加了修改能力。
**核心成员:** **核心成员:**
```csharp ```csharp
// 可读写的属性值 // 可读写的属性值
new T Value { get; set; } new T Value { get; set; }
// 设置值但不触发事件 // 设置值但不触发事件
void SetValueWithoutEvent(T newValue); void SetValueWithoutEvent(T newValue);
``` ```
## 核心类 ## 核心类
### BindableProperty`<T>` ### BindableProperty`<T>`
可绑定属性的完整实现。 可绑定属性的完整实现。
**核心方法:** **核心方法:**
```csharp ```csharp
// 构造函数 // 构造函数
BindableProperty(T defaultValue = default!); BindableProperty(T defaultValue = default!);
// 属性值 // 属性值
T Value { get; set; } T Value { get; set; }
// 注册监听 // 注册监听
IUnRegister Register(Action<T> onValueChanged); IUnRegister Register(Action<T> onValueChanged);
IUnRegister RegisterWithInitValue(Action<T> action); IUnRegister RegisterWithInitValue(Action<T> action);
// 取消监听 // 取消监听
void UnRegister(Action<T> onValueChanged); void UnRegister(Action<T> onValueChanged);
// 设置值但不触发事件 // 设置值但不触发事件
void SetValueWithoutEvent(T newValue); void SetValueWithoutEvent(T newValue);
// 设置自定义比较器 // 设置自定义比较器
BindableProperty<T> WithComparer(Func<T, T, bool> comparer); BindableProperty<T> WithComparer(Func<T, T, bool> comparer);
``` ```
**使用示例:** **使用示例:**
```csharp ```csharp
// 创建可绑定属性 // 创建可绑定属性
var health = new BindableProperty<int>(100); var health = new BindableProperty<int>(100);
// 监听值变化(不会立即触发) // 监听值变化(不会立即触发)
var unregister = health.Register(newValue => var unregister = health.Register(newValue =>
{ {
Console.WriteLine($"Health changed to: {newValue}"); Console.WriteLine($"Health changed to: {newValue}");
}); });
// 设置值(会触发监听器) // 设置值(会触发监听器)
health.Value = 50; // 输出: Health changed to: 50 health.Value = 50; // 输出: Health changed to: 50
// 取消监听 // 取消监听
unregister.UnRegister(); unregister.UnRegister();
// 设置值但不触发事件 // 设置值但不触发事件
health.SetValueWithoutEvent(75); health.SetValueWithoutEvent(75);
``` ```
**高级功能:** **高级功能:**
```csharp ```csharp
// 1. 注册并立即获得当前值 // 1. 注册并立即获得当前值
health.RegisterWithInitValue(value => health.RegisterWithInitValue(value =>
{ {
Console.WriteLine($"Current health: {value}"); // 立即输出当前值 Console.WriteLine($"Current health: {value}"); // 立即输出当前值
// 后续值变化时也会调用 // 后续值变化时也会调用
}); });
// 2. 自定义比较器(静态方法) // 2. 自定义比较器(静态方法)
BindableProperty<int>.Comparer = (a, b) => Math.Abs(a - b) < 1; BindableProperty<int>.Comparer = (a, b) => Math.Abs(a - b) < 1;
// 3. 使用实例方法设置比较器 // 3. 使用实例方法设置比较器
var position = new BindableProperty<Vector3>(Vector3.Zero) var position = new BindableProperty<Vector3>(Vector3.Zero)
.WithComparer((a, b) => a.DistanceTo(b) < 0.01f); // 距离小于0.01认为相等 .WithComparer((a, b) => a.DistanceTo(b) < 0.01f); // 距离小于0.01认为相等
// 4. 字符串比较器示例 // 4. 字符串比较器示例
var name = new BindableProperty<string>("Player") var name = new BindableProperty<string>("Player")
.WithComparer((a, b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase)); .WithComparer((a, b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase));
``` ```
### BindablePropertyUnRegister`<T>` ### BindablePropertyUnRegister`<T>`
可绑定属性的注销器,负责清理监听。 可绑定属性的注销器,负责清理监听。
**使用示例:** **使用示例:**
```csharp ```csharp
var unregister = health.Register(OnHealthChanged); var unregister = health.Register(OnHealthChanged);
// 当需要取消监听时 // 当需要取消监听时
unregister.UnRegister(); unregister.UnRegister();
``` ```
## BindableProperty 工作原理 ## BindableProperty 工作原理
BindableProperty 基于事件系统实现属性变化通知: BindableProperty 基于事件系统实现属性变化通知:
1. **值设置**:当设置 `Value` 属性时,首先进行值比较 1. **值设置**:当设置 `Value` 属性时,首先进行值比较
2. **变化检测**:使用 `EqualityComparer<T>.Default` 或自定义比较器检测值变化 2. **变化检测**:使用 `EqualityComparer<T>.Default` 或自定义比较器检测值变化
3. **事件触发**:如果值发生变化,调用所有注册的回调函数 3. **事件触发**:如果值发生变化,调用所有注册的回调函数
4. **内存管理**:通过 `IUnRegister` 机制管理监听器的生命周期 4. **内存管理**:通过 `IUnRegister` 机制管理监听器的生命周期
## 在 Model 中使用 ## 在 Model 中使用
### 什么时候继续使用 BindableProperty ### 什么时候继续使用 BindableProperty
@ -178,285 +178,285 @@ public class PlayerStateModel : AbstractModel
public sealed record PlayerState(int Health, string Name); public sealed record PlayerState(int Health, string Name);
public sealed record DamageAction(int Amount); public sealed record DamageAction(int Amount);
``` ```
### 定义可绑定属性 ### 定义可绑定属性
```csharp ```csharp
public class PlayerModel : AbstractModel public class PlayerModel : AbstractModel
{ {
// 可读写属性 // 可读写属性
public BindableProperty<string> Name { get; } = new("Player"); public BindableProperty<string> Name { get; } = new("Player");
public BindableProperty<int> Level { get; } = new(1); public BindableProperty<int> Level { get; } = new(1);
public BindableProperty<int> Health { get; } = new(100); public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> MaxHealth { get; } = new(100); public BindableProperty<int> MaxHealth { get; } = new(100);
public BindableProperty<Vector3> Position { get; } = new(Vector3.Zero); public BindableProperty<Vector3> Position { get; } = new(Vector3.Zero);
// 只读属性(外部只能读取和监听) // 只读属性(外部只能读取和监听)
public IReadonlyBindableProperty<int> ReadonlyHealth => Health; public IReadonlyBindableProperty<int> ReadonlyHealth => Health;
protected override void OnInit() protected override void OnInit()
{ {
// 内部监听属性变化 // 内部监听属性变化
Health.Register(hp => Health.Register(hp =>
{ {
if (hp <= 0) if (hp <= 0)
{ {
this.SendEvent(new PlayerDiedEvent()); this.SendEvent(new PlayerDiedEvent());
} }
else if (hp < MaxHealth.Value * 0.3f) else if (hp < MaxHealth.Value * 0.3f)
{ {
this.SendEvent(new LowHealthWarningEvent()); this.SendEvent(new LowHealthWarningEvent());
} }
}); });
// 监听等级变化 // 监听等级变化
Level.Register(newLevel => Level.Register(newLevel =>
{ {
this.SendEvent(new PlayerLevelUpEvent { NewLevel = newLevel }); this.SendEvent(new PlayerLevelUpEvent { NewLevel = newLevel });
}); });
} }
// 业务方法 // 业务方法
public void TakeDamage(int damage) public void TakeDamage(int damage)
{ {
Health.Value = Math.Max(0, Health.Value - damage); Health.Value = Math.Max(0, Health.Value - damage);
} }
public void Heal(int amount) public void Heal(int amount)
{ {
Health.Value = Math.Min(MaxHealth.Value, Health.Value + amount); Health.Value = Math.Min(MaxHealth.Value, Health.Value + amount);
} }
public float GetHealthPercentage() public float GetHealthPercentage()
{ {
return (float)Health.Value / MaxHealth.Value; return (float)Health.Value / MaxHealth.Value;
} }
} }
``` ```
## 在 Controller 中监听 ## 在 Controller 中监听
### UI 数据绑定 ### UI 数据绑定
```csharp ```csharp
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware] [ContextAware]
public partial class PlayerUI : Control, IController public partial class PlayerUI : Control, IController
{ {
[Export] private Label _healthLabel; [Export] private Label _healthLabel;
[Export] private Label _nameLabel; [Export] private Label _nameLabel;
[Export] private ProgressBar _healthBar; [Export] private ProgressBar _healthBar;
private IUnRegisterList _unregisterList = new UnRegisterList(); private IUnRegisterList _unregisterList = new UnRegisterList();
public override void _Ready() public override void _Ready()
{ {
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
// 绑定生命值到UI立即显示当前值 // 绑定生命值到UI立即显示当前值
playerModel.Health playerModel.Health
.RegisterWithInitValue(health => .RegisterWithInitValue(health =>
{ {
_healthLabel.Text = $"HP: {health}/{playerModel.MaxHealth.Value}"; _healthLabel.Text = $"HP: {health}/{playerModel.MaxHealth.Value}";
_healthBar.Value = (float)health / playerModel.MaxHealth.Value * 100; _healthBar.Value = (float)health / playerModel.MaxHealth.Value * 100;
}) })
.AddToUnregisterList(_unregisterList); .AddToUnregisterList(_unregisterList);
// 绑定最大生命值 // 绑定最大生命值
playerModel.MaxHealth playerModel.MaxHealth
.RegisterWithInitValue(maxHealth => .RegisterWithInitValue(maxHealth =>
{ {
_healthBar.MaxValue = maxHealth; _healthBar.MaxValue = maxHealth;
}) })
.AddToUnregisterList(_unregisterList); .AddToUnregisterList(_unregisterList);
// 绑定名称 // 绑定名称
playerModel.Name playerModel.Name
.RegisterWithInitValue(name => .RegisterWithInitValue(name =>
{ {
_nameLabel.Text = name; _nameLabel.Text = name;
}) })
.AddToUnregisterList(_unregisterList); .AddToUnregisterList(_unregisterList);
// 绑定位置(仅用于调试显示) // 绑定位置(仅用于调试显示)
playerModel.Position playerModel.Position
.RegisterWithInitValue(pos => .RegisterWithInitValue(pos =>
{ {
// 仅在调试模式下显示 // 仅在调试模式下显示
#if DEBUG #if DEBUG
Console.WriteLine($"Player position: {pos}"); Console.WriteLine($"Player position: {pos}");
#endif #endif
}) })
.AddToUnregisterList(_unregisterList); .AddToUnregisterList(_unregisterList);
} }
public override void _ExitTree() public override void _ExitTree()
{ {
_unregisterList.UnRegisterAll(); _unregisterList.UnRegisterAll();
} }
} }
``` ```
## 常见使用模式 ## 常见使用模式
### 1. 双向绑定 ### 1. 双向绑定
```c# ```c#
// Model // Model
public class SettingsModel : AbstractModel public class SettingsModel : AbstractModel
{ {
public BindableProperty<float> MasterVolume { get; } = new(1.0f); public BindableProperty<float> MasterVolume { get; } = new(1.0f);
protected override void OnInit() { } protected override void OnInit() { }
} }
// UI Controller // UI Controller
[ContextAware] [ContextAware]
public partial class VolumeSlider : HSlider, IController public partial class VolumeSlider : HSlider, IController
{ {
private BindableProperty<float> _volumeProperty; private BindableProperty<float> _volumeProperty;
public override void _Ready() public override void _Ready()
{ {
_volumeProperty = this.GetModel<SettingsModel>().MasterVolume; _volumeProperty = this.GetModel<SettingsModel>().MasterVolume;
// Model -> UI // Model -> UI
_volumeProperty.RegisterWithInitValue(vol => Value = vol) _volumeProperty.RegisterWithInitValue(vol => Value = vol)
.UnRegisterWhenNodeExitTree(this); .UnRegisterWhenNodeExitTree(this);
// UI -> Model // UI -> Model
ValueChanged += newValue => _volumeProperty.Value = (float)newValue; ValueChanged += newValue => _volumeProperty.Value = (float)newValue;
} }
} }
``` ```
### 2. 计算属性 ### 2. 计算属性
```c# ```c#
public class PlayerModel : AbstractModel public class PlayerModel : AbstractModel
{ {
public BindableProperty<int> Health { get; } = new(100); public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> MaxHealth { get; } = new(100); public BindableProperty<int> MaxHealth { get; } = new(100);
public BindableProperty<float> HealthPercent { get; } = new(1.0f); public BindableProperty<float> HealthPercent { get; } = new(1.0f);
protected override void OnInit() protected override void OnInit()
{ {
// 自动计算百分比 // 自动计算百分比
Action updatePercent = () => Action updatePercent = () =>
{ {
HealthPercent.Value = (float)Health.Value / MaxHealth.Value; HealthPercent.Value = (float)Health.Value / MaxHealth.Value;
}; };
Health.Register(_ => updatePercent()); Health.Register(_ => updatePercent());
MaxHealth.Register(_ => updatePercent()); MaxHealth.Register(_ => updatePercent());
updatePercent(); // 初始计算 updatePercent(); // 初始计算
} }
} }
``` ```
### 3. 属性验证 ### 3. 属性验证
```c# ```c#
public class PlayerModel : AbstractModel public class PlayerModel : AbstractModel
{ {
private BindableProperty<int> _health = new(100); private BindableProperty<int> _health = new(100);
public BindableProperty<int> Health public BindableProperty<int> Health
{ {
get => _health; get => _health;
set set
{ {
// 限制范围 // 限制范围
var clampedValue = Math.Clamp(value.Value, 0, MaxHealth.Value); var clampedValue = Math.Clamp(value.Value, 0, MaxHealth.Value);
_health.Value = clampedValue; _health.Value = clampedValue;
} }
} }
public BindableProperty<int> MaxHealth { get; } = new(100); public BindableProperty<int> MaxHealth { get; } = new(100);
protected override void OnInit() { } protected override void OnInit() { }
} }
``` ```
### 4. 条件监听 ### 4. 条件监听
```c# ```c#
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware] [ContextAware]
public partial class CombatController : Node, IController public partial class CombatController : Node, IController
{ {
public override void _Ready() public override void _Ready()
{ {
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
// 只在生命值低于30%时显示警告 // 只在生命值低于30%时显示警告
playerModel.Health.Register(hp => playerModel.Health.Register(hp =>
{ {
if (hp < playerModel.MaxHealth.Value * 0.3f) if (hp < playerModel.MaxHealth.Value * 0.3f)
{ {
ShowLowHealthWarning(); ShowLowHealthWarning();
} }
else else
{ {
HideLowHealthWarning(); HideLowHealthWarning();
} }
}).UnRegisterWhenNodeExitTree(this); }).UnRegisterWhenNodeExitTree(this);
} }
} }
``` ```
## 性能优化 ## 性能优化
### 1. 避免频繁触发 ### 1. 避免频繁触发
```c# ```c#
// 使用 SetValueWithoutEvent 批量修改 // 使用 SetValueWithoutEvent 批量修改
public void LoadPlayerData(SaveData data) public void LoadPlayerData(SaveData data)
{ {
// 临时关闭事件 // 临时关闭事件
Health.SetValueWithoutEvent(data.Health); Health.SetValueWithoutEvent(data.Health);
Mana.SetValueWithoutEvent(data.Mana); Mana.SetValueWithoutEvent(data.Mana);
Gold.SetValueWithoutEvent(data.Gold); Gold.SetValueWithoutEvent(data.Gold);
// 最后统一触发一次更新事件 // 最后统一触发一次更新事件
this.SendEvent(new PlayerDataLoadedEvent()); this.SendEvent(new PlayerDataLoadedEvent());
} }
``` ```
### 2. 自定义比较器 ### 2. 自定义比较器
```c# ```c#
// 避免浮点数精度问题导致的频繁触发 // 避免浮点数精度问题导致的频繁触发
var position = new BindableProperty<Vector3>() var position = new BindableProperty<Vector3>()
.WithComparer((a, b) => a.DistanceTo(b) < 0.001f); .WithComparer((a, b) => a.DistanceTo(b) < 0.001f);
``` ```
## 实现原理 ## 实现原理
### 值变化检测 ### 值变化检测
```c# ```c#
// 使用 EqualityComparer<T>.Default 进行比较 // 使用 EqualityComparer<T>.Default 进行比较
if (!EqualityComparer<T>.Default.Equals(value, MValue)) if (!EqualityComparer<T>.Default.Equals(value, MValue))
{ {
MValue = value; MValue = value;
_mOnValueChanged?.Invoke(value); _mOnValueChanged?.Invoke(value);
} }
``` ```
### 事件触发机制 ### 事件触发机制
```c# ```c#
// 当值变化时触发所有注册的回调 // 当值变化时触发所有注册的回调
_mOnValueChanged?.Invoke(value); _mOnValueChanged?.Invoke(value);
``` ```
## 最佳实践 ## 最佳实践
1. **在 Model 中定义属性** - BindableProperty 主要用于 Model 层 1. **在 Model 中定义属性** - BindableProperty 主要用于 Model 层
2. **使用只读接口暴露** - 防止外部随意修改 2. **使用只读接口暴露** - 防止外部随意修改
3. **及时注销监听** - 使用 UnRegisterList 或 UnRegisterWhenNodeExitTree 3. **及时注销监听** - 使用 UnRegisterList 或 UnRegisterWhenNodeExitTree
@ -464,14 +464,14 @@ _mOnValueChanged?.Invoke(value);
5. **避免循环依赖** - 属性监听器中修改其他属性要小心 5. **避免循环依赖** - 属性监听器中修改其他属性要小心
6. **使用自定义比较器** - 对于浮点数等需要精度控制的属性 6. **使用自定义比较器** - 对于浮点数等需要精度控制的属性
7. **复杂聚合状态使用 Store** - 当多个字段必须统一演进时,使用 Store 管理聚合状态更清晰 7. **复杂聚合状态使用 Store** - 当多个字段必须统一演进时,使用 Store 管理聚合状态更清晰
## 相关包 ## 相关包
- [`model`](./model.md) - Model 中大量使用 BindableProperty - [`model`](./model.md) - Model 中大量使用 BindableProperty
- [`events`](./events.md) - BindableProperty 基于事件系统实现 - [`events`](./events.md) - BindableProperty 基于事件系统实现
- [`state-management`](./state-management) - 复杂状态树的集中式管理方案 - [`state-management`](./state-management) - 复杂状态树的集中式管理方案
- [`extensions`](./extensions.md) - 提供便捷的注销扩展方法 - [`extensions`](./extensions.md) - 提供便捷的注销扩展方法
--- ---
**许可证**: Apache 2.0 **许可证**: Apache 2.0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -82,6 +82,321 @@ dropItems:
- slime_gel - slime_gel
``` ```
## 推荐接入模板
如果你准备在一个真实游戏项目里首次接入这套配置系统,建议直接采用下面这套目录与启动模板,而不是零散拼装。
### 目录模板
```text
GameProject/
├─ GameProject.csproj
├─ Config/
│ ├─ GameConfigBootstrap.cs
│ └─ GameConfigRuntime.cs
├─ config/
│ ├─ monster/
│ │ ├─ slime.yaml
│ │ └─ goblin.yaml
│ └─ item/
│ └─ potion.yaml
└─ schemas/
├─ monster.schema.json
└─ item.schema.json
```
推荐约定如下:
- `schemas/` 放所有 `*.schema.json`,由 Source Generator 自动拾取
- `config/` 放运行时加载的 YAML 数据,一对象一文件
- `Config/` 放你自己的接入代码,例如启动注册、热重载句柄和对外读取入口
### `csproj` 模板
如果你在仓库内直接用项目引用,最小模板可以写成下面这样:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Game\GFramework.Game.csproj" />
<ProjectReference Include="..\GFramework.SourceGenerators.Abstractions\GFramework.SourceGenerators.Abstractions.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference Include="..\GFramework.SourceGenerators.Common\GFramework.SourceGenerators.Common.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference Include="..\GFramework.SourceGenerators\GFramework.SourceGenerators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
<Import Project="..\GFramework.SourceGenerators\GeWuYou.GFramework.SourceGenerators.targets" />
</Project>
```
这段配置的作用:
- `GFramework.Game` 提供运行时 `YamlConfigLoader``ConfigRegistry` 和只读表实现
- 三个 `ProjectReference(... OutputItemType="Analyzer")` 把生成器接进当前消费者项目
- `GeWuYou.GFramework.SourceGenerators.targets` 自动把 `schemas/**/*.schema.json` 加入 `AdditionalFiles`
如果你使用打包后的 NuGet而不是仓库内项目引用原则保持不变
- 运行时项目需要引用 `GeWuYou.GFramework.Game`
- 生成器项目需要引用 `GeWuYou.GFramework.SourceGenerators`
- schema 目录默认仍然是 `schemas/`
如果你的 schema 不放在默认目录,可以在项目文件里覆盖:
```xml
<PropertyGroup>
<GFrameworkConfigSchemaDirectory>GameSchemas</GFrameworkConfigSchemaDirectory>
</PropertyGroup>
```
### 启动引导模板
推荐把配置系统的初始化收敛到一个单独入口,避免把 `YamlConfigLoader` 注册逻辑散落到多个启动脚本中:
```csharp
using GFramework.Core.Abstractions.Events;
using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config;
using GFramework.Game.Config.Generated;
namespace GameProject.Config;
/// <summary>
/// 负责初始化游戏内容配置运行时入口。
/// </summary>
public sealed class GameConfigBootstrap : IDisposable
{
private readonly ConfigRegistry _registry = new();
private IUnRegister? _hotReload;
/// <summary>
/// 获取当前游戏进程共享的配置注册表。
/// </summary>
public IConfigRegistry Registry => _registry;
/// <summary>
/// 从指定配置根目录加载所有已注册配置表。
/// </summary>
/// <param name="configRootPath">配置根目录。</param>
/// <param name="enableHotReload">是否启用开发期热重载。</param>
public async Task InitializeAsync(string configRootPath, bool enableHotReload = false)
{
var loader = new YamlConfigLoader(configRootPath)
.RegisterMonsterTable()
.RegisterItemTable();
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}");
});
}
}
/// <summary>
/// 停止开发期热重载并释放相关资源。
/// </summary>
public void Dispose()
{
_hotReload?.UnRegister();
}
}
```
这段模板刻意遵循几个约定:
- 优先使用生成器产出的 `Register*Table()`,避免手写表名、路径和 key selector
- 由一个长生命周期对象持有 `ConfigRegistry`
- 热重载句柄和配置生命周期绑在一起,避免监听器泄漏
### 运行时读取模板
推荐不要在业务代码里直接散落字符串表名查询,而是统一依赖生成的强类型入口:
```csharp
using GFramework.Game.Config.Generated;
namespace GameProject.Config;
/// <summary>
/// 封装游戏内容配置读取入口。
/// </summary>
public sealed class GameConfigRuntime
{
private readonly IConfigRegistry _registry;
/// <summary>
/// 使用已初始化的配置注册表创建读取入口。
/// </summary>
/// <param name="registry">配置注册表。</param>
public GameConfigRuntime(IConfigRegistry registry)
{
_registry = registry ?? throw new ArgumentNullException(nameof(registry));
}
/// <summary>
/// 获取指定怪物配置。
/// </summary>
/// <param name="monsterId">怪物主键。</param>
/// <returns>强类型怪物配置。</returns>
public MonsterConfig GetMonster(int monsterId)
{
return _registry.GetMonsterTable().Get(monsterId);
}
/// <summary>
/// 获取怪物配置表。
/// </summary>
/// <returns>生成的强类型表包装。</returns>
public MonsterTable GetMonsterTable()
{
return _registry.GetMonsterTable();
}
}
```
这样做的收益:
- 配置系统对业务层暴露的是强类型表,而不是 `"monster"` 这类 magic string
- 后续如果你要复用配置域、schema 路径或引用元数据,可以继续依赖 `MonsterConfigBindings.Metadata`
`MonsterConfigBindings.References`
- 如果未来把配置初始化接入 `Architecture``Module`,迁移成本也更低
### 生成查询辅助
从当前阶段开始,生成的 `*Table` 包装会为“顶层、非主键、非引用的标量字段”额外产出轻量查询辅助。
如果 `monster.schema.json` 包含顶层标量字段 `name``faction`,则可以直接这样使用:
```csharp
var monsterTable = registry.GetMonsterTable();
var slime = monsterTable.FindByName("Slime");
if (monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster))
{
Console.WriteLine(firstDungeonMonster.Name);
}
```
当前生成规则刻意保持保守:
- 只为顶层标量字段生成 `FindBy*``TryFindFirstBy*`
- 主键字段继续只走 `Get / TryGet`
- 嵌套对象、对象数组、标量数组和 `x-gframework-ref-table` 字段暂不生成查询辅助
- 查询实现基于 `All()` 做线性扫描,不引入运行时索引或缓存
这意味着它的定位是“减少业务层手写过滤样板”,而不是“替代专门索引结构”。
如果你依赖 `TryFindFirstBy*`,应当把它理解为“返回当前表快照遍历顺序下的第一个匹配项”,而不是固定排序语义。
### Architecture 推荐接入模板
如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,推荐把配置系统接到 `OnInitialize()` 阶段,并把 `ConfigRegistry` 注册为 utility
```csharp
using GFramework.Core.Architectures;
using GFramework.Game.Config;
using GFramework.Game.Config.Generated;
public sealed class GameArchitecture : Architecture
{
private readonly string _configRootPath;
public GameArchitecture(string configRootPath)
{
_configRootPath = configRootPath ?? throw new ArgumentNullException(nameof(configRootPath));
}
protected override void OnInitialize()
{
var registry = RegisterUtility(new ConfigRegistry());
var loader = new YamlConfigLoader(_configRootPath)
.RegisterMonsterTable()
.RegisterItemTable();
loader.LoadAsync(registry).GetAwaiter().GetResult();
}
}
```
初始化完成后,业务组件可以继续通过架构上下文读取 utility再走生成的强类型入口
```csharp
var registry = Context.GetUtility<ConfigRegistry>();
var monsterTable = registry.GetMonsterTable();
var slime = monsterTable.Get(1);
```
推荐遵循以下顺序:
- 先注册 `ConfigRegistry`
- 再构造并配置 `YamlConfigLoader`
- 在 `OnInitialize()` 内完成首次 `LoadAsync`
- 初始化完成后只通过注册表和生成表包装访问配置
当前阶段不建议为了配置系统额外引入新的 `IArchitectureModule` 或 service module 抽象;现有 `Architecture + ConfigRegistry + YamlConfigLoader + Register*Table()` 组合已经足够作为官方推荐接入路径。
### 热重载模板
如果你希望把开发期热重载显式收敛为一个可选能力,建议把失败诊断一起写进模板,而不是只打印异常文本:
```csharp
var hotReload = loader.EnableHotReload(
registry,
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}");
});
```
建议只在开发期启用这项能力:
- 生产环境默认更适合静态加载和固定生命周期
- 热重载失败时应优先依赖 `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)
});
```
## 运行时接入 ## 运行时接入
当你希望加载后的配置在运行时以只读表形式暴露时,优先使用生成器产出的注册与访问辅助: 当你希望加载后的配置在运行时以只读表形式暴露时,优先使用生成器产出的注册与访问辅助:
@ -103,13 +418,37 @@ var slime = monsterTable.Get(1);
这组辅助会把以下约定固化到生成代码里: 这组辅助会把以下约定固化到生成代码里:
- 配置域常量,例如 `MonsterConfigBindings.ConfigDomain`
- 表注册名,例如 `monster` - 表注册名,例如 `monster`
- 配置目录相对路径,例如 `monster` - 配置目录相对路径,例如 `monster`
- schema 相对路径,例如 `schemas/monster.schema.json` - schema 相对路径,例如 `schemas/monster.schema.json`
- 主键提取逻辑,例如 `config => config.Id` - 主键提取逻辑,例如 `config => config.Id`
如果你希望把这些约定作为一个统一入口传递或复用,也可以优先读取 `MonsterConfigBindings.Metadata` 下的常量:
```csharp
var domain = MonsterConfigBindings.Metadata.ConfigDomain;
var tableName = MonsterConfigBindings.Metadata.TableName;
var configPath = MonsterConfigBindings.Metadata.ConfigRelativePath;
var schemaPath = MonsterConfigBindings.Metadata.SchemaRelativePath;
```
如果你需要自定义目录、表名或 key selector仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。 如果你需要自定义目录、表名或 key selector仍然可以直接调用 `YamlConfigLoader.RegisterTable(...)` 原始重载。
如果你希望把 schema 路径、比较器以及未来扩展开关集中到一个对象里,推荐改用选项对象入口:
```csharp
var loader = new YamlConfigLoader("config-root")
.RegisterTable(
new YamlConfigTableRegistrationOptions<int, MonsterConfig>(
"monster",
"monster",
static config => config.Id)
{
SchemaRelativePath = "schemas/monster.schema.json"
});
```
## 运行时校验行为 ## 运行时校验行为
绑定 schema 的表在加载时会拒绝以下问题: 绑定 schema 的表在加载时会拒绝以下问题:
@ -148,6 +487,21 @@ var slime = monsterTable.Get(1);
- 引用目标表需要由同一个 `YamlConfigLoader` 注册,或已存在于当前 `IConfigRegistry` - 引用目标表需要由同一个 `YamlConfigLoader` 注册,或已存在于当前 `IConfigRegistry`
- 热重载中若目标表变更导致依赖表引用失效,会整体回滚受影响表,避免注册表进入不一致状态 - 热重载中若目标表变更导致依赖表引用失效,会整体回滚受影响表,避免注册表进入不一致状态
如果你希望在消费者代码里复用这些跨表约定,而不是继续手写字段路径或目标表名,生成的 `*ConfigBindings` 还会暴露引用元数据:
```csharp
var allReferences = MonsterConfigBindings.References.All;
if (MonsterConfigBindings.References.TryGetByDisplayPath("dropItems", out var reference))
{
Console.WriteLine(reference.ReferencedTableName);
Console.WriteLine(reference.ValueSchemaType);
Console.WriteLine(reference.IsCollection);
}
```
当 schema 中存在具体引用字段时,还可以直接通过生成成员访问,例如 `MonsterConfigBindings.References.DropItems`
当前还支持以下“轻量元数据”: 当前还支持以下“轻量元数据”:
- `title`:供 VS Code 插件表单和批量编辑入口显示更友好的字段标题 - `title`:供 VS Code 插件表单和批量编辑入口显示更友好的字段标题
@ -195,14 +549,11 @@ catch (ConfigLoadException exception)
```csharp ```csharp
using GFramework.Game.Abstractions.Config; using GFramework.Game.Abstractions.Config;
using GFramework.Game.Config; using GFramework.Game.Config;
using GFramework.Game.Config.Generated;
var registry = new ConfigRegistry(); var registry = new ConfigRegistry();
var loader = new YamlConfigLoader("config-root") var loader = new YamlConfigLoader("config-root")
.RegisterTable<int, MonsterConfig>( .RegisterMonsterTable();
"monster",
"monster",
"schemas/monster.schema.json",
static config => config.Id);
await loader.LoadAsync(registry); await loader.LoadAsync(registry);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -196,4 +196,4 @@ public interface ISettingsMigration
- 设置迁移是内建能力 - 设置迁移是内建能力
- 设置持久化是内建能力 - 设置持久化是内建能力
- 设置如何应用到具体引擎由 applicator 决定 - 设置如何应用到具体引擎由 applicator 决定
- 存档系统的迁移能力不等同于设置系统;`ISaveRepository<T>` 当前仍需要业务层自己实现迁移策略 - 存档系统也支持内建版本迁移,但入口位于 `ISaveRepository<T>.RegisterMigration(...)`,语义是槽位存档升级而不是设置节初始化

File diff suppressed because it is too large Load Diff

View File

@ -1,323 +1,323 @@
# 快速开始 # 快速开始
本指南将帮助您快速构建第一个基于 GFramework 的应用程序。 本指南将帮助您快速构建第一个基于 GFramework 的应用程序。
## 1. 创建项目架构 ## 1. 创建项目架构
首先定义您的应用架构: 首先定义您的应用架构:
```csharp ```csharp
using GFramework.Core.Architecture; using GFramework.Core.Architecture;
public class GameArchitecture : Architecture public class GameArchitecture : Architecture
{ {
protected override void Init() protected override void Init()
{ {
// 注册模型 - 存储应用状态 // 注册模型 - 存储应用状态
RegisterModel(new PlayerModel()); RegisterModel(new PlayerModel());
RegisterModel(new GameStateModel()); RegisterModel(new GameStateModel());
// 注册系统 - 处理业务逻辑 // 注册系统 - 处理业务逻辑
RegisterSystem(new PlayerSystem()); RegisterSystem(new PlayerSystem());
RegisterSystem(new GameLogicSystem()); RegisterSystem(new GameLogicSystem());
// 注册工具类 - 提供辅助功能 // 注册工具类 - 提供辅助功能
RegisterUtility(new StorageUtility()); RegisterUtility(new StorageUtility());
} }
} }
``` ```
## 2. 定义数据模型 ## 2. 定义数据模型
创建您的数据模型: 创建您的数据模型:
```csharp ```csharp
public class PlayerModel : AbstractModel public class PlayerModel : AbstractModel
{ {
// 使用可绑定属性实现响应式数据 // 使用可绑定属性实现响应式数据
public BindableProperty<string> Name { get; } = new("Player"); public BindableProperty<string> Name { get; } = new("Player");
public BindableProperty<int> Health { get; } = new(100); public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> Score { get; } = new(0); public BindableProperty<int> Score { get; } = new(0);
protected override void OnInit() protected override void OnInit()
{ {
// 监听健康值变化 // 监听健康值变化
Health.Register(OnHealthChanged); Health.Register(OnHealthChanged);
} }
private void OnHealthChanged(int newHealth) private void OnHealthChanged(int newHealth)
{ {
if (newHealth <= 0) if (newHealth <= 0)
{ {
this.SendEvent(new PlayerDiedEvent()); this.SendEvent(new PlayerDiedEvent());
} }
} }
} }
public class GameStateModel : AbstractModel public class GameStateModel : AbstractModel
{ {
public BindableProperty<bool> IsGameRunning { get; } = new(false); public BindableProperty<bool> IsGameRunning { get; } = new(false);
public BindableProperty<int> CurrentLevel { get; } = new(1); public BindableProperty<int> CurrentLevel { get; } = new(1);
} }
``` ```
## 3. 实现业务逻辑 ## 3. 实现业务逻辑
创建处理业务逻辑的系统: 创建处理业务逻辑的系统:
```csharp ```csharp
public class PlayerSystem : AbstractSystem public class PlayerSystem : AbstractSystem
{ {
protected override void OnInit() protected override void OnInit()
{ {
// 监听玩家输入事件 // 监听玩家输入事件
this.RegisterEvent<PlayerMoveEvent>(OnPlayerMove); this.RegisterEvent<PlayerMoveEvent>(OnPlayerMove);
this.RegisterEvent<PlayerAttackEvent>(OnPlayerAttack); this.RegisterEvent<PlayerAttackEvent>(OnPlayerAttack);
} }
private void OnPlayerMove(PlayerMoveEvent e) private void OnPlayerMove(PlayerMoveEvent e)
{ {
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
// 处理移动逻辑 // 处理移动逻辑
Console.WriteLine($"Player moved to {e.Direction}"); Console.WriteLine($"Player moved to {e.Direction}");
} }
private void OnPlayerAttack(PlayerAttackEvent e) private void OnPlayerAttack(PlayerAttackEvent e)
{ {
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
// 处理攻击逻辑 // 处理攻击逻辑
playerModel.Score.Value += 10; playerModel.Score.Value += 10;
this.SendEvent(new EnemyDamagedEvent { Damage = 25 }); this.SendEvent(new EnemyDamagedEvent { Damage = 25 });
} }
} }
public class GameLogicSystem : AbstractSystem public class GameLogicSystem : AbstractSystem
{ {
protected override void OnInit() protected override void OnInit()
{ {
this.RegisterEvent<EnemyDamagedEvent>(OnEnemyDamaged); this.RegisterEvent<EnemyDamagedEvent>(OnEnemyDamaged);
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied); this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
} }
private void OnEnemyDamaged(EnemyDamagedEvent e) private void OnEnemyDamaged(EnemyDamagedEvent e)
{ {
Console.WriteLine($"Enemy took {e.Damage} damage"); Console.WriteLine($"Enemy took {e.Damage} damage");
// 检查是否需要升级关卡 // 检查是否需要升级关卡
CheckLevelProgress(); CheckLevelProgress();
} }
private void OnPlayerDied(PlayerDiedEvent e) private void OnPlayerDied(PlayerDiedEvent e)
{ {
var gameState = this.GetModel<GameStateModel>(); var gameState = this.GetModel<GameStateModel>();
gameState.IsGameRunning.Value = false; gameState.IsGameRunning.Value = false;
Console.WriteLine("Game Over!"); Console.WriteLine("Game Over!");
} }
private void CheckLevelProgress() private void CheckLevelProgress()
{ {
// 实现关卡进度检查逻辑 // 实现关卡进度检查逻辑
} }
} }
``` ```
## 4. 定义事件 ## 4. 定义事件
创建应用中使用的事件: 创建应用中使用的事件:
```csharp ```csharp
public class PlayerMoveEvent : IEvent public class PlayerMoveEvent : IEvent
{ {
public Vector2 Direction { get; set; } public Vector2 Direction { get; set; }
} }
public class PlayerAttackEvent : IEvent public class PlayerAttackEvent : IEvent
{ {
public Vector2 TargetPosition { get; set; } public Vector2 TargetPosition { get; set; }
} }
public class PlayerDiedEvent : IEvent public class PlayerDiedEvent : IEvent
{ {
// 玩家死亡事件 // 玩家死亡事件
} }
public class EnemyDamagedEvent : IEvent public class EnemyDamagedEvent : IEvent
{ {
public int Damage { get; set; } public int Damage { get; set; }
} }
``` ```
## 5. 创建控制器 ## 5. 创建控制器
实现控制器来连接 UI 和业务逻辑: 实现控制器来连接 UI 和业务逻辑:
```csharp ```csharp
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware] [ContextAware]
public partial class GameController : IController public partial class GameController : IController
{ {
private PlayerModel _playerModel; private PlayerModel _playerModel;
private GameStateModel _gameStateModel; private GameStateModel _gameStateModel;
public void Initialize() public void Initialize()
{ {
_playerModel = this.GetModel<PlayerModel>(); _playerModel = this.GetModel<PlayerModel>();
_gameStateModel = this.GetModel<GameStateModel>(); _gameStateModel = this.GetModel<GameStateModel>();
// 初始化事件监听 // 初始化事件监听
InitializeEventListeners(); InitializeEventListeners();
} }
private void InitializeEventListeners() private void InitializeEventListeners()
{ {
// 监听模型变化并更新 UI // 监听模型变化并更新 UI
_playerModel.Health.RegisterWithInitValue(OnHealthChanged); _playerModel.Health.RegisterWithInitValue(OnHealthChanged);
_playerModel.Score.RegisterWithInitValue(OnScoreChanged); _playerModel.Score.RegisterWithInitValue(OnScoreChanged);
_gameStateModel.IsGameRunning.Register(OnGameStateChanged); _gameStateModel.IsGameRunning.Register(OnGameStateChanged);
} }
public void StartGame() public void StartGame()
{ {
_gameStateModel.IsGameRunning.Value = true; _gameStateModel.IsGameRunning.Value = true;
this.SendEvent(new GameStartEvent()); this.SendEvent(new GameStartEvent());
Console.WriteLine("Game started!"); Console.WriteLine("Game started!");
} }
public void MovePlayer(Vector2 direction) public void MovePlayer(Vector2 direction)
{ {
this.SendCommand(new MovePlayerCommand { Direction = direction }); this.SendCommand(new MovePlayerCommand { Direction = direction });
} }
public void PlayerAttack(Vector2 target) public void PlayerAttack(Vector2 target)
{ {
this.SendCommand(new AttackCommand { TargetPosition = target }); this.SendCommand(new AttackCommand { TargetPosition = target });
} }
// UI 更新回调 // UI 更新回调
private void OnHealthChanged(int health) private void OnHealthChanged(int health)
{ {
UpdateHealthDisplay(health); UpdateHealthDisplay(health);
} }
private void OnScoreChanged(int score) private void OnScoreChanged(int score)
{ {
UpdateScoreDisplay(score); UpdateScoreDisplay(score);
} }
private void OnGameStateChanged(bool isRunning) private void OnGameStateChanged(bool isRunning)
{ {
UpdateGameStatusDisplay(isRunning); UpdateGameStatusDisplay(isRunning);
} }
private void UpdateHealthDisplay(int health) private void UpdateHealthDisplay(int health)
{ {
// 更新血条 UI // 更新血条 UI
Console.WriteLine($"Health: {health}"); Console.WriteLine($"Health: {health}");
} }
private void UpdateScoreDisplay(int score) private void UpdateScoreDisplay(int score)
{ {
// 更新分数显示 // 更新分数显示
Console.WriteLine($"Score: {score}"); Console.WriteLine($"Score: {score}");
} }
private void UpdateGameStatusDisplay(bool isRunning) private void UpdateGameStatusDisplay(bool isRunning)
{ {
// 更新游戏状态显示 // 更新游戏状态显示
Console.WriteLine($"Game running: {isRunning}"); Console.WriteLine($"Game running: {isRunning}");
} }
} }
``` ```
## 6. 定义命令 ## 6. 定义命令
创建命令来封装用户操作: 创建命令来封装用户操作:
```csharp ```csharp
public class MovePlayerCommand : AbstractCommand public class MovePlayerCommand : AbstractCommand
{ {
public Vector2 Direction { get; set; } public Vector2 Direction { get; set; }
protected override void OnDo() protected override void OnDo()
{ {
// 发送移动事件 // 发送移动事件
this.SendEvent(new PlayerMoveEvent { Direction = Direction }); this.SendEvent(new PlayerMoveEvent { Direction = Direction });
} }
} }
public class AttackCommand : AbstractCommand public class AttackCommand : AbstractCommand
{ {
public Vector2 TargetPosition { get; set; } public Vector2 TargetPosition { get; set; }
protected override void OnDo() protected override void OnDo()
{ {
// 发送攻击事件 // 发送攻击事件
this.SendEvent(new PlayerAttackEvent { TargetPosition = TargetPosition }); this.SendEvent(new PlayerAttackEvent { TargetPosition = TargetPosition });
} }
} }
``` ```
## 7. 运行应用 ## 7. 运行应用
现在让我们运行这个简单的应用: 现在让我们运行这个简单的应用:
```csharp ```csharp
class Program class Program
{ {
static void Main(string[] args) static void Main(string[] args)
{ {
// 创建并初始化架构 // 创建并初始化架构
var architecture = new GameArchitecture(); var architecture = new GameArchitecture();
architecture.Initialize(); architecture.Initialize();
// 创建控制器 // 创建控制器
var gameController = new GameController(); var gameController = new GameController();
gameController.Initialize(); gameController.Initialize();
// 开始游戏 // 开始游戏
gameController.StartGame(); gameController.StartGame();
// 模拟玩家操作 // 模拟玩家操作
gameController.MovePlayer(new Vector2(1, 0)); gameController.MovePlayer(new Vector2(1, 0));
gameController.PlayerAttack(new Vector2(5, 5)); gameController.PlayerAttack(new Vector2(5, 5));
// 模拟玩家受伤 // 模拟玩家受伤
var playerModel = architecture.GetModel<PlayerModel>(); var playerModel = architecture.GetModel<PlayerModel>();
playerModel.Health.Value = 50; playerModel.Health.Value = 50;
// 模拟玩家死亡 // 模拟玩家死亡
playerModel.Health.Value = 0; playerModel.Health.Value = 0;
Console.WriteLine("Press any key to exit..."); Console.WriteLine("Press any key to exit...");
Console.ReadKey(); Console.ReadKey();
} }
} }
``` ```
## 8. 运行结果 ## 8. 运行结果
执行程序后,您应该看到类似以下输出: 执行程序后,您应该看到类似以下输出:
``` ```
Game started! Game started!
Game running: True Game running: True
Player moved to (1, 0) Player moved to (1, 0)
Player took 25 damage Player took 25 damage
Score: 10 Score: 10
Health: 50 Health: 50
Health: 0 Health: 0
Player died Player died
Game Over! Game Over!
Game running: False Game running: False
Press any key to exit... Press any key to exit...
``` ```
## 下一步 ## 下一步
这个简单的示例展示了 GFramework 的核心概念: 这个简单的示例展示了 GFramework 的核心概念:
1. **架构模式** - 清晰的分层结构 1. **架构模式** - 清晰的分层结构
2. **响应式数据** - BindableProperty 自动更新 2. **响应式数据** - BindableProperty 自动更新
3. **事件驱动** - 松耦合的组件通信 3. **事件驱动** - 松耦合的组件通信
4. **命令模式** - 封装用户操作 4. **命令模式** - 封装用户操作

File diff suppressed because it is too large Load Diff

View File

@ -2,41 +2,30 @@
## 概述 ## 概述
GFramework 的协程系统由两层组成 `GFramework.Godot.Coroutine` 在 Core 协程内核之上提供 Godot 宿主集成,负责把 Godot 的不同更新循环映射为真实的协程阶段语义
- `GFramework.Core.Coroutine` 提供通用调度器、`IYieldInstruction` 和一组等待指令。 - `Segment.Process`
- `GFramework.Godot.Coroutine` 提供 Godot 环境下的运行入口、分段调度以及节点生命周期辅助方法。 - `Segment.ProcessIgnorePause`
- `Segment.PhysicsProcess`
- `Segment.DeferredProcess`
Godot 集成层的核心入口包括: 它同时补充了以下宿主能力
- `RunCoroutine(...)` - 节点归属协程运行入口
- `Timing.RunGameCoroutine(...)` - 节点退树自动终止
- `Timing.RunUiCoroutine(...)` - Godot 真实时间源
- `Timing.CallDelayed(...)` - 句柄控制与快照查询
- `CancelWith(...)`
协程本身使用 `IEnumerator<IYieldInstruction>` ## 启动协程
## 主要能力 ### 直接运行枚举器
- 在 Godot 中按不同更新阶段运行协程
- 等待时间、帧、条件、Task 和事件总线事件
- 显式将协程与一个或多个 `Node` 的生命周期绑定
- 通过 `CoroutineHandle` 暂停、恢复、终止协程
- 将命令、查询、发布操作直接包装为协程运行
## 基本用法
### 启动协程
```csharp ```csharp
using System.Collections.Generic;
using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions; using GFramework.Core.Coroutine.Instructions;
using GFramework.Godot.Coroutine; using GFramework.Godot.Coroutine;
using Godot;
public partial class MyNode : Node public partial class DemoNode : Node
{ {
public override void _Ready() public override void _Ready()
{ {
@ -45,242 +34,131 @@ public partial class MyNode : Node
private IEnumerator<IYieldInstruction> Demo() private IEnumerator<IYieldInstruction> Demo()
{ {
GD.Print("开始执行"); GD.Print("start");
yield return new Delay(1.0);
yield return new Delay(2.0);
GD.Print("2 秒后继续执行");
yield return new WaitForEndOfFrame(); yield return new WaitForEndOfFrame();
GD.Print("当前帧结束后继续执行"); GD.Print("done");
} }
} }
``` ```
`RunCoroutine()` 默认在 `Segment.Process` 上运行,也就是普通帧更新阶段 默认情况下,`RunCoroutine()` 会在 `Segment.Process` 上运行
除了枚举器扩展方法,也可以直接使用 `Timing` 的静态入口: ### 以 Node 作为生命周期所有者运行
更推荐的方式是以节点为入口运行协程:
```csharp ```csharp
Timing.RunCoroutine(Demo()); public override void _Ready()
Timing.RunGameCoroutine(GameLoop());
Timing.RunUiCoroutine(MenuAnimation());
```
### 显式绑定节点生命周期
可以使用 `CancelWith(...)` 将协程与一个或多个节点的生命周期关联。
```csharp
using System.Collections.Generic;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions;
using GFramework.Godot.Coroutine;
using Godot;
public partial class MyNode : Node
{ {
public override void _Ready() this.RunCoroutine(LongRunningTask(), Segment.Process, tag: "ui-blink");
{
LongRunningTask()
.CancelWith(this)
.RunCoroutine();
}
private IEnumerator<IYieldInstruction> LongRunningTask()
{
while (true)
{
GD.Print("tick");
yield return new Delay(1.0);
}
}
} }
``` ```
`CancelWith` 目前有三种重载: 这会自动把协程登记为该节点归属协程,并在节点退出场景树时终止它。
- `CancelWith(Node node)` 你仍然可以继续使用 `CancelWith(...)` 包装已有枚举器;它适合把一个协程显式绑定到多个节点生命周期。
- `CancelWith(Node node1, Node node2)`
- `CancelWith(params Node[] nodes)`
`CancelWith(...)` 内部通过 `Timing.IsNodeAlive(...)` 判断节点是否仍然有效。只要任一被监视的节点出现以下任一情况,包装后的协程就会停止继续枚举: ## Segment 与阶段语义
- 节点引用为 `null` Godot 层会把不同 segment 映射为不同的 `CoroutineExecutionStage`
- Godot 实例已经失效或已被释放
- 节点已进入 `queue_free` / `IsQueuedForDeletion()`
- 节点已退出场景树,`IsInsideTree()` 返回 `false`
这意味着协程不只会在节点真正释放时停止;节点一旦退出场景树,下一次推进时也会停止。 - `Segment.Process`
- 对应默认更新阶段
## Segment 分段 - 场景树暂停时不会推进
- `Segment.ProcessIgnorePause`
Godot 层通过 `Segment` 决定协程挂在哪个调度器上: - 同样对应默认更新阶段
- 场景树暂停时仍会推进
```csharp - `Segment.PhysicsProcess`
public enum Segment - 对应固定更新阶段
{ - `WaitForFixedUpdate` 会在这里真实完成
Process, - `Segment.DeferredProcess`
ProcessIgnorePause, - 对应帧结束阶段
PhysicsProcess, - `WaitForEndOfFrame` 会在这里真实完成
DeferredProcess
}
```
- `Process`:普通 `_Process` 段,场景树暂停时不会推进。
- `ProcessIgnorePause`:同样使用 process delta但即使场景树暂停也会推进。
- `PhysicsProcess`:在 `_PhysicsProcess` 段推进。
- `DeferredProcess`:通过 `CallDeferred` 在当前帧之后推进,场景树暂停时不会推进。
示例: 示例:
```csharp ```csharp
UiAnimation().RunCoroutine(Segment.ProcessIgnorePause); this.RunCoroutine(PhysicsRoutine(), Segment.PhysicsProcess);
PhysicsRoutine().RunCoroutine(Segment.PhysicsProcess); this.RunCoroutine(UiAnimation(), Segment.ProcessIgnorePause);
``` ```
如果你更偏向语义化入口,也可以直接使用: ## 时间等待语义
Godot 集成层为每个调度器同时提供了两套时间源:
- 缩放时间
- 来自 `_Process` / `_PhysicsProcess` 的帧增量
- 真实时间
- 来自 Godot 单调时钟,不受时间缩放和暂停影响
因此:
- `Delay` / `WaitForSecondsScaled` 使用宿主帧增量
- `WaitForSecondsRealtime` 使用真实时间
这意味着 UI 或暂停菜单中的协程可以安全使用 `WaitForSecondsRealtime` 保持真实计时。
## 生命周期管理
### 自动归属
```csharp ```csharp
Timing.RunGameCoroutine(GameLoop()); var handle = this.RunCoroutine(LoadAvatar(), tag: "avatar");
Timing.RunUiCoroutine(MenuAnimation());
``` ```
### 延迟调用 ### 手动绑定多个节点
`Timing` 还提供了两个延迟调用快捷方法: ```csharp
LongRunningTask()
.CancelWith(this, panelNode)
.RunCoroutine();
```
### 主动清理
```csharp
Timing.KillCoroutine(handle);
Timing.KillCoroutines(this);
Timing.KillCoroutines("avatar");
Timing.KillAllCoroutines();
```
## 调试与查询
```csharp
if (Timing.TryGetCoroutineSnapshot(handle, out var snapshot))
{
GD.Print(snapshot.ExecutionStage);
GD.Print(snapshot.WaitingInstructionType);
}
var ownedCount = Timing.GetOwnedCoroutineCount(this);
```
实例级计数器:
- `Timing.Instance.ProcessCoroutines`
- `Timing.Instance.ProcessIgnorePauseCoroutines`
- `Timing.Instance.PhysicsCoroutines`
- `Timing.Instance.DeferredCoroutines`
## 延迟调用
```csharp ```csharp
Timing.CallDelayed(1.0, () => GD.Print("1 秒后执行")); Timing.CallDelayed(1.0, () => GD.Print("1 秒后执行"));
Timing.CallDelayed(1.0, () => GD.Print("节点仍然有效时执行"), this); Timing.CallDelayed(1.0, () => GD.Print("节点仍然有效时执行"), this);
``` ```
第二个重载会在执行前检查传入节点是否仍然存活。 第二个重载内部使用节点归属语义,因此节点退树后不会再触发动作。
## 常用等待指令
以下类型可直接用于 `yield return`
### 时间与帧
```csharp
yield return new Delay(1.0);
yield return new WaitForSecondsRealtime(1.0);
yield return new WaitOneFrame();
yield return new WaitForNextFrame();
yield return new WaitForFrames(5);
yield return new WaitForEndOfFrame();
```
说明:
- `Delay` 是最直接的秒级等待。
- `WaitForSecondsRealtime` 常用于需要独立计时语义的协程场景。
- `WaitOneFrame``WaitForNextFrame``WaitForEndOfFrame` 用于帧级调度控制。
### 条件等待
```csharp
yield return new WaitUntil(() => health > 0);
yield return new WaitWhile(() => isLoading);
```
### Task 等待
```csharp
using System.Threading.Tasks;
using GFramework.Core.Coroutine.Extensions;
Task loadTask = LoadSomethingAsync();
yield return loadTask.AsCoroutineInstruction();
```
也可以先把 `Task` 转成协程枚举器,再直接运行:
```csharp
LoadSomethingAsync()
.ToCoroutineEnumerator()
.RunCoroutine();
```
- 已经在一个协程内部时,优先使用 `yield return task.AsCoroutineInstruction()`,这样可以直接把 `Task` 嵌入当前协程流程。
- 如果要把一个现成的 `Task` 当作独立协程入口交给 Godot 协程系统运行,再使用
`task.ToCoroutineEnumerator().RunCoroutine()`
### 等待事件总线事件
可以通过事件总线等待业务事件:
```csharp
using System.Collections.Generic;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Coroutine.Instructions;
private IEnumerator<IYieldInstruction> WaitForGameEvent(IEventBus eventBus)
{
using var wait = new WaitForEvent<PlayerSpawnedEvent>(eventBus);
yield return wait;
var evt = wait.EventData;
}
```
如需为事件等待附加超时控制,可结合 `WaitForEventWithTimeout<TEvent>`
## 协程控制
协程启动后会返回 `CoroutineHandle`,可用于控制运行状态:
```csharp
var handle = Demo().RunCoroutine(tag: "demo");
Timing.PauseCoroutine(handle);
Timing.ResumeCoroutine(handle);
Timing.KillCoroutine(handle);
Timing.KillCoroutines("demo");
Timing.KillAllCoroutines();
```
如果希望在场景初始化阶段主动确保调度器存在,也可以调用:
```csharp
Timing.Prewarm();
```
## 与 IContextAware 集成 ## 与 IContextAware 集成
`GFramework.Godot.Coroutine` 还提供了一组扩展方法,用于把命令、查询和通知直接包装成协程: Godot 层还提供以下扩展方法,用于把命令、查询和通知直接包装成协程并交给 Timing 调度:
- `RunCommandCoroutine(...)` - `RunCommandCoroutine(...)`
- `RunCommandCoroutine<TResponse>(...)` - `RunCommandCoroutine<TResponse>(...)`
- `RunQueryCoroutine<TResponse>(...)` - `RunQueryCoroutine<TResponse>(...)`
- `RunPublishCoroutine(...)` - `RunPublishCoroutine(...)`
这些方法会把异步操作转换为协程,并交给 `RunCoroutine(...)` 调度执行。 这些 API 仍然可以与 `Segment`、节点归属和标签控制一起使用。
例如:
```csharp
public void StartCoroutines(IContextAware contextAware)
{
contextAware.RunCommandCoroutine(
new EnterBattleCommand(),
Segment.Process,
tag: "battle");
contextAware.RunQueryCoroutine(
new LoadPlayerQuery(),
Segment.ProcessIgnorePause,
tag: "ui");
}
```
这些扩展适合在 Godot 节点或控制器中直接启动和跟踪业务协程。
## 相关文档
- [Godot 概述](./index.md)
- [Godot 扩展方法](./extensions.md)
- [信号扩展](./signal.md)
- [事件系统](../core/events.md)

View File

@ -1,403 +1,403 @@
# ContextAware 生成器 # ContextAware 生成器
> 自动实现 IContextAware 接口,提供架构上下文访问能力 > 自动实现 IContextAware 接口,提供架构上下文访问能力
## 概述 ## 概述
ContextAware 生成器为标记了 `[ContextAware]` 属性的类自动生成 `IContextAware` 接口实现,使类能够便捷地访问架构上下文( ContextAware 生成器为标记了 `[ContextAware]` 属性的类自动生成 `IContextAware` 接口实现,使类能够便捷地访问架构上下文(
`IArchitectureContext`)。这是 GFramework 中最常用的源码生成器之一,几乎所有需要与架构交互的组件都会使用它。 `IArchitectureContext`)。这是 GFramework 中最常用的源码生成器之一,几乎所有需要与架构交互的组件都会使用它。
### 核心功能 ### 核心功能
- **自动接口实现**:无需手动实现 `IContextAware` 接口的 `SetContext()``GetContext()` 方法 - **自动接口实现**:无需手动实现 `IContextAware` 接口的 `SetContext()``GetContext()` 方法
- **懒加载上下文**`Context` 属性在首次访问时自动初始化 - **懒加载上下文**`Context` 属性在首次访问时自动初始化
- **默认提供者**:使用 `GameContextProvider` 作为默认上下文提供者 - **默认提供者**:使用 `GameContextProvider` 作为默认上下文提供者
- **测试友好**:支持通过 `SetContextProvider()` 配置自定义上下文提供者 - **测试友好**:支持通过 `SetContextProvider()` 配置自定义上下文提供者
## 基础使用 ## 基础使用
### 标记类 ### 标记类
使用 `[ContextAware]` 属性标记需要访问架构上下文的类: 使用 `[ContextAware]` 属性标记需要访问架构上下文的类:
```csharp ```csharp
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
[ContextAware] [ContextAware]
public partial class PlayerController : IController public partial class PlayerController : IController
{ {
public void Initialize() public void Initialize()
{ {
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口) // 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
var combatSystem = this.GetSystem<CombatSystem>(); var combatSystem = this.GetSystem<CombatSystem>();
this.SendEvent(new PlayerInitializedEvent()); this.SendEvent(new PlayerInitializedEvent());
} }
public void Attack(Enemy target) public void Attack(Enemy target)
{ {
var damage = this.GetUtility<DamageCalculator>().Calculate(this, target); var damage = this.GetUtility<DamageCalculator>().Calculate(this, target);
this.SendCommand(new DealDamageCommand(target, damage)); this.SendCommand(new DealDamageCommand(target, damage));
} }
} }
``` ```
### 必要条件 ### 必要条件
标记的类必须满足以下条件: 标记的类必须满足以下条件:
1. **必须是 `partial` 类**:生成器需要生成部分类代码 1. **必须是 `partial` 类**:生成器需要生成部分类代码
2. **必须是 `class` 类型**:不能是 `struct``interface` 2. **必须是 `class` 类型**:不能是 `struct``interface`
```csharp ```csharp
// ✅ 正确 // ✅ 正确
[ContextAware] [ContextAware]
public partial class MyController { } public partial class MyController { }
// ❌ 错误:缺少 partial 关键字 // ❌ 错误:缺少 partial 关键字
[ContextAware] [ContextAware]
public class MyController { } public class MyController { }
// ❌ 错误:不能用于 struct // ❌ 错误:不能用于 struct
[ContextAware] [ContextAware]
public partial struct MyStruct { } public partial struct MyStruct { }
``` ```
## 生成的代码 ## 生成的代码
编译器会为标记的类自动生成以下代码: 编译器会为标记的类自动生成以下代码:
```csharp ```csharp
// <auto-generated/> // <auto-generated/>
#nullable enable #nullable enable
namespace YourNamespace; namespace YourNamespace;
partial class PlayerController : global::GFramework.Core.Abstractions.Rule.IContextAware partial class PlayerController : global::GFramework.Core.Abstractions.Rule.IContextAware
{ {
private global::GFramework.Core.Abstractions.Architecture.IArchitectureContext? _context; private global::GFramework.Core.Abstractions.Architecture.IArchitectureContext? _context;
private static global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider? _contextProvider; private static global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider? _contextProvider;
/// <summary> /// <summary>
/// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider /// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider
/// </summary> /// </summary>
protected global::GFramework.Core.Abstractions.Architecture.IArchitectureContext Context protected global::GFramework.Core.Abstractions.Architecture.IArchitectureContext Context
{ {
get get
{ {
if (_context == null) if (_context == null)
{ {
_contextProvider ??= new global::GFramework.Core.Architecture.GameContextProvider(); _contextProvider ??= new global::GFramework.Core.Architecture.GameContextProvider();
_context = _contextProvider.GetContext(); _context = _contextProvider.GetContext();
} }
return _context; return _context;
} }
} }
/// <summary> /// <summary>
/// 配置上下文提供者(用于测试或多架构场景) /// 配置上下文提供者(用于测试或多架构场景)
/// </summary> /// </summary>
/// <param name="provider">上下文提供者实例</param> /// <param name="provider">上下文提供者实例</param>
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider provider) public static void SetContextProvider(global::GFramework.Core.Abstractions.Architecture.IArchitectureContextProvider provider)
{ {
_contextProvider = provider; _contextProvider = provider;
} }
/// <summary> /// <summary>
/// 重置上下文提供者为默认值(用于测试清理) /// 重置上下文提供者为默认值(用于测试清理)
/// </summary> /// </summary>
public static void ResetContextProvider() public static void ResetContextProvider()
{ {
_contextProvider = null; _contextProvider = null;
} }
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architecture.IArchitectureContext context) void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architecture.IArchitectureContext context)
{ {
_context = context; _context = context;
} }
global::GFramework.Core.Abstractions.Architecture.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext() global::GFramework.Core.Abstractions.Architecture.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
{ {
return Context; return Context;
} }
} }
``` ```
### 代码解析 ### 代码解析
生成的代码包含以下关键部分: 生成的代码包含以下关键部分:
1. **私有字段** 1. **私有字段**
- `_context`:缓存的上下文实例 - `_context`:缓存的上下文实例
- `_contextProvider`:静态上下文提供者(所有实例共享) - `_contextProvider`:静态上下文提供者(所有实例共享)
2. **Context 属性** 2. **Context 属性**
- `protected` 访问级别,子类可访问 - `protected` 访问级别,子类可访问
- 懒加载:首次访问时自动初始化 - 懒加载:首次访问时自动初始化
- 使用 `GameContextProvider` 作为默认提供者 - 使用 `GameContextProvider` 作为默认提供者
3. **配置方法** 3. **配置方法**
- `SetContextProvider()`:设置自定义上下文提供者 - `SetContextProvider()`:设置自定义上下文提供者
- `ResetContextProvider()`:重置为默认提供者 - `ResetContextProvider()`:重置为默认提供者
4. **显式接口实现** 4. **显式接口实现**
- `IContextAware.SetContext()`:允许外部设置上下文 - `IContextAware.SetContext()`:允许外部设置上下文
- `IContextAware.GetContext()`:返回当前上下文 - `IContextAware.GetContext()`:返回当前上下文
## 配置上下文提供者 ## 配置上下文提供者
### 测试场景 ### 测试场景
在单元测试中,通常需要使用自定义的上下文提供者: 在单元测试中,通常需要使用自定义的上下文提供者:
```csharp ```csharp
[Test] [Test]
public async Task TestPlayerController() public async Task TestPlayerController()
{ {
// 创建测试架构 // 创建测试架构
var testArchitecture = new TestArchitecture(); var testArchitecture = new TestArchitecture();
await testArchitecture.InitAsync(); await testArchitecture.InitAsync();
// 配置自定义上下文提供者 // 配置自定义上下文提供者
PlayerController.SetContextProvider(new TestContextProvider(testArchitecture)); PlayerController.SetContextProvider(new TestContextProvider(testArchitecture));
try try
{ {
// 测试代码 // 测试代码
var controller = new PlayerController(); var controller = new PlayerController();
controller.Initialize(); controller.Initialize();
// 验证... // 验证...
} }
finally finally
{ {
// 清理:重置上下文提供者 // 清理:重置上下文提供者
PlayerController.ResetContextProvider(); PlayerController.ResetContextProvider();
} }
} }
``` ```
### 多架构场景 ### 多架构场景
在某些高级场景中,可能需要同时运行多个架构实例: 在某些高级场景中,可能需要同时运行多个架构实例:
```csharp ```csharp
public class MultiArchitectureManager public class MultiArchitectureManager
{ {
private readonly Dictionary<string, IArchitecture> _architectures = new(); private readonly Dictionary<string, IArchitecture> _architectures = new();
public void SwitchToArchitecture(string name) public void SwitchToArchitecture(string name)
{ {
var architecture = _architectures[name]; var architecture = _architectures[name];
var provider = new ScopedContextProvider(architecture); var provider = new ScopedContextProvider(architecture);
// 为所有使用 [ContextAware] 的类切换上下文 // 为所有使用 [ContextAware] 的类切换上下文
PlayerController.SetContextProvider(provider); PlayerController.SetContextProvider(provider);
EnemyController.SetContextProvider(provider); EnemyController.SetContextProvider(provider);
// ... // ...
} }
} }
``` ```
## 使用场景 ## 使用场景
### 何时使用 [ContextAware] ### 何时使用 [ContextAware]
推荐在以下场景使用 `[ContextAware]` 属性: 推荐在以下场景使用 `[ContextAware]` 属性:
1. **Controller 层**:需要协调多个 Model/System 的控制器 1. **Controller 层**:需要协调多个 Model/System 的控制器
2. **Command/Query 实现**:需要访问架构服务的命令或查询 2. **Command/Query 实现**:需要访问架构服务的命令或查询
3. **自定义组件**:不继承框架基类但需要上下文访问的组件 3. **自定义组件**:不继承框架基类但需要上下文访问的组件
```csharp ```csharp
[ContextAware] [ContextAware]
public partial class GameFlowController : IController public partial class GameFlowController : IController
{ {
public async Task StartGame() public async Task StartGame()
{ {
var saveSystem = this.GetSystem<SaveSystem>(); var saveSystem = this.GetSystem<SaveSystem>();
var uiSystem = this.GetSystem<UISystem>(); var uiSystem = this.GetSystem<UISystem>();
await saveSystem.LoadAsync(); await saveSystem.LoadAsync();
await uiSystem.ShowMainMenuAsync(); await uiSystem.ShowMainMenuAsync();
} }
} }
``` ```
### 与 IController 配合使用 ### 与 IController 配合使用
在 Godot 项目中,控制器通常同时实现 `IController` 和使用 `[ContextAware]` 在 Godot 项目中,控制器通常同时实现 `IController` 和使用 `[ContextAware]`
```csharp ```csharp
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
[ContextAware] [ContextAware]
public partial class PlayerController : Node, IController public partial class PlayerController : Node, IController
{ {
public override void _Ready() public override void _Ready()
{ {
// 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口) // 使用扩展方法访问架构([ContextAware] 实现 IContextAware 接口)
var playerModel = this.GetModel<PlayerModel>(); var playerModel = this.GetModel<PlayerModel>();
var combatSystem = this.GetSystem<CombatSystem>(); var combatSystem = this.GetSystem<CombatSystem>();
} }
} }
``` ```
**说明** **说明**
- `IController` 是标记接口,标识这是一个控制器 - `IController` 是标记接口,标识这是一个控制器
- `[ContextAware]` 提供架构访问能力 - `[ContextAware]` 提供架构访问能力
- 两者配合使用是推荐的模式 - 两者配合使用是推荐的模式
### 何时继承 ContextAwareBase ### 何时继承 ContextAwareBase
如果类需要更多框架功能(如生命周期管理),应继承 `ContextAwareBase` 如果类需要更多框架功能(如生命周期管理),应继承 `ContextAwareBase`
```csharp ```csharp
// 推荐:需要生命周期管理时继承基类 // 推荐:需要生命周期管理时继承基类
public class PlayerModel : AbstractModel public class PlayerModel : AbstractModel
{ {
// AbstractModel 已经继承了 ContextAwareBase // AbstractModel 已经继承了 ContextAwareBase
protected override void OnInit() protected override void OnInit()
{ {
var config = this.GetUtility<ConfigLoader>().Load<PlayerConfig>(); var config = this.GetUtility<ConfigLoader>().Load<PlayerConfig>();
} }
} }
// 推荐:简单组件使用属性 // 推荐:简单组件使用属性
[ContextAware] [ContextAware]
public partial class SimpleHelper public partial class SimpleHelper
{ {
public void DoSomething() public void DoSomething()
{ {
this.SendEvent(new SomethingHappenedEvent()); this.SendEvent(new SomethingHappenedEvent());
} }
} }
``` ```
## 与 IContextAware 接口的关系 ## 与 IContextAware 接口的关系
生成的代码实现了 `IContextAware` 接口: 生成的代码实现了 `IContextAware` 接口:
```csharp ```csharp
namespace GFramework.Core.Abstractions.Rule; namespace GFramework.Core.Abstractions.Rule;
public interface IContextAware public interface IContextAware
{ {
void SetContext(IArchitectureContext context); void SetContext(IArchitectureContext context);
IArchitectureContext GetContext(); IArchitectureContext GetContext();
} }
``` ```
这意味着标记了 `[ContextAware]` 的类可以: 这意味着标记了 `[ContextAware]` 的类可以:
1. **被架构自动注入上下文**:实现 `IContextAware` 的类在注册到架构时会自动调用 `SetContext()` 1. **被架构自动注入上下文**:实现 `IContextAware` 的类在注册到架构时会自动调用 `SetContext()`
2. **参与依赖注入**:可以作为 `IContextAware` 类型注入到其他组件 2. **参与依赖注入**:可以作为 `IContextAware` 类型注入到其他组件
3. **支持上下文传递**:可以通过 `GetContext()` 将上下文传递给其他组件 3. **支持上下文传递**:可以通过 `GetContext()` 将上下文传递给其他组件
## 最佳实践 ## 最佳实践
### 1. 始终使用 partial 关键字 ### 1. 始终使用 partial 关键字
```csharp ```csharp
// ✅ 正确 // ✅ 正确
[ContextAware] [ContextAware]
public partial class MyController { } public partial class MyController { }
// ❌ 错误:编译器会报错 // ❌ 错误:编译器会报错
[ContextAware] [ContextAware]
public class MyController { } public class MyController { }
``` ```
### 2. 在测试中清理上下文提供者 ### 2. 在测试中清理上下文提供者
```csharp ```csharp
[TearDown] [TearDown]
public void TearDown() public void TearDown()
{ {
// 避免测试之间的状态污染 // 避免测试之间的状态污染
PlayerController.ResetContextProvider(); PlayerController.ResetContextProvider();
EnemyController.ResetContextProvider(); EnemyController.ResetContextProvider();
} }
``` ```
### 3. 避免在构造函数中访问 Context ### 3. 避免在构造函数中访问 Context
```csharp ```csharp
[ContextAware] [ContextAware]
public partial class MyController public partial class MyController
{ {
// ❌ 错误:构造函数执行时上下文可能未初始化 // ❌ 错误:构造函数执行时上下文可能未初始化
public MyController() public MyController()
{ {
var model = this.GetModel<SomeModel>(); // 可能为 null var model = this.GetModel<SomeModel>(); // 可能为 null
} }
// ✅ 正确:在初始化方法中访问 // ✅ 正确:在初始化方法中访问
public void Initialize() public void Initialize()
{ {
var model = this.GetModel<SomeModel>(); // 安全 var model = this.GetModel<SomeModel>(); // 安全
} }
} }
``` ```
### 4. 优先使用 Context 属性而非接口方法 ### 4. 优先使用 Context 属性而非接口方法
```csharp ```csharp
[ContextAware] [ContextAware]
public partial class MyController public partial class MyController
{ {
public void DoSomething() public void DoSomething()
{ {
// ✅ 推荐:使用扩展方法 // ✅ 推荐:使用扩展方法
var model = this.GetModel<SomeModel>(); var model = this.GetModel<SomeModel>();
// ❌ 不推荐:显式调用接口方法 // ❌ 不推荐:显式调用接口方法
var context = ((IContextAware)this).GetContext(); var context = ((IContextAware)this).GetContext();
var model2 = context.GetModel<SomeModel>(); var model2 = context.GetModel<SomeModel>();
} }
} }
``` ```
## 诊断信息 ## 诊断信息
生成器会在以下情况报告编译错误: 生成器会在以下情况报告编译错误:
### GFSG001: 类必须是 partial ### GFSG001: 类必须是 partial
```csharp ```csharp
[ContextAware] [ContextAware]
public class MyController { } // 错误:缺少 partial 关键字 public class MyController { } // 错误:缺少 partial 关键字
``` ```
**解决方案**:添加 `partial` 关键字 **解决方案**:添加 `partial` 关键字
```csharp ```csharp
[ContextAware] [ContextAware]
public partial class MyController { } // ✅ 正确 public partial class MyController { } // ✅ 正确
``` ```
### GFSG002: ContextAware 只能用于类 ### GFSG002: ContextAware 只能用于类
```csharp ```csharp
[ContextAware] [ContextAware]
public partial struct MyStruct { } // 错误:不能用于 struct public partial struct MyStruct { } // 错误:不能用于 struct
``` ```
**解决方案**:将 `struct` 改为 `class` **解决方案**:将 `struct` 改为 `class`
```csharp ```csharp
[ContextAware] [ContextAware]
public partial class MyClass { } // ✅ 正确 public partial class MyClass { } // ✅ 正确
``` ```
## 相关文档 ## 相关文档
- [Source Generators 概述](./index) - [Source Generators 概述](./index)
- [架构上下文](../core/context) - [架构上下文](../core/context)
- [IContextAware 接口](../core/rule) - [IContextAware 接口](../core/rule)
- [日志生成器](./logging-generator) - [日志生成器](./logging-generator)

File diff suppressed because it is too large Load Diff

View File

@ -1,341 +1,341 @@
# 日志生成器 # 日志生成器
> GFramework.SourceGenerators 自动生成日志代码,减少样板代码 > GFramework.SourceGenerators 自动生成日志代码,减少样板代码
## 概述 ## 概述
日志生成器是一个 Source Generator它会自动为标记了 `[Log]` 特性的类生成 ILogger 字段。这消除了手动编写日志字段的需要,让开发者专注于业务逻辑。 日志生成器是一个 Source Generator它会自动为标记了 `[Log]` 特性的类生成 ILogger 字段。这消除了手动编写日志字段的需要,让开发者专注于业务逻辑。
## 基本用法 ## 基本用法
### 标记类 ### 标记类
```csharp ```csharp
using GFramework.SourceGenerators.Abstractions.Logging; using GFramework.SourceGenerators.Abstractions.Logging;
[Log] [Log]
public partial class MyService public partial class MyService
{ {
public void DoSomething() public void DoSomething()
{ {
// 自动生成的 Logger 字段可直接使用 // 自动生成的 Logger 字段可直接使用
Logger.Info("执行操作"); Logger.Info("执行操作");
} }
} }
``` ```
### 生成代码 ### 生成代码
上面的代码会被编译时转换为: 上面的代码会被编译时转换为:
```csharp ```csharp
// <auto-generated/> // <auto-generated/>
public partial class MyService public partial class MyService
{ {
private static readonly ILogger Logger = private static readonly ILogger Logger =
LoggerFactoryResolver.Provider.CreateLogger("YourNamespace.MyService"); LoggerFactoryResolver.Provider.CreateLogger("YourNamespace.MyService");
} }
``` ```
**注意**:生成器只生成 ILogger 字段不生成日志方法。日志方法Info、Debug、Error 等)来自 ILogger 接口本身。 **注意**:生成器只生成 ILogger 字段不生成日志方法。日志方法Info、Debug、Error 等)来自 ILogger 接口本身。
## 日志级别 ## 日志级别
生成的 Logger 字段支持 ILogger 接口的所有方法: 生成的 Logger 字段支持 ILogger 接口的所有方法:
```csharp ```csharp
[Log] [Log]
public partial class MyClass public partial class MyClass
{ {
public void Example() public void Example()
{ {
// 调试信息 // 调试信息
Logger.Debug($"调试信息: {value}"); Logger.Debug($"调试信息: {value}");
// 普通信息 // 普通信息
Logger.Info("操作成功"); Logger.Info("操作成功");
// 警告 // 警告
Logger.Warning($"警告: {message}"); Logger.Warning($"警告: {message}");
// 错误 // 错误
Logger.Error($"错误: {ex.Message}"); Logger.Error($"错误: {ex.Message}");
// 严重错误 // 严重错误
Logger.Critical("系统故障"); Logger.Critical("系统故障");
} }
} }
``` ```
## 自定义日志类别 ## 自定义日志类别
```csharp ```csharp
[Log("Gameplay")] [Log("Gameplay")]
public partial class GameplaySystem public partial class GameplaySystem
{ {
// 日志会标记为 Gameplay 类别 // 日志会标记为 Gameplay 类别
public void Update() public void Update()
{ {
Logger.Info("游戏逻辑更新"); Logger.Info("游戏逻辑更新");
} }
} }
``` ```
## 配置选项 ## 配置选项
### 自定义字段名称 ### 自定义字段名称
```csharp ```csharp
[Log(FieldName = "_customLogger")] [Log(FieldName = "_customLogger")]
public partial class MyClass public partial class MyClass
{ {
public void DoSomething() public void DoSomething()
{ {
// 使用自定义字段名 // 使用自定义字段名
_customLogger.Info("使用自定义日志器"); _customLogger.Info("使用自定义日志器");
} }
} }
``` ```
### 非静态字段 ### 非静态字段
```csharp ```csharp
[Log(IsStatic = false)] [Log(IsStatic = false)]
public partial class InstanceLogger public partial class InstanceLogger
{ {
// 生成实例字段而非静态字段 // 生成实例字段而非静态字段
public void LogMessage() public void LogMessage()
{ {
Logger.Info("实例日志"); Logger.Info("实例日志");
} }
} }
``` ```
### 访问修饰符 ### 访问修饰符
```csharp ```csharp
[Log(AccessModifier = "protected")] [Log(AccessModifier = "protected")]
public partial class ProtectedLogger public partial class ProtectedLogger
{ {
// 生成 protected 字段 // 生成 protected 字段
} }
``` ```
### 配置选项说明 ### 配置选项说明
| 参数 | 类型 | 默认值 | 说明 | | 参数 | 类型 | 默认值 | 说明 |
|----------------|---------|-----------|---------------------------------| |----------------|---------|-----------|---------------------------------|
| Name | string? | null | 日志分类名称(默认使用类名) | | Name | string? | null | 日志分类名称(默认使用类名) |
| FieldName | string | "Logger" | 生成的字段名称 | | FieldName | string | "Logger" | 生成的字段名称 |
| IsStatic | bool | true | 是否生成静态字段 | | IsStatic | bool | true | 是否生成静态字段 |
| AccessModifier | string | "private" | 访问修饰符private/protected/public | | AccessModifier | string | "private" | 访问修饰符private/protected/public |
## 与其他模块集成 ## 与其他模块集成
### 与 Godot 集成 ### 与 Godot 集成
```csharp ```csharp
[Log] [Log]
[ContextAware] [ContextAware]
public partial class GodotController : Node public partial class GodotController : Node
{ {
public override void _Ready() public override void _Ready()
{ {
Logger.Info("控制器已准备就绪"); Logger.Info("控制器已准备就绪");
} }
} }
``` ```
### 与架构集成 ### 与架构集成
```csharp ```csharp
[Log] [Log]
public partial class MySystem : AbstractSystem public partial class MySystem : AbstractSystem
{ {
protected override void OnInit() protected override void OnInit()
{ {
Logger.Info("系统初始化"); Logger.Info("系统初始化");
} }
} }
``` ```
## 实际应用示例 ## 实际应用示例
### 游戏控制器 ### 游戏控制器
```csharp ```csharp
using GFramework.Core.Abstractions.Controller; using GFramework.Core.Abstractions.Controller;
using GFramework.SourceGenerators.Abstractions.Logging; using GFramework.SourceGenerators.Abstractions.Logging;
using GFramework.SourceGenerators.Abstractions.Rule; using GFramework.SourceGenerators.Abstractions.Rule;
[Log] [Log]
[ContextAware] [ContextAware]
public partial class PlayerController : IController public partial class PlayerController : IController
{ {
public void HandleInput(string action) public void HandleInput(string action)
{ {
Logger.Debug($"处理输入: {action}"); Logger.Debug($"处理输入: {action}");
switch (action) switch (action)
{ {
case "jump": case "jump":
Logger.Info("玩家跳跃"); Logger.Info("玩家跳跃");
Jump(); Jump();
break; break;
case "attack": case "attack":
Logger.Info("玩家攻击"); Logger.Info("玩家攻击");
Attack(); Attack();
break; break;
default: default:
Logger.Warning($"未知操作: {action}"); Logger.Warning($"未知操作: {action}");
break; break;
} }
} }
private void Jump() private void Jump()
{ {
try try
{ {
// 跳跃逻辑 // 跳跃逻辑
Logger.Debug("跳跃执行成功"); Logger.Debug("跳跃执行成功");
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error($"跳跃失败: {ex.Message}"); Logger.Error($"跳跃失败: {ex.Message}");
} }
} }
} }
``` ```
### 数据处理服务 ### 数据处理服务
```csharp ```csharp
[Log("DataService")] [Log("DataService")]
public partial class DataProcessor public partial class DataProcessor
{ {
public void ProcessData(string data) public void ProcessData(string data)
{ {
Logger.Info($"开始处理数据,长度: {data.Length}"); Logger.Info($"开始处理数据,长度: {data.Length}");
if (string.IsNullOrEmpty(data)) if (string.IsNullOrEmpty(data))
{ {
Logger.Warning("数据为空,跳过处理"); Logger.Warning("数据为空,跳过处理");
return; return;
} }
try try
{ {
// 处理逻辑 // 处理逻辑
Logger.Debug("数据处理中..."); Logger.Debug("数据处理中...");
// ... // ...
Logger.Info("数据处理完成"); Logger.Info("数据处理完成");
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error($"数据处理失败: {ex.Message}"); Logger.Error($"数据处理失败: {ex.Message}");
throw; throw;
} }
} }
} }
``` ```
## 最佳实践 ## 最佳实践
### 1. 合理使用日志级别 ### 1. 合理使用日志级别
```csharp ```csharp
[Log] [Log]
public partial class BestPracticeExample public partial class BestPracticeExample
{ {
public void ProcessRequest() public void ProcessRequest()
{ {
// Debug: 详细的调试信息 // Debug: 详细的调试信息
Logger.Debug("开始处理请求"); Logger.Debug("开始处理请求");
// Info: 重要的业务流程信息 // Info: 重要的业务流程信息
Logger.Info("请求处理成功"); Logger.Info("请求处理成功");
// Warning: 可恢复的异常情况 // Warning: 可恢复的异常情况
Logger.Warning("缓存未命中,使用默认值"); Logger.Warning("缓存未命中,使用默认值");
// Error: 错误但不影响系统运行 // Error: 错误但不影响系统运行
Logger.Error("处理失败,将重试"); Logger.Error("处理失败,将重试");
// Critical: 严重错误,可能导致系统崩溃 // Critical: 严重错误,可能导致系统崩溃
Logger.Critical("数据库连接失败"); Logger.Critical("数据库连接失败");
} }
} }
``` ```
### 2. 避免过度日志 ### 2. 避免过度日志
```csharp ```csharp
[Log] [Log]
public partial class PerformanceExample public partial class PerformanceExample
{ {
private int _frameCount = 0; private int _frameCount = 0;
public void Update() public void Update()
{ {
// 好的做法:定期记录 // 好的做法:定期记录
if (_frameCount % 1000 == 0) if (_frameCount % 1000 == 0)
{ {
Logger.Debug($"已运行 {_frameCount} 帧"); Logger.Debug($"已运行 {_frameCount} 帧");
} }
_frameCount++; _frameCount++;
// 避免:每帧都记录 // 避免:每帧都记录
// Logger.Debug($"帧 {_frameCount}"); // ❌ 太频繁 // Logger.Debug($"帧 {_frameCount}"); // ❌ 太频繁
} }
} }
``` ```
### 3. 结构化日志信息 ### 3. 结构化日志信息
```csharp ```csharp
[Log] [Log]
public partial class StructuredLogging public partial class StructuredLogging
{ {
public void ProcessUser(int userId, string action) public void ProcessUser(int userId, string action)
{ {
// 好的做法:包含上下文信息 // 好的做法:包含上下文信息
Logger.Info($"用户操作 [UserId={userId}, Action={action}]"); Logger.Info($"用户操作 [UserId={userId}, Action={action}]");
// 避免:信息不完整 // 避免:信息不完整
// Logger.Info("用户操作"); // ❌ 缺少上下文 // Logger.Info("用户操作"); // ❌ 缺少上下文
} }
} }
``` ```
## 常见问题 ## 常见问题
### Q: 为什么需要 partial 关键字? ### Q: 为什么需要 partial 关键字?
**A**: 源代码生成器需要向现有类添加代码,`partial` 关键字允许一个类的定义分散在多个文件中。 **A**: 源代码生成器需要向现有类添加代码,`partial` 关键字允许一个类的定义分散在多个文件中。
### Q: 可以在静态类中使用吗? ### Q: 可以在静态类中使用吗?
**A**: 可以,生成器会自动生成静态字段: **A**: 可以,生成器会自动生成静态字段:
```csharp ```csharp
[Log] [Log]
public static partial class StaticHelper public static partial class StaticHelper
{ {
public static void DoSomething() public static void DoSomething()
{ {
Logger.Info("静态方法日志"); Logger.Info("静态方法日志");
} }
} }
``` ```
### Q: 如何自定义日志工厂? ### Q: 如何自定义日志工厂?
**A**: 通过配置 `LoggerFactoryResolver.Provider` 来自定义日志工厂实现。 **A**: 通过配置 `LoggerFactoryResolver.Provider` 来自定义日志工厂实现。
--- ---
**相关文档** **相关文档**
- [Source Generators 概述](./index) - [Source Generators 概述](./index)
- [枚举扩展生成器](./enum-generator) - [枚举扩展生成器](./enum-generator)
- [ContextAware 生成器](./context-aware-generator) - [ContextAware 生成器](./context-aware-generator)

View File

@ -1,6 +1,6 @@
--- ---
title: 使用协程系统 title: 使用协程系统
description: 学习如何使用协程系统实现异步操作和时间控制 description: 学习如何在 GFramework 中创建调度器、运行协程并结合时间、阶段、Task 与生命周期管理实现常见异步流程。
--- ---
# 使用协程系统 # 使用协程系统
@ -9,590 +9,184 @@ description: 学习如何使用协程系统实现异步操作和时间控制
完成本教程后,你将能够: 完成本教程后,你将能够:
- 理解协程的基本概念和执行机制 - 创建并驱动 `CoroutineScheduler`
- 创建和启动协程 - 编写 `IEnumerator<IYieldInstruction>` 协程
- 使用各种等待指令控制协程执行 - 区分缩放时间、真实时间与阶段等待
- 在架构组件中使用协程 - 使用句柄、取消令牌和快照查询控制协程
- 实现常见的游戏逻辑(延迟执行、循环任务、事件等待) - 在 Godot 中把协程绑定到节点生命周期
## 前置条件
- 已安装 GFramework.Core NuGet 包
- 了解 C# 基础语法和迭代器IEnumerator
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[生命周期管理](/zh-CN/core/lifecycle)
## 步骤 1创建第一个协程 ## 步骤 1创建第一个协程
首先,让我们创建一个简单的协程来理解基本概念。
```csharp ```csharp
using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine; using GFramework.Core.Coroutine;
using GFramework.Core.Coroutine.Instructions; using GFramework.Core.Coroutine.Instructions;
namespace MyGame.Systems public sealed class TutorialLoop
{ {
public class TutorialSystem : AbstractSystem private readonly CoroutineScheduler _scheduler;
public TutorialLoop(ITimeSource timeSource)
{ {
protected override void OnInit() _scheduler = new CoroutineScheduler(timeSource);
{ }
// 启动协程
this.StartCoroutine(MyFirstCoroutine());
}
/// <summary> public void Start()
/// 第一个协程示例 {
/// </summary> _scheduler.Run(MyFirstCoroutine(), tag: "tutorial");
private IEnumerator<IYieldInstruction> MyFirstCoroutine() }
{
Console.WriteLine("协程开始执行");
// 等待 1 秒 public void Tick()
yield return CoroutineHelper.WaitForSeconds(1.0); {
_scheduler.Update();
}
Console.WriteLine("1 秒后执行"); private IEnumerator<IYieldInstruction> MyFirstCoroutine()
{
Console.WriteLine("协程开始");
// 等待 1 帧 yield return new Delay(1.0);
yield return CoroutineHelper.WaitForOneFrame(); Console.WriteLine("1 秒后");
Console.WriteLine("下一帧执行"); yield return new WaitOneFrame();
Console.WriteLine("下一帧");
// 等待 5 帧 yield return new WaitForFrames(3);
yield return CoroutineHelper.WaitForFrames(5); Console.WriteLine("3 帧后");
Console.WriteLine("5 帧后执行");
}
} }
} }
``` ```
**代码说明** 关键点
- 协程方法返回 `IEnumerator<IYieldInstruction>` - 协程返回类型必须是 `IEnumerator<IYieldInstruction>`
- 使用 `yield return` 返回等待指令 - 调度器不会自动运行,你必须在宿主主循环中调用 `Update()`
- `this.StartCoroutine()` 扩展方法启动协程 - `Run(...)` 返回 `CoroutineHandle`,后续控制都依赖这个句柄
- `WaitForSeconds` 等待指定秒数
- `WaitForOneFrame` 等待一帧
- `WaitForFrames` 等待多帧
## 步骤 2实现生命值自动恢复 ## 步骤 2控制协程生命周期
让我们实现一个实用的功能:玩家生命值自动恢复。
```csharp ```csharp
using GFramework.Core.Abstractions.Model; using var cts = new CancellationTokenSource();
using GFramework.Core.Abstractions.Property;
using GFramework.Core.Model;
namespace MyGame.Models var handle = _scheduler.Run(
HealthRegenerationCoroutine(),
tag: "regen",
group: "player",
cancellationToken: cts.Token);
_scheduler.Pause(handle);
_scheduler.Resume(handle);
// 外部取消会在下一次 Update 时生效
cts.Cancel();
var status = await _scheduler.WaitForCompletionAsync(handle);
Console.WriteLine(status);
```
如果你需要观察运行中状态:
```csharp
if (_scheduler.TryGetSnapshot(handle, out var snapshot))
{ {
public class PlayerModel : AbstractModel Console.WriteLine(snapshot.State);
{ Console.WriteLine(snapshot.WaitingInstructionType);
// 当前生命值
public BindableProperty<int> Health { get; } = new(100);
// 最大生命值
public BindableProperty<int> MaxHealth { get; } = new(100);
// 是否启用自动恢复
public BindableProperty<bool> AutoRegenEnabled { get; } = new(true);
private CoroutineHandle? _regenHandle;
protected override void OnInit()
{
// 启动生命值恢复协程
StartHealthRegeneration();
}
/// <summary>
/// 启动生命值恢复
/// </summary>
public void StartHealthRegeneration()
{
// 如果已经在运行,先停止
if (_regenHandle.HasValue)
{
this.StopCoroutine(_regenHandle.Value);
}
// 启动新的恢复协程
_regenHandle = this.StartCoroutine(HealthRegenerationCoroutine());
}
/// <summary>
/// 停止生命值恢复
/// </summary>
public void StopHealthRegeneration()
{
if (_regenHandle.HasValue)
{
this.StopCoroutine(_regenHandle.Value);
_regenHandle = null;
}
}
/// <summary>
/// 生命值恢复协程
/// </summary>
private IEnumerator<IYieldInstruction> HealthRegenerationCoroutine()
{
while (true)
{
// 等待 1 秒
yield return CoroutineHelper.WaitForSeconds(1.0);
// 检查是否启用自动恢复
if (!AutoRegenEnabled.Value)
continue;
// 如果生命值未满,恢复 5 点
if (Health.Value < MaxHealth.Value)
{
Health.Value = Math.Min(Health.Value + 5, MaxHealth.Value);
Console.WriteLine($"生命值恢复: {Health.Value}/{MaxHealth.Value}");
}
}
}
}
} }
``` ```
**代码说明** ## 步骤 3区分时间等待
- 使用 `while (true)` 创建无限循环协程
- 保存协程句柄以便后续控制
- 使用 `StopCoroutine` 停止协程
- 协程中可以访问类成员变量
## 步骤 3实现技能冷却系统
接下来实现一个技能冷却系统,展示如何使用协程管理时间相关的游戏逻辑。
```csharp ```csharp
using GFramework.Core.System; private IEnumerator<IYieldInstruction> CooldownCoroutine()
using System.Collections.Generic;
namespace MyGame.Systems
{ {
public class SkillSystem : AbstractSystem // 使用宿主默认时间
{ yield return new Delay(2.0);
// 技能冷却状态
private readonly Dictionary<string, bool> _skillCooldowns = new();
/// <summary> // 使用真实时间,需要调度器提供 realtimeTimeSource
/// 使用技能 yield return new WaitForSecondsRealtime(2.0);
/// </summary>
public bool UseSkill(string skillName, double cooldownTime)
{
// 检查是否在冷却中
if (_skillCooldowns.TryGetValue(skillName, out var isOnCooldown) && isOnCooldown)
{
Console.WriteLine($"技能 {skillName} 冷却中...");
return false;
}
// 执行技能
Console.WriteLine($"使用技能: {skillName}");
// 启动冷却协程
this.StartCoroutine(SkillCooldownCoroutine(skillName, cooldownTime));
return true;
}
/// <summary>
/// 技能冷却协程
/// </summary>
private IEnumerator<IYieldInstruction> SkillCooldownCoroutine(string skillName, double cooldownTime)
{
// 标记为冷却中
_skillCooldowns[skillName] = true;
Console.WriteLine($"技能 {skillName} 开始冷却 {cooldownTime} 秒");
// 等待冷却时间
yield return CoroutineHelper.WaitForSeconds(cooldownTime);
// 冷却结束
_skillCooldowns[skillName] = false;
Console.WriteLine($"技能 {skillName} 冷却完成");
}
/// <summary>
/// 带进度显示的技能冷却
/// </summary>
private IEnumerator<IYieldInstruction> SkillCooldownWithProgressCoroutine(
string skillName,
double cooldownTime)
{
_skillCooldowns[skillName] = true;
// 使用 WaitForProgress 显示冷却进度
yield return CoroutineHelper.WaitForProgress(
duration: cooldownTime,
onProgress: progress =>
{
Console.WriteLine($"技能 {skillName} 冷却进度: {progress * 100:F0}%");
}
);
_skillCooldowns[skillName] = false;
Console.WriteLine($"技能 {skillName} 冷却完成");
}
}
} }
``` ```
**代码说明** 建议:
- 使用字典管理多个技能的冷却状态 - 普通游戏逻辑优先使用 `Delay`
- 每个技能使用独立的协程管理冷却 - 暂停菜单、真实倒计时、网络超时等场景使用 `WaitForSecondsRealtime`
- `WaitForProgress` 可以在等待期间执行回调
- 协程结束后自动清理冷却状态
## 步骤 4等待事件触发 ## 步骤 4使用阶段等待
实现一个等待玩家完成任务的系统,展示如何在协程中等待事件。 只有宿主为调度器提供了匹配阶段时,阶段等待才会真实生效:
```csharp ```csharp
using GFramework.Core.Abstractions.Events; var fixedScheduler = new CoroutineScheduler(
fixedTimeSource,
executionStage: CoroutineExecutionStage.FixedUpdate);
private IEnumerator<IYieldInstruction> PhysicsCoroutine()
{
yield return new WaitForFixedUpdate();
Console.WriteLine("下一次固定步到达");
}
```
同理,`WaitForEndOfFrame` 需要运行在 `CoroutineExecutionStage.EndOfFrame` 的调度器上。
## 步骤 5等待 Task
```csharp
using GFramework.Core.Coroutine.Extensions;
private IEnumerator<IYieldInstruction> LoadCoroutine()
{
var task = LoadDataAsync();
yield return task.AsCoroutineInstruction();
Console.WriteLine("Task 已完成");
}
```
如果你已经持有调度器,也可以直接把 `Task` 作为顶层协程启动:
```csharp
var handle = _scheduler.StartTaskAsCoroutine(LoadDataAsync());
```
## 步骤 6在 Godot 中绑定 Node 生命周期
```csharp
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions; using GFramework.Core.Coroutine.Instructions;
using GFramework.Godot.Coroutine;
using Godot;
namespace MyGame.Systems public partial class DemoNode : Node
{ {
// 任务完成事件 public override void _Ready()
public record QuestCompletedEvent(int QuestId, string QuestName) : IEvent;
public class QuestSystem : AbstractSystem
{ {
/// <summary> // 推荐:节点作为所有者运行协程
/// 开始任务并等待完成 this.RunCoroutine(BlinkCoroutine(), Segment.ProcessIgnorePause, tag: "blink");
/// </summary> }
public void StartQuest(int questId, string questName)
private IEnumerator<IYieldInstruction> BlinkCoroutine()
{
while (true)
{ {
this.StartCoroutine(QuestCoroutine(questId, questName)); Visible = !Visible;
} yield return new WaitForSecondsRealtime(0.5);
/// <summary>
/// 任务协程
/// </summary>
private IEnumerator<IYieldInstruction> QuestCoroutine(int questId, string questName)
{
Console.WriteLine($"任务开始: {questName}");
// 获取事件总线
var eventBus = this.GetService<IEventBus>();
// 等待任务完成事件
var waitEvent = new WaitForEvent<QuestCompletedEvent>(
eventBus,
evt => evt.QuestId == questId // 过滤条件
);
yield return waitEvent;
// 获取事件数据
var completedEvent = waitEvent.EventData;
Console.WriteLine($"任务完成: {completedEvent.QuestName}");
// 发放奖励
GiveReward(questId);
}
/// <summary>
/// 带超时的任务
/// </summary>
private IEnumerator<IYieldInstruction> TimedQuestCoroutine(
int questId,
string questName,
double timeLimit)
{
Console.WriteLine($"限时任务开始: {questName} (时限: {timeLimit}秒)");
var eventBus = this.GetService<IEventBus>();
// 等待事件,带超时
var waitEvent = new WaitForEventWithTimeout<QuestCompletedEvent>(
eventBus,
timeout: timeLimit,
predicate: evt => evt.QuestId == questId
);
yield return waitEvent;
if (waitEvent.IsTimeout)
{
Console.WriteLine($"任务超时失败: {questName}");
}
else
{
Console.WriteLine($"任务完成: {questName}");
GiveReward(questId);
}
}
private void GiveReward(int questId)
{
Console.WriteLine($"发放任务 {questId} 的奖励");
} }
} }
} }
``` ```
**代码说明** `DemoNode` 退出场景树时,上面的协程会被自动终止。
- `WaitForEvent` 等待特定事件触发 如果你需要绑定多个节点,可以继续使用:
- 可以使用 `predicate` 参数过滤事件
- `WaitForEventWithTimeout` 支持超时机制
- 通过 `EventData` 属性获取事件数据
## 步骤 5协程组合与嵌套
实现一个复杂的游戏流程,展示如何组合多个协程。
```csharp ```csharp
namespace MyGame.Systems BlinkCoroutine()
{ .CancelWith(this, anotherNode)
public class GameFlowSystem : AbstractSystem .RunCoroutine();
{
/// <summary>
/// 游戏开始流程
/// </summary>
public void StartGame()
{
this.StartCoroutine(GameStartSequence());
}
/// <summary>
/// 游戏开始序列
/// </summary>
private IEnumerator<IYieldInstruction> GameStartSequence()
{
Console.WriteLine("=== 游戏开始 ===");
// 1. 显示标题
yield return ShowTitle();
// 2. 加载资源
yield return LoadResources();
// 3. 初始化玩家
yield return InitializePlayer();
// 4. 播放开场动画
yield return PlayOpeningAnimation();
Console.WriteLine("=== 游戏准备完成 ===");
}
/// <summary>
/// 显示标题
/// </summary>
private IEnumerator<IYieldInstruction> ShowTitle()
{
Console.WriteLine("显示游戏标题...");
yield return CoroutineHelper.WaitForSeconds(2.0);
Console.WriteLine("标题显示完成");
}
/// <summary>
/// 加载资源
/// </summary>
private IEnumerator<IYieldInstruction> LoadResources()
{
Console.WriteLine("开始加载资源...");
// 并行加载多个资源
var loadTextures = LoadTexturesCoroutine();
var loadAudio = LoadAudioCoroutine();
var loadModels = LoadModelsCoroutine();
// 等待所有资源加载完成
yield return new WaitForAllCoroutines(
this.GetCoroutineScheduler(),
loadTextures,
loadAudio,
loadModels
);
Console.WriteLine("所有资源加载完成");
}
private IEnumerator<IYieldInstruction> LoadTexturesCoroutine()
{
Console.WriteLine(" 加载纹理...");
yield return CoroutineHelper.WaitForSeconds(1.0);
Console.WriteLine(" 纹理加载完成");
}
private IEnumerator<IYieldInstruction> LoadAudioCoroutine()
{
Console.WriteLine(" 加载音频...");
yield return CoroutineHelper.WaitForSeconds(1.5);
Console.WriteLine(" 音频加载完成");
}
private IEnumerator<IYieldInstruction> LoadModelsCoroutine()
{
Console.WriteLine(" 加载模型...");
yield return CoroutineHelper.WaitForSeconds(0.8);
Console.WriteLine(" 模型加载完成");
}
private IEnumerator<IYieldInstruction> InitializePlayer()
{
Console.WriteLine("初始化玩家...");
yield return CoroutineHelper.WaitForSeconds(0.5);
Console.WriteLine("玩家初始化完成");
}
private IEnumerator<IYieldInstruction> PlayOpeningAnimation()
{
Console.WriteLine("播放开场动画...");
yield return CoroutineHelper.WaitForSeconds(3.0);
Console.WriteLine("开场动画播放完成");
}
/// <summary>
/// 获取协程调度器
/// </summary>
private CoroutineScheduler GetCoroutineScheduler()
{
// 从架构服务中获取
return this.GetService<CoroutineScheduler>();
}
}
}
``` ```
**代码说明**
- 使用 `yield return` 调用其他协程实现嵌套
- `WaitForAllCoroutines` 并行执行多个协程
- 协程可以像函数一样组合和复用
- 清晰的流程控制,避免回调嵌套
## 完整代码
### GameArchitecture.cs
```csharp
using GFramework.Core.Architecture;
namespace MyGame
{
public class GameArchitecture : Architecture
{
public static IArchitecture Interface { get; private set; }
protected override void Init()
{
Interface = this;
// 注册 Model
RegisterModel(new PlayerModel());
// 注册 System
RegisterSystem(new TutorialSystem());
RegisterSystem(new SkillSystem());
RegisterSystem(new QuestSystem());
RegisterSystem(new GameFlowSystem());
}
}
}
```
### 测试代码
```csharp
using MyGame;
using MyGame.Systems;
// 初始化架构
var architecture = new GameArchitecture();
architecture.Initialize();
await architecture.WaitUntilReadyAsync();
// 测试技能系统
var skillSystem = architecture.GetSystem<SkillSystem>();
skillSystem.UseSkill("火球术", 3.0);
await Task.Delay(1000);
skillSystem.UseSkill("火球术", 3.0); // 冷却中
await Task.Delay(3000);
skillSystem.UseSkill("火球术", 3.0); // 冷却完成
// 测试任务系统
var questSystem = architecture.GetSystem<QuestSystem>();
questSystem.StartQuest(1, "击败史莱姆");
// 模拟任务完成
await Task.Delay(2000);
var eventBus = architecture.GetService<IEventBus>();
eventBus.Publish(new QuestCompletedEvent(1, "击败史莱姆"));
// 测试游戏流程
var gameFlowSystem = architecture.GetSystem<GameFlowSystem>();
gameFlowSystem.StartGame();
```
## 运行结果
运行程序后,你将看到类似以下的输出:
```
协程开始执行
1 秒后执行
下一帧执行
5 帧后执行
使用技能: 火球术
技能 火球术 开始冷却 3.0 秒
技能 火球术 冷却中...
技能 火球术 冷却完成
使用技能: 火球术
任务开始: 击败史莱姆
任务完成: 击败史莱姆
发放任务 1 的奖励
=== 游戏开始 ===
显示游戏标题...
标题显示完成
开始加载资源...
加载纹理...
加载音频...
加载模型...
模型加载完成
纹理加载完成
音频加载完成
所有资源加载完成
初始化玩家...
玩家初始化完成
播放开场动画...
开场动画播放完成
=== 游戏准备完成 ===
```
**验证步骤**
1. 协程按预期顺序执行
2. 技能冷却系统正常工作
3. 事件等待功能正确
4. 并行加载资源成功
## 下一步 ## 下一步
恭喜!你已经掌握了协程系统的基本用法。接下来可以学习: - Core 侧更完整的 API 说明见 [Core 协程系统](/zh-CN/core/coroutine)
- Godot 集成细节见 [Godot 协程系统](/zh-CN/godot/coroutine)
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 使用协程实现状态转换
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 在协程中加载资源
- [使用事件系统](/zh-CN/core/events) - 协程与事件系统集成
## 相关文档
- [协程系统](/zh-CN/core/coroutine) - 协程系统详细说明
- [事件系统](/zh-CN/core/events) - 事件系统详解
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
- [System 层](/zh-CN/core/system) - System 详细说明

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,456 +1,456 @@
#!/usr/bin/env pwsh #!/usr/bin/env pwsh
<# <#
.SYNOPSIS .SYNOPSIS
GFramework 命名空间重构脚本 - 将所有文件夹和命名空间从小写改为 PascalCase GFramework 命名空间重构脚本 - 将所有文件夹和命名空间从小写改为 PascalCase
.DESCRIPTION .DESCRIPTION
此脚本执行以下操作 此脚本执行以下操作
1. 重命名文件夹使用 git mv 保留历史 1. 重命名文件夹使用 git mv 保留历史
2. 更新所有 C# 文件中的命名空间声明和 using 语句 2. 更新所有 C# 文件中的命名空间声明和 using 语句
3. 更新文档中的命名空间引用 3. 更新文档中的命名空间引用
4. 验证更改的完整性 4. 验证更改的完整性
.PARAMETER Phase .PARAMETER Phase
指定要执行的阶段 指定要执行的阶段
- 1: 文件夹重命名 - 1: 文件夹重命名
- 2: 命名空间更新 - 2: 命名空间更新
- 3: 文档更新 - 3: 文档更新
- 4: 验证 - 4: 验证
- All: 执行所有阶段默认 - All: 执行所有阶段默认
.PARAMETER DryRun .PARAMETER DryRun
干运行模式只显示将要执行的操作不实际执行 干运行模式只显示将要执行的操作不实际执行
.PARAMETER SkipTests .PARAMETER SkipTests
跳过测试验证 跳过测试验证
.EXAMPLE .EXAMPLE
./refactor-namespaces.ps1 -Phase 1 ./refactor-namespaces.ps1 -Phase 1
./refactor-namespaces.ps1 -DryRun ./refactor-namespaces.ps1 -DryRun
./refactor-namespaces.ps1 -Phase All -SkipTests ./refactor-namespaces.ps1 -Phase All -SkipTests
#> #>
param( param(
[Parameter()] [Parameter()]
[ValidateSet("1", "2", "3", "4", "All")] [ValidateSet("1", "2", "3", "4", "All")]
[string]$Phase = "All", [string]$Phase = "All",
[Parameter()] [Parameter()]
[switch]$DryRun, [switch]$DryRun,
[Parameter()] [Parameter()]
[switch]$SkipTests [switch]$SkipTests
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$rootDir = Split-Path -Parent $scriptDir $rootDir = Split-Path -Parent $scriptDir
$mappingFile = Join-Path $scriptDir "folder-mappings.json" $mappingFile = Join-Path $scriptDir "folder-mappings.json"
# 颜色输出函数 # 颜色输出函数
function Write-ColorOutput { function Write-ColorOutput {
param( param(
[string]$Message, [string]$Message,
[string]$Color = "White" [string]$Color = "White"
) )
Write-Host $Message -ForegroundColor $Color Write-Host $Message -ForegroundColor $Color
} }
function Write-Success { param([string]$Message) Write-ColorOutput "$Message" "Green" } function Write-Success { param([string]$Message) Write-ColorOutput "$Message" "Green" }
function Write-Info { param([string]$Message) Write-ColorOutput " $Message" "Cyan" } function Write-Info { param([string]$Message) Write-ColorOutput " $Message" "Cyan" }
function Write-Warning { param([string]$Message) Write-ColorOutput "$Message" "Yellow" } function Write-Warning { param([string]$Message) Write-ColorOutput "$Message" "Yellow" }
function Write-Error { param([string]$Message) Write-ColorOutput "$Message" "Red" } function Write-Error { param([string]$Message) Write-ColorOutput "$Message" "Red" }
# 阶段 1: 文件夹重命名 # 阶段 1: 文件夹重命名
function Invoke-FolderRename { function Invoke-FolderRename {
Write-Info "=== 阶段 1: 文件夹重命名 ===" Write-Info "=== 阶段 1: 文件夹重命名 ==="
$config = Get-Content $mappingFile | ConvertFrom-Json $config = Get-Content $mappingFile | ConvertFrom-Json
$totalFolders = 0 $totalFolders = 0
foreach ($project in $config.projects) { foreach ($project in $config.projects) {
Write-Info "处理项目: $($project.name)" Write-Info "处理项目: $($project.name)"
$projectPath = Join-Path $rootDir $project.path $projectPath = Join-Path $rootDir $project.path
if (-not (Test-Path $projectPath)) { if (-not (Test-Path $projectPath)) {
Write-Warning "项目路径不存在: $projectPath" Write-Warning "项目路径不存在: $projectPath"
continue continue
} }
# 按深度排序(深度优先,避免路径冲突) # 按深度排序(深度优先,避免路径冲突)
$sortedFolders = $project.folders | Sort-Object { ($_.from -split '/').Count } -Descending $sortedFolders = $project.folders | Sort-Object { ($_.from -split '/').Count } -Descending
foreach ($folder in $sortedFolders) { foreach ($folder in $sortedFolders) {
$fromPath = Join-Path $projectPath $folder.from $fromPath = Join-Path $projectPath $folder.from
$toPath = Join-Path $projectPath $folder.to $toPath = Join-Path $projectPath $folder.to
if (-not (Test-Path $fromPath)) { if (-not (Test-Path $fromPath)) {
Write-Warning "源文件夹不存在: $fromPath" Write-Warning "源文件夹不存在: $fromPath"
continue continue
} }
if ($fromPath -eq $toPath) { if ($fromPath -eq $toPath) {
Write-Info "跳过(路径相同): $($folder.from)" Write-Info "跳过(路径相同): $($folder.from)"
continue continue
} }
# Windows 文件系统不区分大小写,需要两步重命名 # Windows 文件系统不区分大小写,需要两步重命名
$tempPath = "$fromPath`_temp" $tempPath = "$fromPath`_temp"
if ($DryRun) { if ($DryRun) {
Write-Info "[DRY RUN] git mv $fromPath $tempPath" Write-Info "[DRY RUN] git mv $fromPath $tempPath"
Write-Info "[DRY RUN] git mv $tempPath $toPath" Write-Info "[DRY RUN] git mv $tempPath $toPath"
} else { } else {
try { try {
Write-Info "重命名: $($folder.from) -> $($folder.to)" Write-Info "重命名: $($folder.from) -> $($folder.to)"
# 第一步:重命名为临时名称 # 第一步:重命名为临时名称
git mv $fromPath $tempPath git mv $fromPath $tempPath
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "git mv 失败: $fromPath -> $tempPath" throw "git mv 失败: $fromPath -> $tempPath"
} }
# 第二步:重命名为目标名称 # 第二步:重命名为目标名称
git mv $tempPath $toPath git mv $tempPath $toPath
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "git mv 失败: $tempPath -> $toPath" throw "git mv 失败: $tempPath -> $toPath"
} }
$totalFolders++ $totalFolders++
Write-Success "完成: $($folder.from) -> $($folder.to)" Write-Success "完成: $($folder.from) -> $($folder.to)"
} catch { } catch {
Write-Error "重命名失败: $_" Write-Error "重命名失败: $_"
throw throw
} }
} }
} }
if (-not $DryRun) { if (-not $DryRun) {
Write-Info "提交项目 $($project.name) 的文件夹重命名" Write-Info "提交项目 $($project.name) 的文件夹重命名"
git add -A git add -A
git commit -m "refactor($($project.name)): 重命名文件夹为 PascalCase" git commit -m "refactor($($project.name)): 重命名文件夹为 PascalCase"
} }
} }
Write-Success "阶段 1 完成: 共重命名 $totalFolders 个文件夹" Write-Success "阶段 1 完成: 共重命名 $totalFolders 个文件夹"
} }
# 阶段 2: 命名空间更新 # 阶段 2: 命名空间更新
function Invoke-NamespaceUpdate { function Invoke-NamespaceUpdate {
Write-Info "=== 阶段 2: 命名空间更新 ===" Write-Info "=== 阶段 2: 命名空间更新 ==="
$csFiles = Get-ChildItem -Path $rootDir -Filter "*.cs" -Recurse | $csFiles = Get-ChildItem -Path $rootDir -Filter "*.cs" -Recurse |
Where-Object { $_.FullName -notmatch "\\bin\\|\\obj\\|\\Generated\\" } Where-Object { $_.FullName -notmatch "\\bin\\|\\obj\\|\\Generated\\" }
Write-Info "找到 $($csFiles.Count) 个 C# 文件" Write-Info "找到 $($csFiles.Count) 个 C# 文件"
$updatedFiles = 0 $updatedFiles = 0
$totalReplacements = 0 $totalReplacements = 0
# 定义命名空间替换规则(按优先级排序,长的先匹配) # 定义命名空间替换规则(按优先级排序,长的先匹配)
$namespaceRules = @( $namespaceRules = @(
# CQRS 子命名空间 # CQRS 子命名空间
@{ Pattern = '\.cqrs\.notification\b'; Replacement = '.CQRS.Notification' } @{ Pattern = '\.cqrs\.notification\b'; Replacement = '.CQRS.Notification' }
@{ Pattern = '\.cqrs\.command\b'; Replacement = '.CQRS.Command' } @{ Pattern = '\.cqrs\.command\b'; Replacement = '.CQRS.Command' }
@{ Pattern = '\.cqrs\.request\b'; Replacement = '.CQRS.Request' } @{ Pattern = '\.cqrs\.request\b'; Replacement = '.CQRS.Request' }
@{ Pattern = '\.cqrs\.query\b'; Replacement = '.CQRS.Query' } @{ Pattern = '\.cqrs\.query\b'; Replacement = '.CQRS.Query' }
@{ Pattern = '\.cqrs\.behaviors\b'; Replacement = '.CQRS.Behaviors' } @{ Pattern = '\.cqrs\.behaviors\b'; Replacement = '.CQRS.Behaviors' }
@{ Pattern = '\.cqrs\b'; Replacement = '.CQRS' } @{ Pattern = '\.cqrs\b'; Replacement = '.CQRS' }
# 其他嵌套命名空间 # 其他嵌套命名空间
@{ Pattern = '\.coroutine\.instructions\b'; Replacement = '.Coroutine.Instructions' } @{ Pattern = '\.coroutine\.instructions\b'; Replacement = '.Coroutine.Instructions' }
@{ Pattern = '\.coroutine\.extensions\b'; Replacement = '.Coroutine.Extensions' } @{ Pattern = '\.coroutine\.extensions\b'; Replacement = '.Coroutine.Extensions' }
@{ Pattern = '\.coroutine\b'; Replacement = '.Coroutine' } @{ Pattern = '\.coroutine\b'; Replacement = '.Coroutine' }
@{ Pattern = '\.events\.filters\b'; Replacement = '.Events.Filters' } @{ Pattern = '\.events\.filters\b'; Replacement = '.Events.Filters' }
@{ Pattern = '\.events\b'; Replacement = '.Events' } @{ Pattern = '\.events\b'; Replacement = '.Events' }
@{ Pattern = '\.logging\.appenders\b'; Replacement = '.Logging.Appenders' } @{ Pattern = '\.logging\.appenders\b'; Replacement = '.Logging.Appenders' }
@{ Pattern = '\.logging\.filters\b'; Replacement = '.Logging.Filters' } @{ Pattern = '\.logging\.filters\b'; Replacement = '.Logging.Filters' }
@{ Pattern = '\.logging\.formatters\b'; Replacement = '.Logging.Formatters' } @{ Pattern = '\.logging\.formatters\b'; Replacement = '.Logging.Formatters' }
@{ Pattern = '\.logging\b'; Replacement = '.Logging' } @{ Pattern = '\.logging\b'; Replacement = '.Logging' }
@{ Pattern = '\.functional\.async\b'; Replacement = '.Functional.Async' } @{ Pattern = '\.functional\.async\b'; Replacement = '.Functional.Async' }
@{ Pattern = '\.functional\.control\b'; Replacement = '.Functional.Control' } @{ Pattern = '\.functional\.control\b'; Replacement = '.Functional.Control' }
@{ Pattern = '\.functional\.functions\b'; Replacement = '.Functional.Functions' } @{ Pattern = '\.functional\.functions\b'; Replacement = '.Functional.Functions' }
@{ Pattern = '\.functional\.pipe\b'; Replacement = '.Functional.Pipe' } @{ Pattern = '\.functional\.pipe\b'; Replacement = '.Functional.Pipe' }
@{ Pattern = '\.functional\.result\b'; Replacement = '.Functional.Result' } @{ Pattern = '\.functional\.result\b'; Replacement = '.Functional.Result' }
@{ Pattern = '\.functional\b'; Replacement = '.Functional' } @{ Pattern = '\.functional\b'; Replacement = '.Functional' }
@{ Pattern = '\.services\.modules\b'; Replacement = '.Services.Modules' } @{ Pattern = '\.services\.modules\b'; Replacement = '.Services.Modules' }
@{ Pattern = '\.services\b'; Replacement = '.Services' } @{ Pattern = '\.services\b'; Replacement = '.Services' }
# 单层命名空间 # 单层命名空间
@{ Pattern = '\.architecture\b'; Replacement = '.Architecture' } @{ Pattern = '\.architecture\b'; Replacement = '.Architecture' }
@{ Pattern = '\.bases\b'; Replacement = '.Bases' } @{ Pattern = '\.bases\b'; Replacement = '.Bases' }
@{ Pattern = '\.command\b'; Replacement = '.Command' } @{ Pattern = '\.command\b'; Replacement = '.Command' }
@{ Pattern = '\.configuration\b'; Replacement = '.Configuration' } @{ Pattern = '\.configuration\b'; Replacement = '.Configuration' }
@{ Pattern = '\.constants\b'; Replacement = '.Constants' } @{ Pattern = '\.constants\b'; Replacement = '.Constants' }
@{ Pattern = '\.data\b'; Replacement = '.Data' } @{ Pattern = '\.data\b'; Replacement = '.Data' }
@{ Pattern = '\.enums\b'; Replacement = '.Enums' } @{ Pattern = '\.enums\b'; Replacement = '.Enums' }
@{ Pattern = '\.environment\b'; Replacement = '.Environment' } @{ Pattern = '\.environment\b'; Replacement = '.Environment' }
@{ Pattern = '\.extensions\b'; Replacement = '.Extensions' } @{ Pattern = '\.extensions\b'; Replacement = '.Extensions' }
@{ Pattern = '\.internals\b'; Replacement = '.Internals' } @{ Pattern = '\.internals\b'; Replacement = '.Internals' }
@{ Pattern = '\.ioc\b'; Replacement = '.IoC' } @{ Pattern = '\.ioc\b'; Replacement = '.IoC' }
@{ Pattern = '\.lifecycle\b'; Replacement = '.Lifecycle' } @{ Pattern = '\.lifecycle\b'; Replacement = '.Lifecycle' }
@{ Pattern = '\.model\b'; Replacement = '.Model' } @{ Pattern = '\.model\b'; Replacement = '.Model' }
@{ Pattern = '\.pause\b'; Replacement = '.Pause' } @{ Pattern = '\.pause\b'; Replacement = '.Pause' }
@{ Pattern = '\.pool\b'; Replacement = '.Pool' } @{ Pattern = '\.pool\b'; Replacement = '.Pool' }
@{ Pattern = '\.properties\b'; Replacement = '.Properties' } @{ Pattern = '\.properties\b'; Replacement = '.Properties' }
@{ Pattern = '\.property\b'; Replacement = '.Property' } @{ Pattern = '\.property\b'; Replacement = '.Property' }
@{ Pattern = '\.query\b'; Replacement = '.Query' } @{ Pattern = '\.query\b'; Replacement = '.Query' }
@{ Pattern = '\.registries\b'; Replacement = '.Registries' } @{ Pattern = '\.registries\b'; Replacement = '.Registries' }
@{ Pattern = '\.resource\b'; Replacement = '.Resource' } @{ Pattern = '\.resource\b'; Replacement = '.Resource' }
@{ Pattern = '\.rule\b'; Replacement = '.Rule' } @{ Pattern = '\.rule\b'; Replacement = '.Rule' }
@{ Pattern = '\.serializer\b'; Replacement = '.Serializer' } @{ Pattern = '\.serializer\b'; Replacement = '.Serializer' }
@{ Pattern = '\.state\b'; Replacement = '.State' } @{ Pattern = '\.state\b'; Replacement = '.State' }
@{ Pattern = '\.storage\b'; Replacement = '.Storage' } @{ Pattern = '\.storage\b'; Replacement = '.Storage' }
@{ Pattern = '\.system\b'; Replacement = '.System' } @{ Pattern = '\.system\b'; Replacement = '.System' }
@{ Pattern = '\.time\b'; Replacement = '.Time' } @{ Pattern = '\.time\b'; Replacement = '.Time' }
@{ Pattern = '\.utility\b'; Replacement = '.Utility' } @{ Pattern = '\.utility\b'; Replacement = '.Utility' }
@{ Pattern = '\.versioning\b'; Replacement = '.Versioning' } @{ Pattern = '\.versioning\b'; Replacement = '.Versioning' }
) )
foreach ($file in $csFiles) { foreach ($file in $csFiles) {
$content = Get-Content $file.FullName -Raw $content = Get-Content $file.FullName -Raw
$originalContent = $content $originalContent = $content
$fileReplacements = 0 $fileReplacements = 0
foreach ($rule in $namespaceRules) { foreach ($rule in $namespaceRules) {
$matches = [regex]::Matches($content, $rule.Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) $matches = [regex]::Matches($content, $rule.Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($matches.Count -gt 0) { if ($matches.Count -gt 0) {
$content = [regex]::Replace($content, $rule.Pattern, $rule.Replacement, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) $content = [regex]::Replace($content, $rule.Pattern, $rule.Replacement, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$fileReplacements += $matches.Count $fileReplacements += $matches.Count
} }
} }
if ($content -ne $originalContent) { if ($content -ne $originalContent) {
if ($DryRun) { if ($DryRun) {
Write-Info "[DRY RUN] 更新文件: $($file.FullName) ($fileReplacements 处替换)" Write-Info "[DRY RUN] 更新文件: $($file.FullName) ($fileReplacements 处替换)"
} else { } else {
Set-Content -Path $file.FullName -Value $content -NoNewline Set-Content -Path $file.FullName -Value $content -NoNewline
$updatedFiles++ $updatedFiles++
$totalReplacements += $fileReplacements $totalReplacements += $fileReplacements
Write-Info "更新: $($file.Name) ($fileReplacements 处替换)" Write-Info "更新: $($file.Name) ($fileReplacements 处替换)"
} }
} }
} }
if (-not $DryRun) { if (-not $DryRun) {
Write-Info "提交命名空间更新" Write-Info "提交命名空间更新"
git add -A git add -A
git commit -m "refactor: 更新所有命名空间为 PascalCase" git commit -m "refactor: 更新所有命名空间为 PascalCase"
} }
Write-Success "阶段 2 完成: 更新了 $updatedFiles 个文件,共 $totalReplacements 处替换" Write-Success "阶段 2 完成: 更新了 $updatedFiles 个文件,共 $totalReplacements 处替换"
} }
# 阶段 3: 文档更新 # 阶段 3: 文档更新
function Invoke-DocumentationUpdate { function Invoke-DocumentationUpdate {
Write-Info "=== 阶段 3: 文档更新 ===" Write-Info "=== 阶段 3: 文档更新 ==="
$mdFiles = Get-ChildItem -Path $rootDir -Filter "*.md" -Recurse | $mdFiles = Get-ChildItem -Path $rootDir -Filter "*.md" -Recurse |
Where-Object { $_.FullName -notmatch "\\node_modules\\|\\bin\\|\\obj\\" } Where-Object { $_.FullName -notmatch "\\node_modules\\|\\bin\\|\\obj\\" }
Write-Info "找到 $($mdFiles.Count) 个 Markdown 文件" Write-Info "找到 $($mdFiles.Count) 个 Markdown 文件"
$updatedFiles = 0 $updatedFiles = 0
$totalReplacements = 0 $totalReplacements = 0
# 使用与阶段 2 相同的替换规则 # 使用与阶段 2 相同的替换规则
$namespaceRules = @( $namespaceRules = @(
@{ Pattern = '\.cqrs\.notification\b'; Replacement = '.CQRS.Notification' } @{ Pattern = '\.cqrs\.notification\b'; Replacement = '.CQRS.Notification' }
@{ Pattern = '\.cqrs\.command\b'; Replacement = '.CQRS.Command' } @{ Pattern = '\.cqrs\.command\b'; Replacement = '.CQRS.Command' }
@{ Pattern = '\.cqrs\.request\b'; Replacement = '.CQRS.Request' } @{ Pattern = '\.cqrs\.request\b'; Replacement = '.CQRS.Request' }
@{ Pattern = '\.cqrs\.query\b'; Replacement = '.CQRS.Query' } @{ Pattern = '\.cqrs\.query\b'; Replacement = '.CQRS.Query' }
@{ Pattern = '\.cqrs\.behaviors\b'; Replacement = '.CQRS.Behaviors' } @{ Pattern = '\.cqrs\.behaviors\b'; Replacement = '.CQRS.Behaviors' }
@{ Pattern = '\.cqrs\b'; Replacement = '.CQRS' } @{ Pattern = '\.cqrs\b'; Replacement = '.CQRS' }
@{ Pattern = '\.coroutine\.instructions\b'; Replacement = '.Coroutine.Instructions' } @{ Pattern = '\.coroutine\.instructions\b'; Replacement = '.Coroutine.Instructions' }
@{ Pattern = '\.coroutine\.extensions\b'; Replacement = '.Coroutine.Extensions' } @{ Pattern = '\.coroutine\.extensions\b'; Replacement = '.Coroutine.Extensions' }
@{ Pattern = '\.coroutine\b'; Replacement = '.Coroutine' } @{ Pattern = '\.coroutine\b'; Replacement = '.Coroutine' }
@{ Pattern = '\.events\.filters\b'; Replacement = '.Events.Filters' } @{ Pattern = '\.events\.filters\b'; Replacement = '.Events.Filters' }
@{ Pattern = '\.events\b'; Replacement = '.Events' } @{ Pattern = '\.events\b'; Replacement = '.Events' }
@{ Pattern = '\.logging\.appenders\b'; Replacement = '.Logging.Appenders' } @{ Pattern = '\.logging\.appenders\b'; Replacement = '.Logging.Appenders' }
@{ Pattern = '\.logging\.filters\b'; Replacement = '.Logging.Filters' } @{ Pattern = '\.logging\.filters\b'; Replacement = '.Logging.Filters' }
@{ Pattern = '\.logging\.formatters\b'; Replacement = '.Logging.Formatters' } @{ Pattern = '\.logging\.formatters\b'; Replacement = '.Logging.Formatters' }
@{ Pattern = '\.logging\b'; Replacement = '.Logging' } @{ Pattern = '\.logging\b'; Replacement = '.Logging' }
@{ Pattern = '\.functional\.async\b'; Replacement = '.Functional.Async' } @{ Pattern = '\.functional\.async\b'; Replacement = '.Functional.Async' }
@{ Pattern = '\.functional\.control\b'; Replacement = '.Functional.Control' } @{ Pattern = '\.functional\.control\b'; Replacement = '.Functional.Control' }
@{ Pattern = '\.functional\.functions\b'; Replacement = '.Functional.Functions' } @{ Pattern = '\.functional\.functions\b'; Replacement = '.Functional.Functions' }
@{ Pattern = '\.functional\.pipe\b'; Replacement = '.Functional.Pipe' } @{ Pattern = '\.functional\.pipe\b'; Replacement = '.Functional.Pipe' }
@{ Pattern = '\.functional\.result\b'; Replacement = '.Functional.Result' } @{ Pattern = '\.functional\.result\b'; Replacement = '.Functional.Result' }
@{ Pattern = '\.functional\b'; Replacement = '.Functional' } @{ Pattern = '\.functional\b'; Replacement = '.Functional' }
@{ Pattern = '\.services\.modules\b'; Replacement = '.Services.Modules' } @{ Pattern = '\.services\.modules\b'; Replacement = '.Services.Modules' }
@{ Pattern = '\.services\b'; Replacement = '.Services' } @{ Pattern = '\.services\b'; Replacement = '.Services' }
@{ Pattern = '\.architecture\b'; Replacement = '.Architecture' } @{ Pattern = '\.architecture\b'; Replacement = '.Architecture' }
@{ Pattern = '\.bases\b'; Replacement = '.Bases' } @{ Pattern = '\.bases\b'; Replacement = '.Bases' }
@{ Pattern = '\.command\b'; Replacement = '.Command' } @{ Pattern = '\.command\b'; Replacement = '.Command' }
@{ Pattern = '\.configuration\b'; Replacement = '.Configuration' } @{ Pattern = '\.configuration\b'; Replacement = '.Configuration' }
@{ Pattern = '\.constants\b'; Replacement = '.Constants' } @{ Pattern = '\.constants\b'; Replacement = '.Constants' }
@{ Pattern = '\.data\b'; Replacement = '.Data' } @{ Pattern = '\.data\b'; Replacement = '.Data' }
@{ Pattern = '\.enums\b'; Replacement = '.Enums' } @{ Pattern = '\.enums\b'; Replacement = '.Enums' }
@{ Pattern = '\.environment\b'; Replacement = '.Environment' } @{ Pattern = '\.environment\b'; Replacement = '.Environment' }
@{ Pattern = '\.extensions\b'; Replacement = '.Extensions' } @{ Pattern = '\.extensions\b'; Replacement = '.Extensions' }
@{ Pattern = '\.internals\b'; Replacement = '.Internals' } @{ Pattern = '\.internals\b'; Replacement = '.Internals' }
@{ Pattern = '\.ioc\b'; Replacement = '.IoC' } @{ Pattern = '\.ioc\b'; Replacement = '.IoC' }
@{ Pattern = '\.lifecycle\b'; Replacement = '.Lifecycle' } @{ Pattern = '\.lifecycle\b'; Replacement = '.Lifecycle' }
@{ Pattern = '\.model\b'; Replacement = '.Model' } @{ Pattern = '\.model\b'; Replacement = '.Model' }
@{ Pattern = '\.pause\b'; Replacement = '.Pause' } @{ Pattern = '\.pause\b'; Replacement = '.Pause' }
@{ Pattern = '\.pool\b'; Replacement = '.Pool' } @{ Pattern = '\.pool\b'; Replacement = '.Pool' }
@{ Pattern = '\.properties\b'; Replacement = '.Properties' } @{ Pattern = '\.properties\b'; Replacement = '.Properties' }
@{ Pattern = '\.property\b'; Replacement = '.Property' } @{ Pattern = '\.property\b'; Replacement = '.Property' }
@{ Pattern = '\.query\b'; Replacement = '.Query' } @{ Pattern = '\.query\b'; Replacement = '.Query' }
@{ Pattern = '\.registries\b'; Replacement = '.Registries' } @{ Pattern = '\.registries\b'; Replacement = '.Registries' }
@{ Pattern = '\.resource\b'; Replacement = '.Resource' } @{ Pattern = '\.resource\b'; Replacement = '.Resource' }
@{ Pattern = '\.rule\b'; Replacement = '.Rule' } @{ Pattern = '\.rule\b'; Replacement = '.Rule' }
@{ Pattern = '\.serializer\b'; Replacement = '.Serializer' } @{ Pattern = '\.serializer\b'; Replacement = '.Serializer' }
@{ Pattern = '\.state\b'; Replacement = '.State' } @{ Pattern = '\.state\b'; Replacement = '.State' }
@{ Pattern = '\.storage\b'; Replacement = '.Storage' } @{ Pattern = '\.storage\b'; Replacement = '.Storage' }
@{ Pattern = '\.system\b'; Replacement = '.System' } @{ Pattern = '\.system\b'; Replacement = '.System' }
@{ Pattern = '\.time\b'; Replacement = '.Time' } @{ Pattern = '\.time\b'; Replacement = '.Time' }
@{ Pattern = '\.utility\b'; Replacement = '.Utility' } @{ Pattern = '\.utility\b'; Replacement = '.Utility' }
@{ Pattern = '\.versioning\b'; Replacement = '.Versioning' } @{ Pattern = '\.versioning\b'; Replacement = '.Versioning' }
) )
foreach ($file in $mdFiles) { foreach ($file in $mdFiles) {
$content = Get-Content $file.FullName -Raw $content = Get-Content $file.FullName -Raw
$originalContent = $content $originalContent = $content
$fileReplacements = 0 $fileReplacements = 0
foreach ($rule in $namespaceRules) { foreach ($rule in $namespaceRules) {
$matches = [regex]::Matches($content, $rule.Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) $matches = [regex]::Matches($content, $rule.Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($matches.Count -gt 0) { if ($matches.Count -gt 0) {
$content = [regex]::Replace($content, $rule.Pattern, $rule.Replacement, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) $content = [regex]::Replace($content, $rule.Pattern, $rule.Replacement, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$fileReplacements += $matches.Count $fileReplacements += $matches.Count
} }
} }
if ($content -ne $originalContent) { if ($content -ne $originalContent) {
if ($DryRun) { if ($DryRun) {
Write-Info "[DRY RUN] 更新文档: $($file.FullName) ($fileReplacements 处替换)" Write-Info "[DRY RUN] 更新文档: $($file.FullName) ($fileReplacements 处替换)"
} else { } else {
Set-Content -Path $file.FullName -Value $content -NoNewline Set-Content -Path $file.FullName -Value $content -NoNewline
$updatedFiles++ $updatedFiles++
$totalReplacements += $fileReplacements $totalReplacements += $fileReplacements
Write-Info "更新: $($file.Name) ($fileReplacements 处替换)" Write-Info "更新: $($file.Name) ($fileReplacements 处替换)"
} }
} }
} }
if (-not $DryRun) { if (-not $DryRun) {
Write-Info "提交文档更新" Write-Info "提交文档更新"
git add -A git add -A
git commit -m "docs: 更新文档中的命名空间为 PascalCase" git commit -m "docs: 更新文档中的命名空间为 PascalCase"
} }
Write-Success "阶段 3 完成: 更新了 $updatedFiles 个文档,共 $totalReplacements 处替换" Write-Success "阶段 3 完成: 更新了 $updatedFiles 个文档,共 $totalReplacements 处替换"
} }
# 阶段 4: 验证 # 阶段 4: 验证
function Invoke-Verification { function Invoke-Verification {
Write-Info "=== 阶段 4: 验证 ===" Write-Info "=== 阶段 4: 验证 ==="
# 1. 编译验证 # 1. 编译验证
Write-Info "1. 编译验证..." Write-Info "1. 编译验证..."
if ($DryRun) { if ($DryRun) {
Write-Info "[DRY RUN] dotnet build" Write-Info "[DRY RUN] dotnet build"
} else { } else {
Push-Location $rootDir Push-Location $rootDir
try { try {
dotnet build --no-restore dotnet build --no-restore
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) {
Write-Success "编译成功" Write-Success "编译成功"
} else { } else {
Write-Error "编译失败" Write-Error "编译失败"
throw "编译失败" throw "编译失败"
} }
} finally { } finally {
Pop-Location Pop-Location
} }
} }
# 2. 测试验证 # 2. 测试验证
if (-not $SkipTests) { if (-not $SkipTests) {
Write-Info "2. 测试验证..." Write-Info "2. 测试验证..."
if ($DryRun) { if ($DryRun) {
Write-Info "[DRY RUN] dotnet test" Write-Info "[DRY RUN] dotnet test"
} else { } else {
Push-Location $rootDir Push-Location $rootDir
try { try {
dotnet test --no-build dotnet test --no-build
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) {
Write-Success "所有测试通过" Write-Success "所有测试通过"
} else { } else {
Write-Error "测试失败" Write-Error "测试失败"
throw "测试失败" throw "测试失败"
} }
} finally { } finally {
Pop-Location Pop-Location
} }
} }
} else { } else {
Write-Warning "跳过测试验证" Write-Warning "跳过测试验证"
} }
# 3. 检查残留的小写命名空间 # 3. 检查残留的小写命名空间
Write-Info "3. 检查残留的小写命名空间..." Write-Info "3. 检查残留的小写命名空间..."
$csFiles = Get-ChildItem -Path $rootDir -Filter "*.cs" -Recurse | $csFiles = Get-ChildItem -Path $rootDir -Filter "*.cs" -Recurse |
Where-Object { $_.FullName -notmatch "\\bin\\|\\obj\\|\\Generated\\" } Where-Object { $_.FullName -notmatch "\\bin\\|\\obj\\|\\Generated\\" }
$lowercasePatterns = @( $lowercasePatterns = @(
'\.architecture\b', '\.command\b', '\.configuration\b', '\.coroutine\b', '\.architecture\b', '\.command\b', '\.configuration\b', '\.coroutine\b',
'\.cqrs\b', '\.events\b', '\.extensions\b', '\.functional\b', '\.cqrs\b', '\.events\b', '\.extensions\b', '\.functional\b',
'\.ioc\b', '\.logging\b', '\.model\b', '\.query\b', '\.ioc\b', '\.logging\b', '\.model\b', '\.query\b',
'\.resource\b', '\.state\b', '\.system\b', '\.utility\b' '\.resource\b', '\.state\b', '\.system\b', '\.utility\b'
) )
$foundIssues = @() $foundIssues = @()
foreach ($file in $csFiles) { foreach ($file in $csFiles) {
$content = Get-Content $file.FullName -Raw $content = Get-Content $file.FullName -Raw
foreach ($pattern in $lowercasePatterns) { foreach ($pattern in $lowercasePatterns) {
if ($content -match $pattern) { if ($content -match $pattern) {
$foundIssues += "$($file.FullName): 找到小写命名空间 $pattern" $foundIssues += "$($file.FullName): 找到小写命名空间 $pattern"
} }
} }
} }
if ($foundIssues.Count -gt 0) { if ($foundIssues.Count -gt 0) {
Write-Warning "发现 $($foundIssues.Count) 个残留的小写命名空间:" Write-Warning "发现 $($foundIssues.Count) 个残留的小写命名空间:"
$foundIssues | ForEach-Object { Write-Warning $_ } $foundIssues | ForEach-Object { Write-Warning $_ }
} else { } else {
Write-Success "未发现残留的小写命名空间" Write-Success "未发现残留的小写命名空间"
} }
Write-Success "阶段 4 完成: 验证通过" Write-Success "阶段 4 完成: 验证通过"
} }
# 主执行逻辑 # 主执行逻辑
try { try {
Write-Info "GFramework 命名空间重构脚本" Write-Info "GFramework 命名空间重构脚本"
Write-Info "工作目录: $rootDir" Write-Info "工作目录: $rootDir"
Write-Info "配置文件: $mappingFile" Write-Info "配置文件: $mappingFile"
if ($DryRun) { if ($DryRun) {
Write-Warning "*** 干运行模式 - 不会执行实际操作 ***" Write-Warning "*** 干运行模式 - 不会执行实际操作 ***"
} }
if (-not (Test-Path $mappingFile)) { if (-not (Test-Path $mappingFile)) {
Write-Error "配置文件不存在: $mappingFile" Write-Error "配置文件不存在: $mappingFile"
exit 1 exit 1
} }
switch ($Phase) { switch ($Phase) {
"1" { Invoke-FolderRename } "1" { Invoke-FolderRename }
"2" { Invoke-NamespaceUpdate } "2" { Invoke-NamespaceUpdate }
"3" { Invoke-DocumentationUpdate } "3" { Invoke-DocumentationUpdate }
"4" { Invoke-Verification } "4" { Invoke-Verification }
"All" { "All" {
Invoke-FolderRename Invoke-FolderRename
Invoke-NamespaceUpdate Invoke-NamespaceUpdate
Invoke-DocumentationUpdate Invoke-DocumentationUpdate
Invoke-Verification Invoke-Verification
} }
} }
Write-Success "=== 重构完成 ===" Write-Success "=== 重构完成 ==="
} catch { } catch {
Write-Error "重构失败: $_" Write-Error "重构失败: $_"
Write-Error $_.ScriptStackTrace Write-Error $_.ScriptStackTrace
exit 1 exit 1
} }