Skip to content

Getting started

Where mods/ lives

Install mods/ location
Windows Next to openpete-spyro1.exe
Linux .tar.xz, AppImage ~/.local/share/openpete/mods/ ($XDG_DATA_HOME/openpete/mods/ when set)
portable = true at the top level of openpete.toml Next to the executable, when that directory is writable

On Linux a writable mods/ directory already present next to the executable is used in place of the per-user path. --mods-dir DIR or OPENPETE_MODS_DIR overrides every location above.

Enabling mods

mods/enabled.txt lists enabled mod IDs, one per line; # starts a comment. Without the file every mod in the directory loads, in lexicographic order. Order is priority: on a function hooked by several mods, the mod listed later runs first and base() descends toward earlier mods, then the original game code.

--mods a,b or OPENPETE_MODS=a,b replaces the list for one run. --mods "" runs without mods.

The in-game settings overlay (opened with the M key, "the M overlay" below) has a Mods section that toggles a loaded mod at the next tick boundary and edits its settings. It does not reorder mods.

A distribution packaged with the SDK carries working examples under <exe>/sdk/examples/<id>/; copy one into your mods directory to run it. The Examples page lists them first.

Directory layout

Path Contents
mod.toml Manifest. See Manifest.
config.toml Optional player-editable settings, read through api->config_*.
src/*.c C sources, compiled on load against <exe>/sdk/.
shaders/*.vert, *.frag GLSL, compiled on load. See Shaders.
assets/ Overlay root: textures/, music/, sfx/, wad/, levels/. See Textures and music, Sounds, Levels.
data/ Per-mod persistent directory, created on first api->data_dir() call.
.build/ The engine's compile cache. Not part of a release.

An <id>.zip with mod.toml at the archive root loads the same way: the engine unpacks it under mods/.unpacked/<id>/ and skips the unpack while the zip's hash is unchanged. A folder and a zip with the same ID resolve to the folder.

A declarative mod

Assets are picked up by file convention, with no manifest block:

  • assets/music/track_NN.wav replaces music track NN.
  • assets/textures/<name>.png replaces a texture in the native renderer.
  • assets/sfx/<name>.wav replaces a sound effect.

Manifest blocks cover what files alone cannot: [[level]] wires a custom level, [[material]] re-shades geometry the engine draws, [[config]] declares settings the engine renders, [[binding]] declares a rebindable key. See Manifest.

A code mod

A code mod compiles on the player's machine. The Windows archive and the Linux AppImage bundle the compiler; the Linux .tar.xz uses cc on PATH, or the compiler named by OPENPETE_CC.

  1. mods/hello/mod.toml:

    [mod]
    id      = "hello"
    name    = "Hello"
    version = "0.1.0"
    api     = 1
    sim_mutative = false   # never writes guest RAM
    
  2. mods/hello/src/hello.c:

    #include <openpete_mod_api.h>
    
    static const openpete_mod_api_t* g_api;
    static openpete_mod_t* g_self;
    static unsigned calls;
    
    static void on_camera_update(CPUState* cpu) {
        if ((++calls % 300u) == 0u)   /* every ~10 s at 29.913 ticks/s */
            g_api->log(g_self, OP_MOD_LOG_INFO, "CameraUpdate call #%u", calls);
        g_api->base(cpu);
    }
    
    int openpete_mod_entry(const openpete_mod_api_t* api, openpete_mod_t* self) {
        g_api = api; g_self = self;
        return api->override_name(self, "CameraUpdate", on_camera_update);
    }
    
  3. Add hello to mods/enabled.txt and launch.

The engine hashes the sources, compiles them when the hash changed (a "COMPILING MODS" splash stage), loads the object, and runs openpete_mod_entry. A compile failure disables the mod and writes the error to mods/hello/.build/compile.log; the game still starts. A non-zero return from the entry point disables the mod.

api = N is the minimum engine API version the mod needs. The loader refuses a mod whose pin exceeds the engine's version before compiling it. The API struct is append-only, so a newer engine always satisfies an older pin. Never compare api->api_version with !=; guard a single late entry with if (api->api_version >= N).

Ways to hook a function

  1. By name. api->override_name(self, "CameraUpdate", fn). Names come from the function reference and survive engine updates.
  2. Through a prototype. Functions with a natural signature get typed wrappers in openpete_sdk_wrappers.h: OP_OVERRIDE_IMPL_Foo, op_base_Foo(cpu, ...), op_call_Foo(cpu, ...), with guest-to-host pointer marshalling.
  3. By address. api->override_addr(self, 0x8003xxxx, fn). Arguments are in cpu->a0..a3, the return value in cpu->v0; the register fields need #include <psx_runtime.h> (in sdk/), since the API header only forward-declares CPUState. Addresses are pinned to the SCUS-94228 binary; the loader logs a warning on a mod's first raw registration.