As my first project working with an MSX game, I wasn't sure going in how whether it would prove to be too daunting a task for me or not. Learning more about the MSX system and the original cartridge ROM, I found it quite enjoyable to tinker with. The hacking side of the project can be broken up into three basic tasks: getting the original text out, getting the translated text back in, and working with the SRAM.
In order extract the original text, I read up on how MSX formats text data and displays it on screen. It turns out the MSX has a TEXT ROM containing a set of characters that can differ from region to region. The Japanese TEXT ROM is laid out similar to JIS formatting, with a set of hiragana characters split between 0x80-9F and #E0-FE.
Since the menu text in the game is was already in english, I was able search for that in the rom, no problem. Likewise, I could search for bits of katakana text in the game to find the item names and general dialog was stored. I manually overwrote a line of dialog text with some test data...and immediately got an SRAM ERROR when trying to run the game! Once I figured out how to get past that, however, I was able to see the new text in-game and we were in business.
I wrote a script to extract the text from the sections of I had identified, but I noticed that apart from the uppercase English and the katakana, there were random lowercase letters in the displayed throughout the hiragana text. Comparing the bytes in memory with the text on screen, I determined that the game's text has the hiragana characters from #60-9F, removing the lowercase letters and keeping the hiragana together in one contiguous chunk. So I wrote another script to convert the extracted text to the MSX-DOS format so I could then run it through a conversion program I found called MSX Kana Filter to convert it to Shift-JIS. With that, I now had the full script I could pass along to be translated.
The text data is stored at the following addresses in the rom:
item/weapons #31b7-3290
menu screen text #3291-34a7
dialogue #8001-bdc3
The menu items themselves are at #0FE4-1020, but since they are already in English there was no need to extract them.
Once I had the items, menu screens, and some of the dialogue translations in hand, I was able to start working out the finer points of how the game displays text and how to go about reinserting. Since the items and menus are pretty small, it was easier to just edit those directly in the hex editor. The pointers to weapons and items are in a table directly before the names, at #3177 and #3197, respectively. The pointers to the lines for the menu screens are all hardcoded in their subroutines, so I just tried to squeeze them into the original character limit for playtesting until I had a better idea of what we wanted it to say and I could map out the pointer locations.
I set to work analyzing how the game addresses and displays the dialogue lines. When you TALK, SEARCH, or TAKE, the game carries out the following steps:
1: check address #C048 to get the character ID of the tile you are facing.
2: Take that ID times #30 plus an offset for the command chosen and look at that index in a table located at #C299.
3: Increment the index until you reach a 00. The amount of times incremented gives you the line index.
4: Take the character ID, command offset, and line index and check a table of 2-byte addresses at #8000 (read from bank #0E in the rom),
which gives an index for the table at #9000 which has instructions to determine what flags to check for the line of dialog. I've mapped out these flags here.
5: If the flags are not met, decrement the line index
6: Use the ID, offset, and line to reference the table of pointers at #9800. Set byte 7 and this is the pointer for the start of the dialogue line.
7: Read banks #04 and #05 to #8000 and #A000, then start displaying the text at the pointer from step 6 until 00 is read.
The table at 9800 is where we'll need to adjust the pointers for the new translated lines. It is located at #1D800 in the rom.
To insert the translated text, I wrote a script that takes our rom that has the items and menus manually edited, and combines it with the textfile of the translated dialog lines. It copies the bytes of the rom until it gets to address #8000. From there, it saves the current position to an array for the table of pointers, then copies the next line from the translation file. It continues doing this until it reaches the end of the translation file, where it adds "FFFF" to the end of the pointer array, or reaches #A000. Then it goes back to copying the rom file until it gets to 1D800, which is the table of pointers. Here, it reads in two byes at a time from the rom. If both are zeroes, it writes that to the output, otherwise it writes the next address from the array. It continues doing this until it reaches the end of the pointer array, whereupon it continues copying the rom until the end of the file at #40000.
The original rom for War of the Dead is 256kB and uses almost all of it. Anticipating that the translated script would exceed the 2kB allocated for the original, I began looking into how MSX pulls data from the cartridge into memory. There are many different memory mappers used in MSX cartridges depending on the manufacturer. War of the Dead uses the ASCII8 mapper with 8K SRAM, which allows it to read data from the rom to memory in 8kB banks. These banks are swapped in by loading the number of the bank into a specific memory address, so when the game needs to load the dialogue text, it loads #04 to address #7000 which tells it to map the 8kb of memory in the 4th bank of the rom (#8000) to #8000 of the MSX addressable memory. It then increments the bank number and maps the next bank to #A000 by loading it to #7800.
The ASCII8 mapper can support rom sizes up to 1024kB or 2048kB, so by upping the ROM size to 512kB we can split the translated scripts across multiple banks and swap in the one we need at a given time. Since we really only need one additional bank to fit and the game loads two at a time, I figured splitting it near the middle so we had two chunks of about 1.5 banks would be easiest and give us plenty of room to expand on the first half if needed without needing to adjust the split point. Since the lines are all in order by the character ID number (all of character #00's lines, then all of character #01's lines, etc.), I picked a spot in the middle of the script that happened to correspond to character #10. Then I replaced the assembly command to load the first bank with a call to the following subroutine:
3a 48 c0 ld a,(#c048)
e6 10 and #10 (upper bits set for the character ID after the split)
28 04 jr z, +04
3e 20 ld a,#20
18 02 jr +02
3e 04 ld a,#04
32 00 70 ld (#7000),a
c9 ret
It then comes back to the main code, increments and loads the second bank. I found a small bit of unused space at #2800 in the rom, which is loaded into #6800 at runtime. There are three places it needed to be called from #1272, #159F, and #1A0E. The second half of the script is being addressed at bank #20 and #21, so I needed to update the insertion script to handle it. After it reaches the end of the first script half, I have it run through the second half to calculate the pointers, but keeps the output in a separate array. Once it reaches the end of the original rom, it copies the array to the end and then pads with zeroes to fill out the rest of the rom size.
Talking, searching, and taking displays text in a dialog box that is 16 characters wide and 13 rows tall. Most of the time, it will only use the bottom 8 rows to leave space for character portraits or item pictures at the top. It will automatically wrap to the next row after the last column, and back to the first row if the bottom is reached. There are certain special characters or bytes it checks for when displaying text. The original game doesn't need to use these often because the japanese text uses can wrap to the next line in the middle of a word no problem, where english demands more precise formatting. Some of these special characters were not picked up correctly by the extraction script or because they contained bytes of #00 or newline characters, but they were few enough to track down and fix manually. I just put placeholder spaces for them in the translated script and write them into the rom manually after insertion.
Character | Bytecode | Parameters | Function | Notes |
---|---|---|---|---|
\ | 5C | none | New line | Move to the next row of the dialogue box. |
^ | 5E | none | Clear screen | Clear the dialogu box of text and move to the start of the first row. |
@ | 40 | none | Wait | Displays a down arrow icon and waits for player input before continuing. |
9e, 9f, de, df | none | dakuten/handakuten | These are the dakuten and handakuten characters for hiragana and katakana, but have their own special subroutine to display them at half-width (4px). I later realized I could dummy out the call that actually prints the character at #0674 and use the rest of it as a half-width space instead. | 01 | xx | Give item | Gives the character item ID xx. |
03 | xx | Show image | Displays image xx in the center of the two character portraits | |
04 | xx | Start battle | Starts a battle with ID xx. | |
05 | xx | Increase Max MF | Increases the player's Max MF by xx. | |
06 | xx | Take character | This tells the game to have character xx start following you. They will follow behind your sprite until you get back to the church. | |
09 | xx | Character image L | Displays image xx over the left character portrait spot. | |
0A | xx | Character image R | Displays image xx over the right character portrait spot. |
As soon as I made any changes to the original rom, it refused to do anything but give this SRAM ERROR message when booted. Since I didn't think I had made any changes that would affect the save RAM, I worried that it might be some sort of checksum or copy protection. So I tracked down the subroutine that was failing at #0290 in the ROM and had it immediately return instead, that way I could at least boot the game and see the text changes I had made.
It wasn't until playing through later that I realized that the in-game saves weren't actually saving at all. I took a look at what the save/load subroutines where actually doing, and noticed they were accessing bank #20, past the length of the original ROM, which was now where the second half of the script was being stored. Changing the bank to point to #40 didn't seem to make a difference, then I remembered about the SRAM error I was receiving earlier. Turns out this subroutine was to initialize the save RAM, but I didn't understand why it was giving the error before I had expanded the rom then.
I loaded the original unmodified ROM again and that's when I noticed what was happening. The emulator was loading the original ROM with the "ASCII8 (SRAM)" mapper, whereas the modified one was using plain "ASCII8". Turns out, it has a database of known game ROMs it checks and modifying it changed the checksum, so it defaulted back to trying to autodetect the mapper. I manually set the correct mapper and everything worked, no error and saving/loading working correctly!
When I handed it off to the person testing on real hardware, we found out that their flash cart didn't actually support internal SRAM, so we decided to release two versions of the patch in the end, one with SRAM enabled and one without.
Character IDs: read from c048 to determine who you are interacting with and what flags to read/set
00 Carpenter
01 Carrie
02 Franklin
03 Janet
04 Carol
05 Wells
06 Peter
07 Roger's corpse
08 Sandra
09 Patrick
0a Doctor's corpse
0b Herbert
0c Gordon
0d Fran
0e Stephen's corpse
0f Omadon
10 Scott's corpse
11 Cassel's corpse
12 Hyams' corpse
13 Cameron
14 Cronenberg
15 Romero
16 Wes
17 Sam's corpse
18 Toby's corpse
19 grave
1a ruins
1b cross(w sword)
1c city hall door
1d library(manuscript)
1e library(decoder)
1f city hall(family tree)
20 File
21 Manuscript
22 Family Tree
23 Pendant
24 Decoder
25 cross(church passage)
26 Radio
ff nothing
Map IDs: c004
00 Overworld
01 Church
02 Church Basement
03 School
04 School Attic
05 City Hall
06 Drug Store
07 Library
08 Library 2F
09 Hospital
0a Morgue
0b Mountain Hut
0c Hut Basement
0d Base Camp
0e Supply Depot N
0f Supply Depot W
10 Supply Depot E
11 Supply Depot S
12 Ruins
13 Ruins Basement
14 Graveyard
15 Crypt
16 Mansion
17 Mansion Basement
18 Tunnels
Boss tiles: table at 6A52; mapID, x, y, enemy gfx tiles, enemy id 1,2,3,4, background gfx (last 6 bytes copied to 1003)
04 0f 05 04 0c ff ff ff 0c
06 05 01 0c 24 ff ff ff 0e
08 07 05 04 0d 0d 0d ff 0f
0c 04 07 03 0a 0a ff ff 12
15 02 04 04 0c ff ff ff 17
18 24 12 0a 1e ff ff ff 1c
18 2b 20 08 1a ff ff ff 1d
18 55 48 0c 26 ff ff ff 1a
18 52 1e 03 08 ff ff ff 1b
18 2a 38 08 19 ff ff ff 1f
Items, Weapons, and Menu screen pointers
table of pointers to weapons at 3177, items at 3197, menu lines are hardcoded into each of the subroutines.
Lines needing control code adjusting after insertion:
[page1] 43(0a),54(0a),100(0a),106(0a),154(0a),160(0d)
[page2] 26(0a),27(0a),28(0a),29(0a),32(0a),34(0a),38(0a)
There is a chest in the church basement that is not properly openable because the location is incorrect in the chest content table located at 1ECO in the ROM.
The table has 02 0a 03 10, which should actually be 02 0a 0d 10. There is another that is 0e 02 02 12 which should be 0e 03 02 12.