#include <spdlog/spdlog.h>
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
#include "soh/Enhancements/enhancementTypes.h"
#include "soh/Enhancements/randomizer/SeedContext.h"

extern "C" {
#include "src/overlays/actors/ovl_En_Wonder_Talk2/z_en_wonder_talk2.h"
#include "src/overlays/actors/ovl_Elf_Msg/z_elf_msg.h"
#include "src/overlays/actors/ovl_Obj_Switch/z_obj_switch.h"
#include "src/overlays/actors/ovl_Bg_Bdan_Switch/z_bg_bdan_switch.h"
#include "src/overlays/actors/ovl_En_Owl/z_en_owl.h"
#include "src/overlays/actors/ovl_En_Go2/z_en_go2.h"
#include "src/overlays/actors/ovl_En_Heishi2/z_en_heishi2.h"
#include "src/overlays/actors/ovl_En_Ma1/z_en_ma1.h"
#include "src/overlays/actors/ovl_En_Ru2/z_en_ru2.h"
#include "src/overlays/actors/ovl_En_Box/z_en_box.h"
#include "src/overlays/actors/ovl_Demo_Im/z_demo_im.h"
#include "src/overlays/actors/ovl_Demo_Kekkai/z_demo_kekkai.h"
#include "src/overlays/actors/ovl_En_Sa/z_en_sa.h"
#include "src/overlays/actors/ovl_Bg_Ddan_Kd/z_bg_ddan_kd.h"
#include "src/overlays/actors/ovl_En_Tk/z_en_tk.h"
#include "src/overlays/actors/ovl_En_Fu/z_en_fu.h"
#include "src/overlays/actors/ovl_En_Jj/z_en_jj.h"
#include "src/overlays/actors/ovl_En_Daiku/z_en_daiku.h"
#include "src/overlays/actors/ovl_Bg_Spot02_Objects/z_bg_spot02_objects.h"
#include "src/overlays/actors/ovl_Bg_Spot03_Taki/z_bg_spot03_taki.h"
#include "src/overlays/actors/ovl_Bg_Hidan_Kousi/z_bg_hidan_kousi.h"
#include "src/overlays/actors/ovl_Bg_Dy_Yoseizo/z_bg_dy_yoseizo.h"
#include "src/overlays/actors/ovl_En_Dnt_Demo/z_en_dnt_demo.h"
#include "src/overlays/actors/ovl_En_Po_Sisters/z_en_po_sisters.h"
#include "src/overlays/actors/ovl_Obj_Lightswitch/z_obj_lightswitch.h"
#include "src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.h"
#include "src/overlays/actors/ovl_En_Bigokuta/z_en_bigokuta.h"
#include <overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.h>
#include <overlays/actors/ovl_En_Ik/z_en_ik.h>
#include <objects/object_gnd/object_gnd.h>
extern SaveContext gSaveContext;
extern PlayState* gPlayState;
extern int32_t D_8011D3AC;

extern void BgSpot03Taki_HandleWaterfallState(BgSpot03Taki* bgSpot03Taki, PlayState* play);
extern void BgSpot03Taki_ApplyOpeningAlpha(BgSpot03Taki* bgSpot03Taki, s32 bufferIndex);

extern void EnGo2_CurledUp(EnGo2* enGo2, PlayState* play);

extern void EnRu2_SetEncounterSwitchFlag(EnRu2* enRu2, PlayState* play);

extern void EnDaiku_EscapeSuccess(EnDaiku* enDaiku, PlayState* play);

extern void EnJj_WaitToOpenMouth(EnJj* enJj, PlayState* play);
extern void EnJj_WaitForFish(EnJj* enJj, PlayState* play);
extern void EnJj_SetupAction(EnJj* enJj, EnJjActionFunc actionFunc);
}

void EnMa1_EndTeachSong(EnMa1* enMa1, PlayState* play) {
    if (Message_GetState(&gPlayState->msgCtx) == TEXT_STATE_CLOSING) {
        Flags_SetRandomizerInf(RAND_INF_LEARNED_EPONA_SONG);
        Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME);
        enMa1->actor.flags &= ~ACTOR_FLAG_TALK_OFFER_AUTO_ACCEPTED;
        play->msgCtx.ocarinaMode = OCARINA_MODE_04;
        enMa1->actionFunc = EnMa1_Idle;
        enMa1->singingDisabled = 1;
        enMa1->interactInfo.talkState = NPC_TALK_STATE_IDLE;
        return;
    }
}

void EnFu_EndTeachSong(EnFu* enFu, PlayState* play) {
    if (Message_GetState(&gPlayState->msgCtx) == TEXT_STATE_CLOSING) {
        Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME);
        enFu->actionFunc = EnFu_WaitAdult;
        enFu->actor.flags &= ~ACTOR_FLAG_TALK_OFFER_AUTO_ACCEPTED;

        play->msgCtx.ocarinaMode = OCARINA_MODE_04;
        Flags_SetEventChkInf(EVENTCHKINF_PLAYED_SONG_OF_STORMS_IN_WINDMILL);
        Flags_SetEventChkInf(EVENTCHKINF_LEARNED_SONG_OF_STORMS);
        return;
    }
}

void EnDntDemo_JudgeSkipToReward(EnDntDemo* enDntDemo, PlayState* play) {
    // todo: figure out a better way to handle toggling so we don't
    //       need to double check cvars like this
    if (!(IS_RANDO || CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO))) {
        EnDntDemo_Judge(enDntDemo, play);
        return;
    } else if ((IS_RANDO && RAND_GET_OPTION(RSK_SHUFFLE_SPEAK)) || enDntDemo->actor.xzDistToPlayer > 30.0f) {
        if (enDntDemo->judgeTimer > 0 && enDntDemo->judgeTimer < 40) {
            enDntDemo->judgeTimer = 40;
        }
        EnDntDemo_Judge(enDntDemo, play);
        return;
    }

    switch (Player_GetMask(play)) {
        case PLAYER_MASK_SKULL: {
            Flags_SetItemGetInf(ITEMGETINF_OBTAINED_STICK_UPGRADE_FROM_STAGE);
            return;
        }
        case PLAYER_MASK_TRUTH: {
            if (GameInteractor_Should(VB_DEKU_SCRUBS_REACT_TO_MASK_OF_TRUTH, true)) {
                Flags_SetItemGetInf(ITEMGETINF_OBTAINED_NUT_UPGRADE_FROM_STAGE);
            }
            return;
        }
        default: {
            EnDntDemo_Judge(enDntDemo, play);
            return;
        }
    }
}

void BgSpot03Taki_KeepOpen(BgSpot03Taki* bgSpot03Taki, PlayState* play) {
}

static u32 successChimeCooldown = 0;
void RateLimitedSuccessChime() {
    if (gPlayState->gameplayFrames > successChimeCooldown) {
        Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME);
        successChimeCooldown = gPlayState->gameplayFrames + 120;
    }
}

bool ForcedDialogIsDisabled(ForcedDialogMode type) {
    return (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipForcedDialog"),
                           IS_RANDO ? FORCED_DIALOG_SKIP_ALL : FORCED_DIALOG_SKIP_NONE) &
            type) != 0;
}

void TimeSaverOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_list originalArgs) {
    va_list args;
    va_copy(args, originalArgs);

    switch (id) {
        case VB_PLAY_TRANSITION_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), IS_RANDO) || IS_RANDO) {
                // Song of Time
                if (gSaveContext.entranceIndex == ENTR_TEMPLE_OF_TIME_ENTRANCE &&
                    gSaveContext.cutsceneIndex == 0xFFF7) {
                    gSaveContext.entranceIndex = ENTR_HYRULE_FIELD_16;
                    gSaveContext.cutsceneIndex = 0;
                    gSaveContext.nextTransitionType = 3;
                    *should = false;
                }

                // Requiem of Spirit
                if ((gSaveContext.entranceIndex == ENTR_DESERT_COLOSSUS_OUTSIDE_TEMPLE) &&
                    !Flags_GetEventChkInf(EVENTCHKINF_LEARNED_REQUIEM_OF_SPIRIT)) {
                    Flags_SetEventChkInf(EVENTCHKINF_LEARNED_REQUIEM_OF_SPIRIT);
                    // Normally happens in the cutscene
                    gSaveContext.dayTime = gSaveContext.skyboxTime = 0xAC60;
                    if (GameInteractor_Should(VB_GIVE_ITEM_SONG, true, ITEM_SONG_REQUIEM)) {
                        Item_Give(gPlayState, ITEM_SONG_REQUIEM);
                    }
                    *should = false;
                }

                u8 meetsBurningKakRequirements = LINK_IS_ADULT && gSaveContext.cutsceneIndex < 0xFFF0 &&
                                                 gSaveContext.entranceIndex == ENTR_KAKARIKO_VILLAGE_FRONT_GATE &&
                                                 Flags_GetEventChkInf(EVENTCHKINF_USED_FOREST_TEMPLE_BLUE_WARP) &&
                                                 Flags_GetEventChkInf(EVENTCHKINF_USED_FIRE_TEMPLE_BLUE_WARP) &&
                                                 Flags_GetEventChkInf(EVENTCHKINF_USED_WATER_TEMPLE_BLUE_WARP) &&
                                                 !Flags_GetEventChkInf(EVENTCHKINF_BONGO_BONGO_ESCAPED_FROM_WELL);
                if (GameInteractor_Should(VB_BE_ELIGIBLE_FOR_NOCTURNE_OF_SHADOW, meetsBurningKakRequirements)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BONGO_BONGO_ESCAPED_FROM_WELL);
                    // Normally happens in the cutscene
                    Flags_SetEventChkInf(EVENTCHKINF_LEARNED_NOCTURNE_OF_SHADOW);
                    if (GameInteractor_Should(VB_GIVE_ITEM_SONG, true, ITEM_SONG_NOCTURNE)) {
                        Item_Give(gPlayState, ITEM_SONG_NOCTURNE);
                    }
                    *should = false;
                }
            }

            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) {
                // LACS
                if (LINK_IS_ADULT && (gEntranceTable[gSaveContext.entranceIndex].scene == SCENE_TEMPLE_OF_TIME) &&
                    CHECK_QUEST_ITEM(QUEST_MEDALLION_SPIRIT) && CHECK_QUEST_ITEM(QUEST_MEDALLION_SHADOW) &&
                    !Flags_GetEventChkInf(EVENTCHKINF_RETURNED_TO_TEMPLE_OF_TIME_WITH_ALL_MEDALLIONS)) {
                    Flags_SetEventChkInf(EVENTCHKINF_RETURNED_TO_TEMPLE_OF_TIME_WITH_ALL_MEDALLIONS);
                    if (GameInteractor_Should(VB_GIVE_ITEM_LIGHT_ARROW, true)) {
                        Item_Give(gPlayState, ITEM_ARROW_LIGHT);
                    }
                    *should = false;
                }
            }

            if (gSaveContext.entranceIndex == ENTR_GANONS_TOWER_COLLAPSE_EXTERIOR_0) {
                if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipTowerEscape"), false) || IS_BOSS_RUSH) {
                    Flags_SetEventChkInf(EVENTCHKINF_WATCHED_GANONS_CASTLE_COLLAPSE_CAUGHT_BY_GERUDO);
                    gSaveContext.entranceIndex = ENTR_GANON_BOSS_0;
                    *should = false;
                }
            }

            if (gSaveContext.entranceIndex == ENTR_CASTLE_COURTYARD_GUARDS_DAY_0) {
                if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipChildStealth"), false)) {
                    gSaveContext.entranceIndex = ENTR_CASTLE_COURTYARD_ZELDA_0;
                    *should = false;
                }
            }
            break;
        }
        case VB_PLAY_ENTRANCE_CS: {
            s32 entranceFlag = va_arg(args, s32);
            s32 entranceIndex = va_arg(args, s32);

            // Epona LLR fence jump cutscenes not skipped to allow the player and epona to load in the world correctly
            // Nabooru fight cutscene is handled by boss intro skip instead (which deals with other flags needing to be
            // set)
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Entrances"), IS_RANDO) &&
                (entranceFlag != EVENTCHKINF_EPONA_OBTAINED) && entranceIndex != ENTR_SPIRIT_TEMPLE_BOSS_ENTRANCE) {
                *should = false;

                // Check for dispulsion of Ganon's Tower barrier
                switch (entranceIndex) {
                    case ENTR_INSIDE_GANONS_CASTLE_2:
                    case ENTR_INSIDE_GANONS_CASTLE_3:
                    case ENTR_INSIDE_GANONS_CASTLE_4:
                    case ENTR_INSIDE_GANONS_CASTLE_5:
                    case ENTR_INSIDE_GANONS_CASTLE_6:
                    case ENTR_INSIDE_GANONS_CASTLE_7:
                        if (Flags_GetEventChkInf(EVENTCHKINF_COMPLETED_FOREST_TRIAL) &&
                            Flags_GetEventChkInf(EVENTCHKINF_COMPLETED_WATER_TRIAL) &&
                            Flags_GetEventChkInf(EVENTCHKINF_COMPLETED_SHADOW_TRIAL) &&
                            Flags_GetEventChkInf(EVENTCHKINF_COMPLETED_FIRE_TRIAL) &&
                            Flags_GetEventChkInf(EVENTCHKINF_COMPLETED_LIGHT_TRIAL) &&
                            Flags_GetEventChkInf(EVENTCHKINF_COMPLETED_SPIRIT_TRIAL)) {
                            Flags_SetEventChkInf(EVENTCHKINF_DISPELLED_GANONS_TOWER_BARRIER);
                        }
                        break;
                }
            }
            break;
        }
        case VB_PLAY_ONEPOINT_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.OnePoint"), IS_RANDO)) {
                s16* csId = va_arg(args, s16*);
                BgSpot03Taki* taki = NULL;
                switch (*csId) {
                    case 3120:
                    case 3150:
                    case 4180:
                    case 4100:
                        *should = false;
                        RateLimitedSuccessChime();
                        Message_CloseTextbox(gPlayState);
                        taki = (BgSpot03Taki*)Actor_FindNearby(gPlayState, &GET_PLAYER(gPlayState)->actor,
                                                               ACTOR_BG_SPOT03_TAKI, ACTORCAT_BG, 999.0f);
                        if (taki != NULL) {
                            func_8003EBF8(gPlayState, &gPlayState->colCtx.dyna, taki->dyna.bgId);
                        }
                        break;
                    default:
                        SPDLOG_INFO("VB_PLAY_ONEPOINT_CS {}", *csId);
                        break;
                }
            }
            break;
        }
        case VB_PLAY_ONEPOINT_ACTOR_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.OnePoint"), IS_RANDO)) {
                Actor* actor = va_arg(args, Actor*);

                // there are a few checks throughout the game (such as chest spawns) that rely on this
                // the checks are for func_8005B198() == this->dyna.actor.category
                // func_8005B198 just returns D_8011D3AC
                // D_8011D3AC is set to camera->target->category in Camera_Demo5
                D_8011D3AC = actor->category;

                switch (actor->category) {
                    case ACTORCAT_BG:
                        if (actor->id == ACTOR_BG_DDAN_KD) {
                            BgDdanKd* ddanKd = (BgDdanKd*)actor;
                            Flags_SetSwitch(gPlayState, ddanKd->dyna.actor.params);
                        }
                        if (actor->id == ACTOR_BG_MORI_HINERI) {
                            break;
                        }
                        // This is handled in the FasterHeavyBlockLift enhancement
                        if (actor->id == ACTOR_BG_HEAVY_BLOCK) {
                            break;
                        }

                        RateLimitedSuccessChime();
                        *should = false;
                        break;
                }
                switch (actor->id) {
                    case ACTOR_OBJ_SWITCH: {
                        // The DC boss door can be unlocked with OI; One Point is required for it
                        // The Water Temple Dragon Room chest can be obtained with a cutscene dive
                        if (((actor->params == 8224 && gPlayState->sceneNum == SCENE_DODONGOS_CAVERN) ||
                             (actor->params == 6979 && gPlayState->sceneNum == SCENE_WATER_TEMPLE)) &&
                            CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.GlitchAiding"), 0)) {
                            break;
                        }
                        ObjSwitch* switchActor = (ObjSwitch*)actor;
                        switchActor->cooldownTimer = 0;
                        *should = false;
                        RateLimitedSuccessChime();
                        break;
                    }
                    case ACTOR_BG_BDAN_SWITCH: {
                        // The switch in jabu that you are intended to press with a box to reach barinade
                        // can be skipped by either a frame perfect roll open or with OI
                        // Additionally, the blue switch that you are intended to press with Ruto
                        // can be skipped with OI
                        // The One Point for that switch is used in common setups for the former and is required for the
                        // latter to work
                        if ((actor->params == 14848 || actor->params == 14336) &&
                            gPlayState->sceneNum == SCENE_JABU_JABU &&
                            CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.GlitchAiding"), 0)) {
                            break;
                        }
                        BgBdanSwitch* switchActor = (BgBdanSwitch*)actor;
                        switchActor->unk_1D8 = 0;
                        switchActor->unk_1DA = 0;
                        *should = false;
                        RateLimitedSuccessChime();
                        break;
                    }
                    case ACTOR_BG_HIDAN_KOUSI: {
                        BgHidanKousi* switchActor = (BgHidanKousi*)actor;
                        BgHidanKousi_SetupAction(switchActor, func_80889C18);
                        *should = false;
                        RateLimitedSuccessChime();
                        break;
                    }
                    case ACTOR_BG_JYA_BOMBCHUIWA: {
                        BgJyaBombchuiwa* bombchuiwa = (BgJyaBombchuiwa*)actor;
                        if (!(bombchuiwa->drawFlags & 4) && bombchuiwa->timer >= 0 && bombchuiwa->timer < 9) {
                            bombchuiwa->timer = 9;
                        }
                        *should = false;
                        RateLimitedSuccessChime();
                        break;
                    }
                    case ACTOR_EN_GO2: {
                        EnGo2* biggoron = (EnGo2*)actor;
                        biggoron->isAwake = true;
                        *should = false;
                        break;
                    }
                    case ACTOR_EN_BOX: {
                        // The chest that drops in MQ Jabu allowing unintended door entry
                        if (actor->params == -30457 && gPlayState->sceneNum == SCENE_JABU_JABU &&
                            CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.GlitchAiding"), 0)) {
                            break;
                        }
                        EnBox* boxActor = (EnBox*)actor;
                        *should = false;
                        RateLimitedSuccessChime();
                        break;
                    }
                    case ACTOR_EN_SIOFUKI: {
                        // The Spirit Temple MQ water jet cutscene is required for an actor glitch
                        // setup that skips the grate
                        if (actor->params == 6359 && gPlayState->sceneNum == SCENE_SPIRIT_TEMPLE &&
                            CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.GlitchAiding"), 0)) {
                            *should = true;
                            break;
                        }
                        *should = false;
                        break;
                    }
                    case ACTOR_BG_HIDAN_FWBIG:
                    case ACTOR_EN_EX_ITEM:
                    case ACTOR_EN_DNT_NOMAL:
                    case ACTOR_EN_DNT_DEMO:
                    case ACTOR_BG_HAKA_ZOU: {
                        *should = false;
                        break;
                    }
                    case ACTOR_EN_TA:
                    case ACTOR_DOOR_SHUTTER: {
                        // The shutter cutscene occurs post-switch cutscene to focus Link on the unlocked doors
                        if (((actor->params == 9402 && gPlayState->sceneNum == SCENE_JABU_JABU) ||
                             (actor->params == 20460 && gPlayState->sceneNum == SCENE_DODONGOS_CAVERN)) &&
                            CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.GlitchAiding"), 0)) {
                            break;
                        }
                        *should = false;
                        RateLimitedSuccessChime();
                        break;
                    }
                    case ACTOR_OBJ_LIGHTSWITCH: {
                        ObjLightswitch* lightswitch = (ObjLightswitch*)actor;
                        lightswitch->toggleDelay = 0;
                        *should = false;
                        RateLimitedSuccessChime();
                        break;
                    }
                    case ACTOR_BG_ICE_SHUTTER:
                    case ACTOR_OBJ_SYOKUDAI:
                    case ACTOR_OBJ_TIMEBLOCK:
                    case ACTOR_EN_PO_SISTERS:
                    case ACTOR_OBJ_ICE_POLY:
                    case ACTOR_BG_YDAN_MARUTA:
                    case ACTOR_BG_SPOT18_SHUTTER:
                    case ACTOR_BG_SPOT05_SOKO:
                    case ACTOR_BG_SPOT06_OBJECTS:
                    case ACTOR_BG_SPOT18_BASKET:
                    case ACTOR_BG_HIDAN_CURTAIN:
                    case ACTOR_BG_MORI_HINERI:
                    case ACTOR_BG_MIZU_SHUTTER:
                    case ACTOR_SHOT_SUN:
                    case ACTOR_BG_HAKA_GATE:
                    case ACTOR_EN_KAKASI2:
                    case ACTOR_EN_DNT_JIJI:
                        *should = false;
                        RateLimitedSuccessChime();
                        break;
                }
                if (*should) {
                    SPDLOG_INFO("VB_PLAY_ONEPOINT_ACTOR_CS ID:{} Cat:{}", actor->id, actor->category);
                }
            }
            break;
        }
        case VB_FREEZE_LINK_FOR_FOREST_PILLARS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.OnePoint"), IS_RANDO)) {
                *should = false;
            }
            break;
        case VB_SHOW_TITLE_CARD:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.DisableTitleCard"), IS_RANDO)) {
                *should = false;
            }
            break;
        case VB_WONDER_TALK: {
            // We want to show the frog hint if it is on, regardless of cutscene settings
            if (ForcedDialogIsDisabled(FORCED_DIALOG_SKIP_NPC) &&
                !(gPlayState->sceneNum == SCENE_ZORAS_RIVER && IS_RANDO && RAND_GET_OPTION(RSK_FROGS_HINT))) {
                *should = false;
            }

            // If it's near a jailed carpenter, skip it along with introduction of Gerudo mini-boss
            if (gPlayState->sceneNum == SCENE_THIEVES_HIDEOUT &&
                CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                EnWonderTalk2* enWonderTalk = va_arg(args, EnWonderTalk2*);
                EnDaiku* enDaiku =
                    (EnDaiku*)Actor_FindNearby(gPlayState, &enWonderTalk->actor, ACTOR_EN_DAIKU, ACTORCAT_NPC, 999.0f);
                if (enDaiku != NULL) {
                    Flags_SetSwitch(gPlayState, enDaiku->startFightSwitchFlag);
                    *should = false;
                }
            }
            break;
        }
        case VB_NAVI_TALK: {
            if (ForcedDialogIsDisabled(FORCED_DIALOG_SKIP_NAVI)) {
                ElfMsg* naviTalk = va_arg(args, ElfMsg*);
                int32_t paramsHighByte = naviTalk->actor.params >> 8;
                if ((paramsHighByte & 0x80) != 0) {
                    break;
                }
                if ((paramsHighByte & 0x3F) != 0x3F) {
                    Flags_SetSwitch(gPlayState, paramsHighByte & 0x3F);
                }
                Actor_Kill(&naviTalk->actor);
                *should = false;
            }
            break;
        }
        case VB_END_GERUDO_MEMBERSHIP_TALK: {
            if (ForcedDialogIsDisabled(FORCED_DIALOG_SKIP_NPC)) {
                *should = true;
            }
            break;
        }
        case VB_GORON_LINK_BE_SCARED: {
            if (ForcedDialogIsDisabled(FORCED_DIALOG_SKIP_NPC)) {
                EnGo2* goronLink = va_arg(args, EnGo2*);
                goronLink->trackingMode = NPC_TRACKING_NONE;
                goronLink->unk_211 = false;
                goronLink->isAwake = false;
                goronLink->actionFunc = EnGo2_CurledUp;
                Flags_SetInfTable(INFTABLE_STOPPED_GORON_LINKS_ROLLING);
                *should = false;
            }
            break;
        }
        case VB_NOT_BE_GREETED_BY_SARIA:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Entrances"), IS_RANDO) &&
                !Flags_GetInfTable(INFTABLE_GREETED_BY_SARIA)) {
                Flags_SetInfTable(INFTABLE_GREETED_BY_SARIA);
                *should = true;
            }
            break;
        case VB_PLAY_SHIEK_BLOCK_MASTER_SWORD_CS:
        case VB_GIVE_ITEM_LIGHT_ARROW:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) {
                *should = false;
            }
            break;
        case VB_PLAY_NABOORU_CAPTURED_CS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) {
                // we're only here if GetItem is Silver Gauntlets
                // either it's randomiser, or we're about to enter (or skip) the Nabooru Capture
                if (!IS_RANDO) {
                    Flags_SetEventChkInf(EVENTCHKINF_NABOORU_CAPTURED_BY_TWINROVA);
                }
                *should = false;
            }
            break;
        case VB_PLAY_PULL_MASTER_SWORD_CS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_PULLED_MASTER_SWORD_FROM_PEDESTAL)) {
                    // Normally, these would be done in the cutscene, but we're skipping it
                    Flags_SetEventChkInf(EVENTCHKINF_PULLED_MASTER_SWORD_FROM_PEDESTAL);
                    Flags_SetEventChkInf(EVENTCHKINF_ENTERED_MASTER_SWORD_CHAMBER);
                    Flags_SetEventChkInf(EVENTCHKINF_SHEIK_SPAWNED_AT_MASTER_SWORD_PEDESTAL);
                    Flags_SetEventChkInf(EVENTCHKINF_TIME_TRAVELED_TO_ADULT);
                    if (GameInteractor_Should(VB_GIVE_ITEM_LIGHT_MEDALLION, true)) {
                        Item_Give(gPlayState, ITEM_MEDALLION_LIGHT);
                    }
                }
                *should = false;
            }
            break;
        case VB_PLAY_DISPEL_BARRIER_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) {
                static s16 trialEntrances[] = {
                    0,
                    ENTR_INSIDE_GANONS_CASTLE_3,
                    ENTR_INSIDE_GANONS_CASTLE_6,
                    ENTR_INSIDE_GANONS_CASTLE_5,
                    ENTR_INSIDE_GANONS_CASTLE_4,
                    ENTR_INSIDE_GANONS_CASTLE_7,
                    ENTR_INSIDE_GANONS_CASTLE_2,
                };
                RateLimitedSuccessChime();
                DemoKekkai* kekkai = va_arg(args, DemoKekkai*);
                gPlayState->nextEntranceIndex = trialEntrances[kekkai->actor.params];
                gPlayState->transitionTrigger = TRANS_TRIGGER_START;
                gPlayState->transitionType = TRANS_TYPE_FADE_BLACK;
                *should = false;
            }
            break;
        }
        case VB_OWL_INTERACTION: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipOwlInteractions"), IS_RANDO) && *should) {
                EnOwl* enOwl = va_arg(args, EnOwl*);
                s32 owlType = (enOwl->actor.params & 0xFC0) >> 6;

                if (((enOwl->actor.params & 0xFC0) >> 6) == 1) {
                    Flags_SetEventChkInf(EVENTCHKINF_SPOKE_TO_KAEPORA_BY_LOST_WOODS);
                }

                func_80ACA62C(enOwl, gPlayState);
                *should = false;
            }
            break;
        }
        case VB_OVERRIDE_LINK_THE_GORON_DIALOGUE: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO)) {
                u16* textId = va_arg(args, u16*);
                // If the doors are not open yet, prioritize opening them
                if (!Flags_GetInfTable(INFTABLE_GORON_CITY_DOORS_UNLOCKED)) {
                    *textId = 0x3036;
                    *should = true;
                }
            }
            break;
        }
        case VB_PLAY_MWEEP_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), 0)) {
                *should = false;
                Inventory_ReplaceItem(gPlayState, ITEM_LETTER_RUTO, ITEM_BOTTLE);
                Flags_SetEventChkInf(EVENTCHKINF_KING_ZORA_MOVED);
            }
            break;
        }
        case VB_PLAY_BEAN_PLANTING_CS:
        case VB_PLAY_EYEDROP_CREATION_ANIM:
        case VB_PLAY_EYEDROPS_CS:
        case VB_PLAY_DROP_FISH_FOR_JABU_CS:
        case VB_PLAY_DARUNIAS_JOY_CS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO)) {
                *should = false;
            }
            break;
        case VB_PLAY_ZELDAS_LULLABY_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), IS_RANDO) || IS_RANDO) {
                DemoIm* demoIm = va_arg(args, DemoIm*);
                Player* player = GET_PLAYER(gPlayState);
                player->stateFlags1 |= PLAYER_STATE1_IN_CUTSCENE;
                player->stateFlags1 |= PLAYER_STATE1_GETTING_ITEM;
                func_80986794(demoIm);

                static uint32_t demoImUpdateHook = 0;
                static uint32_t demoImKillHook = 0;
                demoImUpdateHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorUpdate>(
                    [](void* actorRef) mutable {
                        Actor* actor = static_cast<Actor*>(actorRef);
                        if (actor->id == ACTOR_DEMO_IM &&
                            (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), IS_RANDO) ||
                             IS_RANDO)) {
                            DemoIm* demoIm = static_cast<DemoIm*>(actorRef);
                            Player* player = GET_PLAYER(gPlayState);
                            player->stateFlags1 |= PLAYER_STATE1_IN_CUTSCENE;
                            player->stateFlags1 |= PLAYER_STATE1_GETTING_ITEM;

                            if (Animation_OnFrame(&demoIm->skelAnime, 25.0f)) {
                                Audio_PlaySoundGeneral(NA_SE_IT_DEKU, &demoIm->actor.projectedPos, 4,
                                                       &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale,
                                                       &gSfxDefaultReverb);
                                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(
                                    demoImUpdateHook);
                                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(
                                    demoImKillHook);
                                demoImUpdateHook = 0;
                                demoImKillHook = 0;
                            } else if (Animation_OnFrame(&demoIm->skelAnime, 15.0f)) {
                                Player* player = GET_PLAYER(gPlayState);
                                // SOH [Randomizer] In entrance rando have impa bring link back to the front of castle
                                // grounds
                                if (IS_RANDO && RAND_GET_OPTION(RSK_SHUFFLE_OVERWORLD_ENTRANCES)) {
                                    gPlayState->nextEntranceIndex = ENTR_CASTLE_GROUNDS_SOUTH_EXIT;
                                } else {
                                    gPlayState->nextEntranceIndex = ENTR_HYRULE_FIELD_17;
                                }
                                gSaveContext.dayTime = gSaveContext.skyboxTime = 0x8000;
                                gPlayState->transitionType = TRANS_TYPE_FADE_WHITE;
                                gPlayState->transitionTrigger = TRANS_TRIGGER_START;
                                gSaveContext.nextTransitionType = 2;
                                Player_SetCsActionWithHaltedActors(gPlayState, &player->actor, 8);
                            }
                        }
                    });
                demoImKillHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneInit>(
                    [](int16_t sceneNum) mutable {
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(demoImUpdateHook);
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(demoImKillHook);
                        demoImUpdateHook = 0;
                        demoImKillHook = 0;
                    });
                *should = false;
            }
            break;
        }
        case VB_PLAY_SARIAS_SONG_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), IS_RANDO) || IS_RANDO) {
                EnSa* enSa = va_arg(args, EnSa*);
                enSa->actionFunc = func_80AF6B20;
                *should = false;
            }
            break;
        }
        case VB_DESPAWN_HORSE_RACE_COW: {
            if (Flags_GetEventChkInf(EVENTCHKINF_WON_COW_IN_MALONS_RACE) &&
                CVarGetInteger(CVAR_ENHANCEMENT("CowOfTime"), 0)) {
                *should = false;
            }
            break;
        }
        case VB_PLAY_DRAIN_WELL_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) {
                *should = false;
                Flags_SetSwitch(gPlayState, 0x2);
                Flags_SetEventChkInf(EVENTCHKINF_PLAYED_SONG_OF_STORMS_IN_WINDMILL);
                Flags_SetEventChkInf(EVENTCHKINF_DRAINED_WELL_IN_KAKARIKO);
            }
            break;
        }
        case VB_PLAY_SUNS_SONG_CS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), IS_RANDO) || IS_RANDO) {
                *should = false;
                Flags_SetEventChkInf(EVENTCHKINF_LEARNED_SUNS_SONG);
                // SoH [Randomizer] TODO: Increment time X amount (find out X)
                // When time is 0, it's changed to 0x46A7
                // When it's 0x8000, it's changed to 0xC090
            }
            break;
        case VB_PLAY_ROYAL_FAMILY_TOMB_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO)) {
                *should = false;
            }
            break;
        }
        case VB_PLAY_ROYAL_FAMILY_TOMB_EXPLODE: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO)) {
                *should = Flags_GetEventChkInf(EVENTCHKINF_DESTROYED_ROYAL_FAMILY_TOMB);
            }
            break;
        }
        case VB_PLAY_CARPENTER_FREE_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.OnePoint"), IS_RANDO)) {
                EnDaiku* enDaiku = va_arg(args, EnDaiku*);
                if (enDaiku->subCamActive) {
                    enDaiku->subCamActive = false;
                    EnDaiku_EscapeSuccess(enDaiku, gPlayState);
                }
                *should = false;
            }
            break;
        }
        case VB_PLAY_TIMEBLOCK_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.OnePoint"), IS_RANDO)) {
                // Todo: Preferable if possible to turn camera as if SoT block cutscene
                *should = false;
            }
            break;
        }
        case VB_PLAY_GORON_FREE_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) {
                *should = false;
            }
            break;
        }
        case VB_PLAY_DOOR_OF_TIME_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO)) {
                *should = false;
                Flags_SetEventChkInf(EVENTCHKINF_OPENED_THE_DOOR_OF_TIME);
                Flags_SetEnv(gPlayState, 2);
                Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME);
            }
            break;
        }
        case VB_PLAY_FIRE_ARROW_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO)) {
                *should = false;
            }
            break;
        }
        case VB_GIVE_ITEM_SONG:
        case VB_PLAY_MINUET_OF_FOREST_CS:
        case VB_PLAY_BOLERO_OF_FIRE_CS:
        case VB_PLAY_SERENADE_OF_WATER_CS:
        case VB_PLAY_SONG_OF_STORMS_CS:
        case VB_PLAY_PRELUDE_OF_LIGHT_CS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), IS_RANDO) || IS_RANDO) {
                *should = false;
            }
            break;
        case VB_FREEZE_ON_SKULL_TOKEN:
            if (CVarGetInteger(CVAR_ENHANCEMENT("SkulltulaFreeze"), 0)) {
                *should = false;
            }
            break;
        case VB_BE_VALID_GRAVEDIGGING_SPOT:
            if (CVarGetInteger(CVAR_ENHANCEMENT("DampeWin"), IS_RANDO)) {
                EnTk* enTk = va_arg(args, EnTk*);
                enTk->validDigHere = true;
                *should = true;
            }
            break;
        case VB_BE_DAMPE_GRAVEDIGGING_GRAND_PRIZE:
            if (CVarGetInteger(CVAR_ENHANCEMENT("DampeWin"), IS_RANDO)) {
                EnTk* enTk = va_arg(args, EnTk*);
                enTk->currentReward = 3;
                *should = true;
            }
            break;
        case VB_DAMPE_GRAVEDIGGING_GRAND_PRIZE_BE_HEART_PIECE:
            if (CVarGetInteger(CVAR_ENHANCEMENT("GravediggingTourFix"), 0) || IS_RANDO) {
                *should = !Flags_GetCollectible(gPlayState, COLLECTFLAG_GRAVEDIGGING_HEART_PIECE);
            }
            break;
        case VB_FIX_SAW_SOFTLOCK:
            // Animation Count should be no more than 1 to guarantee putaway is complete after giving the saw
            // As this is vanilla behavior, it only applies with the Fix toggle or Skip Text enabled.
            *should = (CVarGetInteger(CVAR_ENHANCEMENT("FixSawSoftlock"), 0) != 0 ||
                       CVarGetInteger(CVAR_ENHANCEMENT("SkipText"), 0) != 0)
                          ? gPlayState->animationCtx.animationCount > 1
                          : *should;
            break;
        case VB_BIGGORON_CONSIDER_SWORD_FORGED:
            *should = Environment_GetBgsDayCount() >= CVarGetInteger(CVAR_ENHANCEMENT("ForgeTime"), 3);
            break;
        case VB_BE_ELIGIBLE_FOR_GREAT_FAIRY_REWARD: {
            BgDyYoseizo* bgDyYoseizo = va_arg(args, BgDyYoseizo*);
            RandomizerInf flag = RAND_INF_MAX;

            if (gPlayState->sceneNum == SCENE_GREAT_FAIRYS_FOUNTAIN_SPELLS) {
                switch (bgDyYoseizo->fountainType) {
                    case 0:
                        flag = RAND_INF_ZF_GREAT_FAIRY_REWARD;
                        break;
                    case 1:
                        flag = RAND_INF_HC_GREAT_FAIRY_REWARD;
                        break;
                    case 2:
                        flag = RAND_INF_COLOSSUS_GREAT_FAIRY_REWARD;
                        break;
                }
            } else {
                switch (bgDyYoseizo->fountainType) {
                    case 0:
                        flag = RAND_INF_DMT_GREAT_FAIRY_REWARD;
                        break;
                    case 1:
                        flag = RAND_INF_DMC_GREAT_FAIRY_REWARD;
                        break;
                    case 2:
                        flag = RAND_INF_OGC_GREAT_FAIRY_REWARD;
                        break;
                }
            }

            if (flag != RAND_INF_MAX) {
                if (IS_RANDO) {
                    // If we're in rando, set the flag and fill magic/health. The flag will trigger the check later with
                    // the queue Notably, we ignore the vanilla *should value because in rando we don't care about the
                    // requirements
                    Flags_SetRandomizerInf(flag);
                    gSaveContext.healthAccumulator = MAX_HEALTH;
                    Magic_Fill(gPlayState);
                    // Also prevent the cutscene from playing, technically we could let it play in rando but we'd
                    // need to VB prevent the item gives that happen during the cutscene.
                    *should = false;
                } else {
                    // If we're in vanilla, set the flag _if_ we were eligble, so that anchor can send the reward in
                    // co-op
                    if (*should) {
                        Flags_SetRandomizerInf(flag);
                        // If we're in vanilla and skipping the cutscene, fill health/magic, and prevent the cutscene
                        if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO)) {
                            gSaveContext.healthAccumulator = MAX_HEALTH;
                            Magic_Fill(gPlayState);
                            *should = false;
                        }
                    }
                }
            }

            break;
        }
        case VB_PLAY_GATE_OPENING_OR_CLOSING_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO)) {
                EnHeishi2* enHeishi2 = va_arg(args, EnHeishi2*);
                enHeishi2->unk_2F2[0] = 0;

                // The second argument determines whether the vanilla code should be run anyway. It
                // should be set to `true` ONLY IF said code calls `Play_ClearCamera`, false otherwise.
                bool clearCamera = (bool)va_arg(args, int);
                *should = clearCamera && enHeishi2->cameraId != CAM_ID_MAIN;
            }
            break;
        }
        case VB_PLAY_RAINBOW_BRIDGE_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) {
                *should = false;
                if (!Flags_GetEventChkInf(EVENTCHKINF_RAINBOW_BRIDGE_BUILT)) {
                    Audio_PlaySequenceInCutscene(NA_BGM_BRIDGE_TO_GANONS);
                    // This would have been set 2 frames later, but we're skipping now so the sound doesn't play twice
                    Flags_SetEventChkInf(EVENTCHKINF_RAINBOW_BRIDGE_BUILT);
                }
            }
            break;
        }
        case VB_PHANTOM_GANON_DEATH_SCENE: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.QuickBossDeaths"), IS_RANDO || IS_BOSS_RUSH)) {
                *should = false;
                BossGanondrof* pg = va_arg(args, BossGanondrof*);
                Player* player = GET_PLAYER(gPlayState);
                if (pg->work[GND_ACTION_STATE] == DEATH_SPASM) {
                    // Skip to death scream animation and move ganondrof to middle
                    pg->deathState = DEATH_SCREAM;
                    pg->timers[0] = 50;
                    AnimationHeader* screamAnim = (AnimationHeader*)gPhantomGanonScreamAnim;
                    Animation_MorphToLoop(&pg->skelAnime, screamAnim, -10.0f);
                    pg->actor.world.pos.x = GND_BOSSROOM_CENTER_X;
                    pg->actor.world.pos.y = GND_BOSSROOM_CENTER_Y + 83.0f;
                    pg->actor.world.pos.z = GND_BOSSROOM_CENTER_Z;
                    pg->actor.shape.rot.y = 0;
                    pg->work[GND_BODY_DECAY_INDEX] = 0;
                    Audio_PlayActorSound2(&pg->actor, NA_SE_EN_FANTOM_LAST);

                    // Move Player out of the center of the room
                    player->actor.world.pos.x = GND_BOSSROOM_CENTER_X - 200.0f;
                    player->actor.world.pos.z = GND_BOSSROOM_CENTER_Z;
                }
            }
            break;
        }
        case VB_NABOORU_KNUCKLE_DEATH_SCENE: {
            EnIk* ik = va_arg(args, EnIk*);
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.QuickBossDeaths"), IS_RANDO)) {
                // Because no CS in rando, we hide the death of the knuckle by spawning flames and kill the actor
                if ((ik->actor.colChkInfo.health <= 10)) {
                    s32 i;
                    Vec3f pos;
                    Vec3f sp7C = { 0.0f, 0.5f, 0.0f };
                    int flameAmount = 100;

                    for (i = flameAmount; i >= 0; i--) {
                        pos.x = ik->actor.world.pos.x + Rand_CenteredFloat(120.0f);
                        pos.z = ik->actor.world.pos.z + Rand_CenteredFloat(120.0f);
                        pos.y = ik->actor.world.pos.y + 20.0f + Rand_CenteredFloat(120.0f);
                        EffectSsDeadDb_Spawn(gPlayState, &pos, &sp7C, &sp7C, 100, 0, 255, 255, 255, 255, 0, 0, 255, 1,
                                             9, true);
                    }
                    Actor_Kill(&ik->actor);
                }
                *should = false;
            }
            break;
        }
        case VB_SHOULD_OSSAN_CANCEL: {
            // In shop B means cancel, prevent advancing as if mashing A
            if (CVarGetInteger(CVAR_ENHANCEMENT("SkipText"), 0)) {
                Input* input = va_arg(args, Input*);
                if (!*should)
                    *should = CHECK_BTN_ALL(input->cur.button, BTN_B);
            }
            break;
        }
        case VB_PLAY_SLOW_CHEST_CS: {
            if (CVarGetInteger(CVAR_ENHANCEMENT("FastChests"), 0)) {
                *should = false;
            }
            break;
        }
        case VB_SKIP_SCARECROWS_SONG: {
            if (gPlayState->msgCtx.msgMode == MSGMODE_OCARINA_PLAYING &&
                CVarGetInteger(CVAR_ENHANCEMENT("InstantScarecrow"), 0) && gSaveContext.scarecrowSpawnSongSet) {
                *should = true;
            }
            break;
        }
        default:
            break;
    }

    va_end(args);
}

