Skip to content

# API reference

The contract that every mod compiles against, extracted from `core/include/openpete_mod_api.h`. The staged copy lives at `<exe>/sdk/openpete_mod_api.h`.

> The mod API.

> Staged into `<exe>/sdk/` and compiled into every code mod. A mod reaches the engine only through the `openpete_mod_api_t` struct passed to its entry point; it never links against engine symbols.

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

> Return 0 to load; any other value disables the mod and logs the value.

> Stability The struct layout is frozen at api 11: from there on entries are appended, never moved, retyped or removed, and the loader refuses a prebuilt object built against an earlier api. The manifest `api = N` pin is the minimum version the mod needs; check `api_version < N`, never `!=`. The UI annex (openpete_mod_ui.h) is versioned separately and carries no stability promise. Each entry states the version it appeared in with `@since`.

> Overrides A pre-hook, a post-hook and a full replacement are one mechanism, distinguished by where the override calls `api->base(cpu)`: last, first or never. base() runs the next override in the chain and bottoms out at the recompiled original. There is no event system: detect an event by diffing guest state around base().

> Override bodies take the raw MIPS register file (`CPUState`): arguments in `cpu->a0..a3`, return value in `cpu->v0`. openpete_sdk_wrappers.h provides natural-signature wrappers for every prototyped function.

## Supporting types

Opaque per-mod handle owned by the loader.

    typedef struct openpete_mod openpete_mod_t;

An override body.

Read and write guest state through `cpu`; call `api->base(cpu)` to run the next override or the original.

    typedef void (*openpete_mod_override_fn)(CPUState* cpu);

Context handed to a present hook (`openpete_mod_api.register_present_hook`).

    typedef struct openpete_present_ctx {
        uint32_t tick;   /**< Sim tick counter. */
        float    alpha;  /**< Sub-tick phase [0,1) between tick and tick+1; 0 on a canonical present. */
        float    dt;     /**< Seconds between this scene's animation moment and the previous scene's. */
        float    aspect; /**< Overlay-space width: x spans [0, aspect]. */
    } openpete_present_ctx_t;

A present hook body.

    typedef void (*openpete_mod_present_fn)(const openpete_present_ctx_t* ctx);

Live enable/disable callback (`openpete_mod_api.register_toggle_hook`).

Runs at the tick boundary after the mod's registrations have flipped; guest reads and writes are legal. `on` = 0 is where a mod restores guest bytes its entry or hooks changed.

    typedef void (*openpete_mod_toggle_fn)(CPUState* cpu, int on);

Log levels for `openpete_mod_api.log`.

    enum {
        OP_MOD_LOG_DEBUG = 1,
        OP_MOD_LOG_INFO  = 2,
        OP_MOD_LOG_WARN  = 3,
        OP_MOD_LOG_ERROR = 4,
    };

Flags for `openpete_mod_api.guest_alloc`.

    enum {
        /**
         * The mod promises that after the allocating tick neither it nor the
         * game writes these bytes. The engine may leave them out of savestates
         * and rewind history and may abort on a detected write. Use for parked
         * model data, never for scratch.
         */
        OP_GALLOC_IMMUTABLE = 1u << 0,
    };

Post-process injection points for `openpete_mod_api.postfx_register`.

    enum {
        OP_POSTFX_COMPOSITE = 0, /**< After scene and HUD compose, before the present blit; the default point. */
        OP_POSTFX_SCENE     = 1, /**< After the 3D scene, before the HUD, so the HUD stays unaffected.
                                  *   Scene depth is available at `op_scene_depth` (terrain only, not mobys). */
        OP_POSTFX_PRESENT   = 2, /**< Reserved; registration is refused. */
    };

Render channels: the renderer's feeder paths.

A material selector names one of these. Each channel can be narrowed on the keys listed in openpete_mod_material_selector.

    enum {
        OP_CHAN_MOBY     = 1,
        OP_CHAN_TERRAIN  = 2,
        OP_CHAN_PLAYER   = 3,
        OP_CHAN_FLAME    = 4,
        OP_CHAN_PARTICLE = 5,
        OP_CHAN_SKY      = 6,
        OP_CHAN_SHADOW   = 7,
        OP_CHAN_TRACER   = 8,
        OP_CHAN_PORTAL   = 9,   /**< Destination-sky preview inside a portal. */
    };

