Files
Workspace/Docs/JustAnyProjectArchitectureAudit.md
cneicy 493d280521 docs(architecture): record framework audit decisions
Document the justanyproject comparison, query-command-event boundaries, reversible mod registry rules, package validation gates, and verified EventBus 2.0 state.
2026-08-26 01:18:15 +08:00

189 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<T>` 对内容 id、owner、覆盖优先级和冻结阶段有明确规则:
- 新增内容必须使用 owner namespace。
- 同目标、同优先级覆盖直接报冲突。
- 覆盖目标必须存在。
- 启动完成后冻结,运行阶段只读。
这套规则适合“启动时装载、运行时固定”的游戏内容。ShrinkModFramework 现已把其中可兼容热替换的部分落地:基础项强制使用 `<ownerModId>:<localKey>`,覆盖目标和优先级显式声明,同目标同优先级直接拒绝,owner 卸载时自动恢复下一个覆盖或基础项。
没有复制冻结模型。ShrinkModFramework 支持 revision 热替换和失败回滚,注册表必须随组件 effect 撤回;全局冻结会与此生命周期冲突。模组通过 `RegisterContent` / `OverrideContent` 写入,查询只暴露 `IReadOnlyShrinkModRegistry<T>`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<T>` 的文件、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<TResult>``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<T>` 的 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 和设备侧吞吐仍属于发布前门禁;这些历史数字本次未重新运行确认。