Files
Workspace/DESIGN.md
T
cneicy bbce96426a
Validate ShrinkSDK Workspace / unity (push) Failing after 3m10s
fix(migration): validate minimal Unity consumers
2026-08-26 11:06:24 +08:00

261 lines
20 KiB
Markdown
Raw 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.
# ShrinkSDK 当前架构
> 当前基线:2026-08-26
> 本文是仓库唯一的当前架构文档。实现与本文冲突时,以源码、`package.json`、asmdef 和自动化验证结果为准,并应在同一变更中修正文档。迁移过程与旧代码地图只保存在 `Docs/Archive/`,不再作为当前设计依据。
## 1. 项目定位
ShrinkSDK 是以 Unity Package Manager 包为发布边界的 SDK Workspace,不是单个游戏项目。`Workspace` 根仓库保留集成工程、场景、跨包验证和文档;`Assets/Modules/` 下每个包是同路径 Git submodule,相邻目录 `.meta` 仍由根仓库追踪,避免 Unity GUID 和既有引用变化。一次干净克隆必须使用递归 submodule 初始化,Unity 只需要恢复外部 UPM 依赖。
它提供四层能力:
1. 基础模块:EventBus、DataSaver、Command、Network、Tutorial。
2. 动态组合内核:ShrinkContext.Core,以可逆效应、响应式依赖和 fiber 生命周期承载模块装卸。
3. 应用与扩展宿主:ShrinkApp、Starter、ModFramework。
4. 边界适配与生成工具:`*.Integration.*`、共享 IL 后处理器、独立服务器生成器。
当前默认应用主路径是 `ContextLoader``ClassicHost` 仅是既有运行时兼容面;包分发与安装只使用公开 registry、精确 Git 标签或 `Installer` 引导包,不保留旧安装入口。
## 2. 开发原则
### 2.1 包是发布边界,asmdef 是编译边界
- 每个 `Assets/Modules/<Module>/package.json` 都必须能被独立 UPM 消费。
- 公开发布先走 `https://git.crash.work/api/packages/ShrinkSDK/npm/``com.cneicy` scoped registryGit 安装只能固定到对应 `vX.Y.Z` 标签,禁止浮动分支。
- 主包不反向依赖集成包;跨模块行为放入 `*.Integration.*` 或 Context adapter。
- asmdef 只声明实际编译依赖,不依靠根工程中偶然存在的程序集。
- 内部包依赖版本必须等于被依赖包自身的 `version`,不接受“根工程能编译所以先放着”的漂移。
### 2.2 声明契约,生成注册,避免运行时全域扫描
网络消息、订阅者、命令、事件订阅和 App 安装器使用 Attribute 声明。`ShrinkShared.CodeGen` 在 Unity 编译期写入程序集注册表,运行时读取注册表;实例对象和外部 DLL 走显式实例注册。
独立服务器生成器是编辑器工具,不参与运行时注册。它读取 Unity 已编译的 Player 程序集元数据,而不是用正则解析 C# 文本。因此 `partial`、命名空间、复杂泛型和真实继承关系都由编译器语义决定。可导出的网络消息必须显式声明 `[ShrinkNetworkMessage(opcode, route)]`;单独手写 `RegisterMessage<T>()` 不是可移植的生成输入。
### 2.3 静态门面负责易用性,可实例化内核负责组合
`EventBus``ShrinkApp``ShrinkNetworkRuntime.Default` 等静态入口是默认根的易用门面,不是生命周期所有者。真实服务可实例化,并由 Context 组件安装、发布、撤回。门面注册也必须被视为可逆效应,组件退役时按实例注销,不能留下失效的全局引用。
### 2.4 运行时变更必须可回滚、可解释
组件通过 `ShrinkCtx.Effect/EffectAsync/EffectInverse` 记录逆操作,通过 `Set/Get` 提供和消费能力。加载失败、依赖消失、热替换失败都应恢复到上一个已提交组合。诊断必须能回答:哪个 fiber 在等待什么、当前 provider 是谁、哪次事务失败、是否完成恢复。
### 2.5 生成内容与人工内容分区
- `GeneratedServers/*/Generated/*.g.cs` 是可重建合同,不承载手写业务。
- 服务器模板文件由哈希清单管理。重新生成前先验证全部现有文件仍等于上次生成版本;任何人工修改都会让整个复制阶段在写入前中止。
- 业务扩展应放在非生成文件或独立模块中,不通过修改生成产物维持。
### 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 Workspace/
|-- Assets/Modules/ 21 个同路径 Git submodule UPM 包
|-- Assets/Modules/*.meta 根仓库追踪的 Unity 目录 GUID
|-- Assets/Scenes/ 示例与验收场景
|-- Assets/Resources/ 当前应用配置与组合 Profile
|-- GeneratedServers/ 独立 .NET 宿主、生成合同与烟测工程
|-- GeneratedModSdk/ Mod SDK 导出物
|-- Packages/ 根 Unity 工程依赖
|-- Tools/UpmConsumerValidation/ 干净 UPM 消费工程验证
|-- Tools/RepositoryMigration/ 子模块、发布仓库与独立宿主初始化脚本
|-- Docs/Archive/ 已完成迁移与过期地图,仅供追溯
|-- DESIGN.md 唯一当前架构文档
|-- NETWORK_PITFALLS.md 网络实现经验记录,不是架构基线
`-- .planning/threads/ 工作线程恢复记录,不是产品文档
```
## 4. 包清单
| 包 | 版本 | 职责 |
|---|---:|---|
| `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.2.1 | 模组发现、依赖、可逆生命周期、命名空间内容覆盖、外部 DLL revision 与 Harmony lease |
| `com.cneicy.shrink-tutorial` | 0.1.1 | 数据驱动引导、遮罩、锚点、触发与持久化 |
| `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.2 | 默认 Context 组合根、配置资产和示例入口 |
| `com.cneicy.shrink-context-app-adapter` | 0.1.2 | ContextLoader 宿主、Profile/JSON、诊断与基准 |
| `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 | 命令请求与生命周期事件桥 |
| `com.cneicy.shrink-command-integration-network` | 0.1.0 | `command/execute` RPC 桥 |
| `com.cneicy.shrink-command-integration-app` | 0.1.0 | Command App installer/原生 Context 组件 |
| `com.cneicy.shrink-network-integration-eventbus` | 0.1.1 | 网络事件广播、裁决结果与 delta 去重 |
| `com.cneicy.shrink-network-integration-app` | 0.1.0 | Network App installer/原生 Context 组件 |
| `com.cneicy.shrink-shared-codegen` | 0.1.0 | App、Command、Network 共用的 Editor-only IL 后处理注册表生成器 |
| `com.cneicy.shrink-installer` | 0.1.5 | 安全合并公开 registry、显示诊断并固定版本安装 Starter 或选定模块的 Editor 引导包 |
`ShrinkShared.CodeGen` 不反向引用业务 asmdef,只按程序集名与类型全名读取 Cecil 元数据,因此业务包可以依赖它而不形成包循环。
## 5. 总体分层
```text
Starter / Composition Profile
|
ShrinkAppLoaderBootstrapper ---- ClassicHost (compatibility only)
|
ShrinkContextLoader + ShrinkContextRuntime
|
native module components ---- integration components
| |
Command / DataSaver / Network / EventBus / Tutorial
|
transport, storage, external files, generated .NET server
```
依赖方向只能向下。功能主包不知道 App、Context 或 EventBus 桥;适配包引用双方并负责把副作用变成可撤回效应。
## 6. ContextLoader 当前完成态
Cordis 迁移的阶段 0 至阶段 5 已进入当前架构,不再是待办计划。
### 6.1 核心语义
- `ShrinkCtx.Effect/EffectAsync`:前向执行产生逆操作,按 LIFO 回滚;前向未完成前不会执行逆操作。
- `ShrinkCtx.Set/Get`:provider 安装和撤回触发依赖者响应;同值但不同 provider uid 仍会触发重载。
- `ShrinkContextRuntime.Use`:组件实例化为 fiber,状态按 `Loading -> Active -> Unloading -> Inactive` 惯性转换。
- 退役 provider 时先排空依赖者,再执行 provider 的逆操作。
- `IShrinkIterativeComponent` 支持分步效应和部分回滚。
- `Provide` 声明限制组件写入能力;未声明、未激活和策略拒绝是三种不同错误。
### 6.2 声明式协调
`ShrinkContextLoader.ApplyAsync` 根据 `ShrinkLoaderEntry` 的期望集合增量协调:
- 新条目创建 fiber;消失或禁用的条目退役。
- 组件、配置或 isolate 改变时重建。
- intercept 元数据可原位更新,不改变 provider uid 或 fiber generation。
- 事务失败时恢复旧条目、旧 provider 关系和已提交视图。
- `key -> inject fibers` 倒排索引限制通知候选,再按 realm 精确过滤。
### 6.3 隔离、访问与边界
`isolate` 创建独立解析域;`intercept` 将访问元数据交给领域策略,当前可用于 DataSaver 只读能力。它们是组合与能力介导机制,不是不可信 DLL 的安全沙箱,也不能撤回已经发送的网络数据或外部文件写入。
### 6.4 App 默认组合
`ShrinkAppLoaderBootstrapper.DefaultComposition` 接入 Basic Starter 的七个声明式模块:Command、DataSaver、Network 三个原生组件,Command-Network 集成,以及 Command/DataSaver/Network 的 EventBus 集成。
`ShrinkAppCompositionProfile` 用 ScriptableObject/JSON 决定条目启用、显式排除、isolate 与 intercept;组件工厂仍由代码注册,配置文件不能按任意类型名反射实例化对象。`ShrinkSDK/Cordis/诊断与组合` 显示 fiber、等待依赖、provider target、事务和外部程序集 revision。
`ShrinkApp.IsRunning``ShrinkApp.Services` 已接回 LoaderHost。原生组件停用时使用 `TryUnregister(instance)` 撤回对应门面。旧 `IShrinkAppModuleInstaller` 适配仍可用于 ClassicHost,但默认 ContextLoader 不再通过它启动核心模块。
## 7. 功能模块与组合关系
### 7.1 EventBus
静态 `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
`ShrinkSave``ShrinkSettings``ShrinkDataSaverRuntime` 构成运行时入口。存储层以临时文件原子替换主文件并轮换 `.bak1/.bak2`;损坏时尝试备份恢复。Context 原生组件发布 reader/writer 能力并在停用时做补偿式收尾。
### 7.3 Command
`ShrinkCommandService` 解析字面量与贪婪参数路径,支持来源、权限和同步/Task/UniTask 返回。静态命令使用编译期注册表,实例命令显式注册。网络桥只在 Command 与 Network 两个服务键均可用时激活。
### 7.4 Network
`ShrinkNetworkService` 组合 serializer、message registry、router 与 transport。包头包含协议/Schema 版本、请求 token、会话 token、route、kind 和 payload。Unity 主线程派发通过有界 dispatch queue;独立服务器使用 inline scheduler。
网络合同必须同时满足:
- 类型实现 `IShrinkNetworkMessage``IShrinkNetworkRequest` 或继承 `ShrinkRpcResponseBase`
- 类型显式声明 `[ShrinkNetworkMessage(opcode, route)]`
- opcode 和 route 在导出集合内唯一。
- 导出到无命名空间的服务器合同后,简单类型名仍唯一;不同命名空间的同名类型会明确报错,不再静默丢弃。
`[ShrinkNetworkStateSync]` 标记 JoinRequest、JoinResponse、Command、StateDelta、LeaveNotice 和 Heartbeat。`[ShrinkNetworkEvent]` 进一步区分广播、远端裁决和增量事件。
### 7.5 ModFramework
`ShrinkModContextHost` 将模组生命周期纳入 Context apply。热替换单位是 `ModId + revision`;外部 DLL 用 SHA-256 标识 revision,失败或损坏 revision 不会成为 current,旧组合会恢复。Harmony、Registry 与 Network handler 使用可逆 lease 清理。
`ShrinkModRegistry<T>` 的基础项只能通过 `<ownerModId>:<localKey>` namespace 注册;覆盖必须指向已有基础项,按显式优先级选出 winner,同目标同优先级直接拒绝。基础项和覆盖项都记录 owner,卸载时自动撤回并恢复下一个覆盖或基础值。注册表不采用全局冻结,因为 revision 热替换与事务回滚要求状态可撤回。
Mono 中已加载程序集不能真正卸载。系统只回滚组件实例与效应,并通过 `ShrinkModDiagnostics` 暴露 current/history、累计载入字节和软阈值,达到阈值时建议 Domain Reload 或进程重启。
## 8. 编译期注册与服务器生成
### 8.1 Unity 运行时注册
`ShrinkEventBus/CodeGen/Editor/EventBusILPostProcessor.cs` 为引用方程序集生成实例直接调用绑定和静态模块初始化注册,不运行时枚举方法。`ShrinkShared.CodeGen/Editor/ShrinkRegistryILPostProcessor.cs` 继续负责 Command、Network 和 App 注册表。外部 Mod DLL 必须携带 EventBus 生成合同;没有合同的生产 DLL 不自动反射注册。
### 8.2 独立服务器生成
菜单入口:
- `ShrinkSDK/Network/生成完整独立服务器工程`
- `ShrinkSDK/Network/刷新独立服务器 Generated 合同`
完整生成分两步:
1. 复制 `ServerProjectTemplate``GeneratedServers/ShrinkNetwork.ServerHost`。模板清单记录每个托管文件的 SHA-256;复制前完成全量冲突检查,检测到人工修改时零写入退出。
2. 从 Unity `CompilationPipeline` 的 Player 程序集取得已加载类型,通过 `CustomAttributeData`、接口、基类、属性和泛型元数据生成合同、handler、模块与扫描报告。
语义扫描天然合并 partial 类型,并只导出公开实例 `get/set` 属性。复杂泛型递归格式化;命名空间不同但简单名相同的消息或依赖类型会中止生成并列出所有来源。`Generated/*.g.cs` 仍是生成器所有的可替换区域。
服务器宿主是 .NET 8 控制台工程,包含 TCP/KCP 服务端、协议窗口、会话令牌、登录/刷新、诊断摘要和 `server.properties`。模板提供运行骨架,业务裁决仍需在非生成代码中实现。
## 9. 验证门槛
一次涉及包边界、生成器或组合根的变更至少经过:
1. 内部包图检查:全部 `com.cneicy.*` 依赖版本一致、无循环,普通主包不反向依赖 Integration 包。
2. Unity 编译:目标 asmdef 和根项目无编译错误。
3. EditMode 测试:功能测试、Context 生命周期、语义扫描和模板覆写保护。
4. 真实 UPM 消费:`Tools/UpmConsumerValidation/Validate-UpmConsumer.ps1` 在递归 submodule 初始化后的 Workspace 中创建仓库外形态的临时 Unity 工程,通过 `file:` 安装全部 21 个包,启用 testables,验证包注册、程序集加载并运行 EditMode 测试。发布后还必须分别验证 registry、精确 Git URL 和 Installer 三种空白消费者工程路径。
5. 独立宿主:生成工程与 RuntimeSmoke 按变更范围构建或运行。
6. 涉及真实生命周期时,仍需在目标场景执行 Play Mode 验收;源码检查和 EditMode 不能替代该路径。
## 10. 已知边界
- ClassicHost 和静态门面仍是运行时兼容层;旧 installer 不再是受支持的包安装入口。
- Context 的 intercept 不是进程级沙箱;外部程序集 revision 不可从 Mono 卸载。
- 某些第三方或领域副作用只能补偿,不能保证物理撤回。
- 服务器生成器输出可编译、可注册的合同和扩展点,不推断完整业务规则。
- `GeneratedServers/` 中的认证与协议窗口是基础设施,不等于完整账号体系、密钥轮换、审计、trace、dashboard 或故障演练。
- Profile 的 isolate 变化通过重建条目生效;运行中 fiber 原地迁移 realm 不在当前范围。
## 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。
- 包源码、标签、独立开发宿主与包级 CI 位于 `https://git.crash.work/ShrinkSDK/<Package>`;发布版本只能由与 `package.json.version` 一致的 `vX.Y.Z` 标签产生。
- `NETWORK_PITFALLS.md` 记录实现经验,不描述当前模块清单。
- `Docs/Archive/CORDIS_MIGRATION.completed.md` 保存迁移论证、阶段记录和历史验收数据。
- `Docs/Archive/codebase-map-2026-05-23/` 保存迁移前代码地图,其中关于 Git 状态、模块规模和测试覆盖的描述均已过期。
- `.planning/threads/` 只用于恢复工作上下文;其中历史 Notes 不得覆盖本文和当前源码。