Translating Doubutsu no Mori e+ has been fairly easy so far. Even though most text had been reduced in length, almost all messages, choices, and strings were kept in a separate BMG file. Unfortunately, I ran into a problem when I decided it was finally time to translate the item list. In e+, item names had a max length of 10 characters. In Animal Crossing, the max length is 16. Resizing them would have been a simple task, if the item list wasn’t inside of forestd.rel (forest data relocation module).
The Plan
To start off, I attempted to resize the relocation module to allow for the names to be retrieved from there. At the time, I didn’t know about the relocation and import tables appended to the end of the file. This failed, and I was greeted by a black screen after the Nintendo logo. I decided to learn more about how the relocation file is structured, and learned that about the relocation and import tables. The import table contains a list of module ids (each rel file has an unique id assigned to it) followed by the location in the relocation table where this module is referred to. The relocation table allows the game to dynamically change instructions/data to fit where it is in memory. With that additional knowledge in hand, I attempted to again resize forestd.rel along with the relocation table. Unfortunately I was once again greeted with a blank screen after the logo.
At this point I decided it was impractical to continue attempts to try and resize forestd.rel. After some contemplation, I realized that I could write my own custom item name retrieval routine. I added a file to forest_2nd.arc (.arc is an RARC packed archive, sort of like virtual folders) that contained the translated item names at 16 characters a piece. After that, I arrived at the first problem. As it turns out, all the archive files are loaded into the GameCube’s ARAM (audio ram). It’s not possible to simply address it because it doesn’t exist in the same physical address space. To figure out how the game was retrieving NPC messages, I used Dolphin’s debugger to trip a breakpoint when “mMsgLoad_get_res” was called. From there, I discovered that through the use of relocation tables, an address was written to two instructions: li and lis (load immediate and load immediate shifted). These two instructions had their operations added together to get the address to the start of msg.bmg in ARAM. Then a function called “GetResourceAram” is called which takes three arguments: the address in ARAM to copy from, the address to copy to in main RAM, and the size in bytes to copy. Knowing this, I came up with the idea of determining the static difference between the address of the function I was overriding (mIN_copy_name_str) and the address of mMsgLoad_get_res. I used the instruction lhz (load half word and zero) to load the address to the ARAM start address. This required two calls and an add instruction to get the address.
Initial Attempt
After doing that, I wrote the rest of my routine code. Here’s the first version of it:
stwu r1, -0x20(r1) // allocate local variable storage
mflr r0
stw r0, 0x24(r1)
mr r6, r4 // put r4 in r6 just in case
mr r9, r3 // put address to copy to in r9
bl GET_PC
:: GET_PC
mflr r0 // now we have our pc + 0x14
li r7, 0x7FFC
add r7, r7, r0
lhz r8, 0x7292(r7) // store instruction lower 16 bits r8
rlwinm r8, r8, 16, 0, 31 // left shift instruction by 16 bits, then and it with mask 0xFFFF0000 (to remove the instruction bits)
lhz r5, 0x729A(r7) // store lower 16 bit (addi instr)
add r5, r5, r8 // calculate our offset in r5
lwz r3, (0)r5 // Load the actual Aram offset in memory
subis r3, r3, 0xF // -0xF0000
addi r3, r3, 0x29A0 // add to get the pointer to our file
cmplwi r6, 0x1000 // compare our id to 0x1000 (start of furniture and all valid ids)
blt NOT_VALID_ITEM // if less than 0x1000, go here
cmplwi r6, 0x1FFF // check to see if our item is between 0x1000 and 0x1FFF
bgt GREATER_THAN_1FFF // if it's greater, then we need to jump to a different location (might be 41A1000C)
subis r7, r6, 0x1000 // subtract to make our item index start at 0 now
b GET_FURNITURE_OFFSET
::GREATER_THAN_1FFF
cmplwi r6, 0x2103
bgt GREATER_THAN_2103
addi r3, 0x4000
subis r7, r6, 0x2100
b GET_OFFSET
::GREATER_THAN_2103
cmplwi r6, 0x2200
blt NOT_VALID_ITEM
cmplwi r6, 0x2267
bgt GREATER_THAN_2267
addi r3, 0x5040
subis r7, r6, 0x2200
b GET_OFFSET
::GREATER_THAN_2267
cmplwi r6, 0x2300
blt NOT_VALID_ITEM
cmplwi r6, 0x232F
bgt GREATER_THAN_232F
addi r3, 0x560C
subis r7, r6, 0x2300
b GET_OFFSET
::GREATER_THAN_232F
cmplwi r6, 0x2400
blt NOT_VALID_ITEM
cmplwi r6, 0x2530
bgt GREATER_THAN_2530
addi r3, 0x59C0
subis r7, r6, 0x2500
b GET_OFFSET
::GREATER_THAN_2530
cmplwi r6, 0x2600
blt NOT_VALID_ITEM
cmplwi r6, 0x2644
bgt GREATER_THAN_2644
addi r3, 0x6CD0
subis r7, r6, 0x2600
b GET_OFFSET
::GREATER_THAN_2644
cmplwi r6, 0x2700
blt NOT_VALID_ITEM
cmplwi r6, 0x2744
bgt GREATER_THAN_2744
addi r3, 0x7120
subis r7, r6, 0x2700
b GET_OFFSET
::GREATER_THAN_2744
cmplwi r6, 0x2800
blt NOT_VALID_ITEM
cmplwi r6, 0x2807
bgt GREATER_THAN_2807
addi r3, 0x7570
subis r7, r6, 0x2800
b GET_OFFSET
::GREATER_THAN_2807
cmplwi r6, 0x2900
blt NOT_VALID_ITEM
cmplwi r6, 0x290A
bgt GREATER_THAN_290A
addi r3, 0x75F0
subis r7, r6, 0x2900
b GET_OFFSET
::GREATER_THAN_290A
cmplwi r6, 0x2A00
blt NOT_VALID_ITEM
cmplwi r6, 0x2A8B
bgt GREATER_THAN_2A8B
addi r3, 0x076A0
subis r7, r6, 0x2A80
b GET_OFFSET
::GREATER_THAN_2A8B
cmplwi r6, 0x2B00
blt NOT_VALID_ITEM
cmplwi r6, 0x2B0F
bgt GREATER_THAN_2B0F
addi r3, 0x7F60
subis r7, r6, 0x2B00
b GET_OFFSET
::GREATER_THAN_2B0F
cmplwi r6, 0x2C00
blt NOT_VALID_ITEM
cmplwi r6, 0x2C5F
bgt GREATER_THAN_2C5F
addi r3, 0x7000
addi r3, 0x1060
subis r7, r6, 0x2C00
b GET_OFFSET
::GREATER_THAN_2C5F
cmplwi r6, 0x2D00
blt NOT_VALID_ITEM
cmplwi r6, 0x2D34
bgt GREATER_THAN_2D34
addi r3, 0x7000
addi r3, 0x1660
subis r7, r6, 0x2D00
b GET_OFFSET
::GREATER_THAN_2D34
cmplwi r6, 0x2E00
blt NOT_VALID_ITEM
cmplwi r6, 0x2E01
bgt GREATER_THAN_2E01
addi r3, 0x7000
addi r3, 0x19B0
subis r7, r6, 0x2E00
b GET_OFFSET
::GREATER_THAN_2E01
cmplwi r6, 0x2F00
blt NOT_VALID_ITEM
cmplwi r6, 0x2F03
bgt GREATER_THAN_2F03
addi r3, 0x7000
addi r3, 0x19D0
subis r7, r6, 0x2F00
b GET_OFFSET
::GREATER_THAN_2F03
cmplwi r6, 0x3000
blt NOT_VALID_ITEM
cmplwi r6, 0x345C
bgt NOT_VALID_ITEM
addi r3, 0x7000
addi r3, 0x1A10
subis r7, r6, 0x3000
b GET_FURNITURE_OFFSET
::NOT_VALID_ITEM
li r7, 0x9B80
b SET_OFFSET
::GET_FURNITURE_OFFSET
srawi r7, r7, 2 // this "divides" our index by four (not really but its the same effect)
::GET_OFFSET
mulli r7, r7, 0x0010 // multiply our value by 16 to get the offset the beginning of the file to the name
::SET_OFFSET
add r3, r3, r7 // add the offset from the start of the section (this should be the pointer to the start of the file at this point)
::COPY_NAME
rlwinm r4, r9, 0, 0, 26 // clear the buffer's lower 5 bits to align it and pass it as an argument
// TODO: find a way to memcopy the string 8 bytes up to get it in the correct position (if the string is on a non-32 byte aligned number, also account for that.)
addi r4, r4, 0x20 // add 20 bytes to it to make sure we're not overwriting important memory
li r5, 0x0020 // load 32 for the size argument
lis r8, 0x8000
addi r8, 729C(r8)
mtctr, r8
bctr
lwz r0, 0x24(r1)
mtlr r0
addi r1, r1, 0x0020
blr
There are a couple problems with the code above. Firstly, it went over the 104 instructions that the routine we were replacing had, secondly I didn’t realize that GetResourceAram required that the ARAM address and the address to copy to be aligned to 32 bytes. Ultimately this attempt failed and I went back to the drawing board.
The Final Version
Next, I came up with the idea of using a loop to figure out the index based on a separate, smaller file also in forest_2nd.arc. This file has a structure containing three shorts in a row: the beginning of the item type’s item index, the end of the item type’s index, and the offset into the name file where the first item of that type can be found. After quite a bit of work, I finished a revamped version of the routine:
stwu r1, -0xD0(r1)
mflr r0
stw r0, 0xD4(r1)
lis r20, 0x817F
addi r20, 0xCF00(r20)
mr r6, r4
mr r9, r3
bl GET_PC
:: GET_PC
mflr r0
li r7, 0x7FFC
add r7, r7, r0
lhz r8, 0x728A(r7)
rlwinm r8, r8, 16, 0, 31
lhz r5, 0x7292(r7)
add r5, r5, r8
lwz r17, (0)r5
lis r8, 0x8000
addi r8, 729C(r8)
cmplwi r6, 0x1000
stw r6, 0x1C(r1)
stw r8, 0x20(r1)
stw r9, 0x24(r1)
addi r11, r1, 0xD0
lis r7, 0x800C
addi r7, 0x10CC
mtctr r7
bctr
blt NOT_VALID_ITEM
mr r4, r20
mr r3, r17
subis r3, r3, 0x000F
addi r3, r3, 0x2920
li r5, 0x0080
mtctr, r8
bctr
addi r11, r1, 0xD0
lis r7, 0x800C
addi r7, 0x1118
mtctr r7
bctr
lwz r6, 0x1C(r1)
lwz r8, 0x20(r1)
lwz r9, 0x24(r1)
li r7, 0
::INDEX_LOOP_START
add r18, r7, r3
lhz r10, 0(r18)
lhz r14, 2(r18)
lhz r15, 4(r18)
lhz r16, 6(r18)
cmplwi r10, 0xFFFF
beq NOT_VALID_ITEM
cmplw r6, r14
ble INDEX_LOOP_END
cmplw r6, r16
blt NOT_VALID_ITEM
addi r7, r7, 6
b INDEX_LOOP_START
::INDEX_LOOP_END
subf r14, r10, r6
cmplwi r6, 0x2000
blt GET_FURNITURE_INDEX
cmplwi r10, 0x2FFF
bgt GET_FURNITURE_INDEX
b GET_INDEX
::NOT_VALID_ITEM
li r15, 0
li r14, 0x9B80
b SET_OFFSET
::GET_FURNITURE_INDEX
srawi r14, r14, 2
::GET_INDEX
mulli r14, r14, 0x0010
::SET_OFFSET
mr r3, r17
subis r3, r3, 0x000F
addi r3, r3, 0x29A0
add r3, r3, r15
add r3, r3, r14
rlwinm r7, r3, 0, 27, 31
rlwinm r3, r3, 0, 0, 26
mr r4, r20
stw r7, 0x28(r1)
::COPY_NAME
li r5, 0x0020
mtctr, r8
bctr
lwz r6, 0x1C(r1)
lwz r8, 0x20(r1)
lwz r9, 0x24(r1)
lwz r7, 0x28(r1)
mr r4, r20
mr r3, r9
cmplwi r7, 0
beq MEMCOPY
addi r4, 0x0010(r4)
::MEMCOPY
li r5, 0x0010
bl 0x5C64
addi r11, r1, 0xD0
lis r7, 0x800C
addi r7, 0x1118
mtctr r7
bctr
lwz r0, 0xD4(r1)
mtlr r0
addi r1, r1, 0x00D0
blr
The result? It worked! Although I now had to adjust all the functions that had 10 set as the character size to 16. This introduced a few bugs due to it overwritting memory needed. Two of these that are known are: The bell sound from Nook when buying/selling bugs out, and the music list is glitched out.
Here’s a few screenshots of the translated names:
Conclusion
Overall, I’m satisfied with the results. It was many hours of work, as I had to learn PowerPC Assembly and learn how to turn it into it’s hex counterpart, but it works! I believe it will be a breeze from here on out.