Portal preview kinds: the `ident` key for `OP_CHAN_PORTAL`.

The game draws a portal three ways across the approach; a material keyed on one kind pops as the player walks up, so -1 (all three) is the usual choice.

    enum {
        OP_PORTAL_FLAT    = 0,  /**< Far colour chip / close backstop. */
        OP_PORTAL_DOME    = 1,  /**< Full gouraud dome, close up. */
        OP_PORTAL_BLENDED = 2,  /**< Fogged dome, medium cross-dissolve. */
    };

Particle kinds: the high byte of the `ident` key for `OP_CHAN_PARTICLE`.

The particle feeder draws six effects. The ident of a draw is `(kind << 8) | sel`, where `sel` is the game's own particle class for the two kinds that carry one (quads and lines) and 0 otherwise. `OP_PART_IDENT` builds the key. The ident match is exact, so a kind that carries a class is claimed one class per row. Glow rings and sparkles are separate game renderers that share this channel; a glow-only or sparkle-only material keys on the kind with `sel` 0.

    enum {
        OP_PART_QUAD  = 0,  /**< Textured billboard quads; `sel` = particle class. */
        OP_PART_LINE  = 1,  /**< Streak lines; `sel` = particle class. */
        OP_PART_GLOW  = 2,  /**< Additive glow rings around collectables. */
        OP_PART_FLARE = 3,  /**< Sparkle flares (the twinkle quads). */
        OP_PART_SPARK = 4,  /**< Sparkle lines. */
        OP_PART_STAR  = 5,  /**< Dragon-rescue star fan. */
    };

Legacy 0-based path values; prefer `OP_CHAN_*`.

    enum { OP_RP_MOBY = 0, OP_RP_TERRAIN = 1 };

Selector refinements.

Each pair narrows: setting neither member of a pair selects both.

    enum {
        OP_SEL_ENV_ANIM   = 1u << 0,  /**< Env-animated faces only. */
        OP_SEL_HP_TIER    = 1u << 1,  /**< High-poly tier only; neither tier bit = both. */
        OP_SEL_LP_TIER    = 1u << 2,  /**< Low-poly tier only. */
        OP_SEL_UNTEXTURED = 1u << 3,  /**< Gouraud faces only; neither texture bit = both. */
        OP_SEL_TEXTURED   = 1u << 4,  /**< Textured faces only. */
    };

Chooses the geometry a material may claim.

    typedef struct openpete_mod_material_selector {
        uint32_t struct_size; /**< `sizeof(*this)` at the mod's compile time; the engine refuses a size it does not know. */
        int      channel;     /**< `OP_CHAN_*`; -1 = any channel. */
        int      ident;       /**< Channel-local identity, meaning per channel: flame ribbon or cap,
                               *   particle `OP_PART_*` kind `(kind << 8 | sel)`, shadow caster kind,
                               *   portal preview kind. -1 = any. Player, sky and tracer emit only 0. */
        int      moby_class;  /**< Moby only; -1 = any. */
        int      texture;     /**< Terrain texture-record id 0..127; -1 = any. */
        int      level;       /**< `g_LevelId`; -1 = any. Sector and texture ids are per level, so a
                               *   key without this matches the same id in every level. */
        unsigned flags;       /**< `OP_SEL_*` refinements. */
    } openpete_mod_material_selector_t;

One instance at refine time.

Every field is filled by the engine; `channel` selects the live union member.

    typedef struct openpete_mod_material_key {
        uint32_t struct_size;   /**< `sizeof` at engine compile time. */
        uint32_t version;       /**< `OPENPETE_MATERIAL_KEY_VER_*` for @c channel. */
        int      channel;       /**< `OP_CHAN_*`. */
        union {
            struct {
                uint32_t vaddr;      /**< Guest address of the moby record. */
                int      moby_class; /**< `m_Class`. */
            } moby;
            struct {
                int      texture;    /**< Tiledef id, or -1 for the untextured band. */
                unsigned flags;      /**< The `OP_SEL_*` bits describing this face. */
            } terrain;
        } u;
    } openpete_mod_material_key_t;

