Skip to content

# Code mods

A code mod exports one function:

    int openpete_mod_entry(const openpete_mod_api_t* api, openpete_mod_t* self);

Return 0 to load. The mod reaches the engine only through `api`; it never links against engine symbols. `api->api_version` is the engine's `OPENPETE_MOD_API_VERSION`; compare it with `<`, never `!=`.

## The override chain

Every recompiled guest function dispatches through one chokepoint, so a per-address chain intercepts every call to it, direct or indirect, without patching code. A pre-hook, a post-hook, and a full replacement are one mechanism, distinguished by where the override calls `api->base(cpu)`:

    static void my_override(CPUState* cpu) {
        /* runs before the original: pre-hook */
        api->base(cpu);
        /* runs after the original: post-hook */
        /* omit base() entirely: full replacement */
    }

`base()` runs the next override in the chain and bottoms out at the recompiled original. It may be called any number of times; each call runs the continuation once. Chain order follows the enabled list: the mod listed later runs first.

There is no event system. Detect an event by diffing guest state around `base()`: to detect a gem pickup, post-hook the collector and compare the counter before and after.

`api->call(cpu, addr)` calls a guest function through the same dispatch layer, so the callee's own override chain runs. Write `cpu->a0..a3` before, read `cpu->v0` after. The register fields are declared in `psx_runtime.h` (in `sdk/`); include it alongside the API header. The register conventions an override relies on are in the CPUState reference.

## Tick context and present context

The game simulates at 29.913 ticks per second. The engine may present many frames per tick, extracting scenes ahead of their display instant, and the number of presents per tick depends on the player's machine.

Tick context: the entry point, overrides, toggle hooks, and refine callbacks. Guest reads and writes, `call`, `guest_alloc`, and every registration are legal here and nowhere else.

Present context: hooks installed with `register_present_hook`. Guest access, `call`, `base`, and registration are refused with an error, since anything done to the game from a present would happen a different number of times on different machines. `ctx->alpha` is the sub-tick phase of the scene being drawn, `ctx->dt` the spacing to the previous scene. Publish state from tick hooks into your own variables and evaluate curves at `ctx->alpha` for sub-tick smoothness; the engine never interpolates mod visuals.

`draw_text` works from both contexts. A call from tick context shows for all of that tick's presents; a call from a present hook shows for that present.

## Guest memory

Guest RAM is a 2 MB little-endian mirror. `api->guest(vaddr)` returns a host pointer for a kuseg, kseg0, or kseg1 address; `api->guest_addr` converts back. Pointer fields inside guest structs hold guest addresses; translate them again before following them.

- Use the typed structs. Their layouts are pinned with `_Static_assert`, so a layout change fails the mod's compile.
- Take addresses from the `OP_GADDR_*` constants in Globals and the `OP_FNADDR_*` constants in `openpete_sdk_symbols.h`.
- `op_read_u32(api, va)`, `op_write_u32(api, va, v)` and their siblings for every width and signedness, `op_read_{u,s}{8,16,32}` and `op_write_{u,s}{8,16,32}`, are inline helpers in the API header; each takes the API table as its first argument.

### The arena

`api->guest_alloc(self, size, align, flags, &host_view)` bump-allocates guest-addressable bytes and returns a host view of the same block. The bytes are timeline state: savestates, rewind, runahead, and process handoff carry and restore them like guest RAM. Mutable per-session state (a cooldown, a spawned-set bitmap, a counter) belongs here. Host statics do not roll back; keep them for caches, file handles, and UI drafts.

- There is no free. Allocate at entry and reuse; allocating per level or per toggle grows every future snapshot.
- Write through the host view from tick context only.
- Store guest addresses inside the block, never host pointers: the bytes cross a process boundary at handoff.
- `OP_GALLOC_IMMUTABLE` declares a block written once and never again (a parked model payload). The engine checksums it at each snapshot and logs a changed block with the mod's name.

On reload the allocation sequence is replayed: the i-th call with the same size, alignment, and flags returns the same address and view with its bytes intact. A savestate whose allocation ledger does not prefix-match the live session (a config edit changed a size) is refused with the mod and entry named.

Vertex streams. An animation frame word references its vertex stream through a 21-bit field that cannot hold an arena address. Request a handle with `api->vstream_handle(self, stream_vaddr)` and write the handle into the field; the game's readers resolve it. Request handles at entry, in a fixed order, once per stream: they are ledgered like allocations.

## Toggling and reload

`register_toggle_hook` installs a callback that runs at the tick boundary after the mod is enabled or disabled from the Mods section. The engine freezes a disabled mod's registrations but never rewinds guest state, so a mod that wrote guest bytes restores the stock bytes on `on = 0`.

## Services

Goal | Service | Writes guest RAM
--- | --- | ---
Draw HUD text | `draw_text` | No
Show status lines in the Mods section | `ui_status` | No
Announce an event on screen | `notify` | No
Replace a game sound from code | `sfx_replace` | No
Play the mod's own WAV | `sfx_play` | No
Read player settings | `[[config]]` rows with `config_*` | No
Read a hotkey | `[[binding]]` rows with `binding_down` | No; acting on it in a tick hook diverges gameplay from a replay
Post-process the frame | `shader_register` with `postfx_*` | No
Re-shade what the engine draws | `[[material]]` rows or `material_register` | No
Read another level's data | `read_level_data` | No
Re-target texture-pack fog | `texpack_fog_shift` | No
Persistent files | `data_dir` | No

`ui_status` or `notify`. `ui_status` reports state: lines in the mod's block of the Mods section, visible while that overlay is open, repainted whenever the mod prints again. `notify` reports an event: a toast in the game's gold chrome, shown for a few seconds regardless of any overlay. `notify` renders with the game's glyph set (`0-9`, `A-Z`, space, and `% / ? + ^ . ' :`; other characters render as a space), coalesces toasts that share a key, is rate-limited per mod, and is a no-op on runahead and rewind ticks.

Full contracts: API reference.