lutzleonhardt/rct-bridge
GitHub: lutzleonhardt/rct-bridge
一个将运行中的 RollerCoaster Tycoon 1 进程内存以受控方式暴露给 LLM Agent 的 MCP 桥接器,通过内存窗口、字段掩码和 Undo Journal 实现可审计的游戏内存读写与回滚。
Stars: 0 | Forks: 0
# rct-bridge — RCT1 LLM 访客桥接器
该桥接器为 LLM(或 Shell 中的你)提供对在 Wine 下运行的 RCT.EXE 内存受控的读写访问权限。它被刻意设计为低级别:Agent 自行计算地址;桥接器仅强制执行内存窗口、字段掩码和 Undo-Journal。技术参考为本仓库中的 `PEEP_HANDBUCH.md`(§2 窗口,§4 掩码,§4.2 名称,§11 M1 配方,§12 冒烟测试)。
## 演示视频:优化游客幸福感
[优化 RollerCoaster Tycoon 中的游客幸福感](https://www.tella.tv/video/besucher-happiness-in-rollercoaster-tycoon-optimieren-76ky)
展示了桥接器的实际运行情况,其中 Claude Code 作为 Agent。所有步骤均通过 MCP 工具由 Agent 转换的自然语言指令:
1. 将游乐园游客分为两组:红组和蓝组,进行完全染色(衬衫和裤子)。为红组提供雨伞,将蓝组的雨伞收回(`snippets/outfit_split.py`)。
2. 随后 Agent 使天降雨(`snippets/make_rain.py`,通过 `globals_rw` 白名单进行天气调度)。
3. 蓝组游客离开游乐园(`snippets/leave_park.py`)。
4. 衣物变为幸福感热力图:绿色代表非常开心,从黄色、橙色过渡到深红色;通过 Cronjob 每分钟更新一次(`snippets/happiness_heatmap.py`,作为死循环也可使用 `bridge/heatmap_loop.py`)。
5. 趋势评估:游客当前是变得更开心还是不开心?
6. 最后,Agent 每分钟观察一次三口之家的 Mustermann 家庭,优化他们的互动以达到最大幸福感,并记录观察结果和决策——结果作为 `TAGEBUCH_MUSTERMANN.md` 保存在本仓库中。
## 文件分布
- `bridge/` — Python 包:Daemon、CLI、MCP 服务器、exec 脚本、测试。
- `PEEP_HANDBUCH.md` — 提纯的逆向工程参考(Offsets、规则、配方、冒烟测试)。
- `BNSkripte/` — 逆向工程工作中的 Binary Ninja 脚本,手册即基于此生成。
- `TAGEBUCH_MUSTERMANN.md` — 演示产物:上述演示视频中 Mustermann 家庭的日记。
## 安装(在游戏目录中创建符号链接)
仓库位于游戏目录之外;游戏目录仅通过符号链接与之关联。`bridge/config.py` 从包路径推导游戏目录,而不解析符号链接——因此直接在游戏目录中运行 `python3 -m bridge.daemon` 也能正常工作。
1. 克隆仓库,例如至 `~/projects/rct-bridge`。
2. 在游戏目录中(在 `RCT.EXE` 旁边)创建两个必需的符号链接:
`ln -s ~/projects/rct-bridge/bridge bridge` 和
`ln -s ~/projects/rct-bridge/PEEP_HANDBUCH.md PEEP_HANDBUCH.md` — `handbook` MCP 工具需要手册链接,它会在游戏目录中搜索该文件。
3. 随后,在游戏目录中启动 Daemon、CLI 和 MCP 服务器,如下所述。
此处的参考设置:通过 Bottles 安装的 GOG 版 RollerCoaster Tycoon Deluxe;直接使用系统 Wine 并设置 `WINEPREFIX` 启动,gamescope 负责缩放。
## 架构
```
Claude Code ─stdio/JSON-RPC─▶ MCP-Server ─Unix-Socket─▶ Daemon ─/proc//mem─▶ RCT.EXE (Wine)
(dünner Adapter) (Maske, Journal, exec-Sandbox)
```
一个长驻 Daemon 将 `wine RCT.EXE` 作为子进程启动,并保持 `/proc//mem` 文件描述符处于打开状态。这不是个人偏好问题,而是 Ancestor 陷阱:在 `kernel.yama.ptrace_scope=1` 时,只有祖先进程才能打开游戏内存。如果短生命周期的 CLI 调用生成了 Wine 并退出,下一次调用将无法再访问该进程。因此,由 Daemon 执行所有访问(Single Writer);掩码、Journal 和 exec 沙箱仅在那里存在。客户端通过 Unix Socket 使用 JSON 行协议进行通信;CLI 只是对此的纯 Shell 封装,而 MCP 服务器(`bridge/mcp_server.py`)只是另一个轻量级客户端——它将 MCP 工具调用一一映射为此协议。
## 启动
1. 像手动启动游戏一样准备环境并切换到游戏目录——Daemon 会将其环境变量继承给游戏:
export WINEPREFIX="/home/lutz/Downloads/rct_deluxe/RCT-Deluxe"
cd "$WINEPREFIX/drive_c/GOG Games/RollerCoaster Tycoon Deluxe"
2. 启动 Daemon。最简单的情况:`python3 -m bridge.daemon`(直接生成 `wine RCT.EXE`)。带 gamescope 的参考设置(3072×2304,Nearest-Neighbor 缩放,针对 GOG 版本的 ddraw 覆盖):
python3 -m bridge.daemon --cmd 'gamescope -w 3072 -h 2304 -W 3072 -H 2304 -F nearest -- env WINEDLLOVERRIDES=ddraw=n,b wine RCT.EXE'
Daemon 将命令作为子进程启动,并在其**后代**中查找游戏(Daemon → gamescope → env → wine)——Yama Ancestor 规则涵盖任意深度的后代,因此中间的 Launcher 不是问题。一旦后代将 RCT.EXE 映射到 0x400000 处(“MZ”检查),即视为找到游戏;此后,Daemon 将在 Socket 上监听(默认:`$XDG_RUNTIME_DIR/rctbridge.sock`)。
3. 在第二个 Shell 中使用 CLI,例如
`python3 -m bridge.cli read globals ticks`。
实用技巧:`alias rctbridge='python3 -m bridge.cli'`。
4. 使用 `python3 -m bridge.cli shutdown` 退出——这将关闭 Launcher 和游戏(可在 Daemon 启动时使用 `--leave-game` 禁用,但注意:在 ptrace_scope=1 时,新的 Daemon 无法再访问孤儿进程)。
变体:`--fake` 使用 FakeMemory 后端运行 Daemon(用于在没有游戏的情况下测试协议),`--attach PID` 挂载到正在运行的实例(需要 `sudo sysctl kernel.yama.ptrace_scope=0`)。
## 协议(每个请求和响应对应一行 JSON)
| Op | 示例请求 |
|---|---|
| read (Region) | `{"op":"read","region":"sprites","idx":17,"off":34,"size":2}` |
| read (Global) | `{"op":"read","region":"globals","name":"ticks"}` |
| write | `{"op":"write","region":"sprites","idx":17,"off":48,"data":"1c"}` |
| write (Global) | `{"op":"write","region":"globals","name":"climate_rain_level","data":"01"}` |
| exec | `{"op":"exec","code":"...","args":{"idx":17}}` |
| rollback | `{"op":"rollback","n":1}` 或 `{"op":"rollback","all":true}` |
| status | `{"op":"status"}` |
| shutdown | `{"op":"shutdown"}` |
响应为 `{"ok":true, ...}` 或 `{"ok":false,"error":"..."}`。
字节以 Hex 字符串形式传输。`status` 提供自检信息:包含 Base/Stride/Mode 的所有 Region 以及可读 Global 的名称。
## 窗口与掩码
Region 对应手册 §2:`sprites`(RW 带字段掩码),`strings`(RW,强制执行 Slot 边界),`sprite_lists` 和 `rides`(RO),`globals`(具名地址,除了白名单 `globals_rw` 外均为 RO——目前为天气配方的 Climate 块,手册 §10;仅允许对 Global 的完整宽度进行写入)。字段掩码是一个白名单(§4):仅可写入 Name-ID、颜色、Invalidate 标志、peep_flags、Pathfind-Goal-Reset 和 time_lost——加上 `+0xC5/+0xC6`(Heading/Countdown),这两项仅在有访客占用 Slot(`peep_type == 0`)时可写入,因为员工会将这些字节作为 Order 字段回收利用(§4.1)。按设计原则,Stats、Position、State 和 Money 均不可写入。
## exec
对于批量操作(逐个 poken 800 名游客会因延迟而死),可发送在 Daemon 中运行的 Python 脚本。它只能访问以下四项内容:
1. 包含 `read(region, idx, off, size)`、`read_global(name)`、`write(region, idx, off, bytes)` 和 `write_global(name, bytes)` 的 `bridge` —— 写入操作受掩码/白名单和 Journal 限制,沙箱不会开后门。
2. 用于处理 u16/u32 位级操作的 `struct`。
3. `args` —— 请求中的 JSON 对象(CLI:`--args '{"idx":17}'`)。
4. 用于中间输出的 `log(...)`;结果存入 `result`。
没有 `import`,没有 `open`,没有 `os`。防护目标是防止意外,而非防御攻击者。示例位于 `snippets/` 中(访客扫描、名称查找、M1-goto、天气调度 `make_rain.py`)。注意:脚本中的无限循环会阻塞 Daemon——v0 刻意不设超时。
因此,重复执行的扫描任务不应作为循环放在脚本中,而应放在客户端:`python3 -m bridge.heatmap_loop` (S11) 每 10 秒发送一次来自 `snippets/happiness_heatmap.py` 的扫描(根据 Happiness 设置红绿灯颜色,仅在 Bucket 更改时写入)——模式为“无 LLM 的反射层”:每次请求建立新连接,Daemon 在两次扫描之间保持空闲。停止方式:Ctrl-C 或 `touch bridge/heatmap.stop`。
## Journal 与 Rollback
在执行任何写入操作之前,都会将旧字节和新字节写入 JSONL 文件(默认 `bridge/journal.jsonl`,支持跨运行的审计),并推入 Undo Stack。`rollback` 按后进先出 (LIFO) 原则恢复旧字节——该 Stack 仅在当前 Daemon 运行时有效。
## MCP 服务器
`python3 -m bridge.mcp_server` 通过 stdio 将 Daemon 暴露为 MCP 服务器(JSON-RPC 2.0,每行一条消息;像其他部分一样仅使用标准库实现)。
它是一个没有自身逻辑的纯适配器:每次工具调用都会与 Daemon 建立全新的 Socket 连接——这样,顺序执行的 Daemon 绝不会卡在持久连接中,并在两次请求之间保持空闲(后续在此处运行 Watcher)。因此,Daemon 必须已经在运行;MCP 服务器刻意不启动游戏(规避 Ancestor 陷阱)。
在 Claude Code 中注册(从游戏目录执行):
```
claude mcp add rct-bridge -- python3 -m bridge.mcp_server
```
使用不同的 Socket:`claude mcp add rct-bridge -- python3 -m bridge.mcp_server --socket /pfad/zur.sock`。
这些工具与协议 Op 一一对应:`read`、`read_global`、`write`、`write_global`、`exec`、`rollback`、`status` —— 加上 `handbook`,用于查阅 `PEEP_HANDBUCH.md`(不带参数则返回目录,带 `section` 则返回标题中包含搜索文本的第一个章节)。
`shutdown` 刻意不作为工具暴露:LLM 客户端不应能够终止游戏;此权限仅限于 CLI。
可观测性展望(已决定,将在 MCP 服务器之后实现):Daemon 中的 `watch`/`poll_events` 工具。在 Claude Code 中,该调用作为后台 Bash 进程启动,一旦事件发生即终止——从而唤醒 Main Agent(刻意针对 Claude Code 的特定设计)。设计重点:在 Daemon 中阻塞的 Wait 会导致顺序执行的 Serve Loop 卡死——要么由脚本本身在循环中轮询,要么 Daemon 设置 accept 超时并在连接之间检查条件。
## 测试
`python3 -m pytest bridge/tests/ -q` —— 完全脱离 Wine 针对 FakeMemory 运行(地址计算、包含 Gate 的掩码、Journal、Rollback、exec 沙箱、协议、包含 Socket 端到端测试的 MCP 适配器)。针对游戏的实时验证刻意未采用 pytest:它们以带编号的脚本形式存放在 `bridge/SMOKE.md` 中。
## v0 的限制
没有 Watcher(`register_watch`/`poll_events` 将在后续作为新的 Op 和 MCP 工具加入,见上文),exec 无超时,`rides` 以及 Item-/气球颜色仍为 RO。
## 许可证
代码采用 MIT 许可证(参见 `LICENSE`)。文档资料 `PEEP_HANDBUCH.md` 和 `TAGEBUCH_MUSTERMANN.md` 采用 [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) 许可证——只要注明原作者,即允许转载使用。本仓库不包含任何游戏文件;RollerCoaster Tycoon 的版权归其权利所有者所有。
标签:LLM代理, MCP服务器, Python, Wine, 云资产清单, 内存修改, 无后门, 游戏自动化, 逆向工具, 逆向工程