Refine callback (`openpete_mod_api.material_register_refine`).

The selector decides which geometry a material may claim; the refine callback decides per instance whether it does, and with what uniform block. Example: the selector prunes to class 42, the callback claims only instances that are burning.

The callback runs during extraction in tick context: guest RAM is readable through `api->guest()` and `op_read_*`; host render state is off limits. Results are memoised per instance, so the callback must be a pure function of the key and tick-stable guest state; the call count is not part of the contract.

- `key`: The instance.
- `out`: Uniform block to fill, at most `cap` bytes.
- `cap`: `OPENPETE_MATERIAL_UNIFORMS_MAX`.
- Returns: Bytes written to `out` (0 = claim with no parameters), or -1 to decline: the instance keeps stock shading.

    typedef int (*openpete_mod_refine_fn)(const openpete_mod_material_key_t* key,
                                          void* out, uint32_t cap);

The api version this header describes.

    #define OPENPETE_MOD_API_VERSION 11u

## Index

Every entry and the api version that introduced it. A mod declaring `api = N` in its manifest may use entries with Since ≤ N; the loader refuses a newer pin than the engine provides.

Entry | Since | Group | Brief
--- | --- | --- | ---
`api->api_version` | api 1 |  | The engine's OPENPETE_MOD_API_VERSION; compare with `<`, never `!=`.
`api->override_addr` | api 1 | Overrides | Attach an override by PSX vaddr.
`api->override_name` | api 1 | Overrides | Attach an override by symbol name.
`api->base` | api 1 | Overrides | Run the next override in the chain, or the original.
`api->call` | api 1 | Overrides | Call a guest function by vaddr through the dispatch layer, so the callee's own override chain runs.
`api->guest` | api 1 | Guest memory | Translate a guest vaddr (kuseg, kseg0 or kseg1) to a host pointer.
`api->guest_addr` | api 1 | Guest memory | Inverse of guest().
`api->guest_alloc` | api 1 | Guest memory | Allocate guest-addressable memory.
`api->log` | api 1 | Lifecycle | printf-style logging, tagged `mod:<id>`.
`api->data_dir` | api 1 | Lifecycle | Per-mod persistent data directory (`mods/<id>/data`, created on first call).
`api->register_toggle_hook` | api 1 | Lifecycle | Register the live-toggle callback.
`api->config_bool` | api 1 | Settings | Read a bool.
`api->config_int` | api 1 | Settings | Read an integer.
`api->config_float` | api 1 | Settings | Read a float.
`api->config_str` | api 1 | Settings | Read a string.
`api->ui_status` | api 1 | Settings | Print a status line into this mod's section of the Mods panel.
`api->register_present_hook` | api 1 | Present hooks and screen-space draws | Register a per-present hook.
`api->draw_text` | api 1 | Present hooks and screen-space draws | Draw text in overlay space on the native renderer.
`api->shader_register` | api 1 | Post-process shaders | Register a GLSL shader by path relative to the mod directory.
`api->postfx_register` | api 1 | Post-process shaders | Register a fullscreen fragment pass at an injection point.
`api->postfx_enable` | api 1 | Post-process shaders | Enable or disable a pass.
`api->postfx_set_uniforms` | api 1 | Post-process shaders | Set a pass's uniform block.
`api->sfx_replace` | api 1 | Audio | Substitute the sample behind a named game sound.
`api->sfx_play` | api 1 | Audio | Play `assets/sfx/own/<name>.wav` from this mod's directory as a host-side one-shot.
`api->binding_down` | api 1 | Input bindings | Read a `[[binding]]` declared in this mod's mod.toml.
`api->read_level_data` | api 2 | Game data and texture packs | Read one level's raw Data blob from the mounted disc.
`api->texpack_fog_shift` | api 2 | Game data and texture packs | Retarget the texture-pack fog ambience.
`api->material_register` | api 3 | Materials | Attach a fragment shader to a set of geometry.
`api->material_enable` | api 3 | Materials | Turn one registered material on or off.
`api->material_set_params` | api 3 | Materials | Set the uniform block the material's shader reads.
`api->material_register_refine` | api 4 | Materials | material_register() with a per-instance refine callback.
`api->material_set_selector` | api 6 | Materials | Point a live material at a different selector.
`api->moby_classes` | api 6 | Materials | The moby classes the current level has loaded, ascending.
`api->vstream_handle` | api 9 | Vertex streams | A stream handle for a vertex stream parked in the arena.
`api->notify` | api 8 | Notifications | Post a transient on-screen notification.

