Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pmret/papermario/llms.txt

Use this file to discover all available pages before exploring further.

Every interactive element in Paper Mario’s world — doors, NPCs, cutscenes, shops — runs as an EVT script. You write scripts as const Bytecode[] arrays in C, but you never write raw opcodes by hand. The macro layer in include/script_api/macros.h gives you a readable, structured syntax that compiles down to the bytecode the VM expects. This page walks you through the core patterns so you can read existing scripts and contribute new ones.

Script structure

A script is declared as EvtScript (a typedef for Bytecode[]) and terminated with End. Without End, the interpreter will walk past the end of the array and almost certainly crash.
EvtScript N(EVS_MyScript) = {
    // ... instructions ...
    Return
    End
};
N() is a name-mangling macro used by the map overlay system to avoid symbol collisions between areas. Every script defined inside a map overlay should use it. Scripts in src/evt/ (shared scripts) do not. Every instruction macro places a trailing comma after its bytecode, so lines in a script body do not need extra separators. The C array initializer syntax treats the entire block as comma-separated values.

Script variables

EVT encodes different variable classes as large negative integers, allowing the interpreter to distinguish them from literal values at runtime. The macros in include/script_api/macros.h do this encoding for you.

Local variables

Local Words (LVar0LVarF, i.e. LocalVar(0)LocalVar(15)) are per-thread integer variables. They are copied into any child thread spawned by Exec, ExecWait, Thread, or ChildThread. ExecWait also copies them back when the child finishes.
Set(LVar0, 42)
Add(LVar0, 8)           // LVar0 is now 50
Set(LVar1, LVar0)       // copy LVar0 into LVar1
Local Flags (LFlag0LFlagF, i.e. LocalFlag(0)LocalFlag(15)) are boolean variants with the same lifetime and copy semantics.

Map variables and flags

Map Words (MapVar(0)MapVar(15)) are global across all threads in a map and are cleared when you leave. Map Flags (MapFlag(0)MapFlag(95)) are the boolean equivalent.

Persistent variables

MacroStorageDescription
GameFlag(n)Save filePersistent boolean, 0–2047. Used for story progress, item collection.
AreaFlag(n)Save file, area-scopedPersistent boolean, 0–255. Cleared when you move to a different world area. Used for respawnable coins, Goomnuts, etc.
GameByte(n)Save filePersistent integer. Used for almost all savefile state.
AreaByte(n)Save file, area-scopedPersistent integer, 0–15. Cleared with a new area.

Float literals

Floats in EVT are encoded as fixed-point values (1024 units = 1.0). Use the Float() macro when passing float literals and SetF / AddF / etc. when operating on float variables:
SetF(LVar0, Float(1.5))
AddF(LVar0, Float(0.25))
Call(TranslateModel, MODEL_gate, LVar0, Float(0.0), Float(0.0))

Array variables

Use MallocArray or UseArray to attach an s32 array to the thread, then access elements with ArrayVar(index):
MallocArray(8, LVar0)       // allocate 8-element array; LVar0 holds pointer
Set(ArrayVar(0), 100)
Set(ArrayVar(1), 200)

Control flow

Conditionals

IfEq(LVar0, 5)
    Call(DoSomething)
Else
    Call(DoSomethingElse)
EndIf
The Else block is optional. Comparisons available: IfEq, IfNe, IfLt, IfGt, IfLe, IfGe, IfFlag, IfNotFlag. The shorthand macros IfTrue(b) and IfFalse(b) expand to IfNe(b, 0) and IfEq(b, 0) respectively.

Switch statements

EVT switch statements do not fall through by default. Use CaseOrEq for intentional fallthrough.
Switch(LVar0)
    CaseEq(0)
        Call(HandleCase0)
    CaseEq(1)
        Call(HandleCase1)
    CaseRange(2, 5)
        Call(HandleCases2Through5)
    CaseDefault
        Call(HandleDefault)
EndSwitch

Loops

Loop(10)                    // repeat 10 times
    Call(DoWork)
    Wait(1)                 // block each iteration to avoid a freeze
EndLoop

Loop(0)                     // infinite loop
    Call(PollSomething)
    IfEq(LVar0, 1)
        BreakLoop
    EndIf
    Wait(1)
EndLoop
A loop that never blocks will freeze the game. Always include at least one Wait(1), ExecWait, or Call to a blocking function inside any loop body.

Labels and Goto

Use Label and Goto for simple jumps without the overhead of a loop construct:
Label(0)
    Call(UpdateLerp)
    Call(RotateGroup, MODEL_gate, LVar0, 0, -1, 0)
    Wait(1)
    IfEq(LVar1, 1)
        Goto(0)
    EndIf
