|
| 1 | +# 如何添加新的 API 指令 |
| 2 | + |
| 3 | +本文档详细记录了在 OpenRA Copilot Mod 中添加新 API 指令的流程、架构说明以及开发经验总结。 |
| 4 | + |
| 5 | +## 1. API 架构简介 |
| 6 | + |
| 7 | +OpenRA Copilot 的 API 系统基于 Socket 通信,主要涉及以下几个核心文件: |
| 8 | + |
| 9 | +* **`OpenRA.Game/CopilotCommandServer.cs`**: 服务器入口,负责接收 Socket 请求、解析 JSON、验证基础格式、并路由到对应的处理函数。 |
| 10 | +* **`OpenRA.Game/CopilotModels.cs`**: 定义请求和响应的数据模型,以及参数验证逻辑。 |
| 11 | +* **`OpenRA.Mods.Common/ServerCommands.cs`**: 包含具体的指令处理逻辑(静态方法)。通常在这里解析参数并调用游戏内的逻辑。 |
| 12 | +* **`OpenRA.Mods.Common/Traits/Copilot/`**: 对于复杂的指令(如 `expand_base`),通常会封装成一个 `Trait`(特性),挂载在 Player 或 World 上,由 `ServerCommands` 调用。 |
| 13 | + |
| 14 | +## 2. 添加指令的详细步骤 |
| 15 | + |
| 16 | +假设我们要添加一个名为 `my_command` 的新指令。 |
| 17 | + |
| 18 | +### 步骤 1: 定义参数验证 (CopilotModels.cs) |
| 19 | + |
| 20 | +在 `OpenRA.Game/CopilotModels.cs` 中,找到 `ValidateCommandParams` 方法,添加新指令的参数检查逻辑。 |
| 21 | + |
| 22 | +```csharp |
| 23 | +// CopilotModels.cs |
| 24 | +
|
| 25 | +public static (bool isValid, MCPError error) ValidateCommandParams(string command, JObject parameters) |
| 26 | +{ |
| 27 | + switch (command) |
| 28 | + { |
| 29 | + // ... 其他指令 ... |
| 30 | + case "my_command": |
| 31 | + return ValidateMyCommandParams(parameters); |
| 32 | + // ... |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +// 编写具体的验证函数 |
| 37 | +private static (bool isValid, MCPError error) ValidateMyCommandParams(JObject parameters) |
| 38 | +{ |
| 39 | + // 示例:必须包含 targetId |
| 40 | + if (parameters == null || !parameters.ContainsKey("targetId")) |
| 41 | + { |
| 42 | + return (false, new MCPError |
| 43 | + { |
| 44 | + Code = "MISSING_PARAM", |
| 45 | + Message = "Missing parameter: targetId" |
| 46 | + }); |
| 47 | + } |
| 48 | + return (true, null); |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +### 步骤 2: 实现处理逻辑 (ServerCommands.cs) |
| 53 | + |
| 54 | +在 `OpenRA.Mods.Common/ServerCommands.cs` 中添加静态处理方法。 |
| 55 | + |
| 56 | +```csharp |
| 57 | +// ServerCommands.cs |
| 58 | +
|
| 59 | +public static string MyCommand(JObject json, World world) |
| 60 | +{ |
| 61 | + // 1. 解析玩家 |
| 62 | + var player = ResolvePlayer(json, world); |
| 63 | + |
| 64 | + // 2. 解析参数 |
| 65 | + var targetId = json["targetId"]?.ToObject<int>(); |
| 66 | + |
| 67 | + // 3. 执行游戏逻辑 |
| 68 | + // ... |
| 69 | + |
| 70 | + return "Command executed successfully"; |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +### 步骤 3: 注册指令 (CopilotCommandServer.cs) |
| 75 | + |
| 76 | +在 `OpenRA.Game/CopilotCommandServer.cs` 的 `WorldLoaded` 方法中注册该指令。 |
| 77 | + |
| 78 | +```csharp |
| 79 | +// CopilotCommandServer.cs |
| 80 | +
|
| 81 | +public void WorldLoaded(World w, WorldRenderer wr) |
| 82 | +{ |
| 83 | + if (w.Type == WorldType.Regular && w.CopilotServer != null) |
| 84 | + { |
| 85 | + // ... |
| 86 | + w.CopilotServer.CommandHandlers["my_command"] = ServerCommands.MyCommand; |
| 87 | + // ... |
| 88 | + } |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +### 步骤 4: (可选) 实现复杂逻辑 Trait |
| 93 | + |
| 94 | +如果指令涉及跨多个 Tick 的操作(如 `expand_base` 需要造车、移动、展开、造建筑),建议创建一个独立的 `Trait`。 |
| 95 | + |
| 96 | +1. 在 `OpenRA.Mods.Common/Traits/Copilot/` 下创建新文件(如 `MyComplexManager.cs`)。 |
| 97 | +2. 实现 `ITick` 接口以处理每帧逻辑。 |
| 98 | +3. 在 `ServerCommands.cs` 中获取该 Trait 并调用其方法。 |
| 99 | + |
| 100 | +## 3. 经验总结与避坑指南 (重要) |
| 101 | + |
| 102 | +在开发 `expand_base` 指令的过程中,我们总结了以下关键经验,请务必阅读以避免走弯路: |
| 103 | + |
| 104 | +### 3.1 必须在 YAML 中注册 Trait |
| 105 | +**现象**:代码写得完美无缺,但在运行时调用指令返回 `Player does not have X trait`。 |
| 106 | +**原因**:C# 代码中定义了 Trait 只是第一步,**必须**在模组的规则文件(`rules/player.yaml` 或 `rules/world.yaml`)中显式添加该 Trait,否则它不会被加载到 Actor 上。 |
| 107 | +**对策**: |
| 108 | +* 检查 `mods/ra/rules/player.yaml` |
| 109 | +* 检查 `mods/cnc/rules/player.yaml` |
| 110 | +* 确保你的 Trait 名称(如 `CopilotExpansionManager`)出现在 `Player` 定义下。 |
| 111 | + |
| 112 | +### 3.2 生产队列的“批处理”与“单次” |
| 113 | +**现象**:请求建造多个建筑(如2个矿场),但只造了1个就不动了,或者逻辑卡死。 |
| 114 | +**原因**:旧的逻辑是“检查是否在造 -> 如果没在造 -> 发送一个建造指令”。但这会导致每次 Tick 都尝试发指令,或者造完一个后无法自动衔接下一个。 |
| 115 | +**对策**: |
| 116 | +* 使用 `EnsureProduction(type, quantity)` 模式。 |
| 117 | +* 计算 `需建造总数 - (当前队列中数量 + 已完成未放置数量)`。 |
| 118 | +* 使用 `Order.StartProduction(actor, item, count)` 一次性发送剩余所需的数量,让引擎内部的队列系统去管理排队。 |
| 119 | + |
| 120 | +### 3.3 Order 的执行与 Target |
| 121 | +**现象**:发送了 `Move` 或 `Deploy` 指令,但单位没有任何反应。 |
| 122 | +**原因**: |
| 123 | +1. **Target 构造错误**:`Target.FromCell` 和 `Target.FromActor` 必须正确使用。 |
| 124 | +2. **Order 构造函数**:某些 Order(如 `DeployTransform`)需要特定的构造函数参数(如 `suppressVisualFeedback` 或 `queued`)。 |
| 125 | +3. **队列阻塞**:如果不使用 `queued=false`(即立即执行),新指令可能会被追加到现有指令(如巡逻)之后而迟迟不执行。对于紧急指令,通常应设为不排队(覆盖当前指令)。 |
| 126 | + |
| 127 | +### 3.4 状态机管理长流程 |
| 128 | +**场景**:`expand_base` 需要:造MCV -> 等待MCV -> 移动MCV -> 展开 -> 造电厂 -> 放电厂 -> 造矿厂... |
| 129 | +**经验**:不要试图在一个函数里做完。使用 `enum` 定义状态机(State Machine),在 `ITick.Tick` 中根据当前状态执行微小的一步。 |
| 130 | +* **Waiting 状态**:每个动作发出后,进入对应的 Waiting 状态(如 `WaitingForMCV`)。 |
| 131 | +* **超时/重试**:设置 `waitTicks`,避免每帧都高频检查,同时也作为简单的超时重试机制。 |
| 132 | + |
| 133 | +### 3.5 错误信息反馈 |
| 134 | +**经验**:API 返回的错误信息越详细越好。 |
| 135 | +* 不要只返回 "Failed"。 |
| 136 | +* 如果是缺前置,调用 `DescribeMissingPrerequisites` 返回具体缺什么(如 "缺少:重工厂")。 |
| 137 | +* 如果是状态不对,返回当前正在做什么(如 "Base expansion already in progress")。 |
| 138 | + |
| 139 | +### 3.6 跨 Mod 兼容性 |
| 140 | +**现象**:代码在 RA (红警) 下能跑,在 CNC (泰伯利亚之日) 下报错或无反应。 |
| 141 | +**原因**:不同 Mod 的 Actor 命名不同。例如电厂在 RA 里叫 `pwr` 或 `apwr`,在 CNC 里可能叫 `nukr`。 |
| 142 | +**对策**:在代码中定义别名数组,如 `string[] powerNames = { "POWR", "APWR", "PWR", "NUKR" };`,并遍历尝试解析。 |
0 commit comments