0%

FC游戏背后的秘密(二):超级马里奥兄弟的奇怪关卡

关于所谓FC超级马里奥兄弟初代(以下简称SMB1)隐藏关卡的问题,出自当年都市传闻,如果经常看敖厂长视频的应该有所了解,这是事实上存在的:

  • 先玩一会儿SMB1
  • 快速拔掉卡带换成网球玩一会儿
  • 再拔掉卡带把SMB1卡带插回去
  • 按RESET
  • 进入标题后,按START+A开始游戏

就能进入“随机”隐藏关。但这些关卡似乎在“设计”上有很多问题,要么进去就死,要么玩着玩着就卡死,要么就是没有路或者死循环反正不能过关,再要么就是过关后卡死或者回到标题。

FC吧主金属狂人对此也做过验证,用了不同版本的主机和游戏,该问题基本上能复现,除了以下两种情况:

  1. 美版NES由于锁区问题换卡时相当于按Power重启
  2. 使用欧版《SMB1+打鸭子》合卡时无法进入

在这里先说结论:隐藏关在设计上并不存在。任天堂在SMB1中没有从来没设计过这些隐藏关,而是由于程序bug设置了错误的关卡ID,因此引用错误的数据去创建场景。这些关卡应该叫“bug关卡”。于是我产生了以下疑问:

  1. “bug关卡”触发的原理是什么?
  2. 为什么欧版合卡无法进入“bug关卡”?
  3. 既然复现步骤这么复杂,那怎样在(无法热拔插卡带的)模拟器上玩到这些“bug关卡”?

初期探索

首先是使用模拟器的RAM Search功能(当然,也可以翻翻金手指大全什么的),找到了SMB1存放关卡ID的内存:

当前玩家
大关 $075F
小关 $0760

另一个玩家
大关 $0766
小关 $0767

标题界面将$075F设置为#$00~#$07代表正常的World 1到World 8,超过这个范围就是开局进入9-1、A-1……Z-1或者各种奇怪的符号-1什么的bug关卡。

不过之后在用$075F写入作为断点调试网球的时候,发现这个地址从头到尾都没有动过。既然网球不会操作这个地址,那就存在其他网球用过的地址和SMB1的重合,并在后续RESET时没有被清理,最终传入了$075F。为了验证这个想法,还需要看看RESET及内存初始化相关的代码。

关于内存初始化

首先跟踪初始化后$075F的变化,先设置$075F写入为断点,然后RESET:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; 初始化
90CC: LDX #$07
90CE: LDA #$00
90D0: STA $06
; 外循环X开始
90D2: STX $07
; 内循环Y开始,Y是外面传进来的
90D4: CPX #$01
90D6: BNE $90DC
90D8: CPY #$60
90DA: BCS $90DE ; 跳过$01FF~$0160
; 设置为0x00
90DC: STA ($06),Y
90DE: DEY
90DF: CPY #$FF
90E1: BNE $90D4 ; 内循环结束
90E3: DEX
90E4: BPL $90D2 ; 外循环结束
90E6: RTS

这段程序会被多次调用,第一次执行时,栈里只有两个字节,2D,80,很明显就是$802B附近调用(JSR)了这段程序,SMB1的RESET向量是$8000:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
8000: SEI
8001: CLD
8002: LDA #$10
8004: STA PPU_CTRL
8007: LDX #$FF
8009: TXS
800A: LDA PPU_STATUS
800D: BPL $800A
800F: LDA PPU_STATUS
8012: BPL $800F
8014: LDY #$FE
8016: LDX #$05
8018: LDA $07D7,X
801B: CMP #$0A
801D: BCS $802B
801F: DEX
8020: BPL $8018
8022: LDA $07FF
8025: CMP #$A5
8027: BNE $802B
8029: LDY #$D6
802B: JSR $90CC

第二次执行时,栈顶指针是$8FD3,说明在$8FD1处调用的:

1
2
8FCF: LDY #$6F
8FD1: JSR $90CC

同理,第三次调用:

1
2
8FE4: LDY #$4B
8FE6: JSR $90CC

