In this post I will show you how to write an ASM function that will let you control the geo layout switch command (0x0E). In this example, you will be able to switch between the high-poly and low-poly Mario models by pressing the L button.
* You don't need this file to run the code, but I find it to be more convenient as it allows you to write function names instead of addresses. I'll write out the address in a comment to the side for those who don't want to use it.
Here is the full ASM code in its entirety. I will be explaining what each section does throughout this post.
// Overwriting part of the top levelscript. .org0x108A18 hex{
11 08 00 00 80 2C B1 C0
}
// Function that will copy data from the ROM to the RAM .org0x861C0 ADDIUSP, SP, $FFE8 SWRA, $0014(SP) SWA0, $0010(SP)// lvl cmd 0x11 safeguard
// Do not remove these following 4 instructions JAL@osViBlack // 0x80323340 MOVA0, R0 LUIT0, 0x8039 SWR0, 0xBE24(T0)// Set level accumulator to 0
LUIT0, 0x803F ORIA0, T0, 0x0000// Writing to RAM address 0x803F0000 LUIT1, 0x011F ORIA1, T1, 0x0000// Start copying from ROM address at 0x11F0000 JAL@DmaCopy // 0x80278504 ORIA2, T1, 0x1000// Stop copying from ROM address at 0x11F1000
.org0x11F0800// Custom switch function (0x803F0800) ADDIUSP, SP, 0xFFE8 SWRA, 0x14(SP) SWA2, 0x20(SP)// You want to store A2 0x8 bytes above the stack pointer.
//A1 = pointer to geo node. You must store the switch value to offset 0x1E LUIT0, 0x8034 LBT0, 0xFFFF(T0) SHT0, 0x1E(A1)// Store our byte at 0x8033FFFFF as the switch value.
MOVV0, R0// put return value at zero. LWRA, 0x14(SP) JRRA ADDIUSP, SP, 0x18
/*
Note: You cannot add any more bytes here without breaking Mario's geo layout.
If you want more than 2 options, then you will need to move some of the geo layout code elsewhere.
*/ .org0x12A7AC hex{
0E 00 00 00 80 3F 08 00
04 00 00 00
02 01 00 00 17 00 2C E0
02 01 00 00 17 00 2D 48
05 00 00 00
}
Hooking the code (Top level-script method)
Before we can get any of our code to run, we must first get the data from the ROM cartridge into the N64's memory (RAM). To do this we must use a function called @DmaCopy, which transfers data from ROM to RAM (convenient right?) . What most ROM hackers do is take the useless debug function 0x802CB1C0 in Mario's behavior loop and rewrite it to call the DmaCopy function. However the problem with this method is that it will only run once Mario is active, which means you could not get data into memory before that.
So I took that old method and made an addition to it. Basically, were call that 0x802CB1C0 function from the top-most levelscript and have a DmaCopy function call even before the title screen shows up. The 0x11 level script cmd is used to call an ASM function and takes up 8 bytes of data, so we need to remove 8 bytes from that top-level script to make room for our function call. There happens to be just a place at the ROM address 0x108A18. We can remove the 0x34 & 0x13 cmds that were there and convert them into ASM code.
We now overwrite those bytes with the 0x11 CMD and reorganize Mario's behavior so that it won't call the 0x802CB1C0 function anymore. Instead, we want it to call a custom function that will be at 0x803F0000
// Overwriting part of the top levelscript. .org0x108A18 hex{
11 08 00 00 80 2C B1 C0
} Loading our functions into memory
Now the 0x11 cmd is used to modify the level accumulator value (0x8038BE24), so it expects a return value V0. What we can do is place a couple safeguards to guarantee that the function will work without any problems. // Function that will copy data from the ROM to the RAM .org0x861C0 ADDIUSP, SP, $FFE8 SWRA, $0014(SP) SWA0, $0010(SP)// lvl cmd 0x11 safeguard
// Do not remove these following 4 instructions JAL@osViBlack // 0x80323340 MOVA0, R0 LUIT0, 0x8039 SWR0, 0xBE24(T0)// Set level accumulator to 0
LUIT0, 0x803F ORIA0, T0, 0x0000// Writing to RAM address 0x803F0000 LUIT1, 0x011F ORIA1, T1, 0x0000// Start copying from ROM address at 0x11F0000 JAL@DmaCopy // 0x80278504 ORIA2, T1, 0x1000// Stop copying from ROM address at 0x11F1000
Now that we have our code within memory, we can now write our custom functions. First is the 0x803F00000 function that will check when the player presses the L Button, and toggle the byte at 0x8033FFFF (which is normally empty) to either 0 (high poly Mario) or 1 (low poly Mario).
The button input from controller 1 is located at the address 0x8033AFA0 (u16). To single out an input, we must use the ANDI op code with a certain flag value. Here is a table of the button flags
Flag
Button
0x0001
C-Right
0x0002
C-Left
0x0004
C-Down
0x0008
C-Up
0x0010
R
0x0020
L
0x0100
D-Pad Right
0x0200
D-Pad Left
0x0400
D-Pad Down
0x0800
D-Pad Up
0x1000
Start
0x2000
Z
0x4000
B
0x8000
A
As you can see on the table, we are going to use the flag 0x20 to check for the L button. If the player did press down the L button, then we will swap the byte at 0x8033FFFF using the XORI opcode.
.org0x11F0000// Custom Mario loop function (0x803F0000) ADDIUSP, SP, 0xFFE8 SWRA, 0x14(SP)
LUIA0, 0x8034 LWT0, 0xAFA0(A0) ANDI AT, T0, 0x20 BEQZ AT, 803F0000_END // Check L button, and branch if false NOP
LBT1, 0xFFFF(A0)// Load byte at 0x8033FFFFF XORIT1, T1, 0x01// Switch byte (either 0 or 1) SBT1, 0xFFFF(A0)// Store new value
803F0000_END: LWRA, 0x14(SP) JRRA ADDIUSP, SP, 0x18 Little important note:
There is a subtle difference to loading 0x8033AFA0 as a word vs a half-word. If you use "LH T0, 0xAFA0", then the game will running the swap code over and over until you let go of the button. But if we instead load it as a full word, then it will only run once until we let go of the button. This happens because your previous button press is stored at 0x8033AFA2, which is right next to the current button press. When you load 0x8033AFA0 as a word, your really checking the previous button press instead of the current one which causes the code to run only one time.
The switch function
The Geo Layout command 0x0E is used to change the look of a model by checking a value with an ASM function. Our custom ASM function 0x803F0800 will look at the byte we set at 0x8033FFFF to determine if Mario will use his high-poly model(0) or his low-poly model(1). Our function is simple because all we have to do is store that byte to the switch value. The argument register A1 contains a pointer to the graph node that contains the switch value at the offset 0x1E.
.org0x11F0800// Custom switch function (0x803F0800) ADDIUSP, SP, 0xFFE8 SWRA, 0x14(SP) SWA2, 0x20(SP)// You want to store A2 0x8 bytes above the stack pointer.
//A1 = pointer to node. You must store the switch value to offset 0x1E LUIT0, 0x8034 LBT0, 0xFFFF(T0) SHT0, 0x1E(A1)// Store our byte at 0x8033FFFFF as the switch value.
MOVV0, R0// put return value as zero. LWRA, 0x14(SP) JRRA ADDIUSP, SP, 0x18
Now that we have our function ready, the last thing we need to do is to modify part of Mario's geo layout. The high-poly model is stored at the segmented address 0x17002CE0, while the low-poly model is stored at 0x17002D48.
/*
Note: You cannot add any more bytes here without breaking Mario's geo layout.
If you want more than 2 options, then you will need to move some of the geo layout code elsewhere.
*/ .org0x12A7AC hex{
0E 00 00 00 80 3F 08 00
04 00 00 00
02 01 00 00 17 00 2C E0
02 01 00 00 17 00 2D 48
05 00 00 00
}
Now assemble the code and you should be able to freely switch models whenever you want to. If you have any questions, please leave a reply below and I'll try to answer it the best I can.
(This post was last modified: 23-06-2017, 08:03 PM by queueRAM.
Edit Reason: wiki links
)
This is a very nice and thorough tutorial covering SM64 level scripts, geometry layout, behaviors, DMA, and controller input. Perhaps this goes against some of the purpose of the tutorial, but I played around with your code a bit and reduced it down to just a geo layout 0x0E change.
Instead of creating new code in the ROM and DMAing it to RAM in order to call, I overwrote an unused debug function 802CB394/086394. The controller L-button status is checked inside of the 0x0E geo layout assembly routine and this toggles the graph node 0x1E offset directly. This also avoids using 0x803F0000 which is used by some hardware, allowing this to run in cen64 and on console.
Code:
// SM64 (U) Low poly Mario toggle example using bass assembler
// Based off of David's ASM example: http://origami64.net/showthread.php?tid=408
// Overwrite Unknown802CB394/802CB394 which is used for debug
origin 0x086394
base 0x802CB394
GeoSwitchCaseMario: {
// a1 = pointer to graph node. You must store the switch value to offset 0x1E
la at, Controller1
lw t0, 0(at)
andi at, t0, 0x20
beqz at, GeoSwitchCaseMario_End
nop
// toggle 0x1E offset of graph node between 0 and 1
lh t0, 0x1E(a1)
xori t0, t0, 0x1
sh t0, 0x1E(a1)
GeoSwitchCaseMario_End:
jr ra
ori v0, r0, 0 // return 0
}
// ensure code doesn't spill into BehMarioLoop3/802CB264
if pc() > 0x802CB564 {
error "code > 0x802CB564"
}
fill 0x802CB564 - pc() // fill rest of function with 0x00
// Note: You cannot add any more bytes here without breaking Mario's geo layout.
// If you want more than 2 options, then you will need to move some of the geo layout code elsewhere.
origin 0x12A7AC
dd 0x0E000000, GeoSwitchCaseMario
dd 0x04000000
dd 0x02010000, 0x17002CE0
dd 0x02010000, 0x17002D48
dd 0x05000000
Note, I used the bass assembler for this, so some changes need to be made to convert it to CajeASM.
[ASM] Writing a custom switch function for the 0x0E Geo Layout cmd