跳至内容

Jixun's Blog 填坑还是开坑,这是个好问题。

Battle Kid: Fortress of Peril - 简易分析

前言

这是一个 2010 年制作的游戏,类似于 I Wanna be the Guy 和洛克人的一款 8 位硬核游戏,还做成了 NES 卡带销售。官网 @Sivak Games

文字输出

首先得把负责渲染文字的部分找到。

; Battle Kid 1 - 字符输出简易分析
; 分析 by Jixun<https://jixun.moe/>
; 转载请注明出处。

; FCEUX 断 PPU 写入 $2984 运行几次即可找到该处。

; BANK 09
.org $82E2

_82E2:
  LDA $00D2
  STA $2006
  LDA $00D3
  STA $2006

  LDY $00D1    ; 对话偏移
  LDA ($A0),Y  ; A = 当前对话字符
  BMI _82FA    ; 如果第一位是 1 则跳 (控制符?)
  STA $2007    ; 直接打印符号
  INC $00D1    ; 对话偏移++
  INC $00D3    ; 坐标++
  RTS ; ------------------------------------------------------------------------

_82FA:
  INY          ; Y++
  STY $00D1    ; 对话偏移++

  CMP #$F0     ;
  BCS _831D    ; 如果 A >= 0xF0 则跳到下方的特殊控制符

  AND #$7F     ;
  BEQ _830A    ; 如果 A == 0x80 则跳 (换行)

  STA $00C3    ; 否则就设定等待时间 (0x81~0xEF)
  
  ; 跳转到 _C230
  ; 方便阅读合并到一起了
  INC $000A
  INC $000A
  RTS ; ------------------------------------------------------------------------

_830A:
  LDA $00D3    ;
  AND #$E0     ; A = [00D3] & 0xE0 (行首位置)
  CLC          ;
  ADC #$20     ; A += 0x20 (刚好一行)
  ORA ($A0),Y  ; A = A + ([0xA0] + 下一个字符开头空格数)
  STA $00D3    ; <- 回写 00D3

  BCC _8319    ; 进位
    INC $00D2
  _8319:

    INY
    STY $00D1
    RTS ; ----------------------------------------------------------------------

_831D:
  ; A >= 0xF0
  ; A: 0~F
  AND #$0F
  TAX       ; X

  ; 根据跳转表进行跳转
  LDA CTRL_JUMPS,X
  STA $000B
  LDA CTRL_JUMPS+1,X
  STA $000C

  LDY $00D1
  JMP ($000B)

CTRL_JUMPS:
  ; F0:
 .word $833D
  ; F2:
 .word $8346
  ; F4:
 .word $837C
  ; F6: 显示人名 (3 BYTE)
 .word $834D
  ; F8: 结束对话 (1 BYTE)
 .word $83A2
  ; FA:
 .word $836D
  ; FC:
 .word $8392
  ; FE (NOT USED)

; 分析还没做
_833D:
  nop



_C230:
  INC $000A
  INC $000A
  RTS

目前分析的文字系统大概如下:

  • 00~7F: 正常显示相应贴图块 (文字)
  • 80: 换行
  • 81~EF: 等待 (单位应该是帧,数字减掉 0x80;如 81 就是等待 1 帧)。
  • F0: 未知
  • F2: 未知
  • F4: 未知
  • F6: 显示人名(一次性输出一个单词?),后面跟着 2 个字节。
  • F8: 结束对话。
  • FA: 未知
  • FC: 未知
  • FE: 未使用,可以拿来汉化扩展修改 PPU。

过关密码

强制死亡,出现续命窗口,记录此时显示的密码,转换游戏用的编码。

let offset = 0x10;
let table = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.,!?';

function hex2str(str) {
  return str.replace(/\w{2}/g, x => table[parseInt(x, 16) - offset]||'-')
    .replace(/\s/g, '')
    .replace(/(.{8})/g, '$1 ');
}

function str2hex(str) {
  function prefix_hex(d) {
    return ('0' + d.toString(16)).slice(-2) + ' ';
  }

  return str
    .replace(/./g, x => prefix_hex(table.indexOf(x) + offset))
    .toUpperCase()
    .trim();
}

然后在模拟器进行内存搜索,对找到的内存下写入断点,顺藤摸瓜。

; Battle Kid 密码生成分析
; 分析 by Jixun<https://jixun.moe/>
; 转载请注明出处。

; BANK 04
.org $B498

; 06D0 ~ 06DF 全部重置为 0
  LDA #$00
  LDX #$0F

LOOP_RESET:
  STA $06D0,X
  DEX
  BPL LOOP_RESET

  LDX #$14  ; X = 0x14
  LDY #$00  ; Y = 0