### `api->api_version`

    uint32_t api_version;

The engine's `OPENPETE_MOD_API_VERSION`; compare with `<`, never `!=`.

Since api 1.

## Overrides

Attach an override to a guest function by symbol name (resolved through openpete_sdk_symbols.h, e.g. "CameraUpdate") or by PSX vaddr (pinned to this binary; the loader warns once per mod on the first raw registration). Priority is the enabled-list order: overrides from later-listed mods run first, and base() walks toward earlier mods and then the recompiled original.

### `api->override_addr`

    int (*override_addr)(openpete_mod_t* self, uint32_t addr, openpete_mod_override_fn fn);

Attach an override by PSX vaddr.

- Returns: 0 on success, non-zero for invalid arguments. Since api 1.

### `api->override_name`

    int (*override_name)(openpete_mod_t* self, const char* name, openpete_mod_override_fn fn);

Attach an override by symbol name.

- Returns: 0 on success, non-zero for an unknown name. Since api 1.

### `api->base`

    void (*base)(CPUState* cpu);

Run the next override in the chain, or the original.

Valid only while an override registered by this mod is on the call stack. May be called any number of times; each call runs the continuation once.

Since api 1.

### `api->call`

    void (*call)(CPUState* cpu, uint32_t addr);

Call a guest function by vaddr through the dispatch layer, so the callee's own override chain runs.

Write `cpu->a0..a3` before, read `cpu->v0` after. Valid only inside an override.

Since api 1.

## Guest memory

### `api->guest`

    void* (*guest)(uint32_t vaddr);

Translate a guest vaddr (kuseg, kseg0 or kseg1) to a host pointer.

The 2 MB RAM, the scratchpad and guest_alloc's arena all resolve. Guest RAM and every supported host are little-endian, so typed reads and writes through the pointer see what the game sees. Pointer fields inside guest structs hold guest vaddrs; translate them again before dereferencing. Global addresses are `OP_GADDR_*` in openpete_sdk_symbols.h. Sim thread only.

- Returns: The host pointer, or NULL for an address that names nothing, including guest address 0. Since api 1.

### `api->guest_addr`

    uint32_t (*guest_addr)(const void* host);

Inverse of guest().

Interior pointers map to the corresponding interior vaddr.

- Returns: The guest vaddr of a host pointer into RAM, the scratchpad or an arena allocation; 0 if the pointer names no guest address. Since api 1.

### `api->guest_alloc`

    uint32_t (*guest_alloc)(openpete_mod_t* self, uint32_t size, uint32_t align, uint32_t flags, void** host_view);

Allocate guest-addressable memory.

The arena is a bump allocator with no free: allocate at entry and reuse. Tick context only: call it, and write through `host_view`, only from entry, tick hooks and toggle hooks.

Contents are zero on first allocation. On reload the allocation sequence is replayed: the i-th call with the same size, alignment and flags returns the same vaddr and view, holding the bytes as last written. Initialise what needs initialising.

The vaddr and host view are stable for the process lifetime, and the bytes are reachable through every engine guest access path (`cpu->read_*`, `cpu->write_*`, guest()).

