--- name: et-framework-dev description: >- Develop ET-Framework + YIUI + Luban + Proto + HybridCLR business code in the Survivors Unity project. Use when adding/editing any code under `My project/Packages/cn.etetet.*`, creating new packages, adding RPC/Proto messages, defining Entity/Component, writing GM commands, or touching Hotfix/HotfixView/Model/ModelView assemblies. Enforces the ET source generator rules (ET0001~ET0032), assembly-layout (asmref) discipline, Proto workflow, EntityRef usage, and the MCP-Unity debug loop. --- # ET Framework Development — Survivors Project ET 的 Roslyn 分析器和程序集结构非常严格,**违反规则会一次性触发几十个编译错误**。先按下面的"动手前必查"走一遍,再写代码。 --- ## 动手前必查(每次都先做这 5 件事) ``` - [ ] 1. 看包是否激活:包目录下 Scripts/**/AssemblyReference.asmref 必须齐全, Ignore.*.asmdef 必须保持 IGNORE constraint(否则会顶替 asmref 失效)。 - [ ] 2. 看 Proto 是否生成:cn.etetet.proto/CodeMode/Model/ClientServer/.cs 是否存在; 缺则跑 ET/Proto/Proto2CS 菜单。 - [ ] 3. 看每个新类放到了正确程序集层: - 静态工具 / 占位 DTO → Model + [EnableClass] - Entity/Component → Model/Server 或 Model/Client - System / Service / Handler / Helper(业务)→ Hotfix - UI 数据绑定 / 工具 → ModelView + [EnableClass] - YIUI 事件 / GM 命令 → HotfixView - [ ] 4. 看是否有静态字段:Hotfix/HotfixView 程序集禁止任何非 const 字段和属性。 - [ ] 5. 看静态工具类是否互相调用:BagNoticeHelper ↔ BagComponentSystem 这种环 会触发 ET0013,要拆成单向依赖。 ``` 把这 5 条贴到 todo 里,写代码前一项一项打勾。 --- ## ET 分析器铁律(高频踩坑顺序) | ID | 规则 | 怎么改 | |---|---|---| | **ET0001** | `AddChild` 的 T 必须在 `[ChildOf]` 声明里 | Entity 类加 `[ChildOf(typeof(ParentEntity))]` | | **ET0002** | 访问别人的 Entity 字段需 `[FriendOf(typeof(T))]` | 在调用方类上加 `[FriendOf(typeof(Hero))]` 等 | | **ET0003** | Entity 子类不能写构造函数 / 不能 `new` | 用 `AddChild`/`AddComponent` 创建 | | **ET0004** | **Hotfix/HotfixView 程序集禁止非 const 字段和属性**(包括 `=> "..."` 的 expression-bodied property)| 字段挪到 Model 程序集 + `[EnableClass]`;属性删掉,用 `typeof(T).Name` 之类直接 inline | | **ET0013** | 静态类间环依赖 | 让一边 inline 内嵌另一边的小逻辑,打破环 | | **ET0015** | static 字段必须 `[StaticField]` | 加 attribute;但若同时在 Hotfix 会与 ET0004 冲突 → 必须挪走 | | **ET0024/0025** | `EntitySystem` 必须在 `[EntitySystemOf]` 标记的 partial 类里 | `[EntitySystemOf(typeof(Hero))] partial class HeroSystem` | | **ET0030** | NetMessage 必须从 Proto 生成 | 不要手写 Message 类 | | **ET0031** | 禁止 `new XxxMessage()`(Proto 类) | 用 `XxxMessage.Create()` 工厂方法 | | **ET0032** | Model/ModelView 程序集禁止非 Entity 类(除非加 `[EnableClass]`) | 静态工具类、占位 DTO 加 `[EnableClass]` | 完整规则解读 → 见 [analyzer-rules.md](analyzer-rules.md) --- ## 程序集分层硬规则 每个 ET 包 `cn.etetet.` **必须**在子目录放 `AssemblyReference.asmref`,否则代码不会参与编译(一个隐藏到不报错的"代码静默失踪"陷阱)。 ``` cn.etetet.mypkg/ ├── Ignore.ET.MyPkg.asmdef # 占位 asmdef(IGNORE constraint,不参与编译) ├── Proto/ # 仅放 .proto,由 Proto2CS 产 cs 到 cn.etetet.proto ├── Scripts/ │ ├── Model/ │ │ ├── Share/AssemblyReference.asmref → ET.Model │ │ ├── Client/AssemblyReference.asmref → ET.Model │ │ └── Server/AssemblyReference.asmref → ET.Model │ ├── Hotfix/ │ │ ├── Share/AssemblyReference.asmref → ET.Hotfix │ │ ├── Client/AssemblyReference.asmref → ET.Hotfix │ │ └── Server/AssemblyReference.asmref → ET.Hotfix │ ├── ModelView/Client/AssemblyReference.asmref → ET.ModelView │ └── HotfixView/Client/AssemblyReference.asmref → ET.HotfixView ``` `AssemblyReference.asmref` 内容固定为 `{ "reference": "ET." }`。 **临时屏蔽整个包**:把该包 `Scripts/**/AssemblyReference.asmref`(含 .meta)全删,保留 `Ignore.*.asmdef`。本次会话就用这招屏蔽了 `cn.etetet.gacha`。 **判断要放哪一层**(最常错的决策): | 代码类型 | 放哪 | 原因 | |---|---|---| | Entity / Component 定义 | `Model/Server` 或 `Model/Client` | Model 程序集只允许 Entity 派生类 | | Entity 的 `System`(Awake/Update/Destroy)| `Hotfix/Server`(带 `[EntitySystemOf]`)| 业务逻辑层 | | Service / Helper / Mapper / Handler | `Hotfix/Server` 或 `Hotfix/Client` | 业务工具 | | Proto 消息类 | **不要自己写**,跑 Proto2CS | 自动生成到 `cn.etetet.proto/CodeMode/` | | 普通占位 DTO / Luban 生成类 / Lock 对象 | `Model/Share` + `[EnableClass]` | 需要持有静态状态时必须在 Model | | YIUI 视图组件 / UIBindCDETable 派生 | `ModelView/Client` | YIUI 框架约定 | | YIUI 事件 handler / GM 命令 / 客户端 RPC handler | `HotfixView/Client` | 视图层业务 | 更多分层细节 → 见 [assembly-layout.md](assembly-layout.md) --- ## Proto 工作流(不跑等于不存在) ET 不直接编译 `.proto`,必须先跑生成器。 **1. 命名规则**(Proto2CS 用文件名解析 opcode): ``` __.proto 例:HeroOuter_C_3000.proto # C=客户端服务端通用,opcode 从 3000 开始递增 ``` **2. RPC 注解**(缺一个就不识别为 RPC): ```proto // ResponseType R2C_SummonHero message C2G_SummonHero // ISessionRequest { int32 RpcId = 1; string reqId = 2; int32 configId = 3; } message R2C_SummonHero // ISessionResponse { int32 RpcId = 1; int32 Error = 2; string Message = 3; } // 非 RPC 单向通知用这个 message Hero_HeroInfo // IMessage { HeroInfo hero = 1; } ``` **3. 生成步骤**: ``` 1. 写完 .proto,Unity 菜单 ET → Proto → Proto2CS 2. 生成结果在 cn.etetet.proto/CodeMode/Model/{Client,Server,ClientServer}/.cs 3. Assets → Refresh,让 Unity 索引到新文件 4. recompile_scripts 或 F6 ``` **4. 用 MCP 一键触发**: ``` execute_menu_item({"menuPath": "ET/Proto/Proto2CS"}) execute_menu_item({"menuPath": "Assets/Refresh"}) recompile_scripts({"waitForCompletion": true}) ``` 更多 → 见 [proto-workflow.md](proto-workflow.md) --- ## EntityRef\ 使用规则 `EntityRef` 是 ET 的弱引用包装结构体,**编译器不会自动 unwrap**。 ```csharp // ❌ 编译错误 CS1061 if (bag.ItemMap.TryGetValue(idx, out EntityRef unit)) { unit.ItemId // EntityRef 没有 ItemId 字段 unit.Dispose() // 也没有 Dispose } // ✅ 用隐式转换拆包 if (bag.ItemMap.TryGetValue(idx, out EntityRef unitRef)) { ItemUnit unit = unitRef; // 触发 implicit operator T(EntityRef v) unit.ItemId unit.Dispose() } // ✅ Linq 场景 self.ItemMap.Values .Select(x => (ItemUnit)x) // 显式 cast 也会调 implicit operator .Where(x => x.ItemId == 1) ``` --- ## MCP-Unity 调试循环 调试代码不要手动开 Unity 看日志,用 MCP 闭环。 ``` 1. 改完代码 → CallMcpTool execute_menu_item Assets/Refresh 2. CallMcpTool recompile_scripts {"waitForCompletion": true} 3. 若 errors > 0 → 看 logs 列表,分类批量修 4. errors == 0 → CallMcpTool execute_menu_item "Tools/Survivors/Open Init Scene And Play" 5. CallMcpTool get_console_logs {"types": ["error", "warning"], "count": 50} ``` **MCP 连接失败时**:不要立刻重试,先确认 Unity 编辑器没卡在编译/导入(往往是上一次 Refresh 还在跑)。等 5-10s 再发请求。 --- ## 新建一个业务包的标准流程 参考 cn.etetet.hero / cn.etetet.bag 的结构。**写代码前先把骨架建好**。 ``` Task Progress: - [ ] 1. 拷贝 cn.etetet.statesync 作为模板,改包名 - [ ] 2. 删掉 Runtime/Model 下的 ET.Model.asmdef(保留 Ignore.*.asmdef 即可) - [ ] 3. 保留 / 重建以下 7 个 AssemblyReference.asmref: Scripts/Model/{Share,Client,Server}, Scripts/Hotfix/{Share,Client,Server}, Scripts/ModelView/Client, Scripts/HotfixView/Client(按需) - [ ] 4. package.json dependencies 加 cn.etetet.core / login / logging / console / bag 等 - [ ] 5. Proto/_C_.proto 写消息 - [ ] 6. ET → Proto → Proto2CS - [ ] 7. Model 层写 Entity,配 [ChildOf]/[ComponentOf] - [ ] 8. Hotfix 层写 System([EntitySystemOf])/ Service / Handler - [ ] 9. recompile_scripts 验证 0 错误 - [ ] 10. 写 GM 命令到 HotfixView/Client/GM 验证业务 ``` --- ## 已知 demo 噪音(看到时直接忽略,不要去修) 当前 `GlobalConfig.SceneName = StateSync`,走 `cn.etetet.statesync` + `cn.etetet.yiuistatesync` 这两个**仅参考** demo 包的流程。这两个错每次都会出现,**不影响业务**: | 错误关键字 | 触发时机 | 来源 | |---|---|---| | `actor id is 0` @ `MessageLocationSenderComponentSystem.cs:67` | 走 demo 大厅 → 进入地图 (EnterMapAsync) | statesync M2M 跨地图转移弱实现 | | `cant set parent because parent iSence is null: CoroutineLockComponent` | 按停止键退出 Play | demo `LobbyPanelComponentSystem.OnEventEnterMapInvoke` 在 OnApplicationQuit 后还在 await CloseAsync | 排查规则:先看错误栈是否在 `cn.etetet.statesync` / `cn.etetet.yiuistatesync` 内部。是的话直接跳过。 --- ## 这次会话踩过的坑(不要再犯) 1. **新包没 asmref → 代码静默失踪**。报错全在引用方报"找不到类型",而不是说"包没编译"。第一时间检查 asmref。 2. **Proto 写好不跑 Proto2CS → 类不存在**。F6 不会触发,必须走菜单。 3. **Hotfix 里写 `protected virtual string Module => "Net"`** → ET0004 把属性也当字段。要么挪到 Model + `[EnableClass]`,要么完全删除。 4. **`static readonly Dictionary` 当查找表** → Hotfix 禁。改 `switch` 表达式,或挪到 Model。 5. **`StringSplitOptions.TrimEntries`** → Unity 的 .NET Standard 2.1 不支持。用 `Split(' ', RemoveEmptyEntries).Select(x => x.Trim())`。 6. **`new HeroInfo { ... }`** → ET0031。用 `HeroInfo.Create()` + 逐字段赋值。 7. **`bag.ItemMap.TryGetValue(.., out EntityRef u); u.Field`** → CS1061。先 `T t = u;`。 8. **Helper 之间互调形成环** → ET0013。让低层 helper 内联自己的小逻辑,不调上层。 9. **登录后 GM 面板没自定义命令** → 先查 `cn.etetet./Scripts/HotfixView/Client/GM/*.cs` 是否在 HotfixView 程序集(必须有 asmref),再查 `[GM(EGMType.Common, ...)]` 标签是否正确。 10. **MCP 调用超时** → Unity 还在 Import/Compile,先等 5-10s。 --- ## 链路速记(启动到 GM 出现) ``` Init.unity (cn.etetet.loader) → Entry.Start (cn.etetet.core) → FiberInit_Main (SceneType 由 GlobalConfig.SceneName 决定) → EntryEvent1/2/3 → EntryEvent3_InitClient (cn.etetet.yiuistatesync, SceneType.StateSync) → YIUIMgrComponent.Initialize() → YIUIEventInitializeAfter → YIUIEventInitializeAfterGMHandler (cn.etetet.yiuigm) → GMCommandComponent.Awake → 扫 GMAttribute → 显示在 GM 面板 ``` 要让自定义 GM 出现:cs 文件**必须**位于 `/Scripts/HotfixView/Client/...`(HotfixView 程序集),并标 `[GM(EGMType.XXX, 1, "标题", "tooltip")]`。 --- ## 详细参考 - [analyzer-rules.md](analyzer-rules.md) — ET0001~ET0032 全规则解读 + 触发示例 + 修复模板 - [assembly-layout.md](assembly-layout.md) — 程序集分层完整规则、asmref 模板、新建包步骤 - [proto-workflow.md](proto-workflow.md) — Proto 命名、注解、生成、MCP 一键流程