Level Script Engine Commands & Info « 1 2
Users browsing this thread: 1 Guest(s)

Nice finds, especially the dialog. I always thought it sucked not to have it for all levels
Gone for a while.

While looking into what it would take to relocate the general memory pool (0x8005C000-0x801C1000) to expansion RAM, I noticed something odd with Level script 0x16 that caused the game to crash. From shygoo's notes:

0x16 (8037EC54): Load raw data from ROM to absolute RAM address
16 10 00 00 [XX XX XX XX] [YY YY YY YY] [ZZ ZZ ZZ ZZ]
X = RAM address
Y = ROM address start
Z = ROM address end
Call 0x802786F0 (A0 = X, A1 = Y, A2 = Z)

This is 100% correct. However, the function it calls (FixedCopy/802786F0) makes a large assumption that the destination RAM address is within the memory pool range. SM64 only uses this level script for copying the main menu asm code to RAM. I spent some time decompiling the FixedCopy routine, which you can find below. There are two problems with the code for general use: 1. it calls _pool_alloc() and 2. it computes the length to copy from pool_tail - ram instead of rom_end - rom_start. Without some modifications, this makes it practically unusable for anything but what SM64 already uses it for.

I'm also attaching an updated decompiled level scripts C file which includes some more level scripts and all the _pool_alloc(), _pool_free() and related data structures.

pool_t *pool_tail; // 0x8033B490
u32 FixedCopy(u32 ram, u32 rom_start, u32 rom_end) // begin 802786F0/0336F0
  (void)ram;        // 0x28($sp)
  (void)rom_start;  // 0x2c($sp)
  (void)rom_end;    // 0x30($sp)
  u32 ret_addr = 0; // 0x24($sp)
  u32 len;          // 0x20($sp)
  u32 pool_len;     // 0x1c($sp)

  len = ((rom_end - rom_start) + 0xF) & 0xFFFFFFF0);
  pool_len = (pool_tail - ram) + 0xF) & 0xFFFFFFF0);

  if (pool_len >= len) {
     ret_addr = _pool_alloc(pool_len, 1);
     if (ret_addr != NULL) {
        bzero(ret_addr, pool_len);
        DmaCopy(ret_addr, rom_start, rom_end);
        osInvalCache(ret_addr, pool_len);
        osInvalDCache(ret_addr, pool_len);

  return ret_addr;

Attached Files
Size: 20.64 KB / Downloads: 45 .c   sm64_level_scripts_decompiled.c

For what it's worth, here's that decoder I said I would release a millennium ago
(This post was last modified: 05-02-2017, 01:49 AM by shygoo.)

Attached Files
Size: 36.9 KB / Downloads: 34 .zip   level-scripts-decoded.zip

Today I learned that you can actually load data directly from the ROM cartridge and put it into a register. I had always just assumed that you needed to use the DMACopy function if you wanted to use any data that is stored on the cartridge. This means that we can have jump tables inside the ROM file itself and not have to worry about where to put them in RAM. This makes it trivial to add in new custom level script commands into the game. 

I've written some example code for creating a new custom levelscript command. This simple code does 3 main things:
  1. Modifies function 0x803805C8 to read from a jump table at 0xB07E0000 instead of 0x8038B8B8.
  2. I overwrote the unused function 0x8024B940 to be the code for my new levelscript command.
  3. I copied the table data from ROM address 0x108638 to 0x7E0000, and then added a new entry at the end of the list. I chose 0x7E0000 since that region is not being used by the original 8MB ROM or the extended ROM.
The new levelscript command I created doesn't do anything important. My new 4-byte levelscript 0x3D command takes the value located at 0x80370000 and adds it with the signed halfword in the 0x3D command. Basically, [ 3D 04 01 00 ] is the same as: (*80370000) += 0x100. The region around 0x80370000 is empty and not being used by anything, so it will have no effect in-game.

Spoiler: Source code
// Assemble with Armips v0.9

// This function is modified to get the function pointer for a command from the table at 0xB07E0000.
.orga 0x0FD348
LevelScriptLoad: // begin 803805C8 (0FD348)
addiu sp, sp, -0x18
sw ra, 0x14(sp)
sw a0, 0x18(sp)
addiu t6, r0, 0x1
lui at, 0x8039
sh t6, -0x41e0(at)
lw t7, 0x18(sp)
lui at, 0x8039
sw t7, -0x41d8(at)
lh t8, 0x8038be20 ; lui t8, 0x8039/lh t8, -0x41e0t8
addiu at, r0, 0x1
bne t8, at, LevelScriptLoad_70
LevelScriptLoad_38: ; 80380600
lw t9, 0x8038be28 ; lui t9, 0x8039/lw t9, -0x41d8t9
lbu t0, 0x00(t9)
lui t9, 0xB07E
sll t1, t0, 0x2
addu t9, t9, t1
lw t9, 0x00(t9) ; Get address from new jump table at 0xB07E0000 (ROM address 0x7E0000)
jalr t9
lh t2, 0x8038be20 ; lui t2, 0x8039/lh t2, -0x41e0t2
addiu at, r0, 0x1
beq t2, at, LevelScriptLoad_38
LevelScriptLoad_70: ; 80380638
jal 0x8027E3E0
addiu a0, r0, 0x1
jal 0x80247CCC
jal 0x8027B3B4
jal 0x80247D14 ; CleanupDisplayList
jal 0x80278F2C
move a0, r0
lui v0, 0x8039
b LevelScriptLoad_AC
lw v0, -0x41d8(v0)
b LevelScriptLoad_AC
LevelScriptLoad_AC: ; 80380674
lw ra, 0x14(sp)
jr ra
addiu sp, sp, 0x18

// New levelscript command 0x3D
.orga 0x6940 ; Overwrite the unused function 0x8024B940
.area 0x64 ; Set data import limit to 0x64 bytes
addiu sp, sp, -0x18
sw ra, 0x14(sp)
lw t0, 0x8038be28 ; lui t0, 0x8039/lw t0, -0x41d8t0
lh t1, 0x02(t0)
lui t2, 0x8037
lw t3, 0x00(t2)
add t3, t3, t1
sw t3, 0x00(t2)
LevelScript3D_increment: ; You NEED to include this increment code in every levelscript command
lui at, 0x8039
lbu t8, 0x1(t0)
addu t9, t0, t8
sw t9, -0x41d8(at)
lw ra, 0x14(sp)
jr ra
addiu sp, sp, 0x18

// new levelscript jump table, original is at ROM address 0x108638
.orga 0x007E0000
.word 0x8037E2C4
.word 0x8037E388
.word 0x8037E404
.word 0x8037E47C
.word 0x8037E4FC
.word 0x8037E580
.word 0x8037E5B8
.word 0x8037E620
.word 0x8037E650
.word 0x8037E6D4
.word 0x8037E780
.word 0x8037E7F8
.word 0x8037E878
.word 0x8037E8E8
.word 0x8037E988
.word 0x8037EA18
.word 0x8037EA70
.word 0x8037EA98
.word 0x8037EB04
.word 0x8037EB98
.word 0x8037EBD4
.word 0x8037EC14
.word 0x8037EC54
.word 0x8037ECA4
.word 0x8037ECF8
.word 0x8037ED48
.word 0x8037EDF8
.word 0x8037EE48
.word 0x8037EEA8
.word 0x8037EF00
.word 0x8037EF70
.word 0x8037F010
.word 0x8037F130
.word 0x8037F164
.word 0x8037F214
.word 0x8037F2A4
.word 0x8037F45C
.word 0x8037F36C
.word 0x8037F67C
.word 0x8037F994
.word 0x8037F790
.word 0x80380014
.word 0x8038007C
.word 0x803800BC
.word 0x80380160
.word 0x803801A0
.word 0x8037FE94
.word 0x8037FF14
.word 0x80380274
.word 0x8037F920
.word 0x8038024C
.word 0x803801E0
.word 0x8037FDE4
.word 0x8037FE2C
.word 0x80380300
.word 0x8038039C
.word 0x803803EC
.word 0x8037FF94
.word 0x8037FB18
.word 0x8037FC38
.word 0x80380434
.word 0x8024B940 ; Custom levelscript command 0x3D
.word 0x00000000
(This post was last modified: 22-05-2017, 10:49 AM by David.)

Wow that's revolutional!
Gone for a while.

I've basically figured out what the commands 0x1E & 0x1C do. Nothing exciting though, sadly.

Command 0x1E is used to allocate data from the pool and reset a couple variables:
  • Reallocates memory for the geometry layout node system.
  • Allocates 0xDAC0 bytes and stores pointer to 0x8038EE98. Not sure what this is, but it is related to collision.
  • Allocates memory for collision data. Vanilla SM64 allocates enough space for only 2,300 collision triangles. Newer versions of Skelux's SM64 editor allocate enough space for 32,767 triangles.
  • Sets halfword at address 0x8036125C to 0. (0x8036125C seems to be just a flag that is set when Mario enters into area #2 of any level)
  • Sets byte at address 0x803613FE to 0. (0x803613FE seems to be a read-only byte that tells you how many red coins Mario has collected so far)
Decompiled C notes of cmd 0x1E: https://pastebin.com/raw/XkCfkFyT

0x1E should come before the last 0x11 command that is above the main level loop (command 0x12)

Command 0x1C is used for level & memory cleanup:
  • Frees data in pool allocated from level script
  • Cleanups level related stuff (level objects, pointers, etc)
  • Cleanups geometry layout data
  • Cleanups all 8 area structs
Decompiled C notes of cmd 0x1C: https://pastebin.com/raw/GjDsJYqq

0x1C should come after the main level loop (command 0x12)
(This post was last modified: 03-12-2017, 03:05 AM by David.)

Level Script Engine Commands & Info « 1 2
Users browsing this thread: 1 Guest(s)

  EN ・日本語