# MeowSousChef **Repository Path**: merchannel/meowsouschef ## Basic Information - **Project Name**: MeowSousChef - **Description**: 胡闹厨房2 的 BepInEx 模组:游戏内 WebSocket 服务器,外部客户端可添加/移除玩家、接收场景广播,统一 JSON 协议。正在开发厨师控制与对局数据推送。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-05 - **Last Updated**: 2026-10-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # MeowSousChef 🐱👨‍🍳 《胡闹厨房 2》(Overcooked! 2) 的 BepInEx 模组:在游戏内启动一个 WebSocket 服务器,把游戏接入外部程序——远程控制面板、AI 厨师、陪玩机器人,想怎么玩都行。 > ⚠️ 仅供学习研究。请不要在联机对局里用于影响他人体验的用途。 ## 功能 - **WebSocket 服务器**:监听 `ws://127.0.0.1:48933/`(可配置),最多 4 个客户端同时在线,满员拒绝 - **统一 JSON 协议**:所有消息共用一个信封(动作 / 状态 / 数据),自带请求-响应关联 - **玩家生命周期**:外部客户端可"加入游戏"成为真实玩家——`addplayer` 后进关卡会生成厨师,断开连接自动移除;支持自定义名字、游戏内 UI 移除联动、名额式编号(本机玩家 P1,外部 P2/P3/P4) - **场景广播**:Unity 场景切换(主菜单 / 选关地图 / 关卡)与对局流程阶段(开场演出 / 做菜中 / 结算)实时推送给所有在线客户端 - **对局数据推送**:剩余时间、分数 / 连击 / 小费倍率 / 上菜数、活跃订单(菜名 / 食材清单 / 装盘步骤 / 剩余时间) - **厨房状态推送**:台子(物品 / 盘里食材 / 加工进度 / 出餐口盘子)、烹饪(进度 / 煮熟 / 烧焦)、厨师(位置 / 携带物品 / 盘里食材),1 秒轮询 + 内容去重,无变化不刷屏 - **自动做菜(受限支持)**:仅接受真实玩家、单厨师和首个单食材订单;按订单匹配食材箱,验证取料,按下一加工阶段判断是否需要切菜,放到菜板后在原地切菜并原地取回,再核验装盘并将餐盘送到空上菜口。只有游戏回调确认目标订单成功后才报告完成;复合配方、烹饪及其他无法验证的步骤会安全停止 - **热重载友好**:配合 BepInEx Script Engine,改完代码构建即自动重载 ## 环境要求 - 《胡闹厨房 2》(Steam 版) - [BepInEx 5.4.23](https://github.com/BepInEx/BepInEx/releases)(x64 Unity Mono 版)+ [Script Engine](https://github.com/yyamnb/BepInExScriptEngine)(热重载,可选) - .NET SDK 8.0+(构建用) ## 构建 ```bash dotnet build -c Release ``` 产物自动输出到游戏的 `BepInEx\scripts\` 目录(含 `MeowSousChef.dll`)。没有 Script Engine 的话,手动把 DLL 放进 `BepInEx\plugins\` 也可以。 `lib/` 目录下的引用 DLL 不随仓库分发,需要自行放置: | DLL | 来源 | |---|---| | `Assembly-CSharp.dll`、`Assembly-CSharp-firstpass.dll`、`UnityEngine*.dll` | `Overcooked! 2\Overcooked2_Data\Managed\` | | `BepInEx.dll`、`0Harmony.dll`、`MonoMod.RuntimeDetour.dll` | `BepInEx\core\` | | `websocket-sharp.dll` | [websocket-sharp 1.0.2](https://www.nuget.org/packages/WebSocketSharp)(net35 版,也需复制到 `BepInEx\plugins\`) | ## 配置 生成于 `BepInEx/config/merchannel.meowsouschef.cfg`: | 配置项 | 默认值 | 说明 | |---|---|---| | `Server.Port` | `48933` | 监听端口(仅监听 127.0.0.1) | | `Server.MaxClients` | `4` | 最大同时在线客户端数 | | `Server.VirtualPlayers` | `false` | 虚拟玩家模式:addplayer 只分配编号不接触游戏(协议测试用) | ## 协议 所有消息都是 JSON 对象,字段统一: | 字段 | 类型 | 说明 | |---|---|---| | `action` | string | 动作名:请求=要做什么;推送=事件名 | | `status` | string | `"ok"` / `"error"`(服务端发出必带) | | `data` | object/null | 数据负载 | | `msg` | string | 可选,错误说明 | | `id` | any | 可选,客户端请求带的 id 原样回显(请求-响应关联;服务端主动推送不带) | | `seq` | long | **仅服务端出站**:出站消息序号,从 1 开始严格单调递增 | | `ts` | long | **仅服务端出站**:服务端 Unix 时间(秒),与 `ping` 的 `time` 同单位 | 客户端 → 服务端的请求**不带** `seq`/`ts`;响应会带上服务端自己的序号。 ### `seq` 与 `ts` 的约定 - **只保证单调,不保证连续。** 没有接收方时序号照样消耗(例如 `PushExcept` 命中 0 个目标), 所以客户端**不要**按 `prev + 1` 校验,只判断"有没有变小"。 - **`seq` 变小 = 服务端重启过**(进程退出或插件热重载),此时本地缓存的快照作废, 应发 `getstate` 重新对齐。`welcome` 永远是每个连接的第一条消息,是重新对齐的时机。 - **`ts` 是服务端时钟**,只用于判断缓存快照的陈旧度,不要拿来做客户端超时判断。 ### 客户端 → 服务端 | 动作 | 说明 | |---|---| | `{"action":"ping"}` | 心跳,返回在线人数/最大人数/服务器时间 | | `{"action":"list"}` | 当前连接的会话 id 列表 | | `{"action":"getstate"}` | 主动查询全量快照:`data` 内含 `scene` / `level` / `stations` / `chefs` / `players` 五个子对象,结构与对应推送完全一致(重连后对齐状态用,响应会回显请求 `id`) | | `{"action":"addplayer","data":{"name":"猫咪厨师"}}` | 为当前连接添加一个玩家(name 可选,≤24 字符);重复添加报错 | | `{"action":"removeplayer"}` | 移除当前连接绑定的玩家 | | `{"action":"autocook_start"}` | 启动受限自动做菜:控制绑定玩家完成首个单食材订单的取料、必要切菜、装盘和上菜;不支持的订单类型会拒绝启动 | | `{"action":"autocook_stop"}` | 停止自动控制并恢复原输入方案 | ### 服务端 → 客户端 | 动作 | 触发时机 | |---|---| | `welcome` | 连接成功(含本会话 id、当前人数) | | `join` / `leave` | 有客户端连入 / 断开 | | `reject` | 服务器满员,随后以 1008 关闭连接 | | `player_added` / `player_removed` | 玩家添加 / 移除(含编号、名字、会话) | | `players` | 新客户端连上时定向补发一次当前玩家列表(元素字段同 `player_added`,不含 `clients`) | | `scene` | Unity 场景切换或对局阶段变化 | | `level` | 对局数据变化:剩余时间(整秒粒度)、分数、订单增删 | | `stations` | 台子 / 烹饪状态变化(1 秒轮询去重) | | `chefs` | 厨师位置或携带物变化(1 秒轮询去重) | | `autocook_state` | 自动控制阶段、目标订单、完成/停止/错误原因 | | 对未知 action / 非 JSON 输入 | `status:"error"` + 原因 | 五个推送在**无客户端连接时跳过**,但新客户端连上时会**一次性定向补发全部五个域**(含主菜单下的 `level` 空值与空的 `stations`/`chefs`/`players`)—— **连上即拿到完整状态**,不必等下一次变化。 连上补发期间若状态恰好发生变动,抑制窗口结束后会**对其余客户端补播**对应域:不会因为别人的重连,让老客户端漏掉一次更新。 `level` 推送最多 2 次/秒,但**只合并不丢弃**——节流窗口内累积的变化会在窗口结束时补推一条全量快照。 ### 入站限制 | 限制 | 默认值 | 超出时 | |---|---|---| | 单条消息字节 | 8 KB | 回 `status:"error"` + `message too large`,不入队 | | 待处理消息队列 | 512 条 | 丢弃新消息;连接 / 断开 / 拒绝等**控制事件不受限**,告警限频 | | 单帧处理事件数 | 64 条 | 顺延到下一帧,避免消息洪水拖垮游戏帧率 | ### 推送字段实测值 #### scene | 字段 | 含义 | |---|---| | `scene` | **当前界面的权威信号**:`StartScreen`(主菜单)、`WorldMap`(选关地图)、`Loading`(读条)、`InGameMenu`(对局/大厅 UI 层)、`s_系列_关`(厨房关卡,如 `s_sushi_1_1`) | | `phase` | 对局内流程阶段:`RunLevelIntro`(开场演出) → `InLevel`(做菜中) → `RunLevelOutro`(结算);非对局为 `null` | 判断"是否在对局中"用 `phase != null`,不要只看 `InGameMenu`(它在结算后的地图界面也会叠加加载)。 #### level | 字段 | 含义 | |---|---| | `timeLeft` / `timeLimit` | 剩余秒数 / 总时长(`-1` = 不在对局中或读取失败) | | `score` | 总分(`TotalBaseScore + TotalTipsScore - TotalTimeExpireDeductions`,权威值) | | `combo` / `delivered` / `multiplier` | 连击数 / 本关成功上菜数 / 小费倍率 | | `orders[]` | 活跃订单数组,元素字段见下 | `orders[]` 元素: | 字段 | 含义 | |---|---| | `id` | 订单号(游戏 `OrderID.m_id`,8 位数字) | | `recipe` | 菜名(订单定义节点的资产名,如 `Salad`) | | `platingStep` / `platingStepIcon` | 装盘步骤资产名 / 其图标资产名(常含菜品信息) | | `recipeUid` | 装盘步骤 ID(`m_uID`),可用于订单匹配 | | `ingredients[]` | 需要的食材(订单 UI 树的叶子块主图资产名,`_Icon` 后缀已剥离,如 `Rice`/`Fish`/`Nori`) | | `remaining` / `lifetime` | 剩余 / 总存活秒数(`-1` = 未知) | #### stations | 字段 | 含义 | |---|---| | `stations[]` | 设施数组,元素字段见下 | | `cooking[]` | 正在烹饪的锅数组 | `stations[]` 元素:`type`(设施类型枚举名)、`entityId`、`name`、`x`、`z`,以及按设施类型可选填充的 `hasItem`、`item`(台上物品资产名,随加工阶段变化)、`itemContents`、`working`、`progress`(加工进度 0..1)、`finished`、`hasPlate`。**不适用的字段直接不下发**,客户端按"无此状态"处理。 `itemContents` 是**容器类物品(脏盘子、锅)里装的东西**,数组形式(多个食材就多元素),例如出餐口放着装了切好鱼的盘子时 `item: "Plate 1"` + `itemContents: ["ChoppedFish"]`。不是容器、或容器为空时不下发。 `cooking[]` 元素:`progress`(已烹饪秒数)、`cookTime`(煮熟所需秒数)、`cooked`、`burning`、`state`(`Progressing` / `Completed` / `OverDoing` / `Ruined`)、`stationType`(`Hob` / `Oven` / `DeepFatFryer` / `FirePit` 等)、`x`、`z`。 #### chefs `data` 直接是数组,元素为:`name`(厨师 GameObject 名)、`entityId`、`x`、`z`,以及可选的 `carrying` / `carryingSecondary`(主手 / 背包携带物品名,空手不下发)、`carryingContents`(携带容器里装的东西,语义同 `stations.itemContents`,例如拿着装了切好鱼的盘子:`carrying: "Plate 1"` + `carryingContents: ["ChoppedFish"]`)。 ## 目录结构 ``` MeowSousChef/ ├── MeowPlugin.cs 入口:装配状态聚合 / Socket / 玩家 / 推送 / hooks ├── Core/ 纯基础设施(零游戏引用) │ ├── MeowJson.cs 迷你 JSON(序列化 + 解析,零依赖,线程安全) │ ├── MeowSocket.cs WebSocket 服务器 + 连接事件 + 入站限制 │ ├── MeowProtocol.cs 出站信封 + seq/ts 计数(零依赖,可独立测试) │ ├── MeowLog.cs 日志源集中登记(按名复用,热重载不重复挂进 Logger.Sources) │ └── MeowConfig.cs BepInEx 配置 └── Game/ 游戏相关 ├── Manager/ │ ├── GameStateManager.cs 场景/阶段/时间/分数/订单 状态 + 变更事件 │ ├── StationManager.cs 台子与烹饪状态(1 秒轮询 + 签名去重) │ ├── ChefManager.cs 厨师位置与携带物(1 秒轮询 + 签名去重) │ ├── PushManager.cs 事件 → WebSocket 推送路由 + 新连接快照补发 │ ├── HookManager.cs Harmony 补丁统一注册/移除(失败隔离,热重载安全) │ ├── MeowPlayerManager.cs 玩家生命周期绑定管理 │ └── PlayerService.cs IPlayerService + 虚拟/真实实现 ├── Hook/ │ ├── GameStateHook.cs 对局流程状态切换 │ ├── LevelHooks.cs 计时器 / 订单 / 分数读取(含反射成员缓存) │ ├── ItemInspector.cs 容器内容物读取(脏盘子里的食材) │ └── PlayerHooks.cs 4 个兼容补丁(游戏假设"每个用户都有手柄",虚拟玩家没有) └── State/ MeowGameState 聚合根:唯一数据源,纯数据无逻辑 ├── MeowGameState.cs 聚合根:Scene / Level / Chefs / Stations / Cooking ├── SceneState.cs 场景名 + 对局流程阶段 ├── LevelState.cs 时间 / 分数 / 连击 / 订单 ├── MeowOrderState.cs 订单:菜名 / 食材清单 / 剩余时间 ├── ChefState.cs 厨师:位置 / 携带物品(含盘里食材) ├── StationState.cs 台子:物品 / 盘里食材 / 加工进度 / 出餐口盘子 ├── MeowCookingState.cs 烹饪:进度 / 煮熟 / 烧焦 └── StateSignature.cs 台子/烹饪/厨师去重签名(纯函数,量化粒度集中在此) └── MeowSousChef.Tests/ 自动化测试(NUnit,net48,仅引用零依赖纯逻辑) ├── MeowJsonTests.cs 序列化/解析:转义、数字类型、深度上限、垃圾输入不抛异常 ├── MeowProtocolTests.cs 信封字段 + seq 单调性/并发唯一性 + ts 量级 └── StateSignatureTests.cs 签名去重:哪些字段参与、量化到什么粒度 ``` 依赖方向:入口 → `Game.Manager` → (`Game.Hook`, `Game.State`, `Core`);`Game.Hook` 也取用 `Core` 的日志源。`Core` 不引用任何游戏程序集。 数据流:`Hook` 提取 → `Manager` 就地更新 `MeowGameState` → 变更事件 → `PushManager` 序列化 → WebSocket。所有游戏侧逻辑都在主线程执行(WebSocket 后台回调统一入队,由 `MeowSocket.PumpEvents` 消费)。 ## 测试 ```powershell dotnet test -c Debug # 默认 Debug(推荐) ``` 设计取舍: - **引用插件本体,不复制源码。** 测试工程目标 `net48`,通过 `ProjectReference` 引用 `net35` 的插件 (低版本 .NET Framework 可被高版本消费),所以跑的是**真正编译出来的那套程序集**; internal 成员靠 csproj 里的 `` 对测试程序集开放,不为此把 API 改成 public。 - **只测零依赖纯逻辑。** `MeowJson` / `MeowProtocol` / `StateSignature` 都不引用 BepInEx / Unity / websocket-sharp,因此测试不必起 WS 服务器、不必起游戏。轮询、Hook、推送节流等强依赖运行时的逻辑 仍需进游戏验证。 - **必须用 Debug 跑。** 主工程 `Release` 的 `OutputPath` 指向 `BepInEx\scripts`, `dotnet test -c Release` 会顺带把插件 DLL 写进游戏目录(那是部署动作,不是测试动作)。 - 浮点断言只用二进制精确可表示的值(`1.5` / `2.25`),非精确值改由往返测试覆盖—— `double.ToString("R")` 在 net35 与 net48 之间有历史差异,断言具体字面量会变成在测运行时。 已覆盖 42 条:JSON 序列化/解析(含畸形输入与 1 万层嵌套不抛异常、不栈溢出)、信封字段与 `seq` 单调性 / 并发唯一性、签名去重的字段覆盖与量化粒度。 ## 路线图 - [x] **玩家生命周期**:`addplayer` / `removeplayer`、断开自愈、游戏内 UI 移除联动 - [x] **对局数据推送**:场景 / 阶段 / 剩余时间 / 分数 / 订单 - [x] **厨房设施状态推送**:台子物品与加工进度、出餐口盘子、锅的进度与烧焦、厨师位置与携带物、脏盘子里的食材 - [ ] **控制厨师**:注入自定义输入源,WebSocket 指令驱动移动 / 交互 / 拿放 - [x] **主动查询**:`getstate` 一次返回 `scene` / `level` / `stations` / `chefs` / `players` 全量快照 - [x] **出站序号**:每条服务端消息带 `seq`(单调递增)与 `ts`,客户端据此判断重启与快照陈旧度 - [x] **自动化测试**:`MeowJson` / `MeowProtocol` / `StateSignature` 三块纯逻辑,`dotnet test` 一键跑 ## 致谢 - [BepInEx](https://github.com/BepInEx/BepInEx) / [HarmonyX](https://github.com/BepInEx/HarmonyX) - [websocket-sharp](https://github.com/sta/websocket-sharp) - 数据结构分析基于 dnSpy 对 `Assembly-CSharp.dll` 的逆向