diff --git a/DESIGN.md b/DESIGN.md index 6d72695..a4b8b76 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -45,11 +45,26 @@ ShrinkSDK 是以 Unity Package Manager 包为发布边界的 SDK 单仓库,不 - 服务器模板文件由哈希清单管理。重新生成前先验证全部现有文件仍等于上次生成版本;任何人工修改都会让整个复制阶段在写入前中止。 - 业务扩展应放在非生成文件或独立模块中,不通过修改生成产物维持。 +### 2.6 查询、命令与事实事件分离 + +- 查询方只依赖完成当前读取所需的窄契约;只读消费者不取得包含写操作的完整服务对象。 +- 改变状态的跨模块操作使用 Command、RPC 或明确的应用 facade,并返回成功、失败、拒绝、取消等结构化结果。 +- EventBus 负责广播已发生的事实、通知或具有明确结果合同的异步事件,不用无结果事件绕开权限、错误和调用顺序。 +- 静态 facade 只能转发到显式绑定的实例;绑定和解绑必须按实例配对,不能退化为任意类型的全局 Service Locator。 + +### 2.7 架构约束进入自动化门禁 + +- 包版本、依赖方向、asmdef 可见性、生成内容所有权和卸载回滚等约束必须尽量由测试或验证脚本执行。 +- 优先检查编译期类型、Cecil/程序集元数据、`package.json`/asmdef 依赖图和真实 UPM 消费结果。 +- `Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1 -GraphOnly` 必须拒绝内部包循环、版本漂移以及普通主包反向依赖 `*-integration-*`;Starter 可作为显式组合根聚合 Integration 包。 +- 源码字符串断言只用于短期迁移门禁;稳定规则不得长期依赖格式、局部变量名或实现文本。 +- 任何测试通过都必须标明验证层级;源码审计、CLI 编译、Unity Test Runner、Play Mode 和 Player Build 不能互相替代。 + ## 3. 仓库结构 ```text ShrinkSDK/ -|-- Assets/Modules/ 19 个根仓库直接跟踪的 UPM 包 +|-- Assets/Modules/ 20 个根仓库直接跟踪的 UPM 包 |-- Assets/Scenes/ 示例与验收场景 |-- Assets/Resources/ 当前应用配置与组合 Profile |-- GeneratedServers/ 独立 .NET 宿主、生成合同与烟测工程 @@ -66,17 +81,18 @@ ShrinkSDK/ | 包 | 版本 | 职责 | |---|---:|---| -| `com.cneicy.shrink-eventbus` | 1.3.0 | 类型安全事件总线、优先级派发、编译期自动注册 | +| `com.cneicy.shrink-eventbus` | 2.0.0 | 单一事件模型、多 Bus、生成特性订阅、UniTask 调度与零 GC 热路径 | +| `com.cneicy.shrink-eventbus-entities` | 0.1.0 | ShrinkEventBus 的 ECS/Burst NativeQueue writer 与 playback 适配 | | `com.cneicy.shrink-datasaver` | 2.2.0 | 多槽位存档、设置、迁移、加密、原子写入与备份 | | `com.cneicy.shrink-command` | 0.2.0 | 路径式命令、权限与同步/异步执行 | | `com.cneicy.shrink-network` | 0.2.0 | 消息、RPC、权限、诊断、TCP/KCP/Loopback 与服务器生成 | -| `com.cneicy.shrink-mod-framework` | 0.1.0 | 模组发现、依赖、生命周期、外部 DLL revision 与 Harmony lease | +| `com.cneicy.shrink-mod-framework` | 0.2.0 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease | | `com.cneicy.shrink-tutorial` | 0.1.0 | 数据驱动引导、遮罩、锚点、触发与持久化 | | `com.cneicy.shrink-context-core` | 0.1.0 | 可逆效应、coeffect、fiber、声明式 loader 与诊断 | | `com.cneicy.shrink-app-core` | 0.1.1 | App 设置、服务门面、ClassicHost 兼容面与宿主协议 | | `com.cneicy.shrink-app-starter-basic` | 0.1.0 | 默认 Context 组合根、配置资产和示例入口 | | `com.cneicy.shrink-context-app-adapter` | 0.1.0 | ContextLoader 宿主、Profile/JSON、诊断与基准 | -| `com.cneicy.shrink-context-eventbus-adapter` | 0.1.0 | EventBus 注册/订阅的可逆效应包装 | +| `com.cneicy.shrink-context-eventbus-adapter` | 0.1.0 | EventBus 生成绑定的可逆 EffectAttach 包装 | | `com.cneicy.shrink-datasaver-integration-eventbus` | 2.1.0 | DataSaver 事件桥 | | `com.cneicy.shrink-datasaver-integration-app` | 0.1.0 | DataSaver App installer/原生 Context 组件 | | `com.cneicy.shrink-command-integration-eventbus` | 0.1.1 | 命令请求与生命周期事件桥 | @@ -145,7 +161,7 @@ Cordis 迁移的阶段 0 至阶段 5 已进入当前架构,不再是待办计 ### 7.1 EventBus -静态 `EventBus` 委托给 `IShrinkEventBus` 实例。监听按 phase 和数字优先级稳定归并;订阅句柄可单独释放,实例注册可整体撤销。Context 适配器把二者登记为效应,保证 fiber 卸载自动退订。 +静态 `EventBus` 委托给命名 `IShrinkEventBus` 实例。Bus Key 表达 Game/Scene/Mod/World/Server 所有权,Bus Options 固定 Inline/MainThread/DedicatedThread/TaskPool 调度。事件只实现 `IShrinkEvent`,取消和结果由能力接口声明;注册只允许 `[ShrinkEventSubscriber]` / `[ShrinkSubscribe]` 生成绑定与 `Attach/Dispose` 生命周期。Context 适配器把绑定登记为可逆效应,保证 fiber 卸载自动退订。 ### 7.2 DataSaver @@ -172,13 +188,15 @@ Cordis 迁移的阶段 0 至阶段 5 已进入当前架构,不再是待办计 `ShrinkModContextHost` 将模组生命周期纳入 Context apply。热替换单位是 `ModId + revision`;外部 DLL 用 SHA-256 标识 revision,失败或损坏 revision 不会成为 current,旧组合会恢复。Harmony、Registry 与 Network handler 使用可逆 lease 清理。 +`ShrinkModRegistry` 的基础项只能通过 `:` namespace 注册;覆盖必须指向已有基础项,按显式优先级选出 winner,同目标同优先级直接拒绝。基础项和覆盖项都记录 owner,卸载时自动撤回并恢复下一个覆盖或基础值。注册表不采用全局冻结,因为 revision 热替换与事务回滚要求状态可撤回。 + Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效应,并通过 `ShrinkModDiagnostics` 暴露 current/history、累计载入字节和软阈值,达到阈值时建议 Domain Reload 或进程重启。 ## 8. 编译期注册与服务器生成 ### 8.1 Unity 运行时注册 -`ShrinkShared.CodeGen/Editor/ShrinkRegistryILPostProcessor.cs` 为引用方程序集生成 EventBus、Command、Network 和 App 注册信息。运行时只消费程序集级注册表;外部 DLL 和运行时对象使用实例注册入口。核心程序集自身不被共享织入器回写,避免程序集自引用。 +`ShrinkEventBus/CodeGen/Editor/EventBusILPostProcessor.cs` 为引用方程序集生成实例直接调用绑定和静态模块初始化注册,不运行时枚举方法。`ShrinkShared.CodeGen/Editor/ShrinkRegistryILPostProcessor.cs` 继续负责 Command、Network 和 App 注册表。外部 Mod DLL 必须携带 EventBus 生成合同;没有合同的生产 DLL 不自动反射注册。 ### 8.2 独立服务器生成 @@ -200,10 +218,10 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效 一次涉及包边界、生成器或组合根的变更至少经过: -1. 内部版本一致性检查:全部 `com.cneicy.*` 依赖等于仓库包版本。 +1. 内部包图检查:全部 `com.cneicy.*` 依赖版本一致、无循环,普通主包不反向依赖 Integration 包。 2. Unity 编译:目标 asmdef 和根项目无编译错误。 3. EditMode 测试:功能测试、Context 生命周期、语义扫描和模板覆写保护。 -4. 真实 UPM 消费:`Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1` 创建仓库外形态的临时 Unity 工程,通过 `file:` 安装全部 19 个包,启用 testables,验证包注册、程序集加载并运行 EditMode 测试。 +4. 真实 UPM 消费:`Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1` 创建仓库外形态的临时 Unity 工程,通过 `file:` 安装全部 20 个包,启用 testables,验证包注册、程序集加载并运行 EditMode 测试。 5. 独立宿主:生成工程与 RuntimeSmoke 按变更范围构建或运行。 6. 涉及真实生命周期时,仍需在目标场景执行 Play Mode 验收;源码检查和 EditMode 不能替代该路径。 @@ -218,6 +236,17 @@ Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效 ## 11. 文档治理 +### 11.1 外部架构审计与 EventBus 统一模型 + +- `Docs/JustAnyProjectArchitectureAudit.md` 记录 `D:\UnityBuilds\justanyproject` 的当前框架组织、可迁移经验、不可复制部分和证据边界;其结论已收敛为本文 2.6、2.7 与 Mod registry 约束。 +- ShrinkEventBus 采用一个事件模型、多个命名 Bus 实例;Bus Key 负责生命周期与隔离,Bus Options 负责 Inline/MainThread/DedicatedThread/TaskPool 调度。 +- Unity 中可用 UniTask 时,EventBus 异步调度和 handler 优先使用 UniTask;纯 .NET 使用 ValueTask/同步实现。 +- 生产注册为特性声明 + 生成强类型绑定;旧 EventBase/Register/Subscribe/Trigger 与反射 handler 通道已删除。 +- ECS/Burst 通过 NativeQueue writer/playback 进入对应 World Bus,不直接调用托管事件 handler。 +- EventBus 调试窗口使用 UI Toolkit,以按需详细采样记录全局 Bus 的调度、线程、结果和耗时;窗口暂停或关闭时观测回调为空,独立 Host 始终不被全局诊断或 Network bridge 捕获。 +- 2026-08-24 Unity Play Mode 最终三轮中位数:8 handler class 25.32M、8 handler struct 35.02M、30 handler 10.82M ops/s,同步 struct Post 为 0 B/10,000,000 次;详见包 README。 +- Windows x64 Development IL2CPP Player 已完成构建验证;Standalone 脚本后端随后恢复为 Mono。 + - 当前架构只更新本文。 - 包级使用方式和 API 示例放在各包 README。 - `NETWORK_PITFALLS.md` 记录实现经验,不描述当前模块清单。 diff --git a/Docs/JustAnyProjectArchitectureAudit.md b/Docs/JustAnyProjectArchitectureAudit.md new file mode 100644 index 0000000..06e41bd --- /dev/null +++ b/Docs/JustAnyProjectArchitectureAudit.md @@ -0,0 +1,188 @@ +# justanyproject 代码框架组织审计 + +> 审计日期:2026-08-24 +> 源项目:`D:\UnityBuilds\justanyproject` +> 源基线:`main` / `b336715`,工作树干净,Unity `2022.3.62f3` +> 证据范围:`AGENTS.md`、`Assets/Scripts`、`Assets/Tests`、asmdef、`Packages/manifest.json` 和相关模块文档。本文是源码与工程结构审计,不代表重新执行过源项目的 Unity 编译、EditMode、PlayMode 或运行时验收。 + +## 1. 当前组织概况 + +源项目的第一方运行时代码主要位于 `Assets/Scripts`:共 564 个 C# 文件,按 24 个业务目录组织。规模最大的三个目录是 `EndlessMode`(205)、`CombatRuntime`(82)和 `Bootstrap`(41)。 + +项目共有 12 个第一方 asmdef,其中大部分基础模块已有独立程序集;但 `SeedsKey.Game` 仍引用 28 个程序集,并承载大量未拆分目录。测试分为 EditMode 与 PlayMode 两个程序集,二者都直接引用多个生产程序集。 + +当前框架可以概括为: + +```text +Entry / AppBootstrapper + | +Bootstrap + AppCompositionRoot + | +全局 Manager / 领域系统 / Scene Composition Root + | +CombatRuntime / EndlessMode / StrategicMap / 其它业务模块 + | +ShrinkEventBus / ShrinkDataSaver / ShrinkNetwork / ShrinkModFramework +``` + +目录层面的领域划分已经清楚,但编译边界、对象所有权和运行时入口仍处于新旧模式并存状态:显式组合根、窄接口和结构化 facade 已出现,业务单例、静态入口和事件协调仍然大量存在。 + +## 2. 值得迁移的经验 + +### 2.1 显式 Composition Root + +`AppCompositionRoot` 和 `EndlessBattleCompositionRoot` 把关键依赖集中在可检查的绑定边界。后者通过序列化字段表达场景依赖,并提供 `ValidateRequiredBindings()` 汇总缺失项。这比在业务对象内部临时查找单例、场景对象或 prefab 子节点更容易审计。 + +`AppScope`、`SessionScope`、`SceneScope` 目前只是借用关系和已释放状态的标记:它们会拒绝失效父作用域,但不持有资源集合,也不会级联或反向释放对象。因此可迁移的是“显式作用域边界”,不是现有实现本身。 + +ShrinkSDK 的承载方式: + +- App 级组合继续由 `ShrinkAppLoaderBootstrapper`、Composition Profile 与 `ShrinkAppLoaderHost` 表达。 +- Session/Scene/Mod 生命周期继续映射为 `ShrinkContext` fiber、派生 context 和可逆 effect,不再增加第二套 Scope 容器。 +- 场景组件仍应使用显式序列化引用和独立校验结果,不允许运行时静默补建或猜路径。 + +### 2.2 命令、查询和事实事件分离 + +源项目正在把跨模块调用收敛为三类: + +- 查询依赖窄的只读接口,例如 `ISquadReadOnly`、`IInventorySlotReadOnly`。 +- 改变状态的操作经过 command facade,并返回 `Succeeded / Failed / Rejected` 等结构化结果。 +- EventBus 只广播已经发生的事实;请求事件仅作为有清理计划的兼容桥。 + +`GameSessionFlowFacade` 和 `SceneTransitionFlowFacade` 还处理了重复请求拒绝、异常转结果以及 `finally` 恢复运行状态。这些行为使调用方不必从日志或事件顺序猜测执行结果。 + +ShrinkSDK 的承载方式: + +- `ShrinkCommandExecutionResult`、Network RPC 结果、Context 事务诊断继续作为状态变更的明确结果面。 +- Context key 和 App service 应优先发布消费者真正需要的窄契约;只读消费者不应获得完整可写服务对象。 +- EventBus bridge 只传递事实、通知或明确的异步请求合同,不用事件绕开 Command/Network 的返回值、权限和失败语义。 + +### 2.3 Owner-aware 内容注册表 + +`ContentRegistry` 对内容 id、owner、覆盖优先级和冻结阶段有明确规则: + +- 新增内容必须使用 owner namespace。 +- 同目标、同优先级覆盖直接报冲突。 +- 覆盖目标必须存在。 +- 启动完成后冻结,运行阶段只读。 + +这套规则适合“启动时装载、运行时固定”的游戏内容。ShrinkModFramework 现已把其中可兼容热替换的部分落地:基础项强制使用 `:`,覆盖目标和优先级显式声明,同目标同优先级直接拒绝,owner 卸载时自动恢复下一个覆盖或基础项。 + +没有复制冻结模型。ShrinkModFramework 支持 revision 热替换和失败回滚,注册表必须随组件 effect 撤回;全局冻结会与此生命周期冲突。模组通过 `RegisterContent` / `OverrideContent` 写入,查询只暴露 `IReadOnlyShrinkModRegistry`,owner 由当前 `ShrinkModContext` 决定。 + +### 2.4 把架构约束写成门禁 + +源项目的 EditMode architecture gate 会检查: + +- 只读接口没有混入写操作。 +- 已迁移调用方没有重新引入 Service Locator。 +- command facade 返回结构化结果。 +- 事实事件只在状态变更成功后发布。 +- 兼容请求事件受 allowlist 约束。 + +方向正确,但部分测试通过读取 C# 源文本并匹配字符串实现,重命名、格式化或等价重构都可能导致误报。ShrinkSDK 应优先使用编译期类型检查、Cecil/程序集元数据、package.json/asmdef 图检查和真实 UPM 消费测试;只有无法从语义层验证的临时迁移约束才使用源码文本门禁。 + +### 2.5 文档按所有权分层 + +源项目使用总索引说明模块所有权和主流程,再由目录 README 说明模块内部细节,同时把高代价故障沉淀到 `KNOWN_PITFALLS.md`。这种结构比一份不断膨胀的全局文档更容易维护。 + +ShrinkSDK 继续保持: + +- `DESIGN.md` 只描述当前架构和跨包约束。 +- 包级用法、API 和限制放到各包 README。 +- 已完成迁移与旧地图进入 `Docs/Archive`。 +- 经验文档必须区分源码事实、工具验证和真实运行时验证。 + +## 3. 不应迁移的部分 + +### 3.1 大总程序集 + +`SeedsKey.Game` 的 28 个程序集引用和大量目录归属,使很多“模块”只有文件夹边界,没有独立编译边界。它适合渐进式游戏工程重构,不适合作为 SDK 包组织模板。 + +ShrinkSDK 必须继续以 UPM 包为发布边界、asmdef 为编译边界,主包不得反向依赖集成包。 + +### 3.2 业务单例与全局初始化入口 + +源项目仍有 32 个包含 `BaseManager` 的文件、10 个运行时自动初始化文件,以及大量静态 `Instance`/EventBus 调用。它们说明迁移需要兼容面,不说明 SDK 应复制这种全局所有权模型。 + +ShrinkSDK 的静态入口只能是默认实例 facade;真实生命周期由实例、Context fiber 和 effect 拥有。 + +### 3.3 字符串场景名和补救式 Bootstrap + +源项目的主流程依赖 `Entry -> MainMenu -> ...`、字符串场景名和 `DevBooter` 补齐孤立场景依赖。它们是具体游戏的流程合同,不属于通用 SDK。 + +SDK 可以提供组合、诊断和校验能力,但不应内置游戏场景名、玩法 ready 顺序或自动修复缺失场景对象。 + +### 3.4 游戏内容类型 + +存档槽、库存、法杖、战斗、房间、无尽模式和 Steam Workshop 的具体状态都应留在游戏层。SDK 只提供生命周期、注册、存储、传输和扩展机制。 + +## 4. 对 ShrinkSDK 的实际结论 + +这次审计没有发现需要从源项目复制的新容器或通用框架。ShrinkSDK 当前在包边界、可逆生命周期、热替换回滚、生成式注册和真实 UPM 消费验证上已经更严格。有效经验应表现为边界规则,而不是移植源项目实现: + +1. 在组合入口集中显式绑定并一次性报告缺失项。 +2. 查询依赖窄的只读能力;状态变更使用 command/RPC 并返回结构化结果。 +3. 事件表达事实,不作为无结果的跨模块命令通道。 +4. 可扩展注册表必须记录 owner,冲突要显式拒绝,卸载必须可逆。 +5. 架构规则进入自动化门禁,但优先验证程序集和包语义,避免长期依赖源码字符串测试。 +6. 文档中的数量、测试结果和运行时状态必须标注快照日期与证据来源。 + +上述规则已同步进入 `DESIGN.md`,并完成两项实现: + +- `ShrinkModFramework 0.2.0` 使用只读查询接口与 owner-bound 写命令,支持 namespace、优先级覆盖、冲突拒绝和卸载恢复。 +- UPM Consumer Validation 新增内部包循环和主包反向依赖 Integration 包的门禁,并支持 `-GraphOnly` 快速检查。 + +EventBus 2.0、Context、ModFramework 和 UPM Consumer Validation 继续作为这些规则的主要承载面,没有新增一套并行容器或 Service Locator。 + +## 5. 当前验证边界 + +- 已读取并对照源项目当前 git 基线、目录、asmdef、Composition、Modding 和 architecture gate 源码。 +- 已读取并对照 ShrinkSDK 当前 App、Context、Mod registry、包清单与 UPM 消费验证脚本。 +- 本次没有修改 Unity 序列化资产,也没有修改源项目 `justanyproject`。 +- ShrinkSDK Unity Editor 已确认 `productName=ShrinkSDK`,重新编译为 0 error / 0 warning。 +- `ShrinkModFramework.Tests` EditMode 为 13/13,通过 namespace、覆盖优先级、冲突、只读 API 边界和真实外部 DLL 热替换回滚。 +- `dotnet build ShrinkSDK.sln /m:1` 为 0 warning / 0 error。 +- UPM Consumer Validation 的 `-GraphOnly` 通过;仓库外临时 Unity 工程成功加载 20 个包和 42 个 asmdef。本次该轮使用 `-SkipTests`,包内 EditMode 覆盖由上述定向 Test Runner 承担。 +- 没有重新运行 `justanyproject` 测试、ShrinkSDK PlayMode、Player Build 或运行时场景;附录 A 的历史结果仍不是本次验证。 + +## 附录 A:既有 EventBus 2.0 落地记录 + +以下内容保留此前审计推动的 EventBus 2.0 设计与验证记录。它描述 ShrinkSDK 当前工作树中的既有改造,不是本次重新执行的结果。 + +### A.1 统一事件模型 + +ShrinkSDK 使用一个事件协议和多个命名 Bus 实例: + +```text +game -> Unity 主线程 +scene:id -> Scene/Context 生命周期 +mod:id -> 模组专属线程或宿主指定调度 +server -> Inline 或专属线程 +world:id -> ECS playback 后进入对应 Bus +``` + +公共契约为 `IShrinkEvent`、`IShrinkCancelableEvent`、`IShrinkResultEvent` 和 `IShrinkEventBus.Post/PostAsync/Attach`。订阅通过 `[ShrinkEventSubscriber]`、`[ShrinkSubscribe]` 声明,实例生命周期由 App、Context、Mod 或 Mono scope 接管。 + +### A.2 调度与生成 + +`ShrinkBusOptions` 固定 Bus 创建时的调度策略: + +- `Inline`:调用线程直接执行。 +- `MainThread`:Unity 环境切换到 UniTask PlayerLoop。 +- `DedicatedThread`:单线程串行状态机。 +- `TaskPool`:适合并行计算和 IO。 + +跨线程 `Post` 表示投递;需要等待完成、取消或结果时使用 `PostAsync`。队列容量、关闭排空、取消和异常策略由 options 明确配置。 + +生产注册由 Unity ILPostProcessor 或纯 .NET Roslyn Generator 生成强类型调用,不使用 `MethodInfo.Invoke` 或 `Delegate.DynamicInvoke`。没有生成合同的外部 DLL 不自动进入反射扫描。ECS/Burst 通过 `ShrinkEcsEventQueue` 的 NativeQueue writer/playback 接入,Job 不直接调用托管 delegate 或 UniTask。 + +### A.3 参考边界 + +NeoForge 的 owner/lifecycle、稳定优先级、取消事件和并行事件约束可用于定义行为;MessagePipe 的专用 broker、稳定 handler 快照和低分配发布路径可用于实现参考。ShrinkSDK 不复制 NeoForge 的双协议模型,也不引入 MessagePipe 的 DI 或第二套 Publisher/Subscriber API。 + +### A.4 历史验证快照 + +此前工作记录的结果为:Unity EditMode `206/206`、EventBus PlayMode `1/1`,纯 .NET smoke 覆盖 Inline、DedicatedThread、实例与静态生成绑定;UI Toolkit 调试窗口完成结构读回和截图检查;Windows x64 Development IL2CPP Player 构建成功。 + +此前记录的 Play Mode benchmark 三轮中位数为:8 handler class `25.32M publish/s`、8 handler struct `35.02M publish/s`、30 handler `10.82M publish/s`,同步 struct Post 为 `0 B / 10,000,000` 次。真实 Burst Job 性能、其它平台 AOT 和设备侧吞吐仍属于发布前门禁;这些历史数字本次未重新运行确认。