_B4A6:
  TXA       ;
  ASL       ;
  TAX       ; X = X * 2

  ; word $B398[X] -> $00D4
  ; word $B3C2[X] -> $00D6

  ; Source   Target   SRC_MASK   TAR_MASK
  ; $B398    $B3C2    $B383      $B3EC
  ; C0 06    D8 06    01         01
  ; A2 06    D8 06    01         02
  ; C0 06    D8 06    10         04
  ; A0 06    D8 06    08         08

  ; C1 06    D9 06    02         01
  ; A1 06    D9 06    02         02
  ; C0 06    D9 06    08         04
  ; A0 06    D9 06    02         08

  ; C0 06    DA 06    02         01
  ; A3 06    DA 06    01         02
  ; C1 06    DA 06    10         04
  ; C0 06    DA 06    40         08

  ; A6 06    DB 06    01         01
  ; A0 06    DB 06    04         02
  ; A5 06    DB 06    01         04

  ; C1 06    DC 06    04         01
  ; C0 06    DC 06    04         02
  ; A4 06    DC 06    02         04

  ; A0 06    DD 06    01         01
  ; C1 06    DD 06    08         02
  ; C0 06    DD 06    20         04

  LDA $B398,X
  STA $00D4

  LDA $B399,X
  STA $00D5

  LDA $B3C2,X
  STA $00D6

  LDA $B3C3,X
  STA $00D7

  TXA       ;
  LSR       ;
  TAX       ; X = X / 2

  LDA ($D4),Y   ; A = [[D4]]
  AND $B383,X   ; A = A & [B383 + X]

  BEQ _B4CE     ; 如果 A = 0, 则不附加值
    LDA ($D6),Y   ; A = [[D6] + Y]
    ORA $B3EC,X   ; A = A | [B3EC + X]
    STA ($D6),Y   ; [[D6]+Y] = A

_B4CE:
  DEX
  BPL _B4A6

  LDX $06D8
  LDA $B09F,X
  STA $06D1

  LDX $06D9
  LDA $B0AF,X
  STA $06D2

  LDX $06DA
  LDA $B0BF,X
  STA $06D3

  LDX $06DB
  LDA $B0CF,X
  STA $06D4

  LDX $06DC
  LDA $B0D7,X
  STA $06D5

  LDX $06DD
  LDA $B0DF,X
  STA $06D7

  ; 检查坐标
  ; 对比当前检查点的坐标与检查点列表
  ; 如果存在就查表写出相对应的字符。
  LDX #$48
_B509:
  LDA $001B
  CMP $83BC,X
  BNE _B517

  LDA $001C
  CMP $83BD,X
  BEQ _B51B

_B517:
  DEX
  DEX
  BNE _B509

_B51B:
  LDA $B055,X
  STA $06D0
  LDA $0023
  BNE _B528
  DEC $06D0
_B528:
  LDA $B056,X
  STA $06D6
  RTS ; -----------------------------------------

在做分析,得出下面的数据:

; Battle Kid 密码生成 - 内存区域分析
; 分析 by Jixun<https://jixun.moe/>
; 转载请注明出处。

内存    标志
06C0 01 02 04 08 10 20 40 <- BOSS 123456 & 隐藏 BOSS?
06C1 02 04 08 10          <- 未知数值

06A0 01 02 04 08          <- 钥匙
06A1 02                   <- 跳跃高度
06A2 01                   <- FEATHER FALL / 羽毛
06A3 01                   <- 氧气瓶 (水下呼吸)
06A4 02                   <- 双段跳
06A5 01                   <- 双倍威力 (EASY 模式赠送道具)
06A6 01                   <- 坐标显示

死亡复活点

当前记录点地址: 001B & 001C
当前地图坐标地址: 001D & 001E

序号    坐标
01      18 01
02      16 09
03      12 0B
04      14 0F
05      14 17
06      0E 0F
07      08 12
08      0A 1A
09      04 22
10      00 2C
11      0E 26
12      10 1E
13      18 11
14      18 17
15      1A 1D
16      14 24
17      14 1C
18      1C 17
19      1C 21
20      1C 26
21      14 30
22      10 2B
23      08 2C
24      20 0B
25      24 0B
26      24 15
27      24 1E
28      1A 09
29      1E 00
30      1C 07
31      28 02
32      2C 03
33      30 03
34      2C 0E
35      34 10
36      02 26
37      36 03

密码表

密码作用
BLSPF2HM全道具、全钥匙(位于第一个检查点)
S4WJFKDS全道具、全钥匙(位于最终 BOSS 门前)
BK1MUSIC背景音乐选曲
CHAMPIONBOSS RUSH
PETUNIAX挑战关卡
IAMHAX0R作弊&选关
SGCLEVEL特殊挑战关卡

前两个密码是通过修改内存让它生成我想要的密码;后面五个则是通过搜索游戏文件找到的。

如果想看过关剧情,修改内存 1B 1C 的内容为 08 00,然后死一次选择继续即可触发通关剧情。

知识共享许可协议 本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

评论区