由此可见,三次初始化中范围最大的一次是$0000~$07D6(排除$0160~$01FF),后面越来越小。

特殊入口

进入标题后,直接按START开始游戏,并不会触发写入$075F的断点,但是注意,bug复现步骤中明确提到,要按START+A开始游戏,看来游戏对有没有按下A键有不一样的处理。RESET后回到标题画面启用断点,START+A,果然,程序停在了$830E

1
2
3
4
5
6
7
8
9
82E0: LDA $07FD
82E3: JSR $830E
...
830E: STA $075F ; 此时栈顶指针是指向了$82E5
8311: STA $0766
8314: LDX #$00
8316: STX $0760
8319: STX $0767
831C: RTS

至此,一个新的内存地址$07FD出现在了我们眼前。
按下START+A时,会先从$07FD取出数据,然后设置到$075F$0766,完成World的初始化。$07FD怎么来的暂时不重要,重要的是,刚才发现不管你怎么捅RESET,只要不断电重启机器,三次初始化都不会对$07FD这个值造成影响。那么,只要能在游戏过程中写入$07FD这个地址,任意一个游戏都能替代网球,成为触发bug的扳机。

为什么欧版合卡无法触发bug

吧主大人的解释是欧版合卡的基板与网球不大一致,不兼容,导致无法触发。这样的说法显然不能让我满意,最合理的解释当然就是合卡可能在RESET时对$07FD进行了初始化。要验证这个猜想,只要下个断点就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
80EF: LDA #$00
80F1: LDX #$00
80F3: STA $00,X
80F5: STA $0400,X
80F8: STA $0500,X
80FB: STA $0600,X
80FE: STA $0700,X ; 在此中断
8101: INX
8102: BNE $80F3
8104: LDX #$00
8106: LDA #$F8
8108: STA $0200,X @ $02FD = #$F8
810B: INX
810C: BNE $8108
810E: RTS

很明显了,合卡RESET时,会对$00~$FF$0400~$07FF进行初始化,其中当然包括$07FD,所以插上这张合卡,按下RESET,续关记录就没了,所以无法触发bug。

关于模拟器选关

实际上GoodNES中收录了各种改版的SMB1,数量不小,有951个,其中就有一些开局进入bug关卡的沙雕hack版。“俄国喷神”Kiraman在一期视频《Dendy编年史#1:超级马里奥》中就带我们玩了一下一盘1994年国产的9999合1卡带(GoodNES中也有收录),上面有不同关卡开局的SMB1,根据前文的结论,其实就是将$07FD设为不同值然后引导进入游戏。

不用Hack版也可以用金手指锁定$07FD的方式,或者用Hex Editor手动改一下$07FD,只要掌握了原理,方法就多种多样,随意选择。

关于ROM修改

SMB1对于ROM的利用率非常高,放眼望去几乎没有几处连续的00或者FF,即使有也就几个字节,完全不够修改,查阅了国外大佬提供的一篇关于SMB1内存映射相关文档,上面也表示几乎没有连续的未使用。因此要改程序的话,要么扩容(比如刚才那个SMB1+打鸭子),要么加到trainer中,反正都是在模拟器上运行,就放在trainer中吧。

有机会的话,再做个标题界面按上下键选关玩玩吧。

附录:关于$07FD的原本用途

在随后的测试中,发现只会在GAME OVER以后写入$07FD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
9237: LDA #$00
9239: STA $0774
923C: LDA $06FC
923F: AND #$10
9241: BNE $9248
9243: LDA $07A0
9246: BNE $9281
9248: LDA #$80
924A: STA $FC
924C: JSR $9282
924F: BCC $9264
9251: LDA $075F
9254: STA $07FD
9257: LDA #$00
9259: ASL
925A: STA $0772
925D: STA $07A0
9260: STA $0770
9263: RTS

回到标题画面以后,按START+A就能续关,如果不小心按了START也没关系,只要不GAME OVER,按下RESET还是能续关,我猜测不清理$07FD以及START+A组合键的原本作用也许就是开发人员用来测试的。