Unless flagged `OP_GALLOC_IMMUTABLE` the bytes are timeline state: savestates, rewind, runahead and process handoff carry and restore them like guest RAM. An allocation made after a snapshot keeps its contents when that snapshot is restored; the cursor never rewinds. Keep mutable per-session state here rather than in host statics, and store guest vaddrs rather than host pointers, since carried bytes cross process boundaries at handoff.

Not promised: the arena's base or layout (never hardcode a vaddr; a savestate whose allocation sequence does not prefix-match the live session is refused, naming the mod and entry), survival of `host_view` across handoff, or any particular snapshot mechanism.

Model vertex streams referenced from animation frame words are parked here too; the frame word carries a handle from vstream_handle(), not an address.

- `size`: Bytes to allocate.
- `align`: Power of two; 0 = default.
- `flags`: `OP_GALLOC_*`. An unknown flag fails the call and logs the flag.
- `host_view`: Receives a host alias of the same bytes.
- Returns: The guest vaddr; 0 = exhausted, unavailable or invalid. Since api 1.

## Lifecycle

### `api->log`

    void (*log)(openpete_mod_t* self, int level, const char* fmt, ...);

printf-style logging, tagged `mod:<id>`.

- `level`: `OP_MOD_LOG_*`. Since api 1.

### `api->data_dir`

    const char* (*data_dir)(openpete_mod_t* self);

Per-mod persistent data directory (`mods/<id>/data`, created on first call).

- Returns: An engine-owned absolute path; use plain stdio inside it. Since api 1.

### `api->register_toggle_hook`

    int (*register_toggle_hook)(openpete_mod_t* self, openpete_mod_toggle_fn fn);

Register the live-toggle callback.

Runs at the tick boundary after each flip of this mod's enable state. The engine freezes a disabled mod's registrations but never rewinds guest state, so a mod that wrote guest state restores the stock bytes on `on = 0`. One registration per mod; re-registering replaces.

- Returns: 0. Since api 1.

## Settings