static uint32_t enMa1UpdateHook = 0;
static uint32_t enMa1KillHook = 0;
static uint32_t enFuUpdateHook = 0;
static uint32_t enFuKillHook = 0;
static uint32_t enJjUpdateHook = 0;
static uint32_t enJjKillHook = 0;
static uint32_t bgSpot02UpdateHook = 0;
static uint32_t bgSpot02KillHook = 0;
static uint32_t bgSpot03UpdateHook = 0;
static uint32_t bgSpot03KillHook = 0;
static uint32_t enPoSistersUpdateHook = 0;
static uint32_t enPoSistersKillHook = 0;
static uint32_t enBigokutaUpdateHook = 0;
static uint32_t enBigokutaKillHook = 0;
void TimeSaverOnActorInitHandler(void* actorRef) {
    Actor* actor = static_cast<Actor*>(actorRef);

    if (actor->id == ACTOR_EN_MA1 && gPlayState->sceneNum == SCENE_LON_LON_RANCH) {
        enMa1UpdateHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorUpdate>([](void* innerActorRef) mutable {
                Actor* innerActor = static_cast<Actor*>(innerActorRef);
                if (innerActor->id == ACTOR_EN_MA1 &&
                    (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), IS_RANDO) || IS_RANDO)) {
                    EnMa1* enMa1 = static_cast<EnMa1*>(innerActorRef);
                    if (enMa1->actionFunc == EnMa1_StartTeachSong) {
                        enMa1->actionFunc = EnMa1_EndTeachSong;
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(enMa1UpdateHook);
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enMa1KillHook);
                        enMa1UpdateHook = 0;
                        enMa1KillHook = 0;
                        // They've already learned the song
                    } else if (enMa1->actionFunc == EnMa1_Idle) {
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(enMa1UpdateHook);
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enMa1KillHook);
                        enMa1UpdateHook = 0;
                        enMa1KillHook = 0;
                    }
                }
            });
        enMa1KillHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneInit>([](int16_t sceneNum) mutable {
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(enMa1UpdateHook);
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enMa1KillHook);
                enMa1UpdateHook = 0;
                enMa1KillHook = 0;
            });
    }

    if (actor->id == ACTOR_EN_FU) {
        enFuUpdateHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorUpdate>([](void* innerActorRef) mutable {
                Actor* innerActor = static_cast<Actor*>(innerActorRef);
                if (innerActor->id == ACTOR_EN_FU &&
                    (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), IS_RANDO) || IS_RANDO)) {
                    EnFu* enFu = static_cast<EnFu*>(innerActorRef);
                    if (enFu->actionFunc == EnFu_TeachSong) {
                        enFu->actionFunc = EnFu_EndTeachSong;
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(enFuUpdateHook);
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enFuKillHook);
                        enFuUpdateHook = 0;
                        enFuKillHook = 0;
                    }
                }
            });
        enFuKillHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneInit>([](int16_t sceneNum) mutable {
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(enFuUpdateHook);
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enFuKillHook);
                enFuUpdateHook = 0;
                enFuKillHook = 0;
            });
    }

    if (actor->id == ACTOR_EN_JJ) {
        enJjUpdateHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorUpdate>([](void* innerActorRef) mutable {
                Actor* innerActor = static_cast<Actor*>(innerActorRef);

                if (innerActor->id != ACTOR_EN_JJ || Flags_GetEventChkInf(EVENTCHKINF_OFFERED_FISH_TO_JABU_JABU)) {
                    return;
                }

                bool shouldOpen = IS_RANDO ? RAND_GET_OPTION(RSK_JABU_OPEN).Get()
                                           : CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipJabuJabuFish"), 0);
                if (!shouldOpen) {
                    return;
                }

                EnJj* enJj = static_cast<EnJj*>(innerActorRef);
                if (enJj->actionFunc == EnJj_WaitForFish) {
                    EnJj_SetupAction(enJj, EnJj_WaitToOpenMouth);
                    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(enJjUpdateHook);
                    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enJjKillHook);
                    enJjUpdateHook = 0;
                    enJjKillHook = 0;
                }
            });
        enJjKillHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneInit>([](int16_t sceneNum) mutable {
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(enJjUpdateHook);
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enJjKillHook);
                enJjUpdateHook = 0;
                enJjKillHook = 0;
            });
    }

    if (actor->id == ACTOR_EN_OWL && gPlayState->sceneNum == SCENE_ZORAS_RIVER &&
        CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SleepingWaterfall"), 0) == 2) {
        Actor_Kill(actor);
    }

    if (actor->id == ACTOR_BG_SPOT02_OBJECTS && actor->params == 2) {
        bgSpot02UpdateHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorUpdate>([](void* innerActorRef) mutable {
                Actor* innerActor = static_cast<Actor*>(innerActorRef);
                if (innerActor->id == ACTOR_BG_SPOT02_OBJECTS && innerActor->params == 2 &&
                    (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO))) {
                    BgSpot02Objects* bgSpot02 = static_cast<BgSpot02Objects*>(innerActorRef);
                    if (bgSpot02->actionFunc == func_808ACC34) {
                        bgSpot02->actionFunc = func_808AC908;
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(bgSpot02UpdateHook);
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(bgSpot02KillHook);
                        bgSpot02UpdateHook = 0;
                        bgSpot02KillHook = 0;
                    }
                }
            });
        bgSpot02KillHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneInit>([](int16_t sceneNum) mutable {
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(bgSpot02UpdateHook);
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(bgSpot02KillHook);
                bgSpot02UpdateHook = 0;
                bgSpot02KillHook = 0;
            });
    }

    if (actor->id == ACTOR_BG_SPOT03_TAKI) {
        bgSpot03UpdateHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorUpdate>([](void* innerActorRef) mutable {
                Actor* innerActor = static_cast<Actor*>(innerActorRef);

                if (innerActor->id != ACTOR_BG_SPOT03_TAKI) {
                    return;
                }

                bool shouldKeepOpen = RAND_GET_OPTION(RSK_SLEEPING_WATERFALL) && IS_RANDO;
                if (!shouldKeepOpen) {
                    int enhancement = CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SleepingWaterfall"), 0);
                    shouldKeepOpen =
                        (enhancement == 2 &&
                         ((CHECK_QUEST_ITEM(QUEST_SONG_LULLABY) && (INV_CONTENT(ITEM_OCARINA_TIME) != ITEM_NONE)))) ||
                        (enhancement == 1 && Flags_GetEventChkInf(EVENTCHKINF_OPENED_ZORAS_DOMAIN));
                }

                if (!shouldKeepOpen) {
                    return;
                }

                BgSpot03Taki* bgSpot03 = static_cast<BgSpot03Taki*>(innerActorRef);
                if (bgSpot03->actionFunc == BgSpot03Taki_HandleWaterfallState) {
                    bgSpot03->actionFunc = BgSpot03Taki_KeepOpen;
                    bgSpot03->state = WATERFALL_OPENED;
                    bgSpot03->openingAlpha = 0.0f;
                    Flags_SetSwitch(gPlayState, bgSpot03->switchFlag);
                    func_8003EBF8(gPlayState, &gPlayState->colCtx.dyna, bgSpot03->dyna.bgId);
                    BgSpot03Taki_ApplyOpeningAlpha(bgSpot03, 0);
                    BgSpot03Taki_ApplyOpeningAlpha(bgSpot03, 1);

                    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(bgSpot03UpdateHook);
                    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(bgSpot03KillHook);
                    bgSpot03UpdateHook = 0;
                    bgSpot03KillHook = 0;
                }
            });
        bgSpot03KillHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneInit>([](int16_t sceneNum) mutable {
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(bgSpot03UpdateHook);
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(bgSpot03KillHook);
                bgSpot03UpdateHook = 0;
                bgSpot03KillHook = 0;
            });
    }

    if (actor->id == ACTOR_EN_DNT_DEMO &&
        (IS_RANDO || CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO))) {
        EnDntDemo* enDntDemo = static_cast<EnDntDemo*>(actorRef);
        enDntDemo->actionFunc = EnDntDemo_JudgeSkipToReward;
    }

    // Forest Temple entrance cutscene
    // This is a bit of a hack, we can't effectively override the behavior of the torches
    // or poes from which the cutscene is triggered until we can have a "BeforeActorInit" hook.
    // So for now we're just going to set the flag before they get to the room the cutscene is in
    if (gPlayState->sceneNum == SCENE_FOREST_TEMPLE && actor->id == ACTOR_EN_DOOR &&
        !Flags_GetSwitch(gPlayState, 0x1B) && !Flags_GetSwitch(gPlayState, 0x1C)) {
        if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO) &&
            !CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.GlitchAiding"), 0)) {
            Flags_SetSwitch(gPlayState, 0x1B);
        }
    }

    // Forest Temple purple poe fight speedup
    if (actor->id == ACTOR_EN_PO_SISTERS && actor->params == 28) {
        enPoSistersUpdateHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorUpdate>([](void* innerActorRef) mutable {
                Actor* innerActor = static_cast<Actor*>(innerActorRef);
                if (innerActor->id == ACTOR_EN_PO_SISTERS && innerActor->params == 28 &&
                    (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO))) {
                    EnPoSisters* enPoSisters = static_cast<EnPoSisters*>(innerActorRef);
                    if (enPoSisters->actionFunc == func_80ADB338) {
                        enPoSisters->unk_19C = 0;
                    }
                }
            });
        enPoSistersKillHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneInit>([](int16_t sceneNum) mutable {
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(enPoSistersUpdateHook);
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enPoSistersKillHook);
                enPoSistersUpdateHook = 0;
                enPoSistersKillHook = 0;
            });
    }

    // Fire Temple Darunia cutscene
    if (actor->id == ACTOR_EN_DU && gPlayState->sceneNum == SCENE_FIRE_TEMPLE) {
        if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO) &&
            !CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.GlitchAiding"), 0)) {
            Flags_SetInfTable(INFTABLE_SPOKE_TO_DARUNIA_IN_FIRE_TEMPLE);
            Actor_Kill(actor);
        }
    }

    // Water Temple Ruto cutscene
    if (actor->id == ACTOR_EN_RU2 && gPlayState->sceneNum == SCENE_WATER_TEMPLE) {
        if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) {
            EnRu2* enRu2 = (EnRu2*)actor;
            EnRu2_SetEncounterSwitchFlag(enRu2, gPlayState);
            Actor_Kill(actor);
        }
    }

    // Prevent softlock from pre-battle early hit on Bigocto (possible by cutscene skip)
    if (actor->id == ACTOR_EN_BIGOKUTA) {
        enBigokutaUpdateHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorUpdate>([](void* innerActorRef) mutable {
                Actor* innerActor = static_cast<Actor*>(innerActorRef);
                if (innerActor->id == ACTOR_EN_BIGOKUTA &&
                    (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.OnePoint"), IS_RANDO))) {
                    EnBigokuta* enBigokuta = static_cast<EnBigokuta*>(innerActorRef);
                    if (enBigokuta->actor.params == 2) { // Platform already active
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(
                            enBigokutaUpdateHook);
                        GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enBigokutaKillHook);
                        enBigokutaUpdateHook = 0;
                        enBigokutaKillHook = 0;
                        // Possible action functions after taken damage
                    } else if (enBigokuta->actionFunc == func_809BE058 || enBigokuta->actionFunc == func_809BDF34 ||
                               enBigokuta->actionFunc == func_809BE180) {
                        enBigokuta->actor.home.pos.y = enBigokuta->actor.world.pos.y = -1025.0f;
                        Actor_ChangeCategory(gPlayState, &gPlayState->actorCtx, &enBigokuta->actor, ACTORCAT_ENEMY);
                        enBigokuta->actor.params = 2; // Activate platform
                    }
                }
            });
        enBigokutaKillHook =
            GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneInit>([](int16_t sceneNum) mutable {
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(enBigokutaUpdateHook);
                GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(enBigokutaKillHook);
                enBigokutaUpdateHook = 0;
                enBigokutaKillHook = 0;
            });
    }
}

