diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index e74c14f4c..b2541f9b0 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -161,6 +161,11 @@ if (NOT ESPEAK) list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/ESpeak") 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/*") @@ -294,6 +299,14 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(dr_libs) +FetchContent_Declare( + miniaudio + GIT_REPOSITORY https://github.com/mackron/miniaudio.git + GIT_TAG 350784a9467a79d0fa65802132668e5afbcf3777 + SOURCE_SUBDIR "ignore CMakeLists.txt" +) +FetchContent_MakeAvailable(miniaudio) + find_package(SDL2) set(SDL2-INCLUDE ${SDL2_INCLUDE_DIRS}) @@ -349,6 +362,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE assets ${SDL2-NET-INCLUDE} ${CMAKE_CURRENT_SOURCE_DIR}/assets/ ${dr_libs_SOURCE_DIR} + ${miniaudio_SOURCE_DIR} . ) diff --git a/soh/soh/Enhancements/accessible-actors/AccessibleActorList.cpp b/soh/soh/Enhancements/accessible-actors/AccessibleActorList.cpp new file mode 100644 index 000000000..830dd80d3 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/AccessibleActorList.cpp @@ -0,0 +1,1394 @@ +#include "ActorAccessibility.h" +#include "soh/OTRGlobals.h" + +#include +#include +#include + +extern "C" { +#include "z64collision_check.h" +#include "overlays/actors/ovl_Bg_Bdan_Switch/z_bg_bdan_switch.h" +#include "overlays/actors/ovl_Bg_Mizu_Movebg/z_bg_mizu_movebg.h" +#include "overlays/actors/ovl_Bg_Po_Event/z_bg_po_event.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_Ba/z_en_ba.h" +#include "overlays/actors/ovl_En_Box/z_en_box.h" +#include "overlays/actors/ovl_Door_Shutter/z_door_shutter.h" +#include "overlays/actors/ovl_En_Dog/z_en_dog.h" +#include "overlays/actors/ovl_En_Door/z_en_door.h" +#include "overlays/actors/ovl_En_Eiyer/z_en_eiyer.h" +#include "overlays/actors/ovl_En_Elf/z_en_elf.h" +#include "overlays/actors/ovl_En_Fz/z_en_fz.h" +#include "overlays/actors/ovl_En_Ik/z_en_ik.h" +#include "overlays/actors/ovl_En_G_Switch/z_en_g_switch.h" +#include "overlays/actors/ovl_En_Kakasi2/z_en_kakasi2.h" +#include "overlays/actors/ovl_En_Wood02/z_en_wood02.h" +#include "overlays/actors/ovl_En_Wonder_Item/z_en_wonder_item.h" +#include "overlays/actors/ovl_Obj_Switch/z_obj_switch.h" +#include "overlays/actors/ovl_Obj_Syokudai/z_obj_syokudai.h" + +void EnBox_WaitOpen(EnBox*, PlayState*); +void EnKarebaba_DeadItemDrop(EnKarebaba*, PlayState*); +void EnDog_FollowPlayer(EnDog*, PlayState*); +s8 EnDog_CanFollow(EnDog*, PlayState*); +void EnEiyer_Die(EnEiyer*, PlayState*); +void EnEiyer_Dead(EnEiyer*, PlayState*); +void EnGSwitch_SilverRupeeIdle(EnGSwitch*, PlayState*); + +extern u8 sBgPoEventPuzzleState; +} + +#define MOVEBG_TYPE(params) (((u16)(params) >> 0xC) & 0xF) + +void accessible_switch(AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + ObjSwitch* sw = (ObjSwitch*)actor->actor; + Vec3f& scale = actor->actor->scale; + if ((actor->actor->params & 7) == OBJSWITCH_TYPE_FLOOR) { + if (actor->xyzDistToPlayer > 800) { + return; + } + if (scale.y >= 33.0f / 200.0f) { + if (actor->play->sceneNum == SCENE_DEKU_TREE && actor->play->roomCtx.curRoom.num == 5 && + actor->xzDistToPlayer < 20) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIAMOND_SWITCH); + } + if ((actor->frameCount & 31) != 0) { + return; + } + ActorAccessibility_PlaySoundForActor(actor, 1, NA_SE_EV_FOOT_SWITCH); + } + } else if ((actor->frameCount & 31) != 0) { + return; + } else if ((actor->actor->params & 7) == OBJSWITCH_TYPE_FLOOR_RUSTY) { + if (actor->xyzDistToPlayer < 800 && scale.y >= 33.0f / 200.0f) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_HAMMER_HIT); + } + } else if ((actor->actor->params & 7) == OBJSWITCH_TYPE_EYE) { + s32 subType = (actor->actor->params >> 4) & 7; + if (subType != 0 || sw->eyeTexIndex == 0) { + actor->policy.aimAssist.isProvider = AIM_SHOOT; + actor->policy.ydist = 1000; + // prevent hearing yellow eye in forest temple block puzzle heard on top floor + if (!(actor->play->sceneNum == SCENE_FOREST_TEMPLE && actor->play->roomCtx.curRoom.num == 11 && + actor->pos.y < player->actor.world.pos.y - 100)) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_FOOT_SWITCH); + } + } + } else if (actor->xyzDistToPlayer < 1000) { + actor->policy.aimAssist.isProvider = AIM_ALL; + actor->policy.ydist = 1000; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIAMOND_SWITCH); + } +} + +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 != SCENE_DEATH_MOUNTAIN_TRAIL && + actor->play->sceneNum != SCENE_HYRULE_FIELD && actor->play->sceneNum != SCENE_KAKARIKO_VILLAGE && + actor->play->sceneNum != SCENE_LOST_WOODS) { + return; + } + + if (actor->play->sceneNum == SCENE_HYRULE_FIELD) { + 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; + } + } + } else if (actor->play->sceneNum == SCENE_KAKARIKO_VILLAGE) { + if (actor->sceneIndex == SCENE_GRAVEYARD || actor->sceneIndex == SCENE_HYRULE_FIELD || + actor->sceneIndex == SCENE_DEATH_MOUNTAIN_TRAIL) { + actor->policy.runsAlways = true; + actor->policy.ydist = 5000; + if (actor->xzDistToPlayer > 700) { + if (actor->sceneIndex == SCENE_HYRULE_FIELD) { + 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 == SCENE_BOTTOM_OF_THE_WELL) { + if (!Flags_GetEventChkInf(EVENTCHKINF_DRAINED_WELL_IN_KAKARIKO)) + return; + } else { + actor->policy.ydist = 500; + actor->policy.distance = 1000; + if (actor->xzDistToPlayer > 1000) { + return; + } + } + } else if (actor->play->sceneNum == SCENE_LOST_WOODS || + actor->play->sceneNum == SCENE_CASTLE_COURTYARD_GUARDS_DAY || + actor->play->sceneNum == SCENE_CASTLE_COURTYARD_GUARDS_NIGHT) { + actor->policy.distance = 1000; + if (actor->xzDistToPlayer > 1000) { + return; + } + } else { + if (actor->xzDistToPlayer > 1500) { + return; + } + } + + if (actor->sceneIndex == SCENE_KOKIRI_FOREST || actor->sceneIndex == SCENE_LOST_WOODS) { + if (actor->play->sceneNum == SCENE_LOST_WOODS && + gSaveContext.entranceIndex != ENTR_LOST_WOODS_BRIDGE_EAST_EXIT && + gSaveContext.entranceIndex != ENTR_LOST_WOODS_BRIDGE_WEST_EXIT) { + return; + } + if (actor->play->sceneNum == SCENE_KOKIRI_FOREST && actor->pos.y < 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_HORSE_RUN_LEVEL); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SARIA_MELODY); + } + // kokiri forest and lost woods + } else if (actor->play->sceneNum >= SCENE_DEKU_TREE_BOSS && actor->play->sceneNum <= SCENE_GANONDORF_BOSS) { + return; // dont check for entrances while in boss rooms + } else if (actor->play->sceneNum == SCENE_GROTTOS || actor->play->sceneNum == SCENE_FAIRYS_FOUNTAIN) { + actor->policy.volume = 0.1; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_WARP_HOLE); + } else if (actor->sceneIndex == SCENE_HYRULE_FIELD) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_HORSE_RUN_LEVEL); + } else if (actor->sceneIndex <= SCENE_GERUDO_TRAINING_GROUND) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_STALTU_LAUGH); + } else if (actor->sceneIndex >= SCENE_MARKET_ENTRANCE_DAY && actor->sceneIndex <= SCENE_MARKET_ENTRANCE_RUINS) { + if (actor->play->sceneNum >= SCENE_MARKET_DAY && actor->play->sceneNum <= SCENE_MARKET_RUINS) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_HORSE_RUN_LEVEL); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SMALL_DOG_BARK); + } + } else if (actor->sceneIndex >= SCENE_BACK_ALLEY_DAY && actor->sceneIndex <= SCENE_MARKET_NIGHT) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SMALL_DOG_BARK); + } else if (actor->sceneIndex >= SCENE_MARKET_RUINS && actor->sceneIndex <= SCENE_TEMPLE_OF_TIME_EXTERIOR_RUINS) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_STONE_BOUND); + } else if (actor->play->sceneNum == SCENE_TEMPLE_OF_TIME) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SMALL_DOG_BARK); + } else if (actor->sceneIndex == SCENE_CASTLE_COURTYARD_GUARDS_DAY || + actor->sceneIndex == SCENE_CASTLE_COURTYARD_GUARDS_NIGHT) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_MUSI_SINK); + } else if (actor->sceneIndex == SCENE_KAKARIKO_VILLAGE) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_CHICKEN_CRY_M); + } else if (actor->sceneIndex == SCENE_GRAVEYARD) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_PO_APPEAR); + } else if (actor->sceneIndex == SCENE_ZORAS_RIVER || actor->sceneIndex == SCENE_ZORAS_DOMAIN || + actor->sceneIndex == SCENE_ZORAS_FOUNTAIN) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_RIVER_STREAM_S); + } else if (actor->sceneIndex == SCENE_SACRED_FOREST_MEADOW) { + } else if (actor->sceneIndex == SCENE_LAKE_HYLIA) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_WATER_WALL); + } else if (actor->sceneIndex == SCENE_GERUDO_VALLEY || actor->sceneIndex == SCENE_GERUDOS_FORTRESS) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_CRAWL_SAND); + } else if (actor->sceneIndex == SCENE_DESERT_COLOSSUS || actor->sceneIndex == SCENE_HAUNTED_WASTELAND) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SAND_STORM); + } else if (actor->sceneIndex == SCENE_OUTSIDE_GANONS_CASTLE || actor->sceneIndex == SCENE_HYRULE_CASTLE) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_BRIDGE_OPEN); + } else if (actor->sceneIndex == SCENE_DEATH_MOUNTAIN_TRAIL) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_DODO_K_ROLL); + } else if (actor->sceneIndex == SCENE_DEATH_MOUNTAIN_CRATER) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_DODO_K_LAVA); + } else if (actor->sceneIndex == SCENE_GORON_CITY) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_DARUNIA_HIT_BREAST); + } else if (actor->sceneIndex == SCENE_LON_LON_RANCH) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_COW_CRY); + } else if (actor->sceneIndex >= SCENE_DEKU_TREE_BOSS && actor->sceneIndex <= SCENE_GANONDORF_BOSS) { + return; + } else { + actor->policy.distance = 500; + if (actor->play->sceneNum == SCENE_GRAVEYARD) { + actor->policy.ydist = 0; + } + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_OC_DOOR_OPEN); + } +} + +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); + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx); + 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); + ActorAccessibility_SetSoundPitch(actor, 0, 0.2); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx); + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx); + 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); + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx); + 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); + ActorAccessibility_SetSoundPitch(actor, 0, 0.2); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx); + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, guardsfx); + ActorAccessibility_SetSoundPitch(actor, 0, 1.0); + } + } +} + +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 = "Darunia"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_DU, policy); + policy.englishName = "Nabooru"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_NB, policy); + policy.englishName = "Owl"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_OWL, policy); + ActorAccessibility_InitPolicy(&policy, "Cursed Skulltula Person", NA_SE_VO_ST_DAMAGE); + ActorAccessibility_AddSupportedActor(ACTOR_EN_SSH, 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 = "Shop Keeper"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_OSSAN, policy); + policy.englishName = "Potion Shop Granny"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_DS, policy); + policy.englishName = "Magic Carpet Man"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_JS, 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 = "Zoras"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_ZO, policy); + policy.englishName = "Gorons"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_GO2, policy); + policy.englishName = "Saria"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_SA, policy); + policy.englishName = "King Zora"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_KZ, policy); + policy.englishName = "Lakeside Professor"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_MK, 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 = "Running Man"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_MM, policy); + ActorAccessibility_AddSupportedActor(ACTOR_EN_MM2, policy); + policy.englishName = "Sheik"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_XC, policy); + policy.englishName = "Market Npc"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_HY, policy); + policy.englishName = "Girl Chasing 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 = "Carpenter's Son"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_HS, policy); + ActorAccessibility_AddSupportedActor(ACTOR_EN_HS2, policy); + policy.englishName = "Bean Salesman"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_MS, 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 = "Uncursed Skulltula Person"; + ActorAccessibility_AddSupportedActor(ACTOR_EN_STH, policy); + policy.englishName = "Gossip Stone"; + policy.pitch = 0.75; + ActorAccessibility_AddSupportedActor(ACTOR_EN_GS, policy); + ActorAccessibility_InitPolicy(&policy, "Loading Zone", NA_SE_EV_DIG_UP); + ActorAccessibility_AddSupportedActor(ACTOR_EN_HOLL, policy); + + ActorAccessibility_InitPolicy(&policy, "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); + 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); + + ActorAccessibility_SetSoundPitch(actor, 0, 2.0); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_SMALL_DOG_BARK); + ActorAccessibility_SetSoundPitch(actor, 0, 0.5); + } + }); + 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", [](AccessibleActor* actor) { + if (actor->actor->params == 14) { + } else if (actor->actor->params == 13) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_CHICKEN_CRY_N); + ActorAccessibility_SetSoundPitch(actor, 0, 1.5); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_CHICKEN_CRY_N); + } + }); + + 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", [](AccessibleActor* actor) { + EnWood02* wood = (EnWood02*)actor->actor; + if (wood->actor.params <= WOOD_TREE_KAKARIKO_ADULT) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_TREE_CUT); + if (actor->play->sceneNum == SCENE_HYRULE_FIELD) { + actor->policy.distance = 3000; + actor->policy.ydist = 1000; + } + } else if (wood->actor.params < WOOD_LEAF_GREEN) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_TREE_SWING); + } + }); + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_EN_WOOD02, policy); + ActorAccessibility_InitPolicy(&policy, "Lake Hylia Sun", [](AccessibleActor* actor) { + s32 params = actor->actor->params & 0xFF; + if (params != 0x40 && params != 0x41 && gSaveContext.dayTime >= 0x4555 && gSaveContext.dayTime < 0x5000) { + if (actor->xzDistToPlayer <= 120 && (actor->frameCount & 31) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_BOW_FLICK); + } + actor->policy.aimAssist.isProvider = AIM_BOW; + } else { + actor->policy.aimAssist.isProvider = 0; + } + }); + policy.runsAlways = true; + ActorAccessibility_AddSupportedActor(ACTOR_SHOT_SUN, policy); + ActorAccessibility_InitPolicy(&policy, "Hookshot Pillar", NA_SE_IT_HOOKSHOT_STICK_OBJ); + policy.distance = 1000; + policy.ydist = 1000; + policy.aimAssist.isProvider = AIM_HOOK; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_HSBLOCK, policy); + ActorAccessibility_InitPolicy(&policy, "Water Temple Hookshot Platform", [](AccessibleActor* actor) { + if ((actor->frameCount & 31) == 0) { + BgMizuMovebg* movebg = (BgMizuMovebg*)actor->actor; + if (MOVEBG_TYPE(actor->actor->params) != 7) { + actor->policy.aimAssist.isProvider = 0; + } + if (movebg->sfxFlags & 1) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_ROLL_STAND_2); + } + if (movebg->sfxFlags & 2) { + ActorAccessibility_PlaySoundForActor(actor, 1, NA_SE_EV_ELEVATOR_MOVE); + } + } + }); + policy.distance = 1000; + policy.ydist = 1000; + policy.volume = 0.5; + policy.aimAssist.isProvider = AIM_HOOK; + ActorAccessibility_AddSupportedActor(ACTOR_BG_MIZU_MOVEBG, policy); + ActorAccessibility_InitPolicy(&policy, "Scarecrow", NA_SE_IT_KAKASHI_JUMP); + policy.volume = 2; + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_EN_KAKASI, policy); + ActorAccessibility_AddSupportedActor(ACTOR_EN_KAKASI3, policy); + ActorAccessibility_InitPolicy(&policy, "Scarecrow Spawn", [](AccessibleActor* actor) { + EnKakasi2* kakasi = (EnKakasi2*)actor->actor; + actor->policy.ydist = kakasi->maxSpawnDistance.y; + if (actor->xzDistToPlayer < kakasi->maxSpawnDistance.x) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_KAKASHI_JUMP); + } + if (kakasi->switchFlag >= 0 && Flags_GetSwitch(actor->play, kakasi->switchFlag)) { + actor->policy.aimAssist.isProvider = AIM_HOOK; + } + }); + policy.ydist = 0x4000; + policy.distance = 0x4000; + policy.n = 40; + ActorAccessibility_AddSupportedActor(ACTOR_EN_KAKASI2, 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); + } + // 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) < 0) { + ActorAccessibility_SetSoundPitch(actor, 0, 0.5); + } else if ((xdist + size / 2.0) > 0 && zdist < size / 2.0 && xdist < 150.0) { + ActorAccessibility_PlaySoundForActor(actor, 1, NA_SE_EV_DIAMOND_SWITCH); + } + }); + policy.pitch = 1.1; + policy.distance = 1000; + policy.aimAssist.isProvider = AIM_HOOK; + 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, "Small Crate", NA_SE_EV_WOODBOX_BREAK); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_KIBAKO, policy); + policy.englishName = "Crate"; + policy.aimAssist.isProvider = AIM_HOOK; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_KIBAKO2, policy); + ActorAccessibility_InitPolicy(&policy, "deku stick drops", [](AccessibleActor* actor) { + EnKarebaba* baba = (EnKarebaba*)actor->actor; + if (baba->actionFunc == EnKarebaba_DeadItemDrop && actor->actor->flags == 80) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_NUTS_DAMAGE); + } + }); + + 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); + + 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", NA_SE_EN_NUTS_DAMAGE); + policy.n = 40; + policy.pitch = 1.4; + ActorAccessibility_AddSupportedActor(ACTOR_EN_ITEM00, policy); + ActorAccessibility_InitPolicy(&policy, "Collectible", [](AccessibleActor* actor) { + if (actor->actor->category == ACTORCAT_ITEMACTION) { + s16 params = actor->actor->params; + if (params == FAIRY_HEAL || params == FAIRY_HEAL_TIMED || params == FAIRY_HEAL_BIG) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_BUTTERFRY_TO_FAIRY); + } + } + }); + policy.n = 40; + ActorAccessibility_AddSupportedActor(ACTOR_EN_ELF, policy); + + ActorAccessibility_InitPolicy(&policy, "frog spot", NA_SE_EN_DODO_M_EAT); + policy.distance = 100; + policy.n = 40; + ActorAccessibility_AddSupportedActor(ACTOR_EN_FR, policy); + + ActorAccessibility_InitPolicy(&policy, "big poe spawn", NA_SE_EN_PO_BIG_GET); + policy.distance = 1500; + policy.n = 60; + ActorAccessibility_AddSupportedActor(ACTOR_EN_PO_FIELD, policy); + + ActorAccessibility_InitPolicy(&policy, "market bridge", NA_SE_EV_BRIDGE_OPEN); + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT00_HANEBASI, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT00_BREAK, policy); + + ActorAccessibility_InitPolicy(&policy, "haunted wasteland poe", NA_SE_EN_PO_CRY); + policy.distance = 600; + policy.ydist = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_EN_PO_DESERT, policy); + ActorAccessibility_InitPolicy(&policy, "flag pole", [](AccessibleActor* actor) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_FLUTTER_FLAG); + }); + policy.aimAssist.isProvider = AIM_HOOK; + policy.distance = 1000; + policy.volume = 1.5; + ActorAccessibility_AddSupportedActor(ACTOR_EN_HATA, policy); + ActorAccessibility_InitPolicy(&policy, "oasis", NA_SE_EV_SCOOPUP_WATER); + policy.distance = 2000; + policy.ydist = 500; + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT11_OASIS, 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_AddSupportedActor(ACTOR_BG_HIDAN_KOWARERUKABE, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_MIZU_BWALL, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_JYA_BOMBIWA, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_HAKA_ZOU, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_BOMBWALL, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_BREAKWALL, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT08_BAKUDANKABE, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT17_BAKUDANKABE, policy); + ActorAccessibility_InitPolicy(&policy, "Bronze Boulder", NA_SE_IT_HAMMER_HIT); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_HAMISHI, policy); + ActorAccessibility_InitPolicy(&policy, "Time Block", NA_SE_EV_TIMETRIP_LIGHT); + policy.distance = 800; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_TIMEBLOCK, policy); + ActorAccessibility_InitPolicy(&policy, "Grotto Door", [](AccessibleActor* actor) { + if ((actor->actor->params & 0x300) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DROP_FALL); + } + }); + + 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", [](AccessibleActor* actor) { + DoorShutter* doorShutter = (DoorShutter*)actor->actor; + if (doorShutter->doorType == SHUTTER_KEY_LOCKED && !Flags_GetSwitch(actor->play, actor->actor->params & 0x3F)) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_CHAIN_KEY_UNLOCK_B); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_OC_DOOR_OPEN); + } + }); + policy.n = 30; + policy.distance = 1000; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_DOOR_SHUTTER, policy); + ActorAccessibility_InitPolicy(&policy, "Killer Door", NA_SE_EN_KDOOR_WAVE); + ActorAccessibility_AddSupportedActor(ACTOR_DOOR_KILLER, policy); + ActorAccessibility_InitPolicy(&policy, "Ice Shutter Door", NA_SE_OC_DOOR_OPEN); + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT18_SHUTTER, policy); + ActorAccessibility_InitPolicy(&policy, "Switch", accessible_switch); + policy.distance = 2000; + policy.n = 1; + policy.ydist = 200; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_SWITCH, policy); + ActorAccessibility_InitPolicy(&policy, "Wonder", [](AccessibleActor* actor) { + auto wonder = (EnWonderItem*)actor->actor; + if (wonder->wonderMode == WONDERITEM_INTERACT_SWITCH) { + auto colTypeIndex = wonder->actor.world.rot.z & 0xFF; + switch (colTypeIndex) { + case 1: + actor->policy.aimAssist.isProvider = AIM_BOW; + case 4: + actor->policy.aimAssist.isProvider = AIM_SLING; + case 5: + actor->policy.aimAssist.isProvider = AIM_BOOM; + case 6: + actor->policy.aimAssist.isProvider = AIM_HOOK; + } + } else if (wonder->wonderMode == WONDERITEM_BOMB_SOLDIER) { + actor->policy.aimAssist.isProvider = AIM_SLING; + } + }); + policy.ydist = 1000; + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_EN_WONDER_ITEM, 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 = AIM_ALL; + } + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIAMOND_SWITCH); + }); + policy.volume = 0.6; + policy.distance = 1000; + policy.ydist = 300; + ActorAccessibility_AddSupportedActor(ACTOR_BG_BDAN_SWITCH, policy); + ActorAccessibility_InitPolicy(&policy, "Sunlight Switch", NA_SE_EV_TRIFORCE_FLASH); + policy.volume = 0.5; + policy.n = 60; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_LIGHTSWITCH, policy); + ActorAccessibility_InitPolicy(&policy, "Lightbeam", NA_SE_PL_ARROW_CHARGE_LIGHT); + policy.volume = 0.5; + policy.n = 60; + ActorAccessibility_AddSupportedActor(ACTOR_MIR_RAY, policy); + ActorAccessibility_InitPolicy(&policy, "Jabu Object", [](AccessibleActor* actor) { + if ((actor->actor->params & 0xFF) == 2 && actor->xzDistToPlayer > 50) { + // Jabu Elevator + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_LAND_LADDER); + } + }); + policy.n = 1; + policy.ydist = 50; + ActorAccessibility_AddSupportedActor(ACTOR_BG_BDAN_OBJECTS, policy); + ActorAccessibility_InitPolicy(&policy, "Forest Basement Gate", NA_SE_EV_METALGATE_OPEN); + policy.ydist = 1; + policy.distance = 200; + policy.volume = 0.5; + policy.pitch = 1.2; + ActorAccessibility_AddSupportedActor(ACTOR_BG_MORI_HASHIRA4, policy); + ActorAccessibility_InitPolicy(&policy, "Forest Elevator", NA_SE_EV_ELEVATOR_MOVE2); + policy.n = 40; + policy.ydist = 1; + policy.distance = 300; + ActorAccessibility_AddSupportedActor(ACTOR_BG_MORI_ELEVATOR, policy); + ActorAccessibility_InitPolicy(&policy, "Phantom Ganon", [](AccessibleActor* actor) { + if (actor->actor->params == 1) { + actor->policy.aimAssist.isProvider = AIM_SHOOT | AIM_HOOK; + } else { + actor->policy.aimAssist.isProvider = 0; + } + }); + policy.distance = 2000; + policy.ydist = 2000; + ActorAccessibility_AddSupportedActor(ACTOR_BOSS_GANONDROF, policy); + ActorAccessibility_InitPolicy(&policy, "Phantom Ganon Ball", [](AccessibleActor* actor) { + if (actor->actor->params == 50) { // energy ball + int distance = actor->xyzDistToPlayer; + int freq = distance < 200 ? 1 : distance < 400 ? 3 : 7; + if ((actor->frameCount & freq) == 0) { + ActorAccessibility_PlaySoundForActor( + actor, 0, freq <= 3 ? NA_SE_IT_SWORD_REFLECT_MG : NA_SE_IT_SHIELD_REFLECT_MG); + } + } + }); + policy.volume = 2; + policy.distance = 900; + policy.ydist = 900; + ActorAccessibility_AddSupportedActor(ACTOR_EN_FHG_FIRE, 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", NA_SE_EV_TRAP_BOUND); + policy.n = 30; + policy.distance = 800; + policy.pitch = 1.1; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_OSHIHIKI, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT18_OBJ, policy); // Ideally should only play while adult + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT15_RRBOX, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_JYA_BLOCK, policy); + policy.distance = 1000; + policy.ydist = 500; + ActorAccessibility_AddSupportedActor(ACTOR_BG_HIDAN_ROCK, policy); + ActorAccessibility_InitPolicy(&policy, "Torch", [](AccessibleActor* actor) { + ObjSyokudai* torch = (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) { + actor->policy.volume = torch->litTimer != 0 ? 0.1 : 1.0; + if ((actor->frameCount & 31) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_BOMB_IGNIT); + } + return; + } else if ((actor->frameCount & 31) == 0) { + // unlit permanent torches + if ((actor->actor->params) == 8192) { + if (torch->litTimer == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_BOMB_IGNIT); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_ANUBIS_FIRE); + } + } + + // 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); + } + } + }); + policy.aimAssist.isProvider = AIM_HOOK; + policy.n = 1; + policy.pitch = 1.1; + policy.distance = 800; + ActorAccessibility_InitPolicy(&policy, "Po Torch", NA_SE_EN_ANUBIS_FIRE); + policy.n = 40; + policy.volume = 0.5; + policy.distance = 500; + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_SYOKUDAI, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_PO_SYOKUDAI, policy); + ActorAccessibility_InitPolicy(&policy, "Deku Tree Moving Platform", [](AccessibleActor* actor) { + if ((actor->actor->params) == 0) { + actor->policy.ydist = 1000; + actor->policy.distance = 1000; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_OCTAROCK_ROCK); + } else if ((actor->actor->params) == 1) { + actor->policy.ydist = 1000; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_DAMAGE); + } + }); + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_BG_YDAN_HASI, policy); + ActorAccessibility_InitPolicy(&policy, "Poe Object", [](AccessibleActor* actor) { + BgPoEvent* po = (BgPoEvent*)actor->actor; + if (po->type == 0) { + actor->policy.distance = 400; + if ((actor->frameCount & 31) == 0) { + ActorAccessibility_SetSoundPitch(actor, 0, 0.5f + 1.0f * po->index); + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_PO_APPEAR); + } + } else if (po->type == 1) { + actor->policy.distance = 300; + if ((actor->frameCount & 31) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_TRAP_BOUND); + } + } else if (po->index == sBgPoEventPuzzleState) { + actor->policy.aimAssist.isProvider = AIM_BOW; + if ((actor->frameCount & 63) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_PO_CRY); + } + } else if (po->type != 4) { + actor->policy.aimAssist.isProvider = 0; + } + }); + policy.aimAssist.isProvider = AIM_BOW; + policy.aimAssist.tolerance = 40.0f; + policy.n = 1; + policy.ydist = 1000; + policy.distance = 1500; + ActorAccessibility_AddSupportedActor(ACTOR_BG_PO_EVENT, policy); + ActorAccessibility_InitPolicy(&policy, "Poe Sister", [](AccessibleActor* actor) { + if (actor->actor->category == ACTORCAT_PROP) { + actor->policy.aimAssist.isProvider = 0; + } + }); + policy.aimAssist.isProvider = AIM_ALL; + policy.aimAssist.tolerance = 20.0f; + policy.n = 1; + ActorAccessibility_AddSupportedActor(ACTOR_EN_PO_SISTERS, policy); + ActorAccessibility_InitPolicy(&policy, "Lake Hylia Object", nullptr); + policy.aimAssist.isProvider = AIM_HOOK; + policy.n = 1; + policy.ydist = 600; + policy.distance = 600; + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT06_OBJECTS, policy); + ActorAccessibility_InitPolicy(&policy, "Pot", NA_SE_EV_POT_BROKEN); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_TSUBO, policy); + ActorAccessibility_InitPolicy(&policy, "Platform collapsible", NA_SE_EV_BLOCK_SHAKE); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_LIFT, policy); + ActorAccessibility_InitPolicy(&policy, "Ladder in Slingshot Room", [](AccessibleActor* actor) { + if (actor->actor->params == 1) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_LAND_LADDER); + } + }); + ActorAccessibility_AddSupportedActor(ACTOR_BG_YDAN_MARUTA, policy); + ActorAccessibility_InitPolicy(&policy, "231 dekus", [](AccessibleActor* actor) { + if (actor->actor->params == 1) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_NUTS_FAINT); + ActorAccessibility_SetSoundPitch(actor, 0, 1.0); + } else if (actor->actor->params == 2) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_NUTS_FAINT); + ActorAccessibility_SetSoundPitch(actor, 0, 0.5); + } else if (actor->actor->params == 3) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_NUTS_FAINT); + ActorAccessibility_SetSoundPitch(actor, 0, 1.5); + } + }); + 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, "Totem Pole", NA_SE_IT_HAMMER_HIT); + ActorAccessibility_AddSupportedActor(ACTOR_BG_HIDAN_DALM, policy); + policy.englishName = "Hammer Platform"; + ActorAccessibility_AddSupportedActor(ACTOR_BG_HIDAN_HROCK, policy); + + ActorAccessibility_InitPolicy(&policy, "Blue Fire", [](AccessibleActor* actor) { + if (actor->actor->params == -1) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_FLAME); + } + }); + policy.n = 40; + policy.distance = 900; + policy.volume = 2; + ActorAccessibility_AddSupportedActor(ACTOR_EN_ICE_HONO, policy); + ActorAccessibility_InitPolicy(&policy, "Barred", NA_SE_EV_CHAIN_KEY_UNLOCK); + ActorAccessibility_AddSupportedActor(ACTOR_BG_ICE_SHUTTER, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_MIZU_SHUTTER, policy); + ActorAccessibility_InitPolicy(&policy, "Ice Block", NA_SE_PL_SLIP_ICE_LELEL); + policy.distance = 750; + policy.volume = 2; + ActorAccessibility_AddSupportedActor(ACTOR_BG_GND_ICEBLOCK, policy); + ActorAccessibility_AddSupportedActor(ACTOR_BG_ICE_OBJECTS, policy); + ActorAccessibility_InitPolicy(&policy, "Iceberg", NA_SE_EV_ICE_FREEZE); + policy.distance = 1500; + ActorAccessibility_AddSupportedActor(ACTOR_BG_SPOT08_ICEBLOCK, policy); + policy.englishName = "Red Ice"; + policy.distance = 300; + ActorAccessibility_AddSupportedActor(ACTOR_BG_ICE_SHELTER, policy); + + ActorAccessibility_InitPolicy(&policy, "Statue Eye", [](AccessibleActor* actor) { + actor->policy.aimAssist.isProvider = + ABS((s16)(actor->actor->yawTowardsPlayer - actor->actor->shape.rot.y)) < 0x2000 ? AIM_BOW : 0; + }); + policy.n = 1; + policy.ydist = 500; + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_BG_MENKURI_EYE, policy); + + ActorAccessibility_InitPolicy(&policy, "uninteractable rocks in kokiri forest", [](AccessibleActor* actor) { + if (actor->actor->params == 1) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_OCTAROCK_ROCK); + } else if (actor->actor->params == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIG_UP); + } + }); + ActorAccessibility_AddSupportedActor(ACTOR_OBJ_HANA, policy); + ActorAccessibility_InitPolicy(&policy, "Keese", nullptr); + policy.ydist = 1000; + policy.distance = 1500; + policy.aimAssist.isProvider = AIM_SHOOT; + ActorAccessibility_AddSupportedActor(ACTOR_EN_FIREFLY, policy); + ActorAccessibility_InitPolicy(&policy, "gold skulltula token", NA_SE_EN_NUTS_DAMAGE); + policy.aimAssist.isProvider = AIM_BOOM | AIM_HOOK; + ActorAccessibility_AddSupportedActor(ACTOR_EN_SI, policy); + ActorAccessibility_InitPolicy(&policy, "Gold and Wall skulltulas", nullptr); + policy.aimAssist.isProvider = AIM_ALL | AIM_CUP; + policy.ydist = 600; + policy.distance = 700; + ActorAccessibility_AddSupportedActor(ACTOR_EN_SW, policy); + ActorAccessibility_InitPolicy(&policy, "Big Skulltula", [](AccessibleActor* actor) { + s16 angleTowardsLink = ABS((s16)(actor->actor->yawTowardsPlayer - actor->actor->shape.rot.y)); + if (angleTowardsLink >= 0x3FFC) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_VO_ST_DAMAGE); + } else if ((actor->frameCount & 63) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_STALTU_LAUGH); + } + }); + policy.ydist = 100; + ActorAccessibility_AddSupportedActor(ACTOR_EN_ST, policy); + ActorAccessibility_InitPolicy(&policy, "goma larva egg", [](AccessibleActor* actor) { + if (actor->actor->bgCheckFlags == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_GOMA_BJR_EGG1); + } + }); + policy.distance = 1000; + policy.ydist = 1000; + ActorAccessibility_AddSupportedActor(ACTOR_EN_GOMA, policy); + ActorAccessibility_InitPolicy(&policy, "jabu jabu", NA_SE_EV_JABJAB_HICCUP); + policy.distance = 10000; + ActorAccessibility_AddSupportedActor(ACTOR_EN_JJ, policy); + ActorAccessibility_InitPolicy(&policy, "small jellyfish", NA_SE_EN_BIRI_FLY); + ActorAccessibility_AddSupportedActor(ACTOR_EN_BILI, policy); + ActorAccessibility_InitPolicy(&policy, "stinger", [](AccessibleActor* actor) { + auto actionFunc = ((EnEiyer*)actor->actor)->actionFunc; + if (actionFunc == EnEiyer_Die || actionFunc == EnEiyer_Dead) { + ActorAccessibility_StopAllSoundsForActor(actor); + } else 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); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_IT_FISHING_REEL_HIGH); + } + }); + 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_AddSupportedActor(ACTOR_EN_ANUBICE, policy); + ActorAccessibility_InitPolicy(&policy, "tentacle obstacle", NA_SE_EN_BALINADE_THUNDER); + policy.distance = 100; + ActorAccessibility_AddSupportedActor(ACTOR_EN_BX, policy); + ActorAccessibility_InitPolicy(&policy, "tentacle", [](AccessibleActor* actor) { + if (actor->actor->params < EN_BA_DEAD_BLOB) { + actor->policy.volume = 2.5 - (actor->actor->world.pos.y - actor->actor->home.pos.y) / 200.0; + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_OWL_FLUTTER); + } + }); + policy.distance = 1500; + policy.ydist = 1000; + policy.n = 5; + ActorAccessibility_AddSupportedActor(ACTOR_EN_BA, policy); + ActorAccessibility_InitPolicy(&policy, "redead", NA_SE_EN_REDEAD_CRY); + ActorAccessibility_AddSupportedActor(ACTOR_EN_RD, policy); + ActorAccessibility_InitPolicy(&policy, "freezard", [](AccessibleActor* actor) { + EnFz* fz = (EnFz*)actor->actor; + if (fz->state != 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_FREEZAD_DEAD); + } + }); + policy.n = 20; + policy.aimAssist.isProvider = AIM_ALL; + ActorAccessibility_AddSupportedActor(ACTOR_EN_FZ, policy); + ActorAccessibility_InitPolicy(&policy, "Iron Knuckle", [](AccessibleActor* actor) { + EnIk* ik = (EnIk*)actor->actor; + if (ik->unk_2F8 == 3) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EN_GERUDOFT_BREATH); + } + }); + policy.distance = 400; + policy.n = 20; + ActorAccessibility_AddSupportedActor(ACTOR_EN_IK, policy); + ActorAccessibility_InitPolicy(&policy, "Beamos", NA_SE_EN_BIMOS_AIM); + ActorAccessibility_AddSupportedActor(ACTOR_EN_VM, policy); + ActorAccessibility_InitPolicy(&policy, "heart canister", NA_SE_EN_NUTS_DAMAGE); + ActorAccessibility_AddSupportedActor(ACTOR_ITEM_B_HEART, policy); + ActorAccessibility_InitPolicy(&policy, "Ocarina of Time", [](AccessibleActor* actor) { + if (actor->play->sceneNum == SCENE_ZORAS_DOMAIN || actor->play->sceneNum == SCENE_HYRULE_FIELD || + actor->play->sceneNum == SCENE_LAKE_HYLIA) { + int freq = actor->xzDistToPlayer < 10 ? 0 + : actor->xzDistToPlayer < 20 ? 1 + : actor->xzDistToPlayer < 40 ? 3 + : actor->xzDistToPlayer < 70 ? 7 + : actor->xzDistToPlayer < 110 ? 15 + : actor->xzDistToPlayer < 200 ? 31 + : 63; + if ((actor->frameCount & freq) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_BOMB_DROP_WATER); + } + } + }); + policy.n = 1; + policy.distance = 1500; + policy.ydist = 500; + ActorAccessibility_AddSupportedActor(ACTOR_EN_EX_RUPPY, policy); + ActorAccessibility_AddSupportedActor(ACTOR_ITEM_ETCETERA, policy); + ActorAccessibility_AddSupportedActor(ACTOR_ITEM_OCARINA, policy); + ActorAccessibility_InitPolicy(&policy, "Gohma", [](AccessibleActor* actor) { + BossGoma* goma = (BossGoma*)actor->actor; + if (goma->visualState == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_DIAMOND_SWITCH); + } + }); + + 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, "Armos Statue", NA_SE_EN_AMOS_WAVE); + policy.n = 30; + ActorAccessibility_AddSupportedActor(ACTOR_EN_AM, policy); + ActorAccessibility_InitPolicy(&policy, "Big Rupee", [](AccessibleActor* actor) { + if ((actor->frameCount & 31) == 0) { + EnGSwitch* gswitch = (EnGSwitch*)actor->actor; + if (gswitch->actionFunc == EnGSwitch_SilverRupeeIdle) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_FIVE_COUNT_LUPY); + } + } + }); + policy.aimAssist.isProvider = AIM_SHOOT | AIM_CUP; + 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, "Void", NA_SE_OC_ABYSS); + policy.distance = 500; + policy.ydist = 500; + ActorAccessibility_AddSupportedActor(VA_VOID, policy); + ActorAccessibility_AddSupportedActor(ACTOR_EN_STREAM, policy); + ActorAccessibility_InitPolicy(&policy, "Ladder/climable", [](AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + f32 waterLoc = player->actor.yDistToWater + player->actor.world.pos.y; + if (actor->pos.y < waterLoc) { + actor->pos.y = waterLoc; + } + if (actor->yDistToPlayer < 80) + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_LAND_LADDER); + }); + policy.pitch = 1.3; + ActorAccessibility_AddSupportedActor(VA_CLIMB, policy); + ActorAccessibility_InitPolicy(&policy, "Door", [](AccessibleActor* actor) { + if (((actor->actor->params >> 7) & 7) == DOOR_LOCKED && + !Flags_GetSwitch(actor->play, actor->actor->params & 0x3F)) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_CHAIN_KEY_UNLOCK_B); + } else { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_OC_DOOR_OPEN); + } + }); + policy.n = 30; + policy.pitch = 1.1; + policy.distance = 1000; + ActorAccessibility_AddSupportedActor(VA_DOOR, policy); + ActorAccessibility_AddSupportedActor(ACTOR_EN_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); + + // Virtual actors for a given location (scene and room number). + VirtualActorList* list; + AccessibleActor* temp; + + list = ActorAccessibility_GetVirtualActorList(SCENE_KOKIRI_FOREST, 0); + ActorAccessibility_AddVirtualActor(list, VA_CRAWLSPACE, { -784, 120, 1046 }); + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 2146, 1, -142.8 }); + + // Kokiri Forest Room with boulder and kokiri sword + list = ActorAccessibility_GetVirtualActorList(SCENE_KOKIRI_FOREST, 2); + ActorAccessibility_AddVirtualActor(list, VA_CRAWLSPACE, { -788, 120, 1392 }); + + list = ActorAccessibility_GetVirtualActorList(SCENE_DESERT_COLOSSUS, 0); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 2259, 108, -1550 }); + temp->policy.sound = NA_SE_EV_ROCK_BROKEN; + + list = ActorAccessibility_GetVirtualActorList(SCENE_LOST_WOODS, 1); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1348, 25, -25 }); + temp->policy.aimAssist.isProvider = AIM_SLING; + temp->policy.distance = 700; + temp->policy.n = 1; + + 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.5 }); + ActorAccessibility_AddVirtualActor(list, VA_MARKER, { -181.76, -905, -28.3 }); + + list = ActorAccessibility_GetVirtualActorList(SCENE_DODONGOS_CAVERN, 0); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { -80, 310, -1540 }); + temp->policy.distance = 150; + temp->policy.ydist = 100; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 80, 310, -1540 }); + temp->policy.distance = 150; + temp->policy.ydist = 100; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { -80, 510, -1540 }); + temp->policy.distance = 150; + temp->policy.ydist = 100; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 80, 510, -1540 }); + temp->policy.distance = 150; + temp->policy.ydist = 100; + + 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); + 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; + + list = ActorAccessibility_GetVirtualActorList(SCENE_TEMPLE_OF_TIME, 0); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 0, 0, 0 }); + temp->policy.englishName = "Master Sword Pedestal"; + temp->policy.distance = 1500; + temp->policy.sound = NA_SE_PL_SWORD_CHARGE; + + list = ActorAccessibility_GetVirtualActorList(SCENE_WINDMILL_AND_DAMPES_GRAVE, 0); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { -50, -530, -2300 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 25, -530, -2900 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 300, -530, -3020 }); + temp->policy.distance = 500; + + list = ActorAccessibility_GetVirtualActorList(SCENE_WINDMILL_AND_DAMPES_GRAVE, 1); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 370, -500, -3430 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 410, -530, -3770 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 675, -570, -3930 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 675, -610, -4300 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 560, -600, -4500 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 470, -570, -4775 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 300, -570, -4910 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 230, -570, -5300 }); + temp->policy.distance = 500; + + list = ActorAccessibility_GetVirtualActorList(SCENE_WINDMILL_AND_DAMPES_GRAVE, 2); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 300, -570, -5400 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 500, -570, -5400 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 650, -570, -5275 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1200, -730, -5125 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1345, -730, -4930 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1560, -730, -4765 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1730, -730, -4550 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1940, -730, -4430 }); + temp->policy.distance = 500; + + list = ActorAccessibility_GetVirtualActorList(SCENE_WINDMILL_AND_DAMPES_GRAVE, 3); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1990, -730, -4185 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1800, -730, -3950 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1720, -730, -3850 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1690, -730, -3145 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1655, -668, -3035 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1710, -668, -2660 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 2285, -610, -2650 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 2625, -610, -2700 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 3080, -530, -2700 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 3230, -470, -2515 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 3170, -420, -2300 }); + temp->policy.distance = 500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 2960, -410, -2000 }); + temp->policy.distance = 500; + + auto forest_basement = [](AccessibleActor* actor) { + Actor* walls = Actor_Find(&actor->play->actorCtx, ACTOR_BG_MORI_KAITENKABE, ACTORCAT_BG); + if (walls != nullptr) { + actor->pos.x = + walls->world.pos.x + Math_CosS(-walls->world.rot.y) * (actor->policy.sound == 0 ? -300.0f : 300.0f); + actor->pos.z = + walls->world.pos.z + Math_SinS(-walls->world.rot.y) * (actor->policy.sound == 0 ? -300.0f : 300.0f); + if ((actor->frameCount & 31) == 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_EV_TRAP_BOUND); + } + } + }; + list = ActorAccessibility_GetVirtualActorList(SCENE_FOREST_TEMPLE, 17); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 119, -779, -1566 }); + temp->policy.callback = forest_basement; + temp->policy.sound = 0; + temp->policy.distance = 333; + temp->policy.n = 1; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 119, -779, -1566 }); + temp->policy.callback = forest_basement; + temp->policy.sound = 1; + temp->policy.distance = 333; + temp->policy.n = 1; + + auto forest_twisted_hallway = [](AccessibleActor* actor) { + Actor* twisted = Actor_Find(&actor->play->actorCtx, ACTOR_BG_MORI_HINERI, ACTORCAT_BG); + if (twisted != nullptr) { + switch (twisted->params) { + case 0: // boss key on wall + if (actor->pos.x != -1835) + return; + break; + case 1: // boss key on floor + if (actor->pos.x != -1760) + return; + break; + case 2: // to ice eye + if (actor->pos.x != 1877) + return; + break; + } + ActorAccessibility_PlaySoundForActor(actor, 0, NA_SE_PL_LAND_LADDER); + } + }; + list = ActorAccessibility_GetVirtualActorList(SCENE_FOREST_TEMPLE, 19); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { -1835, 1033, -3320 }); + temp->policy.callback = forest_twisted_hallway; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { -1760, 1033, -3200 }); + temp->policy.callback = forest_twisted_hallway; + + list = ActorAccessibility_GetVirtualActorList(SCENE_FOREST_TEMPLE, 20); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1877, 1033, -3320 }); + temp->policy.callback = forest_twisted_hallway; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 2000, 1033, -3200 }); + temp->policy.sound = NA_SE_PL_LAND_LADDER; + + list = ActorAccessibility_GetVirtualActorList(SCENE_FOREST_TEMPLE, 15); + // falling ceiling safe spots + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 2070, -403, -3000 }); + temp->policy.volume = 0.5; + temp->policy.distance = 1500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 2150, -403, -2560 }); + temp->policy.volume = 0.5; + temp->policy.distance = 1500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 2070, -403, -3000 }); + temp->policy.volume = 0.5; + temp->policy.distance = 1500; + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 1990, -403, -1850 }); + temp->policy.volume = 0.5; + temp->policy.distance = 1500; + + list = ActorAccessibility_GetVirtualActorList(SCENE_FIRE_TEMPLE, 10); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { -2350, 2840, 475 }); + list = ActorAccessibility_GetVirtualActorList(SCENE_FIRE_TEMPLE, 16); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 475, 2840, -30 }); + + list = ActorAccessibility_GetVirtualActorList(SCENE_ICE_CAVERN, 9); + temp = ActorAccessibility_AddVirtualActor(list, VA_MARKER, { 860, 200, -2400 }); + temp->policy.aimAssist.isProvider = AIM_CUP; + temp->policy.distance = 750; + temp->policy.ydist = 100; + temp->policy.volume = 1.5; + temp->policy.sound = NA_SE_EV_BLOCK_SHAKE; + + ActorAccessibility_InitPolicy(&policy, "Terrain cue helper", nullptr); + policy.n = 1; + policy.runsAlways = true; + policy.distance = 500; + ActorAccessibility_AddSupportedActor(VA_TERRAIN_CUE, policy); + + AccessibleActor* actor = new AccessibleActor; + actor->actor = nullptr; + actor->basePitch = 1.0; + actor->baseVolume = 1.0; + actor->currentPitch = 1.0; + actor->currentVolume = 1.0; + actor->frameCount = 0; + actor->id = VA_TERRAIN_CUE; + actor->instanceID = ActorAccessibility_GetNextID(); + actor->isDrawn = 1; + actor->play = nullptr; + actor->pos = { 0, 0, 0 }; + actor->sceneIndex = 0; + actor->managedSoundSlots = 0; + actor->aimFramesSinceAimAssist = 0; + actor->aimFrequency = 10; + actor->policy = policy; + ActorAccessibility_AddTerrainCues(actor); +} diff --git a/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.cpp b/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.cpp new file mode 100644 index 000000000..36425fc01 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.cpp @@ -0,0 +1,614 @@ +#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 + +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 pan calculation from game itself. Todo: this is technical debt, so copy/revise or something +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, 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 >= 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) { + return; + } + + if (action.handle != 0) { + initSoundExtras(sound); + // 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); + } else { + sound->extras.base.pNodeGraph = NULL; + ma_node_attach_output_bus(&sound->sound, 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();) { + bool deadSlots = true; + for (int x = 0; x < AAE_SLOTS_PER_HANDLE; x++) { + if (i->second[x].active) { + if (!ma_sound_is_playing(&i->second[x].sound)) { + destroySound(&i->second[x]); + } else { + deadSlots = false; + } + } + } + if (deadSlots) { + 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, 0); + return true; +} + +void AccessibleAudioEngine::destroySound(SoundSlot* slot) { + if (slot->extras.base.pNodeGraph != NULL) { + ma_node_detach_all_output_buses(&slot->extras); + ma_sound_uninit(&slot->sound); + ma_gainer_uninit(&slot->extras.gainer, NULL); + } else { + ma_sound_uninit(&slot->sound); + } + + 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 (uint32_t i = 0; i < nextChunk * AAE_CHANNELS; i++) + scalar = std::max(scalar, mixedChunk[i]); + if (scalar > 1.0) { + scalar = 1.0 / scalar; + for (uint32_t 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, uint8_t slot, const char* path) { + if (slot >= AAE_SLOTS_PER_HANDLE) + return; + SoundAction& action = getNextOutgoingSoundAction(); + action.handle = handle; + action.slot = slot; + action.command = AAE_START; + action.path = path; +} + +void AccessibleAudioEngine::stopSound(uintptr_t handle, uint8_t slot) { + if (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, uint8_t slot, float pitch) { + if (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, uint8_t slot, float mod) { + if (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, uint8_t slot, float volume) { + if (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, uint8_t slot, float pan) { + if (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, uint8_t slot, float cutoff) { + if (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, uint8_t slot, size_t offset) { + if (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, uint8_t slot, float posX, float posY, float posZ, + float distToPlayer, float maxDistance) { + if (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..6bb473ddb --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/AccessibleAudioEngine.h @@ -0,0 +1,147 @@ +#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 8 + +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. + uint8_t slot; // Allows multiple sounds per handle. The exact number is controlled by AAE_SOUNDS_PER_HANDLE. + uint8_t 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. + 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, uint8_t slot, const char* path); + void stopSound(uintptr_t handle, uint8_t slot); + // Stop all sounds belonging to a handle. + void stopAllSounds(uintptr_t handle); + + void setPitch(uintptr_t handle, uint8_t slot, float pitch); + void setPitchBehindModifier(uintptr_t handle, uint8_t slot, float mod); + void setVolume(uintptr_t handle, uint8_t slot, float volume); + void setPan(uintptr_t handle, uint8_t slot, float pan); + // Set the lowpass filter cutoff. Set to 1.0 for no filtering. + void setFilter(uintptr_t handle, uint8_t slot, float cutoff); + // Seek the sound to a particular PCM frame. + void seekSound(uintptr_t handle, uint8_t slot, size_t offset); + void setSoundPosition(uintptr_t handle, uint8_t 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..67348a8d8 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/ActorAccessibility.cpp @@ -0,0 +1,856 @@ +#include +#include +#include +#include + +#include "ActorAccessibility.h" +#include "AccessibleAudioEngine.h" +#include "soh/OTRGlobals.h" + +#include +#include +#include +#include "SfxExtractor.h" + +#include "File.h" +#include "soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/ObjectExtension/ObjectExtension.h" + +extern "C" { +#include "overlays/actors/ovl_Shot_Sun/z_shot_sun.h" +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 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; + +struct SfxRecord { + std::string path; + std::shared_ptr resource; +}; + +struct A11yID { + uint64_t id; +}; + +class AudioGlossaryData { + public: + AccessibleActorList_t accessibleActorList; + AccessibleActorList_t::iterator current = accessibleActorList.begin(); + bool GlossaryStarted = false; + u16 frameCount = 0; + s16 currentScene = -1; + s8 currentRoom = -1; + s8 cooldown = 0; +}; + +class ActorAccessibility { + public: + bool isOn = false; + uint64_t nextActorID = 0; + SupportedActors_t supportedActors; + 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; + s16 currentScene = -1; + s8 currentRoom = -1; + bool currentRoomClear = false; + u8 framesUntilChime = 0; + Vec3f prevPos = { 0, 0, 0 }; + s16 prevYaw = 0; + bool extractSfx = false; + TerrainCueState* terrainCues = nullptr; + VirtualActorList* currentSceneGlobal = nullptr; + VirtualActorList* currentRoomLocal = nullptr; +}; +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 = !std::filesystem::exists(Ship::Context::GetPathRelativeToAppBundle("accessibility.o2r")); + 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) { + policy->distance = 500; + policy->ydist = 80; + policy->englishName = englishName; + policy->n = 20; + policy->pitch = 1.5; + policy->runsAlways = false; + policy->volume = 1.0; + policy->pitchModifier = 0.1; + policy->aimAssist.isProvider = 0; + policy->aimAssist.sfx = NA_SE_SY_HITPOINT_ALARM; + policy->aimAssist.tolerance = 0.0; +} + +void ActorAccessibility_InitPolicy(ActorAccessibilityPolicy* policy, const char* englishName, + ActorAccessibilityCallback callback) { + policy->callback = callback; + policy->sound = 0; + ActorAccessibility_InitPolicy(policy, englishName); +} + +void ActorAccessibility_InitPolicy(ActorAccessibilityPolicy* policy, const char* englishName, s16 sfx) { + policy->callback = nullptr; + policy->sound = sfx; + ActorAccessibility_InitPolicy(policy, englishName); +} + +void ActorAccessibility_AddSupportedActor(s16 type, ActorAccessibilityPolicy policy) { + aa->supportedActors[type] = policy; +} + +void ActorAccessibility_AddTerrainCues(AccessibleActor* actor) { + aa->terrainCues = InitTerrainCueState(actor); +} + +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) { + return min + Rand_ZeroOne() * (max - min); +} + +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.aimFramesSinceAimAssist = 255; + accessibleActor.aimFrequency = 10; + + ObjectExtension::GetInstance().Set(actor, A11yID{ .id = accessibleActor.instanceID }); + aa->accessibleActorList[accessibleActor.instanceID] = accessibleActor; +} + +void ActorAccessibility_RemoveTrackedActor(Actor* actor) { + const auto id = ObjectExtension::GetInstance().Get(actor); + if (id == nullptr) + return; + AccessibleActorList_t::iterator i2 = aa->accessibleActorList.find(id->id); + if (i2 == aa->accessibleActorList.end()) + return; + 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) { + const char* path = ActorAccessibility_MapSfxToExternalAudio(sfxId); + if (path == NULL) + return; + aa->audioEngine->playSound((uintptr_t)handle, slot, path); +} + +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, float pan) { + 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) { + if (slot < 0 || slot >= AAE_SLOTS_PER_HANDLE) + return; + ActorAccessibility_PlaySound(actor, slot, sfxId); + 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; +} + +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->pos = actor->actor->world.pos; + actor->xyzDistToPlayer = sqrtf(actor->actor->xyzDistToPlayerSq); +} + +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 (actor->actor != nullptr) { + ActorAccessibility_CopyParamsFromRealActor(actor); + } else { + Player* player = GET_PLAYER(play); + f32 w = 0.0f; + // Set actor->projectedPos. + SkinMatrix_Vec3fMtxFMultXYZW(&play->viewProjectionMtxF, &actor->pos, &actor->projectedPos, &w); + actor->xzDistToPlayer = Math_Vec3f_DistXZ(&actor->pos, &player->actor.world.pos); + actor->xyzDistToPlayer = Math_Vec3f_DistXYZ(&actor->pos, &player->actor.world.pos); + actor->yDistToPlayer = fabs((actor->pos.y) - (player->actor.world.pos.y)); + } + + if (actor->policy.aimAssist.isProvider) { + Player* player = GET_PLAYER(play); + if ((player->stateFlags1 & PLAYER_STATE1_FIRST_PERSON) && + ((actor->policy.aimAssist.isProvider & AIM_CUP) || + (player->stateFlags1 & (PLAYER_STATE1_USING_BOOMERANG | PLAYER_STATE1_ITEM_IN_HAND)))) { + bool aim = false; + f32 dist = 1000; + if (player->unk_6AD == 2) { + switch (player->heldItemAction) { + case PLAYER_IA_BOW: + case PLAYER_IA_BOW_FIRE: + case PLAYER_IA_BOW_ICE: + case PLAYER_IA_BOW_LIGHT: + case PLAYER_IA_BOW_0C: + case PLAYER_IA_BOW_0D: + case PLAYER_IA_BOW_0E: + aim = actor->policy.aimAssist.isProvider & AIM_BOW; + break; + case PLAYER_IA_SLINGSHOT: + aim = actor->policy.aimAssist.isProvider & AIM_SLING; + break; + case PLAYER_IA_HOOKSHOT: + dist = 380; + aim = actor->policy.aimAssist.isProvider & AIM_HOOK; + break; + case PLAYER_IA_LONGSHOT: + dist = 770; + aim = actor->policy.aimAssist.isProvider & AIM_HOOK; + break; + case PLAYER_IA_BOOMERANG: + dist = 380; + aim = actor->policy.aimAssist.isProvider & AIM_BOOM; + break; + case PLAYER_IA_NONE: + aim = actor->policy.aimAssist.isProvider & AIM_CUP; + break; + } + } else { + aim = actor->policy.aimAssist.isProvider & AIM_CUP; + } + if (aim && actor->xyzDistToPlayer < dist) { + auto aimAssistProps = ActorAccessibility_ProvideAimAssistForActor(actor); + if (++actor->aimFramesSinceAimAssist >= actor->aimFrequency) { + actor->aimFramesSinceAimAssist = 0; + ActorAccessibility_PlaySoundForActor(actor, 7, actor->policy.aimAssist.sfx); + } + ActorAccessibility_SetSoundPitch(actor, 7, aimAssistProps.pitch); + ActorAccessibility_SetSoundVolume(actor, 7, aimAssistProps.volume); + ActorAccessibility_SetSoundPan(actor, 7, aimAssistProps.pan); + } + } else { + // Make sure there's no delay the next time you draw your bow or whatever. + actor->aimFramesSinceAimAssist = 255; + } + } + + actor->frameCount++; + if (aa->glossary->GlossaryStarted) { + aa->glossary->frameCount++; + } + + if (actor->actor != NULL && fabs(actor->actor->yDistToPlayer) > actor->policy.ydist) { + return; + } else if (!actor->policy.runsAlways && actor->xyzDistToPlayer > actor->policy.distance) { + return; + } else if (actor->isDrawn == 0 && actor->id != ACTOR_EN_HOLL && actor->id != ACTOR_EN_KAKASI2 && + actor->id != ACTOR_EN_IT && actor->id != ACTOR_EN_OKARINA_TAG && actor->id != ACTOR_EN_WONDER_ITEM && + actor->id != ACTOR_SHOT_SUN && !aa->glossary->GlossaryStarted) { + return; + } + + for (int i = 0; i < AAE_SLOTS_PER_HANDLE; i++) { + if (actor->managedSoundSlots & (1 << i)) { + ActorAccessibility_SetSoundPos(actor, i, &actor->projectedPos, i == 7 ? 0 : actor->xyzDistToPlayer, + i == 7 ? 1 : 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. + } + } + + if (actor->frameCount % actor->policy.n == 0) { + if (actor->policy.callback != nullptr) { + actor->policy.callback(actor); + } else if (actor->policy.sound != 0) { + ActorAccessibility_PlaySoundForActor(actor, 0, actor->policy.sound); + } + } +} + +void ActorAccessibility_RunAccessibilityForAllActors(PlayState* play) { + Player* player = GET_PLAYER(play); + + if (aa->currentScene != play->sceneNum) { + if (aa->terrainCues) + ActorAccessibility_StopAllSounds(aa->terrainCues); + ActorAccessibility_StopAllVirtualActors(aa->currentSceneGlobal); + ActorAccessibility_StopAllVirtualActors(aa->currentRoomLocal); + ActorAccessibility_InterpretCurrentScene(play); + aa->currentSceneGlobal = ActorAccessibility_GetVirtualActorList(play->sceneNum, -1); + aa->currentRoomLocal = ActorAccessibility_GetVirtualActorList(play->sceneNum, play->roomCtx.curRoom.num); + + aa->currentScene = play->sceneNum; + aa->currentRoom = play->roomCtx.curRoom.num; + aa->currentRoomClear = Flags_GetClear(play, aa->currentRoom); + } else if (aa->currentRoom != play->roomCtx.curRoom.num) { + ActorAccessibility_StopAllVirtualActors(aa->currentRoomLocal); + ActorAccessibility_AnnounceRoomNumber(play); + aa->currentRoomLocal = ActorAccessibility_GetVirtualActorList(play->sceneNum, play->roomCtx.curRoom.num); + aa->currentRoom = play->roomCtx.curRoom.num; + aa->currentRoomClear = Flags_GetClear(play, aa->currentRoom); + } + + 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; + } + + ActorAccessibility_GeneralHelper(play); + + // Real actors. + for (AccessibleActorList_t::iterator i = aa->accessibleActorList.begin(); i != aa->accessibleActorList.end(); i++) + ActorAccessibility_RunAccessibilityForActor(play, &i->second); + + if (aa->terrainCues) { + RunTerrainCueState(aa->terrainCues, play); + } + + // Virtual actors for the current room and scene. + VAList_t* 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_GeneralHelper(PlayState* play) { + Player* player = GET_PLAYER(play); + if (player == nullptr) + return; + + // Report when a room is completed. + if (!aa->currentRoomClear && Flags_GetClear(play, aa->currentRoom)) { + aa->currentRoomClear = Flags_GetClear(play, aa->currentRoom); + ActorAccessibility_AnnounceRoomNumber(play); + } + + if (player->actor.wallPoly && player->actor.speedXZ > 0 && + (player->yDistToLedge == 0 || player->yDistToLedge >= 79.0f)) { + f32 movedsq = SQ(aa->prevPos.x - player->actor.world.pos.x) + SQ(aa->prevPos.z - player->actor.world.pos.z); + if (movedsq < 0.125) { + ActorAccessibility_PlaySound(nullptr, 3, NA_SE_IT_WALL_HIT_SOFT); + ActorAccessibility_SetSoundVolume(nullptr, 3, 0.5); + } else if (movedsq < 9) { + ActorAccessibility_PlaySound(nullptr, 3, NA_SE_IT_SHIELD_POSTURE); + ActorAccessibility_SetSoundVolume(nullptr, 3, 0.6); + } else { + ActorAccessibility_PlaySound(nullptr, 3, NA_SE_PL_WALK_WALL); + ActorAccessibility_SetSoundVolume(nullptr, 3, std::max(0.3f, 10.0f / movedsq)); + } + } + + bool compassOn = false; + if (aa->prevPos.x == player->actor.world.pos.x && aa->prevPos.z == player->actor.world.pos.z) { + if (ABS(aa->prevYaw - player->yaw) > 0x400) { + compassOn = true; + aa->prevYaw = player->yaw; + } + } else { + aa->prevPos = player->actor.world.pos; + } + + if (aa->framesUntilChime > 0) { + aa->framesUntilChime--; + } else { + if (!compassOn) { + OSContPad* trackerButtonsPressed = + std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetControlDeck())->GetPads(); + compassOn = trackerButtonsPressed != nullptr && (trackerButtonsPressed[0].button & BTN_DDOWN) && + (trackerButtonsPressed[0].button & BTN_L); + } + if (compassOn) { + ActorAccessibility_PlaySound(nullptr, 0, NA_SE_EV_SHIP_BELL); + ActorAccessibility_SetSoundPitch(nullptr, 0, 1.5f + Math_CosS(player->yaw) / 2); + ActorAccessibility_SetSoundPan(nullptr, 0, -Math_SinS(player->yaw)); + s16 range = ABS(((player->yaw + 0xA000) & 0x3FFF) - 0x2000); + aa->framesUntilChime = range <= 0x400 ? 10 : range <= 0x1000 ? 20 : 30; + } + } + + if (fabs(player->unk_860 - 25) < 24.0 && player->heldItemId == 0) { + ActorAccessibility_PlaySound(nullptr, 1, NA_SE_SY_WARNING_COUNT_N); + } + + if (Player_HoldsHookshot(player) && player->heldActor != NULL && player->actor.scale.y >= 0.0f && + (player->stateFlags1 & PLAYER_STATE1_FIRST_PERSON)) { + CollisionPoly* colPoly; + s32 bgId; + Vec3f firstHit; + f32 hookshotLength = ((player->heldItemAction == PLAYER_IA_HOOKSHOT) ? 380.0f : 770.0f) * + CVarGetFloat(CVAR_CHEAT("HookshotReachMultiplier"), 1.0f); + Vec3f hookshotEnd = player->heldActor->world.pos; + hookshotEnd.x += + Math_SinS(player->heldActor->world.rot.y) * Math_SinS(-player->heldActor->world.rot.x) * hookshotLength; + hookshotEnd.y += Math_SinS(-player->heldActor->world.rot.x) * hookshotLength; + hookshotEnd.z += + Math_CosS(player->heldActor->world.rot.y) * Math_CosS(-player->heldActor->world.rot.x) * hookshotLength; + if (BgCheck_AnyLineTest3(&play->colCtx, &player->heldActor->world.pos, &hookshotEnd, &firstHit, &colPoly, 1, 1, + 1, 1, &bgId)) { + if (SurfaceType_IsHookshotSurface(&play->colCtx, colPoly, bgId)) { + ActorAccessibility_PlaySound(nullptr, 2, NA_SE_IT_HOOKSHOT_STICK_OBJ); + ActorAccessibility_SetSoundVolume(nullptr, 2, 0.5f); + } + } + } +} + +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; + + return (VirtualActorList*)&aa->vaZones[sr.raw]; +} + +AccessibleActor* ActorAccessibility_AddVirtualActor(VirtualActorList* list, VIRTUAL_ACTOR_TABLE type, Vec3f where) { + ActorAccessibilityPolicy* policy = ActorAccessibility_GetPolicyForActor(type); + AccessibleActor actor; + actor.actor = nullptr; + 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 = nullptr; + actor.pos = where; + actor.sceneIndex = 0; + actor.managedSoundSlots = 0; + actor.aimFramesSinceAimAssist = 0; + actor.aimFrequency = 10; + actor.policy = *policy; + + VAList_t* l = (VAList_t*)list; + l->push_back(actor); + return &(*l)[l->size() - 1]; +} + +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); + } else if (SurfaceType_GetSceneExitIndex(&play->colCtx, poly, BGCHECK_SCENE) != 0) { + ActorAccessibility_PolyToVirtualActor(play, poly, VA_AREA_CHANGE, list); + } else if (func_80041EA4(&play->colCtx, poly, BGCHECK_SCENE) == 12) { + ActorAccessibility_PolyToVirtualActor(play, poly, VA_VOID, list); + } else if (SurfaceType_IsHookshotSurface(&play->colCtx, poly, BGCHECK_SCENE)) { + ActorAccessibility_PolyToVirtualActor(play, poly, VA_MARKER, 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); + 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)); + Vec3f where = { + .x = maxX - (maxX - minX) / 2, + .y = std::min(polyVerts[0].y, std::min(polyVerts[1].y, polyVerts[2].y)), + .z = maxZ - (maxZ - minZ) / 2, + }; + AccessibleActor* actor = ActorAccessibility_AddVirtualActor(destination, va, where); + if (actor == NULL) + return; + if (va == VA_MARKER) { + actor->policy.sound = 0; + } + + if (va == VA_AREA_CHANGE) { + if (play->sceneNum != SCENE_GROTTOS && play->sceneNum != SCENE_FAIRYS_FOUNTAIN) { + u32 sceneIndex = SurfaceType_GetSceneExitIndex(&play->colCtx, poly, BGCHECK_SCENE); + s16 nextEntranceIndex = play->setupExitList[sceneIndex - 1]; + actor->sceneIndex = gEntranceTable[nextEntranceIndex].scene; + } + } else if (SurfaceType_IsHookshotSurface(&play->colCtx, poly, BGCHECK_SCENE)) { + if (va == VA_MARKER) { + f32 maxY = std::max(polyVerts[0].y, std::max(polyVerts[1].y, polyVerts[2].y)); + actor->pos.y = (actor->pos.y + maxY) / 2; + } + actor->policy.aimAssist.isProvider = AIM_HOOK; + } +} + +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; + SpeechSynthesizer::Instance->Speak(ss.str().c_str(), GetLanguageCode()); +} + +AimAssistProps ActorAccessibility_ProvideAimAssistForActor(AccessibleActor* actor) { + Player* player = GET_PLAYER(actor->play); + s32 angle = player->actor.focus.rot.x; + angle = angle / -14000.0 * 16384; + f32 cos_angle = Math_CosS(angle); + f32 slope = cos_angle == 0.0f ? 0.0f : Math_SinS(angle) / cos_angle; + f32 x = actor->pos.x, z = actor->pos.z, xzDist = actor->xzDistToPlayer; + s32 yHeight = actor->pos.y + 25; + if (actor->id == ACTOR_BG_MIZU_MOVEBG) { + x += Math_SinS(actor->actor->shape.rot.y) * 50; + z += Math_CosS(actor->actor->shape.rot.y) * 50; + xzDist = sqrtf(SQ(player->actor.world.pos.x - x) + SQ(player->actor.world.pos.z - z)); + } else if (actor->id == ACTOR_SHOT_SUN) { + ShotSun* sun = (ShotSun*)actor->actor; + x = sun->hitboxPos.x; + z = sun->hitboxPos.z; + yHeight = sun->hitboxPos.y + 55; + xzDist = sqrtf(SQ(player->actor.world.pos.x - x) + SQ(player->actor.world.pos.z - z)); + } + s32 yIntercept = slope * xzDist + player->actor.focus.pos.y; + AimAssistProps aimAssistProps; + if (yIntercept > yHeight + 25) { + aimAssistProps.pitch = 1.5; + } else if (yIntercept < yHeight - 25) { + aimAssistProps.pitch = 0.5; + } else { + aimAssistProps.pitch = 1.0; + } + s32 yDiff = fabs(yIntercept - yHeight); + if (yIntercept - yHeight > 0) { + s32 correction = 100.0f - 100.0f / std::max(slope, 1.0f); + yDiff = std::max(yDiff - correction, 0); + } + if (yDiff > 300) { + actor->aimFrequency = 30; + } else { + actor->aimFrequency = 1 + (uint8_t)(yDiff / 5); + } + s16 yawdiff = player->yaw - Math_Atan2S(z - player->actor.world.pos.z, x - player->actor.world.pos.x); + if (yawdiff > -0x1000 && yawdiff < 0x1000) { + aimAssistProps.volume = 1.0 - (yawdiff * yawdiff) / (float)0x2000000; + } else if (yawdiff > -0x2000 && yawdiff < 0x2000) { + aimAssistProps.volume = 0.4; + } else { + aimAssistProps.volume = 0.2; + } + aimAssistProps.pan = std::min(std::max(yawdiff / (float)0x1000, -1.0f), 1.0f); + return aimAssistProps; +} + +// 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; + if (aa->terrainCues) { + DeleteTerrainCueState(aa->terrainCues); + } + aa->isOn = false; + } +} + +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(); +} + +static ObjectExtension::Register RegisterA11yID; diff --git a/soh/soh/Enhancements/accessible-actors/ActorAccessibility.h b/soh/soh/Enhancements/accessible-actors/ActorAccessibility.h new file mode 100644 index 000000000..6d65e3d5c --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/ActorAccessibility.h @@ -0,0 +1,181 @@ +#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*); + +struct VirtualActorList; + +#define AIM_ALL 0x0F +#define AIM_BOW 0x01 +#define AIM_SLING 0x02 +#define AIM_SHOOT 0x03 +#define AIM_HOOK 0x04 +#define AIM_BOOM 0x08 +#define AIM_CUP 0x10 + +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. + bool runsAlways; // If set, then the distance policy is ignored. + + u8 n; // How often to run callback in frames. + f32 distance; // Maximum xz distance from player before actor should be considered out of range. + f32 ydist; // Maximum y distance from player before actor should be considered out of range. + f32 pitch; + f32 volume; + f32 pitchModifier; + // Aim assist settings. + struct { + u8 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 center of actor does Link have to aim 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; // null for virtual actors + s16 id; // For real actors, copy actor ID. For virtual actors we have our own table of values which + // are out of range for real actors. + f32 yDistToPlayer; + f32 xzDistToPlayer; + f32 xyzDistToPlayer; + Vec3f pos; + Vec3f projectedPos; + PlayState* play; + u8 isDrawn; // Do we just never play accessibility sounds for actors that aren't drawn? + + u16 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. + u8 managedSoundSlots; // These have their attenuation and panning parameters updated every frame automatically. + u8 aimFramesSinceAimAssist; // Used for rate-based vertical aim assist. + u8 aimFrequency; // How often the sound will be played. Lower frequencies indicate vertical aim is getting closer. + + ActorAccessibilityPolicy policy; // A copy, so it can be customized on a per-actor basis if needed. +}; + +struct AimAssistProps { + f32 pitch; + f32 volume; + f32 pan; +}; + +struct TerrainCueState; +void DeleteTerrainCueState(TerrainCueState*); +TerrainCueState* InitTerrainCueState(AccessibleActor*); +void RunTerrainCueState(TerrainCueState*, PlayState*); + +// Initialize accessibility. +void ActorAccessibility_Init(); +void ActorAccessibility_InitActors(); +void ActorAccessibility_Shutdown(); +void ActorAccessibility_InitPolicy(ActorAccessibilityPolicy* policy, const char* englishName); +void ActorAccessibility_InitPolicy(ActorAccessibilityPolicy* policy, const char* englishName, + ActorAccessibilityCallback callback); +void ActorAccessibility_InitPolicy(ActorAccessibilityPolicy* policy, const char* englishName, s16 sfx); + +uint64_t ActorAccessibility_GetNextID(); +void ActorAccessibility_TrackNewActor(Actor* actor); +void ActorAccessibility_RemoveTrackedActor(Actor* actor); +void ActorAccessibility_AddSupportedActor(s16 type, ActorAccessibilityPolicy policy); +void ActorAccessibility_AddTerrainCues(AccessibleActor* actor); + +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); +// 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, float pan); +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); + +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); +// Stuff related to lists of virtual actors. +typedef enum { + // Similar to the game's actual actor table + VA_INITIAL = 1000, + VA_CRAWLSPACE, + VA_TERRAIN_CUE, + VA_WALL_CUE, + VA_VOID, + VA_CLIMB, + VA_DOOR, + VA_AREA_CHANGE, + VA_MARKER, + VA_MAX, +} VIRTUAL_ACTOR_TABLE; + +// 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, Vec3f 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. +AimAssistProps 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_GeneralHelper(PlayState* play); +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..ab57e1003 --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/SfxExtractor.cpp @@ -0,0 +1,218 @@ +#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::isAllSilence(int16_t* buffer, size_t count) { + for (size_t i = 0; i < count; i++) { + if (!isSilentSample(buffer[i])) // Tolerance for low-amplitude dither noise. + return false; + } + return true; +} +bool SfxExtractor::isSilentSample(int16_t sample) { + return abs(sample) <= SFX_EXTRACTION_SILENCE_THRESHOLD; +} + +// Find the beginning of a captured signal. +size_t SfxExtractor::adjustedStartOfInput() { + size_t startOfInput = 0; + while (startOfInput + 2 < SFX_EXTRACTION_BUFFER_SIZE * 2 && isSilentSample(tempBuffer[startOfInput]) && + isSilentSample(tempBuffer[startOfInput + 1])) { + startOfInput += 2; + } + return startOfInput; +} + +size_t SfxExtractor::adjustedEndOfInput(size_t endOfInput) { + while (endOfInput > 0 && (!isSilentSample(tempBuffer[endOfInput]) || isSilentSample(tempBuffer[endOfInput - 1]))) { + 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; + currentSfx = -1; + currentStep = STEP_MAIN; + archive = std::make_shared(sohAccessibilityPath); + archive->Open(); + } catch (...) { currentStep = STEP_ERROR; } +} + +void SfxExtractor::ripNextSfx() { + // This entire method is expected to be atomic; Don't try to narrow the scope of this lock please! + // Todo: remove the thread altogether as we don't actually need or want parallelism here. + 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); + captureThreadState = CT_READY; + maybeGiveProgressReport(); +} +void SfxExtractor::finished() { + OTRAudio_UninstallSfxCaptureThread(); // Returns to normal audio opperation. + 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() { + int frameLimit = 0; // A couple of sounds don't come to a full stop until another sound is loaded, but should be + // effectively silent after a couple of seconds. + do { + AudioMgr_CreateNextAudioBuffer(tempBuffer + 0, SFX_EXTRACTION_ONE_FRAME); + } while (frameLimit++ < 200 && !isAllSilence(tempBuffer + 0, SFX_EXTRACTION_ONE_FRAME * 2)); + 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, sizeof(tempBuffer)); + 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 (isAllSilence(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..daa3cb78d --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/SfxExtractor.h @@ -0,0 +1,51 @@ +#pragma once +#include "libultraship/libultraship.h" + +#define SFX_EXTRACTION_BUFFER_SIZE 32000 * 15 +#define SFX_EXTRACTION_ONE_FRAME 560 +#define SFX_EXTRACTION_SILENCE_THRESHOLD 6 // Corresponds to an amplitude of -75dB. + +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 isAllSilence(int16_t* buffer, size_t count); + bool isSilentSample(int16_t sample); + 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..8f32b861b --- /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[] = { + 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 = sizeof(sfxTable) / sizeof(s16); 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..8db4cd6de --- /dev/null +++ b/soh/soh/Enhancements/accessible-actors/accessibility_cues.cpp @@ -0,0 +1,1366 @@ +#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); +} +#define DETECTION_DISTANCE 500.0 +#define MIN_INCLINE_DISTANCE 5.0 +#define MIN_DECLINE_DISTANCE 5.0 +#define DEFAULT_PROBE_SPEED 5.5 +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 DiscoveredTerrain { + DISCOVERED_NOTHING, + 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; + s8 restFrames; // Used to control how often sounds get played. + + // Call to start playback. + void play(s16 sfx) { + ActorAccessibility_PlaySound(this, 0, sfx); + ActorAccessibility_SetSoundPos(this, 0, &terrainProjectedPos, xzDistToPlayer, actor->policy.distance); + ActorAccessibility_SetSoundPitch(this, 0, currentPitch); + } + + // Call when terrain is no longer present to stop playback. + void stop() { + ActorAccessibility_StopSound(this, 0); + } + + // Custom terrain sound behaviour. + virtual void run() = 0; + // Update sound position and volume once per frame. + 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); + f32 distance = actor->policy.distance * (player->stateFlags1 & PLAYER_STATE1_FIRST_PERSON ? 2.0 : 1.0); + ActorAccessibility_SetSoundPos(this, 0, &terrainProjectedPos, xzDistToPlayer, distance); + ActorAccessibility_SetSoundPitch(this, 0, currentPitch); + } + + public: + TerrainCueSound(AccessibleActor* actor, Vec3f pos) + : actor(actor), currentPitch(1.0), xzDistToPlayer(0), restFrames(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; + this->pitchModifier = pitchModifier; + + play(NA_SE_PL_MAGIC_SOUL_FLASH); + } + virtual ~Incline() = default; + virtual void run() { + if (restFrames > 0) { + restFrames--; + if (restFrames == 0) + play(NA_SE_PL_MAGIC_SOUL_FLASH); + + 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; + this->pitchModifier = 0.0; + + play(NA_SE_PL_MAGIC_SOUL_FLASH); + } + virtual ~Decline() = default; + virtual void run() { + if (restFrames > 0) { + restFrames--; + if (restFrames == 0) + play(NA_SE_PL_MAGIC_SOUL_FLASH); + + return; + } + ActorAccessibility_SetSoundPitch(this, 0, 1.0 + pitchModifier); + } + 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. + + public: + Ledge(AccessibleActor* actor, Vec3f pos, Vec3s probeRot, s8 type = 0) : TerrainCueSound(actor, pos) { + if (type == 1) + currentPitch = 2.0; + savedType = type; + + if (type == 0) { + currentPitch = probeRot.y == 0 ? 0.4 : probeRot.y < 0 ? 0.2 : 0.8; + } + + play(sfx()); + } + virtual ~Ledge() = default; + s8 type() { + return savedType; + } + s16 sfx() { + switch (savedType) { + case 0: + return NA_SE_EV_WIND_TRAP; + case 1: + return NA_SE_EV_WOOD_BOUND; + case 2: + return NA_SE_PL_LAND_WATER0; + case 3: + return NA_SE_SY_WARNING_COUNT_N; + default: + return 0; + } + } + void run() { + if (restFrames == 0) { + play(sfx()); + restFrames = 10; + } else { + restFrames--; + } + } +}; + +class Platform : protected TerrainCueSound { + public: + Platform(AccessibleActor* actor, Vec3f pos) : TerrainCueSound(actor, pos) { + currentPitch = 2.0; + // actor->policy.volume = 1.5; + } + virtual ~Platform() = default; + void setActor(AccessibleActor* actor) { + this->actor = actor; + } + void setPosition(Vec3f& pos) { + updatePositions(pos); + } + void run() { + if (restFrames == 0) { + play(NA_SE_IT_SHIELD_REFLECT_SW); + restFrames = 10; + } else { + restFrames--; + } + } +}; + +class Wall : protected TerrainCueSound { + f32 targetPitch; + + public: + Wall(AccessibleActor* actor, Vec3f pos, Vec3s rot) : TerrainCueSound(actor, pos) { + currentPitch = 0.5; + + targetPitch = (f32)rot.y / (16384.0f * 2.0f); + if (rot.y != 0 && targetPitch < -0.4) + targetPitch = -0.4; + + play(NA_SE_IT_SWORD_CHARGE); + } + virtual ~Wall() = default; + void run() { + restFrames++; + + if (restFrames == 20) { + restFrames = 0; + play(NA_SE_IT_SWORD_CHARGE); + ActorAccessibility_SeekSound(this, 0, 44100 * 2); + } + f32 pitchModifier = 0.0; + + if (targetPitch < 0) + pitchModifier = LERP(2.5, 0.5 + targetPitch, (f32)restFrames / 20.0f); + else if (targetPitch > 0) + pitchModifier = LERP(0.1, (0.5 + targetPitch), (f32)restFrames / 20.0f); + + ActorAccessibility_SetSoundPitch(this, 0, pitchModifier); + } +}; + +class Spike : protected TerrainCueSound { + public: + Spike(AccessibleActor* actor, Vec3f pos) : TerrainCueSound(actor, pos) { + currentPitch = 0.5; + play(NA_SE_IT_SWORD_PICKOUT); + } + virtual ~Spike() = default; + void run() { + if (restFrames == 0) { + play(NA_SE_IT_SWORD_PICKOUT); + restFrames = 10; + return; + } + restFrames--; + } +}; + +class Water : protected TerrainCueSound { + public: + Water(AccessibleActor* actor, Vec3f pos) : TerrainCueSound(actor, pos) { + currentPitch = 0.5; + play(NA_SE_PL_LAND_WATER0); + } + virtual ~Water() = default; + void run() { + if (restFrames == 0) { + play(NA_SE_PL_LAND_WATER0); + restFrames = 10; + } else { + restFrames--; + } + } +}; + +class Ground : protected TerrainCueSound { + float pitchModifier; + + public: + Ground(AccessibleActor* actor, Vec3f pos, float pitchModifier) : TerrainCueSound(actor, pos) { + currentPitch = 1.0; + this->pitchModifier = 0.0; + play(NA_SE_EV_WOOD_BOUND); + } + virtual ~Ground() = default; + void run() { + if (restFrames == 0) { + play(NA_SE_EV_WOOD_BOUND); + restFrames = 10; + } else { + 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; + play(NA_SE_SY_WARNING_COUNT_N); + } + virtual ~Lava() = default; + void run() { + if (restFrames == 0) { + play(NA_SE_SY_WARNING_COUNT_N); + restFrames = 10; + } else { + restFrames--; + } + } +}; + +class TerrainCueDirection final { + AccessibleActor* actor; + PlayerBodyPart 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; + DiscoveredTerrain 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) { + rot = ogRot; + f32 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. + TerrainCueDirection(AccessibleActor* actor, Vec3s rot, PlayerBodyPart startingBodyPart = PLAYER_BODYPART_MAX) + : platform(actor, { 0, 0, 0 }) { + this->actor = actor; + this->relRot = rot; + this->rot = { 0, 0, 0 }; + this->startingBodyPart = startingBodyPart; + + terrainDiscovered = DISCOVERED_NOTHING; + currentSound = NULL; + + disabled = false; + trackingMode = false; + trackingModeStarted = false; + } + + ~TerrainCueDirection() { + switch (terrainDiscovered) { + case DISCOVERED_NOTHING: + break; + case DISCOVERED_INCLINE: + this->incline.~Incline(); + break; + case DISCOVERED_DECLINE: + this->decline.~Decline(); + break; + case DISCOVERED_LEDGE: + this->ledge.~Ledge(); + break; + case DISCOVERED_WALL: + this->wall.~Wall(); + break; + case DISCOVERED_SPIKE: + this->spike.~Spike(); + break; + case DISCOVERED_WATER: + this->water.~Water(); + break; + case DISCOVERED_GROUND: + this->ground.~Ground(); + break; + case DISCOVERED_LAVA: + this->lava.~Lava(); + break; + } + this->platform.~Platform(); + } + + // 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; + } + 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) { + 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; + Vec3f polyVerts[3]; + + // Is this a spiked wall? + 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; + } +}; + +// 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; +} + +struct TerrainCueState { + AccessibleActor* actor; + TerrainCueDirection directions[3]; // Directly ahead of Link, 90 degrees to his left and 90 degrees to his right + + TerrainCueState(AccessibleActor* actor) + : actor(actor), directions{ + { actor, { 0, 0, 0 } }, + { actor, { 0, 16384, 0 } }, + { actor, { 0, -16384, 0 } }, + } { + } + + ~TerrainCueState() { + delete actor; + } + + void Run(PlayState* play) { + ActorAccessibility_RunAccessibilityForActor(play, actor); + for (int i = 0; i < 3; i++) { + directions[i].scan(); + } + } +}; + +TerrainCueState* InitTerrainCueState(AccessibleActor* actor) { + return new TerrainCueState(actor); +} + +void DeleteTerrainCueState(TerrainCueState* terrainCues) { + delete terrainCues; +} + +void RunTerrainCueState(TerrainCueState* terrainCues, PlayState* play) { + terrainCues->Run(play); +} \ 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..c674da3f4 --- /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 \ 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 51a6ab5a8..e60e28085 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -70,6 +70,7 @@ DEFINE_HOOK(OnUpdateFileNameSelection, (int16_t charCode)); DEFINE_HOOK(OnFileChooseMain, (void* gameState)); 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 f69cc29af..f86dbc764 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -312,6 +312,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 a6f1563f2..40998fd48 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -79,6 +79,8 @@ void GameInteractor_ExecuteOnFileChooseMain(void* gameState); // 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 7d2194cca..0c6cd0114 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -127,6 +127,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" @@ -295,6 +298,12 @@ void OTRGlobals::Initialize() { } } } + + 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); }); @@ -588,6 +597,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, @@ -1276,6 +1290,9 @@ extern "C" void InitOTR() { #endif OTRMessage_Init(); +#if !defined(__SWITCH__) && !defined(__WIIU__) + ActorAccessibility_Init(); +#endif OTRAudio_Init(); OTRExtScanner(); VanillaItemTable_Init(); @@ -1327,6 +1344,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. @@ -2598,6 +2618,40 @@ extern "C" void Gfx_RegisterBlendedTexture(const char* name, u8* mask, u8* repla } } +void OTRAudio_SfxCaptureThread() { + while (audio.running) { + // This entire body is expected to be atomic; Don't try to narrow the scope of this lock please! + // Todo: remove the thread altogether as we don't actually need or want parallelism here. + std::unique_lock Lock(audio.mutex); + while (!audio.processing && audio.running) { + audio.cv_to_thread.wait(Lock); + } + + if (!audio.running) { + break; + } +#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) { if (auto intP = dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()) ->GetInterpreterWeak() diff --git a/soh/soh/OTRGlobals.h b/soh/soh/OTRGlobals.h index 032686f8e..2b644fe82 100644 --- a/soh/soh/OTRGlobals.h +++ b/soh/soh/OTRGlobals.h @@ -176,8 +176,12 @@ void Messagebox_ShowErrorBox(char* title, char* body); 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 2472e325b..df1ff692e 100644 --- a/soh/soh/SohGui/SohMenuSettings.cpp +++ b/soh/soh/SohGui/SohMenuSettings.cpp @@ -236,6 +236,12 @@ 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, "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 d8263f5de..0a713eba9 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -82,6 +82,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; @@ -1264,6 +1265,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; @@ -2561,6 +2563,7 @@ u32 D_80116068[ACTORCAT_MAX] = { }; void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { + Actor* refActor; Actor* actor; Player* player; @@ -2576,6 +2579,11 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { sp74 = NULL; unkFlag = 0; + if (freezeActors) { + GameInteractor_ExecuteOnPlayerUpdate(); + 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();