Document the justanyproject comparison, query-command-event boundaries, reversible mod registry rules, package validation gates, and verified EventBus 2.0 state.
189 lines
12 KiB
Markdown
189 lines
12 KiB
Markdown
# 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 和设备侧吞吐仍属于发布前门禁;这些历史数字本次未重新运行确认。
|