逆向工程NES TETRIS添加硬滴

2021-03-21 22:53:32

NES的TETRIS是我最喜欢的TETRIS版本之一。只​​有投诉只是它缺乏“硬滴”的能力 - 立即丢弃当前的件并将其锁定到位。

这篇文章描述了我对NES TETRIS进行的修改,以便按下“向上”按钮导致当前件到硬滴,使游戏呈现“幽灵件” - 当前件的点缀轮廓显示它将降落在哪里。

当前的作品向下移动每个游戏时的一个空间.TETTIS实现通常提供两种方式,用于加速这一点 - 软液滴和硬滴。

对于软液滴,按下按钮将立即将当前的块移动到一个空间,并保持按钮将导致它比否则更快地降低。

硬下降瞬间掉落当前的件并将其锁定到位。因为Playerto可以难以在目视讲述这件作品是否与他们想要的地方排队,但是俄罗斯特拉的挡泥板通常会显示鬼件,显示鬼件显示当前的鬼件一块会最终。

我制作了一个Rust程序,读取了InesFormat中的NES ROM文件。如果它的输入是NES TETRIS(通常在命名为“TETRIS(U)[!])的文件中。NES”),它将产生输出,一个新的NES ROM文件,修补了NES TETRIS,修补了硬滴。

$ Cargo Install NES-Tetris-Hard-Drop-Patcher#安装我的工具$ NES-Tetris-Hard-Drop-Patcher< '俄罗斯(U)[!]。NES' > tetris-hd.nes#patch a nes tetris rom $ fceux tetris-hd.nes#在仿真器中运行结果

此工具依赖于用户获取NES TETRIS ROM文件。它没有TETRIS内置。生成的ROM文件与所有NES仿真器兼容 - 它没有特定于FCEUX。

几年前,我制作了一个nes仿真器。结果是一个有用的逆向工程工具,因为它很容易介绍它正在运行的程序上的emulatorto进行实验。特别地,记录每个Instruction的能力,与视频内存更新等有趣的事件进行交错,非常方便。它可以呈现给GIF,我用它来生成此帖子中的所有动画!

要测试我的仿真器,我制作了一个生锈库以生锈嵌入的域的语言铭记NES装配程序。这是一个示例,即“累加器”寄存器中的值为12:

b .inst(clc,()); //清除携带标志b .inst(rol(累加器),()); //旋转累加器1位左侧(x2)b .inst(rol(累加器),()); //旋转累加器1位向左(x4)b .inst(sta(zeropage),0x20); //在地址0x0020 b处存储电流累加器值(rol(累加器),()); //旋转累加器1位左侧(x8)b .inst(adc(zeropage),0x20); //以0x0020的值添加累加器(x12)

这让我使用RUST作为NES装配程序的宏观语言。在将自定义代码加到80秒编写的现有程序上时,该灵活性本领此视角至关重要。

在调试我的模拟器时,我制作了一个简单的解压缩,可以显示NES程序的每函数集合。

最后,我使用了名为MESEN的第三方NES仿真器,它拥有Arich一组调试工具。这有助于了解对存储器的当前中心以及图形芯片的当前状态。

Tetris使用精灵绘制当前的片断和下一件件,以及其他一切的背景图形。下面的图像隔离上面的场景中的两种类型图形,左侧的背景和右侧的精灵。

游戏显然有逻辑已经使用精灵绘制当前的帖子,索托最简单的方法渲染幽灵片似乎重新使用该逻辑,但使用幽灵瓷砖而不是普通的瓷砖。

说到幽灵瓷砖,我为游戏添加了一个新的瓷砖用于鬼件:

我的目标是击中俄罗斯方块的一部分,使当前的作品呈现为衡量奖励鬼块的代码。

要在网元上呈现精灵,可以使用Sprite元数据(位置,磁贴等)填充主内存区域,然后将此存储区域的开始的地址写入Oamdma寄存器。(对象属性内存直接内存访问 - OAM是存储Sprite元数据的特殊内存,以及DMAIS直接读取和写入主存储器的一般术语。)将地址写入Oamdma,导致NE上的图形硬件将Sprepite元数据从指定的主内存区域复制出来,并进入专门的区域在渲染期间将咨询对象属性存储器以绘制精灵。

Oamdma寄存器在地址0x4014处映射到CPU的地址空间。为此地址进行搜索,为此地址进行了显示:

