Files
Workspace/Docs/JustAnyProjectArchitectureAudit.md
T
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

12 KiB
Raw Blame History

justanyproject 代码框架组织审计

审计日期:2026-08-24 源项目:D:\UnityBuilds\justanyproject 源基线:main / b336715,工作树干净,Unity 2022.3.62f3 证据范围:AGENTS.mdAssets/ScriptsAssets/Tests、asmdef、Packages/manifest.json 和相关模块文档。本文是源码与工程结构审计,不代表重新执行过源项目的 Unity 编译、EditMode、PlayMode 或运行时验收。

1. 当前组织概况

源项目的第一方运行时代码主要位于 Assets/Scripts:共 564 个 C# 文件,按 24 个业务目录组织。规模最大的三个目录是 EndlessMode205)、CombatRuntime82)和 Bootstrap41)。

项目共有 12 个第一方 asmdef,其中大部分基础模块已有独立程序集;但 SeedsKey.Game 仍引用 28 个程序集,并承载大量未拆分目录。测试分为 EditMode 与 PlayMode 两个程序集,二者都直接引用多个生产程序集。

当前框架可以概括为:

Entry / AppBootstrapper
        |
Bootstrap + AppCompositionRoot
        |
全局 Manager / 领域系统 / Scene Composition Root
        |
CombatRuntime / EndlessMode / StrategicMap / 其它业务模块
        |
ShrinkEventBus / ShrinkDataSaver / ShrinkNetwork / ShrinkModFramework

目录层面的领域划分已经清楚,但编译边界、对象所有权和运行时入口仍处于新旧模式并存状态:显式组合根、窄接口和结构化 facade 已出现,业务单例、静态入口和事件协调仍然大量存在。

2. 值得迁移的经验

2.1 显式 Composition Root

AppCompositionRootEndlessBattleCompositionRoot 把关键依赖集中在可检查的绑定边界。后者通过序列化字段表达场景依赖,并提供 ValidateRequiredBindings() 汇总缺失项。这比在业务对象内部临时查找单例、场景对象或 prefab 子节点更容易审计。

AppScopeSessionScopeSceneScope 目前只是借用关系和已释放状态的标记:它们会拒绝失效父作用域,但不持有资源集合,也不会级联或反向释放对象。因此可迁移的是“显式作用域边界”,不是现有实现本身。

ShrinkSDK 的承载方式:

  • App 级组合继续由 ShrinkAppLoaderBootstrapper、Composition Profile 与 ShrinkAppLoaderHost 表达。
  • Session/Scene/Mod 生命周期继续映射为 ShrinkContext fiber、派生 context 和可逆 effect,不再增加第二套 Scope 容器。
  • 场景组件仍应使用显式序列化引用和独立校验结果,不允许运行时静默补建或猜路径。

2.2 命令、查询和事实事件分离

源项目正在把跨模块调用收敛为三类:

  • 查询依赖窄的只读接口,例如 ISquadReadOnlyIInventorySlotReadOnly
  • 改变状态的操作经过 command facade,并返回 Succeeded / Failed / Rejected 等结构化结果。
  • EventBus 只广播已经发生的事实;请求事件仅作为有清理计划的兼容桥。

GameSessionFlowFacadeSceneTransitionFlowFacade 还处理了重复请求拒绝、异常转结果以及 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 实例:

game       -> Unity 主线程
scene:id   -> Scene/Context 生命周期
mod:id     -> 模组专属线程或宿主指定调度
server     -> Inline 或专属线程
world:id   -> ECS playback 后进入对应 Bus

公共契约为 IShrinkEventIShrinkCancelableEventIShrinkResultEvent<TResult>IShrinkEventBus.Post/PostAsync/Attach。订阅通过 [ShrinkEventSubscriber][ShrinkSubscribe] 声明,实例生命周期由 App、Context、Mod 或 Mono scope 接管。

A.2 调度与生成

ShrinkBusOptions 固定 Bus 创建时的调度策略:

  • Inline:调用线程直接执行。
  • MainThreadUnity 环境切换到 UniTask PlayerLoop。
  • DedicatedThread:单线程串行状态机。
  • TaskPool:适合并行计算和 IO。

跨线程 Post 表示投递;需要等待完成、取消或结果时使用 PostAsync。队列容量、关闭排空、取消和异常策略由 options 明确配置。

生产注册由 Unity ILPostProcessor 或纯 .NET Roslyn Generator 生成强类型调用,不使用 MethodInfo.InvokeDelegate.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 和设备侧吞吐仍属于发布前门禁;这些历史数字本次未重新运行确认。