217 lines
6.0 KiB
Markdown
Raw Permalink 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.

# Proto 完整工作流
## 1. 命名与放置
`.proto` 文件必须放在某个 `cn.etetet.<pkg>/Proto/` 目录下,且文件名遵守:
```
<Module>_<Direction>_<StartOpcode>.proto
```
| 字段 | 取值 | 说明 |
|---|---|---|
| `Module` | 任意(如 `HeroOuter``BagOuter``LoginInner`| 模块标识 |
| `Direction` | `C` / `S` / `CS` | C=外网消息客户端可见S=内网消息仅服务端CS=同时 |
| `StartOpcode` | 整数 | Opcode 从这里递增分配给每条 message |
opcode 分配规范(避免冲突):
| 模块 | Outer 起始 | Inner 起始 |
|---|---|---|
| Login | 1000 | 20001 |
| Router | 1100 | - |
| StateSync | 11001 | 21001 |
| ActorLocation | - | 20100 |
| Hero | 3000 | - |
| Bag | 3100 | - |
| Gacha | 3200 | - |
| 业务新模块 | 4000+ | 30000+ |
---
## 2. 消息注解(决定生成的 C# 类继承谁)
### RPC 请求 / 响应
```proto
// ResponseType R2C_SummonHero ← 必须在 message 上一行
message C2G_SummonHero // ISessionRequest
{
int32 RpcId = 1;
string reqId = 2; 推荐字段,前 2 个固定 RpcId + reqId
int32 configId = 3;
}
message R2C_SummonHero // ISessionResponse
{
int32 RpcId = 1;
int32 Error = 2; 3 个固定 RpcId / Error / Message
string Message = 3;
HeroInfo hero = 4;
}
```
生成结果继承 `MessageObject, ISessionRequest``MessageObject, ISessionResponse`,并通过 `[ResponseType(nameof(R2C_SummonHero))]` 自动配对。
### 单向通知(服务端推客户端)
```proto
message Hero_HeroInfo // IMessage
{
HeroInfo hero = 1;
}
```
### 嵌套 DTO
```proto
message HeroInfo
{
int64 heroId = 1;
int32 level = 2;
repeated SkillInfo skills = 3; // List<SkillInfo>
}
message SkillInfo
{
int32 skillId = 1;
int32 skillLevel = 2;
}
```
不加任何 `//` 注解的 message 默认继承 `MessageObject`,可作为 DTO 嵌入其他消息。
### 完整可识别的父类标记
Proto2CS 解析时,把 `message X // Y` 里的 `Y` 直接拼到生成类的继承列表。常见值:
- `IRequest` / `IResponse` — 内网请求/响应
- `ISessionRequest` / `ISessionResponse` — 客户端 ↔ Gate 的 RPC
- `IMessage` — 单向通知
- `IActorMessage` / `IActorRequest` / `IActorResponse` — Actor 模型消息
---
## 3. 生成
**Unity 菜单触发**
```
ET → Proto → Proto2CS
```
**MCP 触发**
```
CallMcpTool execute_menu_item {"menuPath": "ET/Proto/Proto2CS"}
```
工具会:
1. 扫描所有 `cn.etetet.*/Proto/*.proto`
2. 按文件名解析 Opcode
3. 输出三份 C# 到:
- `cn.etetet.proto/CodeMode/Model/Client/<Name>.cs`
- `cn.etetet.proto/CodeMode/Model/Server/<Name>.cs`
- `cn.etetet.proto/CodeMode/Model/ClientServer/<Name>.cs`
> CodeMode 下只有 `ClientServer/` 子目录有 `AssemblyReference.asmref`(指向 ET.Model所以在 `CodeMode: 3 = ClientServer` 模式下,**只有 ClientServer 那份会被编译**进 ET.Model。`Client/` 和 `Server/` 是预留给纯客户端/纯服务端模式的。
---
## 4. 调用
### 客户端发 RPC
```csharp
C2G_SummonHero request = C2G_SummonHero.Create();
request.configId = 1;
R2C_SummonHero response = await clientScene
.GetComponent<ClientSenderComponent>()
.Call(request, false) as R2C_SummonHero;
if (response.Error != 0)
{
Log.Error(response.Message);
}
```
### 服务端 RPC Handler
```csharp
// Scripts/Hotfix/Server/C2G_SummonHeroHandler.cs
[MessageSessionHandler(SceneType.Gate)]
public class C2G_SummonHeroHandler : RpcMessageHandler<C2G_SummonHero, R2C_SummonHero>
{
protected override async ETTask OnRun(
Session session, C2G_SummonHero request, R2C_SummonHero response)
{
Player player = session.GetComponent<SessionPlayerComponent>()?.Player;
if (player == null) {
response.Error = ErrorCode.ERR_RpcFail;
return;
}
// 业务...
await ETTask.CompletedTask;
}
}
```
注意:
- **不要**写 `protected override string Module => "X"` 这种属性ET0004 会报。
- 文件放 `Scripts/Hotfix/Server/`
### 服务端推单向通知
```csharp
Hero_HeroInfo notice = Hero_HeroInfo.Create();
notice.hero = HeroMapper.ToProto(hero);
session.Send(notice);
```
### 客户端通知 Handler
```csharp
// Scripts/Hotfix/Client/Hero_HeroInfoHandler.cs
[MessageHandler(SceneType.Main)]
[FriendOf(typeof(HeroComponent))]
public class Hero_HeroInfoHandler : MessageHandler<Scene, Hero_HeroInfo>
{
protected override async ETTask Run(Scene scene, Hero_HeroInfo message)
{
var comp = scene.GetComponent<HeroComponent>() ?? scene.AddComponent<HeroComponent>();
comp.HeroMap[message.hero.heroId] = message.hero;
EventSystem.Instance.Publish(scene, new HeroUpdatedEvent { HeroId = message.hero.heroId });
await ETTask.CompletedTask;
}
}
```
---
## 5. 常见 Proto 坑
1. **写完 .proto 不跑 Proto2CS → 引用方报"找不到类型"**:必须先生成。
2. **缺 `// ISessionRequest`**:消息生成出来不继承 `ISessionRequest`handler 的 `MessageSessionHandler<TReq, TResp>` 类型约束失败 → 编译报错。
3. **缺 `// ResponseType R2C_X`**:客户端 `await session.Call(req)` 无法推断响应类型。
4. **文件名 opcode 重复**:和别的 proto 撞 opcode → 运行时崩。规范分配规则见上面表格。
5. **手写 `new HeroInfo {...}`** → ET0031。改 `HeroInfo.Create()`
6. **修改 .proto 字段顺序 / 类型** → MemoryPack 序列化兼容性问题。线上版本要么不改,要么走版本兼容方案。
---
## 6. 一键自检 checklist
写完一个新 .proto 后:
```
- [ ] 文件名格式 <Module>_<Direction>_<Opcode>.proto
- [ ] Opcode 不与其他 .proto 冲突grep "_C_<同段号>_"
- [ ] RPC 消息有 // ResponseType
- [ ] 请求带 // ISessionRequest 或 // IRequest
- [ ] 响应带 // ISessionResponse 或 // IResponse
- [ ] 通知带 // IMessage
- [ ] 跑了 ET/Proto/Proto2CS
- [ ] 检查 cn.etetet.proto/CodeMode/Model/ClientServer/<Name>.cs 已生成
- [ ] Assets/Refresh 让 Unity 索引到新文件
- [ ] recompile_scripts 验证 0 错误
```