将我1993年的Amiga游戏移植到Godot,使用LLM解读68000汇编代码

Hacker News Top 新闻

摘要

一位工程师描述了他将1993年的Amiga游戏'Babylonian Twins'移植到Godot的过程,使用Claude Fable 5 LLM来辅助读取和翻译68000汇编代码,结合个人历史与AI驱动开发。

这些是我将我的Amiga游戏移植到Godot的笔记,该游戏最初于1993年在巴格达用MC68000汇编构建。我在去年七月假期使用Claude Fable 5 LLM进行了移植,只花了一个晚上!调整手感和发布又花了几个周末和晚上。<p>过去几周,我分析了Claude的工作,将我33年来构建游戏的记忆、笔记和git仓库提供给它。它撰写了文章的第一稿,我花了整个星期逐行编辑。我1993年地图编辑器的截图是我自那时以来首次运行它。唯一我自己从未检查的是108字节的解释。<p>请提出任何问题。我还将免费发布原始游戏。
查看原文
查看缓存全文

缓存时间: 2026/09/03 17:58

# 用LLM阅读68000汇编代码,将我的1993年Amiga游戏移植到Godot——《巴比伦双子》 来源:https://babyloniantwins.com/blog/porting-a-1993-amiga-game-to-godot/ 1993年,在巴格达,我用一台Amiga 500电脑制作了一款名为《巴比伦双子》的游戏:512KB内存,无硬盘,连接电视运行。那时我是一名二十多岁的工程系学生。完全使用68000汇编语言编写,每个精灵图和每条扫描线都靠手工调整。美术由穆尔塔达·萨尔曼绘制,音乐由马希尔·阿尔萨尔曼创作。当时我们身处制裁之下:没有互联网,没有游戏开发资源,只有一本《Amiga硬件参考手册》,我用它来直接编程硬件,每天只有几小时的供电。频繁的软盘切换(由于内存太小)和50°C的夏季高温,三次烧毁了我的磁盘驱动器。 左图:1993年,在Amiga上。右图:2026年,同一个入口。 在Amiga上,“手工”意味着游戏运行时完全不向操作系统请求任何资源。启动时,它保存中断向量,关闭操作系统中断,并接管整个机器: ``` move.l #$dff000,a0 ;硬件寄存器基地址 lea save(pc),a1 ;从AMIGA获取系统控制权 move.w #$4000,intena(A0) ``` “从AMIGA获取系统控制权”是我1993年写的注释。从那一刻起,显示屏使用的是游戏自己的copper列表(Amiga的可编程视频协处理器),实时重写以处理精灵图和天空颜色。图块通过直接写入blitter寄存器并等待其完成标志来移动。摇杆直接从硬件端口读取,发射按钮是CIA芯片上的一个引脚。操作系统仅在关卡之间恢复,用于从磁盘加载下一关的文件,然后立即再次关闭。 这是伊拉克制作的首款商业游戏,但很长一段时间内都很少有人玩到。Commodore公司破产,制裁吓跑了发行商,于是这款完成的游戏被束之高阁。2008年,一个Amiga论坛通过我弟弟的YouTube上传发现了它,并联系我获取磁盘;那个帖子至今仍在(https://eab.abime.net/showthread.php?t=39120)。 这款游戏曾在2010年被手工移植过一次。同一个团队用从头编写的引擎为iPhone重制了它,大约34,000行C++代码,历时数月的夜晚和周末。苹果和谷歌都推荐了它,下载量超过两百万次。这个故事在这里(https://babyloniantwins.com/story)。 这次移植不是我做的。我提出了要求,每晚都玩结果版,指出感觉不对劲的地方,并为一些需要1993年亲历者决策的事情做了决定。文件格式和汇编阅读是AI完成的,关于如何迁移三十年前代码的决策也是,它的速度快到我跟不上。这篇文章是我在几周后坐下来阅读对我自己游戏所做的修改时发现的内容。有些是错误的,而我数周都没注意到。 ## 为什么再次尝试 我之前试过这个。大约一年前,我给早期版本的模型同样的Amiga材料,让它理解我的二进制关卡地图。它最终成功了,但经过了好几轮,还需要我提供大量提示。后来Claude Fable 5发布,我给了它同样的文件。这是一个刻意的测试。我的猜测是LLM训练数据中几乎没有Amiga汇编代码。如果模型更擅长推演而非回忆,这将是它展现能力的地方。 七月四日周末即将到来,我计划了三步,每一步都依赖于前一步的成功。 第一步,稳妥的请求:把我自己2010年的引擎(34,000行C++)迁移到Godot 4。这是对照组。 第二步,不可能的任务:原始的72,758行68000汇编代码,针对一台已停产的机器,几乎没有注释,与C++毫无共同之处。也用Godot重建,并保持Amiga原始的50Hz刷新率。 第三步,贪婪的请求:将第二个放入第一个中,这样购买现代游戏的同时,也能获得一个可以启动的1993年原版。 三步都成功了。一年前需要多轮和我纠正才能得到的关卡格式,这次一次通过,我没有提供任何提示。 ## 如何运行 我在Claude Code中运行它,所以它有一个终端和我的文件系统。它可以编辑文件、运行汇编器、构建游戏、启动游戏并读取返回结果。当我在下面说它重建了我的1993年二进制文件并检查它们时,它通过运行vasm并比较输出来实现。早期,它给游戏添加了一组命令行参数,这样它可以在没有我的情况下运行: ``` --level= 直接加载一个关卡 --pose= 将双胞胎置于精确位置 --drive= 按脚本逐帧按键 --probe 转储开关/门/钥匙状态 --screenshot= 渲染一帧并退出 ``` 这将“跳跃感觉对不对”变成了机器可读的内容: ``` drive[btw_jump:2.2] pos=(25.44, 24.04) vel=(0.00, -14.51) ground=false apex_y=22.48 ``` 它还有两个可以在给我看任何东西之前运行的无头检查:一个编译所有脚本,另一个构建所有关卡并报告失败。在Amiga端,它驱动真实的工具链,用vasm汇编,用FS-UAE启动结果。 没有自动化的部分:现代移植版没有图像比较(它截图,我看),也没有检查游戏感觉是否正确。 ## 第一步:一晚上迁移34,000行C++ 周三晚上,稳妥的请求。时间戳,未编辑: ``` 22:23 Godot 4项目脚手架,资源同步,TMX关卡管道 22:44 双胞胎都可玩——碰撞、物理、相机、切换 23:19 所有38种实体类型已移植——完整对象清单运行中 00:35 完整屏幕流程——菜单、地图、故事、保存、游戏流程 02:15 导出到macOS、iOS和Android ``` 从空项目到可玩角色只用了21分钟。那天晚上它迁移的每一行代码都是我在2010年花了几个月写的。我困惑地上了床。让它感觉正确又花了大约三天:跳跃弧线和弹跳床时机,以及奖励连按的命中检测,在7月2日、3日和4日分批修复。我不是独自测试的。我十三岁的儿子和我一起玩了每一次构建。他一直知道我制作了这个游戏,这是他从小就了解的父亲事实,但他从未见过我工作。测试变成了一个我没有计划的父子项目,这是整个项目中我最喜欢的部分之一。 ### 相同的单位,相同的tick 所有游戏状态都存在于图块单位中(1.0 = 一个48像素的图块),更新以固定的60Hz运行,因为2010年iOS构建运行在60Hz。这很重要,因为原版每帧都乘性应用阻力: ``` static const float GROUND_DRAG_FACTOR = 0.85f; this->velocity.x *= GROUND_DRAG_FACTOR; // 每帧! ``` 每秒乘以0.85六十次得到一种摩擦力;乘以五十次得到另一种。移植到不同的tick率,游戏中的每条加速曲线都会改变。不会崩溃,只是感觉永远不对劲,而且你无法通过阅读差异发现它。在60Hz下,常量可以原样移植。这也是为什么1993年重建版运行在50Hz,而现代版运行在60Hz:两组手工调整的数字,每组只在自己的tick下正确。它保留了两个时钟。我本会忍不住把它们合并整理。 ### 没有使用CharacterBody2D Godot提供了`CharacterBody2D`和`move_and_slide()`,每个教程都告诉你使用它们。但移植版没有为玩家使用任何一个。原版有自己的手写移动代码,在别人的物理引擎上重建它,感觉会略有不对劲,而且追踪起来很痛苦。玩家是一个普通的`Node2D`,150行的碰撞例程逐行迁移过来,包括我十五年前凭感觉选择的凑合数字,以及我写给未来自己的注释: ``` # 加0.5是因为我们希望角色的脚在图块中间。 var bottom := pos.y + dim.y / 2 + 0.5 + i + fraction if int(bottom) == int(pos.y + dim.y / 2 + 0.49): continue var right := pos.x var left := pos.x - dim.x / 4 # 非对称探测! ``` 没有清理那个奇怪的`0.49`。没有测试,没有文档;这些注释就是规范。 ## 第二步:68000汇编 7月5日周日下午,我交出了真正想测试的东西。26个文件共72,758行代码,为一台只有512KB内存的机器而写,由我为我而写,带着一个从不期望别人阅读的人的注释习惯。没有文档。2008年转移到现代存储时缩短了所有长文件名,所以每个include都指向不再存在的名字。五个关卡源文件之一在一个数据表中间被截断。没有其他副本。 在移植任何东西之前,它让1993年的源代码再次汇编成功,使用Apple Silicon Mac上的vasm,并持续进行直到输出与发行的二进制文件逐字节相同。 ``` 14:34 导入Amiga源代码、资源、参考文献 14:49 vasm工具链逐字节重现发行的二进制文件 15:20 重建磁盘映像 15:42 重建的演示在FS-UAE中启动并运行 ``` 从文件夹中的文件到第一次重建匹配发行字节只用了十五分钟。我用ASM-One编写这些,它的方言与vasm在改变字节的方面有所不同:ASM-One将`cmp #4,d0`编码为CMPI,vasm选择不同但同样有效的编码,因此告诉它不要优化是必要的,但还不够。它没有编辑我的源代码,而是写了一个预处理通道来桥接五个这样的差异,并逐个文件重建了损坏的文件名映射文件。 昂贵的是`org`。没有链接器和重定位,关卡源代码手动布局Amiga的内存,一个地址接一个地址: ``` org $6a000 ; 此部分位于地址$6a000 Mapadd: incbin "btwins:binary/L1/Map1.b" ; 游戏地图 org mapadd+73*1024 ; 跳到地图起始位置后73 KB处 GLBtable: dc.w $3333,50,20,100 ; 一条对象记录开始 dc.w SahamR-grb,26 ; 例程,长度... ... org glbtable+2*1024 ; 对象表正好占用2 KB ``` 关卡一的地图使用了74,752字节中的74,400字节,余量352字节,1993年只有我检查过它。(`SahamR-grb`将对象行为作为命名偏移附加;saham是阿拉伯语“箭头”的意思。) ASM-One的`org`还可以将位置计数器向后移动,而vasm不能。第一个变通方法在一种情况下出错:`ds.b 800`在一个倒回的块内,ASM-One将其视为“跳过800字节”,被写为800字节的零。文件中该点之后的所有内容,包括copper列表,都位于发行二进制文件位置之外944字节处。游戏汇编并启动了,但绘制了错误的东西。 即使之后,有些块仍然不匹配,在变量区域中分散着大约108字节。这些字节解释了发行文件的来源。ASM-One汇编到内存中,游戏通过在游戏运行后保存该内存来转移到磁盘上。所以发行文件是一个已经运行过的游戏的快照,而不是干净的汇编器输出。新鲜汇编在那些变量中是零,因为还没有设置任何东西;发行的磁盘包含了它们在保存时机器上的值。代码在读取之前写入它们,所以零是无害的。 当时我读了那行,继续前进,等待实际的游戏。几周后我才明白这是项目中最重要的事情,而且没有人要求过它。从那时起,关于这款游戏的每个说法都可以通过比较字节来确定。我自己不会这么做。我已经有二进制文件了,而且十八年来,从源代码重建它们似乎从不值得花一个下午。 ## 格式 对于每种格式,它都去读取字节的代码,并从那里反向推导。关卡加载器是1,652行无注释的68000代码,这就是为什么我总是使用十六进制编辑器。 ### 关卡 关卡是一个图块网格:一长串数字,每个数字意味着“把图片47放在这里”,按照我自己1993年的布局。这是更早版本的模型和我一年前辛苦处理的格式。这是一关用的完整图块集,共256个,每个16×16像素: 1993年Amiga游戏关卡一的完整16x16图块集:石块、梯子、水、棕榈叶、装饰砖 以及关卡一的一个切片,由这些图块组成: 从提取的地图数据渲染的关卡一水平切片 输入是一个没有标题和尺寸的数字列表,在一个压缩块内。这次我什么都没解释。它找到了绘图例程,读取网格如何遍历,从文件中其他地方的常量推断出宽度和高度,并在第一次尝试时就为所有五个关卡生成了正确的地图。然后它从提取的数据重新渲染每个关卡,并将结果与我2020年制作的完整关卡截图逐像素比较。当不匹配时,它寻找原因并发现了两个铜效果:天空渐变和水的颜色循环。考虑到这两个因素后:五张完整关卡图像,零个不同像素。仅关卡一就有600个图块宽,9,600像素。 ### 地图单元格属性 绘制关卡只是地图单元格功能的一半。每个单元格是一个16位字,图片是其中较小的部分: ``` 一个地图单元格,16位: 位15..10 属性:这个方块做什么(6位) 位8 使用两个图块库中的哪一个(1位) 位7..0 绘制256个图块图片中的哪一个(8位) ``` 属性是关卡不可见的物理。1是实体地面。2和3可以攀爬。10到13都意味着“这会伤害”,四个代码是因为击退需要方向。14直接杀死。63是一扇门。这些都没有任何记录。它之所以被恢复,是因为两个例程读取同一个字,每个都揭示了自己的那一半:绘图循环掩码掉低字节,碰撞检查做相反操作: ``` move.w (a1),d6 ; 同一个单元格 and.w #$fc00,d6 ; 保留高6位 lsr.w #2,d6 lsr.w #8,d6 ; d6 = 属性,0..63 bsr cbCheck ; 2或3?你可以爬这个 bsr Checkrmh ; 10..13?这会伤害,从哪一边 ``` `Checkrmh`将痛苦情况交给一个名为`rmhEnjury`的标签,这是1993年的我拼写的“injury”。这些位是在编辑器中绘制的。在构建游戏之前,我必须构建构建它的工具:`MEDITOR.S`,1,254行汇编代码,以其自己的头部注明日期,用我1993年的英语: ``` ; *********************************************************************** ; * 本程序在四天内编写完成 * ; * 1993-2-8/7/6/5 * ; * 我制作它是为了帮助我为我的第一款认真的 * ; * 游戏制作地图 * ; *********************************************************************** ``` 1993年二月四天。用鼠标绘制图块,在面板的“当前标志”计数器上选择一个属性编号,用“放置标志”将其印到单元格上,标志视图会标记每个带有所选编号的单元格。 写这篇文章时,我让模型运行地图编辑器并获取截图。它用现代汇编器汇编了1993年的源代码,将发行的第二关数据按编辑器期望的方式布局在内存中,并在模拟器中启动了结果。 我1993年的地图编辑器在2026年运行,编辑真实的第二关,选择了标志1(实体):瀑布场景,实体地面被标志视图标记,当前标志计数器显示0001 我自己的工具,三十三年后,编辑真实的第二关,标志视图开启。当前标志显示0001,实体,你可以站立的地面被标记,而你穿过的装饰没有。面板显示1994:面板图像是编辑器加载的独立位图文件,而生存下来的副本比1993年2月的代码要晚。面板上的另一个名字,乌代,是1993年我的搭档……

相似文章

这个周末你打算做什么?

Lobsters Hottest

一位开发者描述了将《完美黑暗64》关卡移植到 noclip.website 的过程,强调了读取 N64 显示列表和重新实现渲染引擎的挑战。