diff --git a/CHANGELOG.md b/CHANGELOG.md index 23bb114..4766a11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,11 +38,18 @@ patch release must remain ABI-compatible with its minor line. - The selected decompilation configuration and default 60 ms simulation step now match the exercised PAL 1.1 ROM. +- Internal libultra compatibility functions now use liboot-prefixed symbols, + preventing collisions when a static host supplies the same N64 functions, + including the incompatible `sins` and `coss` helpers used by SM64 ports. - Public documentation now describes liboot as a host-driven Link runtime and separates current capabilities from host responsibilities and limitations. ### Fixed +- The checked engine SFX player now accepts the documented negative pan range, + so callers can place sounds left of center. +- Repositioning Link now clears pre-warp momentum and synchronizes his internal + facing fields, preventing movement from leaking across host-owned warps. - Relocatable `pkg-config` metadata and installed-package checks now support multi-component library directories, custom include and documentation roots, install-prefix overrides, and multi-config CMake generators. diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e4c5df..ce11223 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,19 @@ target_compile_definitions(oot PRIVATE PLATFORM_GC=0 PLATFORM_IQUE=0 F3DEX_GBI_2 + # Keep decompilation-era compatibility functions private to liboot. Static + # hosts such as SM64 ports commonly provide functions with the original + # libultra names, so leaving those names global makes otherwise valid + # integrations fail at the final link step. + osCreateMesgQueue=liboot_internal_osCreateMesgQueue + osSendMesg=liboot_internal_osSendMesg + osRecvMesg=liboot_internal_osRecvMesg + guPerspective=liboot_internal_guPerspective + guMtxF2L=liboot_internal_guMtxF2L + guMtxIdent=liboot_internal_guMtxIdent + guMtxIdentF=liboot_internal_guMtxIdentF + sins=liboot_internal_sins + coss=liboot_internal_coss ) if(BUILD_SHARED_LIBS) target_compile_definitions(oot PRIVATE LIBOOT_MULTI_INSTANCE=1) @@ -204,6 +217,21 @@ if(BUILD_TESTING) COMMAND liboot_engine_init_test ) + if(NOT BUILD_SHARED_LIBS) + # Static game ports often carry their own libultra compatibility + # layer. This executable deliberately owns names that have collided + # in real hosts and must still link the complete engine. + add_executable(liboot_static_namespace_test + test/static_namespace_test.c + ) + liboot_configure_test(liboot_static_namespace_test) + target_link_libraries(liboot_static_namespace_test PRIVATE liboot::oot) + add_test( + NAME static-host-symbol-namespace + COMMAND liboot_static_namespace_test + ) + endif() + # rom_util.c is deliberately tested directly because it is an internal, # hidden-symbol parser rather than part of the installed ABI. add_executable(liboot_rom_util_test diff --git a/Makefile b/Makefile index b88784a..662c300 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,16 @@ PYTHON3 ?= python3 OOT_DEFINES := -DLIBOOT_HOST_BUILD=1 -D_LANGUAGE_C -DNON_MATCHING -DAVOID_UB -DDEBUG_FEATURES=0 \ -DOOT_VERSION=PAL_1_1 -DOOT_REVISION=1 \ -DPLATFORM_N64=1 -DPLATFORM_GC=0 -DPLATFORM_IQUE=0 \ - -DF3DEX_GBI_2 + -DF3DEX_GBI_2 \ + -DosCreateMesgQueue=liboot_internal_osCreateMesgQueue \ + -DosSendMesg=liboot_internal_osSendMesg \ + -DosRecvMesg=liboot_internal_osRecvMesg \ + -DguPerspective=liboot_internal_guPerspective \ + -DguMtxF2L=liboot_internal_guMtxF2L \ + -DguMtxIdent=liboot_internal_guMtxIdent \ + -DguMtxIdentF=liboot_internal_guMtxIdentF \ + -Dsins=liboot_internal_sins \ + -Dcoss=liboot_internal_coss CFLAGS := -g -Wall -Wno-unused-function -Wno-unused-variable \ -fno-strict-aliasing -funsigned-char -fPIC -fvisibility=hidden \ -DOOT_LIB_EXPORT -DLIBOOT_MULTI_INSTANCE=1 $(OOT_DEFINES) \ diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 7665c55..35e8abc 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -608,6 +608,9 @@ Immutable sequence/SFX catalogs and the Ocarina song table remain on the low-level API. Raw mutable audio calls have no engine selector and must not be used as controls for an `OoTEngine`. +`oot_engine_audio_sfx_play` uses pan `-1.0` for full left, `0.0` for center, +and `1.0` for full right; volume is `0.0..1.0`. + --- # Low-level API (`liboot.h`) diff --git a/docs/ENGINE_INTEGRATION.md b/docs/ENGINE_INTEGRATION.md index 0860eee..a3d9a06 100644 --- a/docs/ENGINE_INTEGRATION.md +++ b/docs/ENGINE_INTEGRATION.md @@ -72,6 +72,10 @@ The exported target supplies the installed include path, so source uses it is not a system location. The installation also provides `liboot.pc`, so a non-CMake host can use `pkg-config --cflags --libs liboot`. +The static archive namespaces its internal libultra message-queue and +perspective helpers. A host may provide the original N64 function names without +creating duplicate symbols at link time. + For an engine plugin, ship the native library beside the executable or in the engine's platform-specific native-library directory. The CMake project has platform-aware shared/static target definitions, but published prebuilt @@ -415,10 +419,10 @@ flags, lock-on and water. Useful details: offset and remain independent of host pointer addresses and ASLR. `oot_engine_link_set_pose` directly changes position and facing without -recreating Link. It intentionally preserves the current action state; pair it -with `oot_engine_link_freeze` when the host needs a clean warp. Deleting and -recreating Link remains useful for a complete gameplay reset and invalidates -helper actors and host targets. +recreating Link. It clears linear and per-axis velocity but intentionally +preserves the current action state; pair it with `oot_engine_link_freeze` when +the host needs a clean warp. Deleting and recreating Link remains useful for a +complete gameplay reset and invalidates helper actors and host targets. Equipment combinations are clamped by the game. In particular, adult and child have different valid swords, shields and items. The wrapper returns @@ -651,6 +655,7 @@ OoTResult result = oot_engine_audio_render_s16( Use `oot_audio_sequence_count/name/get_info` for the 110-entry music selector and `oot_audio_sfx_catalog_count/get` for all seven SFX banks; these catalog queries are immutable. Start catalog sounds through `oot_engine_audio_sfx_play`. +Its pan range is `-1.0` (left) through `0.0` (center) to `1.0` (right). The four players mirror main BGM, fanfare, SFX and secondary BGM. Serialize all calls on an engine against its render callback; concurrent calls report `OOT_ENGINE_RESULT_BUSY`. Shared builds preserve separate AudioSeq state per diff --git a/src/liboot.c b/src/liboot.c index 20ccd24..84b04f4 100644 --- a/src/liboot.c +++ b/src/liboot.c @@ -888,8 +888,9 @@ void oot_link_delete( int32_t linkId ) s_linkFrozen = false; } -/* liboot vNEXT: reposition Link in place. Snap prevPos/home to the new spot so - the next update does not treat the move as a one-frame velocity spike. */ +/* liboot vNEXT: reposition Link in place. Snap prevPos/home to the new spot and + clear the old motion so a host-owned warp cannot leak pre-warp momentum into + the destination. The current action is intentionally preserved. */ bool oot_link_set_pose( int32_t linkId, float x, float y, float z, int16_t yaw ) { if( linkId != 0 || !s_state.player ) return false; @@ -899,8 +900,17 @@ bool oot_link_set_pose( int32_t linkId, float x, float y, float z, int16_t yaw ) player->actor.world.pos.z = z; player->actor.prevPos = player->actor.world.pos; player->actor.home.pos = player->actor.world.pos; + player->actor.velocity.x = 0.0f; + player->actor.velocity.y = 0.0f; + player->actor.velocity.z = 0.0f; + player->actor.speed = 0.0f; + player->speedXZ = 0.0f; + player->pushedSpeed = 0.0f; player->actor.shape.rot.y = yaw; player->actor.world.rot.y = yaw; + player->yaw = yaw; + player->parallelYaw = yaw; + player->pushedYaw = yaw; liboot_world_events_observe_pose( y ); return true; } diff --git a/src/liboot.h b/src/liboot.h index c3467bb..1b65235 100644 --- a/src/liboot.h +++ b/src/liboot.h @@ -663,9 +663,10 @@ extern OOT_LIB_FN void oot_link_delete( int32_t linkId ); /* liboot vNEXT: move Link in place without the delete/recreate dance. Sets his world position and facing yaw (binary angle) directly, and snaps the - previous-position/home anchors so the next tick does not interpolate a huge - step. Does NOT reset action state; combine with oot_link_freeze for a clean - reposition. Returns false for a bad id or no live Link. */ + previous-position/home anchors and clears velocity so the next tick does not + interpolate a huge step or retain pre-warp momentum. Does NOT reset action + state; combine with oot_link_freeze for a clean reposition. Returns false + for a bad id or no live Link. */ extern OOT_LIB_FN bool oot_link_set_pose( int32_t linkId, float x, float y, float z, int16_t yaw ); /* liboot vNEXT: freeze/unfreeze Link's simulation. While frozen, oot_link_tick diff --git a/src/liboot_engine.c b/src/liboot_engine.c index b99ef37..2855406 100644 --- a/src/liboot_engine.c +++ b/src/liboot_engine.c @@ -2679,7 +2679,7 @@ OoTResult oot_engine_audio_sfx_play(OoTEngine *engine, uint16_t sfxId, float pan, float volume) { OoTResult result; - if (!isfinite(pan) || !isfinite(volume) || pan < 0.0f || pan > 1.0f || + if (!isfinite(pan) || !isfinite(volume) || pan < -1.0f || pan > 1.0f || volume < 0.0f || volume > 1.0f) return OOT_ENGINE_RESULT_INVALID_ARGUMENT; result = engine_lock(engine); diff --git a/src/liboot_engine.h b/src/liboot_engine.h index c4a9636..52b860e 100644 --- a/src/liboot_engine.h +++ b/src/liboot_engine.h @@ -466,8 +466,9 @@ extern OOT_LIB_FN OoTResult oot_engine_link_damage(OoTEngine *engine, int16_t am extern OOT_LIB_FN OoTResult oot_engine_link_set_magic(OoTEngine *engine, uint8_t level, int16_t amount); -/* liboot vNEXT: move Link in place (position + facing yaw) without recreating - him; combine with oot_engine_link_freeze for a clean reposition. */ +/* liboot vNEXT: move Link in place (position + facing yaw), clearing velocity + without recreating him; combine with oot_engine_link_freeze for a clean + reposition while preserving action state. */ extern OOT_LIB_FN OoTResult oot_engine_link_set_pose(OoTEngine *engine, float x, float y, float z, int16_t yaw); /* liboot vNEXT: freeze/unfreeze Link's simulation; a frozen Link still renders. */ @@ -671,6 +672,7 @@ extern OOT_LIB_FN OoTResult oot_engine_audio_render_f32( extern OOT_LIB_FN OoTResult oot_engine_audio_render_s16( OoTEngine *engine, int16_t *stereo, uint32_t frames, uint32_t sampleRate, uint32_t *outFrames); +/* pan is -1 (left) through 0 (center) to 1 (right). */ extern OOT_LIB_FN OoTResult oot_engine_audio_sfx_play( OoTEngine *engine, uint16_t sfxId, float pan, float volume); extern OOT_LIB_FN OoTResult oot_engine_audio_sfx_stop( diff --git a/test/engine_api_test.c b/test/engine_api_test.c index a587bd6..bc33be3 100644 --- a/test/engine_api_test.c +++ b/test/engine_api_test.c @@ -650,6 +650,31 @@ int main(int argc, char **argv) input.stickY = 0.0f; } + if (frame_is_sane(frame)) { + float poseX = frame->link.position[0]; + float poseY = frame->link.position[1]; + float poseZ = frame->link.position[2]; + + ok &= expect_result("freeze before clean pose", + oot_engine_link_freeze(engine, 1u), + OOT_ENGINE_RESULT_OK); + ok &= expect_result("clean Link pose", oot_engine_link_set_pose( + engine, poseX, poseY, poseZ, 0x1234), + OOT_ENGINE_RESULT_OK); + ok &= expect_result("clean pose frame", + oot_engine_step(engine, &input, &frame), + OOT_ENGINE_RESULT_OK); + ok &= frame_is_sane(frame) && + fabsf(frame->link.velocity[0]) < 0.001f && + fabsf(frame->link.velocity[1]) < 0.001f && + fabsf(frame->link.velocity[2]) < 0.001f && + fabsf(frame->link.linearVelocity) < 0.001f && + frame->link.faceAngle == 0x1234; + ok &= expect_result("unfreeze after clean pose", + oot_engine_link_freeze(engine, 0u), + OOT_ENGINE_RESULT_OK); + } + ok &= expect_result("child age", oot_engine_link_set_age(engine, OOT_AGE_CHILD), OOT_ENGINE_RESULT_OK); ok &= expect_result("stale target", oot_engine_target_move( diff --git a/test/engine_limits_test.c b/test/engine_limits_test.c index b8e38d6..4670dfd 100644 --- a/test/engine_limits_test.c +++ b/test/engine_limits_test.c @@ -171,6 +171,14 @@ int main(void) oot_engine_scene_get_dropped_triangles(NULL, &dropped) == OOT_ENGINE_RESULT_INVALID_ARGUMENT && dropped == 0u); + ok &= expect("checked audio accepts left pan", + oot_engine_audio_sfx_play( + (OoTEngine *)(uintptr_t)1u, 0u, -0.5f, 1.0f) == + OOT_ENGINE_RESULT_NOT_INITIALIZED); + ok &= expect("checked audio rejects pan below left", + oot_engine_audio_sfx_play( + (OoTEngine *)(uintptr_t)1u, 0u, -1.01f, 1.0f) == + OOT_ENGINE_RESULT_INVALID_ARGUMENT); if (!ok) { return 1; diff --git a/test/static_namespace_test.c b/test/static_namespace_test.c new file mode 100644 index 0000000..c1347ee --- /dev/null +++ b/test/static_namespace_test.c @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later + * Copyright (C) 2026 Cycl0o0 + */ + +#include "liboot_engine.h" + +#include + +/* These signatures are intentionally host-local. Only the global symbol names + * matter: liboot's static archive must not define the unprefixed fallbacks. */ +void osCreateMesgQueue(void) +{ +} + +int32_t osSendMesg(void) +{ + return 0; +} + +int32_t osRecvMesg(void) +{ + return 0; +} + +void guPerspective(void) +{ +} + +void guMtxF2L(void) +{ +} + +void guMtxIdent(void) +{ +} + +void guMtxIdentF(void) +{ +} + +/* SM64 ports expose these names with floating-point return values. OoT's + * libultra functions return signed fixed-point values, so accidentally + * resolving either call to the host is an ABI mismatch, not just a duplicate + * implementation. */ +float sins(int16_t angle) +{ + (void)angle; + return -0.25f; +} + +float coss(int16_t angle) +{ + (void)angle; + return -0.5f; +} + +int16_t liboot_internal_sins(uint16_t angle); +int16_t liboot_internal_coss(uint16_t angle); + +int main(void) +{ + if (oot_engine_api_version() != OOT_ENGINE_API_VERSION) { + return 1; + } + if (liboot_internal_sins(0x4000u) < 32760 || + liboot_internal_coss(0u) < 32760) { + return 2; + } + return 0; +}