0xAB63 LDA(立即)0x02#负载累加器,20xAB65 STA(绝对)0x4014#写入累加器到0x4014

这将值2写入OAMDMA导致从0x0200到0x02ff的内存将要复制到OAM.Searching 0x0200的代码,并且一个功能尤其跳出作为填充OAM DMA缓冲区的负责。此功能位于0x8a0a,可以告诉我们一个很好的拖信如何工作。

它首先读取来自地址0x0040和0x0041的值,将每个乘以8,并将它们添加到某些偏移量。每个图块是8x8像素,因此这似乎从平铺坐标转换为像素坐标,其中偏移是电路板左上角的像素坐标的组件。几分钟内在MESEN中倾向于此 - 0x40是x坐标,0x41是当前件的Y坐标。

然后函数从0x42读取。此位置始终包含0到12之间的值,它似乎编码当前件的Thehape,以及其旋转。对于具有旋转对称的形状(例如“S”件),Themultiple相同的旋转在0x42中获得单个值。我将此值称为“形状索引”。

尖端中的每件件由4个瓷砖组成,每个瓷砖都呈现了一个精灵。0x40和0x41的坐标是该件的位置,但是为了使其呈现每个瓦片的位置。为此,此功能会在ROMAT地址0x8A9C中查询表中的表,我将参考“形状表”。 13件(包括唯一旋转)中的每一个都具有形状表中的12字节条目。片段的形状表条目为4个图块中的每一个存储3个字节:

此功能计算当前块的每个磁块的位置和精灵索引,并利用此信息填充TheOAM DMA缓冲器。要渲染幽灵片,我需要一个类似的功能,除了它与鬼瓷砖的瓦片瓦片而不是来自形状表的瓷砖,它在垂直的垂直部件上呈现该块,使得这件作品出现在它之后的位置一个硬的掉落。它将是非琐碎的,修改这个函数就是在幽灵片和常规作品上普遍存在,所以我复制/粘贴了代码并改变了它做我需要的事情。

我开始使用Mesen的记忆Viewer来定位一个看似未使用的ROM的区域。我不知道为什么它带有0x00和0xff的条纹!此外,我不知道如何将MESEN的字体更改为单座!

我声称512个字节的内存开始于地址0xD6D0。我添加到此区域的第一个代码是WASA函数,只需调用现有的OAM DMA缓冲区更新功能:

我的修补工具将所有调用替换为原始函数(0x8a0a)的调用,调用此新函数。

接下来,我从原始OAM DMA缓冲区更新函数和手动翻译成NES组件的特定于域的语言的拆卸代码。

b .label(" render-ghost-pacters"); //函数标签,所以它可以通过名称调用b .inst(lda(zeropage),0x40); b .inst(ASL(累加器),()); b .inst(ASL(累加器),()); b .inst(ASL(累加器),()); b .inst(ADC(立即),0x60); b .inst(STA(零零),0xAA); ......

我修改了我的OAM DMA缓冲区更新副本以使用Ghost瓦而不是来自Shape Buffer的Tileread。要测试此更改,我将OAM-DMA缓冲区更新更新为Callththis函数而不是原件:

接下来我使我的幽灵级渲染函数拍摄一个参数,指定垂直距离收据的垂直距离它应该渲染鬼件。最终,这将基于这件作品在碰撞之前向下移动的次数,但是Firsti尝试用常数调用它(6)。

b .label(" oam-dma-buffer-更新"); //呼叫原始函数首先b .inst(JSR(绝对),0x8a0a); //渲染Ghost块,传递地址`0x0028中的垂直偏移参数。 b .inst(LDA(立即),6); b .inst(STA(零),0x28); b .inst(JSR(绝对),"渲染 - 幽灵片"); //返回b .inst(rts,());

现在要将当前件的真实垂直偏移量计算到它将降落的地方。通过使用MESEN观看记忆,我观察到似乎没有从0x0020到0x0028使用的内存。前256个字节的内存被称为“零页”,并且提供比内存的其余内存所带到的速度更快.I想要8零页在碰撞检测期间存储当前块的每个瓦片的x,y坐标的字节,以及在计算期间存储临时值的一个附加字节。

Warning: Can only detect less than 5000 characters

现在幽灵片是渲染,下一步是使其使控制器上的“向上”按钮被按下时,发生硬盘。 “向上”按钮不使用BYTETTIS,因此我们不必担心丢失一些功能,以获得硬下降。