void TimeSaverOnSceneInitHandler(int16_t sceneNum) {
    switch (sceneNum) {
        case SCENE_HYRULE_CASTLE:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO) &&
                !Flags_GetInfTable(INFTABLE_ENTERED_HYRULE_CASTLE)) {
                Flags_SetInfTable(INFTABLE_ENTERED_HYRULE_CASTLE);
                Flags_SetInfTable(INFTABLE_MET_CHILD_MALON_AT_CASTLE_OR_MARKET);
                Flags_SetEventChkInf(EVENTCHKINF_SPOKE_TO_CHILD_MALON_AT_CASTLE_OR_MARKET);
            }
            break;
        case SCENE_LON_LON_RANCH:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO) &&
                GameInteractor_Should(VB_MALON_RETURN_FROM_CASTLE,
                                      Flags_GetEventChkInf(EVENTCHKINF_TALON_RETURNED_FROM_CASTLE)) &&
                (!IS_RANDO || !RAND_GET_OPTION(RSK_SHUFFLE_SPEAK) ||
                 Flags_GetRandomizerInf(RAND_INF_CAN_SPEAK_HYLIAN))) {
                Flags_SetEventChkInf(EVENTCHKINF_SPOKE_TO_CHILD_MALON_AT_RANCH);
                Flags_SetInfTable(INFTABLE_CHILD_MALON_SAID_EPONA_WAS_AFRAID_OF_YOU);
                Flags_SetEventChkInf(EVENTCHKINF_INVITED_TO_SING_WITH_CHILD_MALON);
            }
            break;
        case SCENE_DEKU_TREE_BOSS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_GOHMA_BATTLE)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_GOHMA_BATTLE);
                }
            }
            break;
        case SCENE_DODONGOS_CAVERN_BOSS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_KING_DODONGO_BATTLE)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_KING_DODONGO_BATTLE);
                }
            }
            break;
        case SCENE_JABU_JABU_BOSS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_BARINA_BATTLE)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_BARINA_BATTLE);
                }
            }
            break;
        case SCENE_FOREST_TEMPLE_BOSS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_PHANTOM_GANON_BATTLE)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_PHANTOM_GANON_BATTLE);
                }
            }
            break;
        case SCENE_FIRE_TEMPLE_BOSS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_VOLVAGIA_BATTLE)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_VOLVAGIA_BATTLE);
                }
            }
            break;
        case SCENE_WATER_TEMPLE_BOSS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_MORPHA_BATTLE)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_MORPHA_BATTLE);
                }
            }
            break;
        case SCENE_SPIRIT_TEMPLE_BOSS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_TWINROVA_BATTLE)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_TWINROVA_BATTLE);
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_NABOORU_BATTLE);
                    Flags_SetEventChkInf(EVENTCHKINF_NABOORU_ORDERED_TO_FIGHT_BY_TWINROVA);
                }
            }
            break;
        case SCENE_SHADOW_TEMPLE_BOSS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_BONGO_BONGO_BATTLE)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_BONGO_BONGO_BATTLE);
                }
            }
            break;
        case SCENE_GANONDORF_BOSS:
            if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
                if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE)) {
                    Flags_SetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE);
                }
            }
            break;
    }
}

