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 |
channel-gallery — Channel Gallery¶
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 |