与添加幽灵片一起一样,我开始找到一个我可以用新的功能替换的函数在做额外的东西之前调用原件 - 在这种情况下检查是否被按下了,如果是的话,滴滴滴。

我的第一次尝试是寻找读取控制器状态寄存器0x4016的代码,但似乎是读取该读数和更新基于该按钮的名称状态之间的间接的公平位置。

我的第二个想法是介绍我的仿真器来记录执行的每个指令。我加载了tetris,并导航菜单开始新游戏,然后保存了一个状态文件.My仿真器可以选择为特定数量的帧运行.I将其设置为运行20帧,加载状态文件,并记录每个指令而无需按任何控件。然后我重复了这个过程,但是在20帧窗口期间,我周围的这次是在左按钮.I现在有两个日志指令流 - 没有按下控制的一个,并且用控件压制为第二个。它代理是这些流不同的第一个位置是第一次将程序分支第一次分支左按钮的状态。

@@ -116912,9 +116912,175 @@ 0x89b8 lda(zeropage)0xb5 0x89ba和(立即)0x03 0x89bc bne(相对)0x15 -0x89be lda(zeropage)0xb6-0x89c0和(立即)0x03-0x89c2 beq(相对) 0x45 + 0x89d3 LDA(立即)0x00 + 0x89d5 sta(Zeropage)0x46 + 0x89d7 LDA(Zeropage)0xB6 + 0x89d9和(立即)0x01 + 0x89db beq(相对)0x0f ...

0x89ae lda(Zeropage)0x400x89b0 sta(zeropage)0xae0x89b2 lda(Zeropage)0xb60x89b4和(立即)0x040x89b6 bnne(相对)0x51(相对:0x51,绝对:0x8a09)0x89b8 lda(zeropage)0xb50x89ba和(立即)0x030x89bc bne(相对) 0x15(相对:0x15,绝对:0x89d3)0x89be lda(zeropage)0xb60x89c0和(立即)0x030x89c2 beq(相对)0x45(相对:0x45,绝对:0x8a09)...

该分支基于地址0x00b5和0x00b6的内容。在捣碎控制的同时,在mesen中打开这些地址,使得我的印象是0xb5将帧存储到控制器状态下的帧差异,0xb6存储该电流控制器状态。尽管不要被方块而被使用,但“上升”按钮的状态反映在这些值中。

我开始这个函数与我对OAM DMA缓冲区更新的替换相同 - 所有它都已删除原始功能并返回:

现在添加检查是否按下“向上”按钮。目前,按下按钮时,只需传送当前的偶然时间即可固定的高度:

b .label("手柄控制"); const controller_state:u8 = 0xb6; const controller_bit_up:u8 = 0x08; //调用原始函数b .inst(JSR(绝对),0x89ae); //如果控制器状态的上位未设置B.inst(LDA(Zeropage),Controller_State),则跳到结束; b .inst(和(立即),controller_bit_up); b .inst(beq,labelrelativeOffset("控制器端")); //将当前的零件' s y坐标为7 b .inst(LDA(立即),7); b .inst(sta(Zeropage),zp_piece_coord_y); b .label("控制器 - end"); //返回b .inst(rts,());

这是一个操作的代码,我多次按下“向上”:

接下来,用实际位置替换测试常数7,即在硬滴后方将最终最终。使用Compute-Hard-Drop-FlationFunction,我们为Ghost块渲染写作,然后只添加该件的函数来获取绝对y坐标,它将最终删除:

b .label("手柄控制"); const controller_state:u8 = 0xb6; const controller_bit_up:u8 = 0x08; //调用原始函数b .inst(JSR(绝对),0x89ae); //如果控制器状态的上位未设置B.inst(LDA(Zeropage),Controller_State),则跳到结束; b .inst(和(立即),controller_bit_up); b .inst(beq,labelrelativeOffset("控制器端")); //从电流件到掉落目的地的计算距离,将结果放在累加器B.inst(JSR(绝对),"计算硬降距离"); //添加当前块' s y坐标b .inst(clc,()); b .inst(adc(zeropage),zp_piece_coord_y); //更新当前块' s y坐标与结果b .inst(sta(zeropage),zp_piece_coord_y); b .label("控制器 - end"); //返回b .inst(rts,());

虽然有一个小问题。似乎游戏等待,直到下滑前的“勾选”结束

......