static GetItemEntry vanillaQueuedItemEntry = GET_ITEM_NONE;

extern void TimeSaverQueueItem(RandomizerGet randoGet) {
    vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(randoGet).GetGIEntry_Copy();
}

void TimeSaverOnFlagSetHandler(int16_t flagType, int16_t flag) {
    // Do nothing when in a boss rush
    if (IS_BOSS_RUSH) {
        return;
    }

    if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), 0)) {
        switch (flagType) {
            case FLAG_EVENT_CHECK_INF:
                switch (flag) {
                    case EVENTCHKINF_SPOKE_TO_SARIA_ON_BRIDGE:
                        TimeSaverQueueItem(RG_FAIRY_OCARINA);
                        break;
                    case EVENTCHKINF_OBTAINED_KOKIRI_EMERALD_DEKU_TREE_DEAD:
                        TimeSaverQueueItem(RG_KOKIRI_EMERALD);
                        break;
                    case EVENTCHKINF_USED_DODONGOS_CAVERN_BLUE_WARP:
                        TimeSaverQueueItem(RG_GORON_RUBY);
                        break;
                    case EVENTCHKINF_USED_JABU_JABUS_BELLY_BLUE_WARP:
                        TimeSaverQueueItem(RG_ZORA_SAPPHIRE);
                        break;
                    case EVENTCHKINF_USED_FOREST_TEMPLE_BLUE_WARP:
                        TimeSaverQueueItem(RG_FOREST_MEDALLION);
                        break;
                    case EVENTCHKINF_USED_FIRE_TEMPLE_BLUE_WARP:
                        TimeSaverQueueItem(RG_FIRE_MEDALLION);
                        break;
                    case EVENTCHKINF_USED_WATER_TEMPLE_BLUE_WARP:
                        TimeSaverQueueItem(RG_WATER_MEDALLION);
                        break;
                    case EVENTCHKINF_RETURNED_TO_TEMPLE_OF_TIME_WITH_ALL_MEDALLIONS:
                        TimeSaverQueueItem(RG_LIGHT_ARROWS);
                        break;
                    case EVENTCHKINF_TIME_TRAVELED_TO_ADULT:
                        TimeSaverQueueItem(RG_LIGHT_MEDALLION);
                        break;
                }
                break;
            case FLAG_RANDOMIZER_INF:
                switch (flag) {
                    case RAND_INF_DUNGEONS_DONE_SHADOW_TEMPLE:
                        TimeSaverQueueItem(RG_SHADOW_MEDALLION);
                        break;
                    case RAND_INF_DUNGEONS_DONE_SPIRIT_TEMPLE:
                        TimeSaverQueueItem(RG_SPIRIT_MEDALLION);
                        break;
                }
                break;
        }
    }

    if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), 0)) {
        switch (flagType) {
            case FLAG_RANDOMIZER_INF:
                switch (flag) {
                    case RAND_INF_ZF_GREAT_FAIRY_REWARD:
                        TimeSaverQueueItem(RG_FARORES_WIND);
                        break;
                    case RAND_INF_HC_GREAT_FAIRY_REWARD:
                        TimeSaverQueueItem(RG_DINS_FIRE);
                        break;
                    case RAND_INF_COLOSSUS_GREAT_FAIRY_REWARD:
                        TimeSaverQueueItem(RG_NAYRUS_LOVE);
                        break;
                    case RAND_INF_DMT_GREAT_FAIRY_REWARD:
                        TimeSaverQueueItem(RG_MAGIC_SINGLE);
                        break;
                    case RAND_INF_DMC_GREAT_FAIRY_REWARD:
                        TimeSaverQueueItem(RG_MAGIC_DOUBLE);
                        break;
                    case RAND_INF_OGC_GREAT_FAIRY_REWARD:
                        TimeSaverQueueItem(RG_DOUBLE_DEFENSE);
                        break;
                }
                break;
            case FLAG_ITEM_GET_INF:
                switch (flag) {
                    case ITEMGETINF_OBTAINED_STICK_UPGRADE_FROM_STAGE: {
                        TimeSaverQueueItem(CUR_UPG_VALUE(UPG_STICKS) == 2 ? RG_DEKU_STICK_CAPACITY_30
                                                                          : RG_DEKU_STICK_CAPACITY_20);
                        break;
                    }
                    case ITEMGETINF_OBTAINED_NUT_UPGRADE_FROM_STAGE: {
                        TimeSaverQueueItem(CUR_UPG_VALUE(UPG_NUTS) == 2 ? RG_DEKU_NUT_CAPACITY_40
                                                                        : RG_DEKU_NUT_CAPACITY_30);
                        break;
                    }
                }
                break;
        }
    }

    if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), 0)) {
        switch (flagType) {
            case FLAG_EVENT_CHECK_INF:
                switch (flag) {
                    case EVENTCHKINF_LEARNED_ZELDAS_LULLABY:
                        TimeSaverQueueItem(RG_ZELDAS_LULLABY);
                        break;
                    case EVENTCHKINF_LEARNED_MINUET_OF_FOREST:
                        TimeSaverQueueItem(RG_MINUET_OF_FOREST);
                        break;
                    case EVENTCHKINF_LEARNED_BOLERO_OF_FIRE:
                        TimeSaverQueueItem(RG_BOLERO_OF_FIRE);
                        break;
                    case EVENTCHKINF_LEARNED_SERENADE_OF_WATER:
                        TimeSaverQueueItem(RG_SERENADE_OF_WATER);
                        break;
                    case EVENTCHKINF_LEARNED_REQUIEM_OF_SPIRIT:
                        TimeSaverQueueItem(RG_REQUIEM_OF_SPIRIT);
                        break;
                    case EVENTCHKINF_BONGO_BONGO_ESCAPED_FROM_WELL:
                        TimeSaverQueueItem(RG_NOCTURNE_OF_SHADOW);
                        break;
                    case EVENTCHKINF_LEARNED_PRELUDE_OF_LIGHT:
                        TimeSaverQueueItem(RG_PRELUDE_OF_LIGHT);
                        break;
                    case EVENTCHKINF_LEARNED_SARIAS_SONG:
                        TimeSaverQueueItem(RG_SARIAS_SONG);
                        break;
                    case EVENTCHKINF_LEARNED_SONG_OF_TIME:
                        TimeSaverQueueItem(RG_SONG_OF_TIME);
                        break;
                    case EVENTCHKINF_LEARNED_SONG_OF_STORMS:
                        TimeSaverQueueItem(RG_SONG_OF_STORMS);
                        break;
                    case EVENTCHKINF_LEARNED_SUNS_SONG:
                        TimeSaverQueueItem(RG_SUNS_SONG);
                        break;
                }
                break;
            case FLAG_RANDOMIZER_INF:
                switch (flag) {
                    case RAND_INF_LEARNED_EPONA_SONG:
                        TimeSaverQueueItem(RG_EPONAS_SONG);
                        break;
                }
                break;
        }
    }
}

