Notes:Mega Man (DOS)
This page contains notes for the game Mega Man (DOS).
Contents
Reverse engineering source code etc.
In case I never properly write up my findings on Mega Man PC, here is some disorganized source code and other resources. It has, for example, unpackers for the .scn (maps), .blk (tiles), .frm (sprites), and .sta (fullscreen graphics) formats. There's a program to rip the sound effects as .wav files. Some additional information is documented as code comments, for example the meaning of tile properties (gravity, friction, etc.) in map/main.go. There's some lightly analyzed disassembly of the game code.
git clone https://www.bamsoftware.com/git/megamanpc.git
EXEPACK
The mm.exe executable is packed with EXEPACK. You have to unpack it in order for the disassembly to make sense. As of 2018-12-07, the suggested OpenKB unexepack.c tool has a bug: it stops processing the compressed relocation tables when it reaches a section that is empty of relocations, ignoring any following sections that may not be empty. mm.exe has non-empty sections following empty sections and is affected by this bug. Instead, either use https://github.com/w4kfu/unEXEPACK (commit eaf8f6a1 or later), or apply this patch to the OpenKB version:
--- unexepack.c.orig +++ unexepack.c @@ -330,8 +330,6 @@ int num_entries = READ_WORD(pbuffer, p);p += 2; - if (num_entries == 0) break; - int k; for (k = 0; k < num_entries; k++) {
Details about the invincibility code
The way the invincibility cheat code works is, a magic global variable has to have the value 0x01 when the menu finishes. The value of the variable is initially 0x00. Pressing n
ORs the variable with 0x02, and pressing d
XORs the variable with 0x03. So nd
works, because (0x00 | 0x02) ^ 0x03 == 0x01
, but dn
does not work, because (0x00 ^ 0x03) | 0x02 == 0x03
. But other variations will work, like nddd
, ddnd
, dnnd
.
WILEY trigger tile callbacks
It initially appears that four of the trigger tile callbacks in WILEY.BIN are unused, because their corresponding tile indices point to out-of-bounds tiles. But running in a debugger with breakpoints set on the callbacks shows that they actually are called, in the boss arenas during refights.
Here is the trigger tile table, which starts at offset a88 in WILEY.BIN. The highlighted rows are the ones that at first seem to be unused.
tile index | tile coordinates | callback address |
---|---|---|
0585 | (13, 7) | 0bb1 |
04c6 | (22, 6) | 0c89 |
04e0 | (48, 6) | 0ce3 |
04fa | (74, 6) | 0d3d |
0512 | (98, 6) | 0d97 |
052d | (125, 6) | 0df1 |
0ddd | (149, 17) | 0e80 |
0001 | (1, 0) | 0ffa |
088d | (189, 10) | 10db |
1676 | (150, 28) | 11c8 |
1806 | (150, 30) | 11c8 |
17b0 | (64, 30) | 122a |
17c0 | (80, 30) | 126a |
17ce | (94, 30) | 12aa |
1860 | (40, 31) | 1327 |
0002 | (2, 0) | 14c8 |
19d2 | (10, 33) | 15c1 |
22a6 | (70, 44) | 163a |
2386 | (94, 45) | 1695 |
2554 | (156, 47) | 178f |
0003 | (3, 0) | 1904 |
0004 | (4, 0) | 19e5 |
2895 | (189, 51) | 19f5 |
2dee | (158, 58) | 1af3 |
2c48 | (136, 56) | 1b49 |
2dd8 | (136, 58) | 1b49 |
2e81 | (105, 59) | 1b9f |
2e67 | (79, 59) | 1c14 |
2e4b | (51, 59) | 1cee |
This is where each callback is called:
- 0ffa is called after beating Sonic Man
- 14c8 is called after beating Volt Man
- 1904 is called after beating Dyna Man
- 19e5 is called upon entering Dyna Man's arena
This is what I did:
Drives→Import Image
in JPC-RR on the directory containing the game files. Image Type: Hard Disk; Sides: 16; Sectors: 63; Tracks: 16.- Assemble a machine with the image as HDA, then
Drives→dump→HDA
, save as hda.img. Drives→dump→FreeDOS (initial fda image)
, save as freedos.img.qemu-system-i386 -icount shift=10,align=on -drive file=freedos.img,index=0,if=floppy,format=raw -drive file=hda.img,index=3,media=disk,format=raw -boot a -s
Press F5 during startup so as not to run autoexec.bat; otherwise you'll get the error "Packed file is corrupt".gdb -ex 'target remote 127.0.0.1:1234' -ex 'set architecture i8086' -ex 'break *(0x5f870+0x0bb1)' -ex 'break *(0x5f870+0x0ffa)' -ex 'break *(0x5f870+0x14c8)' -ex 'break *(0x5f870+0x1904)' -ex 'break *(0x5f870+0x19e5)' -ex 'continue'
5f870 is the address at which .BIN files are loaded into memory; i.e., the registercs
=5f87. 0bb1 is the trigger tile callback for the initial shutter, included as a check to see that the breakpoints were working.- Inside QEMU,
c:
andmm.exe
. Use thend
cheat code and play through to the Wily level. - As each breakpoint is hit, you can
delete
it by number andcontinue
.
Source code fragment
There's a photograph of a source code listing, with handwritten annotations, at 13:00 in The Gaming Historian's video.
FRSG DW 0 ;UnInitialized SEG. pointer ???TR ENDS ;18 BYTES ???TR STRUC SPCI ;Sprite Code Index for animating Select 1st SPXC DWI 0 ;x center pos SPYC DWI 0 ;y center pos SPFR DWI 0 ;frame SPHP DBI 0 ;hit points block # of explode SPAP DBI 0 ;attack points ✓ SPF0 DBI 0 ;flags 1 04 ???? SPF1 DBI 0 ;flags 2 40 active SPXL DW 0 ; SPXR DW 0 ; SPYT DW 0 ; SPYB DW 0 ; XMAX DWI 0 ;init x XMIN DWI 0 ;Block #screen ptr of explode YMAX DWI 0 ;init y YMIN DWI 0 ; SPHC DB 0 ;UnInitialized hit timer (0)Block # of explodeSPRC DB 0 ;UnInitialized count 1 (0) delay SPCC DB 0 ;UnInitialized count 2 (0) SPFC DB 0 ;UnInitialized face (right) JUMP DW 0 ;UnInitialized jmp flgs (0) SPXM DW 0 ;UnInitialized x mmntm (0) SPYM DW 0 ;UnInitialized y mmntm (0) ???TR ENDS ;38 bytes ???TR STRUC
The data structure defined mostly corresponds to what is called an "actor" in the HUD script used for a TAS:
function read_actor(base) local frm_addr = BIN_AREA + jpcrr.read_word(base+4) return { x = jpcrr.read_word(base), y = jpcrr.read_word(base+2), frm_addr = jpcrr.read_word(base+4), xmin = jpcrr.read_word_signed(frm_addr), xmax = jpcrr.read_word_signed(frm_addr+2), ymin = jpcrr.read_word_signed(frm_addr+4), ymax = jpcrr.read_word_signed(frm_addr+6), hp = jpcrr.read_byte_signed(base+6), damage_amount = jpcrr.read_byte(base+7), -- or could be pickup type: 1=1up 2=etank 3=big health 4=small health 5=big weapon 6=small weapon flags = jpcrr.read_byte(base+9), x_bound_max = jpcrr.read_word(base+10), x_bound_min = jpcrr.read_word(base+12), y_bound_max = jpcrr.read_word(base+14), y_bound_min = jpcrr.read_word(base+16), counter_0 = jpcrr.read_byte(base+18), counter_1 = jpcrr.read_byte(base+19), counter_2 = jpcrr.read_byte(base+20), facing = jpcrr.read_byte(base+21), extra_0x18 = jpcrr.read_word_signed(base+0x18), extra_0x1a = jpcrr.read_word_signed(base+0x1a) } end