AI 读 72758 行 68000 汇编:1993 年 Amiga 游戏移植进 Godot 全记录

1993 年的巴格达,一名工程学生在 Amiga 500 上做完了一款平台跳跃游戏。512KB 内存,没有硬盘,机器接在电视上;他写的是纯 68000 汇编,画面靠手写的 copper 列表逐行刷新,瓷砖移动靠直接写 blitter 寄存器,摇杆状态从硬件端口读入。夏天五十度的高温加上频繁换盘,他的软驱坏过三次。这款叫《Babylonian Twins》的游戏成了伊拉克第一款商业游戏,然后停在货架上:Commodore 倒闭,制裁吓跑了所有发行商。

2010 年,原作者带团队手工把它移植到 iPhone:自研引擎,34,000 行 C++,几个月的夜晚和周末,后来下载量超过两百万。

2026 年 7 月,同一个人做了一次完全不同的移植,并把全过程写在博客里。这次的工作由 Claude Fable 5 完成,运行环境是 Claude Code,带终端和文件系统权限。三步计划全部跑通:第一步,把 2010 年的 34,000 行 C++ 迁入 Godot 4;第二步,把 1993 年的 72,758 行 68000 汇编(26 个文件)先在真实工具链里复原、再整体移植进 Godot;第三步,把 1993 年原作作为第二启动选项装进新游戏,一个晚上完成。原版磁盘镜像已免费放出 itch.io,Steam 完整版今秋发售。

1993 年与 2026 年的同一处门洞

题目经过设计:模型背不出来的题材

开发者明确说这是一次测试。他的判断是:LLM 训练集里 Amiga 汇编代码极少,模型在这种题材上只能靠推理,靠记忆没有素材可背。一年前他用当时的模型试过同一批文件,解析二进制关卡地图磨了好几轮,中间还要他持续给提示。Claude Fable 5 拿到同样的地图格式,一次通过,零提示。

三步计划每一步以前一步成功为前提。「安全请求」是 34,000 行 C++ 的迁移,充当对照组;「不公平请求」才是真正的考题:给一台停产机器写的 72,758 行汇编,几乎无注释,和 C++ 版没有任何共同代码,要求按 Amiga 原生 50 Hz 重建进 Godot。

对照组:34,000 行 C++,一个晚上

周三晚上的时间戳:22:23 建 Godot 4 工程骨架,22:44 双主角可玩(碰撞、物理、相机、切换),23:19 全部 38 种实体类型迁完,00:35 菜单、地图、剧情、存档全流程跑通,02:15 配好 macOS、iOS、Android 三端导出。从空工程到可玩角色 21 分钟。之后花了三天调手感:跳跃弧线、蹦床时机、奖励连打的命中判定,7 月 2 日到 4 日分三批修完。

这次迁移有两个反教程的决定,都和保护手感有关。

其一,更新频率锁死 60 Hz,因为 2010 年 iOS 版就是 60 Hz。原版物理里地面阻力按每帧乘法施加:velocity.x *= 0.85,每秒乘 60 次和乘 50 次得到的摩擦完全不同。把这种代码搬去别的帧率,所有加速度曲线一起变形,不崩溃、不报错,diff 里看不出来,只有手感不对。1993 年版按 50 Hz 跑,2010 年版按 60 Hz 跑,两套手调数字各配各的时钟,移植时保留了两套,没有合并成一个。

其二,玩家角色没用 Godot 自带的 CharacterBody2D 和 move_and_slide(),只是普通 Node2D 加 150 行逐行搬过来的手写碰撞例程,连当年凭手感填的 0.49 修正值和写给未来自己的注释都原样保留。这个项目没有测试也没有文档,注释就是规格说明。

考题:先把 1993 年的字节复原

拿到汇编源码后,模型做的第一件事超出了「移植」的范围:用 Apple Silicon Mac 上的 vasm 把 1993 年的源码重新汇编,目标是输出与当年发货二进制逐字节一致。时间戳从导入到首个字节级一致的重 build 用了 15 分钟,随后磁盘镜像重建、FS-UAE 模拟器里开机可玩。

这条路布满方言坑。源码当年写在 ASM-One 里,它的汇编方言与 vasm 有五处差异会改变输出字节,比如 cmp #4,d0 这条指令,ASM-One 编码成 CMPI,vasm 选另一种同样合法的编码。模型没有改动原始源码,而是写了一个预处理通道弥合五处差异,再逐文件重建 2008 年转录时被截断的文件名映射。

最贵的一处是 org 伪指令。没有链接器、没有重定位,关卡源码靠手工逐地址排布内存:第一关地图用掉 74,752 字节配额中的 74,400,余量 352 字节,1993 年之后唯一检查这个余量的人只有作者自己。ASM-One 的 org 可以把位置计数器往回拨,vasm 不行;第一版兼容方案在一处回拨块内的 ds.b 800 上出错,把「跳过 800 字节」写成了 800 个零,之后的所有数据(包括 copper 列表)整体偏移 944 字节。结果是:游戏汇编通过、正常开机、画错了东西。