void TimeSaverOnPlayerUpdateHandler() {
    if (vanillaQueuedItemEntry.itemId == ITEM_NONE)
        return;

    Player* player = GET_PLAYER(gPlayState);
    if (player == NULL || Player_InBlockingCsMode(gPlayState, player) ||
        player->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS || player->stateFlags1 & PLAYER_STATE1_GETTING_ITEM ||
        player->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) {
        return;
    }

    SPDLOG_INFO("Attempting to give Item: mod {} item {}", vanillaQueuedItemEntry.modIndex,
                vanillaQueuedItemEntry.itemId);
    GiveItemEntryWithoutActor(gPlayState, vanillaQueuedItemEntry);
    if (player->stateFlags1 & PLAYER_STATE1_IN_WATER) {
        // Allow the player to receive the item while swimming
        player->stateFlags2 |= PLAYER_STATE2_UNDERWATER;
        Player_ActionHandler_2(player, gPlayState);
    }
}

void TimeSaverOnItemReceiveHandler(GetItemEntry receivedItemEntry) {
    if (vanillaQueuedItemEntry.itemId == ITEM_NONE)
        return;

    if (vanillaQueuedItemEntry.modIndex == receivedItemEntry.modIndex &&
        vanillaQueuedItemEntry.itemId == receivedItemEntry.itemId) {
        SPDLOG_INFO("Item received: mod {} item {}", receivedItemEntry.modIndex, receivedItemEntry.itemId);
        vanillaQueuedItemEntry = GET_ITEM_NONE;
    }
}

