【解释向】超级马里奥水下256关的成因解析

阅前提醒:所有研究的资料来源于百度贴吧的“溢出关卡吧”!
众所周知,2015年,敖厂长上传过一个探索过超级玛丽水下256关的视频。此后便不断有人去探索这些神秘的关卡。然而,早在2012年便有人进行过了探索,并且一发不可收拾了好几年,留下了很多宝贵的结论。
我自己也曾在2021年研究过这些老帖子,直到前些天,我看到了一个2019年的老视频。

视频里提到的这些改动所导致的水下256关其实都是比较“邪门”的改法,那么为您呈现的,便是整套的溢出关卡体系。
(由于很少改版改过这个区域,所以这一规律基本通用)

开始前,我们先说明一些基本概念。
溢出关卡:(来自百度百科)由马里奥系列游戏,通过修改内存或运用bug技而进入的数据指针错误的非正常关卡。
(研究后的补充定义)溢出关卡是通过直接修改内存(包括关卡号、空间编号、数据指针内存)或运用bug技达到修改如上内存目的而进入的数据错误的非正常关卡。
水下256关:(大概为敖厂长的原意)SMB(就是超级玛丽/超级马里奥Super Mario Brothers的简称,下文都将以此代指)本身只有8个世界(大关),而通过内存溢出进入的溢出关卡大关能有256个,而这些关卡多在水下,所以被称为“超级马里奥水下256关”。
大关号:即世界数,游玩UI下的A-B中的A所代表的数字。
小关号:游玩UI下A-B中B所代表的数字。需要注意的是,游戏存储了两个小关号,有时两个小关号数值会不一样,UI中则只显示一个,稍后便会提到。
十六进制:一种计数方式,逢十六进一,10~15分别用字母A~F表示,能够和二进制较方便地转换。
空间号:也称房间号(Room),每个关卡/场景都会对应一个空间,而重复的关卡/场景对应的是一个空间,每个空间会对应一个十六进制的空间号。(十六进制与十进制的数下文不作严格区分,需要的地方才会注明,但空间号一定是十六进制的)
6502汇编:一种汇编语言,即编程语言,所有红白机游戏所采用的语言。
地址:内存中一个或多个字节所在内存中的位置、序号。一般用十六进制表示,并且在前面会加上一个$美元符号;相对应的,在程序中的数值用#$作为前缀。6502中,地址后面打上逗号","相当于地址的相加;地址外面打上括号"($xx)"则代表的是“相对地址”,实际地址是这个地址指向依次的两个字节,如:$00、$01两个字节为45,11,($00)则代表的是$1145。
指针:数据/代码所指向的地址。
6502地址布局:在65536字节的CPU内存中,$0000-$07FF为RAM,$0800-$1FFF为其3次镜像(读写都会在实际对应区域镜像,但是有例外);$2000-$2007为PPU图形寄存器,$2008-$3FFF为其反复镜像(同上,但是代码读取方面有待考究);$4000-$4017为APU声音寄存器,$4018-$5FFF为无效内存,与程序执行过程有关;$6000-$7FFF为SRAM(SaveRAM),大部分时候可做RAM使用,有记忆存储的功能;$8000-$FFFF为ROM部分。
6502寄存器:主要有4个:A累加器,X、Y变址寄存器,P状态寄存器,本质都为8位的一字节寄存器。一些二进制的逻辑运算主要依托于A,而X、Y为前文提到的地址中逗号后接着的,因而得名;P则主要在程序中受一些比较、加减、读取操作的影响,并提供结果,而其中7位都分别代表了一个标志位,包括N,V,B,D,I,Z,C。

一、关于用大关号、小关号确定空间号的过程
大关号位于$075F,而此处使用的小关号位于$0760。一段代码如下:

“LD”为load的缩写,表示读取;CLC表示清除C标志位;ADC为“带进位位C的加法”;RTS为返回主程序,相当于这只是一段子程序,用于调用。那么这段程序解释起来如下:
读取大关号的值,作为$9CB4往后的偏移值(称为y值,即实际指针为9CB4+(075F的值)),再与$0760相加得到一个值;把这个值作为$9CBC往后的偏移值存入累加器A中。
那么这段程序代表了什么呢?我们看一下$9CB4,$9CBC后的一些数据:

实际上,$9CBC起(即25 29 C0 26往后)的是每一关的空间编号。不过推导之前,就要说一下我们之前提到的“小关号的伏笔”——空间29。众所周知,在1-2、2-2、4-2、7-2四关开始前,会有一个马里奥自动进水管的过场动画。这一过场动画显示上(用的是$075C)是没有增加小关号的,但这里用到的$0760却被增加了,也就说真正的1-2这里是1-3,真正的2-2这里是2-3,本身的1-2、2-2都是这一“自行走关卡”。
结合我们分析的代码,在1世界中,1-1~1-4分别对应$9CBC偏移的0、2、3、4(自行走关卡不计),对应的空间编号便是25,C0,26,60;当在2-1时,$075F为01(实际上显示的关卡号比原本要+1),偏移$9CB4后,$9CB5的值为05,$9CBC偏移的也就为5、7、8、9,对应的空间编号也就是28,01,27,62了,以此类推。
通过这个,我们能说明一个问题:“关卡复用”。比如说,1-3与5-3地形完全一致,5-3敌人会多一些;1-4与6-4,2-2与7-2,2-3与7-3,2-4与5-4同理。而通过上图,我们能得出,每组关卡的对应空间号为26,60,01,27,62,也就是每组用的是同一空间号,而敌人的不同则是关卡内敌人的一些设置问题了。
由此,我们也能得出一个结论:所有水下256关都能够转化为1-x,也就是其所在的“y值”+1。比如(都计入自行走关卡,即0760的值):2-1就是1-6,8-1就是1-33,9-1就是1-38(其实8-5并非9-1,而是8-6,因为W9对应的y值是1-1的空间编号25,即37,越过了空间编号所在9CE0的1F,直接链接到9CE1的06).
RTS后,实际程序还有一段才会又一个RTS(子程序套子程序是非常普遍的现象):

JSR表示“调用子程序”,我们刚才9C13起的程序是返回到9C06的;“ST”为store的简称,即存储,AND表示“与”运算,ASL为算术左移,ROL为循环左移。
是不是这些运算太多了?实际上,这些都是二进制的逻辑运算。我们把刚刚得到的结果化成二进制来看:
首先,$9CBC偏移后的值存入$0750(这也是空间号的存储地方,但有时实质是暂存),这里假设是1-1的25,二进制表示为0010 0101;接着AND #$60,即0110 0000。0010 0101和0110 000作与运算,与运算对于每一位,只有两个都为1结果的位上才输出1。所以结果是0010 0000,即十六进制的20;算术左移就是把每一位的数据都向左移动一位,最低为填充0,可以理解为十进制上的“乘2”,结果为0100 0000,40;ROL为循环左移,会把左移后最高位存入C位,C位移入最低位,三次左移后变成了1000 0000,0000 0000,0000 0001,最终为01,存入074E。
其实,这一段就是确定074E“场景编号”的程序,之后还会再用一次。

二、确定关卡的指针
这一段代码便比较长了。

没有新的命令语法,直接分析:读取0750的值,执行9C09的子程序(是前面提到的确定074E的程序),存到Y内;读取0750,做和#$1F的与运算,存入074F(相当于取其二进制的后5位)。以Y为偏移值,读取9CE0偏移后的数值,加上074F,作为新的Y值;以此为新偏移值,读取9CE4,9D06偏移后的数值,分别存入$E9,$EA;类似的,读取074E,以此读取9D28偏移后的值,再加074F,作为新偏移值;读取9D2C,9D4E偏移后的数值,分别存入$E7,$E8。
如果不清楚的话,可以以这张图来说明:

$E9,$EA两个字节是敌人指针,$E7,$E8两个字节是地形指针。
限于本人概括能力不足,引用原贴的话:
结合这张图,我们对这一大段代码进行分析。
首先是重新获取场景代码的过程,在后面的代码中,我们得知,这一场景代码是在读取“敌人偏移值”和“地形偏移值”时作为偏移值使用的。
然后分别是获取敌人代码和地形代码(分别有2个)的过程。看一下上面的图,我们发现,敌人代码和地形代码合起来一共有4组,每一组都有34个数值。为什么是34个数值呢?想想楼上发过的表格,发现什么了没有?没错,正常关卡的地形一共有34种,对应的敌人也有34种。
接下来,获取的敌人代码和地形代码分别被存到了如下内存地址:00E7(地形代码1)、00E8(地形代码2)、00E9(敌人代码1)、00EA(敌人代码2)。注意到其中的00E8了没有?没错,它储存了一个地形代码!所以使用金手指00E8会改变地形……
存入上述4个代码之后,接下来的过程就跟这张图没关系了。
备注:各个代码区的起始地址为:y值9CB4,空间编号9CBC,敌人偏移值9CE0,敌人代码1 9CE4,敌人代码2 9D06,地形偏移值9D28,地形代码1 9D2C,地形代码2 9D4E
所以说每一空间对应的关卡都能确定;换句话说,所有能通过256大关、256小关进入的关卡都能确定了。原贴中整理了这样两个表:


值得注意的是,实际空间编号有效的只有00-7F。因为代码里从头到尾就没碰过第7位,也就是最高位,所以说80-FF等效于00-7F。而之后还有一个值得注意的地方:

9C58-9CA5之间的代码做了一件事——读取地形头部数据。地形头有两个字节,每几位一组各代表了一些数据,包括初始的地板样式、背景样式、出场高度、时间等。而实际的地形单位数据在这两个字节后,所以需要地形指针整体+2。也就不可避免地提到ADC的“C”问题了:进行加法运算时,两个8位的二进制数相加是可能会溢出到第9位的,会存入C中;这也相当于是加法的运算进位了,所以ADC #$00实际上加的可能不是0,也有可能因为C置1的进了位而加1,相当于最终结果也是整体在加减。由此保证了地图读取的正常性。

所以视频中反映的问题在哪里呢?0x1CC0与0x1D39这两处,对应内存中的便是$9CB0和$9D29。如果你还记得的话,刚才这段代码紧接着就是$9CB4,所以9CB0其实对应的是“ADC #$00”这一句中的数据“00”。更改这里,相当于就是在地形指针的高位又加了一个数,而不仅仅只是2,最终的效果便是改变了地形指针的高位,而低位相比正常没有改变。本质上不是水下256关,但也属于溢出关卡。
第二种情况更改的是地形指针的地上“场景偏移值”,074E的值为1所偏移到的。原本为03,改了之后,074F加上这个偏移值就能去很多别的不同地形,比如改为10,1-1的25地形则对应到了32,8-2,敌人却仍然是1-1的;改为FF,1-1则地形对应21,3-1;视频里改为00,则变为22 4-1,与本身074F的取值有关。数值改大一点可能还会出现更有趣的现象,出现更多的地形。本质上也不是水下256关,但是也是溢出关卡。
最后再提一嘴“水下”256关的问题。再回到这张图:

9CBC-9DBB(是最后一行那个数9D的地方)即为正常空间号的所有取值范围。当中别的不看,9D06-9D1C是很大一片水下空间(9D-9F相当于1D-1F),对应的空间编号偏移值为4A~60(十进制的1-75~1-97),很容易就会取到这一片。并且再根据这张图:

空间1D~1F正好对应水下4-4,2-4,3-4,所以基本上从x-1开始进去就是一关就停,也看不到后面的关卡。所以水下的占比实质上和感受上都较多。
(注:著名的0-1相当于1-77,对应的是9D08处9D这一数值,由此为空间1D水下4-4,并且0-3就是水下2-4,部分D版ROM有可能会因为自行走过场等问题使0-3显示为0-2)