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 | 背景音乐选曲 |
CHAMPION | BOSS RUSH |
PETUNIAX | 挑战关卡 |
IAMHAX0R | 作弊&选关 |
SGCLEVEL | 特殊挑战关卡 |
前两个密码是通过修改内存让它生成我想要的密码;后面五个则是通过搜索游戏文件找到的。
如果想看过关剧情,修改内存 1B 1C
的内容为 08 00
,然后死一次选择继续即可触发通关剧情。