Every object in Super Mario 64 is driven by a behavior script: a statically compiled array ofDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/n64decomp/sm64/llms.txt
Use this file to discover all available pages before exploring further.
uintptr_t values (type-aliased as BehaviorScript) that acts as a compact bytecode program. The script engine interprets this array one command at a time, advancing a per-object program counter (curBhvCommand) each frame. Behaviors mix declarative setup commands (set a field, load a model, configure physics) with control-flow commands (loops, subroutine calls) and CALL_NATIVE dispatches that hand off to ordinary C functions for complex logic.
BehaviorScript type
const BehaviorScript bhvFoo[] = { ... };. On 32-bit N64 hardware each element is 4 bytes. On 64-bit host builds the size doubles, which is why the project has separate rawData and ptrData unions in struct Object.
Command encoding
Every command word packs a 1-byte opcode into the most-significant byte, with the remaining 3 bytes carrying arguments:CALL_NATIVE is 2 words: opcode word + function pointer word).
Command reference
Control flow commands
Control flow commands
| Macro | Opcode | Description |
|---|---|---|
BEGIN(objList) | 0x00 | Opens the script; registers the object into the given ObjectList. |
DELAY(n) | 0x01 | Pauses the script for n frames by returning BHV_PROC_BREAK until bhvDelayTimer reaches 0. |
CALL(addr) | 0x02 | Pushes the next command address onto bhvStack and jumps to addr. |
RETURN() | 0x03 | Pops the top of bhvStack and resumes from that address. |
GOTO(addr) | 0x04 | Unconditional jump; does not save a return address. |
BEGIN_REPEAT(count) | 0x05 | Begins a loop that repeats count times. |
END_REPEAT() | 0x06 | Ends the counted loop; returns BHV_PROC_BREAK after the last iteration. |
END_REPEAT_CONTINUE() | 0x07 | Like END_REPEAT but continues executing after the loop on the same frame. |
BEGIN_LOOP() | 0x08 | Marks the start of an infinite loop. |
END_LOOP() | 0x09 | Jumps back to the matching BEGIN_LOOP. |
BREAK() | 0x0A | Exits the script for this frame (BHV_PROC_BREAK). |
DEACTIVATE() | 0x1D | Sets activeFlags = 0; object is removed. |
Native function call
Native function call
CALL_NATIVE is a 2-word command. Opcode 0x0C tells the engine to read the following word as a function pointer and call it with no arguments. The return value is ignored; the function interacts with the object through gCurrentObject. This is how all actual game logic (movement, AI, collision response) is invoked from scripts.Field manipulation commands
Field manipulation commands
These commands read or write a
Example usage — setting
rawData slot by index, eliminating the need for a separate init C function for simple assignments:| Macro | Opcode | Effect |
|---|---|---|
SET_FLOAT(field, value) | 0x0E | rawData.asF32[field] = value |
ADD_FLOAT(field, value) | 0x0D | rawData.asF32[field] += value |
SET_INT(field, value) | 0x10 | rawData.asS32[field] = value |
ADD_INT(field, value) | 0x0F | rawData.asS32[field] += value |
OR_INT(field, flags) | 0x11 | rawData.asS32[field] |= flags |
oFlags at the start of most scripts:Object setup commands
Object setup commands
| Macro | Opcode | Description |
|---|---|---|
SET_MODEL(modelID) | 0x1B | Assigns gLoadedGraphNodes[modelID] to sharedChild. |
BILLBOARD() | 0x21 | Sets GRAPH_RENDER_BILLBOARD; object always faces the camera. |
DISABLE_RENDERING() | 0x35 | Clears GRAPH_RENDER_ACTIVE. |
HIDE() | 0x22 | Hides the object without deactivating it. |
SET_HOME() | 0x2D | Copies current position into oHomeX/Y/Z. |
DROP_TO_FLOOR() | 0x1E | Snaps oPosY to the floor triangle below. |
SCALE(unused, percent) | 0x32 | Scales the object uniformly. |
LOAD_ANIMATIONS(field, anims) | 0x27 | Sets oAnimations pointer. |
ANIMATE(index) | 0x28 | Starts animation at the given index. |
LOAD_COLLISION_DATA(ptr) | 0x2A | Sets collisionData. |
Hitbox and physics commands
Hitbox and physics commands
Spawning commands
Spawning commands
| Macro | Opcode | Description |
|---|---|---|
SPAWN_CHILD(model, behavior) | 0x1C | Spawns a child at the parent’s position/angle. |
SPAWN_CHILD_WITH_PARAM(param, model, behavior) | 0x29 | Like SPAWN_CHILD but also sets oBhvParams2ndByte. |
SPAWN_OBJ(model, behavior) | 0x2C | Spawns an object and stores it in prevObj. |
The bhvStack: subroutine calls
Each object carries an 8-entry call stack:CALL command pushes the address of the word after the call onto the stack, then jumps to the target. RETURN pops the saved address and resumes from there. This lets shared initialization sequences be written once and called from multiple behavior scripts.
Real example: bhvYellowCoin
The yellow coin is one of the simplest complete behaviors in the game. Here is the full script fromdata/behavior_data.c, with each command explained:
src/game/behaviors/coin.inc.c):
bhvOneCoin script shows how GOTO redirects into the middle of another behavior to share its body:
How behaviors are assigned to objects
Thebehavior pointer in struct Object (at offset 0x20C) holds the address of the object’s behavior script array:
create_object(bhvScript) is called, it:
- Allocates a slot from
gFreeObjectList. - Stores
bhvScriptinobj->behavior. - Sets
obj->curBhvCommand = segmented_to_virtual(bhvScript)— this is the program counter. - Processes the
BEGINcommand immediately to determine which object list to join.
cur_obj_update() resumes from curBhvCommand and advances it through commands until a BREAK, DELAY, or loop-back is hit. The object’s position in the script thus persists across frames — the program counter is simply a pointer into the statically allocated array.
GOTO is a raw jump: it replaces gCurBhvCommand with no return address. It is typically used to share the body of one behavior with a variant that only differs in initial setup (like bhvOneCoin → bhvYellowCoin).Execution flow per frame
BHV_PROC_CONTINUE (0) lets the engine execute the next command in the same frame. BHV_PROC_BREAK (1) stops and saves gCurBhvCommand back into obj->curBhvCommand for the next frame. DELAY returns BHV_PROC_BREAK every frame until the timer expires, effectively pausing the script.