Read a value from `mods/<id>/config.toml`. Keys are dotted paths: "chests.collision" is key `collision` in table `[chests]`. `def` is returned when the file, the key or a type-compatible value is absent. The file is parsed once, before entry runs; when the user commits a change in the Mods panel (rendered from the manifest's `[[config]]` rows) the engine rewrites config.toml and reloads the mod, so read config at entry. Coercion: bools read as 0/1 through config_int and config_float; ints read through config_float; floats truncate through config_int; strings never coerce.

### `api->config_bool`

    int (*config_bool)(openpete_mod_t* self, const char* key, int def);

Read a bool. Since api 1.

### `api->config_int`

    int64_t (*config_int)(openpete_mod_t* self, const char* key, int64_t def);

Read an integer. Since api 1.

### `api->config_float`

    double (*config_float)(openpete_mod_t* self, const char* key, double def);

Read a float. Since api 1.

### `api->config_str`

    int (*config_str)(openpete_mod_t* self, const char* key, const char* def, char* buf, int cap);

Read a string.

Copies at most `cap-1` chars plus NUL into `buf.`

- Returns: 1 if the key was present, 0 if the default was copied. Since api 1.

### `api->ui_status`

    void (*ui_status)(openpete_mod_t* self, const char* fmt, ...);

Print a status line into this mod's section of the Mods panel.

Legal from any context; host state only. Lines accumulate within a tick and persist until the next tick in which the mod calls ui_status again, so a line printed once stays visible and a per-tick printer repaints its block each tick. The block truncates past about 24 lines. Custom panels use openpete_mod_ui.h.

Since api 1.

## Present hooks and screen-space draws

### `api->register_present_hook`

    int (*register_present_hook)(openpete_mod_t* self, openpete_mod_present_fn fn);

Register a per-present hook.

Runs once per scene the engine extracts (the canonical one each tick plus every sub-tick scene at a higher `--render-fps`). The hook fires when its scene is extracted on the sim thread, ahead of display, and the engine may produce fewer scenes than display slots; call count and timing are not part of the contract. `ctx->alpha` is the animation moment the scene shows and `ctx->dt` the spacing to the previous scene, so a curve evaluated at `ctx->alpha` is correct under any cadence. Publish state from tick hooks into your own variables and evaluate it here; the engine never interpolates mod visuals.

Render-pure, enforced: guest access, game calls, base() and override registration are refused inside a present hook, since presents are non-deterministic in count and timing.

Runs only when something is presented: a plain headless run never calls it, a headless run that takes a screenshot does.

Since api 1.

### `api->draw_text`

    void (*draw_text)(float x, float y, float cap_h, uint32_t rgb, const char* str);

Draw text in overlay space on the native renderer.

Overlay space: units of window height, origin top-left, +y down, x in [0, aspect], y in [0, 1]. Host state only. Immediate mode: a call from tick context shows for all of that tick's presents (redraw every tick); a call from a present hook shows for that present. Glyph set: digits, A-Z (lowercase folds up), space, and `% / ? + ^ . ' :`, drawn with the game's own gold text glyphs; text appears once the game has loaded the glyph font. Headless and PsyCross-only runs consume nothing.

- `cap_h`: Glyph cap height in overlay units.
- `rgb`: Glyph tint (0x605010 = HUD gold). Since api 1.

## Post-process shaders

### `api->shader_register`

    int (*shader_register)(openpete_mod_t* self, const char* path);

Register a GLSL shader by path relative to the mod directory.

The extension selects the stage (e.g. "shaders/crt.frag"). The engine compiles it to SPIR-V on load (in-process glslang, Vulkan 1.0 dialect), cached by content hash under `.build/shaders/`. A prebuilt `<path>.spv` next to a missing source loads instead if a `<path>.spv.ihash` sidecar matches the current shader-interface hash. `#include "openpete_shader_api.glsl"` resolves to the SDK interface header. Entry or tick context. Sources join the hot-reload watch.

- Returns: A shader handle >= 0, or -1 for a missing file or compile error (see `.build/shaders/<name>.log`; the mod still loads). Since api 1.

### `api->postfx_register`

    int (*postfx_register)(openpete_mod_t* self, int frag_shader, int point, int order);

Register a fullscreen fragment pass at an injection point.

Pass order across mods is enabled-list priority; `order` breaks ties within a mod (lower runs first). The pass samples the previous pass (set 0) and receives the engine UBO (time, tick, alpha, resolution; see `shaders/openpete_shader_api.glsl`) plus the mod's uniform block. Entry or tick context.

- `frag_shader`: Handle from shader_register().
- `point`: `OP_POSTFX_*`.
- Returns: An fx handle >= 0, registered disabled, or -1. Since api 1.

### `api->postfx_enable`

    int (*postfx_enable)(openpete_mod_t* self, int fx, int enabled);

Enable or disable a pass. Any context. Since api 1.

### `api->postfx_set_uniforms`

    int (*postfx_set_uniforms)(openpete_mod_t* self, int fx, const void* data, uint32_t size);

Set a pass's uniform block.

Copies `data` on call. Any context; push from tick or present hooks.

- `size`: At most 1024 bytes. Since api 1.

## Audio

### `api->sfx_replace`

    int (*sfx_replace)(openpete_mod_t* self, const char* name, const void* adpcm_body, uint32_t len, uint16_t pitch);

Substitute the sample behind a named game sound.

The code form of dropping `<name>.wav` into this mod's `assets/sfx`. The swap happens at the voice's PCM upload, below the SPU RAM image and voice mirror, so guest RAM, the gameplay rand() stream and the tick on which the game sees the sound end (it polls the stock sample's duration) stay stock; a longer replacement rings out past that tick. Sounds without a SoundTable name (a moby's own m_Sounds def) are unreachable here; a hash-named file in `assets/sfx/` covers every sample and needs no code.

- `name`: A game sound name.
- `adpcm_body`: Raw SPU-ADPCM frames (no VAG header, final block end-flag 0x01); decoded and copied by the engine.
- `pitch`: SPU units (0x1000 = 44.1 kHz), overriding the game's pitch writes; 0 keeps the game's own pitch.
- Returns: 0, or -1 for an unknown name. Since api 1.

### `api->sfx_play`

    int (*sfx_play)(openpete_mod_t* self, const char* name, float gain);

Play `assets/sfx/own/<name>.wav` from this mod's directory as a host-side one-shot.

Never routed through the SPU, so it is legal from tick and present hooks. WAVs are cached after the first play.

- `gain`: 0..4; 1.0 = as authored.
- Returns: 0, or -1 for a missing file or no audio device (logged). Since api 1.

## Input bindings

### `api->binding_down`

    int (*binding_down)(openpete_mod_t* self, const char* name);

Read a `[[binding]]` declared in this mod's mod.toml.

Declared bindings (`{name, key, purpose}`) are conflict-checked across the enabled set at load, and the user rebinds through openpete.toml `[keys.mod.<id>]` (keyboard names, gamepad names such as "pad:north", or "none"). Reading host input in a tick hook diverges gameplay from a replay by design; display-only reads belong in present hooks.

- Returns: 1 while the bound host key is held; an undeclared name logs an error and returns 0. Since api 1.

## Game data and texture packs

### `api->read_level_data`

    int (*read_level_data)(openpete_mod_t* self, int level_id, void* buf, uint32_t cap);

Read one level's raw Data blob from the mounted disc.

Lets a mod use another level's sky, palette or geometry without shipping derived data.

- `buf`: Destination of `cap` bytes; NULL queries the length.
- Returns: The blob length, or < 0 when no game layer provides level data or the id is unknown. Since api 2.

### `api->texpack_fog_shift`

    int (*texpack_fog_shift)(openpete_mod_t* self, const float scale_rgb[3], const float offset_rgb[3]);

Retarget the texture-pack fog ambience.

Fogged replacement texels blend toward `scale*stock+offset` (offsets in 0..255 units) instead of the stock ladder colours, for packs whose art re-grades the level. NULL/NULL restores stock. Per-texture grade fits take precedence where they have signal. Display only.

Since api 2.

## Materials

### `api->material_register`

    int (*material_register)(openpete_mod_t* self, const openpete_mod_material_selector_t* sel, int shader);

Attach a fragment shader to a set of geometry.

The set is chosen by a render channel and that channel's keys (moby class, terrain texture or tier, channel-local identity, level). Build the selector with openpete_mod_material_sel_any() and narrow only what you mean; the engine refuses a selector it cannot fully honour rather than matching wider than asked.

The shader reshades within the draw group the geometry already belongs to: it cannot change blending, depth policy or draw order. Unmatched or failed builds keep stock shading; a material cannot make geometry invisible.

- `shader`: A frag handle from shader_register().
- Returns: A material handle, registered enabled, or -1. Since api 3.

### `api->material_enable`

    int (*material_enable)(openpete_mod_t* self, int material, int on);

Turn one registered material on or off.

Callable from any context; the natural target of a `[[config]]` toggle.

Since api 3.

### `api->material_set_params`

    int (*material_set_params)(openpete_mod_t* self, int material, const void* block, uint32_t len);

Set the uniform block the material's shader reads.

May be called per tick; for time-varying effects prefer the engine UBO's time in the shader.

- `block`: std140, at most `OPENPETE_MATERIAL_UNIFORMS_MAX` bytes. Since api 3.

### `api->material_register_refine`

    int (*material_register_refine)(openpete_mod_t* self, const openpete_mod_material_selector_t* sel, int shader, openpete_mod_refine_fn refine);

material_register() with a per-instance refine callback.

The engine calls `refine` once per candidate instance during extraction and it returns that instance's uniform block, or -1 to leave it stock. Instances with byte-identical blocks share one slot and one batch, so a callback with few distinct outcomes is cheap and one with a distinct block per instance costs a draw each. Scope the selector tightly and let the callback decide the rare cases.

Available on the moby channel (keyed per instance) and on terrain (keyed per texture id or the untextured band, asked once per present); on other channels register declaratively and drive the material with material_set_params().

Since api 4.

### `api->material_set_selector`

    int (*material_set_selector)(openpete_mod_t* self, int material, const openpete_mod_material_selector_t* sel);

Point a live material at a different selector.

Keeps its handle, shader, refine callback and enabled state. Selection is constant across a present, so a call from a present hook or UI section takes effect at the next tick; from entry or a tick hook it applies at once. The material keeps shading under its old key until the new one applies. The selector is validated as material_register() validates it.

Since api 6.

### `api->moby_classes`

    int (*moby_classes)(openpete_mod_t* self, uint16_t* out, int cap);

The moby classes the current level has loaded, ascending.

Level-scoped, not visibility-scoped: a superset of the classes with a live instance at this instant, and the set worth offering a user to choose from. Reads an engine-side snapshot refreshed once per tick, so it is safe from a UI section. Empty before the first level finishes loading.

- `out`: Destination of `cap` entries; `out = NULL, cap = 0` sizes the buffer.
- Returns: The count. Since api 6.

## Vertex streams

### `api->vstream_handle`

    uint32_t (*vstream_handle)(openpete_mod_t* self, uint32_t stream_vaddr);

A stream handle for a vertex stream parked in the arena.

The handle is the value to write into an animation frame word's 21-bit vertex-stream field (bits 0-20) in place of an address that does not fit. The game's readers resolve it to the full vaddr, so a stream may live anywhere guest_alloc() puts it. The same vaddr always yields the same handle; handles are below 0x8000 and never 0. The game stores frame-word addresses halved; a handle is written as-is in both encodings.

Ask at entry, in a fixed order, once per stream: every call is ledgered like an allocation, replayed on reload, and verified when a savestate loads, so a sequence that depends on the order levels were visited makes a savestate from another route refuse to load. Tick context only.

- Returns: The handle, or 0 for vaddr 0, a kernel-area vaddr, or an exhausted table. Since api 9.

## Notifications

### `api->notify`

    void (*notify)(openpete_mod_t* self, const char* key, uint32_t timeout_ms, const char* fmt, ...);

Post a transient on-screen notification.

Drawn in the game's gold-bordered chrome at the anchor the player chose. ui_status() reports state; notify reports an event.

Legal from entry, tick hooks and present hooks; host state only. Rate-limited per mod (a small burst, then a slow refill). A no-op on runahead and rewind-replay ticks, whose effects are rolled back or re-executed; a mod therefore sees fewer toasts than calls.

Glyph set: 0-9, A-Z (lowercase folds up), space, and `% / ? + ^ . ' :`. Any other character renders as a space.

- `key`: May be NULL. Coalesces: a live toast with the same key is repainted with the new text and a restarted timer instead of a second toast stacking. The engine prefixes the mod id, so keys never collide across mods.
- `timeout_ms`: 0 takes the player's `--osd-timeout` default; values above 30000 clamp to it. A repaint takes the repainting call's timeout. Since api 8.

## Convenience guest accessors

Typed guest-RAM helpers, defined inline in the header itself, so they need no extra link step:

    static inline uint32_t op_read_u32(const openpete_mod_api_t* api, uint32_t va);
    static inline void op_write_u32(const openpete_mod_api_t* api, uint32_t va, uint32_t v);
    static inline int32_t op_read_s32(const openpete_mod_api_t* api, uint32_t va);
    static inline void op_write_s32(const openpete_mod_api_t* api, uint32_t va, int32_t v);
    static inline uint16_t op_read_u16(const openpete_mod_api_t* api, uint32_t va);
    static inline void op_write_u16(const openpete_mod_api_t* api, uint32_t va, uint16_t v);
    static inline int16_t op_read_s16(const openpete_mod_api_t* api, uint32_t va);
    static inline void op_write_s16(const openpete_mod_api_t* api, uint32_t va, int16_t v);
    static inline uint8_t op_read_u8(const openpete_mod_api_t* api, uint32_t va);
    static inline void op_write_u8(const openpete_mod_api_t* api, uint32_t va, uint8_t v);
    static inline int8_t op_read_s8(const openpete_mod_api_t* api, uint32_t va);
    static inline void op_write_s8(const openpete_mod_api_t* api, uint32_t va, int8_t v);