[ASM] Writing a custom switch function for the 0x0E Geo Layout cmd
Users browsing this thread: 1 Guest(s)

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.

[Image: 6HTrHUq.png]

You will need the following: * 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.
Spoiler: ASM Code
.include "SM64Functions.txt"

// Re-organize Mario's behavior
.org 0x21CCC0
hex
{
00 00 00 00
10 05 00 00
11 01 01 00
11 03 00 01
23 00 00 00 00 25 00 A0
08 00 00 00
0C 00 00 00 80 29 CA 58
0C 00 00 00 80 2C B2 64
0C 00 00 00 80 3F 00 00
09 00 00 00
}


// Overwriting part of the top levelscript.
.org 0x108A18
hex
{
11 08 00 00 80 2C B1 C0
}


// Function that will copy data from the ROM to the RAM
.org 0x861C0
ADDIU SP, SP, $FFE8
SW RA, $0014 (SP)
SW A0, $0010 (SP)
// lvl cmd 0x11 safeguard

// Do not remove these following 4 instructions
JAL @osViBlack
// 0x80323340
MOV A0, R0
LUI T0, 0x8039
SW R0, 0xBE24(T0)
// Set level accumulator to 0

LUI T0, 0x803F
ORI A0, T0, 0x0000
// Writing to RAM address 0x803F0000
LUI T1, 0x011F
ORI A1, T1, 0x0000
// Start copying from ROM address at 0x11F0000
JAL @DmaCopy
// 0x80278504
ORI A2, T1, 0x1000
// Stop copying from ROM address at 0x11F1000

LW V0, $0010 (SP)
// lvl cmd 0x11 safeguard
LW RA, $0014 (SP)
JR RA
ADDIU SP, SP, $0018

.org 0x11F0000
// Custom Mario loop function (0x803F0000)
ADDIU SP, SP, 0xFFE8
SW RA, 0x14 (SP)

LUI A0, 0x8034
LW T0, 0xAFA0(A0)
ANDI AT, T0, 0x20
BEQZ AT, 803F0000_END
// Check L button, and branch if false
NOP

LB T1, 0xFFFF(A0)
// Load byte at 0x8033FFFFF
XORI T1, T1, 0x01
// Switch byte (either 0 or 1)
SB T1, 0xFFFF(A0)
// Store new value

803F0000_END:
LW RA, 0x14 (SP)
JR RA
ADDIU SP, SP, 0x18

.org 0x11F0800
// Custom switch function (0x803F0800)
ADDIU SP, SP, 0xFFE8
SW RA, 0x14 (SP)
SW A2, 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
LUI T0, 0x8034
LB T0, 0xFFFF(T0)
SH T0, 0x1E(A1)
// Store our byte at 0x8033FFFFF as the switch value.

MOV V0, R0
// put return value at zero.
LW RA, 0x14 (SP)
JR RA
ADDIU SP, 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.
*/

.org 0x12A7AC
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.

[Image: PgOWVyg.png]
[Image: 1mg5UUe.png]

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

// Re-organize Mario's behavior
.org 0x21CCC0 
hex

00 00 00 00 
10 05 00 00 
11 01 01 00 
11 03 00 01 
23 00 00 00 00 25 00 A0 
08 00 00 00 
0C 00 00 00 80 29 CA 58 
0C 00 00 00 80 2C B2 64 
0C 00 00 00 80 3F 00 00 
09 00 00 00 
}


// Overwriting part of the top levelscript.
.org 0x108A18
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
.org 0x861C0
ADDIU SP, SP, $FFE8
SW RA, $0014 (SP)
SW A0, $0010 (SP)
// lvl cmd 0x11 safeguard

// Do not remove these following 4 instructions
JAL @osViBlack
// 0x80323340
MOV A0, R0
LUI T0, 0x8039
SW R0, 0xBE24(T0)
// Set level accumulator to 0

LUI T0, 0x803F
ORI A0, T0, 0x0000
// Writing to RAM address 0x803F0000
LUI T1, 0x011F
ORI A1, T1, 0x0000
// Start copying from ROM address at 0x11F0000
JAL @DmaCopy
// 0x80278504
ORI A2, T1, 0x1000
// Stop copying from ROM address at 0x11F1000

LW V0, $0010 (SP)
// lvl cmd 0x11 safeguard
LW RA, $0014 (SP)
JR RA
ADDIU SP, SP, $0018 

Checking for input

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.

.org 0x11F0000 // Custom Mario loop function (0x803F0000)
ADDIU SP, SP, 0xFFE8
SW RA, 0x14 (SP)

LUI A0, 0x8034
LW T0, 0xAFA0(A0)
ANDI AT, T0, 0x20
BEQZ AT, 803F0000_END
// Check L button, and branch if false
NOP

LB T1, 0xFFFF(A0)
// Load byte at 0x8033FFFFF
XORI T1, T1, 0x01
// Switch byte (either 0 or 1)
SB T1, 0xFFFF(A0)
// Store new value

803F0000_END:
LW RA, 0x14 (SP)
JR RA
ADDIU SP, 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.

.org 0x11F0800 // Custom switch function (0x803F0800)
ADDIU SP, SP, 0xFFE8
SW RA, 0x14 (SP)
SW A2, 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
LUI T0, 0x8034
LB T0, 0xFFFF(T0)
SH T0, 0x1E(A1)
// Store our byte at 0x8033FFFFF as the switch value.

MOV V0, R0
// put return value as zero.
LW RA, 0x14 (SP)
JR RA
ADDIU SP, 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.
*/

.org 0x12A7AC
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 )


Attached Files
Size: 2.2 KB / Downloads: 61 .txt   SwitchTest.txt


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

arch n64.cpu
endian msb
output "sm64.lowpoly.z64", create

include "n64.inc"

// definitions
constant Controller1(0x8033AFA0)

origin 0x0
insert "sm64.u.z64"

// 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
Users browsing this thread: 1 Guest(s)