275 lines
12 KiB
Markdown
275 lines
12 KiB
Markdown
---
|
||
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/<Name>.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>` 的 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.<name>` **必须**在子目录放 `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.<ProgramAssembly>" }`。
|
||
|
||
**临时屏蔽整个包**:把该包 `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):
|
||
|
||
```
|
||
<Name>_<C|S|CS>_<startOpcode>.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}/<Name>.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\<T\> 使用规则
|
||
|
||
`EntityRef<T>` 是 ET 的弱引用包装结构体,**编译器不会自动 unwrap**。
|
||
|
||
```csharp
|
||
// ❌ 编译错误 CS1061
|
||
if (bag.ItemMap.TryGetValue(idx, out EntityRef<ItemUnit> unit))
|
||
{
|
||
unit.ItemId // EntityRef<ItemUnit> 没有 ItemId 字段
|
||
unit.Dispose() // 也没有 Dispose
|
||
}
|
||
|
||
// ✅ 用隐式转换拆包
|
||
if (bag.ItemMap.TryGetValue(idx, out EntityRef<ItemUnit> unitRef))
|
||
{
|
||
ItemUnit unit = unitRef; // 触发 implicit operator T(EntityRef<T> 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/<Pkg>_C_<opcode>.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<T> u); u.Field`** → CS1061。先 `T t = u;`。
|
||
8. **Helper 之间互调形成环** → ET0013。让低层 helper 内联自己的小逻辑,不调上层。
|
||
9. **登录后 GM 面板没自定义命令** → 先查 `cn.etetet.<pkg>/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 文件**必须**位于 `<pkg>/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 一键流程
|