Dragon Quest III:ROM map
All values given are ROM offsets. They assume that a 512-byte copier header is not present. If there is a header present, you will have to add 512 ($200) to each offset lister here to get the correct offset in the ROM file.
Note that the ROM is mapped into the SNES address space starting at $C00000. Therefore, if you want to use one of these ROM offsets as a breakpoint, you should add $C00000 to it. I've given a few examples below that already do this.
Menu Size Data
Almost every menu has a 16-bit (2-byte) value that determines its starting position (X/Y coordinates) and its width. Menu size data is stored in the following binary format:
%1WWW WWYY YYYX XXXX
- WWWWW = menu width (not just usable space, this includes the edge characters)
- YYYYY = vertical tile offset from top of screen
- XXXXX = horizontal tile offset from left of screen
There is no value to determine the height of a menu. This is done internally by the drawing code for each individual menu.
Like almost everything else on the SNES, this data is in low-endian format. Thus, the YYYX XXXX byte comes first in the ROM, then the 1WWW WWYY byte.
Here are a few menu size values that I have found. Note again that this controls only menu width and position. Menu content string pointers are stored elsewhere.
- $30348-$30349: Introductory menu (contains Begin a New Quest, Continue Quest, Copy Quest, etc., etc.)
- $30378-$30379: Change Message Speed menu (opened from introductory menu)
- $30384-$30385: Mono/Stereo selector menu
- $30024-$30025: Main walkabout menu (contains Talk, Spell, Item, Search, Status, Tactics)
- $300FC-$300FD: Gold menu, associated with the main walkabout menu
- $3036C-$3036D: Change Message Speed menu (opened from Tactics)
What if the menu you want to expand (or contract!) isn't listed above? Here's how to find it.
- Open the menu you want, and take a screenshot of it. As an example, let's try the gold box that appears when you open the main menu.
- Open the screenshot in a paint program, and find out the X coordinate, Y coordinate, and width. Remember that these coordinates are in tiles, so you have to find the coordinate (or width) in pixels and then divide that number by 8.
- Convert these values to hexadecimal and write them down. In this case, our X coordinate is 21 ($15), the Y coordinate is 16 ($10), and the width (including borders) is 9 ($09).
- Open the ROM in a debugging emulator (I use Geiger's Snes9x debugging version).
- Prepare things so that it will only take 1 more button press to get into the menu you want.
- Set an execution breakpoint at $C90717.
- Open the menu — this should set off the breakpoint.
- Keep an eye on the A register.
- You might see something like this as the breakpoint is hit four times (On Geiger Snes9x, hit the "Run" button to get to the next breakpoint).
- $C9/0717 08 PHP A:0000 X:0000 Y:0015 P:envmxdIZc
- $C9/0717 08 PHP A:0015 X:0000 Y:0015 P:envmxdIzc
- $C9/0717 08 PHP A:1015 X:0000 Y:0015 P:envmxdIzc
- $C9/0717 08 PHP A:0009 X:0000 Y:0015 P:envmxdIzc
- This is for the gold menu. If you followed the steps above, you should see some familiar information here. Note how on the second iteration, we see the X coordinate in the accumulator, and on the third iteration, we can see both the X *and* Y coordinates in the top and bottom halves. And on the fifth iteration, there is the width.
- Don't proceed past the fifth iteration (the one where A = menu width). Instead, single step ("Step Into" button on Geiger Snes9x) until you get to $C902B1.
- You'll see something like this:
- $C9/02B1 33 10 AND ($10,s),y[$C3:00FD] A:0F80 X:00C3 Y:0000 P:envmXdIzc
- See that [$C3:00FD]? Now just take that number and subtract $C00001 (yes, $C00001, not $C00000) from it.
- We now have the location of the menu size data; in this case, $300FC. Remember that the value at this address is two bytes long, and low-endian.
- Note that, in some cases, there will be unwanted $C90717 breakpoints before the one we're looking for. That's why you should monitor the A register and make sure we're looking at the right iteration. The method I detailed above works only if you check the address on the iteration that contains the menu width ($0009, in this case).
Menu Text Pointers
To change the menu text, you need to find the pointers for that text. In most cases, this pointer is actually the 16-bit operand of an LDA opcode. Pointers can usually only reference the 64K block of memory from which they are called; in other words, if a pointer is at $360D1 in the ROM, it can't reference anything above $3FFFF or below $30000. In some cases, a pointer can't be made to point backwards without further hacking, because of the way that they are referenced. (More to come...) Fortunately, this shouldn't really be a big issue, because there is a block of apparently unused space (solid $FFs) from $3F30A-$3FFFF. Most pointers are in the range of this memory. (Please let me know if I'm mistaken and this empty space actually serves a purpose!)
A list of menu pointers can be found at the Kojiro Translations wiki.
Menu Padding Routine
Some menu entries have extra spaces padded onto the end that is not accounted for by the data at the expected pointers. These spaces are inserted by the use of a special routine located at $32B70 in the ROM. When this routine is called, the X register contains the number of spaces to insert. If you're doing some menu expansion, you may have to set breakpoints at $C32B70. Single-step through until RTL is executed, so you can see where it came from, and then back up five bytes from the resulting address. If these padding spaces are throwing off your menu borders, replace the JSL $C32B70 opcode with four NOPs.
Calling $32B8D inserts a single dummy space. The "Change Message Speed" option on the "Tactics" menu uses this method, so look out for it if you're editing that menu item. There may be others I haven't found yet that do the same thing. Again, you can track down the originating JSL opcode by breaking on the subroutine start, stepping through until RTL is executed, and then subtracting five bytes from the program counter at that point.