Skip to content

Examples

The example mods that ship with the SDK under <exe>/sdk/examples/<id>/, extracted from each one's mod.toml header. Copy one into your mods directory to run it; start from the one nearest to your idea.

hello-anchor — Hello Anchor

v0.1.0 · writes no guest RAM · src: hello.c

The smallest code mod. A named override that logs, calls base(), and publishes a counter to the Mods panel.

CameraUpdate runs once per gameplay tick, so the override is a cheap anchor for anything that wants a per-tick heartbeat. The body counts calls, logs the first one, and hands control on. It reads and writes no guest RAM, so gameplay stays byte-identical with the mod enabled; the three keys below declare that to the engine.

Uses Why
api->override_name attaches the body to CameraUpdate by name, the tier that keeps working when the decompilation renames a function
api->base runs the rest of the chain and then the original, which is what makes this a pre-hook rather than a replacement
api->log one stderr line on the first call, tagged with the mod id
api->ui_status publishes the running count into the mod's block of the Mods panel

natural-gems — Natural Gems

v0.1.0 · writes guest RAM · src: natural_gems.c

Every gem pickup counts double, written against the typed wrapper surface.

The raw override tier hands a body CPUState* and leaves it to read a0-a3 and write v0. The typed wrappers exist so a prototyped function can be overridden with its natural C signature instead, and this mod is that surface end to end: a typed body, a typed fall-through, and a typed call into an unrelated game function. It writes g_GemTotal, so it is mutative and a memory card played with it enabled carries the modset tag.

Uses Why
OP_OVERRIDE_IMPL_CollectItem declares the override body with a typed Moby* parameter instead of raw registers
OP_REGISTER_OVERRIDE_CollectItem attaches that body without naming an address or a string
op_base_CollectItem falls through to the original with the same typed arguments
op_call_Sin calls an unrelated game function from inside the override, saving and restoring the argument registers around it
api->log reports the doubled pickup and the result of the nested call
api->ui_status shows the running totals in the Mods panel

hud-stopwatch — HUD Stopwatch

v0.1.0 · writes no guest RAM · [[binding]], [[config]] · src: stopwatch.c

An "M:SS.hh" gameplay stopwatch drawn as HUD text.

A tick hook counts sim ticks and a present hook evaluates the time at ctx->alpha, so the hundredths advance smoothly at a high --render-fps rather than stepping 29.913 times a second. That split is the pattern for any mod with a moving visual: the engine never interpolates mod visuals, so the mod owns its own sub-tick curve. Time base is sim ticks divided by 29.913; the mod draws through draw_text only and writes no guest RAM.

Uses Why
api->override_name a CameraUpdate pre-hook, used as the per-tick clock source
api->base hands control back so the camera still updates
api->register_present_hook runs once per present, where the sub-tick time is evaluated
api->draw_text draws the clock in overlay space, in the game's HUD gold
api->binding_down reads the reset key, which the player can rebind without touching the mod
api->config_float reads the three layout rows the Mods panel renders
api->ui_status shows running state and the reset count
api->log one line when the watch starts ticking

crt-beans — CRT (crt-beans)

v1.0.0 · writes no guest RAM · [[config]] · src: crt_beans.c

CRT display simulation. A port of Andrew Duffey's crt-beans, the "fast" three-pass variant (MIT, see LICENSE).

Electron-beam spot brightness and width vary with signal intensity, so the scanlines stay correct at any output resolution instead of aliasing against the pixel grid; an aperture-grille phosphor mask follows. The three passes chain in registration order over the composed frame, and one parameter block feeds all three. Post-process only: the passes read pixels and write pixels, and the mod never touches guest RAM.

Uses Why
api->shader_register compiles each of the three fragment shaders at load
api->postfx_register places each pass at OP_POSTFX_COMPOSITE, after the HUD, with its order in the chain
api->postfx_set_uniforms hands all three passes the same parameter block
api->postfx_enable switches the chain on, since a pass is registered disabled
api->config_float reads the seven [[config]] rows the Mods panel renders
api->log names a shader that failed to build, so the log says why the effect is missing
api->ui_status reports how many of the three passes are live

declarative-example — Declarative Example

v0.1.0 · writes no guest RAM · [[material]]

Every feature a mod can use with no code, in one mod. No src/, no entry point, nothing compiled on the host: the mod is this manifest, three asset files and one fragment shader.

Each asset root is matched by filename, so replacing a texture, a track or a sound is a matter of naming a file correctly. The [[material]] rows then reshade the engine's own draws through selector keys alone. Custom levels are the one declarative feature not shown here; the Custom levels page covers them. [[binding]] and [[config]] rows are read by a mod's own code, so they belong to a code mod, and hud-stopwatch shows both.

Uses Why
[[material]] rows five selectors, each pairing one fragment shader with a different channel, so the same shader shades a different subset per row
assets/textures/ one HD replacement named by content identity, the form the texture dump writes
assets/music/ a track replacement plus a manifest.txt mapping index to file
assets/sfx/ a symbolic slot replacement, the form that follows whatever the current level maps to that slot
shaders/*.frag the material shader the rows share, written against openpete_psx_material.glsl

v0.1.0 · writes no guest RAM · [[config]] · src: channel_gallery.c

One material selector per render channel, toggled live.

Every row registers the same highlight shader with a different selector, so ticking a row shows exactly what that selector claims. It answers "can I shade only X?" for each channel and key, and shows which draw paths are not separately addressable. Materials are display-only: they reshade what the renderer already drew and cannot move, reorder or re-composite anything.

Uses Why
api->shader_register compiles the one highlight shader every row shares
api->material_register registers a row's selector against that shader
api->material_register_refine the rows that need a per-instance decision, where a callback supplies the block or declines the match
api->material_set_selector re-keys a live row when the class combo changes, instead of tearing the material down
api->material_set_params pushes the highlight strength slider into the shared block
api->material_enable a row's checkbox, since a material is registered disabled
api->moby_classes lists the classes the current level actually loaded, so the combo offers only rows that can light
api->guest reads the matched moby inside a refine callback to decide the instance's block
api->register_present_hook counts what lit this present for the row tally
api->config_float, api->config_int, api->config_str the rows the harness drives from config.toml
openpete_mod_ui.h widget vtable checkbox, combo, sliders, tooltips and separators build the panel with no ImGui version in the mod's build

teapot-dragons — Teapot Dragons

v0.1.0 · writes guest RAM · [[config]] · src: teapot_dragons.c

Every crystallised dragon statue is a Utah teapot.

The densest single model the Spyro 1 animated-moby format admits, a 255-vertex count byte against 9-bit face indices, parked in the mod arena and installed over the level's crystal-dragon class from a model post-hook. Class 250 is the last model of every level and its table row bounds the model buffer the rescue cutscene loads into, so the mod shows the stock row to that loader and its own row to everything else.

The geometry is generated from the public-domain Newell teapot patch data by tools/gen_teapot.py into assets/teapot_s1pack.bin.

Uses Why
api->guest_alloc parks the model pack in the mod arena, which savestates and rewind carry like any other guest bytes
api->vstream_handle the animation frame word holds a 21-bit field that cannot address the arena, so each stream is requested as a handle instead
api->override_addr hooks PatchMobyModelPointers, which runs several times per level over the same models
api->override_name hooks LoadDragonCutscene, where the stock model row has to be restored
api->base lets the game do its own patching first, then overwrites the one row
api->guest resolves the parked vaddr to a host pointer before writing the model header
api->data_dir locates the generated asset pack next to the mod
api->config_bool an enable switch and a debug dump, both from the Mods panel
api->log, api->ui_status report the install per level load and the parked address

imgui-panel — ImGui Panel

v0.1.0 · writes no guest RAM · [[config]] · src: imgui_panel.c

The full ImGui C API (openpete_imgui.h) driving a real panel.

A tick hook counts the level's live mobys into a host-side ring and copies the ten nearest to Spyro. The UI section then draws that copy: a plot over the ring, a table of the ten, a floating window with a colour editor, and a foreground draw-list marker. Every guest read happens in the tick hook and the section reads the host copy only, which is the rule for a body running on the compose thread. The section is an always section, so the stats window and the marker draw every present while the detail window waits for the overlay.

Two Containment toggles ship off. Each drives one of the engine's containment paths for scripts/check_mod_imgui.sh: one omits an ImGui_End so the error bracket has something to recover, the other calls ImGui_Text from a present hook so the guard has something to drop.

Uses Why
openpete_imgui.h the whole ImGui C API through OPENPETE_MOD_IMGUI(), for the plot, table, colour editor and draw list the widget vtable cannot express
OPENPETE_MOD_UI_SECTION_FLAGS registers the body as an always section, running on every present rather than only inside the Mods panel
ui->overlay_open draws the interactive half only while the overlay is up, since an always window takes no input when it is closed
ui->text, ui->text_disabled the vtable and the ImGui calls mix freely in one body
api->override_name a GamestateUpdate hook, the per-tick sampler
api->base hands control back after sampling
api->register_present_hook registered only by the containment toggle, to make a call from the wrong context
api->config_bool the two containment toggles
api->ui_status explains in the Mods panel what the toggles do
api->log one line when sampling starts