OpenRCT2 — 把一款 x86 汇编游戏彻底用 C++ 重写
是什么
OpenRCT2 是 RollerCoaster Tycoon 2(过山车大亨 2)的完整开源 C++ 重实现。日常类比:就像有人把一本只能看、无法改的手写食谱,逐字翻译成可以协作编辑的电子文档,然后顺手把缺页补全了。
原版 RCT2 由 Chris Sawyer 一人用 x86 汇编独立编写,代码极度优化但完全不可读,只跑在 Windows 上。OpenRCT2 的目标是:用 C++ 完全重现原版玩法,同时新增在线多人联机、跨平台支持(Windows / macOS / Linux)、更高的游戏限制、TypeScript 插件 API,以及一批 QoL 改进。
项目起步于 2014 年,2023 年完成最后一批核心汇编代码的 C++ 转换,约 15k GitHub stars,是游戏逆向工程领域最完整的开源重实现案例之一。
为什么重要
不了解 OpenRCT2,下面这些问题都没法解释:
- 为什么 x86 汇编游戏能在 macOS 和 Linux 上运行,却不需要 Wine 或虚拟机
- 为什么”不改变玩法”的情况下,多人联机可以事后追加到一个单机游戏里
- 为什么逆向工程进度可以在保证”零回归”的前提下分阶段推进十年
- 为什么社区 mod 可以用 TypeScript 写,而不用修改任何 C++ 引擎代码
核心要点
-
逐步替换策略:不是从零重写,而是把 x86 汇编逐片段翻译成等价 C++,用与原始寄存器同名的全局变量保持过渡期兼容,再分批重构为类和模块。类比:修一艘正在航行的船,每次只换一块甲板,船不能停。这个方法把”全部重写”的风险分摊到数年。
-
确定性帧步进(Lockstep)多人模式:联机玩家的游戏状态必须完全同步。OpenRCT2 用锁步帧同步——所有玩家每一帧执行相同输入,最终状态必须比特级一致。为此所有浮点运算必须跨平台可复现,任何随机性要用固定种子。类比:乐队不靠指挥,靠所有人对着同一个节拍器演奏。
-
TypeScript 插件沙盒:引擎暴露 JavaScript 运行时(Duktape),允许插件用 TypeScript 读写游戏状态、注册自定义 UI、自动化操作。插件运行在沙盒里,无法破坏引擎核心。类比:Excel 里的宏——你能自定义工作表,但不能改 Excel 本身的公式引擎。这让社区在不 fork 的情况下无限扩展游戏。
实践案例
案例 1:把汇编全局变量迁移为 C++ 类成员
原始 x86 汇编里,玩家金钱直接存在某个固定内存地址(比如 0x013573DC)。第一步翻译时,代码变成:
// money32 是 OpenRCT2 自定义的整型货币单位(1 单位 = 0.01 英镑),// 不是标准 C++ 类型——整个项目里统一用它避免浮点误差
// 过渡期:用全局变量对应原始地址static money32 gCash;
// 读取金钱的汇编等价体money32 GetCash() { return gCash;}几百次提交之后,才重构为:
// 重构完成:封装进 GameState 结构体struct GameState { money32 cash; // ...其他字段};GameState gGameState;关键点:两个阶段之间所有测试必须通过,游戏存档必须加载成功。每个”重构 PR”本身不改变任何可见行为——这是零回归迁移的核心原则。
案例 2:实现 TypeScript 插件来自动化建造
OpenRCT2 内置了 Duktape 解释器(可以理解为”游戏引擎里的微型浏览器 JS 引擎”),让玩家用 TypeScript 写自动化脚本而无需修改任何 C++ 代码,例如批量添加设施:
// plugin.ts — 自动在地图四角添加厕所function placeToiletsAtCorners() { const mapSize = map.size; const corners = [ { x: 2, y: 2 }, { x: mapSize.x - 2, y: 2 }, { x: 2, y: mapSize.y - 2 }, { x: mapSize.x - 2, y: mapSize.y - 2 }, ]; for (const pos of corners) { // 调用引擎暴露的 API 建造设施 map.place({ type: "facility", object: "toilets", ...pos, z: 0 }); }}// 在游戏菜单里注册为一个可点击的按钮ui.registerMenuItem("Place Corner Toilets", placeToiletsAtCorners);关键点:脚本运行在沙盒里,无法访问任何不在官方 API 里的内部状态,引擎版本升级时 API 向后兼容。这让社区数百个插件可以长期维护而不随引擎改动而失效。
案例 3:调试多人帧不同步
联机时如果两个客户端的游戏状态产生偏差(desync,即”两人看到的游乐园已经不一样了”),游戏会立刻断线以防止进一步撕裂。排查方法:在两台联机的电脑上分别打开终端,运行以下命令过滤状态哈希日志:
# 在终端(命令行)里运行 OpenRCT2 并过滤出含 "hash" 的行# "grep" 是过滤命令,只显示匹配关键词的日志openrct2 --network-log-level=verbose 2>&1 | grep "hash"
# 对比两台机器在第 N 帧输出的哈希值# 如果哈希不同,说明那一帧出现了分歧
# 常见根因:# - 随机数没用固定种子(改用 gScenarioRand,不能用 std::rand())# - 平台浮点精度差异(改用整数或定点数替代浮点)# - 计时器精度在 Windows/Linux 上不一致关键点:状态哈希是多人模式的”体检报告”,每帧广播一次;客户端对比发现偏差立刻报告,比”等到视觉上出问题”早几十帧。这种方法也适用于任何需要确定性模拟的联机游戏。
踩过的坑
-
必须提供原版 RCT2 安装目录:OpenRCT2 不附带游戏资源(图形、声音、地图),版权归 Atari。拿不到原版文件就无法启动,连 demo 都跑不了。
-
汇编遗留大量隐式全局状态:原作者用汇编直接操作内存,没有”对象”的概念。翻译出来的 C++ 充满互相依赖的全局变量,改一处容易触发距离很远的副作用。每次重构都要大量回归测试。
-
浮点不一致导致联机断线:不同编译器、不同 CPU 处理浮点的行为有细微差异(比如 x87 vs SSE2 精度)。任何用于游戏逻辑的浮点运算都需要用整数或定点数重写,否则联机必然断线。
-
旧存档格式无公开文档:
.sv6(存档)和.sc6(场景)格式由 Chris Sawyer 自己设计,从未公开。所有字段语义靠反汇编原版执行文件逐比特推断,加上社区多年对齐测试。新增字段时要维护向前兼容性。
适用 vs 不适用场景
适用:
- 逆向工程老游戏、老软件并发布为开源版本(参考路径:OpenTTD / OpenRA / openage)
- 给没有 mod 支持的老游戏追加插件系统,且不想 fork 或改核心
- 需要在原始单机游戏基础上实现联机,但引擎架构完全不考虑网络的情况
- 学习”如何在不中断服务的前提下大规模重构遗留系统”
不适用:
- 从头构建新游戏(用 bevy / panda3d 等游戏引擎更高效)
- 需要完全原版体验但不想提供 RCT2 授权文件(OpenRCT2 不能替代购买原版)
- 期望插件能访问底层渲染管线(插件 API 只暴露游戏逻辑层,不涉及 OpenGL/Vulkan)
- 汇编基础薄弱而又必须深度参与核心逆向工作的团队
历史小故事(可跳过)
- 2002 年:RollerCoaster Tycoon 2 发售,Chris Sawyer 用汇编独立完成整个引擎,代码极度紧凑。
- 2011 年:社区开始研究 RCT2 可执行文件,最初只是用内存补丁解锁游戏内的限制(骑乘高度、游客数量等)。
- 2014 年:开发者 IntelOrca 发起 OpenRCT2 项目,目标是用 C++ 完整重写,让游戏跨平台并开放源码。
- 2018 年:在线多人模式正式稳定,lockstep 帧同步实现,全球玩家可以联机建园。
- 2020 年:TypeScript 插件 API 发布,社区数百个插件涌现,覆盖自动化、数据分析、自定义 UI。
- 2023 年:核心游戏逻辑中最后一批 x86 汇编等价代码完成 C++ 重构,标志着”翻译阶段”正式结束,进入纯 C++ 开发模式。
学到什么
- 逆向工程 + 迁移可以分阶段做:不需要一次性重写,逐片翻译 + 过渡期全局变量 + 大量测试,能把风险降到每次只承担一小步。
- 确定性模拟是联机的基础:多人模式不靠服务器权威,而是靠”所有客户端执行完全相同的计算”。任何非确定性(随机、浮点、时序)都是 bug。
- 好的插件 API 比 fork 更有生命力:TypeScript 沙盒让社区在不破坏核心的前提下无限扩展,比让人 fork 修改 C++ 吸引更广泛的贡献者。
- 文档缺失是遗留系统最大的技术债:原版 RCT2 存档格式靠反汇编推断耗费了社区数年时间,提前写文档的边际成本极低,但事后补的代价极高。
延伸阅读
- OpenRCT2 官方文档与下载(含各平台安装指南和 changelog)
- 插件 API 开发文档(TypeScript 插件接口完整参考)
- OpenTTD —— Transport Tycoon 的同类重实现,走了相似的逆向工程路径
- bevy —— Rust 游戏引擎,代表”从头写新游戏”的现代路线,与 OpenRCT2 的”重实现老游戏”路线形成对比
- minetest —— 另一个开源游戏项目,展示社区驱动开发如何维护长期活跃度
关联
- bevy —— 现代 Rust 游戏引擎,适合从头构建;OpenRCT2 则是逆向重实现的典型路线,两者目标相反
- minetest —— 同为开源游戏,展示了如何用插件生态替代 mod 文件,插件理念与 OpenRCT2 TypeScript API 相近
- panda3d —— Python 3D 游戏引擎,与 OpenRCT2 同属”游戏引擎”分类但面向全新创作
- cocos2d-x —— 跨平台游戏引擎,同样解决”一份代码跑多平台”的问题,但走的是引擎抽象层路线而非逆向迁移
- nvm —— 版本管理工具,体现了”不改变原始行为、只增加管控层”的相似工程哲学