diff --git a/.gitmodules b/.gitmodules index 7098d9d08..1a4f40487 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "OTRExporter"] path = OTRExporter url = https://github.com/harbourmasters/OTRExporter +[submodule "miniaudio"] + path = soh/include/miniaudio + url = https://github.com/mackron/miniaudio diff --git a/run-clang-format.ps1 b/run-clang-format.ps1 index 39905f9ed..c4f10e60a 100644 --- a/run-clang-format.ps1 +++ b/run-clang-format.ps1 @@ -36,7 +36,8 @@ if (-not (Test-Path $clangFormatFilePath) -or ($currentVersion -ne $requiredVers $basePath = (Resolve-Path .).Path $files = Get-ChildItem -Path $basePath\soh -Recurse -File ` - | Where-Object { ($_.Extension -eq '.c' -or $_.Extension -eq '.cpp' -or ` + | Where-Object { (-not ($_.FullName -like "*\soh\include\miniaudio*")) -and ` + ($_.Extension -eq '.c' -or $_.Extension -eq '.cpp' -or ` (($_.Extension -eq '.h' -or $_.Extension -eq '.hpp') -and ` (-not ($_.FullName -like "*\soh\src\*" -or $_.FullName -like "*\soh\include\*")))) -and ` (-not ($_.FullName -like "*\soh\assets\*")) } diff --git a/run-clang-format.sh b/run-clang-format.sh index 20129e63d..0ee4126a4 100755 --- a/run-clang-format.sh +++ b/run-clang-format.sh @@ -26,4 +26,4 @@ # and pass it as an argument to clang-format # verbose to print files being formatted and X out of Y status -find soh -type f \( -name "*.c" -o -name "*.cpp" -o \( \( -name "*.h" -o -name "*.hpp" \) ! -path "soh/src/*" ! -path "soh/include/*" \) \) ! -path "soh/assets/*" -print0 | xargs -0 clang-format-14 -i --verbose +find soh -type f \( ! -path "soh/include/miniaudio/*" -a \( -name "*.c" -o -name "*.cpp" -o \( \( -name "*.h" -o -name "*.hpp" \) ! -path "soh/src/*" ! -path "soh/include/*" \) \) \) ! -path "soh/assets/*" -print0 | xargs -0 clang-format-14 -i --verbose diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index 7277b2dfb..b51689ef3 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -153,6 +153,11 @@ else() list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/(Darwin|SAPI).*") endif() +# handle accessible audio engine removals +if (CMAKE_SYSTEM_NAME MATCHES "NintendoSwitch|CafeOS") + list(FILTER soh__Enhancements EXCLUDE REGEX "soh/Enhancements/accessible-actors/") +endif() + # soh/Extractor {{{ list(FILTER ship__ EXCLUDE REGEX "soh/Extractor/*") diff --git a/soh/include/miniaudio b/soh/include/miniaudio new file mode 160000 index 000000000..350784a94 --- /dev/null +++ b/soh/include/miniaudio @@ -0,0 +1 @@ +Subproject commit 350784a9467a79d0fa65802132668e5afbcf3777 diff --git a/soh/soh/Enhancements/accessible-actors/AccessibleActorList.cpp b/soh/soh/Enhancements/accessible-actors/AccessibleActorList.cpp new file mode 100644 index 000000000..90a9c313f --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/AccessibleActorList.cpp @@ -0,0 +1,1005 @@ +#include "ActorAccessibility.h" +#include "soh/OTRGlobals.h" + +#include +#include + +#include +#include +#include +#include +#include + +extern "C" { +#include "overlays/actors/ovl_Obj_Switch/z_obj_switch.h" +#include "overlays/actors/ovl_Bg_Bdan_Switch/z_bg_bdan_switch.h" +#include "overlays/actors/ovl_Boss_Goma/z_boss_goma.h" +#include "overlays/actors/ovl_En_Karebaba/z_en_karebaba.h" +#include "overlays/actors/ovl_En_Box/z_en_box.h" +#include "overlays/actors/ovl_Obj_Syokudai/z_obj_syokudai.h" +#include "overlays/actors/ovl_En_Dog/z_en_dog.h" + +void EnBox_WaitOpen(EnBox*, PlayState*); +void EnKarebaba_DeadItemDrop(EnKarebaba*, PlayState*); +void EnDog_FollowPlayer(EnDog*, PlayState*); +s8 EnDog_CanFollow(EnDog*, PlayState*); +} + +// User data for the general helper VA. +typedef struct { + s16 currentScene; + s8 currentRoom; + bool currentRoomClear; +} GeneralHelperData; + +typedef struct { + f32 linearVelocity; + int framesUntilChime; +} AudioCompassData; + +typedef struct { + int framesUntilAboveChime; +} SwitchData; + +void accessible_en_pickups(AccessibleActor* actor) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_NUTS_DAMAGE, false); +} + +void accessible_test(AccessibleActor* actor) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_TRAP_BOUND, false); +} + +void accessible_grotto(AccessibleActor* actor) { + if ((actor->actor->params & 0x300) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DROP_FALL, false); + } +} + +void accessible_torches(AccessibleActor* actor) { + ObjSyokudai* torche = (ObjSyokudai*)actor->actor; + // temporary torches + if ((actor->actor->params) == 4230 || (actor->actor->params) == 4220 || (actor->actor->params) == 4227 || + (actor->actor->params) == 4380 || actor->actor->params == 4321) { + if (torche->litTimer != 0) { + actor->policy.volume = 0.1; + if ((actor->frameCount & 31) != 0) { + return; + } + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_BOMB_IGNIT, false); + } else { + if (actor->policy.volume != 1.0) { + actor->policy.volume = 1.0; + } + } + if ((actor->frameCount & 31) != 0) { + return; + } + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_BOMB_IGNIT, false); + } + if ((actor->frameCount & 31) != 0) { + return; + } + + // unlit permanent torches + if ((actor->actor->params) == 8192) { + if (torche->litTimer == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_BOMB_IGNIT, false); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_ANUBIS_FIRE, false); + } + } + // lit permanent torches + if ((actor->actor->params) == 9216 || (actor->actor->params) == 962) { + + actor->policy.volume = 0.5; + actor->policy.distance = 200.0; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_ANUBIS_FIRE, false); + } +} + +void accessible_hasi(AccessibleActor* actor) { + if ((actor->actor->params) == 0) { + actor->policy.ydist = 1000; + actor->policy.distance = 1000; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_OCTAROCK_ROCK, false); + } else if ((actor->actor->params) == 1) { + actor->policy.ydist = 1000; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_DAMAGE, false); + } +} +bool accessible_switch_init(AccessibleActor* actor) { + SwitchData* data = (SwitchData*)malloc(sizeof(SwitchData)); + if (data == nullptr) + return false; // failure to allocate memory. + data->framesUntilAboveChime = 0; + actor->userData = (void*)data; + return true; +} +void accessible_switch_cleanup(AccessibleActor* actor) { + free(actor->userData); +} + +void accessible_switch(AccessibleActor* actor) { + SwitchData* data = (SwitchData*)actor->userData; + + Player* player = GET_PLAYER(actor->play); + ObjSwitch* sw = (ObjSwitch*)actor->actor; + Vec3f& scale = actor->actor->scale; + if ((actor->actor->params & 7) == 0) { + if (actor->xyzDistToPlayer > 800) { + return; + } + if (scale.y >= 33.0f / 200.0f) { + if (actor->play->sceneNum == 0 && actor->play->roomCtx.curRoom.num == 5 && actor->xzDistToPlayer < 20) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIAMOND_SWITCH, false); + } + if ((actor->frameCount & 31) != 0) { + return; + } + ActorAccessibility_PlaySoundForActor(actor, 1, NA_SE_EV_FOOT_SWITCH, false); + } + } else if ((actor->frameCount & 31) != 0) { + return; + } else if ((actor->actor->params & 7) == 1) { + if (actor->xyzDistToPlayer > 800) { + return; + } + if (scale.y >= 33.0f / 200.0f) { //(!(Flags_GetSwitch(actor->play, (actor->params >> 8 & 0x3F)))) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_HAMMER_HIT, false); + } + } else if ((actor->actor->params & 7) == 2) { + if (sw->eyeTexIndex == 0) { //(!(Flags_GetSwitch(actor->play, (actor->params >> 8 & 0x3F)))) + // make it only play for open eye + actor->policy.aimAssist.isProvider = true; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_FOOT_SWITCH, false); + } + } else { + if (actor->xyzDistToPlayer > 800) { + return; + } + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIAMOND_SWITCH, false); + } +} + +void accessible_larva(AccessibleActor* actor) { + if (actor->actor->bgCheckFlags == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_GOMA_BJR_EGG1, false); + } +} + +void accessible_eiyer(AccessibleActor* actor) { + if (GET_PLAYER(actor->play)->actor.world.pos.y > actor->actor->world.pos.y - 8) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_FISHING_REEL_SLOW, false); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_FISHING_REEL_HIGH, false); + } +} + +void accessible_door(AccessibleActor* actor) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_OC_DOOR_OPEN, false); +} + +void accessible_maruta(AccessibleActor* actor) { + if (actor->actor->params == 1) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_LAND_LADDER, false); + } +} + +void accessible_area_change(AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + actor->policy.distance = 1500; + actor->policy.ydist = 2000; + + if (actor->yDistToPlayer > 500.0 && actor->sceneIndex != 96 && actor->play->sceneNum != 81 && + actor->play->sceneNum != 82) { + return; + } + /*switch (actor->sceneIndex) { + case 85 || 91: + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SARIA_MELODY, false); + case 81: + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_INGO_HORSE_NEIGH, false); + case 0: + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_FANTOM_WARP_L, false); + }*/ + + // hyrule field attenuation + if (actor->play->sceneNum == 81) { + if (actor->xzDistToPlayer > 700) { + actor->policy.distance = actor->xzDistToPlayer * 1.2; + if (actor->xzDistToPlayer > 8000) { + return; + } + } else { + actor->policy.distance = 1500; + if (actor->xzDistToPlayer > 1500) { + return; + } + } + } + // kakariko village attenuation + else if (actor->play->sceneNum == 82) { + if (actor->sceneIndex == 83 || actor->sceneIndex == 81 || actor->sceneIndex == 96) { + actor->policy.runsAlways = true; + actor->policy.ydist = 5000; + if (actor->xzDistToPlayer > 700) { + if (actor->sceneIndex == 81) { + actor->policy.distance = actor->xyzDistToPlayer * 1.4; + } else { + actor->policy.distance = actor->xyzDistToPlayer * 1.2; + } + if (actor->xzDistToPlayer > 8000) { + return; + } + } else { + actor->policy.distance = 1500; + if (actor->xzDistToPlayer > 1500) { + return; + } + } + } else if (actor->sceneIndex == 8) { + if (!(((gSaveContext.eventChkInf[6]) >> (7)) & 1)) + return; + } else { + actor->policy.ydist = 500; + actor->policy.distance = 1000; + if (actor->xzDistToPlayer > 1000) { + return; + } + } + } + + else if (actor->play->sceneNum == 91 || actor->play->sceneNum == 69 || actor->play->sceneNum == 70) { + actor->policy.distance = 1000; + if (actor->xzDistToPlayer > 1000) { + return; + } + } + /* if (actor->play->sceneNum <= 11) { + actor->policy.distance = 500; + }*/ + else { + if (actor->xzDistToPlayer > 1500) { + return; + } + } + if (actor->sceneIndex == 85 || actor->sceneIndex == 91) { + if (actor->play->sceneNum == 91 && gSaveContext.entranceIndex != 1504 && gSaveContext.entranceIndex != 1246) { + return; + } + if (actor->play->sceneNum == 85 && actor->world.pos.y < 0) { + + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_HORSE_RUN_LEVEL, false); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SARIA_MELODY, false); + } + // kokiri forest and lost woods + } else if (actor->play->sceneNum >= 17 && actor->play->sceneNum <= 25) { + return; // dont check for entrances while in boss rooms + } else if (actor->sceneIndex == 81) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_HORSE_RUN_LEVEL, false); + // hyrule field + } else if (actor->sceneIndex == 10 && actor->play->sceneNum != 85) { // temp + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_OC_DOOR_OPEN, false); + } else if (actor->sceneIndex <= 11) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_FANTOM_WARP_L, false); + // dungeons + } else if (actor->sceneIndex >= 27 && actor->sceneIndex <= 29) { + if (actor->play->sceneNum >= 32 && actor->play->sceneNum <= 34) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_HORSE_RUN_LEVEL, false); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SMALL_DOG_BARK, false); + } + } else if (actor->sceneIndex >= 30 && actor->sceneIndex <= 33) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SMALL_DOG_BARK, false); + // market sound + } else if ((actor->sceneIndex >= 34 && actor->sceneIndex <= 36) || actor->sceneIndex == 67) { + if (actor->play->sceneNum == 67) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SMALL_DOG_BARK, false); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_STONE_BOUND, false); + } + + // ToT sound + } else if (actor->sceneIndex == 69 || actor->sceneIndex == 70) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_MUSI_SINK, false); + } else if (actor->sceneIndex == 82) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_CHICKEN_CRY_M, false); + // kakariko sound + } else if (actor->sceneIndex == 83) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_PO_APPEAR, false); + // graveyard sound + } else if (actor->sceneIndex == 84 || actor->sceneIndex == 88 || + actor->sceneIndex == 89) { // last one is zora fountain maybe seperate? + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_RIVER_STREAM_S, false); + // zora sound + } else if (actor->sceneIndex == 86) { // might not need to exist + // forest medow sound + } else if (actor->sceneIndex == 87) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_WATER_WALL, false); + // Lake Hylia sound + } else if (actor->sceneIndex == 90 || actor->sceneIndex == 93) { // gerudo valley and fortress + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_GERUDOFT_BREATH, false); + // gerudo valley sound + } else if (actor->sceneIndex == 92 || actor->sceneIndex == 94) { // haunted wasteland and desert colosus + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SAND_STORM, false); + + } else if (actor->sceneIndex == 100 || actor->sceneIndex == 95) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_BRIDGE_OPEN, false); + // Hyrule Castle sound + } else if (actor->sceneIndex == 96) { + + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_DODO_K_ROLL, false); + // DMT sound + } else if (actor->sceneIndex == 97) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_DODO_K_LAVA, false); + // DMC sound + } else if (actor->sceneIndex == 98) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_DARUNIA_HIT_BREAST, false); + // Goron City + } else if (actor->sceneIndex == 99) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_COW_CRY, false); + // Lon Lon + } else if (actor->sceneIndex >= 17 && actor->sceneIndex <= 25) { + return; // boss rooms + } else { + actor->policy.distance = 500; + if (actor->play->sceneNum == 83) { + actor->policy.ydist = 0; + } + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_OC_DOOR_OPEN, false); + } +} + +void accessible_231_dekus(AccessibleActor* actor) { + if (actor->actor->params == 1) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_NUTS_FAINT, false); + ActorAccessibility_SetSoundPitch(actor, 0, 1.0); + } else if (actor->actor->params == 2) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_NUTS_FAINT, false); + ActorAccessibility_SetSoundPitch(actor, 0, 0.5); + } else if (actor->actor->params == 3) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_NUTS_FAINT, false); + ActorAccessibility_SetSoundPitch(actor, 0, 1.5); + } else { + return; + } +} + +void accessible_hana(AccessibleActor* actor) { + if (actor->actor->params == 1) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_OCTAROCK_ROCK, false); + } else if (actor->actor->params == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIG_UP, false); + } +} + +void accessible_climable(AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + f32 waterLoc = player->actor.yDistToWater + player->actor.world.pos.y; + if (actor->world.pos.y < waterLoc) { + actor->world.pos.y = waterLoc; + } + if (actor != nullptr && actor->yDistToPlayer < 80) + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_LAND_LADDER, false); +} + +void accessible_en_guard(AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + f32 guardsfx = NA_SE_IT_SWORD_IMPACT; + if (fabs(actor->actor->world.pos.x - player->actor.world.pos.x) > + fabs(actor->actor->world.pos.z - player->actor.world.pos.z)) { + if (fabs(actor->actor->shape.rot.y - 16384) < 1000) { + if (actor->actor->world.pos.x < player->actor.world.pos.x) { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 0.2); + } + } else if ((actor->actor->shape.rot.y + 16384) < 1000) { + if (actor->actor->world.pos.x < player->actor.world.pos.x) { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 0.2); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 1.0); + } + } else { + if (fabs(actor->actor->shape.rot.y) < 1000) { + if (actor->actor->world.pos.z < player->actor.world.pos.z) { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 0.2); + } + } else if (fabs(actor->actor->shape.rot.y + 32768) < 1000) { + if (actor->actor->world.pos.z < player->actor.world.pos.z) { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 0.2); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx, false); + ActorAccessibility_SetSoundPitch(actor, 0, 1.0); + } + } +} + +void accessible_en_dogs(AccessibleActor* actor) { + EnDog* dog = (EnDog*)actor->actor; + if (EnDog_CanFollow(dog, actor->play) == 1) { + dog->actionFunc = EnDog_FollowPlayer; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIAMOND_SWITCH, false); + ActorAccessibility_SetSoundPitch(actor, 0, 1.0); + } + if ((actor->frameCount & 31) != 0) { + return; + } + if (actor->actor->params == 608 || actor->actor->params == 336 || actor->actor->params == 304 || + actor->actor->params == 3088 || actor->actor->params == 2576 || actor->actor->params < 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SMALL_DOG_BARK, false); + + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SMALL_DOG_BARK, false); + ActorAccessibility_SetSoundPitch(actor, 0, 0.5); + } +} + +void accessible_goma(AccessibleActor* actor) { + BossGoma* goma = (BossGoma*)actor->actor; + if (goma->visualState == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIAMOND_SWITCH, false); + } +} + +void accessible_sticks(AccessibleActor* actor) { + EnKarebaba* baba = (EnKarebaba*)actor->actor; + + if (baba->actionFunc != EnKarebaba_DeadItemDrop) + return; + if (actor->actor->flags == 80) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_NUTS_DAMAGE, false); + } +} + +void accessible_cucco(AccessibleActor* actor) { + if (actor->actor->params == 14) { + + } else if (actor->actor->params == 13) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_CHICKEN_CRY_N, false); + ActorAccessibility_SetSoundPitch(actor, 0, 1.5); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_CHICKEN_CRY_N, false); + } +} + +bool accessible_general_helper_init(AccessibleActor* actor) { + GeneralHelperData* data = (GeneralHelperData*)malloc(sizeof(GeneralHelperData)); + if (data == nullptr) + return false; + data->currentRoom = -1; + data->currentRoomClear = false; + data->currentScene = -1; + + actor->userData = data; + return true; +} + +void accessible_general_helper_cleanup(AccessibleActor* actor) { + free(actor->userData); + actor->userData = nullptr; +} + +void accessible_va_general_helper(AccessibleActor* actor) { + GeneralHelperData* data = (GeneralHelperData*)actor->userData; + if (data->currentScene == actor->play->sceneNum && data->currentRoom != actor->play->roomCtx.curRoom.num) { + ActorAccessibility_AnnounceRoomNumber(actor->play); + data->currentRoom = actor->play->roomCtx.curRoom.num; + data->currentRoomClear = Flags_GetClear(actor->play, data->currentRoom); + } + if (data->currentScene != actor->play->sceneNum) { + ActorAccessibility_InterpretCurrentScene(actor->play); + data->currentScene = actor->play->sceneNum; + data->currentRoom = actor->play->roomCtx.curRoom.num; + data->currentRoomClear = Flags_GetClear(actor->play, data->currentRoom); + } + // Report when a room is completed. + if (!data->currentRoomClear && Flags_GetClear(actor->play, data->currentRoom)) { + data->currentRoomClear = Flags_GetClear(actor->play, data->currentRoom); + ActorAccessibility_AnnounceRoomNumber(actor->play); + } +} +bool accessible_audio_compass_init(AccessibleActor* actor) { + AudioCompassData* data = (AudioCompassData*)malloc(sizeof(AudioCompassData)); + if (data == nullptr) + return false; + data->linearVelocity = 0; + data->framesUntilChime = 0; + + actor->userData = data; + return true; +} +void accessible_audio_compass_cleanup(AccessibleActor* actor) { + free(actor->userData); +} +void accessible_audio_compass(AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + if (player->stateFlags1 & PLAYER_STATE1_Z_TARGETING || player->stateFlags1 & PLAYER_STATE1_CLIMBING_LADDER) + return; + OSContPad* trackerButtonsPressed = + std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetControlDeck())->GetPads(); + AudioCompassData* data = (AudioCompassData*)actor->userData; + bool compassCombo = trackerButtonsPressed != nullptr && trackerButtonsPressed[0].button & BTN_DDOWN && + trackerButtonsPressed[0].button & BTN_L; + actor->world.pos = player->actor.world.pos; + actor->world.pos.z -= 50; + + if (data->framesUntilChime > 0) + data->framesUntilChime--; + if (compassCombo && data->framesUntilChime <= 0) { + + ActorAccessibility_PlaySoundForActor(actor, 0, actor->policy.sound, false); + data->framesUntilChime = 30; + } +} + +void accessible_stick_warning(AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + actor->world.pos = player->actor.world.pos; + actor->world.pos.z -= 50; + if (fabs(player->unk_860 - 25) < 24.0 && player->heldItemId == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_SY_WARNING_COUNT_N, false); + } +} + +void ActorAccessibility_InitActors() { + const int Npc_Frames = 35; + ActorAccessibilityPolicy policy; + ActorAccessibility_InitPolicy(&policy, "Rock", NA_SE_EN_OCTAROCK_ROCK); + ActorAccessibility_AddSupportedActor(ACTOR_EN_ISHI, policy); + + ActorAccessibility_InitPolicy(&policy, "Story NPCs", NA_SE_VO_NA_HELLO_0); + policy.englishName = "Mido"; + policy.n = Npc_Frames; + policy.distance = 1000; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_EN_MD, policy); + policy.englishName = "Malon"; + policy.distance = 500; + ActorAccessibility_AddSupportedActor(ACTOR_EN_MA1, policy); + policy.englishName = "Talon"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_TA, policy); + policy.englishName = "King Zora"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_KZ, policy); + policy.englishName = "Diving Zora"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_DIVING_GAME, policy); + policy.englishName = "Child Zelda"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_ZL4, policy); + policy.englishName = "Ingo"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_IN, policy); + policy.englishName = "Cucco Lady"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_NIW_LADY, policy); + policy.englishName = "Windmill Man"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_FU, policy); + policy.englishName = "Durania"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_DU, policy); + policy.englishName = "Owl"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_OWL, policy); + ActorAccessibility_InitPolicy(&policy, "Catching Guards", accessible_en_guard); + policy.n = 10; + policy.distance = 500; + policy.ydist = 300; + ActorAccessibility_AddSupportedActor(ACTOR_EN_HEISHI1, policy); + ActorAccessibility_AddSupportedActor(ACTOR_EN_HEISHI3, policy); + + ActorAccessibility_InitPolicy(&policy, "Passive Guards", NA_SE_IT_SWORD_IMPACT); + ActorAccessibility_AddSupportedActor(ACTOR_EN_HEISHI2, policy); + ActorAccessibility_AddSupportedActor(ACTOR_EN_HEISHI4, policy); + + ActorAccessibility_InitPolicy(&policy, "Shopkeepers", NA_SE_VO_NA_HELLO_1); + policy.pitch = 0.6; + policy.n = 30; + policy.englishName = "Shooting Gallery Man"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_SYATEKI_MAN, policy); + policy.englishName = "Bombchu Bowling Alley Lady"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_BOM_BOWL_MAN, policy); + policy.englishName = "ShopKeeper"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_OSSAN, policy); + policy.englishName = "Potion Shop Granny"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_DS, policy); + + // general NPCs + ActorAccessibility_InitPolicy(&policy, "Kokiri Child", NA_SE_VO_NB_LAUGH); + policy.n = Npc_Frames; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_EN_KO, policy); + policy.englishName = "Gorons"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_GO2, policy); + policy.englishName = "Saria"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_SA, policy); + policy.englishName = "Graveyard Kid"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_CS, policy); + policy.englishName = "Dampe (Alive)"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_TK, policy); + policy.englishName = "Happy Mask Shop Customer"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_GUEST, policy); + policy.englishName = "Market Npc"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_HY, policy); + policy.englishName = "Girl Chassing Cucco"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_NIW_GIRL, policy); + policy.englishName = "Honey & Darling"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_TG, policy); + policy.englishName = "Haggling Townspeople"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_MU, policy); + policy.englishName = "Skull Kid"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_SKJ, policy); + policy.englishName = "Boss Carpenter"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_TORYO, policy); + policy.englishName = "Carpenters (Kakariko)"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_DAIKU_KAKARIKO, policy); + policy.englishName = "Kakariko Rooftop Man"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_ANI, policy); + policy.englishName = "Cursed Skulltula People"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_SSH, policy); + policy.englishName = "Gossip Stone"; + policy.pitch = 0.75; + ActorAccessibility_AddSupportedActor(ACTOR_EN_GS, policy); + + ActorAccessibility_InitPolicy(&policy, "Dogs", accessible_en_dogs); + policy.n = 1; + ActorAccessibility_AddSupportedActor(ACTOR_EN_DOG, policy); + + ActorAccessibility_InitPolicy(&policy, "Horses", NA_SE_EV_HORSE_NEIGH); + policy.n = 30; + ActorAccessibility_AddSupportedActor(ACTOR_EN_HORSE_NORMAL, policy); + ActorAccessibility_InitPolicy(&policy, "Cows", NA_SE_EV_COW_CRY_LV); + policy.n = 30; + ActorAccessibility_AddSupportedActor(ACTOR_EN_COW, policy); + ActorAccessibility_InitPolicy(&policy, "Cuccos", accessible_cucco); + ActorAccessibility_AddSupportedActor(ACTOR_EN_NIW, policy); + ActorAccessibility_InitPolicy(&policy, "Bush", NA_SE_PL_PULL_UP_PLANT); + ActorAccessibility_AddSupportedActor(ACTOR_EN_KUSA, policy); + ActorAccessibility_InitPolicy(&policy, "Trees", NA_SE_EV_TREE_CUT); + ActorAccessibility_AddSupportedActor(ACTOR_EN_WOOD02, policy); + + ActorAccessibility_InitPolicy(&policy, "Chest", [](AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + EnBox* chest = (EnBox*)actor->actor; + if (chest->actionFunc != EnBox_WaitOpen) + return; + s32 treasureFlag = actor->actor->params & 0x1F; + s8 size; + if (chest->type <= 8 && chest->type >= 5) { + size = 15; // small + } else { + size = 30; // large + } + if (!(treasureFlag >= 20 && treasureFlag < 32)) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_TBOX_UNLOCK, false); + } + // Only chests that are "waiting to be opened" should play a sound. Chests which have not yet appeared (because + // some enemy has not been killed, switch has not been hit, etc) will not be in this action mode. + f32 leftAngle = actor->actor->world.rot.y - 16384; + f32 velocityXRight = Math_SinS(leftAngle); + f32 velocityZRight = Math_CosS(leftAngle); + + f32 frontAngle = actor->actor->world.rot.y; + f32 velocityXFront = Math_SinS(frontAngle); + f32 velocityZFront = Math_CosS(frontAngle); + + f32 xdist = (player->actor.world.pos.x - actor->actor->world.pos.x) * velocityXFront + + (player->actor.world.pos.z - actor->actor->world.pos.z) * velocityZFront; + f32 zdist = fabs((player->actor.world.pos.x - actor->actor->world.pos.x) * velocityXRight + + (player->actor.world.pos.z - actor->actor->world.pos.z) * velocityZRight); + + if ((xdist - size / 2) < 0) { + ActorAccessibility_SetSoundPitch(actor, 0, 0.5); + } else if ((xdist + size / 2) > 0 && zdist < size / 2 && xdist < 150.0) { + ActorAccessibility_PlaySoundForActor(actor, 1, NA_SE_EV_DIAMOND_SWITCH, false); + } + }); + policy.pitch = 1.1; + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_EN_BOX, policy); + ActorAccessibility_InitPolicy(&policy, "Sign", NA_SE_IT_REFLECTION_WOOD); + policy.n = 40; + policy.pitch = 1.6; + policy.distance = 800; + ActorAccessibility_AddSupportedActor(ACTOR_EN_KANBAN, policy); + + // ACTOR_EN_A_OBJ has exactly the same configuration. + ActorAccessibility_AddSupportedActor(ACTOR_EN_A_OBJ, policy); + ActorAccessibility_InitPolicy(&policy, "Large Crate", NA_SE_EV_WOODBOX_BREAK); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_KIBAKO, policy); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_KIBAKO2, policy); + ActorAccessibility_InitPolicy(&policy, "deku stick drops", accessible_sticks); + ActorAccessibility_AddSupportedActor(ACTOR_EN_DEKUBABA, policy); + ActorAccessibility_AddSupportedActor(ACTOR_EN_KAREBABA, policy); + ActorAccessibility_InitPolicy(&policy, "Owl", NA_SE_EN_OWL_FLUTTER); + ActorAccessibility_AddSupportedActor(ACTOR_EN_OWL, policy); + // will probably just get replaced with ghost actors anyways + // ActorAccessibility_AddSupporte dActor(ACTOR_EN_HOLL, "Room Changing Plane", nullptr, 30, 500, 1.0, 1.0, + // NA_SE_EV_STONEDOOR_STOP /*NOT SURE YET*/); + + ActorAccessibility_InitPolicy(&policy, "Ruto", NA_SE_VO_RT_LAUGH_0); + policy.n = 40; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_EN_RU1, policy); + + ActorAccessibility_InitPolicy(&policy, "Bean patch", NA_SE_EN_MUSI_SINK); + policy.n = 60; + policy.distance = 2400; + policy.pitch = 1.3; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_BEAN, policy); + ActorAccessibility_InitPolicy(&policy, "Graveyard Digging Spot", NA_SE_IT_WOODSTICK_BROKEN); + ActorAccessibility_AddSupportedActor(ACTOR_EN_IT, policy); + ActorAccessibility_InitPolicy(&policy, "Collectible", accessible_en_pickups); + policy.n = 40; + policy.pitch = 1.4; + ActorAccessibility_AddSupportedActor(ACTOR_EN_ITEM00, policy); + // TODO better gerudo guard logic + ActorAccessibility_InitPolicy(&policy, "Gerudo Guard", NA_SE_VO_NB_LAUGH); + policy.n = Npc_Frames; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_EN_GE1, policy); + ActorAccessibility_InitPolicy(&policy, "Boulder", NA_SE_EV_ROCK_BROKEN); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_BOMBIWA, policy); + ActorAccessibility_InitPolicy(&policy, "Bronze Boulder", NA_SE_IT_HAMMER_HIT); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_HAMISHI, policy); + ActorAccessibility_InitPolicy(&policy, "Grotto Door", accessible_grotto); + policy.n = 30; + policy.pitch = 1.0; + ActorAccessibility_AddSupportedActor(ACTOR_DOOR_ANA, policy); + ActorAccessibility_InitPolicy(&policy, "Web", NA_SE_EV_WEB_BROKEN); + policy.n = 40; + policy.ydist = 2000; + policy.distance = 2000; + policy.pitch = 1.2; + ActorAccessibility_AddSupportedActor(ACTOR_BG_YDAN_SP, policy); + + ActorAccessibility_InitPolicy(&policy, "Shutter Door", accessible_door); + policy.n = 30; + policy.distance = 1000; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_DOOR_SHUTTER, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT18_SHUTTER, policy); + ActorAccessibility_InitPolicy(&policy, "Switch", accessible_switch); + policy.distance = 2000; + policy.cleanupUserData = accessible_switch_cleanup; + policy.initUserData = accessible_switch_init; + policy.n = 1; + policy.ydist = 200; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_SWITCH, policy); + ActorAccessibility_InitPolicy(&policy, "Jabu Switch", [](AccessibleActor* actor) { + int type = actor->actor->params & 0xFF; + if (type == YELLOW_TALL_1 || type == YELLOW_TALL_2) { + actor->policy.aimAssist.isProvider = true; + } + if ((actor->frameCount & 31) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIAMOND_SWITCH, false); + } + }); + policy.n = 1; + policy.volume = 0.6; + policy.distance = 1000; + policy.ydist = 300; + ActorAccessibility_AddSupportedActor(ACTOR_BG_BDAN_SWITCH, policy); + ActorAccessibility_InitPolicy(&policy, "Jabu Elevator", [](AccessibleActor* actor) { + if ((actor->actor->params & 0xFF) == 2 && actor->xzDistToPlayer > 50) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_LAND_LADDER, false); + } + }); + policy.n = 1; + policy.ydist = 50; + ActorAccessibility_AddSupportedActor(ACTOR_BG_BDAN_OBJECTS, policy); + ActorAccessibility_InitPolicy(&policy, "Ocarina Spots", NA_SE_EV_DIAMOND_SWITCH); + policy.n = 30; + policy.distance = 800; + policy.pitch = 1.1; + policy.ydist = 500; + ActorAccessibility_AddSupportedActor(ACTOR_EN_OKARINA_TAG, policy); + ActorAccessibility_InitPolicy(&policy, "Pushable Block", accessible_test); + policy.n = 30; + policy.distance = 800; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_OSHIHIKI, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT15_RRBOX, policy); + ActorAccessibility_InitPolicy(&policy, "Torch", accessible_torches); + policy.n = 1; + policy.pitch = 1.1; + policy.distance = 800; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_SYOKUDAI, policy); + ActorAccessibility_InitPolicy(&policy, "Deku Tree Moving Platform", accessible_hasi); + // policy.volume = 1.3; + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_BG_YDAN_HASI, policy); + ActorAccessibility_InitPolicy(&policy, "Pot", NA_SE_EV_POT_BROKEN); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_TSUBO, policy); + // ActorAccessibility_InitPolicy(&policy, "Deku Tree Entrance", NA_SE_EV_FANTOM_WARP_L); + // policy.distance = 5000; + // ActorAccessibility_AddSupportedActor(ACTOR_BG_TREEMOUTH, policy); + ActorAccessibility_InitPolicy(&policy, "Platform collapsable", NA_SE_EV_BLOCK_SHAKE); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_LIFT, policy); + ActorAccessibility_InitPolicy(&policy, "Ladder in Slingshot Room", accessible_maruta); + ActorAccessibility_AddSupportedActor(ACTOR_BG_YDAN_MARUTA, policy); + ActorAccessibility_InitPolicy(&policy, "bombable wall", NA_SE_EN_OCTAROCK_ROCK); + ActorAccessibility_AddSupportedActor(ACTOR_BG_BREAKWALL, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_BOMBWALL, policy); + ActorAccessibility_InitPolicy(&policy, "231 dekus", accessible_231_dekus); + policy.distance = 2000; + policy.n = 50; + ActorAccessibility_AddSupportedActor(ACTOR_EN_HINTNUTS, policy); + ActorAccessibility_InitPolicy(&policy, "Flame Circle", NA_SE_EV_FIRE_PILLAR); + ActorAccessibility_AddSupportedActor(ACTOR_BG_HIDAN_CURTAIN, policy); + ActorAccessibility_InitPolicy(&policy, "uninteractable rocks in kokiri forest", accessible_hana); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_HANA, policy); + ActorAccessibility_InitPolicy(&policy, "gold skulltula token", accessible_en_pickups); + ActorAccessibility_AddSupportedActor(ACTOR_EN_SI, policy); + ActorAccessibility_InitPolicy(&policy, "Gold and Wall skulltulas", nullptr); + policy.aimAssist.isProvider = true; + policy.n = 1; + policy.ydist = 500; + policy.distance = 750; + ActorAccessibility_AddSupportedActor(ACTOR_EN_SW, policy); + ActorAccessibility_InitPolicy(&policy, "goma larva egg", accessible_larva); + policy.distance = 1000; + policy.ydist = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_EN_GOMA, policy); + ActorAccessibility_InitPolicy(&policy, "small jellyfish", NA_SE_EN_BIRI_FLY); + ActorAccessibility_AddSupportedActor(ACTOR_EN_BILI, policy); + ActorAccessibility_InitPolicy(&policy, "stinger", accessible_eiyer); + policy.n = 1; + policy.distance = 1000; + policy.ydist = 200; + ActorAccessibility_AddSupportedActor(ACTOR_EN_EIYER, policy); + ActorAccessibility_InitPolicy(&policy, "bubble", NA_SE_EN_DAIOCTA_SPLASH); + policy.ydist = 200; + ActorAccessibility_AddSupportedActor(ACTOR_EN_BUBBLE, policy); + ActorAccessibility_InitPolicy(&policy, "tentacle", NA_SE_EN_BALINADE_THUNDER); + ActorAccessibility_AddSupportedActor(ACTOR_EN_BA, policy); + policy.distance = 100; + ActorAccessibility_AddSupportedActor(ACTOR_EN_BX, policy); + ActorAccessibility_InitPolicy(&policy, "redead", NA_SE_EN_REDEAD_CRY); + ActorAccessibility_AddSupportedActor(ACTOR_EN_RD, policy); + ActorAccessibility_InitPolicy(&policy, "Beamos", NA_SE_EN_BIMOS_AIM); + ActorAccessibility_AddSupportedActor(ACTOR_EN_VM, policy); + ActorAccessibility_InitPolicy(&policy, "heart canister", accessible_en_pickups); + ActorAccessibility_AddSupportedActor(ACTOR_ITEM_B_HEART, policy); + ActorAccessibility_InitPolicy(&policy, "Goma", accessible_goma); + policy.distance = 5000; + policy.ydist = 2000; + ActorAccessibility_AddSupportedActor(ACTOR_BOSS_GOMA, policy); + ActorAccessibility_InitPolicy(&policy, "bombflowers", NA_SE_EV_BOMB_BOUND); + ActorAccessibility_AddSupportedActor(ACTOR_EN_BOMBF, policy); + ActorAccessibility_InitPolicy(&policy, "Amos Statue", NA_SE_EN_AMOS_WAVE); + policy.n = 30; + ActorAccessibility_AddSupportedActor(ACTOR_EN_AM, policy); + ActorAccessibility_InitPolicy(&policy, "shooting gallery rupees", nullptr); + policy.aimAssist.isProvider = true; + policy.distance = 1000; + policy.n = 1; + ActorAccessibility_AddSupportedActor(ACTOR_EN_G_SWITCH, policy); + ActorAccessibility_InitPolicy(&policy, "crawlspace", NA_SE_EN_MUSI_SINK); + policy.volume = 1.5; + policy.distance = 2000; + ActorAccessibility_AddSupportedActor(VA_CRAWLSPACE, policy); + ActorAccessibility_InitPolicy(&policy, "Ladder/climable", accessible_climable); + // policy.volume = 1.5; + policy.pitch = 1.3; + // policy.distance = 2000; + ActorAccessibility_AddSupportedActor(VA_CLIMB, policy); + ActorAccessibility_InitPolicy(&policy, "Door", NA_SE_OC_DOOR_OPEN); + policy.n = 30; + policy.pitch = 1.1; + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(VA_DOOR, policy); + + ActorAccessibility_InitPolicy(&policy, "Area Change", accessible_area_change); + policy.n = 60; + policy.distance = 100000; + ActorAccessibility_AddSupportedActor(VA_AREA_CHANGE, policy); + ActorAccessibility_InitPolicy(&policy, "marker", NA_SE_EV_DIAMOND_SWITCH); + policy.distance = 1000; + policy.pitch = 1.7; + ActorAccessibility_AddSupportedActor(VA_MARKER, policy); + // ActorAccessibility_InitPolicy(&policy, "Spike", NA_SE_EV_DIAMOND_SWITCH); + // policy.distance = 200; + // policy.pitch = 0.5; + // ActorAccessibility_AddSupportedActor(VA_SPIKE, policy); + ActorAccessibility_InitPolicy(&policy, "Stick Burnout Warning", accessible_stick_warning); + policy.n = 1; + policy.runsAlways = true; + ActorAccessibility_AddSupportedActor(VA_STICK_WARNING, policy); + ActorAccessibility_InitPolicy(&policy, "System general helper", accessible_va_general_helper); + policy.n = 1; + policy.cleanupUserData = accessible_general_helper_cleanup; + policy.initUserData = accessible_general_helper_init; + policy.runsAlways = true; + ActorAccessibility_AddSupportedActor(VA_GENERAL_HELPER, policy); + ActorAccessibility_InitPolicy(&policy, "Audio Compass", accessible_audio_compass); + policy.n = 1; + policy.cleanupUserData = accessible_audio_compass_cleanup; + policy.initUserData = accessible_audio_compass_init; + policy.runsAlways = true; + policy.sound = NA_SE_EV_SHIP_BELL; // Setting this here so it's easy to change if we ever decide to change it. + policy.pitch = 0.5; + + ActorAccessibility_AddSupportedActor(VA_AUDIO_COMPASS, policy); + + // Now query a list of virtual actors for a given location (scene and room number). + VirtualActorList* list = (VirtualActorList*)ActorAccessibility_GetVirtualActorList(EVERYWHERE, 0); + + // Now place the actor. + ActorAccessibility_AddVirtualActor(list, VA_GENERAL_HELPER, { { 0.0, 0.0, 0.0 }, { 0, 0, 0 } }); + ActorAccessibility_AddVirtualActor(list, VA_AUDIO_COMPASS, { { 0.0, 0.0, 0.0 }, { 0, 0, 0 } }); + ActorAccessibility_AddVirtualActor(list, VA_STICK_WARNING, { { 0.0, 0.0, 0.0 }, { 0, 0, 0 } }); + + list = ActorAccessibility_GetVirtualActorList(SCENE_KOKIRI_FOREST, 0); // Kokiri Forest + ActorAccessibility_AddVirtualActor(list, VA_CRAWLSPACE, { { -784.0, 120.0, 1046.00 }, { 0, 14702, 0 } }); + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 2146.5, 1.0, -142.8 } }); + + // Kokiri Forest Room with boulder and kokiri sword + list = ActorAccessibility_GetVirtualActorList(SCENE_KOKIRI_FOREST, 2); + ActorAccessibility_AddVirtualActor(list, VA_CRAWLSPACE, { { -788.0, 120.0, 1392.00 }, { 0, 14702, 0 } }); + + list = ActorAccessibility_GetVirtualActorList(SCENE_DEKU_TREE, 2); // deku tree slingshot room + + list = ActorAccessibility_GetVirtualActorList(SCENE_DEKU_TREE, 10); // deku tree compass room + + list = ActorAccessibility_GetVirtualActorList(SCENE_DEKU_TREE, 7); // bombable wall room + ActorAccessibility_AddVirtualActor(list, VA_CRAWLSPACE, { { -1209, -820.0, 3.5 } }); + + list = ActorAccessibility_GetVirtualActorList(SCENE_DEKU_TREE, 3); // basement 1 lobby + ActorAccessibility_AddVirtualActor(list, VA_CRAWLSPACE, { { -901, -820.0, 0.5 } }); + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { -181.761, -905.0, -28.3 } }); + + list = ActorAccessibility_GetVirtualActorList(SCENE_DODONGOS_CAVERN, 2); // dodongo bombflower stairs room + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { -1958, 20, -1297 } }); + + list = ActorAccessibility_GetVirtualActorList(SCENE_JABU_JABU, 2); + AccessibleActor* temp = + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { -260, -400, -3377 } }); // green tentacle hole + temp->policy.distance = 200; + temp->policy.sound = NA_SE_EN_DAIOCTA_DEAD; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 230, -400, -3211 } }); // ruto hole + temp->policy.distance = 200; + temp->policy.sound = NA_SE_VO_RT_FALL; + + list = ActorAccessibility_GetVirtualActorList(SCENE_CASTLE_COURTYARD_GUARDS_DAY, 0); + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 1734.0, 0.0, 140.514 } }); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 1734.0, 0.0, 140.514 } }); + temp->policy.pitch = 0.3; + temp->policy.volume = 0.5; + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 1040.0, 0.0, 140.514 } }); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 1734.0, 0.0, 140.514 } }); + temp->policy.pitch = 0.6; + temp->policy.volume = 0.5; + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 230.0, 0.0, 188.514 } }); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 1734.0, 0.0, 140.514 } }); + temp->policy.pitch = 0.9; + temp->policy.volume = 0.5; + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { -426.0, 0.0, 130.514 } }); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 1734.0, 0.0, 140.514 } }); + temp->policy.pitch = 1.2; + temp->policy.volume = 0.5; + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { -1206.0, 0.0, 133.514 } }); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 1734.0, 0.0, 140.514 } }); + temp->policy.pitch = 1.5; + temp->policy.volume = 0.5; + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { -1571.0, 0.0, -834.514 } }); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { { 1734.0, 0.0, 140.514 } }); + temp->policy.pitch = 1.8; + temp->policy.volume = 0.5; + + ActorAccessibility_InitCues(); +} diff --git a/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.cpp b/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.cpp new file mode 100644 index 000000000..215b2cca5 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.cpp @@ -0,0 +1,599 @@ +#define AAE_CHANNELS 2 +#define AAE_SAMPLE_RATE 44100 +#define AAE_MAX_BUFFER_SIZE AAE_SAMPLE_RATE / 10 +#define AAE_PREP_CHUNK_SIZE 64 +#define AAE_MIX_CHUNK_SIZE 64 +#define AAE_GC_INTERVAL 20 * 60 // How often, in frames, do we clean up sound handles that are no longer active. +#define AAE_MAX_DB_REDUCTION -20 +#define AAE_LPF_ORDER 4 + +#define NOMINMAX // because Windows is a joke. +#define MINIAUDIO_IMPLEMENTATION +#include "AccessibleAudioEngine.h" + +extern "C" { +int AudioPlayer_GetDesiredBuffered(); +} +#include +#include +#include +#include + +enum AAE_COMMANDS { + AAE_START = 0, + AAE_STOP, + AAE_STOP_ALL, + AAE_PITCH, + AAE_PITCH_BEHIND, // Specify how much to change the pitch when the sound is behind the listener. + AAE_VOLUME, + AAE_PAN, + AAE_FILTER, + AAE_SEEK, + AAE_POS, + AAE_PREPARE, + AAE_TERMINATE, +}; +typedef int8_t s8; +typedef uint8_t u8; +// Processing for our custom audio positioning. +static float lerp_aae(float x, float y, float z) { + return (1.0 - z) * x + z * y; +} + +static float computeGain(SoundExtras* extras) { + if (extras->maxDistance == 0) + return 0; + float leftover = ma_volume_db_to_linear(AAE_MAX_DB_REDUCTION); + float normDist = fabs(extras->distToPlayer) / extras->maxDistance; + float db = lerp_aae(0, AAE_MAX_DB_REDUCTION, normDist); + float gain = ma_volume_db_to_linear(db); + gain -= lerp_aae(0, leftover, normDist); + return gain; +} +// Borrow the pan calculation from the game itself. Todo: this is technical debt, so copy/ revise it or something at +// some point. +extern "C" int8_t Audio_ComputeSoundPanSigned(float x, float z, uint8_t token); +static void positioner_process_pcm_frames(ma_node* pNode, const float** ppFramesIn, ma_uint32* pFrameCountIn, + float** ppFramesOut, ma_uint32* pFrameCountOut) { + + const float* framesIn = ppFramesIn[0]; + float* framesOut = ppFramesOut[0]; + ma_copy_pcm_frames(framesOut, framesIn, *pFrameCountIn, ma_format_f32, 2); + *pFrameCountOut = *pFrameCountIn; + SoundExtras* extras = (SoundExtras*)pNode; + // Pan the sound based on its projected position. + float pan; + // Use the game's panning mechanism, which returns a signed 8-bit integer between 0 (far-left) and 127 (far-right). + // It would appear that the correct thing to do is interpret this value as a gain factor in decibels. In practice, + // values below 38 or above 90 are never seen, so a sound that's panned far to one side or the other amounts to + // about -25DB worth of attenuation. Also: lie about the value of Z and give it a constant value to prevent weird + // behaviour when Z is far away. + s8 panSigned = Audio_ComputeSoundPanSigned(extras->x, extras->z, 4); + int db; + if (panSigned < 64) + db = 64 - panSigned; + else + db = panSigned - 64; + pan = 1.0 - fabs(ma_volume_db_to_linear(-db / 2)); + if (panSigned < 64) + pan = -pan; + + ma_panner_set_pan(&extras->panner, pan); + ma_panner_process_pcm_frames(&extras->panner, framesOut, framesOut, *pFrameCountIn); + // Next we'll apply the gain based on the object's distance relationship to the player. The strategy here is to use + // a combination of decibel-based and linear attenuation, so that the gain reaches 0 at the exact point when the + // object is at exactly the maximum distance from the player. + + float gain = computeGain(extras); + ma_gainer_set_gain(&extras->gainer, gain); + ma_gainer_process_pcm_frames(&extras->gainer, framesOut, framesOut, *pFrameCountIn); + // Run LPF only when necessary because we can't afford to run a 4th-order lowpass on every single sound. This + // probably causes minor glitches when the filter switches on and off. Todo: cross that bridge. + if (extras->cutoff != 1.0) + ma_lpf_process_pcm_frames(&extras->filter, framesOut, framesOut, *pFrameCountIn); +} + +static ma_node_vtable positioner_vtable = { positioner_process_pcm_frames, NULL, 1, 1, 0 }; +static ma_uint32 positioner_channels[1] = { 2 }; + +void AccessibleAudioEngine::destroy() { + switch (initialized) { + case 3: + ma_engine_uninit(&engine); + case 2: + ma_pcm_rb_uninit(&preparedOutput); + case 1: + ma_resource_manager_uninit(&resourceManager); + } +} + +void AccessibleAudioEngine::destroyAndThrow(const char* exceptionText) { + destroy(); + throw std::runtime_error(exceptionText); +} + +uint32_t AccessibleAudioEngine::retrieve(float* buffer, uint32_t nFrames) { + uint32_t framesAvailable = ma_pcm_rb_available_read(&preparedOutput); + if (nFrames > framesAvailable) + nFrames = framesAvailable; + if (nFrames == 0) + return 0; + uint32_t ogNFrames = nFrames; + while (nFrames > 0) { + void* readBuffer; + uint32_t framesObtained = nFrames; + ma_pcm_rb_acquire_read(&preparedOutput, &framesObtained, (void**)&readBuffer); + if (framesObtained > nFrames) + framesObtained = nFrames; + + memcpy(buffer, readBuffer, sizeof(float) * framesObtained * AAE_CHANNELS); + buffer += framesObtained * AAE_CHANNELS; + nFrames -= framesObtained; + ma_pcm_rb_commit_read(&preparedOutput, framesObtained); + } + + return ogNFrames; +} + +void AccessibleAudioEngine::doPrepare(SoundAction& action) { + framesUntilGC--; + int nFrames = ma_pcm_rb_available_write(&preparedOutput); + if (nFrames <= 0) + return; + + float* chunk; + while (nFrames > 0) { + // This should not loop more than twice. + uint32_t nextChunk = nFrames; + ma_pcm_rb_acquire_write(&preparedOutput, &nextChunk, + (void**)&chunk); // Might reduce nextChunk if there isn't enough buffer space available + // to accommodate the request. + ma_uint64 framesRead = 0; + ma_engine_read_pcm_frames(&engine, chunk, nextChunk, &framesRead); + // Even if we get fewer frames than expected, we should still submit a full buffer of silence. + if (framesRead < nextChunk) + ma_silence_pcm_frames(chunk + (framesRead * 2), (nextChunk - framesRead), ma_format_f32, 2); + ma_pcm_rb_commit_write(&preparedOutput, (uint32_t)nextChunk); + nFrames -= nextChunk; + } +} +int AccessibleAudioEngine::getSoundActions(SoundAction* dest, int limit) { + std::unique_lock lock(mtx); + while (soundActions.empty()) + cv.wait(lock); + int actionsOut = 0; + while (!soundActions.empty() && limit > 0) { + dest[actionsOut] = soundActions.front(); + soundActions.pop_front(); + actionsOut++; + limit--; + } + return actionsOut; +} +void AccessibleAudioEngine::postSoundActions() { + { + std::scoped_lock lock(mtx); + for (int i = 0; i < nextOutgoingSoundAction; i++) + soundActions.push_back(outgoingSoundActions[i]); + } + cv.notify_one(); + nextOutgoingSoundAction = 0; +} +void AccessibleAudioEngine::postHighPrioritySoundAction(SoundAction& action) { + std::scoped_lock lock(mtx); + soundActions.push_front(action); + cv.notify_one(); +} +SoundAction& AccessibleAudioEngine::getNextOutgoingSoundAction() { + if (nextOutgoingSoundAction >= AAE_SOUND_ACTION_BATCH_SIZE) + postSoundActions(); + nextOutgoingSoundAction++; + return outgoingSoundActions[nextOutgoingSoundAction - 1]; +} +void AccessibleAudioEngine::runThread() { + bool shouldTerminate = false; + SoundAction incomingSoundActions[AAE_SOUND_ACTION_BATCH_SIZE]; + while (true) { + processAudioJobs(); + if (framesUntilGC <= 0) + garbageCollect(); + + int batchSize = getSoundActions(incomingSoundActions, AAE_SOUND_ACTION_BATCH_SIZE); + for (int i = 0; i < batchSize; i++) { + SoundAction& action = incomingSoundActions[i]; + switch (action.command) { + case AAE_TERMINATE: + return; + case AAE_START: + doPlaySound(action); + break; + case AAE_STOP: + doStopSound(action); + break; + case AAE_STOP_ALL: + doStopAllSounds(action); + break; + case AAE_PITCH: + doSetPitch(action); + break; + case AAE_PITCH_BEHIND: + doSetPitchBehindModifier(action); + break; + case AAE_VOLUME: + doSetVolume(action); + break; + case AAE_PAN: + doSetPan(action); + break; + case AAE_FILTER: + doSetFilter(action); + break; + case AAE_SEEK: + doSeekSound(action); + break; + case AAE_POS: + doSetSoundPos(action); + break; + case AAE_PREPARE: + doPrepare(action); + break; + } + } + } +} + +SoundSlot* AccessibleAudioEngine::findSound(SoundAction& action) { + if (action.slot < 0 || action.slot >= AAE_SLOTS_PER_HANDLE) + return NULL; + auto i = sounds.find(action.handle); + if (i == sounds.end()) + return NULL; + SoundSlot& target = i->second[action.slot]; + if (!target.active) + return NULL; + return ⌖ +} + +void AccessibleAudioEngine::doPlaySound(SoundAction& action) { + SoundSlot* sound; + if (sounds.contains(action.handle)) { + sound = &sounds[action.handle][action.slot]; + if (sound->active) { + ma_sound_stop(&sound->sound); + destroySound(sound); + } + } else { + SoundSlots temp; + for (int i = 0; i < AAE_SLOTS_PER_HANDLE; i++) + temp[i].active = false; + + sounds[action.handle] = temp; + sound = &sounds[action.handle][action.slot]; + } + + ma_result result = ma_sound_init_from_file(&engine, action.path.c_str(), + MA_SOUND_FLAG_NO_SPATIALIZATION | MA_SOUND_FLAG_NO_DEFAULT_ATTACHMENT, + NULL, NULL, &sound->sound); + if (result != MA_SUCCESS) { + SPDLOG_ERROR("failed to play sound: {}", ma_result_description(result)); + return; + } + + initSoundExtras(sound); + ma_sound_set_looping(&sound->sound, action.looping); + // We actually attach the extras to the engine, not the sound itself. + ma_node_attach_output_bus(&sound->extras, 0, ma_node_graph_get_endpoint(&engine.nodeGraph), 0); + + ma_sound_start(&sound->sound); + + sound->active = true; +} + +void AccessibleAudioEngine::doStopSound(SoundAction& action) { + SoundSlot* slot = findSound(action); + if (slot == NULL) + return; + destroySound(slot); +} +void AccessibleAudioEngine::doStopAllSounds(SoundAction& action) { + auto it = sounds.find(action.handle); + if (it == sounds.end()) + return; + SoundSlots& slots = it->second; + for (int i = 0; i < AAE_SLOTS_PER_HANDLE; i++) { + if (slots[i].active) + destroySound(&slots[i]); + } + sounds.erase(it); +} +void AccessibleAudioEngine::doSetPitch(SoundAction& action) { + SoundSlot* slot = findSound(action); + if (slot == NULL) + return; + slot->extras.pitch = action.pitch; + float pitch = action.pitch; + if (slot->extras.z < 0) + pitch *= (1.0 - slot->extras.pitchBehindModifier); + ma_sound_set_pitch(&slot->sound, pitch); +} + +void AccessibleAudioEngine::doSetPitchBehindModifier(SoundAction& action) { + SoundSlot* slot = findSound(action); + if (slot == NULL) + return; + slot->extras.pitchBehindModifier = action.pitch; +} +void AccessibleAudioEngine::doSetVolume(SoundAction& action) { + SoundSlot* slot = findSound(action); + if (slot == NULL) + return; + ma_sound_set_volume(&slot->sound, action.pitch); +} +void AccessibleAudioEngine::doSetPan(SoundAction& action) { + SoundSlot* slot = findSound(action); + if (slot == NULL) + return; + ma_sound_set_pan(&slot->sound, action.pan); +} +void AccessibleAudioEngine::doSetFilter(SoundAction& action) { + SoundSlot* slot = findSound(action); + if (slot == NULL) + return; + slot->extras.cutoff = action.cutoff; + ma_lpf_config config = ma_lpf_config_init(ma_format_f32, AAE_CHANNELS, AAE_SAMPLE_RATE, + lerp_aae(0.0, AAE_SAMPLE_RATE / 2, action.cutoff), AAE_LPF_ORDER); + ma_lpf_reinit(&config, &slot->extras.filter); +} +void AccessibleAudioEngine::doSeekSound(SoundAction& action) { + SoundSlot* slot = findSound(action); + if (slot == NULL) + return; + ma_sound_seek_to_pcm_frame(&slot->sound, action.offset); +} +void AccessibleAudioEngine::doSetSoundPos(SoundAction& action) { + SoundSlot* slot = findSound(action); + if (slot == NULL) + return; + slot->extras.x = action.posX; + slot->extras.y = action.posY; + slot->extras.z = action.posZ; + slot->extras.distToPlayer = action.distToPlayer; + slot->extras.maxDistance = action.maxDistance; + float pitch = slot->extras.pitch; + if (action.posZ < 0) + pitch *= (1.0 - slot->extras.pitchBehindModifier); + ma_sound_set_pitch(&slot->sound, pitch); +} +void AccessibleAudioEngine::garbageCollect() { + for (auto i = sounds.begin(); i != sounds.end();) { + int deadSlots = 0; + for (int x = 0; x < AAE_SLOTS_PER_HANDLE; x++) { + if (!i->second[x].active) + deadSlots++; + else if (!ma_sound_is_playing(&i->second[x].sound)) { + destroySound(&i->second[x]); + i->second[x].active = false; + deadSlots++; + } + } + if (deadSlots == AAE_SLOTS_PER_HANDLE) // Entire batch is garbage. + i = sounds.erase(i); + else + i++; + } + framesUntilGC = AAE_GC_INTERVAL; +} +void AccessibleAudioEngine::processAudioJobs() { + ma_job job; + while (ma_resource_manager_next_job(&resourceManager, &job) == MA_SUCCESS) + ma_job_process(&job); +} +bool AccessibleAudioEngine::initSoundExtras(SoundSlot* slot) { + ma_node_config config = ma_node_config_init(); + config.inputBusCount = 1; + config.outputBusCount = 1; + config.pInputChannels = positioner_channels; + config.pOutputChannels = positioner_channels; + config.vtable = &positioner_vtable; + memset(&slot->extras, 0, sizeof(SoundExtras)); + if (ma_node_init(&engine.nodeGraph, &config, NULL, &slot->extras) != MA_SUCCESS) + return false; + ma_panner_config pc = ma_panner_config_init(ma_format_f32, AAE_CHANNELS); + pc.mode = ma_pan_mode_balance; + ma_panner_init(&pc, &slot->extras.panner); + ma_gainer_config gc = ma_gainer_config_init( + AAE_CHANNELS, + AAE_SAMPLE_RATE / 20); // Allow one in-game frame for the gain to work its way towards the target value. + if (ma_gainer_init(&gc, NULL, &slot->extras.gainer) != MA_SUCCESS) + return false; + ma_lpf_config fc = + ma_lpf_config_init(ma_format_f32, AAE_CHANNELS, AAE_SAMPLE_RATE, AAE_SAMPLE_RATE / 2, AAE_LPF_ORDER); + ma_lpf_init(&fc, NULL, &slot->extras.filter); + slot->extras.cutoff = 1.0f; + slot->extras.pitch = 1.0f; + slot->extras.pitchBehindModifier = 0.0f; + // ma_node_attach_output_bus(&slot->sound, 0, &slot->extras.filter, 0); + ma_node_attach_output_bus(&slot->sound, 0, &slot->extras, 0); + return true; +} + +void AccessibleAudioEngine::destroySound(SoundSlot* slot) { + ma_node_detach_all_output_buses(&slot->extras); + ma_sound_uninit(&slot->sound); + ma_gainer_uninit(&slot->extras.gainer, NULL); + + slot->active = false; +} + +AccessibleAudioEngine::AccessibleAudioEngine() { + initialized = 0; + ma_resource_manager_config rmc = ma_resource_manager_config_init(); + rmc.decodedChannels = AAE_CHANNELS; + rmc.decodedFormat = ma_format_f32; + rmc.decodedSampleRate = AAE_SAMPLE_RATE; + rmc.flags = MA_RESOURCE_MANAGER_FLAG_NON_BLOCKING; + rmc.jobThreadCount = 0; + if (ma_resource_manager_init(&rmc, &resourceManager) != MA_SUCCESS) + destroyAndThrow("AccessibleAudioEngine: Unable to initialize the resource manager."); + initialized = 1; + if (ma_pcm_rb_init(ma_format_f32, AAE_CHANNELS, AAE_MAX_BUFFER_SIZE, NULL, NULL, &preparedOutput) != MA_SUCCESS) + destroyAndThrow("AccessibleAudioEngine: Unable to initialize the output buffer."); + initialized = 2; + ma_engine_config ec = ma_engine_config_init(); + ec.channels = AAE_CHANNELS; + ec.noDevice = true; + ec.sampleRate = AAE_SAMPLE_RATE; + ec.pResourceManager = &resourceManager; + ec.listenerCount = 1; + + if (ma_engine_init(&ec, &engine) != MA_SUCCESS) + destroyAndThrow("AccessibleAudioEngine: Unable to initialize the audio engine."); + initialized = 3; + nextOutgoingSoundAction = 0; + framesUntilGC = AAE_GC_INTERVAL; + thread = std::thread(&AccessibleAudioEngine::runThread, this); +} +AccessibleAudioEngine::~AccessibleAudioEngine() { + // Place a terminate command on the top of the pile, then wait for thread to die. + SoundAction action; + action.command = AAE_TERMINATE; + postHighPrioritySoundAction(action); + thread.join(); + destroy(); +} +void AccessibleAudioEngine::mix(int16_t* ogBuffer, uint32_t nFrames) { + float sourceChunk[AAE_MIX_CHUNK_SIZE * AAE_CHANNELS]; + float mixedChunk[AAE_MIX_CHUNK_SIZE * AAE_CHANNELS]; + while (nFrames > 0) { + uint32_t nextChunk = std::min(AAE_MIX_CHUNK_SIZE, nFrames); + // This is so that it doesn't matter if we have less output available than expected. + ma_silence_pcm_frames(sourceChunk, nextChunk, ma_format_f32, AAE_CHANNELS); + ma_silence_pcm_frames(mixedChunk, nextChunk, ma_format_f32, AAE_CHANNELS); + retrieve(sourceChunk, nextChunk); + // The game's output is changed to 32-bit floating point samples. + ma_pcm_s16_to_f32(mixedChunk, ogBuffer, nextChunk * AAE_CHANNELS, ma_dither_mode_none); + ma_mix_pcm_frames_f32(mixedChunk, sourceChunk, nextChunk, AAE_CHANNELS, 1.0); + // If we've gone over 1.0, we'll need to scale back before we go back to 16-bit or we'll distort. + float scalar = 1.0; + for (int i = 0; i < nextChunk * AAE_CHANNELS; i++) + scalar = std::max(scalar, mixedChunk[i]); + if (scalar > 1.0) { + scalar = 1.0 / scalar; + for (int i = 0; i < nextChunk * AAE_CHANNELS; i++) + mixedChunk[i] *= scalar; + } + // Chunk is ready to go out via the game's usual channels + ma_pcm_f32_to_s16(ogBuffer, mixedChunk, nextChunk * AAE_CHANNELS, ma_dither_mode_triangle); + ogBuffer += nextChunk * AAE_CHANNELS; + nFrames -= nextChunk; + } +} + +void AccessibleAudioEngine::playSound(uintptr_t handle, int slot, const char* path, bool looping) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.handle = handle; + action.slot = slot; + action.command = AAE_START; + action.path = path; + action.looping = looping; +} + +void AccessibleAudioEngine::stopSound(uintptr_t handle, int slot) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.command = AAE_STOP; + action.handle = (uintptr_t)handle; + action.slot = slot; +} + +void AccessibleAudioEngine::stopAllSounds(uintptr_t handle) { + SoundAction& action = getNextOutgoingSoundAction(); + action.command = AAE_STOP_ALL; + action.handle = handle; +} + +void AccessibleAudioEngine::setPitch(uintptr_t handle, int slot, float pitch) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.command = AAE_PITCH; + action.handle = handle; + action.slot = slot; + action.pitch = pitch; +} + +void AccessibleAudioEngine::setPitchBehindModifier(uintptr_t handle, int slot, float mod) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.command = AAE_PITCH_BEHIND; + action.handle = handle; + action.slot = slot; + action.pitch = mod; +} + +void AccessibleAudioEngine::setVolume(uintptr_t handle, int slot, float volume) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.command = AAE_VOLUME; + action.handle = handle; + action.slot = slot; + action.volume = volume; +} + +void AccessibleAudioEngine::setPan(uintptr_t handle, int slot, float pan) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.command = AAE_PAN; + action.handle = handle; + action.slot = slot; + action.pan = pan; +} +void AccessibleAudioEngine::setFilter(uintptr_t handle, int slot, float cutoff) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + if (cutoff < 0.0 || cutoff > 1.0) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.handle = handle; + action.slot = slot; + action.command = AAE_FILTER; + action.cutoff = cutoff; +} + +void AccessibleAudioEngine::seekSound(uintptr_t handle, int slot, size_t offset) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.handle = handle; + action.slot = slot; + action.command = AAE_SEEK; + action.offset = offset; +} + +void AccessibleAudioEngine::setSoundPosition(uintptr_t handle, int slot, float posX, float posY, float posZ, + float distToPlayer, float maxDistance) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.command = AAE_POS; + action.handle = handle; + action.slot = slot; + action.posX = posX; + action.posY = posY; + action.posZ = posZ; + action.distToPlayer = distToPlayer; + action.maxDistance = maxDistance; +} + +void AccessibleAudioEngine::prepare() { + SoundAction& action = getNextOutgoingSoundAction(); + action.command = AAE_PREPARE; + // This is called once at the end of every frame, so now is the time to post all of the accumulated commands. + postSoundActions(); +} \ No newline at end of file diff --git a/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.h b/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.h new file mode 100644 index 000000000..16f6826b4 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.h @@ -0,0 +1,152 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +#include "soh/Enhancements/audio/miniaudio.h" + +#define AAE_SOUND_ACTION_BATCH_SIZE 64 +#define AAE_SLOTS_PER_HANDLE 16 +class IResource; +struct DecodedSample { + void* data; // A wav file. + size_t dataSize; +}; +struct SoundAction { + uintptr_t handle; // This handle is user-defined and uniquely identifies a sound source. It can be anything, but the + // address of an object with which the sound is associated is recommended. + int slot; // Allows multiple sounds per handle. The exact number is controlled by AAE_SOUNDS_PER_HANDLE. + int command; // One of the items belonging to AAE_COMMANDS. + std::string path; // If command is AAE_START, this is the path to the desired resource. + bool looping; // If command is AAE_START, specifies whether or not the sound should loop. + union { + float pitch; + float volume; + float pan; + float cutoff; + size_t offset; // for seeking. + float distance; + }; + + // Position and rotation vectors for AAE_POS + float posX; + float posY; + float posZ; + float distToPlayer; + float maxDistance; + uint32_t frames; // If command is AAE_PREPARE, this tells the engine how many PCM frames to get ready. +}; + +typedef struct { + ma_node_base base; + ma_panner panner; + ma_gainer gainer; + ma_lpf filter; + float cutoff; + float x; + float y; + float z; + float distToPlayer; + float maxDistance; + float pitch; + float pitchBehindModifier; +} SoundExtras; // Used for attenuation and other effects. + +typedef struct { + ma_sound sound; + SoundExtras extras; + + bool active; +} SoundSlot; +typedef std::array SoundSlots; + +class AccessibleAudioEngine { + int initialized; + ma_engine engine; + ma_pcm_rb preparedOutput; // Lock-free single producer single consumer. + std::deque soundActions; // A command cue. + std::thread thread; + std::condition_variable cv; + std::mutex mtx; + std::unordered_map sounds; + SoundAction + outgoingSoundActions[AAE_SOUND_ACTION_BATCH_SIZE]; // Allows batch delivery of SoundActions to the FIFO to + // minimize the amount of time spent locking and unlocking. + int nextOutgoingSoundAction; + int framesUntilGC; + void destroy(); // Called by the destructor, or if a throw occurs during construction. + // Dismantal a partial initialization and throw an exception. + void destroyAndThrow(const char* exceptionText); + + // Retrieve some audio from the output buffer. This is to be performed by the audio thread. May return less data + // than requested. + uint32_t retrieve(float* buffer, uint32_t nFrames); + // Functions dealing with the command queue. + int getSoundActions(SoundAction* dest, int limit); + void postSoundActions(); + void postHighPrioritySoundAction(SoundAction& action); // For now this is used only for termination events. + + SoundAction& getNextOutgoingSoundAction(); + void runThread(); + // Find a sound by handle and slot, if it exists. + SoundSlot* findSound(SoundAction& action); + + // Functions which correspond to SoundAction commands. + // Ready a sound for playback. + void doPlaySound(SoundAction& action); + void doStopSound(SoundAction& action); + void doStopAllSounds(SoundAction& action); + + void doSetPitch(SoundAction& action); + void doSetPitchBehindModifier(SoundAction& action); + void doSetVolume(SoundAction& action); + void doSetPan(SoundAction& action); + void doSetFilter(SoundAction& action); + void doSeekSound(SoundAction& action); + + void doSetListenerPos(SoundAction& action); + void doSetSoundPos(SoundAction& action); + // Generate some output, and store it in the output buffer for later retrieval. May generate less output than + // requested if buffer space is insufficient. + void doPrepare(SoundAction& action); + // Run every so often to clean up expired sound handles. + void garbageCollect(); + // Run MiniAudio's jobs. + void processAudioJobs(); + // Set up the panner and other effect processing on a sound slot. + bool initSoundExtras(SoundSlot* slot); + void destroySound(SoundSlot* slot); + + public: + AccessibleAudioEngine(); + ~AccessibleAudioEngine(); + // Mix the game's audio with this engine's audio to produce the final mix. To be performed exclusively in the audio + // thread. Mixing is done in-place (meaning the buffer containing the game's audio is overwritten with the mixed + // content). + void mix(int16_t* ogBuffer, uint32_t nFrames); + // Start playing a sound. + void playSound(uintptr_t handle, int slot, const char* path, bool looping); + void stopSound(uintptr_t handle, int slot); + // Stop all sounds belonging to a handle. + void stopAllSounds(uintptr_t handle); + + void setPitch(uintptr_t handle, int slot, float pitch); + void setPitchBehindModifier(uintptr_t handle, int slot, float mod); + void setVolume(uintptr_t handle, int slot, float volume); + void setPan(uintptr_t handle, int slot, float pan); + // Set the lowpass filter cutoff. Set to 1.0 for no filtering. + void setFilter(uintptr_t handle, int slot, float cutoff); + // Seek the sound to a particular PCM frame. + void seekSound(uintptr_t handle, int slot, size_t offset); + void setSoundPosition(uintptr_t handle, int slot, float posX, float posY, float posZ, float distToPlayer, + float maxDistance); + // Schedule the preparation of output for delivery. + void prepare(); + + ma_resource_manager resourceManager; +}; diff --git a/soh/soh/Enhancements/accessible-actors/ActorAccessibility.cpp b/soh/soh/Enhancements/accessible-actors/ActorAccessibility.cpp new file mode 100644 index 000000000..597758d74 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/ActorAccessibility.cpp @@ -0,0 +1,716 @@ +#include "ActorAccessibility.h" +#include "AccessibleAudioEngine.h" +#include "soh/OTRGlobals.h" +#include "resource/type/Blob.h" + +#include +#include +#include +#include +#include +#include +#include "ResourceType.h" +#include "SfxExtractor.h" + +#include +#include "File.h" +#include +#include "soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h" +#include "soh/Enhancements/tts/tts.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +extern "C" { +extern PlayState* gPlayState; +extern bool freezeGame; +extern bool freezeActors; +} + +const char* GetLanguageCode(); + +// This is the amount in DB that a sound will be reduced by when it is at the maximum distance from the player. +#define MAX_DB_REDUCTION 35 + +extern "C" { +// Used to tell where polygons are located. +void CollisionPoly_GetVertices(CollisionPoly* poly, Vec3s* vtxList, Vec3f* dest); +} + +typedef struct { + union { + struct { + s16 sceneIndex; // Corresponds directly to the game's scene indices. + s16 roomIndex; // Corresponds directly to the game's room indices. + } values; + s32 raw; // Combination of the two which can be used for dictionary lookups. + }; + +} SceneAndRoom; + +// Maps actors to their accessibility policies, which describe how accessibility should treat them. +typedef std::map SupportedActors_t; +typedef std::map + TrackedActors_t; // Maps real actors to internal IDs specific to accessibility. + // Maps internal IDs to wrapped actor objects. These actors can be real or virtual. +typedef std::map AccessibleActorList_t; +typedef std::vector VAList_t; // Denotes a list of virtual actors specific to a single room. +typedef std::map VAZones_t; // Maps room/ scene indices to their corresponding virtual actor collections. +// A list of scenes which have already been visited (since the game was launched). Used to prevent +// re-creation of terrain VAs every time the player reloads a scene. +typedef std::unordered_set SceneList_t; + +typedef struct { + std::string path; + std::shared_ptr resource; +} SfxRecord; + +class AudioGlossaryData { + public: + AccessibleActorList_t accessibleActorList; + AccessibleActorList_t::iterator current = accessibleActorList.begin(); + bool GlossaryStarted = false; + int cooldown = 0; + int frameCount = 0; + s16 currentScene = -1; + s8 currentRoom = -1; +}; + +class ActorAccessibility { + public: + int isOn = 0; + uint64_t nextActorID = 0; + SupportedActors_t supportedActors; + TrackedActors_t trackedActors; + AccessibleActorList_t accessibleActorList; + AudioGlossaryData* glossary; + VAZones_t vaZones; + SceneList_t sceneList; + AccessibleAudioEngine* audioEngine; + SfxExtractor sfxExtractor; + // Maps internal sfx to external (prerendered) resources. + std::unordered_map sfxMap; + int extractSfx = 0; + s16 currentScene = -1; + s8 currentRoom = -1; + VirtualActorList* currentEverywhere = NULL; + VirtualActorList* currentSceneGlobal = NULL; + VirtualActorList* currentRoomLocal = NULL; +}; +static ActorAccessibility* aa; + +uint64_t ActorAccessibility_GetNextID() { + return aa->nextActorID++; +} + +void ActorAccessibility_PrepareNextAudioFrame(); + +// Hooks for game-interactor. +void ActorAccessibility_OnActorInit(void* actor) { + ActorAccessibility_TrackNewActor((Actor*)actor); +} +void ActorAccessibility_OnGameFrameUpdate() { + if (gPlayState == NULL) + return; + if (!GameInteractor::IsSaveLoaded() && !aa->extractSfx) + return; // Title screen, skip. + + ActorAccessibility_RunAccessibilityForAllActors(gPlayState); +} +void ActorAccessibility_OnActorDestroy(void* actor) { + ActorAccessibility_RemoveTrackedActor((Actor*)actor); +} +void ActorAccessibility_OnGameStillFrozen() { + if (gPlayState == NULL) + return; + if (aa->extractSfx) + ActorAccessibility_HandleSoundExtractionMode(gPlayState); +} + +void ActorAccessibility_Init() { + aa = new ActorAccessibility(); + aa->glossary = new AudioGlossaryData(); + aa->isOn = CVarGetInteger(CVAR_SETTING("A11yAudioInteraction"), 0); + if (!aa->isOn) + return; + aa->extractSfx = CVarGetInteger("gExtractSfx", 0); + if (aa->extractSfx) + freezeGame = true; + ActorAccessibility_InitAudio(); + ActorAccessibility_InitActors(); + GameInteractor::Instance->RegisterGameHook(ActorAccessibility_OnActorInit); + GameInteractor::Instance->RegisterGameHook(ActorAccessibility_OnActorDestroy); + + GameInteractor::Instance->RegisterGameHook(ActorAccessibility_OnGameFrameUpdate); + GameInteractor::Instance->RegisterGameHook(ActorAccessibility_OnGameStillFrozen); +} +void ActorAccessibility_Shutdown() { + ActorAccessibility_ShutdownAudio(); + delete aa; +} +void ActorAccessibility_InitPolicy(ActorAccessibilityPolicy* policy, const char* englishName, + ActorAccessibilityCallback callback) { + policy->callback = callback; + policy->distance = 500; + policy->ydist = 80; + policy->englishName = englishName; + policy->n = 20; + policy->pitch = 1.5; + policy->runsAlways = false; + policy->sound = 0; + policy->volume = 1.0; + policy->initUserData = NULL; + policy->cleanupUserData = NULL; + policy->pitchModifier = 0.1; + policy->aimAssist.isProvider = false; + policy->aimAssist.sfx = NA_SE_SY_HITPOINT_ALARM; + policy->aimAssist.tolerance = 0.0; +} + +void ActorAccessibility_InitPolicy(ActorAccessibilityPolicy* policy, const char* englishName, s16 sfx) { + policy->callback = nullptr; + policy->distance = 500; + policy->ydist = 80; + policy->englishName = englishName; + policy->n = 20; + policy->pitch = 1.5; + policy->runsAlways = false; + policy->sound = sfx; + policy->volume = 1.0; + policy->initUserData = NULL; + policy->cleanupUserData = NULL; + policy->pitchModifier = 0.1; + policy->aimAssist.isProvider = false; + policy->aimAssist.sfx = NA_SE_SY_HITPOINT_ALARM; + policy->aimAssist.tolerance = 0.0; +} + +void ActorAccessibility_AddSupportedActor(s16 type, ActorAccessibilityPolicy policy) { + aa->supportedActors[type] = policy; +} + +ActorAccessibilityPolicy* ActorAccessibility_GetPolicyForActor(s16 type) { + SupportedActors_t::iterator i = aa->supportedActors.find(type); + if (i == aa->supportedActors.end()) + return NULL; + return &(i->second); +} +int ActorAccessibility_GetRandomStartingFrameCount(int min, int max) { + static std::mt19937 gen; + std::uniform_int_distribution<> dist(min, max); + return dist(gen); +} + +void ActorAccessibility_TrackNewActor(Actor* actor) { + // Don't track actors for which no accessibility policy has been configured. + ActorAccessibilityPolicy* policy = ActorAccessibility_GetPolicyForActor(actor->id); + if (policy == NULL) + return; + AccessibleActor accessibleActor; + accessibleActor.instanceID = ActorAccessibility_GetNextID(); + accessibleActor.actor = actor; + accessibleActor.id = actor->id; + // Stagger the start times so that all of the sounds don't play at exactly the same time. + accessibleActor.frameCount = ActorAccessibility_GetRandomStartingFrameCount(0, policy->n); + accessibleActor.basePitch = policy->pitch; + accessibleActor.policy = *policy; + accessibleActor.currentPitch = accessibleActor.policy.pitch; + accessibleActor.baseVolume = accessibleActor.policy.volume; + accessibleActor.currentVolume = accessibleActor.policy.volume; + accessibleActor.sceneIndex = 0; + accessibleActor.managedSoundSlots = 0; + accessibleActor.aimAssist.framesSinceAimAssist = 32768; + accessibleActor.aimAssist.frequency = 10; + accessibleActor.aimAssist.pitch = 1.0; + + aa->trackedActors[actor] = accessibleActor.instanceID; + aa->accessibleActorList[accessibleActor.instanceID] = accessibleActor; + AccessibleActor& savedActor = aa->accessibleActorList[accessibleActor.instanceID]; + if (policy->initUserData && !policy->initUserData(&savedActor)) { + ActorAccessibility_RemoveTrackedActor(actor); + + return; // Probably a malloc error preventing user data initialization. + } +} +void ActorAccessibility_RemoveTrackedActor(Actor* actor) { + TrackedActors_t::iterator i = aa->trackedActors.find(actor); + if (i == aa->trackedActors.end()) + return; + uint64_t id = i->second; + aa->trackedActors.erase(i); + AccessibleActorList_t::iterator i2 = aa->accessibleActorList.find(id); + if (i2 == aa->accessibleActorList.end()) + return; + if (i2->second.policy.cleanupUserData) + i2->second.policy.cleanupUserData(&i2->second); + ActorAccessibility_StopAllSoundsForActor(&i2->second); + aa->accessibleActorList.erase(i2); +} + +f32 ActorAccessibility_DBToLinear(float gain) { + return powf(10.0, gain / 20.0f); +} +f32 ActorAccessibility_ComputeCurrentVolume(f32 maxDistance, f32 xzDistToPlayer) { + if (maxDistance == 0) + return 0.0; + f32 absDistance = fabs(xzDistToPlayer); + f32 db = LERP(0.0 - MAX_DB_REDUCTION, 0.0, (maxDistance - absDistance) / maxDistance); + + return ActorAccessibility_DBToLinear(db); +} +const char* ActorAccessibility_MapSfxToExternalAudio(s16 sfxId); +void ActorAccessibility_PlaySound(void* handle, int slot, s16 sfxId, bool looping) { + const char* path = ActorAccessibility_MapSfxToExternalAudio(sfxId); + if (path == NULL) + return; + aa->audioEngine->playSound((uintptr_t)handle, slot, path, looping); +} + +void ActorAccessibility_StopSound(void* handle, int slot) { + aa->audioEngine->stopSound((uintptr_t)handle, slot); +} +void ActorAccessibility_StopAllSounds(void* handle) { + aa->audioEngine->stopAllSounds((uintptr_t)handle); +} +void ActorAccessibility_SetSoundPitch(void* handle, int slot, float pitch) { + aa->audioEngine->setPitch((uintptr_t)handle, slot, pitch); +} +void ActorAccessibility_SetPitchBehindModifier(void* handle, int slot, float mod) { + aa->audioEngine->setPitchBehindModifier((uintptr_t)handle, slot, mod); +} +void ActorAccessibility_SetSoundPos(void* handle, int slot, Vec3f* pos, f32 distToPlayer, f32 maxDistance) { + aa->audioEngine->setSoundPosition((uintptr_t)handle, slot, pos->x, pos->y, pos->z, distToPlayer, maxDistance); +} +void ActorAccessibility_SetSoundVolume(void* handle, int slot, float volume) { + aa->audioEngine->setVolume((uintptr_t)handle, slot, volume); +} +void ActorAccessibility_SetSoundPan(void* handle, int slot, Vec3f* projectedPos) { + float pan = projectedPos->x / 270; + if (pan < -1.0) + pan = -1.0; + if (pan > 1.0) + pan = 1.0; + aa->audioEngine->setPan((uintptr_t)handle, slot, pan); +} +void ActorAccessibility_SetSoundFilter(void* handle, int slot, float cutoff) { + aa->audioEngine->setFilter((uintptr_t)handle, slot, cutoff); +} +void ActorAccessibility_SeekSound(void* handle, int slot, size_t offset) { + aa->audioEngine->seekSound((uintptr_t)handle, slot, offset); +} +void ActorAccessibility_ConfigureSoundForActor(AccessibleActor* actor, int slot) { + ActorAccessibility_SetSoundPitch(actor, slot, actor->policy.pitch); + ActorAccessibility_SetPitchBehindModifier(actor, slot, actor->policy.pitchModifier); + ActorAccessibility_SetSoundPos(actor, slot, &actor->projectedPos, actor->xyzDistToPlayer, actor->policy.distance); + ActorAccessibility_SetSoundVolume(actor, slot, actor->policy.volume); + actor->managedSoundSlots |= 1 << slot; +} +void ActorAccessibility_PlaySoundForActor(AccessibleActor* actor, int slot, s16 sfxId, bool looping) { + if (slot < 0 || slot > AAE_SLOTS_PER_HANDLE) + return; + ActorAccessibility_PlaySound(actor, slot, sfxId, looping); + ActorAccessibility_ConfigureSoundForActor(actor, slot); +} +void ActorAccessibility_StopSoundForActor(AccessibleActor* actor, int slot) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + ActorAccessibility_StopSound(actor, slot); + actor->managedSoundSlots &= ~(1 << slot); +} + +void ActorAccessibility_StopAllSoundsForActor(AccessibleActor* actor) { + ActorAccessibility_StopAllSounds(actor); + actor->managedSoundSlots = 0; +} + +bool ActorAccessibility_IsRealActor(AccessibleActor* actor) { + return actor->actor != NULL; +} +void ActorAccessibility_CopyParamsFromRealActor(AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + if (actor->actor == NULL) + return; + actor->projectedPos = actor->actor->projectedPos; + actor->xzDistToPlayer = actor->actor->xzDistToPlayer; + actor->isDrawn = actor->actor->isDrawn; + actor->xyzDistToPlayer = Math_Vec3f_DistXYZ(&actor->actor->world.pos, &player->actor.world.pos); +} + +void ActorAccessibility_StopAllVirtualActors(VirtualActorList* list) { + if (list == NULL) + return; + + VAList_t* val = (VAList_t*)list; + for (auto i = val->begin(); i != val->end(); i++) + ActorAccessibility_StopAllSounds((void*)&(*i)); +} + +void ActorAccessibility_RunAccessibilityForActor(PlayState* play, AccessibleActor* actor) { + actor->play = play; + if (ActorAccessibility_IsRealActor(actor)) { + ActorAccessibility_CopyParamsFromRealActor(actor); + } else { + Player* player = GET_PLAYER(play); + f32 w = 0.0f; + // Set actor->projectedPos. + SkinMatrix_Vec3fMtxFMultXYZW(&play->viewProjectionMtxF, &actor->world.pos, &actor->projectedPos, &w); + actor->xzDistToPlayer = Math_Vec3f_DistXZ(&actor->world.pos, &player->actor.world.pos); + actor->xyzDistToPlayer = Math_Vec3f_DistXYZ(&actor->world.pos, &player->actor.world.pos); + actor->yDistToPlayer = fabs((actor->world.pos.y) - (player->actor.world.pos.y)); + } + + if (actor->actor != NULL && fabs(actor->actor->yDistToPlayer) > actor->policy.ydist) { + return; + } + // Send sound parameters to the new audio engine. Eventually remove the old stuff once all actors are carried over. + for (int i = 0; i < AAE_SLOTS_PER_HANDLE; i++) { + if (actor->managedSoundSlots & (1 << i)) { + ActorAccessibility_SetSoundPos(actor, i, &actor->projectedPos, actor->xyzDistToPlayer, + actor->policy.distance); + // Judgement call: pitch changes are rare enough that it doesn't make sense to pay the cost of updating it + // every frame. If you want a pitch change, call the function as needed. + } + } + actor->frameCount++; + if (aa->glossary->GlossaryStarted) { + aa->glossary->frameCount++; + } + if (actor->frameCount % actor->policy.n) + return; + if (!actor->policy.runsAlways && actor->xyzDistToPlayer > actor->policy.distance) { + return; + } + if (actor->isDrawn == 0 && actor->actor->id != ACTOR_EN_IT && actor->actor->id != ACTOR_EN_OKARINA_TAG && + !aa->glossary->GlossaryStarted) + return; + if (actor->policy.aimAssist.isProvider) { + Player* player = GET_PLAYER(play); + if (player->stateFlags1 & PLAYER_STATE1_FIRST_PERSON && + (player->stateFlags1 & PLAYER_STATE1_USING_BOOMERANG || player->stateFlags1 & PLAYER_STATE1_ITEM_IN_HAND)) { + ActorAccessibility_SetSoundPitch(actor, 9, actor->aimAssist.pitch); + actor->aimAssist.framesSinceAimAssist++; + ActorAccessibility_ProvideAimAssistForActor(actor); + // The above will have taken care of setting the appropriate frequency and pitch, so we'll take care of the + // audio here based on those results. + if (actor->aimAssist.framesSinceAimAssist >= actor->aimAssist.frequency) { + + actor->aimAssist.framesSinceAimAssist = 0; + ActorAccessibility_PlaySoundForActor(actor, 9, actor->policy.aimAssist.sfx, false); + } + } else { + // Make sure there's no delay the next time you draw your bow or whatever. + actor->aimAssist.framesSinceAimAssist = 32768; + } + } + + if (actor->policy.callback != nullptr) { + actor->policy.callback(actor); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, actor->policy.sound, false); + } +} +void ActorAccessibility_RunAccessibilityForAllActors(PlayState* play) { + Player* player = GET_PLAYER(play); + if (play->sceneNum != aa->currentScene) { + ActorAccessibility_StopAllVirtualActors(aa->currentEverywhere); + ActorAccessibility_StopAllVirtualActors(aa->currentSceneGlobal); + ActorAccessibility_StopAllVirtualActors(aa->currentRoomLocal); + aa->currentEverywhere = ActorAccessibility_GetVirtualActorList(EVERYWHERE, 0); + aa->currentSceneGlobal = ActorAccessibility_GetVirtualActorList(play->sceneNum, -1); + aa->currentScene = play->sceneNum; + aa->currentRoomLocal = NULL; + aa->currentRoom = -1; + } + if (aa->currentRoom != play->roomCtx.curRoom.num) { + ActorAccessibility_StopAllVirtualActors(aa->currentRoomLocal); + aa->currentRoomLocal = ActorAccessibility_GetVirtualActorList(play->sceneNum, play->roomCtx.curRoom.num); + aa->currentRoom = play->roomCtx.curRoom.num; + } + if (aa->glossary->currentScene != play->sceneNum || aa->glossary->currentRoom != play->roomCtx.curRoom.num) { + if (aa->glossary->GlossaryStarted) { + aa->glossary->cooldown = 0; + aa->glossary->GlossaryStarted = false; + freezeActors = false; + } + } + if (player->stateFlags1 & PLAYER_STATE1_IN_CUTSCENE) { + return; + } + ActorAccessibility_AudioGlossary(play); + if (aa->glossary->GlossaryStarted) { + + return; + } + // Real actors. + for (AccessibleActorList_t::iterator i = aa->accessibleActorList.begin(); i != aa->accessibleActorList.end(); i++) + ActorAccessibility_RunAccessibilityForActor(play, &i->second); + // Virtual actors in the "everywhere" group. + VAList_t* list = (VAList_t*)aa->currentEverywhere; + + for (VAList_t::iterator i = list->begin(); i != list->end(); i++) + ActorAccessibility_RunAccessibilityForActor(play, &(*i)); + // Virtual actors for the current room and scene. + list = (VAList_t*)aa->currentRoomLocal; + for (VAList_t::iterator i = list->begin(); i != list->end(); i++) + ActorAccessibility_RunAccessibilityForActor(play, &(*i)); + // Scene-global virtual actors. Most of these are automatically generated VAs from polygons, because there's no way + // to sort these into rooms. + list = (VAList_t*)aa->currentSceneGlobal; + for (VAList_t::iterator i = list->begin(); i != list->end(); i++) + ActorAccessibility_RunAccessibilityForActor(play, &(*i)); + + // Processes external audio engine. + ActorAccessibility_PrepareNextAudioFrame(); +} + +void ActorAccessibility_AudioGlossary(PlayState* play) { + if (aa->glossary->GlossaryStarted) { + freezeActors = true; + AccessibleActor glossaryActor = (*aa->glossary->current).second; + ActorAccessibility_CopyParamsFromRealActor(&glossaryActor); + glossaryActor.policy.distance = glossaryActor.xzDistToPlayer * 3; + glossaryActor.policy.ydist = 1000; + glossaryActor.frameCount = aa->glossary->frameCount; + ActorAccessibility_RunAccessibilityForActor(play, &glossaryActor); + } + if (aa->glossary->cooldown != 0) { + aa->glossary->cooldown--; + return; + } + + OSContPad* trackerButtonsPressed = + std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetControlDeck())->GetPads(); + bool comboStartGlossary = trackerButtonsPressed != nullptr && trackerButtonsPressed[0].button & BTN_DUP && + trackerButtonsPressed[0].button & BTN_L; + if (comboStartGlossary) { + aa->glossary->GlossaryStarted = true; + aa->glossary->current = aa->accessibleActorList.begin(); + aa->glossary->currentScene = play->sceneNum; + aa->glossary->currentRoom = play->roomCtx.curRoom.num; + SpeechSynthesizer::Instance->Speak((*aa->glossary->current).second.policy.englishName, GetLanguageCode()); + return; + } + bool comboNextGlossary = trackerButtonsPressed != nullptr && trackerButtonsPressed[0].button & BTN_DRIGHT && + trackerButtonsPressed[0].button & BTN_L; + if (comboNextGlossary && aa->glossary->GlossaryStarted) { + aa->glossary->current++; + if (aa->glossary->current == aa->accessibleActorList.end()) { + aa->glossary->current = aa->accessibleActorList.begin(); + }; + aa->glossary->cooldown = 5; + SpeechSynthesizer::Instance->Speak((*aa->glossary->current).second.policy.englishName, GetLanguageCode()); + } + bool comboPrevGlossary = trackerButtonsPressed != nullptr && trackerButtonsPressed[0].button & BTN_DLEFT && + trackerButtonsPressed[0].button & BTN_L; + if (comboPrevGlossary && aa->glossary->GlossaryStarted) { + if (aa->glossary->current != aa->accessibleActorList.begin()) { + aa->glossary->current--; + } + aa->glossary->cooldown = 5; + + SpeechSynthesizer::Instance->Speak((*aa->glossary->current).second.policy.englishName, GetLanguageCode()); + } + bool comboDisableGlossary = trackerButtonsPressed != nullptr && trackerButtonsPressed[0].button & BTN_DDOWN && + trackerButtonsPressed[0].button & BTN_L; + if (comboDisableGlossary) { + aa->glossary->cooldown = 0; + aa->glossary->GlossaryStarted = false; + freezeActors = false; + } + // Processes external audio engine. + ActorAccessibility_PrepareNextAudioFrame(); +} + +// Virtual actor config. +VirtualActorList* ActorAccessibility_GetVirtualActorList(s16 sceneNum, s8 roomNum) { + SceneAndRoom sr; + sr.values.sceneIndex = sceneNum; + sr.values.roomIndex = roomNum; + if (sceneNum == EVERYWHERE) + sr.values.sceneIndex = EVERYWHERE; + + VAList_t* l = &aa->vaZones[sr.raw]; + return (VirtualActorList*)l; +} +AccessibleActor* ActorAccessibility_AddVirtualActor(VirtualActorList* list, VIRTUAL_ACTOR_TABLE type, PosRot where) { + ActorAccessibilityPolicy* policy = ActorAccessibility_GetPolicyForActor(type); + if (policy == NULL) + return NULL; + + AccessibleActor actor; + actor.actor = NULL; + actor.basePitch = 1.0; + actor.baseVolume = 1.0; + actor.currentPitch = 1.0; + actor.currentVolume = 1.0; + actor.frameCount = 0; + actor.id = (s16)type; + actor.instanceID = ActorAccessibility_GetNextID(); + actor.isDrawn = 1; + actor.play = NULL; + actor.world = where; + actor.sceneIndex = 0; + actor.managedSoundSlots = 0; + actor.aimAssist.framesSinceAimAssist = 0; + actor.aimAssist.frequency = 10; + actor.aimAssist.pitch = 1.0; + + actor.policy = *policy; + VAList_t* l = (VAList_t*)list; + l->push_back(actor); + size_t index = l->size() - 1; + AccessibleActor* savedActor = &(*l)[l->size() - 1]; + if (policy->initUserData && !policy->initUserData(savedActor)) { + l->pop_back(); + + return NULL; // Probably a malloc error preventing user data initialization. + } + return savedActor; +} +void ActorAccessibility_InterpretCurrentScene(PlayState* play) { + if (aa->sceneList.contains(play->sceneNum)) + return; // Scene interpretation already complete for this scene. + aa->sceneList.insert(play->sceneNum); + VirtualActorList* list = ActorAccessibility_GetVirtualActorList(play->sceneNum, -1); // Scene-global VAs. + if (list == NULL) + return; + for (int i = 0; i < play->colCtx.colHeader->numPolygons; i++) { + CollisionPoly* poly = &play->colCtx.colHeader->polyList[i]; + // checks if climable + if ((func_80041DB8(&play->colCtx, poly, BGCHECK_SCENE) == 8 || + func_80041DB8(&play->colCtx, poly, BGCHECK_SCENE) == 3)) { + ActorAccessibility_PolyToVirtualActor(play, poly, VA_CLIMB, list); + } + if (SurfaceType_IsWallDamage(&play->colCtx, poly, BGCHECK_SCENE)) { + ActorAccessibility_PolyToVirtualActor(play, poly, VA_SPIKE, list); + } + if (SurfaceType_GetSceneExitIndex(&play->colCtx, poly, BGCHECK_SCENE) != 0) + ActorAccessibility_PolyToVirtualActor(play, poly, VA_AREA_CHANGE, list); + /*s8 floorparam = func_80041D4C(&play->colCtx, poly, BGCHECK_SCENE); + if (floorparam == 2) { + ActorAccessibility_PolyToVirtualActor(play, poly, VA_SPIKE, list); + }*/ + } +} +// Convert poly to VA. +void ActorAccessibility_PolyToVirtualActor(PlayState* play, CollisionPoly* poly, VIRTUAL_ACTOR_TABLE va, + VirtualActorList* destination) { + Vec3f polyVerts[3]; + CollisionPoly_GetVertices(poly, play->colCtx.colHeader->vtxList, polyVerts); + PosRot where; + where.pos.y = std::min(polyVerts[0].y, std::min(polyVerts[1].y, polyVerts[2].y)); + f32 minX = std::min(polyVerts[0].x, std::min(polyVerts[1].x, polyVerts[2].x)); + f32 maxX = std::max(polyVerts[0].x, std::max(polyVerts[1].x, polyVerts[2].x)); + f32 minZ = std::min(polyVerts[0].z, std::min(polyVerts[1].z, polyVerts[2].z)); + f32 maxZ = std::max(polyVerts[0].z, std::max(polyVerts[1].z, polyVerts[2].z)); + where.pos.x = maxX - ((maxX - minX) / 2); + where.pos.z = maxZ - ((maxZ - minZ) / 2); + where.rot = { 0, 0, 0 }; + AccessibleActor* actor = ActorAccessibility_AddVirtualActor(destination, va, where); + if (actor == NULL) + return; + if (va == VA_AREA_CHANGE) { + actor->sceneIndex = SurfaceType_GetSceneExitIndex(&play->colCtx, poly, BGCHECK_SCENE); + s16 nextEntranceIndex = play->setupExitList[actor->sceneIndex - 1]; + actor->sceneIndex = gEntranceTable[nextEntranceIndex].scene; + } +} +void ActorAccessibility_AnnounceRoomNumber(PlayState* play) { + std::stringstream ss; + ss << "Room" << (int)play->roomCtx.curRoom.num; + if (Flags_GetClear(play, play->roomCtx.curRoom.num)) + ss << ", completed." << std::endl; + else + ss << "." << std::endl; + SpeechSynthesizer::Instance->Speak(ss.str().c_str(), GetLanguageCode()); +} +// Aim cue support. +void ActorAccessibility_ProvideAimAssistForActor(AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + s32 angle = player->actor.focus.rot.x; + angle = angle / -14000.0 * 16384; + f32 slope = Math_SinS(angle) / Math_CosS(angle) * 1.0; + s32 yIntercept = (slope * (actor->xzDistToPlayer)) + player->actor.focus.pos.y; + s32 yHight = actor->actor->world.pos.y + 25; + if (slope < 1) { + slope = 1; + } + s32 correction = (1 - 1 / slope) * 100; + if ((yIntercept) > yHight + 25) { + actor->aimAssist.pitch = 1.5; + } else if ((yIntercept) < yHight - 25) { + actor->aimAssist.pitch = 0.5; + } + s32 yDiff = fabs(yIntercept - yHight); + if (yIntercept - yHight > 0) { + yDiff -= correction; + if (yDiff < 0) { + yDiff = 0; + } + } + if (yDiff > 300) { + actor->aimAssist.frequency = 30; + } else { + actor->aimAssist.frequency = 1 + (uint8_t)(yDiff / 5); + } +} + +// External audio engine stuff. +bool ActorAccessibility_InitAudio() { + try { + aa->audioEngine = new AccessibleAudioEngine(); + } catch (...) { + aa->audioEngine = NULL; + return false; + } + return true; +} +void ActorAccessibility_ShutdownAudio() { + if (aa->isOn) { + delete aa->audioEngine; + aa->isOn = 0; + } +} +void ActorAccessibility_MixAccessibleAudioWithGameAudio(int16_t* ogBuffer, uint32_t nFrames) { + if (aa->isOn) { + aa->audioEngine->mix(ogBuffer, nFrames); + } +} +// Map one of the game's sfx to a path which as understood by the external audio engine. The returned token is a +// short hex string that can be passed directly to the audio engine. +const char* ActorAccessibility_MapSfxToExternalAudio(s16 sfxId) { + SfxRecord* record; + auto it = aa->sfxMap.find(sfxId); + if (it == aa->sfxMap.end()) { + SfxRecord tempRecord; + std::string fullPath = SfxExtractor::getExternalFileName(sfxId); + auto res = Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager()->LoadFile(fullPath); + + if (res == nullptr) + return NULL; // Resource doesn't exist, user's gotta run the extractor. + tempRecord.resource = res; + std::stringstream ss; + ss << std::setw(4) << std::setfill('0') << std::hex << sfxId; + tempRecord.path = ss.str(); + auto pair = aa->sfxMap.insert({ sfxId, tempRecord }); + record = &pair.first->second; + ma_resource_manager_register_decoded_data(&aa->audioEngine->resourceManager, record->path.c_str(), + record->resource->Buffer->data(), + record->resource->Buffer->size() / 2, ma_format_s16, 1, 44100); + } else { + record = &it->second; + } + + return record->path.c_str(); +} + +// Call once per frame to tell the audio engine to start working on the latest batch of queued instructions. +void ActorAccessibility_PrepareNextAudioFrame() { + aa->audioEngine->prepare(); +} + +void ActorAccessibility_HandleSoundExtractionMode(PlayState* play) { + aa->sfxExtractor.frameCallback(); +} + +void ActorAccessibility_DoSoundExtractionStep() { + aa->sfxExtractor.captureCallback(); +} diff --git a/soh/soh/Enhancements/accessible-actors/ActorAccessibility.h b/soh/soh/Enhancements/accessible-actors/ActorAccessibility.h new file mode 100644 index 000000000..218d04e87 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/ActorAccessibility.h @@ -0,0 +1,180 @@ +#pragma once +#include + +struct AccessibleActor; +// A callback that is run regularely as the game progresses in order to provide accessibility services for an actor. + +typedef void (*ActorAccessibilityCallback)(AccessibleActor*); +// A callback which allows AccessibleActor instances to initialize custom user data (called once per instantiation). +typedef bool (*ActorAccessibilityUserDataInit)(AccessibleActor*); +// A callback that can be used to clean up user data when an actor is destroyed. +typedef void (*ActorAccessibilityUserDataCleanup)(AccessibleActor*); + +struct VirtualActorList; + +struct ActorAccessibilityPolicy { + const char* englishName; + + ActorAccessibilityCallback callback; // If set, it will be called once every n frames. If null, then sfx will be + // played once every n frames. + s16 sound; // The ID of a sound to play. Ignored if the callback is set. + + int n; // How often to run the callback in frames. + f32 distance; // Maximum xz distance from player before the actor should be considered out of range. + f32 ydist; // Maximum y distance from player before the actor should be considered out of range. + f32 pitch; + f32 volume; + f32 pitchModifier; + bool runsAlways; // If set, then the distance policy is ignored. + ActorAccessibilityUserDataInit initUserData; + ActorAccessibilityUserDataCleanup cleanupUserData; + // Aim assist settings. + struct { + bool isProvider; // determines whether or not this actor supports aim assist. + s16 sfx; // The sound to play when this actor provides aim assist. Uses sound slot 9. + f32 tolerance; // How close to the center of the actor does Link have to aim for aim assist to consider + // it lined up. + } aimAssist; +}; + +// Accessible actor object. This can be a "real" actor (one that corresponds to an actual actor in the game) or a +// "virtual" actor (which does not actually exist in the game, but is used to create extra sounds for the player). +// One potential use of virtual actors is to place sounds at static platforms or other things that aren't represented by +// actors. + +struct AccessibleActor { + uint64_t instanceID; + + Actor* actor; // This can be null for a virtual actor. + s16 id; // For real actors, we copy the ID of the actor. For virtual actors we have our own table of values which + // are out of range for real actors. + f32 yDistToPlayer; + f32 xzDistToPlayer; + f32 xyzDistToPlayer; + PosRot world; + Vec3f projectedPos; + PlayState* play; + u8 isDrawn; // Do we just never play accessibility sounds for actors that aren't drawn? + + int frameCount; // Incremented every time the callback is called. The callback is free to modify this. Can be used + // to implement playback of sounds at regular intervals. + f32 baseVolume; + f32 currentVolume; + f32 basePitch; + + f32 currentPitch; + s16 sceneIndex; // If this actor represents a scene transition, then this will contain the destination scene index. + // Zero otherwise. + u16 managedSoundSlots; // These have their attenuation and panning parameters updated every frame automatically. + struct { + u16 framesSinceAimAssist; // Allows rate-based vertical aim assist. Incremented every frame for aim assist + // actors. Manually reset by aim assist provider. + f32 pitch; // Used to report whether Link is aiming higher or lower than the actor. + u8 frequency; // How often the sound will be played. Lower frequencies indicate that Link's vertical aim is + // closer to the actor. + } aimAssist; + + // Add more state as needed. + ActorAccessibilityPolicy policy; // A copy, so it can be customized on a per-actor basis if needed. + void* userData; // Set by the policy. Can be anything. +}; + +// Initialize accessibility. +void ActorAccessibility_Init(); +void ActorAccessibility_InitActors(); +void ActorAccessibility_Shutdown(); +void ActorAccessibility_InitPolicy(ActorAccessibilityPolicy* policy, const char* englishName, + ActorAccessibilityCallback callback); +void ActorAccessibility_InitPolicy(ActorAccessibilityPolicy* policy, const char* englishName, s16 sfx); + +void ActorAccessibility_TrackNewActor(Actor* actor); +void ActorAccessibility_RemoveTrackedActor(Actor* actor); +void ActorAccessibility_AddSupportedActor(s16 type, ActorAccessibilityPolicy policy); + +void ActorAccessibility_RunAccessibilityForActor(PlayState* play, AccessibleActor* actor); +void ActorAccessibility_RunAccessibilityForAllActors(PlayState* play); +/* + *Play sounds (usually from the game) using the external sound engine. This is probably not the function you want to + *call most of the time (see below). handle: pointer to an arbitrary object. This object can be anything as it's only + *used as a classifier, but it's recommended that you use an AccessibleActor* as your handle whenever possible. Using + *AccessibleActor* as the handle gives you automatic cleanup when the actor is killed. slot: Allows multiple sounds to + *be assigned to a single handle. The maximum number of slots per actor is 10 by default (but can be controlled by + *modifying AAE_SLOTS_PER_HANDLE). sfxId: one of the game's sfx IDs. Note that this plays prerendered sounds which you + *must have previously prepared. looping: whether to play the sound just once or on a continuous loop. + */ +void ActorAccessibility_PlaySound(void* actor, int slot, s16 sfxId, bool looping); +// Stop a sound. Todo: consider making this a short fade instead of just cutting it off. +void ActorAccessibility_StopSound(void* handle, int slot); +void ActorAccessibility_StopAllSounds(void* handle); + +void ActorAccessibility_SetSoundPitch(void* handle, int slot, float pitch); +// When we don't have access to something super fancy (such as HRTF), blind-accessible games generally use a change in +// pitch to tell the player that an object is behind the player. +void ActorAccessibility_SetPitchBehindModifier(void* handle, int slot, float mod); + +void ActorAccessibility_SetListenerPos(Vec3f* pos, Vec3f* rot); +void ActorAccessibility_SetSoundPos(void* handle, int slot, Vec3f* pos, f32 distToPlayer, f32 maxDistance); + +void ActorAccessibility_SetSoundVolume(void* handle, int slot, float volume); +void ActorAccessibility_SetSoundPan(void* handle, int slot, Vec3f* projectedPos); +void ActorAccessibility_SetSoundFilter(void* handle, int slot, float cutoff); +void ActorAccessibility_SeekSound(void* handle, int slot, size_t offset); + +/* + * Play a sound on behalf of an AccessibleActor. + * This version includes automatic sound management: pitch, panning and attenuation parameters will be updated + * automatically based on the actor's position. + * + */ +void ActorAccessibility_PlaySoundForActor(AccessibleActor* actor, int slot, s16 sfxId, bool looping); + +void ActorAccessibility_StopSoundForActor(AccessibleActor* actor, int slot); +void ActorAccessibility_StopAllSoundsForActor(AccessibleActor* actor); +f32 ActorAccessibility_ComputeCurrentVolume(f32 maxDistance, f32 xzDistToPlayer); +// Computes a relative angle based on Link's (or some other actor's) current angle. +Vec3s ActorAccessibility_ComputeRelativeAngle(Vec3s* origin, Vec3s* offset); +void ActorAccessibility_InitCues(); +// Stuff related to lists of virtual actors. +typedef enum { + // Similar to the game's actual actor table. Values here start at 10000 just to be extra safe. + VA_INITIAL = 1000, + VA_PROTOTYPE, // Remove this one once this thing is working. + VA_CRAWLSPACE, + VA_TERRAIN_CUE, + VA_WALL_CUE, + VA_CLIMB, + VA_DOOR, + VA_AREA_CHANGE, + VA_MARKER, + VA_SPIKE, + VA_GENERAL_HELPER, // Room announcements, action icon and other misc help. + VA_AUDIO_COMPASS, // Points north. + VA_STICK_WARNING, // beep when stick is about to burn out. + VA_MAX, +} VIRTUAL_ACTOR_TABLE; + +#define EVERYWHERE -32768 // Denotes a virtual actor that is global + +// Get the list of virtual actors for a given scene and room index. +VirtualActorList* ActorAccessibility_GetVirtualActorList(s16 sceneNum, s8 roomNum); +AccessibleActor* ActorAccessibility_AddVirtualActor(VirtualActorList* list, VIRTUAL_ACTOR_TABLE type, PosRot where); +// Parses the loaded seen and converts select polygons (like ladders, spikes and scene exits) into virtual actors. +void ActorAccessibility_InterpretCurrentScene(PlayState* play); +// Convert a collision polygon into a virtual actor. +void ActorAccessibility_PolyToVirtualActor(PlayState* play, CollisionPoly* poly, VIRTUAL_ACTOR_TABLE va, + VirtualActorList* destination); +// Report which room of a dungeon the player is in. +void ActorAccessibility_AnnounceRoomNumber(PlayState* play); +// Aim cue support. +void ActorAccessibility_ProvideAimAssistForActor(AccessibleActor* actor); +// External audio engine stuff. +// Initialize the accessible audio engine. +bool ActorAccessibility_InitAudio(); +void ActorAccessibility_ShutdownAudio(); +// Combine the games' audio with the output from AccessibleAudioEngine. To be called exclusively from the audio thread. +void ActorAccessibility_MixAccessibleAudioWithGameAudio(int16_t* ogBuffer, uint32_t nFrames); +void ActorAccessibility_HandleSoundExtractionMode(PlayState* play); +// This is called by the audio thread when it's ready to try to pull sfx from the game. +void ActorAccessibility_DoSoundExtractionStep(); + +void ActorAccessibility_AudioGlossary(PlayState* play); diff --git a/soh/soh/Enhancements/accessible-actors/SfxExtractor.cpp b/soh/soh/Enhancements/accessible-actors/SfxExtractor.cpp new file mode 100644 index 000000000..901dc6829 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/SfxExtractor.cpp @@ -0,0 +1,220 @@ +#include "SfxExtractor.h" +#include "soh/Enhancements/audio/miniaudio.h" +#include "soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h" +#include "soh/Enhancements/tts/tts.h" +#include "soh/OTRGlobals.h" +#include "SfxTable.h" +#include +extern "C" { +#include "z64.h" +#include "functions.h" +#include "variables.h" +void AudioMgr_CreateNextAudioBuffer(s16* samples, u32 num_samples); +extern bool freezeGame; +} + +bool SfxExtractor::isAllZero(int16_t* buffer, size_t count) { + for (size_t i = 0; i < count; i++) { + if (buffer[i] != 0) + return false; + } + return true; +} + +// Find the beginning of a captured signal. +size_t SfxExtractor::adjustedStartOfInput() { + size_t startOfInput = 0; + while (startOfInput + 2 < SFX_EXTRACTION_BUFFER_SIZE * 2 && + (tempBuffer[startOfInput] == 0 || tempBuffer[startOfInput + 1] == 0)) { + startOfInput += 2; + } + return startOfInput; +} + +size_t SfxExtractor::adjustedEndOfInput(size_t endOfInput) { + while (endOfInput > 0 && (tempBuffer[endOfInput] == 0 || tempBuffer[endOfInput - 1] == 0)) { + endOfInput -= 2; + } + return endOfInput; +} + +bool SfxExtractor::renderOutput(size_t endOfInput) { + size_t startOfInput = adjustedStartOfInput(); + endOfInput = adjustedEndOfInput(endOfInput); + if (endOfInput <= startOfInput) { + return true; + } + + ma_channel_converter_config config = + ma_channel_converter_config_init(ma_format_s16, 2, NULL, 1, NULL, ma_channel_mix_mode_default); + ma_channel_converter converter; + if (ma_channel_converter_init(&config, NULL, &converter) != MA_SUCCESS) { + return false; + } + std::vector fileData; + std::string fileName = getExternalFileName(currentSfx); + int16_t chunk[64]; + int16_t* mark = tempBuffer + startOfInput; + while (mark < tempBuffer + endOfInput) { + size_t chunkSize = std::min(64, ((tempBuffer + endOfInput) - mark) / 2); + ma_result converter_result = ma_channel_converter_process_pcm_frames(&converter, chunk, mark, chunkSize); + if (converter_result != MA_SUCCESS) { + return false; + } + fileData.insert(fileData.end(), (uint8_t*)chunk, (uint8_t*)(chunk + chunkSize)); + mark += chunkSize * 2; + } + return archive->WriteFile(fileName.c_str(), fileData); +} + +void SfxExtractor::setup() { + try { + SpeechSynthesizer::Instance->Speak( + "Sfx extraction speedrun initiated. Please wait. This will take a few minutes.", "en-US"); + // Kill the audio thread so we can take control. + captureThreadState = CT_WAITING; + OTRAudio_InstallSfxCaptureThread(); + // Make sure we're starting from a clean slate. + std::string sohAccessibilityPath = Ship::Context::GetPathRelativeToAppBundle("accessibility.o2r"); + if (std::filesystem::exists(sohAccessibilityPath)) { + currentStep = STEP_ERROR_FILE_EXISTS; + return; + } + + sfxToRip = 0; + currentStep = STEP_MAIN; + archive = std::make_shared(sohAccessibilityPath); + archive->Open(); + } catch (...) { currentStep = STEP_ERROR; } +} + +void SfxExtractor::ripNextSfx() { + { + auto lock = OTRAudio_Lock(); + if (captureThreadState == CT_READY || captureThreadState == CT_PRIMING) + return; // Keep going. + } + // Was the last sfx a loop? If so then we need to stop it, and then we need to run audio out to nowhere for as long + // as it takes to get back to a blank slate. + if (currentSfx != -1) { + Audio_StopSfxByPos(&gSfxDefaultPos); + captureThreadState = CT_PRIMING; + currentSfx = -1; + + return; + } + if (sfxToRip == sfxCount) { + currentStep = STEP_FINISHED; // Caught 'em all! + return; + } + + currentSfx = sfxTable[sfxToRip++]; + Audio_PlaySoundGeneral(currentSfx, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale, + &gSfxDefaultReverb); + + { + auto lock = OTRAudio_Lock(); + captureThreadState = CT_READY; + } + maybeGiveProgressReport(); +} +void SfxExtractor::finished() { + OTRAudio_UninstallSfxCaptureThread(); // Returns to normal audio opperation. + CVarClear("gExtractSfx"); + CVarSave(); + archive->Close(); + archive = nullptr; + freezeGame = false; + + Audio_QueueSeqCmd(NA_BGM_TITLE); + + if (currentStep >= STEP_ERROR) { + Audio_PlaySoundGeneral(NA_SE_SY_ERROR, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, + &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); + Audio_PlaySoundGeneral(NA_SE_EN_GANON_LAUGH, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, + &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); + std::stringstream ss; + ss << "Sorry, we tried to extract the sound effects, but Ganondorf overruled us with an iron fist." + << std::endl; + if (currentStep == STEP_ERROR_FILE_EXISTS) + ss << "In all seriousness, please delete accessibility.o2r and try again."; + SpeechSynthesizer::Instance->Speak(ss.str().c_str(), "en-US"); + } else + Audio_PlayFanfare(NA_BGM_ITEM_GET); +} +void SfxExtractor::maybeGiveProgressReport() { + for (int i = 0; i < 9; i++) { + if (sfxToRip == sfxCount * (i + 1) / 10) { + std::stringstream ss; + ss << (i + 1) * 10 << " percent complete."; + SpeechSynthesizer::Instance->Speak(ss.str().c_str(), "en-US"); + } + } +} + +SfxExtractor::SfxExtractor() { + currentStep = STEP_SETUP; +} + +void SfxExtractor::frameCallback() { + switch (currentStep) { + case STEP_SETUP: + setup(); + break; + case STEP_MAIN: + ripNextSfx(); + break; + default: // Handles finished as well as a number of error conditions. + finished(); + } +} + +void SfxExtractor::prime() { + while (true) { + AudioMgr_CreateNextAudioBuffer(tempBuffer + 0, SFX_EXTRACTION_ONE_FRAME); + if (isAllZero(tempBuffer + 0, SFX_EXTRACTION_ONE_FRAME * 2)) + break; + } + captureThreadState = CT_FINISHED; +} + +void SfxExtractor::captureCallback() { + if (captureThreadState == CT_PRIMING) + prime(); + if (captureThreadState != CT_READY) + return; // No work to do at the moment. + memset(tempBuffer, 0, SFX_EXTRACTION_BUFFER_SIZE * 4); + int16_t* mark = tempBuffer + 0; + size_t samplesLeft = SFX_EXTRACTION_BUFFER_SIZE; + bool outputStarted = false; + int waitTime = 0; + while (samplesLeft > 0) { + AudioMgr_CreateNextAudioBuffer(mark, SFX_EXTRACTION_ONE_FRAME); + + if (isAllZero(mark, SFX_EXTRACTION_ONE_FRAME * 2)) { + if (outputStarted) { + break; + } else if (waitTime++ < 300) { + continue; // Output is silent, allow more time for audio to begin. + } + captureThreadState = CT_FINISHED; // Sound is unavailable, so skip over it and move on. + return; + } + + outputStarted = true; + size_t samples = std::min(SFX_EXTRACTION_ONE_FRAME, samplesLeft); + mark += samples * 2; + samplesLeft -= samples; + } + if (renderOutput(mark - tempBuffer)) { + captureThreadState = CT_FINISHED; + } else { + SPDLOG_ERROR("failed to write file to archive, trying again"); + } +} + +std::string SfxExtractor::getExternalFileName(int16_t sfxId) { + std::stringstream ss; + ss << "accessibility/audio/" << std::hex << std::setw(4) << std::setfill('0') << sfxId << ".wav"; + return ss.str(); +} diff --git a/soh/soh/Enhancements/accessible-actors/SfxExtractor.h b/soh/soh/Enhancements/accessible-actors/SfxExtractor.h new file mode 100644 index 000000000..34d62a043 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/SfxExtractor.h @@ -0,0 +1,49 @@ +#pragma once +#include "libultraship/libultraship.h" + +#define SFX_EXTRACTION_BUFFER_SIZE 44100 * 15 +#define SFX_EXTRACTION_ONE_FRAME 736 + +enum CaptureThreadStates { + CT_WAITING, // for a sound to start ripping. + CT_PRIMING, + CT_READY, // to start ripping a sound. + CT_FINISHED, // ripping the current sound. + CT_SHUTDOWN, +}; + +enum SfxExtractionSteps { + STEP_SETUP = 0, + STEP_MAIN, + STEP_FINISHED, + STEP_ERROR, + STEP_ERROR_FILE_EXISTS, +}; + +class SfxExtractor { + std::shared_ptr archive; + SfxExtractionSteps currentStep; + CaptureThreadStates captureThreadState; + int sfxToRip; + s16 currentSfx; + // Stores raw audio data for the sfx currently being ripped. + int16_t tempBuffer[(SFX_EXTRACTION_BUFFER_SIZE + SFX_EXTRACTION_ONE_FRAME * 3) * 2]; + // Check if a buffer contains meaningful audio output. + bool isAllZero(int16_t* buffer, size_t count); + size_t adjustedStartOfInput(); + size_t adjustedEndOfInput(size_t endOfInput); + bool renderOutput(size_t endOfInput); + void setup(); + void ripNextSfx(); + void finished(); // Also handles failure. + void maybeGiveProgressReport(); + + public: + SfxExtractor(); + + void frameCallback(); + void prime(); + // The below is called by the (hijacked) audio thread. + void captureCallback(); + static std::string getExternalFileName(int16_t sfxId); +}; diff --git a/soh/soh/Enhancements/accessible-actors/SfxTable.h b/soh/soh/Enhancements/accessible-actors/SfxTable.h new file mode 100644 index 000000000..4933895a6 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/SfxTable.h @@ -0,0 +1,1215 @@ +#pragma once +// A big nasty array containing every SFX ID in the game. +// All of the "DUMMY's" and "YOBI's" have been removed as they produce duplicate sounds at best, and cause errors or +// even crashes at worst. + +const s16 sfxTable[1207] = { + NA_SE_PL_WALK_GROUND, + NA_SE_PL_WALK_SAND, + NA_SE_PL_WALK_CONCRETE, + NA_SE_PL_WALK_DIRT, + NA_SE_PL_WALK_WATER0, + NA_SE_PL_WALK_WATER1, + NA_SE_PL_WALK_WATER2, + NA_SE_PL_WALK_MAGMA, + NA_SE_PL_WALK_GRASS, + NA_SE_PL_WALK_IRON, + NA_SE_PL_WALK_LADDER, + NA_SE_PL_WALK_GLASS, + NA_SE_PL_WALK_WALL, + NA_SE_PL_WALK_HEAVYBOOTS, + NA_SE_PL_WALK_ICE, + NA_SE_PL_JUMP, + NA_SE_PL_JUMP_SAND, + NA_SE_PL_JUMP_CONCRETE, + NA_SE_PL_JUMP_DIRT, + NA_SE_PL_JUMP_WATER0, + NA_SE_PL_JUMP_WATER1, + NA_SE_PL_JUMP_WATER2, + NA_SE_PL_JUMP_MAGMA, + NA_SE_PL_JUMP_GRASS, + NA_SE_PL_JUMP_IRON, + NA_SE_PL_JUMP_LADDER, + NA_SE_PL_JUMP_GLASS, + NA_SE_PL_JUMP_HEAVYBOOTS, + NA_SE_PL_JUMP_ICE, + NA_SE_PL_LAND, + NA_SE_PL_LAND_SAND, + NA_SE_PL_LAND_CONCRETE, + NA_SE_PL_LAND_DIRT, + NA_SE_PL_LAND_WATER0, + NA_SE_PL_LAND_WATER1, + NA_SE_PL_LAND_WATER2, + NA_SE_PL_LAND_MAGMA, + NA_SE_PL_LAND_GRASS, + NA_SE_PL_LAND_IRON, + NA_SE_PL_LAND_LADDER, + NA_SE_PL_LAND_GLASS, + NA_SE_PL_LAND_HEAVYBOOTS, + NA_SE_PL_LAND_ICE, + NA_SE_PL_SLIPDOWN, + NA_SE_PL_CLIMB_CLIFF, + NA_SE_PL_SIT_ON_HORSE, + NA_SE_PL_GET_OFF_HORSE, + NA_SE_PL_TAKE_OUT_SHIELD, + NA_SE_PL_CHANGE_ARMS, + NA_SE_PL_CATCH_BOOMERANG, + NA_SE_PL_SWIM, + NA_SE_PL_THROW, + NA_SE_PL_BODY_BOUND, + NA_SE_PL_ROLL, + NA_SE_PL_SKIP, + NA_SE_PL_BODY_HIT, + NA_SE_PL_DAMAGE, + NA_SE_PL_SLIP, + NA_SE_PL_SLIP_SAND, + NA_SE_PL_SLIP_CONCRETE, + NA_SE_PL_SLIP_DIRT, + NA_SE_PL_SLIP_WATER0, + NA_SE_PL_SLIP_WATER1, + NA_SE_PL_SLIP_WATER2, + NA_SE_PL_SLIP_MAGMA, + NA_SE_PL_SLIP_GRASS, + NA_SE_PL_SLIP_IRON, + NA_SE_PL_SLIP_LADDER, + NA_SE_PL_SLIP_GLASS, + NA_SE_PL_SLIP_HEAVYBOOTS, + NA_SE_PL_SLIP_ICE, + NA_SE_PL_BOUND, + NA_SE_PL_BOUND_SAND, + NA_SE_PL_BOUND_CONCRETE, + NA_SE_PL_BOUND_DIRT, + NA_SE_PL_BOUND_WATER0, + NA_SE_PL_BOUND_WATER1, + NA_SE_PL_BOUND_WATER2, + NA_SE_PL_BOUND_MAGMA, + NA_SE_PL_BOUND_GRASS, + NA_SE_PL_BOUND_IRON, + NA_SE_PL_BOUND_LADDER, + NA_SE_PL_BOUND_WOOD, + NA_SE_PL_BOUND_HEAVYBOOTS, + NA_SE_PL_BOUND_ICE, + NA_SE_PL_FACE_UP, + NA_SE_PL_DIVE_BUBBLE, + NA_SE_PL_MOVE_BUBBLE, + NA_SE_PL_METALEFFECT_KID, + NA_SE_PL_METALEFFECT_ADULT, + NA_SE_PL_SPARK, + NA_SE_PL_PULL_UP_PLANT, + NA_SE_PL_PULL_UP_ROCK, + NA_SE_PL_IN_BUBBLE, + NA_SE_PL_PULL_UP_BIGROCK, + NA_SE_PL_SWORD_CHARGE, + NA_SE_PL_FREEZE, + NA_SE_PL_PULL_UP_POT, + NA_SE_PL_KNOCK, + NA_SE_PL_CALM_HIT, + NA_SE_PL_CALM_PAT, + NA_SE_PL_SUBMERGE, + NA_SE_PL_FREEZE_S, + NA_SE_PL_ICE_BROKEN, + NA_SE_PL_SLIP_ICE_LELEL, + NA_SE_PL_PUT_OUT_ITEM, + NA_SE_PL_PULL_UP_WOODBOX, + NA_SE_PL_MAGIC_FIRE, + NA_SE_PL_MAGIC_WIND_NORMAL, + NA_SE_PL_MAGIC_WIND_WARP, + NA_SE_PL_MAGIC_SOUL_NORMAL, + NA_SE_PL_ARROW_CHARGE_FIRE, + NA_SE_PL_ARROW_CHARGE_ICE, + NA_SE_PL_ARROW_CHARGE_LIGHT, + NA_SE_PL_PULL_UP_RUTO, + NA_SE_PL_CRAWL, + NA_SE_PL_CRAWL_SAND, + NA_SE_PL_CRAWL_CONCRETE, + NA_SE_PL_CRAWL_DIRT, + NA_SE_PL_CRAWL_WATER0, + NA_SE_PL_CRAWL_WOOD, + NA_SE_PL_CRAWL_ICE, + NA_SE_PL_MAGIC_SOUL_FLASH, + NA_SE_PL_ROLL_DUST, + NA_SE_PL_MAGIC_SOUL_BALL, + NA_SE_PL_SPIRAL_HEAL_BEAM, + NA_SE_PL_BOUND_NOWEAPON, + NA_SE_PL_PLANT_GROW_UP, + NA_SE_PL_PLANT_TALLER, + NA_SE_PL_MAGIC_WIND_VANISH, + NA_SE_PL_HOBBERBOOTS_LV, + NA_SE_PL_PLANT_MOVE, + NA_SE_EV_WALL_MOVE_SP, + NA_SE_PL_SLIP_LEVEL, + NA_SE_PL_SLIP_SAND_LEVEL, + NA_SE_PL_SLIP_CONCRETE_LEVEL, + NA_SE_PL_SLIP_DIRT_LEVEL, + NA_SE_PL_SLIP_WATER0_LEVEL, + NA_SE_PL_SLIP_WATER1_LEVEL, + NA_SE_PL_SLIP_WATER2_LEVEL, + NA_SE_PL_SLIP_MAGMA_LEVEL, + NA_SE_PL_SLIP_GRASS_LEVEL, + NA_SE_PL_SLIP_IRON_LEVEL, + NA_SE_PL_SLIP_GLASS_LEVEL, + NA_SE_PL_SLIP_WOOD_LEVEL, + NA_SE_PL_SLIP_HEAVYBOOTS_LEVEL, + NA_SE_PL_SLIP_ICE_LEVEL, + NA_SE_PL_JUMP_METAL, + NA_SE_PL_LAND_METAL, + NA_SE_PL_WALK_RUNNINGMAN, + NA_SE_PL_WALK_ZELDA_DEMO, + NA_SE_IT_SWORD_IMPACT, + NA_SE_IT_SWORD_SWING, + NA_SE_IT_SWORD_PUTAWAY, + NA_SE_IT_SWORD_PICKOUT, + NA_SE_IT_ARROW_SHOT, + NA_SE_IT_BOOMERANG_THROW, + NA_SE_IT_SHIELD_BOUND, + NA_SE_IT_BOW_DRAW, + NA_SE_IT_SHIELD_REFLECT_SW, + NA_SE_IT_ARROW_STICK_HRAD, + NA_SE_IT_HAMMER_HIT, + NA_SE_IT_HOOKSHOT_CHAIN, + NA_SE_IT_SHIELD_REFLECT_MG, + NA_SE_IT_BOMB_IGNIT, + NA_SE_IT_BOMB_EXPLOSION, + NA_SE_IT_BOMB_UNEXPLOSION, + NA_SE_IT_BOOMERANG_FLY, + NA_SE_IT_SWORD_STRIKE, + NA_SE_IT_HAMMER_SWING, + NA_SE_IT_HOOKSHOT_REFLECT, + NA_SE_IT_ARROW_STICK_CRE, + NA_SE_IT_ARROW_STICK_OBJ, + NA_SE_IT_SWORD_SWING_HARD, + NA_SE_IT_WALL_HIT_HARD, + NA_SE_IT_WALL_HIT_SOFT, + NA_SE_IT_STONE_HIT, + NA_SE_IT_WOODSTICK_BROKEN, + NA_SE_IT_LASH, + NA_SE_IT_SHIELD_POSTURE, + NA_SE_IT_SLING_SHOT, + NA_SE_IT_SLING_DRAW, + NA_SE_IT_SWORD_CHARGE, + NA_SE_IT_ROLLING_CUT, + NA_SE_IT_SWORD_STRIKE_HARD, + NA_SE_IT_SLING_REFLECT, + NA_SE_IT_SHIELD_REMOVE, + NA_SE_IT_HOOKSHOT_READY, + NA_SE_IT_HOOKSHOT_RECEIVE, + NA_SE_IT_HOOKSHOT_STICK_OBJ, + NA_SE_IT_SWORD_REFLECT_MG, + NA_SE_IT_DEKU, + NA_SE_IT_WALL_HIT_BUYO, + NA_SE_IT_SWORD_PUTAWAY_STN, + NA_SE_IT_ROLLING_CUT_LV1, + NA_SE_IT_ROLLING_CUT_LV2, + NA_SE_IT_BOW_FLICK, + NA_SE_IT_BOMBCHU_MOVE, + NA_SE_IT_SHIELD_CHARGE_LV1, + NA_SE_IT_SHIELD_CHARGE_LV2, + NA_SE_IT_SHIELD_CHARGE_LV3, + NA_SE_IT_SLING_FLICK, + NA_SE_IT_SWORD_STICK_STN, + NA_SE_IT_REFLECTION_WOOD, + NA_SE_IT_SHIELD_REFLECT_MG2, + NA_SE_IT_MAGIC_ARROW_SHOT, + NA_SE_IT_EXPLOSION_FRAME, + NA_SE_IT_EXPLOSION_ICE, + NA_SE_IT_EXPLOSION_LIGHT, + NA_SE_IT_FISHING_REEL_SLOW, + NA_SE_IT_FISHING_REEL_HIGH, + NA_SE_IT_PULL_FISHING_ROD, + NA_SE_IT_DM_FLYING_GOD_PASS, + NA_SE_IT_DM_FLYING_GOD_DASH, + NA_SE_IT_DM_RING_EXPLOSION, + NA_SE_IT_DM_RING_GATHER, + NA_SE_IT_INGO_HORSE_NEIGH, + NA_SE_IT_EARTHQUAKE, + NA_SE_IT_KAKASHI_JUMP, + NA_SE_IT_FLAME, + NA_SE_IT_SHIELD_BEAM, + NA_SE_IT_FISHING_HIT, + NA_SE_IT_GOODS_APPEAR, + NA_SE_IT_MAJIN_SWORD_BROKEN, + NA_SE_IT_HAND_CLAP, + NA_SE_IT_MASTER_SWORD_SWING, + NA_SE_FISHING_REEL_SLOW2, + NA_SE_IT_SPIDERNET_HIT1, + NA_SE_IT_LURE_LAND1, + NA_SE_IT_HOOKSHOT_STICK_OBJ_WATER, + NA_SE_IT_SWORD_PICKOUT_GANON, + NA_SE_IT_BOMB_IGNIT_DODO_M, + NA_SE_EV_DOOR_OPEN, + NA_SE_EV_DOOR_CLOSE, + NA_SE_EV_EXPLOSION, + NA_SE_EV_HORSE_WALK, + NA_SE_EV_HORSE_RUN, + NA_SE_EV_HORSE_NEIGH, + NA_SE_EV_RIVER_STREAM, + NA_SE_EV_WATER_WALL_BIG, + NA_SE_EV_OUT_OF_WATER, + NA_SE_EV_DIVE_WATER, + NA_SE_EV_ROCK_SLIDE, + NA_SE_EV_MAGMA_LEVEL, + NA_SE_EV_BRIDGE_OPEN, + NA_SE_EV_BRIDGE_CLOSE, + NA_SE_EV_BRIDGE_OPEN_STOP, + NA_SE_EV_BRIDGE_CLOSE_STOP, + NA_SE_EV_WALL_BROKEN, + NA_SE_EV_CHICKEN_CRY_N, + NA_SE_EV_CHICKEN_CRY_A, + NA_SE_EV_CHICKEN_CRY_M, + NA_SE_EV_SLIDE_DOOR_OPEN, + NA_SE_EV_FOOT_SWITCH, + NA_SE_EV_HORSE_GROAN, + NA_SE_EV_BOMB_DROP_WATER, + NA_SE_EV_HORSE_JUMP, + NA_SE_EV_HORSE_LAND, + NA_SE_EV_HORSE_SLIP, + NA_SE_EV_FAIRY_DASH, + NA_SE_EV_SLIDE_DOOR_CLOSE, + NA_SE_EV_STONE_BOUND, + NA_SE_EV_STONE_STATUE_OPEN, + NA_SE_EV_TBOX_UNLOCK, + NA_SE_EV_TBOX_OPEN, + NA_SE_SY_TIMER, + NA_SE_EV_FLAME_IGNITION, + NA_SE_EV_SPEAR_HIT, + NA_SE_EV_ELEVATOR_MOVE, + NA_SE_EV_WARP_HOLE, + NA_SE_EV_LINK_WARP, + NA_SE_EV_PILLAR_SINK, + NA_SE_EV_WATER_WALL, + NA_SE_EV_RIVER_STREAM_S, + NA_SE_EV_RIVER_STREAM_F, + NA_SE_EV_HORSE_LAND2, + NA_SE_EV_HORSE_SANDDUST, + NA_SE_EV_LIGHTNING, + NA_SE_EV_BOMB_BOUND, + NA_SE_EV_WATERDROP, + NA_SE_EV_TORCH, + NA_SE_EV_MAGMA_LEVEL_M, + NA_SE_EV_FIRE_PILLAR, + NA_SE_EV_FIRE_PLATE, + NA_SE_EV_BLOCK_BOUND, + NA_SE_EV_METALDOOR_SLIDE, + NA_SE_EV_METALDOOR_STOP, + NA_SE_EV_BLOCK_SHAKE, + NA_SE_EV_BOX_BREAK, + NA_SE_EV_HAMMER_SWITCH, + NA_SE_EV_MAGMA_LEVEL_L, + NA_SE_EV_SPEAR_FENCE, + NA_SE_EV_GANON_HORSE_NEIGH, + NA_SE_EV_GANON_HORSE_GROAN, + NA_SE_EV_FANTOM_WARP_S, + NA_SE_EV_FANTOM_WARP_L, + NA_SE_EV_FOUNTAIN, + NA_SE_EV_KID_HORSE_WALK, + NA_SE_EV_KID_HORSE_RUN, + NA_SE_EV_KID_HORSE_NEIGH, + NA_SE_EV_KID_HORSE_GROAN, + NA_SE_EV_WHITE_OUT, + NA_SE_EV_LIGHT_GATHER, + NA_SE_EV_TREE_CUT, + NA_SE_EV_VOLCANO, + NA_SE_EV_GUILLOTINE_UP, + NA_SE_EV_GUILLOTINE_BOUND, + NA_SE_EV_ROLLCUTTER_MOTOR, + NA_SE_EV_CHINETRAP_DOWN, + NA_SE_EV_PLANT_BROKEN, + NA_SE_EV_SHIP_BELL, + NA_SE_EV_FLUTTER_FLAG, + NA_SE_EV_TRAP_BOUND, + NA_SE_EV_ROCK_BROKEN, + NA_SE_EV_FANTOM_WARP_S2, + NA_SE_EV_FANTOM_WARP_L2, + NA_SE_EV_COFFIN_CAP_OPEN, + NA_SE_EV_COFFIN_CAP_BOUND, + NA_SE_EV_WIND_TRAP, + NA_SE_EV_TRAP_OBJ_SLIDE, + NA_SE_EV_METALDOOR_OPEN, + NA_SE_EV_METALDOOR_CLOSE, + NA_SE_EV_BURN_OUT, + NA_SE_EV_BLOCKSINK, + NA_SE_EV_CROWD, + NA_SE_EV_WATER_LEVEL_DOWN, + NA_SE_EV_NAVY_VANISH, + NA_SE_EV_LADDER_DOUND, + NA_SE_EV_WEB_VIBRATION, + NA_SE_EV_WEB_BROKEN, + NA_SE_EV_ROLL_STAND, + NA_SE_EV_BUYODOOR_OPEN, + NA_SE_EV_BUYODOOR_CLOSE, + NA_SE_EV_WOODDOOR_OPEN, + NA_SE_EV_METALGATE_OPEN, + NA_SE_IT_SCOOP_UP_WATER, + NA_SE_EV_FISH_LEAP, + NA_SE_EV_KAKASHI_SWING, + NA_SE_EV_KAKASHI_ROLL, + NA_SE_EV_BOTTLE_CAP_OPEN, + NA_SE_EV_JABJAB_BREATHE, + NA_SE_EV_SPIRIT_STONE, + NA_SE_EV_TRIFORCE_FLASH, + NA_SE_EV_FALL_DOWN_DIRT, + NA_SE_EV_NAVY_FLY, + NA_SE_EV_NAVY_CRASH, + NA_SE_EV_WOOD_HIT, + NA_SE_EV_SCOOPUP_WATER, + NA_SE_EV_DROP_FALL, + NA_SE_EV_WOOD_GEAR, + NA_SE_EV_TREE_SWING, + NA_SE_EV_HORSE_RUN_LEVEL, + NA_SE_EV_ELEVATOR_MOVE2, + NA_SE_EV_ELEVATOR_STOP, + NA_SE_EV_TRE_BOX_APPEAR, + NA_SE_EV_CHAIN_KEY_UNLOCK, + NA_SE_EV_SPINE_TRAP_MOVE, + NA_SE_EV_HEALING, + NA_SE_EV_GREAT_FAIRY_APPEAR, + NA_SE_EV_GREAT_FAIRY_VANISH, + NA_SE_EV_RED_EYE, + NA_SE_EV_ROLL_STAND_2, + NA_SE_EV_WALL_SLIDE, + NA_SE_EV_TRE_BOX_FLASH, + NA_SE_EV_WINDMILL_LEVEL, + NA_SE_EV_GOTO_HEAVEN, + NA_SE_EV_POT_BROKEN, + NA_SE_PL_PUT_DOWN_POT, + NA_SE_EV_DIVE_INTO_WATER, + NA_SE_EV_JUMP_OUT_WATER, + NA_SE_EV_GOD_FLYING, + NA_SE_EV_TRIFORCE, + NA_SE_EV_AURORA, + NA_SE_EV_DEKU_DEATH, + NA_SE_EV_BUYOSTAND_RISING, + NA_SE_EV_BUYOSTAND_FALL, + NA_SE_EV_BUYOSHUTTER_OPEN, + NA_SE_EV_BUYOSHUTTER_CLOSE, + NA_SE_EV_STONEDOOR_STOP, + NA_SE_EV_S_STONE_REVIVAL, + NA_SE_EV_MEDAL_APPEAR_S, + NA_SE_EV_HUMAN_BOUND, + NA_SE_EV_MEDAL_APPEAR_L, + NA_SE_EV_EARTHQUAKE, + NA_SE_EV_SHUT_BY_CRYSTAL, + NA_SE_EV_GOD_LIGHTBALL_2, + NA_SE_EV_RUN_AROUND, + NA_SE_EV_CONSENTRATION, + NA_SE_EV_TIMETRIP_LIGHT, + NA_SE_EV_BUYOSTAND_STOP_A, + NA_SE_EV_BUYOSTAND_STOP_U, + NA_SE_EV_OBJECT_FALL, + NA_SE_EV_JUMP_CONC, + NA_SE_EV_ICE_MELT, + NA_SE_EV_FIRE_PILLAR_S, + NA_SE_EV_BLOCK_RISING, + NA_SE_EV_NABALL_VANISH, + NA_SE_EV_SARIA_MELODY, + NA_SE_EV_LINK_WARP_OUT, + NA_SE_EV_FIATY_HEAL, + NA_SE_EV_CHAIN_KEY_UNLOCK_B, + NA_SE_EV_WOODBOX_BREAK, + NA_SE_EV_PUT_DOWN_WOODBOX, + NA_SE_EV_LAND_DIRT, + NA_SE_EV_FLOOR_ROLLING, + NA_SE_EV_DOG_CRY_EVENING, + NA_SE_EV_JABJAB_HICCUP, + NA_SE_EV_NALE_MAGIC, + NA_SE_EV_FROG_JUMP, + NA_SE_EV_ICE_FREEZE, + NA_SE_EV_BURNING, + NA_SE_EV_WOODPLATE_BOUND, + NA_SE_EV_GORON_WATER_DROP, + NA_SE_EV_JABJAB_GROAN, + NA_SE_EV_DARUMA_VANISH, + NA_SE_EV_BIGBALL_ROLL, + NA_SE_EV_ELEVATOR_MOVE3, + NA_SE_EV_DIAMOND_SWITCH, + NA_SE_EV_FLAME_OF_FIRE, + NA_SE_EV_RAINBOW_SHOWER, + NA_SE_EV_FLYING_AIR, + NA_SE_EV_PASS_AIR, + NA_SE_EV_COME_UP_DEKU_JR, + NA_SE_EV_SAND_STORM, + NA_SE_EV_TRIFORCE_MARK, + NA_SE_EV_GRAVE_EXPLOSION, + NA_SE_EV_LURE_MOVE_W, + NA_SE_EV_POT_MOVE_START, + NA_SE_EV_DIVE_INTO_WATER_L, + NA_SE_EV_OUT_OF_WATER_L, + NA_SE_EV_GANON_MANTLE, + NA_SE_EV_DIG_UP, + NA_SE_EV_WOOD_BOUND, + NA_SE_EV_WATER_BUBBLE, + NA_SE_EV_ICE_BROKEN, + NA_SE_EV_FROG_GROW_UP, + NA_SE_EV_WATER_CONVECTION, + NA_SE_EV_GROUND_GATE_OPEN, + NA_SE_EV_FACE_BREAKDOWN, + NA_SE_EV_FACE_EXPLOSION, + NA_SE_EV_FACE_CRUMBLE_SLOW, + NA_SE_EV_ROUND_TRAP_MOVE, + NA_SE_EV_HIT_SOUND, + NA_SE_EV_ICE_SWING, + NA_SE_EV_DOWN_TO_GROUND, + NA_SE_EV_KENJA_ENVIROMENT_0, + NA_SE_EV_KENJA_ENVIROMENT_1, + NA_SE_EV_SMALL_DOG_BARK, + NA_SE_EV_ZELDA_POWER, + NA_SE_EV_RAIN, + NA_SE_EV_IRON_DOOR_OPEN, + NA_SE_EV_IRON_DOOR_CLOSE, + NA_SE_EV_WHIRLPOOL, + NA_SE_EV_TOWER_PARTS_BROKEN, + NA_SE_EV_COW_CRY, + NA_SE_EV_METAL_BOX_BOUND, + NA_SE_EV_ELECTRIC_EXPLOSION, + NA_SE_EV_HEAVY_THROW, + NA_SE_EV_FROG_CRY_0, + NA_SE_EV_FROG_CRY_1, + NA_SE_EV_COW_CRY_LV, + NA_SE_EV_RONRON_DOOR_CLOSE, + NA_SE_EV_BUTTERFRY_TO_FAIRY, + NA_SE_EV_FIVE_COUNT_LUPY, + NA_SE_EV_STONE_GROW_UP, + NA_SE_EV_STONE_LAUNCH, + NA_SE_EV_STONE_ROLLING, + NA_SE_EV_TOGE_STICK_ROLLING, + NA_SE_EV_TOWER_ENERGY, + NA_SE_EV_TOWER_BARRIER, + NA_SE_EV_CHIBI_WALK, + NA_SE_EV_KNIGHT_WALK, + NA_SE_EV_PILLAR_MOVE_STOP, + NA_SE_EV_ERUPTION_CLOUD, + NA_SE_EV_LINK_WARP_OUT_LV, + NA_SE_EV_LINK_WARP_IN, + NA_SE_EV_OCARINA_BMELO_0, + NA_SE_EV_OCARINA_BMELO_1, + NA_SE_EV_EXPLOSION_FOR_RENZOKU, + NA_SE_EV_ELEVATOR_MOVE_KABE1, + NA_SE_EV_RIVER_STREAM_F_IDO, + NA_SE_EV_GUILLOTINE_BOUND_copyOrigin, + NA_SE_EV_HEALING_TOU, + NA_SE_EV_RUMUBLE_KEMURI, + NA_SE_EV_GANON_HADOU, + NA_SE_EV_KANOKE_OPEN, + NA_SE_EV_KANOKE_CLOSE, + NA_SE_EV_SEEK_CLOTH1, + NA_SE_EV_SEEK_CLOTH2, + NA_SE_EV_BOTTLE_CAP_CLOSE, + NA_SE_EV_ELEVATOR_MOVE_KABE2, + NA_SE_EV_WATER_LEVEL_DOWN_STOP, + NA_SE_EV_DAIKU_CLOTH1, + NA_SE_EV_DEMO_EPONA_LAND, + NA_SE_EV_DIVE_INTO_WATER_BLOCK, + NA_SE_EV_TORCH2, + NA_SE_EV_TORCH3, + NA_SE_EV_TORCH4, + NA_SE_EV_TORCH5, + NA_SE_EV_EARTHQUAKE_LAST, + NA_SE_EV_YAMI_TRAP_CHAIN, + NA_SE_EV_FLAME_IGNITION_GANON, + NA_SE_EV_MGANON_DOWN2, + NA_SE_EV_EXPLOSION_HOUKAI, + NA_SE_EV_BLOCKSINK_GANON, + NA_SE_EV_DOG_WALK, + NA_SE_EV_GANON_HOUKAI_KEMURI1, + NA_SE_EN_FLOORMASTER_SLIDING, + NA_SE_EN_FLOORMASTER_SM_STICK, + NA_SE_EN_DODO_J_WALK, + NA_SE_EN_DODO_J_CRY, + NA_SE_EN_DODO_J_FIRE, + NA_SE_EN_DODO_J_DAMAGE, + NA_SE_EN_DODO_J_DEAD, + NA_SE_EN_DODO_K_CRY, + NA_SE_EN_DODO_K_DAMAGE, + NA_SE_EN_DODO_K_DEAD, + NA_SE_EN_DODO_K_WALK, + NA_SE_EN_DODO_K_FIRE, + NA_SE_EN_GOMA_WALK, + NA_SE_EN_GOMA_HIGH, + NA_SE_EN_GOMA_CLIM, + NA_SE_EN_GOMA_DOWN, + NA_SE_EN_GOMA_CRY1, + NA_SE_EN_GOMA_CRY2, + NA_SE_EN_GOMA_DAM1, + NA_SE_EN_GOMA_DAM2, + NA_SE_EN_GOMA_DEAD, + NA_SE_EN_GOMA_UNARI, + NA_SE_EN_GOMA_BJR_EGG1, + NA_SE_EN_GOMA_BJR_EGG2, + NA_SE_EN_GOMA_BJR_WALK, + NA_SE_EN_GOMA_BJR_CRY, + NA_SE_EN_GOMA_BJR_DAM1, + NA_SE_EN_GOMA_BJR_DAM2, + NA_SE_EN_GOMA_BJR_DEAD, + NA_SE_EN_GOMA_DEMO_EYE, + NA_SE_EN_GOMA_LAST, + NA_SE_EN_GOMA_UNARI2, + NA_SE_EN_GOMA_FAINT, + NA_SE_EN_GOMA_BJR_FREEZE, + NA_SE_EN_DODO_M_CRY, + NA_SE_EN_DODO_M_DEAD, + NA_SE_EN_DODO_M_MOVE, + NA_SE_EN_DODO_M_DOWN, + NA_SE_EN_DODO_M_UP, + NA_SE_EN_GANON_THROW_MASIC, + NA_SE_EN_DODO_M_EAT, + NA_SE_EN_GANON_DD_THUNDER, + NA_SE_EN_RIZA_ONGND, + NA_SE_EN_RIZA_CRY, + NA_SE_EN_RIZA_ATTACK, + NA_SE_EN_RIZA_DAMAGE, + NA_SE_EN_RIZA_WARAU, + NA_SE_EN_RIZA_DEAD, + NA_SE_EN_RIZA_WALK, + NA_SE_EN_RIZA_JUMP, + NA_SE_EN_STALKID_WALK, + NA_SE_EN_STALKID_ATTACK, + NA_SE_EN_STALKID_DAMAGE, + NA_SE_EN_STALKID_DEAD, + NA_SE_EN_TEKU_WALK_WATER, + NA_SE_EN_LIGHT_ARROW_HIT, + NA_SE_EN_TUBOOCK_FLY, + NA_SE_EN_STAL_WARAU, + NA_SE_EN_STAL_SAKEBI, + NA_SE_EN_STAL_DAMAGE, + NA_SE_EN_STAL_DEAD, + NA_SE_EN_WOLFOS_APPEAR, + NA_SE_EN_STAL_WALK, + NA_SE_EN_WOLFOS_CRY, + NA_SE_EN_WOLFOS_ATTACK, + NA_SE_EN_FFLY_ATTACK, + NA_SE_EN_FFLY_FLY, + NA_SE_EN_FFLY_DEAD, + NA_SE_EN_WOLFOS_DAMAGE, + NA_SE_EN_AMOS_WALK, + NA_SE_EN_AMOS_WAVE, + NA_SE_EN_AMOS_DEAD, + NA_SE_EN_AMOS_DAMAGE, + NA_SE_EN_AMOS_VOICE, + NA_SE_EN_SHELL_MOUTH, + NA_SE_EN_SHELL_DEAD, + NA_SE_EN_WOLFOS_DEAD, + NA_SE_EN_DODO_K_COLI, + NA_SE_EN_DODO_K_COLI2, + NA_SE_EN_DODO_K_ROLL, + NA_SE_EN_DODO_K_BREATH, + NA_SE_EN_DODO_K_DRINK, + NA_SE_EN_DODO_K_DOWN, + NA_SE_EN_DODO_K_OTAKEBI, + NA_SE_EN_DODO_K_END, + NA_SE_EN_DODO_K_LAST, + NA_SE_EN_DODO_K_LAVA, + NA_SE_EN_GANON_FLOAT, + NA_SE_EN_GANON_DARKWAVE_M, + NA_SE_EN_DODO_J_BREATH, + NA_SE_EN_DODO_J_TAIL, + NA_SE_EN_WOLFOS_WALK, + NA_SE_EN_DODO_J_EAT, + NA_SE_EN_DEKU_MOUTH, + NA_SE_EN_DEKU_ATTACK, + NA_SE_EN_DEKU_DAMAGE, + NA_SE_EN_DEKU_DEAD, + NA_SE_EN_DEKU_JR_MOUTH, + NA_SE_EN_DEKU_JR_ATTACK, + NA_SE_EN_DEKU_JR_DEAD, + NA_SE_EN_DEKU_SCRAPE, + NA_SE_EN_TAIL_FLY, + NA_SE_EN_TAIL_CRY, + NA_SE_EN_TAIL_DEAD, + NA_SE_EN_GANON_SPARK, + NA_SE_EN_STALTU_DOWN, + NA_SE_EN_STALTU_UP, + NA_SE_EN_STALTU_LAUGH, + NA_SE_EN_STALTU_DAMAGE, + NA_SE_EN_STAL_JUMP, + NA_SE_EN_TEKU_DAMAGE, + NA_SE_EN_TEKU_DEAD, + NA_SE_EN_TEKU_WALK, + NA_SE_EN_PO_KANTERA, + NA_SE_EN_PO_FLY, + NA_SE_EN_PO_AWAY, + NA_SE_EN_PO_APPEAR, + NA_SE_EN_PO_DISAPPEAR, + NA_SE_EN_PO_DAMAGE, + NA_SE_EN_PO_DEAD, + NA_SE_EN_PO_DEAD2, + NA_SE_EN_EXTINCT, + NA_SE_EN_GOLON_LAND_BIG, + NA_SE_EN_RIZA_DOWN, + NA_SE_EN_DODO_M_GND, + NA_SE_EN_NUTS_UP, + NA_SE_EN_NUTS_DOWN, + NA_SE_EN_NUTS_THROW, + NA_SE_EN_NUTS_WALK, + NA_SE_EN_NUTS_DAMAGE, + NA_SE_EN_NUTS_DEAD, + NA_SE_EN_NUTS_FAINT, + NA_SE_EN_PO_BIG_GET, + NA_SE_EN_STALTU_ROLL, + NA_SE_EN_STALWALL_DEAD, + NA_SE_EN_PO_SISTER_DEAD, + NA_SE_EN_BARI_SPLIT, + NA_SE_EN_TEKU_REVERSE, + NA_SE_EN_VALVAISA_LAND2, + NA_SE_EN_TEKU_LAND_WATER, + NA_SE_EN_LAST_DAMAGE, + NA_SE_EN_STALWALL_ROLL, + NA_SE_EN_STALWALL_DASH, + NA_SE_EN_TEKU_JUMP_WATER, + NA_SE_EN_TEKU_LAND_WATER2, + NA_SE_EN_FALL_AIM, + NA_SE_EN_FALL_UP, + NA_SE_EN_FALL_CATCH, + NA_SE_EN_FALL_LAND, + NA_SE_EN_FALL_WALK, + NA_SE_EN_FALL_DAMAGE, + NA_SE_EN_FALL_DEAD, + NA_SE_EN_KAICHO_FLUTTER, + NA_SE_EN_BIRI_FLY, + NA_SE_EN_BIRI_JUMP, + NA_SE_EN_BIRI_SPARK, + NA_SE_EN_BIRI_DEAD, + NA_SE_EN_BIRI_BUBLE, + NA_SE_EN_BARI_ROLL, + NA_SE_EN_GOMA_JR_FREEZE, + NA_SE_EN_BARI_DEAD, + NA_SE_EN_GANON_FIRE, + NA_SE_EN_FANTOM_TRANSFORM, + NA_SE_EN_FANTOM_THUNDER, + NA_SE_EN_FANTOM_SPARK, + NA_SE_EN_FANTOM_FLOAT, + NA_SE_EN_FANTOM_MASIC1, + NA_SE_EN_FANTOM_MASIC2, + NA_SE_EN_FANTOM_FIRE, + NA_SE_EN_FANTOM_HIT_THUNDER, + NA_SE_EN_FANTOM_ATTACK, + NA_SE_EN_FANTOM_STICK, + NA_SE_EN_FANTOM_EYE, + NA_SE_EN_FANTOM_LAST, + NA_SE_EN_FANTOM_THUNDER_GND, + NA_SE_EN_FANTOM_DAMAGE, + NA_SE_EN_FANTOM_DEAD, + NA_SE_EN_FANTOM_LAUGH, + NA_SE_EN_FANTOM_DAMAGE2, + NA_SE_EN_FANTOM_VOICE, + NA_SE_EN_KAICHO_DAMAGE, + NA_SE_EN_GANON_ATTACK_DEMO, + NA_SE_EN_GANON_FIRE_DEMO, + NA_SE_EN_KAICHO_CRY, + NA_SE_EN_KAICHO_ATTACK, + NA_SE_EN_MORIBLIN_WALK, + NA_SE_EN_MORIBLIN_SLIDE, + NA_SE_EN_MORIBLIN_ATTACK, + NA_SE_EN_MORIBLIN_VOICE, + NA_SE_EN_MORIBLIN_SPEAR_AT, + NA_SE_EN_MORIBLIN_SPEAR_NORM, + NA_SE_EN_MORIBLIN_DEAD, + NA_SE_EN_MORIBLIN_DASH, + NA_SE_EN_OCTAROCK_ROCK, + NA_SE_EN_OCTAROCK_FLOAT, + NA_SE_EN_OCTAROCK_JUMP, + NA_SE_EN_OCTAROCK_LAND, + NA_SE_EN_OCTAROCK_SINK, + NA_SE_EN_OCTAROCK_BUBLE, + NA_SE_EN_OCTAROCK_DEAD1, + NA_SE_EN_OCTAROCK_DEAD2, + NA_SE_EN_BUBLE_WING, + NA_SE_EN_BUBLE_MOUTH, + NA_SE_EN_BUBLE_LAUGH, + NA_SE_EN_BUBLE_BITE, + NA_SE_EN_BUBLE_UP, + NA_SE_EN_BUBLE_DOWN, + NA_SE_EN_BUBLE_DEAD, + NA_SE_EN_BUBLEFALL_FIRE, + NA_SE_EN_VALVAISA_APPEAR, + NA_SE_EN_VALVAISA_ROAR, + NA_SE_EN_VALVAISA_MAHI1, + NA_SE_EN_VALVAISA_MAHI2, + NA_SE_EN_VALVAISA_KNOCKOUT, + NA_SE_EN_VALVAISA_DAMAGE1, + NA_SE_EN_VALVAISA_DAMAGE2, + NA_SE_EN_VALVAISA_ROCK, + NA_SE_EN_VALVAISA_SW_NAIL, + NA_SE_EN_VALVAISA_DEAD, + NA_SE_EN_VALVAISA_BURN, + NA_SE_EN_VALVAISA_FIRE, + NA_SE_EN_BARI_DAMAGE, + NA_SE_EN_MOFER_CORE_LAND, + NA_SE_EN_MOFER_CORE_MOVE_WT, + NA_SE_EN_MOFER_CORE_SMJUMP, + NA_SE_EN_MONBLIN_GNDWAVE, + NA_SE_EN_MONBLIN_HAM_DOWN, + NA_SE_EN_MONBLIN_HAM_UP, + NA_SE_EN_BUBLE_DAMAGE, + NA_SE_EN_REDEAD_CRY, + NA_SE_EN_REDEAD_AIM, + NA_SE_EN_REDEAD_DAMAGE, + NA_SE_EN_REDEAD_DEAD, + NA_SE_EN_REDEAD_ATTACK, + NA_SE_EN_NYU_MOVE, + NA_SE_EN_NYU_HIT_STOP, + NA_SE_EN_KAICHO_DEAD, + NA_SE_EN_PO_LAUGH, + NA_SE_EN_PO_CRY, + NA_SE_EN_PO_ROLL, + NA_SE_EN_PO_LAUGH2, + NA_SE_EN_MOFER_APPEAR, + NA_SE_EN_MOFER_ATTACK, + NA_SE_EN_MOFER_WAVE, + NA_SE_EN_MOFER_CATCH, + NA_SE_EN_MOFER_CUT, + NA_SE_EN_MOFER_MOVE_DEMO, + NA_SE_EN_MOFER_BUBLE_DEMO, + NA_SE_EN_MOFER_CORE_JUMP, + NA_SE_EN_MOFER_DEAD, + NA_SE_EN_MOFER_LASTVOICE, + NA_SE_EN_MOFER_CORE_ROLL, + NA_SE_EN_MOFER_CORE_FLY, + NA_SE_EN_GOLON_WAKE_UP, + NA_SE_EN_GOLON_SIT_DOWN, + NA_SE_EN_CHICKEN_FLUTTER, + NA_SE_EN_DEKU_WAKEUP, + NA_SE_EN_DEADHAND_BITE, + NA_SE_EN_DEADHAND_WALK, + NA_SE_EN_DEADHAND_GRIP, + NA_SE_EN_DEADHAND_HAND_AT, + NA_SE_EN_DAIOCTA_MAHI, + NA_SE_EN_DAIOCTA_SPLASH, + NA_SE_EN_DAIOCTA_VOICE, + NA_SE_EN_DAIOCTA_DAMAGE, + NA_SE_EN_DAIOCTA_SINK, + NA_SE_EN_DAIOCTA_DEAD, + NA_SE_EN_DAIOCTA_DEAD2, + NA_SE_EN_GANON_HIT_THUNDER, + NA_SE_EN_TWINROBA_APPEAR_MS, + NA_SE_EN_TWINROBA_TRANSFORM, + NA_SE_EN_TWINROBA_MS_FIRE, + NA_SE_EN_TWINROBA_FIRE_EXP, + NA_SE_EN_TWINROBA_POWERUP, + NA_SE_EN_TWINROBA_SHOOT_FREEZE, + NA_SE_EN_TWINROBA_MS_FREEZE, + NA_SE_EN_TWINROBA_MASIC_SET, + NA_SE_EN_TWINROBA_CUTBODY, + NA_SE_EN_GANON_HIT_GND_IMP, + NA_SE_EN_TWINROBA_DAMAGE_VOICE, + NA_SE_EN_TWINROBA_REFL_FIRE, + NA_SE_EN_TWINROBA_REFL_FREEZE, + NA_SE_EN_GANON_CUTBODY, + NA_SE_EN_TWINROBA_YOUNG_DAMAGE, + NA_SE_EN_TWINROBA_YOUNG_DEAD, + NA_SE_EN_GOLON_EYE_BIG, + NA_SE_EN_GOLON_GOOD_BIG, + NA_SE_EN_TWINROBA_FB_FLY, + NA_SE_EN_TWINROBA_FLY, + NA_SE_EN_TWINROBA_UNARI, + NA_SE_EN_TWINROBA_ROLL, + NA_SE_EN_TWINROBA_SHOOT_FIRE, + NA_SE_EN_TWINROBA_THROW_MASIC, + NA_SE_EN_DARUNIA_HIT_BREAST, + NA_SE_EN_DARUNIA_HIT_LINK, + NA_SE_EN_OWL_FLUTTER, + NA_SE_EN_VALVAISA_LAND, + NA_SE_EN_IRONNACK_WALK, + NA_SE_EN_IRONNACK_SWING_AXE, + NA_SE_EN_IRONNACK_ARMOR_DEMO, + NA_SE_EN_IRONNACK_STAGGER_DEMO, + NA_SE_EN_IRONNACK_ARMOR_OFF_DEMO, + NA_SE_EN_IRONNACK_ARMOR_LAND1_DEMO, + NA_SE_EN_IRONNACK_ARMOR_LAND2_DEMO, + NA_SE_EN_IRONNACK_ARMOR_LAND3_DEMO, + NA_SE_EN_FLOORMASTER_ATTACK, + NA_SE_EN_FLOORMASTER_SM_WALK, + NA_SE_EN_FLOORMASTER_SM_DEAD, + NA_SE_EN_FLOORMASTER_RESTORE, + NA_SE_EN_FLOORMASTER_EXPAND, + NA_SE_EN_FLOORMASTER_SPLIT, + NA_SE_EN_FLOORMASTER_SM_LAND, + NA_SE_EN_IRONNACK_WAVE_DEMO, + NA_SE_EN_IRONNACK_FINGER_DEMO, + NA_SE_EN_IRONNACK_ARMOR_HIT, + NA_SE_EN_NUTS_CUTBODY, + NA_SE_EN_BALINADE_LEVEL, + NA_SE_EN_BALINADE_DAMAGE, + NA_SE_EN_BALINADE_FAINT, + NA_SE_EN_BALINADE_BREAK, + NA_SE_EN_BALINADE_DEAD, + NA_SE_EN_BALINADE_STICK, + NA_SE_EN_BALINADE_THUNDER, + NA_SE_EN_BALINADE_BL_SPARK, + NA_SE_EN_BALINADE_BL_DEAD, + NA_SE_EN_BALINADE_BREAK2, + NA_SE_EN_BALINADE_HIT_RINK, + NA_SE_EN_GANON_WAVE_GND, + NA_SE_EN_AWA_BOUND, + NA_SE_EN_AWA_BREAK, + NA_SE_EN_BROB_WAVE, + NA_SE_EN_NYU_DEAD, + NA_SE_EN_EIER_DAMAGE, + NA_SE_EN_EIER_DEAD, + NA_SE_EN_EIER_FLUTTER, + NA_SE_EN_EIER_FLY, + NA_SE_EN_SHADEST_TAIKO_LOW, + NA_SE_EN_SHADEST_TAIKO_HIGH, + NA_SE_EN_SHADEST_CLAP, + NA_SE_EN_SHADEST_FLY_ATTACK, + NA_SE_EN_PIHAT_UP, + NA_SE_EN_PIHAT_FLY, + NA_SE_EN_PIHAT_DAMAGE, + NA_SE_EN_PIHAT_LAND, + NA_SE_EN_BALINADE_HAND_DOWN, + NA_SE_EN_BALINADE_HAND_UP, + NA_SE_EN_BALINADE_HAND_DAMAGE, + NA_SE_EN_BALINADE_HAND_DEAD, + NA_SE_EN_GOMA_JR_WALK, + NA_SE_EN_GOMA_JR_CRY, + NA_SE_EN_GOMA_JR_DAM1, + NA_SE_EN_GOMA_JR_DAM2, + NA_SE_EN_GOMA_JR_DEAD, + NA_SE_EN_GOMA_EGG1, + NA_SE_EN_GOMA_EGG2, + NA_SE_EN_GANON_BODY_SPARK, + NA_SE_EN_SHADEST_HAND_WAVE, + NA_SE_EN_SHADEST_CATCH, + NA_SE_EN_SHADEST_LAND, + NA_SE_EN_SHADEST_HAND_FLY, + NA_SE_EN_SHADEST_SHAKEHAND, + NA_SE_EN_SHADEST_DAMAGE, + NA_SE_EN_SHADEST_DAMAGE_HAND, + NA_SE_EN_SHADEST_DISAPPEAR, + NA_SE_EN_GANON_CHARGE_MASIC, + NA_SE_EN_GANON_THROW_BIG, + NA_SE_EN_SHADEST_FREEZE, + NA_SE_EN_SHADEST_DEAD, + NA_SE_EN_BIMOS_ROLL_HEAD, + NA_SE_EN_BIMOS_LAZER, + NA_SE_EN_BIMOS_LAZER_GND, + NA_SE_EN_BIMOS_AIM, + NA_SE_EN_BUBLEWALK_WALK, + NA_SE_EN_BUBLEWALK_AIM, + NA_SE_EN_BUBLEWALK_REVERSE, + NA_SE_EN_BUBLEWALK_DAMAGE, + NA_SE_EN_BUBLEWALK_DEAD, + NA_SE_EN_YUKABYUN_FLY, + NA_SE_EN_FLAME_DAMAGE, + NA_SE_EN_TWINROBA_FLY_DEMO, + NA_SE_EN_FLAME_KICK, + NA_SE_EN_FLAME_RUN, + NA_SE_EN_FLAME_ROLL, + NA_SE_EN_FLAME_MAN_RUN, + NA_SE_EN_FLAME_MAN_DAMAGE, + NA_SE_EN_FLAME_LAUGH, + NA_SE_EN_FLAME_MAN_SLIDE, + NA_SE_EN_FLAME_FIRE_ATTACK, + NA_SE_EN_PIHAT_SM_FLY, + NA_SE_EN_PIHAT_SM_DEAD, + NA_SE_EN_RIVA_APPEAR, + NA_SE_EN_AKINDONUTS_HIDE, + NA_SE_EN_RIVA_DAMAGE, + NA_SE_EN_RIVA_DEAD, + NA_SE_EN_RIVA_MOVE, + NA_SE_EN_FLAME_MAN_SURP, + NA_SE_EN_SHADEST_LAST, + NA_SE_EN_SHADEST_MOVE, + NA_SE_EN_SHADEST_PRAY, + NA_SE_EN_MGANON_ROAR, + NA_SE_EN_LIKE_WALK, + NA_SE_EN_LIKE_UNARI, + NA_SE_EN_LIKE_DRINK, + NA_SE_EN_LIKE_EAT, + NA_SE_EN_LIKE_THROW, + NA_SE_EN_LIKE_DAMAGE, + NA_SE_EN_LIKE_DEAD, + NA_SE_EN_MGANON_SWORD, + NA_SE_EN_GERUDOFT_ATTACK, + NA_SE_EN_GERUDOFT_DAMAGE, + NA_SE_EN_GERUDOFT_DEAD, + NA_SE_EN_MGANON_DAMAGE, + NA_SE_EN_ANUBIS_FIRE, + NA_SE_EN_ANUBIS_FIREBOMB, + NA_SE_EN_MGANON_DEAD1, + NA_SE_EN_ANUBIS_DEAD, + NA_SE_EN_MUSI_LAND, + NA_SE_EN_MGANON_DEAD2, + NA_SE_EN_EIER_ATTACK, + NA_SE_EN_EIER_CRY, + NA_SE_EN_FREEZAD_BREATH, + NA_SE_EN_FREEZAD_DAMAGE, + NA_SE_EN_FREEZAD_DEAD, + NA_SE_EN_DEADHAND_LAUGH, + NA_SE_EN_DEADHAND_HIDE, + NA_SE_EN_DEADHAND_DAMAGE, + NA_SE_EN_DEADHAND_HAND_DEAD, + NA_SE_EN_DEADHAND_DEAD, + NA_SE_EN_IRONNACK_BREAK_PILLAR2, + NA_SE_EN_IRONNACK_BREAK_PILLAR, + NA_SE_EN_IRONNACK_HIT_GND, + NA_SE_EN_MGANON_BREATH, + NA_SE_EN_TWINROBA_LAUGH, + NA_SE_EN_TWINROBA_LAUGH2, + NA_SE_EN_TWINROBA_SHOOT_VOICE, + NA_SE_EN_TWINROBA_SENSE, + NA_SE_EN_TWINROBA_DIE, + NA_SE_EN_TWINROBA_YOUNG_DAMAGE2, + NA_SE_EN_TWINROBA_YOUNG_SHOOTVC, + NA_SE_EN_TWINROBA_YOUNG_LAUGH, + NA_SE_EN_TWINROBA_YOUNG_WINK, + NA_SE_EN_IRONNACK_DAMAGE, + NA_SE_EN_IRONNACK_DASH, + NA_SE_EN_IRONNACK_DEAD, + NA_SE_EN_IRONNACK_PULLOUT, + NA_SE_EN_IRONNACK_WAKEUP, + NA_SE_EN_GERUDOFT_BREATH, + NA_SE_EN_GANON_LAUGH, + NA_SE_EN_GANON_VOICE_DEMO, + NA_SE_EN_GANON_THROW, + NA_SE_EN_GANON_AT_RETURN, + NA_SE_EN_GANON_HIT_GND, + NA_SE_EN_GANON_DAMAGE1, + NA_SE_EN_GANON_DAMAGE2, + NA_SE_EN_GANON_DOWN, + NA_SE_EN_GANON_RESTORE, + NA_SE_EN_GANON_DEAD, + NA_SE_EN_GANON_BREATH, + NA_SE_EN_GANON_TOKETU, + NA_SE_EN_GANON_CASBREAK, + NA_SE_EN_GANON_BIGMASIC, + NA_SE_EN_GANON_DARKWAVE, + NA_SE_EN_FANTOM_ST_LAUGH, + NA_SE_EN_MGANON_WALK, + NA_SE_EN_MGANON_STAND, + NA_SE_EN_MGANON_UNARI, + NA_SE_EN_STALGOLD_ROLL, + NA_SE_EN_KDOOR_WAVE, + NA_SE_EN_KDOOR_HIT, + NA_SE_EN_KDOOR_BREAK, + NA_SE_EN_KDOOR_HIT_GND, + NA_SE_EN_MGANON_SWDIMP, + NA_SE_EN_STALTU_WAVE, + NA_SE_EN_STALTU_DOWN_SET, + NA_SE_EN_GOMA_BJR_LAND, + NA_SE_EN_GOMA_BJR_LAND2, + NA_SE_EN_GOMA_JR_LAND, + NA_SE_EN_GOMA_JR_LAND2, + NA_SE_EN_TWINROBA_FIGHT, + NA_SE_EN_PO_BIG_CRY, + NA_SE_EN_MUSI_SINK, + NA_SE_EN_STALGOLD_UP_CRY, + NA_SE_EN_GOLON_CRY, + NA_SE_EN_MOFER_CORE_DAMAGE, + NA_SE_EN_DAIOCTA_LAND_WATER, + NA_SE_EN_RIVA_BIG_APPEAR, + NA_SE_EN_MONBLIN_HAM_LAND, + NA_SE_EN_MUSI_WALK, + NA_SE_EN_MIMICK_BREATH, + NA_SE_EN_STALWALL_LAUGH, + NA_SE_EN_TWINROBA_TRANSFORM2, + NA_SE_EN_KAICHO_PIYORI, + NA_SE_EN_DODO_K_WALK_APPEAR, + NA_SE_EN_DODO_K_STOP, + NA_SE_EN_TEKU_GND993, + NA_SE_EN_TEKU_JUMP993, + NA_SE_EN_MORIBLIN_DEMO, + NA_SE_EN_LEADED_WHITE, + NA_SE_EN_RIVA_HIDE_NEW, + NA_SE_EN_STALBABY_HIDE_NEW, + NA_SE_EN_STAL_REBORN, + NA_SE_EN_DAIKU_FOOT, + NA_SE_EN_STALKID_DAMAGE_NEW, + NA_SE_EN_STALKID_DOWN_NEW, + NA_SE_EN_BALINADE_ARM_LAND, + NA_SE_EN_BALINADE_ARM_DEAD, + NA_SE_EN_READED_WALK, + NA_SE_SY_WIN_OPEN, + NA_SE_SY_WIN_CLOSE, + NA_SE_SY_CORRECT_CHIME, + NA_SE_SY_GET_RUPY, + NA_SE_SY_MESSAGE_WOMAN, + NA_SE_SY_MESSAGE_MAN, + NA_SE_SY_ERROR, + NA_SE_SY_TRE_BOX_APPEAR, + NA_SE_SY_DECIDE, + NA_SE_SY_CURSOR, + NA_SE_SY_CANCEL, + NA_SE_SY_HP_RECOVER, + NA_SE_SY_ATTENTION_ON, + NA_SE_SY_LOCK_OFF, + NA_SE_SY_LOCK_ON_HUMAN, + NA_SE_SY_CAMERA_ZOOM_UP, + NA_SE_SY_CAMERA_ZOOM_DOWN, + NA_SE_SY_ATTENTION_ON_OLD, + NA_SE_SY_MESSAGE_PASS, + NA_SE_SY_WARNING_COUNT_N, + NA_SE_SY_WARNING_COUNT_E, + NA_SE_SY_HITPOINT_ALARM, + NA_SE_SY_DEMO_CUT, + NA_SE_SY_NAVY_CALL, + NA_SE_SY_GAUGE_UP, + NA_SE_SY_PIECE_OF_HEART, + NA_SE_SY_GET_ITEM, + NA_SE_SY_WIN_SCROLL_LEFT, + NA_SE_SY_WIN_SCROLL_RIGHT, + NA_SE_SY_OCARINA_ERROR, + NA_SE_SY_CAMERA_ZOOM_UP_2, + NA_SE_SY_CAMERA_ZOOM_DOWN_2, + NA_SE_SY_GLASSMODE_ON, + NA_SE_SY_GLASSMODE_OFF, + NA_SE_SY_FOUND, + NA_SE_SY_HIT_SOUND, + NA_SE_SY_MESSAGE_END, + NA_SE_SY_RUPY_COUNT, + NA_SE_SY_LOCK_ON, + NA_SE_SY_GET_BOXITEM, + NA_SE_SY_WHITE_OUT_L, + NA_SE_SY_WHITE_OUT_S, + NA_SE_SY_WHITE_OUT_T, + NA_SE_SY_START_SHOT, + NA_SE_SY_METRONOME, + NA_SE_SY_ATTENTION_URGENCY, + NA_SE_SY_METRONOME_LV, + NA_SE_SY_FSEL_CURSOR, + NA_SE_SY_FSEL_DECIDE_S, + NA_SE_SY_FSEL_DECIDE_L, + NA_SE_SY_FSEL_CLOSE, + NA_SE_SY_FSEL_ERROR, + NA_SE_SY_SET_FIRE_ARROW, + NA_SE_SY_SET_ICE_ARROW, + NA_SE_SY_SET_LIGHT_ARROW, + NA_SE_SY_SYNTH_MAGIC_ARROW, + NA_SE_SY_METRONOME_2, + NA_SE_SY_KINSTA_MARK_APPEAR, + NA_SE_SY_FIVE_COUNT_LUPY, + NA_SE_SY_CARROT_RECOVER, + NA_SE_EV_FAIVE_LUPY_COUNT, + NA_SE_SY_KANADE_ISHI, + NA_SE_SY_NA_HELLO_2, + NA_SE_SY_MAGIC_SOUL_NORMAL, + NA_SE_SY_MAGIC_SOUL_FLASH, + NA_SE_SY_CANCEL_CHALLENGE, + NA_SE_SY_DECIDE_CHALLENGE, + NA_SE_OC_OCARINA, + NA_SE_OC_ABYSS, + NA_SE_OC_DOOR_OPEN, + NA_SE_OC_SECRET_WARP_IN, + NA_SE_OC_SECRET_WARP_OUT, + NA_SE_OC_SECRET_HOLE_OUT, + NA_SE_OC_REVENGE, + NA_SE_OC_HINT_MOVIE, + NA_SE_OC_HINT_MOVIE2_WHITE, + NA_SE_OC_HINT_MOVIE_ZOOMIN, + NA_SE_OC_HIBIKI_ISHI, + NA_SE_VO_LI_SWORD_N, + NA_SE_VO_LI_SWORD_L, + NA_SE_VO_LI_LASH, + NA_SE_VO_LI_HANG, + NA_SE_VO_LI_CLIMB_END, + NA_SE_VO_LI_DAMAGE_S, + NA_SE_VO_LI_FREEZE, + NA_SE_VO_LI_FALL_S, + NA_SE_VO_LI_FALL_L, + NA_SE_VO_LI_BREATH_REST, + NA_SE_VO_LI_BREATH_DRINK, + NA_SE_VO_LI_DOWN, + NA_SE_VO_LI_TAKEN_AWAY, + NA_SE_VO_LI_HELD, + NA_SE_VO_LI_SNEEZE, + NA_SE_VO_LI_SWEAT, + NA_SE_VO_LI_DRINK, + NA_SE_VO_LI_RELAX, + NA_SE_VO_LI_SWORD_PUTAWAY, + NA_SE_VO_LI_GROAN, + NA_SE_VO_LI_AUTO_JUMP, + NA_SE_VO_LI_MAGIC_NALE, + NA_SE_VO_LI_SURPRISE, + NA_SE_VO_LI_MAGIC_FROL, + NA_SE_VO_LI_PUSH, + NA_SE_VO_LI_HOOKSHOT_HANG, + NA_SE_VO_LI_LAND_DAMAGE_S, + NA_SE_VO_LI_NULL_0x1b, + NA_SE_VO_LI_MAGIC_ATTACK, + NA_SE_VO_BL_DOWN, + NA_SE_VO_LI_DEMO_DAMAGE, + NA_SE_VO_LI_ELECTRIC_SHOCK_LV, + NA_SE_VO_LI_SWORD_N_KID, + NA_SE_VO_LI_ROLLING_CUT_KID, + NA_SE_VO_LI_LASH_KID, + NA_SE_VO_LI_HANG_KID, + NA_SE_VO_LI_CLIMB_END_KID, + NA_SE_VO_LI_DAMAGE_S_KID, + NA_SE_VO_LI_FREEZE_KID, + NA_SE_VO_LI_FALL_S_KID, + NA_SE_VO_LI_FALL_L_KID, + NA_SE_VO_LI_BREATH_REST_KID, + NA_SE_VO_LI_BREATH_DRINK_KID, + NA_SE_VO_LI_DOWN_KID, + NA_SE_VO_LI_TAKEN_AWAY_KID, + NA_SE_VO_LI_HELD_KID, + NA_SE_VO_LI_SNEEZE_KID, + NA_SE_VO_LI_SWEAT_KID, + NA_SE_VO_LI_DRINK_KID, + NA_SE_VO_LI_RELAX_KID, + NA_SE_VO_LI_SWORD_PUTAWAY_KID, + NA_SE_VO_LI_GROAN_KID, + NA_SE_VO_LI_AUTO_JUMP_KID, + NA_SE_VO_LI_MAGIC_NALE_KID, + NA_SE_VO_LI_SURPRISE_KID, + NA_SE_VO_LI_MAGIC_FROL_KID, + NA_SE_VO_LI_PUSH_KID, + NA_SE_VO_LI_HOOKSHOT_HANG_KID, + NA_SE_VO_LI_LAND_DAMAGE_S_KID, + NA_SE_VO_LI_NULL_0x1b_KID, + NA_SE_VO_LI_MAGIC_ATTACK_KID, + NA_SE_VO_BL_DOWN_KID, + NA_SE_VO_LI_DEMO_DAMAGE_KID, + NA_SE_VO_LI_ELECTRIC_SHOCK_LV_KID, + NA_SE_VO_NAVY_ENEMY, + NA_SE_VO_NAVY_HELLO, + NA_SE_VO_NAVY_HEAR, + NA_SE_VO_NAVY_CALL, + NA_SE_VO_NA_HELLO_3, + NA_SE_VO_TA_SLEEP, + NA_SE_VO_TA_SURPRISE, + NA_SE_VO_TA_CRY_0, + NA_SE_VO_TA_CRY_1, + NA_SE_VO_IN_CRY_0, + NA_SE_VO_IN_LOST, + NA_SE_VO_IN_LASH_0, + NA_SE_VO_IN_LASH_1, + NA_SE_VO_FR_LAUGH_0, + NA_SE_VO_FR_SMILE_0, + NA_SE_VO_NB_AGONY, + NA_SE_VO_NB_CRY_0, + NA_SE_VO_NB_NOTICE, + NA_SE_VO_NA_HELLO_0, + NA_SE_VO_NA_HELLO_1, + NA_SE_VO_NA_HELLO_2, + NA_SE_VO_RT_CRASH, + NA_SE_VO_RT_DISCOVER, + NA_SE_VO_RT_FALL, + NA_SE_VO_RT_LAUGH_0, + NA_SE_VO_RT_LIFT, + NA_SE_VO_RT_THROW, + NA_SE_VO_RT_UNBALLANCE, + NA_SE_VO_ST_DAMAGE, + NA_SE_VO_ST_ATTACK, + NA_SE_VO_Z0_HURRY, + NA_SE_VO_Z0_MEET, + NA_SE_VO_Z0_QUESTION, + NA_SE_VO_Z0_SIGH_0, + NA_SE_VO_Z0_SMILE_0, + NA_SE_VO_Z0_SURPRISE, + NA_SE_VO_Z0_THROW, + NA_SE_VO_SK_CRY_0, + NA_SE_VO_SK_CRY_1, + NA_SE_VO_SK_CRASH, + NA_SE_VO_SK_LAUGH, + NA_SE_VO_SK_SHOUT, + NA_SE_VO_Z1_CRY_0, + NA_SE_VO_Z1_CRY_1, + NA_SE_VO_Z1_OPENDOOR, + NA_SE_VO_Z1_SURPRISE, + NA_SE_VO_Z1_PAIN, + NA_SE_VO_KZ_MOVE, + NA_SE_VO_NB_LAUGH, + NA_SE_VO_IN_LAUGH, + NA_SE_VO_LI_AUTO_JUMP_DARKLINK, +}; +const int sfxCount = 1207; diff --git a/soh/soh/Enhancements/accessible-actors/accessibility_cues.cpp b/soh/soh/Enhancements/accessible-actors/accessibility_cues.cpp new file mode 100644 index 000000000..ce24b6cd3 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/accessibility_cues.cpp @@ -0,0 +1,1500 @@ +#include "ActorAccessibility.h" +#include "z64.h" +#include "macros.h" +#include "functions.h" +#include +extern "C" { +s32 Player_PosVsWallLineTest(PlayState* play, Player* p, Vec3f* offset, CollisionPoly** wallPoly, s32* bgId, + Vec3f* posResult); +void Player_GetSlopeDirection(CollisionPoly* floorPoly, Vec3f* slopeNormal, s16* downwardSlopeYaw); +void CollisionPoly_GetVertices(CollisionPoly* poly, Vec3s* vtxList, Vec3f* dest); +f32 BgCheck_RaycastFloorImpl(PlayState* play, CollisionContext* colCtx, u16 xpFlags, CollisionPoly** outPoly, + s32* outBgId, Vec3f* pos, Actor* actor, u32 arg7, f32 chkDist); +#include "soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h" +#include "soh/Enhancements/tts/tts.h" +} +#define DETECTION_DISTANCE 500.0 +#define MIN_INCLINE_DISTANCE 5.0 +#define MIN_DECLINE_DISTANCE 5.0 +#define DEFAULT_PROBE_SPEED 5.5 +#define NOMINMAX +static Player fakePlayer; // Used for wall height detection. +static Vec3f D_80854798 = { 0.0f, 18.0f, 0.0f }; // From z_player.c. + +const char* GetLanguageCode(); + +enum { + DISCOVERED_NOTHING = 0, + DISCOVERED_INCLINE, + DISCOVERED_DECLINE, + DISCOVERED_LEDGE, + DISCOVERED_WALL, + DISCOVERED_SPIKE, + DISCOVERED_WATER, + DISCOVERED_GROUND, + DISCOVERED_LAVA, +}; +// Abstract class for terrain cue sound handling. Implementations should not allocate memory. These are always in-place +// constructed in static memory owned by the TerrainCueDirection object. +class TerrainCueSound { + protected: + AccessibleActor* actor; + Vec3f terrainPos; + Vec3f terrainProjectedPos; + f32 currentPitch; + f32 xzDistToPlayer; + s16 currentSFX; + int restFrames; // Used to control how often sounds get played. + + bool shouldLoop; + // Call to start playback. + void play() { + ActorAccessibility_PlaySound(this, 0, currentSFX, shouldLoop); + ActorAccessibility_SetSoundPos(this, 0, &terrainProjectedPos, xzDistToPlayer, actor->policy.distance); + ActorAccessibility_SetSoundPitch(this, 0, currentPitch); + } + + // Call when terrain is no longer present to stop playback. + virtual void stop() { + ActorAccessibility_StopSound(this, 0); + } + + // Custom terrain sound behaviour. + virtual void run() = 0; + // Update sound position and volume once per frame. + virtual void updatePositions(Vec3f& pos) { + terrainPos = pos; + Player* player = GET_PLAYER(actor->play); + + f32 w = 0.0f; + // Set projectedPos. + SkinMatrix_Vec3fMtxFMultXYZW(&actor->play->viewProjectionMtxF, &terrainPos, &terrainProjectedPos, &w); + + // Set xzDistToPlayer. + xzDistToPlayer = Math_Vec3f_DistXZ(&terrainPos, &player->actor.world.pos); + ActorAccessibility_SetSoundPos(this, 0, &terrainProjectedPos, xzDistToPlayer, actor->policy.distance); + ActorAccessibility_SetSoundPitch(this, 0, currentPitch); + } + + public: + TerrainCueSound(AccessibleActor* actor, Vec3f pos) { + this->actor = actor; + currentPitch = 1.0; + shouldLoop = false; + restFrames = 0; + xzDistToPlayer = 0; + currentSFX = 0; + } + virtual ~TerrainCueSound() { + stop(); + } + void update(Vec3f& pos) { + updatePositions(pos); + run(); + } +}; +class Incline : protected TerrainCueSound { + float pitchModifier; + + public: + Incline(AccessibleActor* actor, Vec3f pos, float pitchModifier = 0) : TerrainCueSound(actor, pos) { + currentPitch = 0.5; + currentSFX = NA_SE_PL_MAGIC_SOUL_FLASH; + this->pitchModifier = pitchModifier; + + play(); + } + virtual ~Incline() { + } + virtual void run() { + if (restFrames > 0) { + restFrames--; + if (restFrames == 0) + play(); + + return; + } + ActorAccessibility_SetSoundPitch(this, 0, 0.5 + (1 - pitchModifier)); + } + void setPitchModifier(float modifier) { + pitchModifier = modifier; + } +}; + +class Decline : protected TerrainCueSound { + float pitchModifier; + + public: + Decline(AccessibleActor* actor, Vec3f pos, float pitchModifier) : TerrainCueSound(actor, pos) { + restFrames = 0; + currentPitch = 2.0; + currentSFX = NA_SE_PL_MAGIC_SOUL_FLASH; + this->pitchModifier = 0.0; + + play(); + } + virtual ~Decline() { + } + virtual void run() { + if (restFrames > 0) { + restFrames--; + if (restFrames == 0) + play(); + + return; + } + ActorAccessibility_SetSoundPitch(this, 0, 1.0 + pitchModifier); + /*currentPitch -= 0.1; + if (currentPitch < 0.5) { + stop(); + currentPitch = 2.0; + restFrames = 5; + }*/ + } + void setPitchModifier(float mod) { + pitchModifier = mod; + } +}; +class Ledge : protected TerrainCueSound { + s8 savedType; // Distinguishes between a ledge link can fall from and one he can climb up. + Vec3s probeRot; + + public: + Ledge(AccessibleActor* actor, Vec3f pos, Vec3s probeRot, s8 type = 0) : TerrainCueSound(actor, pos) { + if (type == 1) + currentPitch = 2.0; + savedType = type; + switch (type) { + case 0: + currentSFX = NA_SE_EV_WIND_TRAP; + shouldLoop = 1; + break; + + case 1: + currentSFX = NA_SE_EV_WOOD_BOUND; + shouldLoop = 0; + break; + case 2: + currentSFX = NA_SE_PL_LAND_WATER0; + shouldLoop = 0; + break; + case 3: + currentSFX = NA_SE_SY_WARNING_COUNT_N; + shouldLoop = 0; + break; + } + + this->probeRot = probeRot; + if (type == 0) { + if (probeRot.y == 0) + currentPitch = 0.4; + else if (probeRot.y < 0) + currentPitch = 0.2; + else + currentPitch = 0.8; + } + + play(); + } + virtual ~Ledge() { + } + s8 type() { + return savedType; + } + void run() { + if (savedType == 0) + return; // Downward ledges play a looping sound and do not need ongoing maintenance. + if (restFrames == 0) { + play(); + restFrames = 10; + return; + } + restFrames--; + } +}; +class Platform : protected TerrainCueSound { + public: + Platform(AccessibleActor* actor, Vec3f pos) : TerrainCueSound(actor, pos) { + currentPitch = 2.0; + // actor->policy.volume = 1.5; + currentSFX = NA_SE_IT_SHIELD_REFLECT_SW; + shouldLoop = false; + } + virtual ~Platform() { + } + void setActor(AccessibleActor* actor) { + this->actor = actor; + } + void setPosition(Vec3f& pos) { + updatePositions(pos); + } + void run() { + if (restFrames == 0) { + play(); + restFrames = 10; + return; + } + restFrames--; + } +}; + +class Wall : protected TerrainCueSound { + int frames; + Vec3s probeRot; + f32 targetPitch; + + public: + Wall(AccessibleActor* actor, Vec3f pos, Vec3s rot) : TerrainCueSound(actor, pos) { + probeRot = rot; + currentPitch = 0.5; + + targetPitch = (f32)probeRot.y / (16384.0f * 2.0f); + if (probeRot.y != 0 && targetPitch < -0.4) + targetPitch = -0.4; + + currentSFX = NA_SE_IT_SWORD_CHARGE; + + frames = 0; + + play(); + } + virtual ~Wall() { + } + void run() { + + frames++; + + if (frames == 20) { + frames = 0; + play(); + ActorAccessibility_SeekSound(this, 0, 44100 * 2); + } + f32 pitchModifier = 0.0; + + if (targetPitch < 0) + pitchModifier = LERP(2.5, 0.5 + targetPitch, (f32)frames / 20.0f); + else if (targetPitch > 0) + pitchModifier = LERP(0.1, (0.5 + targetPitch), (f32)frames / 20.0f); + + ActorAccessibility_SetSoundPitch(this, 0, pitchModifier); + } +}; +class Spike : protected TerrainCueSound { + public: + Spike(AccessibleActor* actor, Vec3f pos) : TerrainCueSound(actor, pos) { + currentPitch = 0.5; + currentSFX = NA_SE_IT_SWORD_PICKOUT; + play(); + } + virtual ~Spike() { + } + void run() { + if (restFrames == 0) { + play(); + restFrames = 10; + return; + } + restFrames--; + } +}; +class Water : protected TerrainCueSound { + public: + Water(AccessibleActor* actor, Vec3f pos) : TerrainCueSound(actor, pos) { + currentPitch = 0.5; + currentSFX = NA_SE_PL_LAND_WATER0; // NA_SE_EN_DAIOCTA_LAND_WATER; // change? + play(); + } + virtual ~Water() { + } + void run() { + if (restFrames == 0) { + play(); + restFrames = 10; + return; + } + restFrames--; + } +}; + +class Ground : protected TerrainCueSound { + float pitchModifier; + + public: + Ground(AccessibleActor* actor, Vec3f pos, float pitchModifier) : TerrainCueSound(actor, pos) { + currentPitch = 1.0; + currentSFX = NA_SE_EV_WOOD_BOUND; + this->pitchModifier = 0.0; + play(); + } + virtual ~Ground() { + } + void run() { + if (restFrames == 0) { + play(); + restFrames = 10; + return; + } + ActorAccessibility_SetSoundPitch(this, 0, 1.0 + (2 * pitchModifier)); + restFrames--; + } + void setPitchModifier(float modifier) { + pitchModifier = modifier; + } +}; + +class Lava : protected TerrainCueSound { + public: + Lava(AccessibleActor* actor, Vec3f pos) : TerrainCueSound(actor, pos) { + currentPitch = 1.0; + currentSFX = NA_SE_SY_WARNING_COUNT_N; // change? + play(); + } + virtual ~Lava() { + } + void run() { + if (restFrames == 0) { + play(); + restFrames = 10; + return; + } + restFrames--; + } +}; + +class TerrainCueDirection { + AccessibleActor* actor; + int startingBodyPart; // Decides where the probe starts from. Probes going out to the left or right of the player + // start from the shoulders. + Vec3f pos; + Vec3f prevPos; + Vec3s relRot; // Relative angle. + Vec3s rot; // Actual angle. + f32 wallCheckHeight; + f32 wallCheckRadius; + f32 ceilingCheckHeight; + f32 probeSpeed; // Approximate for now. + Vec3f velocity; + Vec3f expectedVelocity; + int terrainDiscovered = DISCOVERED_NOTHING; + CollisionPoly* floorPoly; + CollisionPoly* wallPoly; + s32 wallBgId; + f32 wallHeight; + s32 floorBgId; + f32 pushedSpeed; + bool disabled; // Only used for debugging. + bool trackingMode; // A debugging feature which forces Link to move along the probe's path. Used to catch collision + // violations and other disagreements between how Link moves and how the probe travels. + bool trackingModeStarted; + + s16 pushedYaw; + union { + Incline incline; + Decline decline; + Ledge ledge; + Wall wall; + Spike spike; + Water water; + Ground ground; + Lava lava; + }; + Platform platform; + + TerrainCueSound* currentSound; + // Apply an offset b to a Vec3f a. + Vec3f applyVec3fOffset(Vec3f& a, Vec3f& b) { + Vec3f c; + c.x = a.x + b.x; + c.y = a.y + b.y; + c.z = a.z + b.z; + return c; + } + // If a sound is currently playing, disable it. + void destroyCurrentSound() { + if (currentSound == NULL) + return; + currentSound->~TerrainCueSound(); + currentSound = NULL; + terrainDiscovered = DISCOVERED_NOTHING; + } + // Play a sound from the position of a previously discovered incline. + + void discoverIncline(Vec3f pos, float pitchModifier = 0) { + if (terrainDiscovered == DISCOVERED_INCLINE) { + incline.setPitchModifier(pitchModifier); + + return; + } + + destroyCurrentSound(); + + new (&incline) Incline(actor, pos, pitchModifier); + currentSound = (TerrainCueSound*)&incline; + terrainDiscovered = DISCOVERED_INCLINE; + } + // Play a sound from the position of a previously discovered decline. + + void discoverDecline(Vec3f pos, float pitchModifier = 0) { + if (terrainDiscovered == DISCOVERED_DECLINE) { + incline.setPitchModifier(pitchModifier); + return; + } + + destroyCurrentSound(); + + new (&decline) Decline(actor, pos, pitchModifier); + + currentSound = (TerrainCueSound*)&decline; + terrainDiscovered = DISCOVERED_DECLINE; + } + // Play a sound from the position of a previously discovered ledge. + + void discoverLedge(Vec3f pos, s8 type = 0) { + if (terrainDiscovered == DISCOVERED_LEDGE && ledge.type() == type) + return; + + destroyCurrentSound(); + + new (&ledge) Ledge(actor, pos, relRot, type); + currentSound = (TerrainCueSound*)&ledge; + terrainDiscovered = DISCOVERED_LEDGE; + } + // Play a sound from the position of a previously discovered wall. + + void discoverWall(Vec3f pos) { + Player* player = GET_PLAYER(actor->play); + if (player->stateFlags1 & PLAYER_STATE1_FIRST_PERSON) { + if (terrainDiscovered == DISCOVERED_WALL) + destroyCurrentSound(); + return; + } + if (terrainDiscovered == DISCOVERED_WALL) + return; + + destroyCurrentSound(); + + new (&wall) Wall(actor, pos, relRot); + currentSound = (TerrainCueSound*)&wall; + terrainDiscovered = DISCOVERED_WALL; + } + void discoverSpike(Vec3f pos) { + if (terrainDiscovered == DISCOVERED_SPIKE) + return; + destroyCurrentSound(); + new (&spike) Spike(actor, pos); + currentSound = (TerrainCueSound*)&spike; + terrainDiscovered = DISCOVERED_SPIKE; + } + + void discoverWater(Vec3f pos) { + if (terrainDiscovered == DISCOVERED_WATER) + return; + destroyCurrentSound(); + new (&water) Water(actor, pos); + currentSound = (TerrainCueSound*)&water; + terrainDiscovered = DISCOVERED_WATER; + } + + void discoverGround(Vec3f pos, float pitchModifier = 0) { + if (terrainDiscovered == DISCOVERED_GROUND) { + ground.setPitchModifier(pitchModifier); + + return; + } + + destroyCurrentSound(); + + new (&ground) Ground(actor, pos, pitchModifier); + currentSound = (TerrainCueSound*)&ground; + terrainDiscovered = DISCOVERED_GROUND; + } + + void discoverLava(Vec3f pos) { + if (terrainDiscovered == DISCOVERED_LAVA) + return; + destroyCurrentSound(); + new (&lava) Lava(actor, pos); + currentSound = (TerrainCueSound*)&lava; + terrainDiscovered = DISCOVERED_LAVA; + } + // Find out how high a wall goes. + f32 findWallHeight(Vec3f& pos, CollisionPoly* poly) { + Player* player = GET_PLAYER(actor->play); + f32 wallHeight; + if (ABS(wallPoly->normal.y) >= 600) { + wallHeight = 399.96002f; + return wallHeight; + } + D_80854798.y = 18.0f; + D_80854798.z = player->ageProperties->wallCheckRadius + 10.0f; + f32 wallYaw = Math_Atan2S(poly->normal.z, poly->normal.x); + f32 nx = COLPOLY_GET_NORMAL(poly->normal.x); + f32 ny = COLPOLY_GET_NORMAL(poly->normal.y); + + f32 nz = COLPOLY_GET_NORMAL(poly->normal.z); + + // Logic adapted from contents of z_player.c (beginning around line 10148). + f32 wallDistance = Math3D_UDistPlaneToPos(nx, ny, nz, poly->dist, &pos); + f32 wd10 = wallDistance + 10.0f; + + Vec3f raycast; + raycast.x = pos.x - (wd10 * nx); + raycast.z = pos.z - (wd10 * nz); + raycast.y = pos.y + player->ageProperties->unk_0C; + CollisionPoly* testPoly; + wallHeight = BgCheck_EntityRaycastFloor1(&actor->play->colCtx, &testPoly, &raycast) - pos.y; + f32 outY; + s32 bgId; + if ((wallHeight < 18.0f) || + BgCheck_EntityCheckCeiling(&actor->play->colCtx, &outY, &pos, wallHeight + 20.0f, &testPoly, &bgId, NULL)) { + wallHeight = 399.96002f; + } else { + D_80854798.y = (wallHeight + 5.0f); + fakePlayer.actor.shape.rot = player->actor.shape.rot; + fakePlayer.actor.world.pos = pos; + Vec3f collisionResult; + + // The following replicates some pretty confusing logic in z_player.c (another series of conditions which + // determines whether wallHeight should be set to the magic number 399.96002f). Rather than copying the + // relevant functions to eliminate dependency on the player object, or risking weird side effects from + // passing in the real player with a temporarily modified pos vector, I'm using this fake player instance + // instead. These functions only need the player's position and shape rotation vectors set. + if (Player_PosVsWallLineTest(actor->play, &fakePlayer, &D_80854798, &testPoly, &bgId, &collisionResult) && + std::abs(wallYaw - Math_Atan2S(testPoly->normal.z, testPoly->normal.x)) < 0x4000 && + !func_80041E18(&actor->play->colCtx, testPoly, bgId)) { + wallHeight = 399.96002f; + } + } + return wallHeight; + } + + // Check if traveling from point A to point B is obstructed by a wall. + CollisionPoly* checkWall(Vec3f& pos, Vec3f& prevPos, Vec3f& collisionPos) { + Player* player = GET_PLAYER(actor->play); + BgCheck_EntitySphVsWall3(&actor->play->colCtx, &collisionPos, &pos, &prevPos, wallCheckRadius, &wallPoly, + &wallBgId, NULL, wallCheckHeight); + return wallPoly; + } + // Another copy/modify job from z_player.c. This function sets windspeed and wind direction, which are used for + // pushing the player up and down slopes. "Inspired" by func_8083E318. + + s32 computePushedSpeedEtc() { + s32 pad; + s16 sp4A; + Vec3f sp3C; + s16 sp3A; + f32 temp1; + f32 temp2; + s16 temp3; + PlayState* play = actor->play; + + if (SurfaceType_GetSlope(&play->colCtx, floorPoly, floorBgId) == 1) { + sp4A = Math_Atan2S(velocity.z, velocity.x); + Player_GetSlopeDirection(floorPoly, &sp3C, &sp3A); + temp3 = sp3A - sp4A; + + if (ABS(temp3) > 16000) { + temp1 = (1.0f - sp3C.y) * 40.0f; + temp2 = (temp1 * temp1) * 0.015f; + if (temp2 < 1.2f) { + temp2 = 1.2f; + } + pushedYaw = sp3A; + pushedSpeed = temp1; + // Math_StepToF(&pushedSpeed / probeSpeed, temp1, temp2); + } + } else + pushedSpeed = 0.0; + + // Math_StepToF(&this->pushedSpeed, 0.0f, 1.0f); // Todo: only step by 0.5F when in water. + return 0; + } + void setVelocity() { + + velocity.x = Math_SinS(rot.y) * probeSpeed; + velocity.y = 25.0; + velocity.z = Math_CosS(rot.y) * probeSpeed; + expectedVelocity = velocity; + + computePushedSpeedEtc(); + if (pushedSpeed == 0.0) + return; + velocity.x += pushedSpeed * Math_SinS(pushedYaw); + velocity.z += pushedSpeed * Math_CosS(pushedYaw); + } + + bool checkPerpendicularWall(Vec3f_ ppos, Vec3s_ ogRot) { + pos = ppos; + Player* player = GET_PLAYER(actor->play); + Vec3f wallPos; + rot.y = player->actor.shape.rot.y; + rot.y += 16384; + rot.y += 16384; + setVelocity(); + move(false); + move(false); + move(false); + move(false); + + rot = ogRot; + rot.y += 16384; + rot.y += 16384; + setVelocity(); + if (!move(false)) { + return true; + } + if (!move(false)) { + return true; + } + if (!move(false)) { + return true; + } + prevPos = pos; + rot = ogRot; + setVelocity(); + if (!move(false)) { + return true; + } + if (!move(false)) { + return true; + } + if (!move(false)) { + return true; + } + if (!move(false)) { + return true; + } + wallPoly = checkWall(pos, prevPos, wallPos); + if (wallPoly == NULL || rdist(pos) > 200) { + return false; + } + + return true; + } + + bool checkVinePlatform(Vec3f_ ppos, Vec3s_ ogRot, f32 playerHeight) { + f32 floorHeight; + rot = ogRot; + floorHeight = BgCheck_EntityRaycastFloor3(&actor->play->colCtx, &floorPoly, &floorBgId, &pos); + if ((floorHeight - playerHeight) > 100.0) { + destroyCurrentSound(); + pos.y -= floorHeight; // TODO remove? + platform.setPosition(pos); + platform.run(); + return true; + } + return false; + } + // Check if we're being pushed away from our intended destination. + bool isPushedAway() { + f32 dist = Math_Vec3f_DistXZ(&velocity, &expectedVelocity); + if (dist >= probeSpeed) + return true; + return false; + } + bool proveClimbableStep() { + setVelocity(); + if (!move()) + return false; + if (isPushedAway()) + return false; + Vec3f wallPos; + if (checkWall(prevPos, pos, wallPos)) + return false; + return true; + } + bool proveClimbable() { + Vec3s ogRot = rot; + Vec3f ogPos = pos; + pos.y += wallHeight; + // Find the floor up here. + probeSpeed = 1.0; + bool foundFloor = false; + for (int i = 0; i < 100; i++) { + setVelocity(); + if (!move()) + return false; + if (pos.y >= ogPos.y + wallHeight - 10) { + foundFloor = true; + break; + } else + pos.y = ogPos.y + wallHeight; + } + probeSpeed = DEFAULT_PROBE_SPEED; + + if (!foundFloor) + return false; + prevPos = pos; + rot.y = ogRot.y + 16384; + bool clockwiseTest = proveClimbableStep(); + f32 clockwiseY = pos.y; + rot.y = ogRot.y - 16384; + pos = prevPos; + + bool counterclockwiseTest = proveClimbableStep(); + f32 counterclockwiseY = pos.y; + rot.y = ogRot.y; + pos = ogPos; + + return clockwiseTest && counterclockwiseTest && + (fabs(clockwiseY - counterclockwiseY) < 10.0 || fabs(clockwiseY - counterclockwiseY) > wallHeight - 5.0); + } + + public: + // Initialize a TerrainCueDirection based on a relative angle and position offset. + void init(AccessibleActor* actor, Vec3s rot, int startingBodyPart = PLAYER_BODYPART_MAX) { + this->actor = actor; + this->relRot = rot; + this->rot = { 0, 0, 0 }; + this->startingBodyPart = startingBodyPart; + + terrainDiscovered = DISCOVERED_NOTHING; + currentSound = NULL; + new (&platform) Platform(actor, { 0.0, 0.0, 0.0 }); + + disabled = false; + trackingMode = false; + trackingModeStarted = false; + } + // Move a probe to its next point along a line, ensuring that it remains on the floor. Returns false if the move + // would put the probe out of bounds. Does not take walls into account. + bool move(s8 gravity = true) { + Player* player = GET_PLAYER(actor->play); + if (gravity == 2) { + if (fabs(player->actor.world.pos.y - pos.y) > 500.0) { + return false; + } + pos.y += probeSpeed; + if (!BgCheck_PosInStaticBoundingBox(&actor->play->colCtx, &pos)) + return false; // Out of bounds. + return true; + } + pos.x += velocity.x; + + pos.z += velocity.z; + + if (gravity == 1) { + pos.y += velocity.y; + f32 floorHeight = 0; + floorHeight = BgCheck_EntityRaycastFloor3(&actor->play->colCtx, &floorPoly, &floorBgId, &pos); + if (floorHeight == BGCHECK_Y_MIN) + return false; // I'm guessing this means out of bounds? + pos.y = floorHeight; + if (!BgCheck_PosInStaticBoundingBox(&actor->play->colCtx, &pos)) + return false; // Out of bounds. + } + + return true; + } + bool isHeadOnCollision(Vec3f& wallPos, Vec3f& velocity) { + return true; + + Vec3f pos = wallPos; + if (!move()) + return true; // Arbitrary, but hopefully this can't happen under normal gameplay circumstances. + Vec3f newWallPos; + if (!checkWall(pos, wallPos, newWallPos)) + return false; + f32 dist = Math_Vec3f_DistXYZ(&wallPos, &newWallPos); + return fabs(dist) < 0.25; + } + + // Perform all terrain detection and sound book keeping. Call once per frame. + + float rdist(Vec3f pos) { + Player* player = GET_PLAYER(actor->play); + float xdist = fabs(pos.x - player->actor.world.pos.x); + float zdist = fabs(pos.z - player->actor.world.pos.z); + float r = sqrt((zdist * zdist) + (xdist * xdist)); + return r; + } + + bool checkForLava(Vec3f_ pos) { + CollisionPoly* floorpoly; + int32_t bgid = BGCHECK_SCENE; + CollisionContext* colCtx = &actor->play->colCtx; + pos.y += 20.0; + + pos.y = BgCheck_RaycastFloorImpl(actor->play, colCtx, (1 << 1), &floorpoly, &bgid, &pos, NULL, 28, 1.0f); + + if (floorpoly == NULL) { + return 0; + } + s8 floorparam = func_80041D4C(colCtx, floorpoly, BG_ACTOR_MAX); + return floorparam == 2 || floorparam == 3; + } + + void scan() { + Player* player = GET_PLAYER(actor->play); + CollisionContext* colCtx = &actor->play->colCtx; + + if (player->stateFlags1 & PLAYER_STATE1_IN_CUTSCENE) { + destroyCurrentSound(); + return; + } + if (disabled) + return; + if (trackingMode) + destroyCurrentSound(); + // Adapted from code in z_player.c, lines 10000 - 10008. + if (player->stateFlags2 & PLAYER_STATE2_CRAWLING) { + wallCheckRadius = 10.0f; + wallCheckHeight = 15.0f; + ceilingCheckHeight = 30.0f; + } else { + wallCheckRadius = player->ageProperties->wallCheckRadius; + wallCheckHeight = 26.0f; + ceilingCheckHeight = player->ageProperties->ceilingCheckHeight; + } + // The virtual cue actors travel in lines relative to Link's angle. + rot = ActorAccessibility_ComputeRelativeAngle(&player->actor.world.rot, &relRot); + pushedSpeed = 0.0; + pushedYaw = 0; + probeSpeed = DEFAULT_PROBE_SPEED; // Experiment with this. + // Draw a line from Link's position to the max detection distance based on the configured relative angle. + if (!trackingModeStarted) { + pos = player->actor.world.pos; + // If a starting body part has been specified, then set the probe's initial X and Z position only. + if (startingBodyPart != PLAYER_BODYPART_MAX) { + pos.x = player->bodyPartsPos[startingBodyPart].x; + pos.z = player->bodyPartsPos[startingBodyPart].y; + } + } + + if (trackingMode) + trackingModeStarted = true; + + f32 distToTravel = DETECTION_DISTANCE; + if (trackingMode) + distToTravel = 1.0; + Vec3f collisionResult; + s32 bgId = 0; + // Don't be fooled: link being in the air does not mean we've found a dropoff. I mean... it could mean that, but + // it's a little too late to do anything about it at that point anyway. + + if (player->stateFlags3 & PLAYER_STATE3_MIDAIR || player->stateFlags2 & PLAYER_STATE2_HOPPING) { + f32 floorHeight = 0; + floorHeight = BgCheck_EntityRaycastFloor3(&actor->play->colCtx, &floorPoly, &floorBgId, &pos); + if (floorHeight == BGCHECK_Y_MIN) + return; // Link is about to void out of bounds or something. + pos.y = floorHeight; + } else { + floorPoly = player->actor.floorPoly; + floorBgId = player->actor.floorBgId; + } + while (distToTravel >= 0) { + prevPos = pos; + setVelocity(); + f32 step = fabs(velocity.x + velocity.z); + distToTravel -= (step + fabs(pos.y - pos.y)); + // checks if link is in the water, needs different logic + if (player->stateFlags1 & PLAYER_STATE1_IN_WATER) { + pos.y = player->actor.prevPos.y; + if (!move()) { + destroyCurrentSound(); + break; // Probe is out of bounds. + } + if (rdist(pos) > 500.00) { + destroyCurrentSound(); + break; // too far too hear + } + // is there an incline ahead that leads out of the water + if (pos.y > player->actor.world.pos.y) { + discoverIncline(pos); + break; + } + // keeps probe at links feet + if (pos.y < player->actor.world.pos.y) { + pos.y = player->actor.world.pos.y; + } + + Vec3f wallPos; + CollisionPoly* wallPoly = checkWall(pos, prevPos, wallPos); + if (wallPoly == NULL) { + continue; + } + + // sets probe to be at surface of water + pos.y += player->actor.yDistToWater; + prevPos.y += player->actor.yDistToWater; + + // checks for new wall poly + wallPoly = checkWall(pos, prevPos, wallPos); + + // if not climable and exists then treats it as a wall + if (wallPoly != NULL) { + discoverWall(pos); + break; + } + + // checks for ledges + pos.y = player->actor.world.pos.y - 10.0; + f32 ogStep = step; + step = 1.0; + while (pos.y < player->actor.world.pos.y + player->actor.yDistToWater) { + pos.y = player->actor.world.pos.y - 10.0; + pos.y += 50.0; + if (!move()) { + break; // Probe is out of bounds. + } + } + step = ogStep; + if (player->ageProperties->unk_92 == 0) { + wallHeight = fabs(pos.y - (player->actor.world.pos.y + player->actor.yDistToWater - + 45.5)); // change that number just a guess + } else { + wallHeight = fabs(pos.y - (player->actor.world.pos.y + player->actor.yDistToWater - 30.0)); + } + + prevPos = pos; + Vec3s ogRot = rot; + Vec3f ogPos = pos; + pos.y += 20.0; + if (!move()) { + break; // Probe is out of bounds. + } + bool forwardTest = fabs(pos.y - ogPos.y) < 1.0; + rot.y = ogRot.y + 16384; + bool clockwiseTest = proveClimbableStep(); + f32 clockwiseY = pos.y; + rot.y = ogRot.y - 16384; + pos = prevPos; + + bool counterclockwiseTest = proveClimbableStep(); + f32 counterclockwiseY = pos.y; + rot.y = ogRot.y; + pos = ogPos; + + if (clockwiseTest && counterclockwiseTest && (forwardTest || wallHeight < 44.0) && + wallHeight < 48 && // probably have to change for adult + (fabs(clockwiseY - counterclockwiseY) < 2.0 || + fabs(clockwiseY - counterclockwiseY) > wallHeight - 5.0)) { + discoverLedge(pos, true); + break; + } else { + discoverWall(pos); + break; + } + // link is climbing + } else if (player->stateFlags1 == PLAYER_STATE1_CLIMBING_LADDER) { + f32 playerHeight = + BgCheck_EntityRaycastFloor3(&actor->play->colCtx, &floorPoly, &floorBgId, &player->actor.world.pos); + f32 floorHeight; + s8 moveMethod = false; + Vec3s_ ogRot = rot; + setVelocity(); + + if (ogRot.y == player->actor.world.rot.y) { + // sets forward probe to look above link + moveMethod = 2; + } + player->actor.world.rot.y = player->actor.shape.rot.y; // corrects links rotation + + if (!move(moveMethod)) { + destroyCurrentSound(); + + break; // Probe is out of bounds. + } + // this following bit checks the wall poly and for now just checks if it has a drop off below it + // or if it is the forward probe, checks if the vine ends otherwise it continues + Vec3f wallPos; + CollisionPoly* wallPoly = checkWall(pos, prevPos, wallPos); + + if (wallPoly != NULL) { + + if ((moveMethod == 2) && (func_80041DB8(&actor->play->colCtx, wallPoly, BGCHECK_SCENE) != 8 && + func_80041DB8(&actor->play->colCtx, wallPoly, BGCHECK_SCENE) != 3)) { + + if (fabs(pos.y - player->actor.world.pos.y) < 100) { + discoverLedge(pos, false); + + break; + } else { + destroyCurrentSound(); + break; + } + + } else { + if (moveMethod != 2 && checkVinePlatform(pos, ogRot, playerHeight)) { + break; + } + continue; + } + } + // this means that either the wall poly found above is not a vine or is NULL + // the next three secections check infront and behind the probe for wall polys + // + if (moveMethod != 2) { + prevPos = pos; + rot.y = player->actor.shape.rot.y; + setVelocity(); + int i = 0; + while (wallPoly == NULL && i < 4) { + move(false); + wallPoly = checkWall(pos, prevPos, wallPos); + i += 1; + } + if (wallPoly != NULL) { + if ((func_80041DB8(&actor->play->colCtx, wallPoly, BGCHECK_SCENE) != 8 && + func_80041DB8(&actor->play->colCtx, wallPoly, BGCHECK_SCENE) != 3)) { + if (checkPerpendicularWall(pos, ogRot)) { + discoverWall(pos); + break; + } + discoverLedge(pos, false); + break; + + } else { + if (checkVinePlatform(pos, ogRot, playerHeight)) { + break; + } + continue; + } + } + Vec3f_ forwardPos = pos; + pos = prevPos; + rot.y = player->actor.shape.rot.y; + + rot.y += 16384; + rot.y += 16384; + setVelocity(); + i = 0; + while (wallPoly == NULL && i < 4) { + + move(false); + i += 1; + wallPoly = checkWall(pos, prevPos, wallPos); + } + if (wallPoly != NULL) { + if ((func_80041DB8(&actor->play->colCtx, wallPoly, BGCHECK_SCENE) != 8 && + func_80041DB8(&actor->play->colCtx, wallPoly, BGCHECK_SCENE) != 3)) { + discoverLedge(pos, false); + + break; + } else { + if (checkVinePlatform(pos, ogRot, playerHeight)) { + break; + } + continue; + } + } + Vec3f_ backPos = pos; + pos = prevPos; + rot = ogRot; + wallPoly = checkWall(backPos, forwardPos, wallPos); + if (wallPoly != NULL) { + if ((func_80041DB8(&actor->play->colCtx, wallPoly, BGCHECK_SCENE) != 8 && + func_80041DB8(&actor->play->colCtx, wallPoly, BGCHECK_SCENE) != 3)) { + discoverLedge(pos, false); + + break; + } else { + if (checkVinePlatform(pos, ogRot, playerHeight)) { + break; + } + continue; + } + } + } + // this means no wall polys were found, first we check for ceilng poly + if (moveMethod == 2) { + rot.y = player->actor.shape.rot.y; + rot.y += 16384; + rot.y += 16384; + setVelocity(); + if (!move(moveMethod)) { + destroyCurrentSound(); + + break; // Probe is out of bounds. + } + prevPos = pos; + // pos.y += 200; + f32 checkHeight = fabs(player->actor.world.pos.y - pos.y); + f32 ceilingPos; + if (BgCheck_AnyCheckCeiling(&actor->play->colCtx, &ceilingPos, &player->actor.world.pos, + checkHeight + 30)) { + + if (checkHeight < 100) { + pos.y = ceilingPos; + discoverWall(pos); + break; + } + } + destroyCurrentSound(); + break; /*else { + if (checkHeight < 200) { + discoverLedge(pos, true); + break; + } + }*/ // not needed? + } + if (moveMethod != 2 && checkPerpendicularWall(pos, ogRot)) { + discoverWall(pos); + break; + } + discoverLedge(pos, false); + break; + + } + + else if (checkForLava(player->actor.world.pos)) { + if (!move()) { + destroyCurrentSound(); + break; // Probe is out of bounds. + } + if (!checkForLava(pos)) { + discoverGround(pos); + break; + } + + } + + // link is on land + else { + + if (!move()) { + destroyCurrentSound(); + break; // Probe is out of bounds. + } + + if (isPushedAway() && player->stateFlags1 != PLAYER_STATE1_CLIMBING_LADDER) { + // Call this a wall for now. + discoverWall(pos); + break; + } + + if (pos.y < prevPos.y && fabs(pos.y - prevPos.y) >= 20 && + player->stateFlags1 != PLAYER_STATE1_CLIMBING_LADDER) { + // This is a fall. + + bool foundLiquid = false; + if (((pos.y - player->actor.prevPos.y) < player->actor.yDistToWater - 30) && + (player->actor.yDistToWater < 0)) { + discoverLedge(pos, 2); + foundLiquid = true; + } else if (rdist(pos) < 100.0) { + s8 i = 50; + Vec3f_ oldPos = pos; + while (i > 0) { + move(); + if (checkForLava(pos)) { + discoverLedge(pos, 3); + foundLiquid = true; + break; + } + i -= 1; + } + pos = oldPos; + } + if (!foundLiquid) { + discoverLedge(pos); + } + + testForPlatform(); + + break; + } + + // checks for water + if (((pos.y - player->actor.prevPos.y) < player->actor.yDistToWater) && + (player->actor.yDistToWater < 0 && player->stateFlags1 != PLAYER_STATE1_CLIMBING_LADDER)) { + discoverWater(pos); + break; + } + + if ((player->actor.yDistToWater > 0) && + (fabs(pos.y - (player->actor.world.pos.y + player->actor.yDistToWater)) > 30.0)) { + discoverLedge(pos); + } + + if (pos.y > prevPos.y && fabs(pos.y - prevPos.y) < 20 && fabs(pos.y - prevPos.y) > 1.2 && + player->stateFlags1 != PLAYER_STATE1_CLIMBING_LADDER) { + // This is an incline. + Vec3f_ bottom = pos; + f32 ogStep = step; + step = 1.0; + move(); + if (fabs(pos.y - bottom.y) > 3.5) { + discoverWall(pos); + break; + } + while ((pos.y > prevPos.y && fabs(pos.y - prevPos.y) < 20 && fabs(pos.y - prevPos.y) > 1.2 && + player->stateFlags1 != PLAYER_STATE1_CLIMBING_LADDER)) { + prevPos = pos; + if (!move()) { + // destroyCurrentSound(); + break; // Probe is out of bounds. + } + } + f32 distToGo = Math_Vec3f_DistXYZ(&bottom, &pos); + if (distToGo > 500.0) { + distToGo = 500.0; + } + f32 pitchModifier = distToGo / 500.0; + + pos = bottom; + discoverIncline(bottom, pitchModifier); + break; + } + if (pos.y < prevPos.y && fabs(pos.y - prevPos.y) < 20 && fabs(pos.y - prevPos.y) > 1.2 && + player->stateFlags1 != PLAYER_STATE1_CLIMBING_LADDER) { + // This is a decline. discover top + Vec3f_ top = pos; + + while ((pos.y < prevPos.y && fabs(pos.y - prevPos.y) < 20 && fabs(pos.y - prevPos.y) > 1.2 && + player->stateFlags1 != PLAYER_STATE1_CLIMBING_LADDER)) { + prevPos = pos; + if (!move()) { + // destroyCurrentSound(); + break; // Probe is out of bounds. + } + } + if (checkForLava(pos)) { + discoverLava(pos); + break; + } + f32 distToGo = Math_Vec3f_DistXYZ(&top, &pos); + if (distToGo > 500.0) { + distToGo = 500.0; + } + f32 pitchModifier = distToGo / 500.0; + + pos = top; + discoverDecline(pos, pitchModifier); + break; + } + + if (checkForLava(pos)) { + discoverLava(pos); + break; + } + + Vec3f wallPos; + CollisionPoly* wallPoly = checkWall(pos, prevPos, wallPos); + if (wallPoly == NULL) + continue; + // Is this a spiked wall? + Vec3f polyVerts[3]; + + CollisionPoly_GetVertices(wallPoly, colCtx->colHeader->vtxList, polyVerts); + if (SurfaceType_IsWallDamage(&actor->play->colCtx, wallPoly, BGCHECK_SCENE)) { + discoverSpike(pos); + break; + } + // is this a ladder or vine wall? + + wallHeight = findWallHeight(pos, wallPoly); + if (wallHeight <= player->ageProperties->unk_0C && + player->stateFlags1 != PLAYER_STATE1_CLIMBING_LADDER) { + // Ledge at top of wall can be reached. + if (proveClimbable()) { + discoverLedge(pos, true); + } else { + discoverWall(pos); + } + + break; + } + + else { + continue; + } + + if (isHeadOnCollision(pos, velocity) && player->stateFlags1 != PLAYER_STATE1_CLIMBING_LADDER) { + discoverWall(pos); + break; + } + } + } + if (trackingMode) + player->actor.world.pos = pos; + // Emit sound from the discovered position. + if (currentSound) + currentSound->update(pos); + if (currentSound && trackingMode) { + disabled = true; + trackingMode = false; + trackingModeStarted = false; + } + } + void testForPlatform() { + Player* player = GET_PLAYER(actor->play); + + f32 ledgeCheckDistance = 200.0; + Vec3f startingPos = pos; + while (ledgeCheckDistance >= 0) { + prevPos = pos; + setVelocity(); + pos.y = player->actor.prevPos.y + 100.0; + f32 step = fabs(velocity.x + velocity.z); + + if (!move()) { + break; // Probe is out of bounds. + } + ledgeCheckDistance -= (step + fabs(pos.y - pos.y)); + + if ((fabs(pos.y - player->actor.prevPos.y) <= 70.00) && fabs(pos.y - prevPos.y) >= 20.0) { + platform.setPosition(pos); + platform.run(); + break; + } + } + pos = startingPos; + } +}; + +typedef struct { + TerrainCueDirection directions[3]; // Directly ahead of Link, 90 degrees to his left and 90 degrees to his right. + int previousAction; // previous action icon state +} TerrainCueState; + +// Callback for initialization of terrain cue state. +bool ActorAccessibility_InitTerrainCueState(AccessibleActor* actor) { + TerrainCueState* state = (TerrainCueState*)malloc(sizeof(TerrainCueState)); + if (state == NULL) + return false; + state->directions[0].init(actor, { 0, 0, 0 }); + state->directions[1].init(actor, { 0, 16384, 0 }); //, PLAYER_BODYPART_L_SHOULDER); + state->directions[2].init(actor, { 0, -16384, 0 }); //, PLAYER_BODYPART_R_SHOULDER); + state->previousAction = DO_ACTION_NONE; + + actor->userData = state; + return true; +} +void ActorAccessibility_CleanupTerrainCueState(AccessibleActor* actor) { + free(actor->userData); + actor->userData = NULL; +} +// Computes a relative angle based on Link's (or some other actor's) current angle. +Vec3s ActorAccessibility_ComputeRelativeAngle(Vec3s* origin, Vec3s* offset) { + Vec3s rot = *origin; + rot.x += offset->x; + rot.y += offset->y; + rot.z += offset->z; + return rot; +} + +void accessible_va_terrain_cue(AccessibleActor* actor) { + TerrainCueState* state = (TerrainCueState*)actor->userData; + + for (int i = 0; i < 3; i++) + state->directions[i].scan(); + + int currentState = actor->play->interfaceCtx.unk_1F0; + Player* player = GET_PLAYER(actor->play); + + if (state->previousAction != currentState) { + switch (currentState) { + case DO_ACTION_CHECK: + SpeechSynthesizer::Instance->Speak("Check", GetLanguageCode()); + break; + case DO_ACTION_CLIMB: + SpeechSynthesizer::Instance->Speak("Climb", GetLanguageCode()); + break; + case DO_ACTION_ENTER: + SpeechSynthesizer::Instance->Speak("Enter", GetLanguageCode()); + break; + case DO_ACTION_GRAB: + SpeechSynthesizer::Instance->Speak("Grab", GetLanguageCode()); + break; + case DO_ACTION_OPEN: + SpeechSynthesizer::Instance->Speak("Open", GetLanguageCode()); + break; + case DO_ACTION_SPEAK: + SpeechSynthesizer::Instance->Speak("Speak", GetLanguageCode()); + break; + case DO_ACTION_STOP: + SpeechSynthesizer::Instance->Speak("Stop", GetLanguageCode()); + break; + } + + state->previousAction = currentState; + } else { + state->previousAction = currentState; + } +} + +/* + void accessible_va_wall_cue(AccessibleActor* actor) { +Player* player = GET_PLAYER(actor->play); +//The virtual cue actors travel in lines relative to Link's angle. +Vec3s rot = computeRelativeAngle(player->actor.world.rot, actor->world.rot); +Vec3f velocity; +velocity.x = Math_SinS(rot.y); +velocity.z = Math_CosS(rot.y); +//Draw a line from Link's position to the max detection distance based on the configured relative angle. +Vec3f pos = player->actor.world.pos; +Vec3f headPos = player->bodyPartsPos[PLAYER_BODYPART_TORSO]; +f32 headDistY = headPos.y - pos.y; + +f32 step = fabs(velocity.x + velocity.z); +f32 distToTravel = detectionDistance; +CollisionPoly* poly = NULL; +Vec3f collisionResult; +s32 bgId = 0; +while (true) { +Vec3f prevPos = pos; +Vec3f prevHeadPos = headPos; + +pos.x += velocity.x; +pos.y += 1; +pos.z += velocity.z; +headPos.x += velocity.x; +headPos.y += 1; +headPos.z += velocity.z; + +distToTravel -= step; +f32 floorHeight = 0; +CollisionPoly tempPoly; +floorHeight = BgCheck_AnyRaycastFloor1(&actor->play->colCtx, &tempPoly, &pos); + pos.y = floorHeight; +headPos.y = floorHeight + headDistY; + +if (BgCheck_AnyLineTest3(&actor->play->colCtx, &prevPos, &pos, &collisionResult, &poly, 1, 0, 0, 0, &bgId)) { + s16 wallYaw = Math_Atan2S(poly->normal.z, poly->normal.x); + CollisionPoly* headPoly = NULL; + + BgCheck_AnyLineTest3(&actor->play->colCtx, &prevHeadPos, &headPos, &collisionResult, &headPoly, 1, 0, 0, 0, + &bgId); + if (headPoly != NULL) + break; + + poly = NULL; + +} + +if (distToTravel <= 0) + break; + +} +if (poly == NULL) { +//Audio_StopSfxByPosAndId(&actor->world.pos, wallCueSound); +return; // Nothing found. + +} +// Move the virtual actor to the position of the discovered wall so we can use preexisting sound + // logic. +actor->world.pos = collisionResult; +framesUntilCue--; +if (framesUntilCue < 1) { +framesUntilCue = 10; +ActorAccessibility_PlaySpecialSound(actor, wallCueSound); + +} + +} +*/ + +void ActorAccessibility_InitCues() { + ActorAccessibilityPolicy policy; + ActorAccessibility_InitPolicy(&policy, "Terrain cue helper", accessible_va_terrain_cue); + policy.n = 1; + policy.runsAlways = true; + policy.distance = 500; + policy.initUserData = ActorAccessibility_InitTerrainCueState; + policy.cleanupUserData = ActorAccessibility_CleanupTerrainCueState; + + ActorAccessibility_AddSupportedActor(VA_TERRAIN_CUE, policy); + VirtualActorList* list = ActorAccessibility_GetVirtualActorList(EVERYWHERE, 0); + ActorAccessibility_AddVirtualActor(list, VA_TERRAIN_CUE, { { 0, 0, 0 }, { 0, 0, 0 } }); +} \ No newline at end of file diff --git a/soh/soh/Enhancements/audio/miniaudio.h b/soh/soh/Enhancements/audio/miniaudio.h new file mode 100644 index 000000000..4c7e80aef --- /dev/null +++ b/soh/soh/Enhancements/audio/miniaudio.h @@ -0,0 +1,9 @@ +#pragma once +#define MA_NO_FLAC +#define MA_NO_MP3 +#define MA_NO_THREADING +#define MA_NO_DEVICE_IO +#define MA_NO_GENERATION +#define MA_NO_STDIO +#define MA_ENABLE_ONLY_SPECIFIC_BACKENDS +#include "miniaudio/miniaudio.h" \ No newline at end of file diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h index ecd970b31..5b2aa3f6d 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -27,6 +27,7 @@ DEFINE_HOOK(OnCuccoOrChickenHatch, ()); DEFINE_HOOK(OnShopSlotChange, (uint8_t cursorIndex, int16_t price)); DEFINE_HOOK(OnActorInit, (void* actor)); DEFINE_HOOK(OnActorUpdate, (void* actor)); +DEFINE_HOOK(OnActorDestroy, (void* actor)); DEFINE_HOOK(OnActorKill, (void* actor)); DEFINE_HOOK(OnEnemyDefeat, (void* actor)); DEFINE_HOOK(OnBossDefeat, (void* actor)); @@ -65,6 +66,7 @@ DEFINE_HOOK(OnUpdateFileBossRushOptionSelection, (uint8_t optionIndex, uint8_t o DEFINE_HOOK(OnUpdateFileNameSelection, (int16_t charCode)); DEFINE_HOOK(OnSetGameLanguage, ()); +DEFINE_HOOK(OnGameStillFrozen, ()); DEFINE_HOOK(OnFileDropped, (std::string filePath)); DEFINE_HOOK(OnAssetAltChange, ()); DEFINE_HOOK(OnKaleidoUpdate, ()); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 5e52bed71..0d96da967 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -110,7 +110,9 @@ void GameInteractor_ExecuteOnActorUpdate(void* actor) { GameInteractor::Instance->ExecuteHooksForPtr((uintptr_t)actor, actor); GameInteractor::Instance->ExecuteHooksForFilter(actor); } - +void GameInteractor_ExecuteOnActorDestroy(void* actor) { + GameInteractor::Instance->ExecuteHooks(actor); +} void GameInteractor_ExecuteOnActorKill(void* actor) { GameInteractor::Instance->ExecuteHooks(actor); GameInteractor::Instance->ExecuteHooksForID(((Actor*)actor)->id, actor); @@ -286,6 +288,10 @@ void GameInteractor_ExecuteOnSetGameLanguage() { GameInteractor::Instance->ExecuteHooks(); } +void GameInteractor_ExecuteOnGameStillFrozen() { + GameInteractor::Instance->ExecuteHooks(); +} + // MARK: - System void GameInteractor_RegisterOnAssetAltChange(void (*fn)(void)) { diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index b1f9195b4..22a372c89 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -29,6 +29,7 @@ void GameInteractor_ExecuteOnOcarinaSongAction(); void GameInteractor_ExecuteOnCuccoOrChickenHatch(); void GameInteractor_ExecuteOnActorInit(void* actor); void GameInteractor_ExecuteOnActorUpdate(void* actor); +void GameInteractor_ExecuteOnActorDestroy(void* actor); void GameInteractor_ExecuteOnActorKill(void* actor); void GameInteractor_ExecuteOnEnemyDefeat(void* actor); void GameInteractor_ExecuteOnBossDefeat(void* actor); @@ -74,6 +75,8 @@ void GameInteractor_ExecuteOnUpdateFileNameSelection(int16_t charCode); // MARK: - Game void GameInteractor_ExecuteOnSetGameLanguage(); +void GameInteractor_ExecuteOnGameStillFrozen(); + // MARK: - System void GameInteractor_RegisterOnAssetAltChange(void (*fn)(void)); diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 72030657e..528c02f5d 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -128,6 +128,9 @@ Sail* Sail::Instance; #include "soh/config/ConfigUpdaters.h" #include "soh/ShipInit.hpp" +#if !defined(__SWITCH__) && !defined(__WIIU__) +#include "Enhancements/accessible-actors/ActorAccessibility.h" +#endif extern "C" { #include "src/overlays/actors/ovl_En_Dns/z_en_dns.h" @@ -290,6 +293,12 @@ OTRGlobals::OTRGlobals() { } } } + + std::string sohAccessibilityPath = Ship::Context::GetPathRelativeToAppBundle("accessibility.o2r"); + if (std::filesystem::exists(sohAccessibilityPath)) { + OTRFiles.push_back(sohAccessibilityPath); + } + std::sort(patchOTRs.begin(), patchOTRs.end(), [](const std::string& a, const std::string& b) { return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), [](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); }); @@ -580,6 +589,11 @@ void OTRAudio_Thread() { for (int i = 0; i < AUDIO_FRAMES_PER_UPDATE; i++) { AudioMgr_CreateNextAudioBuffer(audio_buffer + i * (num_audio_samples * NUM_AUDIO_CHANNELS), num_audio_samples); +#if !defined(__SWITCH__) && !defined(__WIIU__) + // Give accessibility a chance to merge its own audio in. + ActorAccessibility_MixAccessibleAudioWithGameAudio( + audio_buffer + i * (num_audio_samples * NUM_AUDIO_CHANNELS), num_audio_samples); +#endif } AudioPlayer_Play((u8*)audio_buffer, @@ -1239,6 +1253,9 @@ extern "C" void InitOTR() { #endif OTRMessage_Init(); +#if !defined(__SWITCH__) && !defined(__WIIU__) + ActorAccessibility_Init(); +#endif OTRAudio_Init(); OTRExtScanner(); VanillaItemTable_Init(); @@ -1290,6 +1307,9 @@ extern "C" void DeinitOTR() { } SDLNet_Quit(); #endif +#if !defined(__SWITCH__) && !defined(__WIIU__) + ActorAccessibility_Shutdown(); +#endif // Destroying gui here because we have shared ptrs to LUS objects which output to SPDLOG which is destroyed before // these shared ptrs. @@ -2513,6 +2533,41 @@ extern "C" void Gfx_RegisterBlendedTexture(const char* name, u8* mask, u8* repla gfx_register_blended_texture(name, mask, replacement); } +void OTRAudio_SfxCaptureThread() { + while (audio.running) { + { + std::unique_lock Lock(audio.mutex); + while (!audio.processing && audio.running) { + audio.cv_to_thread.wait(Lock); + } + + if (!audio.running) { + break; + } + } + std::unique_lock Lock(audio.mutex); +#if !defined(__SWITCH__) && !defined(__WIIU__) + ActorAccessibility_DoSoundExtractionStep(); +#endif + audio.processing = false; + audio.cv_from_thread.notify_one(); + } +} + +extern "C" void OTRAudio_InstallSfxCaptureThread() { + OTRAudio_Exit(); + audio.running = true; + audio.thread = std::thread(OTRAudio_SfxCaptureThread); +} +extern "C" void OTRAudio_UninstallSfxCaptureThread() { + OTRAudio_Exit(); + audio.running = true; + audio.thread = std::thread(OTRAudio_Thread); +} +std::unique_lock OTRAudio_Lock() { + return std::unique_lock(audio.mutex); +} + extern "C" void Gfx_UnregisterBlendedTexture(const char* name) { gfx_unregister_blended_texture(name); } diff --git a/soh/soh/OTRGlobals.h b/soh/soh/OTRGlobals.h index b14d322d2..b221acfc9 100644 --- a/soh/soh/OTRGlobals.h +++ b/soh/soh/OTRGlobals.h @@ -164,6 +164,7 @@ void Gfx_TextureCacheDelete(const uint8_t* addr); void SaveManager_ThreadPoolWait(); void CheckTracker_OnMessageClose(); +int32_t GetGIID(uint32_t itemID); GetItemID RetrieveGetItemIDFromItemID(ItemID itemID); RandomizerGet RetrieveRandomizerGetFromItemID(ItemID itemID); #endif @@ -172,8 +173,12 @@ RandomizerGet RetrieveRandomizerGetFromItemID(ItemID itemID); extern "C" { #endif uint64_t GetUnixTimestamp(); +void OTRAudio_InstallSfxCaptureThread(); +void OTRAudio_UninstallSfxCaptureThread(); #ifdef __cplusplus }; +std::unique_lock OTRAudio_Lock(); + #endif #endif diff --git a/soh/soh/SohGui/SohMenuSettings.cpp b/soh/soh/SohGui/SohMenuSettings.cpp index 909c647bd..818635f5c 100644 --- a/soh/soh/SohGui/SohMenuSettings.cpp +++ b/soh/soh/SohGui/SohMenuSettings.cpp @@ -195,6 +195,18 @@ void SohMenu::AddMenuSettings() { .CVar(CVAR_SETTING("A11yDisableIdleCam")) .RaceDisable(false) .Options(CheckboxOptions().Tooltip("Disables the automatic re-centering of the camera when idle.")); + + AddWidget(path, "Accessible Audio Cues", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_SETTING("A11yAudioInteraction")) + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Enables accessibility audio cues")); + + AddWidget(path, "Extract Sfx", WIDGET_CVAR_CHECKBOX) + .CVar("gExtractSfx") + .RaceDisable(false) + .Options(CheckboxOptions().Tooltip("Extracts the sfx to be used in accessible audio cues, must be run once for " + "the audio cues to play then restart game")); + AddWidget(path, "EXPERIMENTAL", WIDGET_SEPARATOR_TEXT).Options(TextOptions().Color(Colors::Orange)); AddWidget(path, "ImGui Menu Scaling", WIDGET_CVAR_COMBOBOX) .CVar(CVAR_SETTING("ImGuiScale")) diff --git a/soh/src/code/code_800F9280.c b/soh/src/code/code_800F9280.c index 8651e2a4d..dc15d4b1f 100644 --- a/soh/src/code/code_800F9280.c +++ b/soh/src/code/code_800F9280.c @@ -3,6 +3,7 @@ #include "soh/mixer.h" #include "soh/Enhancements/audio/AudioEditor.h" +extern bool freezeGame; typedef struct { u8 unk_0; @@ -372,6 +373,9 @@ extern f32 D_80130F24; extern f32 D_80130F28; void Audio_QueueSeqCmd(u32 cmd) { + if (freezeGame) + return; // No music during SFX rip. + // u8 op = cmd >> 28; if (op == 0 || op == 2 || op == 12) { u8 seqId = cmd & 0xFF; diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index 3ca67ad3b..859c6634c 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -80,6 +80,7 @@ #include "textures/place_title_cards/g_pn_56.h" #include "textures/place_title_cards/g_pn_57.h" #endif +bool freezeActors = false; static CollisionPoly* sCurCeilingPoly; static s32 sCurCeilingBgId; @@ -1267,6 +1268,7 @@ void Actor_Init(Actor* actor, PlayState* play) { } void Actor_Destroy(Actor* actor, PlayState* play) { + GameInteractor_ExecuteOnActorDestroy(actor); if (actor->destroy != NULL) { actor->destroy(actor, play); actor->destroy = NULL; @@ -2556,6 +2558,7 @@ u32 D_80116068[ACTORCAT_MAX] = { }; void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { + Actor* refActor; Actor* actor; Player* player; @@ -2571,6 +2574,11 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { sp74 = NULL; unkFlag = 0; + if (freezeActors) { + GameInteractor_ExecuteOnPlayerUpdate(player); + return; // for AudioGlossary + } + if (play->numSetupActors != 0) { actorEntry = &play->setupActorList[0]; for (i = 0; i < play->numSetupActors; i++) { diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index dfc832fc7..aa1580d9e 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -21,6 +21,7 @@ #include #include +bool freezeGame = false; // Used for SFX ripper. TransitionUnk sTrnsnUnk; s32 gTrnsnUnkState; VisMono gPlayVisMono; @@ -694,6 +695,11 @@ void Play_Update(PlayState* play) { s32 isPaused; s32 pad1; + if (freezeGame) { + GameInteractor_ExecuteOnGameStillFrozen(); + return; + } + if ((SREG(1) < 0) || (DREG(0) != 0)) { SREG(1) = 0; ZeldaArena_Display();