还剩 108 字节对不上,散布在变量区。它们的来源解释了这批发货文件的出身:ASM-One 汇编进内存,游戏存盘靠把内存整块倒出去,而倒出去的时机是游戏跑过之后。发货文件是运行状态的一份内存快照,全新汇编的那些变量位置是零,磁盘上是当年那台机器保存时的残值。代码先写后读,零值无害。从确认这一点起,关于这款游戏的每个论断都有了裁判:比字节。

格式逆向:从读数据的代码反推数据

字节裁判建立之后,进入格式逆向。关卡加载器是 1,652 行无注释 68000 代码,作者过去宁可拿十六进制编辑器手看,模型的做法是直接读这段代码,从「怎么读」反推「怎么存」。

关卡是一串瓷砖编号,256 种瓷砖、每块 16×16 像素,输入是无头无尺寸信息、压在压缩块里的数字列表。模型找到绘制例程,读出网格遍历方式,从文件其他位置的常量算出宽高,五关地图首次尝试全部正确。随后它把每关从自己提取的数据重新渲染,和作者 2020 年截的整关画面逐像素比对,对不上的部分追出两种 copper 效果(天空渐变和水面变色);补上这两项,五张整关图零像素差异。第一关一张地图 600 块瓷砖宽,9,600 像素。

33 岁的地图编辑器在模拟器里编辑真实的第二关数据

每个地图格子是 16 位:高 6 位是属性(这块地做什么),1 位选瓷砖库,低 8 位选 256 张瓷砖画里的哪一张。属性是关卡的隐形物理:1 是实地,2 和 3 可攀爬,10 到 13 都造成伤害(四种编码对应四个击退方向),14 直接致死,63 是门。这些定义没写在任何地方,恢复手段是读两个例程:绘制循环屏蔽低字节,碰撞检查反着来,同一份数字各自暴露自己那一半。

敌人不存进地图。世界按屏幕存储,每屏 25×20 格,附一张对象表;每个敌人是一行 16 位字:标记、帧号、屏内坐标,然后是行为——以例程偏移的形式挂接,后面的参数只有那个例程自己懂。模型找到每帧遍历这些表的例程,让例程给字段命名,再把五关所有对象换算成世界坐标,和渲染出的地图核对。

有一处结论和作者三十三年的记忆相反。他一直确信门画在地图里,加载关卡后门的位置却全是空洞,没有门瓷砖。答案是一个 18 字节的运行时记录把门(1×4 瓷砖的柱)盖上去:关闭态四块瓷砖 $528 $53C $550 $564 是实心的,开启态四块恰好右移一个表列。地图数据说没有门,关卡代码说有门;门由代码在运行时绘制,从来没有被画进地图。跳过这层逻辑的直接移植,会得到一座门洞里全是天空的塔。

天空也是同理。每关 31 号色是天空色,瓷砖美术从不绘制它;copper 协处理器在选定扫描行重刷背景色,画出垂直渐变。整个第二关的天空是 24 个字的数据。第一次重建漏掉了它,关卡看起来没问题,只是平了,说不出的平;像素比对始终不过,渐变才补回去。精灵图则有一处真正的歧义:5 个位平面下,帧数×宽×高×2×5 的文件尺寸既可以读成双宽帧,也可以读成上下两行各一个朝向。两种读法与文件每个字节都吻合。模型标记了歧义,发问,作者拍了板:是两行朝向,1993 年他的选择。

模型犯的错,清单很长

第二关走廊里,玩家凭空挨一下打。攻击者是十三块瓷砖上方、隔着实心岩石的掷矛兵:原版代码里他的推击有上下两道围栏(垂直距离大于 4 行或小于 -2 行都不管),移植版保留下界、丢了上界,本该覆盖自己三行的推击纵贯整列地图,穿过地板,伸进他根本不在的走廊。

其余的:每个关卡都有第二层瓷砖(门开后露出的隐藏层、假墙碎掉后的通道),照搬渲染会让所有密道从头敞开;敌人先于玩家更新,蹦床跳会被计两次、弹上二十块瓷砖高;"p1,p2,p3,p4" 表示四棵棕榈共用的门,被读成一把名字奇怪的单钥匙,教学关的出口永远打不开;音效循环长度按立体声算、素材实际是单声道,每个音效放到一半就掐断重头。

最贵的一个bug牵出一个设计真相。作者要求在 1993 版里加「双胞胎任意距离换位」,邻近检查一去掉,雕像开始互相腐蚀。真答案藏在原 routine 里:那个检查从来不是距离限制,闲置的双胞胎以雕像形式印进地图,两座雕像叠在一起会互吃瓷砖。守卫恢复了,作者在 1993 版里砍掉了这个功能。

