the cutting room mirror

Mega Man (DOS)

This article has a talk page!
view on tcrf.net

This page contains notes for the game Mega Man (DOS).

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 register cs=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: and mm.exe. Use the nd cheat code and play through to the Wily level.
  • As each breakpoint is hit, you can delete it by number and continue.

Source code fragment#

Original design documents contain a printout of a source code listing with handwritten annotations. The same printed page appears at 13:00 in The Gaming Historian's video.

XBSTR  STRUCT
   XBMP    DW  0   ;Explode Block Map Position
   XBHP    DB  0   ;Hit Points
   XBRD    DB  0   ;Regular Damage
   XBSD    DB  0   ;Sonic Damage
   XBDD    DB  0   ;Dyna Damage (Volt No Damage)
XBSTR  ENDS    ;6 bytes

FRSTR  STRUC
   FRXL    DW  0   ;x left offset
   FRXR    DW  0   ;x right offset
   FRYT    DW  0   ;y top offset
   FRYB    DW  0   ;y bottom offset
   FRXO    DW  0   ;x offset
   FRYO    DW  0   ;y offset
   FRWD    DW  0   ;UnInitialized width
   FROF    DW  0   ;UnInitialized :OFS pointer
   FRSG    DW  0   ;UnInitialized SEG: pointer
FRSTR  ENDS    ;18 BYTES

SPSTR  STRUC
   SPCI      I     ;Sprite Code Index for animating    Select 1st
   SPXC    DW✓ 0   ;x center pos
   SPYC    DW✓ 0   ;y center pos
   SPFR    DW✓ 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 explode
   SPRC    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)
SPSTR  ENDS    ;36 bytes

BFSTR  STRUC
   BFFL    DB  0   ;Flags ✓
   BFYO    DB  0   ;Y Pixel Offset ✓
   BFSP    DW  0   ;Sprite Offset
   BFXB    DW  0   ;X Block ✓
   BFYB    DW  0   ;Y Block ✓
   BFXM    DW  0   ;X Movement ✓
   BFYM    DW  0   ;Y Movement ✓
   BFBT    DB  0   ;Bullet Timer ✓
   BFBL    DB  0   ;Background Block ✓
BFSTR  ENDS    ;14 bytes

TBSTR  STRUC
   TBCN    DB  0   ;Count to place block
   TBBN    DB  0   ;Block to place

The SPSTR data structure 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