Valid label IDs are 0–22.

Calling API functions

Call invokes any API_CALLABLE function and passes arguments after the function pointer. Arguments are resolved as EVT expressions, so you can pass literals, variable references (LVar0), float literals (Float(1.5)), and pointer constants (Ref(myArray)).
Call(SetNpcPos, NPC_Goomba, 100, 0, 200)
Call(DisablePlayerInput, true)
Call(SpeakToPlayer, NPC_PARTNER, ANIM_Goompa_Talk, ANIM_Goompa_Idle, 0, MSG_CH0_001C)
If Call targets a blocking function (one that returns ApiStatus_BLOCK), the script thread suspends until the function returns ApiStatus_DONE1 or ApiStatus_DONE2.

Inline thread blocks

You can fork a thread inline without a separate EvtScript array:
Thread
    Wait(30)
    Call(PlaySound, SOUND_GATE_CREAK)
EndThread
// parent continues immediately; the thread runs concurrently
ChildThread / EndChildThread creates a thread that is automatically killed when the parent dies:
ChildThread
    Loop(0)
        Call(UpdateSparkleEffect)
        Wait(1)
    EndLoop
EndChildThread
Return      // child thread dies here with the parent
End

Spawning and waiting on scripts

// fire-and-forget
Exec(N(EVS_SetupGate))

// fire-and-forget, keep the thread ID
ExecGetTID(N(EVS_Loop), LVar9)

// block until the script finishes; LVars are copied back
ExecWait(N(EVS_RunCutscene))

// kill a previously launched thread
KillThread(LVar9)

Binding triggers

BindTrigger registers a script to run when the player interacts with a collider or entity:
BindTrigger(Ref(N(EVS_OpenGate)), TRIGGER_WALL_PRESS_A, COLLIDER_gate, 1, 0)
BindTrigger(Ref(N(EVS_ExitWalk)), TRIGGER_FLOOR_ABOVE, COLLIDER_exit1, 1, 0)
Only one thread runs per trigger at a time. Use Unbind inside the triggered script to remove the binding after it fires.

A complete example

The following script is drawn from src/world/area_kmr/kmr_02/main.c. It opens a gate by animating a rotation lerp, then modifies collider flags so the player can pass through:
EvtScript N(EVS_OpenGoombaRoadGate) = {
    Call(DisablePlayerInput, true)
    Call(PlaySoundAtCollider, COLLIDER_tt2, SOUND_GOOMBA_GATE_OPEN, SOUND_SPACE_DEFAULT)
    Call(MakeLerp, 0, 120, 20, EASING_COS_IN_OUT)
    Label(0)
        Call(UpdateLerp)
        Call(RotateGroup, MODEL_g197, LVar0, 0, -1, 0)
        Call(RotateGroup, MODEL_g196, LVar0, 0, 1, 0)
        Wait(1)
        IfEq(LVar1, 1)
            Goto(0)
        EndIf
    Call(ModifyColliderFlags, MODIFY_COLLIDER_FLAGS_SET_BITS, COLLIDER_tt2, COLLIDER_FLAGS_UPPER_MASK)
    Call(ModifyColliderFlags, MODIFY_COLLIDER_FLAGS_CLEAR_BITS, COLLIDER_o757, COLLIDER_FLAGS_UPPER_MASK)
    Call(DisablePlayerInput, false)
    Return
    End
};
Key patterns here:
  • MakeLerp initializes a lerp; UpdateLerp advances it each frame, writing the current value into LVar0 and a “still running” flag into LVar1.
  • Wait(1) inside the label loop prevents a freeze.
  • IfEq(LVar1, 1) / Goto(0) loops back while the lerp is active.

Defining a local callable

You can define an API_CALLABLE function directly in a map file and call it from a script in the same file. Use the N() macro to avoid symbol conflicts:
API_CALLABLE(N(SetMapChangeFadeSlowest)) {
    set_map_change_fade_rate(1);
    return ApiStatus_DONE2;
}

EvtScript N(EVS_Main) = {
    // ...
    Call(N(SetMapChangeFadeSlowest))
    // ...
    Return
    End
};

INCLUDE_ASM for unmatched scripts

Scripts that have not yet been matched to C source are kept in hand-written MIPS assembly and pulled in with INCLUDE_ASM:
INCLUDE_ASM(s32, "world/area_kkj/kkj_00/main", func_80240000_8B7B70);
When you match such a function, you replace the INCLUDE_ASM line with the equivalent C definition using the EVT macros described on this page.
When writing a new script, always end with Return before End. A script that reaches End without a Return will be killed by the VM — but a script that loops forever without blocking will freeze the game entirely.

Build docs developers (and LLMs) love