手感层面另有一类:蹦床「弹得太高」。常数全部正确,二十行模拟程序预测 19.1 块瓷砖,实测 19.5。问题在输入语义——2010 版是事件驱动,因为多年前给 tvOS 的一个怪癖打的补丁,按住跳跃会读成松开,直到再次物理按下;Godot 轮询输入,持续上报按住。复刻这个偶然,才有「高弹跳需要一次重新按下的时机」,那正是手机上的玩法,也是作者的手指预期的。这类信息不存在于任何源码里,它只存在于当年那台电视和那只手上。

第一关的 256 块瓷砖图集

让工作闭环的验证装置

回看整个流程,真正的骨架是三套互相独立的验证,每套都把「对不对」变成机器可判定的命题。

字节层:vasm 重 build 与发货二进制 diff,逐字节一致是后续一切的前置。像素层:五关重渲染与 2020 年截图逐像素比对,天空渐变、水面变色、门的运行时盖印全靠这一层逼出来。行为层:模型早期给自己加了一组命令行旗标,--level 直载关卡、--drive 按脚本逐帧按键、--probe 倾倒开关门钥匙状态、--screenshot 渲染一帧即退,把「这一跳手感对不对」变成 pos=(25.44, 24.04) vel=(0.00, -14.51) apex_y=22.48 这样的可读输出。另有两条 headless 检查:编译全部脚本、构建全部关卡并报告失败。Amiga 侧驱动的是真工具链:vasm 汇编,FS-UAE 开机。

没有自动化的部分同样清楚:现代版没有图像比对(模型截图,人来看),也没有任何东西检查手感。发布环节的人机边界同样明确:App Store 和 Google Play 走官方 API,Steam 没有元数据 API,模型驱动浏览器填 Steamworks 的表单;登录和一切「提交、发布、定价、上线」按钮由人按。商店截图从真游戏里出:按各商店像素尺寸启动、把角色走到指定位置、截图、用游戏自带字体画字幕条,模型从不直接渲染商店文字。有一次字幕真坏了,俄文和韩文渲染成一行行空框,作者在输出目录里看到,挡在上传之前。

它还读了十五年积累的用户评论:Google Play 官方 API 只给最近七天,模型用公开抓取器逐语言补齐,读完列出描述真实缺陷的条目,作者逐条批准。其中一条一星评论拼写糟糕,字面读就是个缺陷报告:「第一关过不了门,门开了但关卡不结束」。它是对的。开门和走进门是两个动作,说明提示在十一种语言里都存在,却只摆在十八关中的两关,缺提示的恰好包括最后一个免费关。站在开着的门前无路可走的玩家,只能得出游戏坏了的结论。十五年里作者、测试者和两次重制都没发现它,发现它的是陌生人的差评。修复以 2.0.3 上架双商店,那条评论被保留着。

对开发者的参考

这个案例能带走的做法有三条。迁移前先建立基准真值:字节级重 build 把后续每个论断都变成一次 diff,没有它,944 字节偏移和 108 字节残值都会被「能跑」掩盖。保留全部历史约束:两套时钟、0.85 的阻力因子、0.49 的修正值,清理它们的冲动在手感类项目里几乎总是错的。给代理装上裁决机制:字节 diff、像素 diff、行为探针三层各管一段,模型的输出才能被逐条验收,而不是被逐段信任。

边界也画得清楚。机制重建、格式逆向、验证执行,模型全包;歧义裁决(精灵图两行还是双宽)、手感判断(蹦床「太高」)、以及只存在于亲历者记忆里的输入语义,来自人。作者对自己角色的总结是一句话:他提出要求、每晚试玩、做决定。那篇博客本身也是同一个流程写的——模型拿移植笔记和他口述的地图编码细节起稿,他花一周逐行编辑。文章末尾他留了一个未核实项:108 字节的内存快照解释来自模型的判断,他读完后直接采纳,没有亲自查过。

背景与现状

《Babylonian Twins》1993 年完稿于巴格达,美术由 Murtadha Salman 绘制,音乐由 Mahir AlSalman 作曲。2008 年,Amiga 论坛用户通过 YouTube 上的录像找到作者本人,讨到了磁盘镜像。1993 年原版 ADF 现已免费发布在 itch.io,可在 FS-UAE、WinUAE 或真机上运行;《Babylonian Twins: Definitive Edition》已上架 iOS 和 Android,Steam 提供 Demo,完整版(Windows、Mac、Linux)今秋发售,1993 年原作作为内置的第二启动选项随附。移植笔记发布在作者个人站点 babyloniantwins.com,含全部时间戳与代码清单。

来源: