This guide starts with the versioned engine-neutral API in
liboot_engine.h. The raw compatibility API in liboot.h remains available
when an integration needs direct control of its geometry buffers. For Unity,
Godot, Unreal, Rust, Python, and custom-engine patterns, continue with
ENGINE_INTEGRATION.md.
- A C11 compiler and GNU Make, or CMake 3.16 or newer.
- Python 3.8 or newer for
make checkand CMake builds with testing enabled (the default). A library-only CMake build can use-DBUILD_TESTING=OFF. - Linux, macOS, or Windows x86-64 under MSYS2 UCRT64/MinGW. CI builds shared and static libraries on Linux x86-64, Linux ARM64, macOS, and Windows UCRT64.
- A legally obtained, compatible Ocarina of Time ROM supplied at runtime.
ROMs and extracted assets must not be committed to a project using liboot;
the engine wrapper accepts buffers from
OOT_ENGINE_MIN_ROM_SIZE(0x1060) bytes throughOOT_ENGINE_MAX_ROM_SIZE(256 MiB), inclusive. - SDL2 and OpenGL development packages only if you want the interactive playground. The core library itself does not depend on SDL or OpenGL.
On Debian or Ubuntu, the full playground toolchain is:
sudo apt install build-essential pkg-config libsdl2-dev libgl-devOn Windows, open an MSYS2 UCRT64 shell and install
mingw-w64-ucrt-x86_64-gcc, mingw-w64-ucrt-x86_64-cmake,
mingw-w64-ucrt-x86_64-ninja, make, and python. Release DLLs use the same
UCRT64 ABI.
Using Make:
make
# dist/liboot.so
# dist/include/liboot.h
# dist/include/liboot_engine.h
# dist/include/liboot.hppUsing CMake:
cmake -S . -B build-cmake -DCMAKE_BUILD_TYPE=Release
cmake --build build-cmake
cmake --install build-cmake --prefix "$PWD/stage"Set -DBUILD_SHARED_LIBS=OFF for a static library. Installed CMake consumers
can link liboot::oot; installed pkg-config consumers can use liboot.
For a separate ASan/UBSan development build:
make sanitizersThis creates build/sanitizers and runs its complete ROM-free test suite.
Leak detection is enabled on Linux and disabled for Apple's sanitizer runtime;
ASan and UBSan remain active on both. LIBOOT_SANITIZERS defaults to
address,undefined; override it with a comma-separated GCC/Clang sanitizer
list when isolating one sanitizer. The runtime must be available to the
selected compiler, either system-wide or through that compiler's configured
library search path.
make -C examples
./examples/engine /path/to/your/oot.z64The preferred source is examples/engine.c. examples/basic.c demonstrates the raw compatibility API. The engine-neutral lifecycle is:
- Read the user-provided ROM into memory.
- Require
oot_engine_api_version() == OOT_ENGINE_API_VERSIONbefore calling any initializer. - Initialize
OoTEngineConfig, check the initializer result, calloot_engine_create, check that result, and release the ROM buffer after it returns. - Load host collision with
oot_engine_static_world_loador a ROM scene withoot_engine_scene_load. - Create the engine's Link instance with
oot_engine_link_create. - Call
oot_engine_steponce every 60 ms, or useoot_engine_advance. - Render or inspect the borrowed
OoTEngineFrame. - Call
oot_engine_destroy; it also deletes an active Link.
Loading a world before Link is mandatory. Player_Init immediately queries
the real collision engine, so the wrapper rejects an unsafe ordering.
PAL gameplay advances one tick every 60 ms (3/50 seconds, or 16⅔ Hz). Render
at any rate. The wrapper includes a capped accumulator and latches a button tap
received during a render frame that does not produce a simulation tick.
void host_frame(float elapsed_seconds) {
if (oot_engine_api_version() != OOT_ENGINE_API_VERSION) {
report_api_mismatch();
return;
}
OoTEngineInput input;
OoTResult result = oot_engine_input_init(&input);
if (result != OOT_ENGINE_RESULT_OK) {
report_error(result);
return;
}
sample_input(&input);
uint32_t steps = 0;
const OoTEngineFrame *frame = NULL;
result = oot_engine_advance(
engine, elapsed_seconds, &input, &steps, &frame);
if (result == OOT_ENGINE_RESULT_OK && frame != NULL) {
render_link(&frame->link, &frame->geometry,
frame->interpolationAlpha);
}
}In production, perform the API-version comparison once when loading the native
library rather than once per render frame. The C initializer name above is an
ergonomic macro over oot_engine_input_init_sized; it supplies sizeof(input)
and OOT_ENGINE_API_VERSION, and its OoTResult must still be checked.
Call oot_engine_step instead if the host already owns a fixed 60 ms loop.
Raw oot_link_tick also advances exactly one tick and never accepts delta
time. fixedStepSeconds is a scheduling interval and accepts values from
0.001 through 1.0 seconds; values other than the authentic 3/50 change
gameplay speed. maxSubsteps accepts 1 through 1000 (default 4). For either
field, zero selects the initialized default.
Buttons are sampled by the original game logic. A tap must be present for at
least one simulation tick. Hold/release edges matter for the bow, hookshot,
boomerang, bombs, shielding, and chargeable actions. Clamp stick axes to
[-1, 1]. camLookX/camLookZ represent the horizontal camera-to-Link
direction; the wrapper normalizes them and treats zero as +Z.
OoTEngineFrame.geometry and the raw OoTLinkGeometryBuffers are
renderer-neutral:
- positions and normals contain three floats per vertex;
- colors contain RGB floats per vertex;
- UVs contain two floats per vertex;
triTexture[t]selects a texture for trianglet, or0xFFFFfor none;- the engine wrapper reports
numTrianglesand its configuredtriangleCapacity; raw callers useoot_link_tick_sizedwith the appendedtriangleCapacity/numTrianglesUsed32fields. The legacyoot_link_tickremains bounded byOOT_GEO_MAX_TRIANGLESfor binary compatibility. - frame and scene batch arrays identify contiguous source-entity and material ranges, including texture, pass, blend, depth, culling, and RDP mode data.
frame->linkGeometryTruncatedreports whether the combined Link/Navi/actor stream omitted valid triangles at that cap.
Call oot_engine_texture_count/oot_engine_texture_get after ticks (or the
raw equivalents). Cache textures by index and upload again only when the
reported revision changes. Pixels are RGBA8. Honor wrapS and wrapT: 0
repeat, 1 mirror, 2 clamp.
Navi wings and projectile geometry are opt-in and append to the configured frame triangle buffer:
oot_engine_set_render_flags(
engine, OOT_ENGINE_RENDER_NAVI | OOT_ENGINE_RENDER_ACTORS);The batch array identifies the source kind and instance, source actor or room,
triangle range, texture, and material state. The wrapper also includes
projectile transforms in OoTEngineFrame.actors. Raw API hosts use
oot_navi_set_render, oot_actor_set_render, and oot_actor_query directly.
For a host-owned level, submit static integer triangles with
oot_engine_static_world_load. Coordinates are right-handed, Y-up OoT world
units and must fit signed 16-bit range. Water boxes are axis-aligned XZ
rectangles extending downward from their surface height. A world accepts at
most OOT_ENGINE_MAX_STATIC_SURFACES (2730) triangles and
OOT_ENGINE_MAX_WATER_BOXES (65,535) water boxes; water dimensions must be
positive.
Create moving floors, doors, or platforms with
oot_engine_dynamic_collision_create, update their transforms before a step,
and delete their generation-checked handles when done. Live objects are rebound
after a static-world or scene replacement. The shared native budget is 50
objects, 512 triangles, and 512 unique local vertices.
For supported ROM scenes, call oot_engine_scene_load(engine, scene, room, &nativeResult). Scene loading replaces the current static world. Query
opaque and translucent room geometry with oot_engine_scene_get_geometry,
use oot_engine_scene_get_spawn for an entrance, and copy the live room
behavior with oot_engine_scene_get_runtime. The latter reports the real room
type/environment, echo, Lens behavior, warp restriction and camera type that
the vendored Player code is currently using. A custom-world load makes this
query return OOT_ENGINE_RESULT_NOT_AVAILABLE.
Use oot_engine_scene_load_ex when an explicit child/adult and day/night layer
is required. Exit and void-out contacts are queued as
OoTWorldEvent records; the host chooses and loads the destination. Scene actor
entries, room-image sources, and animated-material references can be queried
without executing arbitrary actor overlays or submitting graphics commands.
Set OoTEngineConfig.sfxCallback before creation, or use
oot_engine_set_callbacks, to receive the original sound requests including
ID, pitch, volume, position, refresh, and stop events.
Despite its compatibility name, oot_engine_voice_get (and the raw
oot_get_voice_sample) returns decoded mono PCM16 for mapped gameplay SFX and
Link/Navi voice clips. oot_engine_ocarina_note_get supplies sample rates and
loop points for all five Ocarina notes.
For music and the full sound selector, use the handle-taking
oot_engine_audio_* functions. They select the engine's own AudioSeq state in
shared-library builds. oot_engine_audio_sequence_play addresses all 110 ROM
sequences on the main, fanfare, SFX, or sub player;
oot_engine_audio_nature_play applies one of the 19 ambience IO presets. The
immutable raw oot_audio_sfx_catalog_get query enumerates the seven named
banks. Pull canonical interleaved S16 with oot_engine_audio_render_s16, or
F32 converted from that stream with oot_engine_audio_render_f32. Both accept
host rates from 8 to 192 kHz and allocate no memory while rendering.
Call oot_engine_audio_sequence_prewarm for every track the application may
start before opening the device (the playground prewarms all 110), so first-use
VADPCM decoding stays out of the real-time callback.
Callbacks run synchronously inside the tick. Queue events for the engine audio
thread instead of calling the API recursively. Serialize engine audio control,
state, rendering, and gameplay calls; contention returns
OOT_ENGINE_RESULT_BUSY rather than waiting inside the library.
- Shared-library builds advertise
OOT_ENGINE_CAPABILITY_MULTI_INSTANCEand isolate writable native state per engine. Each engine owns one Link. - Static archives advertise
OOT_ENGINE_CAPABILITY_PROCESS_SINGLETON; a second engine returnsOOT_ENGINE_RESULT_SINGLETON_IN_USE. - The raw lifecycle API is process-global. Do not mix it with active engines.
- Calls must be serialized on one gameplay thread.
- Concurrent or callback-reentrant wrapper calls return
OOT_ENGINE_RESULT_BUSY. - Deleting/recreating Link or switching age despawns helper actors and host targets; recreate host attention targets afterward.
OoTLinkState.actionis populated from a curated set of named Player action functions; unclassified actions reportOOT_ACTION_OTHER.animIdis the stable one-based identity of the activelink_animetionentry andanimFrameis its current frame; zero means unknown.- The raw
oot_global_inithas no fallible result value. Use the versioned wrapper for explicit result codes and owned frame buffers.
make -C test playground
make -C test engine_api_test
./test/playground /path/to/oot.z64
./test/playground /path/to/oot.z64 --suite 1000
./test/engine_api_test /path/to/oot.z64Press F9 or Tab in the playground for equipment, items, worlds, audio, rendering diagnostics, fixed-step control, and its 18 ROM-scene presets.