static void TimeSaverRegisterHooks() {
    COND_HOOK(OnSceneInit,
              CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), IS_RANDO) ||
                  CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO),
              TimeSaverOnSceneInitHandler);
    COND_HOOK(OnVanillaBehavior, true, TimeSaverOnVanillaBehaviorHandler);
    COND_HOOK(OnActorInit, true, TimeSaverOnActorInitHandler);
    COND_HOOK(OnSceneInit, true, [](int16_t sceneNum) { successChimeCooldown = 0; });

    // item queue for use outside rando, rando has its own queue
    COND_HOOK(OnLoadGame, !IS_RANDO, [](int32_t fileNum) { vanillaQueuedItemEntry = GET_ITEM_NONE; });
    COND_HOOK(OnItemReceive, !IS_RANDO, TimeSaverOnItemReceiveHandler);
    COND_HOOK(OnPlayerUpdate, !IS_RANDO, TimeSaverOnPlayerUpdateHandler);
    COND_HOOK(OnFlagSet,
              !IS_RANDO && (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), 0) ||
                            CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"), 0) ||
                            CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong"), 0)),
              TimeSaverOnFlagSetHandler);
}

static RegisterShipInitFunc initFunc_RegisterHooks(TimeSaverRegisterHooks,
                                                   { "IS_RANDO", CVAR_ENHANCEMENT("TimeSavers.SkipMiscInteractions"),
                                                     CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"),
                                                     CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"),
                                                     CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong") });
