From 1876435e9823309da9a55ac26ec092d1915aabc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:59:14 +0000 Subject: [PATCH 01/30] Fix skulltula hints ending text too soon (#6516) --- soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp b/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp index 56821bf609..d62c2e0f14 100644 --- a/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp @@ -140,7 +140,7 @@ void BuildSkulltulaPeopleMessage(uint16_t* textId, bool* loadFromMessageTable) { "et j'aurai quelque chose à te donner! [[color]]([[1]])%w"); msg.InsertNumber(count); msg.Replace("[[color]]", item.GetColor()); - msg.InsertNames({ item.GetHint().GetHintMessage().GetForCurrentLanguage() }); + msg.InsertNames({ item.GetHint().GetHintMessage() }); msg.AutoFormat(); msg.LoadIntoFont(); *loadFromMessageTable = false; @@ -158,7 +158,7 @@ void Build100SkullsHintMessage(uint16_t* textId, bool* loadFromMessageTable) { Rando::Item& item = Rando::StaticData::RetrieveItem(RAND_GET_ITEM_LOC(RC_KAK_100_GOLD_SKULLTULA_REWARD)->GetPlacedRandomizerGet()); msg.Replace("[[color]]", item.GetColor()); - msg.InsertNames({ item.GetHint().GetHintMessage().GetForCurrentLanguage() }); + msg.InsertNames({ item.GetHint().GetHintMessage() }); msg.AutoFormat(); msg.LoadIntoFont(); *loadFromMessageTable = false; From 412b60a02f366d9e3ac4e3bf73da07991a33ba6d Mon Sep 17 00:00:00 2001 From: A Green Spoon <121978037+A-Green-Spoon@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:50:55 +0900 Subject: [PATCH 02/30] Fix Duplicate Reticles in Anchor (#6520) --- soh/soh/Enhancements/Items/AdditionalReticles.cpp | 2 +- .../game-interactor/vanilla-behavior/GIVanillaBehavior.h | 5 +++-- soh/src/code/z_player_lib.c | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/soh/soh/Enhancements/Items/AdditionalReticles.cpp b/soh/soh/Enhancements/Items/AdditionalReticles.cpp index 2cb3b975af..9b8b5cfcaf 100644 --- a/soh/soh/Enhancements/Items/AdditionalReticles.cpp +++ b/soh/soh/Enhancements/Items/AdditionalReticles.cpp @@ -36,7 +36,7 @@ void RegisterAdditionalReticles() { bool shouldRegister = CVAR_BOW_RETICLE_VALUE || CVAR_BOOMERANG_RETICLE_VALUE; COND_VB_SHOULD(VB_DRAW_ADDITIONAL_RETICLES, shouldRegister, { - Player* player = GET_PLAYER(gPlayState); + Player* player = va_arg(args, Player*); Actor* heldActor = player->heldActor; if (CVAR_BOW_RETICLE_VALUE && ((player->heldItemAction >= PLAYER_IA_BOW && player->heldItemAction <= PLAYER_IA_BOW_LIGHT) || diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index 4e13fdf33e..09ce04580a 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -540,10 +540,11 @@ typedef enum { // #### `result` // ```c - // true + // (this->heldItemAction == PLAYER_IA_HOOKSHOT) || + // (this->heldItemAction == PLAYER_IA_LONGSHOT) // ``` // #### `args` - // - None + // - '*Player' VB_DRAW_ADDITIONAL_RETICLES, // #### `result` diff --git a/soh/src/code/z_player_lib.c b/soh/src/code/z_player_lib.c index 34750ed2cc..83dfd7608c 100644 --- a/soh/src/code/z_player_lib.c +++ b/soh/src/code/z_player_lib.c @@ -1906,8 +1906,10 @@ void Player_PostLimbDrawGameplay(PlayState* play, s32 limbIndex, Gfx** dList, Ve } if (this->actor.scale.y >= 0.0f) { - if (GameInteractor_Should(VB_DRAW_ADDITIONAL_RETICLES, (this->heldItemAction == PLAYER_IA_HOOKSHOT) || - (this->heldItemAction == PLAYER_IA_LONGSHOT))) { + if (GameInteractor_Should(VB_DRAW_ADDITIONAL_RETICLES, + (this->heldItemAction == PLAYER_IA_HOOKSHOT) || + (this->heldItemAction == PLAYER_IA_LONGSHOT), + this)) { Matrix_MultVec3f(&D_80126184, &this->unk_3C8); if (heldActor != NULL) { From 9c321862ca989bb2b283733a84baa4eb77d6637f Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 16 Apr 2026 21:30:28 -0400 Subject: [PATCH 03/30] Fix Rate Limited Success Chime (#6512) --- soh/soh/Enhancements/timesaver_hook_handlers.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/soh/soh/Enhancements/timesaver_hook_handlers.cpp b/soh/soh/Enhancements/timesaver_hook_handlers.cpp index be043b6da3..b609224349 100644 --- a/soh/soh/Enhancements/timesaver_hook_handlers.cpp +++ b/soh/soh/Enhancements/timesaver_hook_handlers.cpp @@ -1415,12 +1415,10 @@ static void TimeSaverRegisterHooks() { 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; - successChimeCooldown = 0; - }); + 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, From 072838613ad1430f667447d05ea84cd9ec81d963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Fri, 17 Apr 2026 03:53:24 +0000 Subject: [PATCH 04/30] Fix kak bazaar items having articles (#6522) IsShop didn't include SCENE_TEST01, but that's used as placeholder for kak bazaar vs market bazaar --- soh/soh/Enhancements/randomizer/location.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/location.cpp b/soh/soh/Enhancements/randomizer/location.cpp index 6de708c013..cb2d6dbe5b 100644 --- a/soh/soh/Enhancements/randomizer/location.cpp +++ b/soh/soh/Enhancements/randomizer/location.cpp @@ -64,7 +64,7 @@ bool Rando::Location::IsOverworld() const { } bool Rando::Location::IsShop() const { - return scene >= SCENE_BAZAAR && scene <= SCENE_BOMBCHU_SHOP; + return (scene >= SCENE_BAZAAR && scene <= SCENE_BOMBCHU_SHOP) || scene == SCENE_TEST01; } bool Rando::Location::IsVanillaCompletion() const { From 256ab01630b4d71a64485456e08990a0b6271f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Fri, 17 Apr 2026 04:31:02 +0000 Subject: [PATCH 05/30] Fishing: don't say default 6/7 when minimum reduced by enhancement (#6523) --- soh/soh/Enhancements/Fishing.cpp | 21 +++++++++++++++++++ .../overlays/actors/ovl_Fishing/z_fishing.c | 6 ++++++ 2 files changed, 27 insertions(+) create mode 100644 soh/soh/Enhancements/Fishing.cpp diff --git a/soh/soh/Enhancements/Fishing.cpp b/soh/soh/Enhancements/Fishing.cpp new file mode 100644 index 0000000000..109533f984 --- /dev/null +++ b/soh/soh/Enhancements/Fishing.cpp @@ -0,0 +1,21 @@ +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include + +f32 Fishing_GetMinimumRequiredScore(); +} + +void BuildFishingMessage(uint16_t* textId, bool* loadFromMessageTable) { + if (gSaveContext.minigameScore == 0) { + gSaveContext.minigameScore = Fishing_GetMinimumRequiredScore(); + } +} + +void RegisterFishingMessages() { + COND_ID_HOOK(OnOpenText, 0x40AE, CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0), BuildFishingMessage); + COND_ID_HOOK(OnOpenText, 0x4080, CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0), BuildFishingMessage); +} + +static RegisterShipInitFunc initFunc(RegisterFishingMessages, { CVAR_ENHANCEMENT("CustomizeFishing") }); diff --git a/soh/src/overlays/actors/ovl_Fishing/z_fishing.c b/soh/src/overlays/actors/ovl_Fishing/z_fishing.c index 77e16a147c..41aee59654 100644 --- a/soh/src/overlays/actors/ovl_Fishing/z_fishing.c +++ b/soh/src/overlays/actors/ovl_Fishing/z_fishing.c @@ -432,6 +432,8 @@ static FishingEffect sFishingEffects[FISHING_EFFECT_COUNT]; static Vec3f sStreamSoundProjectedPos; static s16 sFishOnHandParams; +f32 Fishing_GetMinimumRequiredScore(); + u8 AllHyruleLoaches() { return CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0) && CVarGetInteger(CVAR_ENHANCEMENT("AllHyruleLoaches"), 0); @@ -904,12 +906,16 @@ void Fishing_Init(Actor* thisx, PlayState* play2) { if (sLinkAge == LINK_AGE_CHILD) { if ((HIGH_SCORE(HS_FISHING) & HS_FISH_LENGTH_CHILD) != 0) { sFishingRecordLength = HIGH_SCORE(HS_FISHING) & HS_FISH_LENGTH_CHILD; + } else if (CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0)) { + sFishingRecordLength = Fishing_GetMinimumRequiredScore(); } else { sFishingRecordLength = 40.0f; // 6 lbs } } else { if ((HIGH_SCORE(HS_FISHING) & HS_FISH_LENGTH_ADULT) != 0) { sFishingRecordLength = (HIGH_SCORE(HS_FISHING) & HS_FISH_LENGTH_ADULT) >> 0x18; + } else if (CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0)) { + sFishingRecordLength = Fishing_GetMinimumRequiredScore(); } else { sFishingRecordLength = 45.0f; // 7 lbs } From 39dcc0a73c561850279be1d6d1ff548cd443ee6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:21:29 +0000 Subject: [PATCH 06/30] Triforce Hunt: drain queue before credits (#6519) --- soh/soh/Enhancements/randomizer/hook_handlers.cpp | 10 ++++++++-- soh/soh/Enhancements/randomizer/randomizer.cpp | 8 +------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index ee2aecd551..d6ff0dc5be 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -2629,8 +2629,14 @@ void RandomizerOnPlayerUpdateHandler() { } if (!GameInteractor::IsGameplayPaused() && RAND_GET_OPTION(RSK_TRIFORCE_HUNT).IsNot(RO_TRIFORCE_HUNT_OFF)) { - // Warp to credits - if (GameInteractor::State::TriforceHuntCreditsWarpActive) { + // Warp to credits once item queue has drained to avoid losing queued items + if (GameInteractor::State::TriforceHuntCreditsWarpActive && randomizerQueuedChecks.empty() && + randomizerQueuedCheck == RC_UNKNOWN_CHECK) { + gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] = + static_cast(GAMEPLAYSTAT_TOTAL_TIME); + gSaveContext.ship.stats.gameComplete = 1; + Play_PerformSave(gPlayState); + Notification::Emit({ .message = "Game autosaved" }); gPlayState->nextEntranceIndex = ENTR_CHAMBER_OF_THE_SAGES_0; gSaveContext.nextCutsceneIndex = 0xFFF2; gPlayState->transitionTrigger = TRANS_TRIGGER_START; diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 924d22ab66..71f88e67d3 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -3902,13 +3902,7 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { if (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT) == RO_TRIFORCE_HUNT_WIN) { - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] = - static_cast(GAMEPLAYSTAT_TOTAL_TIME); - gSaveContext.ship.stats.gameComplete = 1; - Play_PerformSave(play); - Notification::Emit({ - .message = "Game autosaved", - }); + // Save and warp are deferred until item queue drains GameInteractor_SetTriforceHuntCreditsWarpActive(true); } } From 3be7eff02c7c85dab4127aeb0d6379398e3496b1 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 19 Apr 2026 11:04:13 -0400 Subject: [PATCH 07/30] Arrow cycle should check for sufficient magic (#6532) --- soh/soh/Enhancements/ArrowCycle.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/soh/soh/Enhancements/ArrowCycle.cpp b/soh/soh/Enhancements/ArrowCycle.cpp index 202f24c4e5..f8b9a84fb5 100644 --- a/soh/soh/Enhancements/ArrowCycle.cpp +++ b/soh/soh/Enhancements/ArrowCycle.cpp @@ -43,11 +43,11 @@ static bool HasArrowType(PlayerItemAction itemAction) { case PLAYER_IA_BOW: return true; case PLAYER_IA_BOW_FIRE: - return INV_CONTENT(ITEM_ARROW_FIRE) == ITEM_ARROW_FIRE; + return INV_CONTENT(ITEM_ARROW_FIRE) == ITEM_ARROW_FIRE && gSaveContext.magic >= sMagicArrowCosts[0]; case PLAYER_IA_BOW_ICE: - return INV_CONTENT(ITEM_ARROW_ICE) == ITEM_ARROW_ICE; + return INV_CONTENT(ITEM_ARROW_ICE) == ITEM_ARROW_ICE && gSaveContext.magic >= sMagicArrowCosts[1]; case PLAYER_IA_BOW_LIGHT: - return INV_CONTENT(ITEM_ARROW_LIGHT) == ITEM_ARROW_LIGHT; + return INV_CONTENT(ITEM_ARROW_LIGHT) == ITEM_ARROW_LIGHT && gSaveContext.magic >= sMagicArrowCosts[2]; default: return false; } From 3b65eaa4ef7c24611e2884210191c4519d494db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:50:11 +0000 Subject: [PATCH 08/30] Fix cutscene skips causing credits to spawn player in Lake Hylia (#6534) SkipBlueWarp was intercepting credits. Disable during GAMEMODE_END_CREDITS --- .../TimeSavers/SkipCutscene/Story/SkipBlueWarp.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipBlueWarp.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipBlueWarp.cpp index 0ade592d64..1bbbc3cd01 100644 --- a/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipBlueWarp.cpp +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipBlueWarp.cpp @@ -86,8 +86,7 @@ void RegisterShouldPlayBlueWarp() { * should also account for the difference between your first and following visits to the blue warp. */ REGISTER_VB_SHOULD(VB_PLAY_TRANSITION_CS, { - // Do nothing when in a boss rush - if (IS_BOSS_RUSH) { + if (IS_BOSS_RUSH || gSaveContext.gameMode == GAMEMODE_END_CREDITS) { return; } From bf37645d723254abc9eade79f6f4dc2d4b4b1c80 Mon Sep 17 00:00:00 2001 From: Reppan <72985260+Jepvid@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:18:08 +0200 Subject: [PATCH 09/30] Fix mirror shield color editor (#6542) Oversight on my end that customequipment.cpp unloaded and reloaded assets abit to aggressive. Adds a guard to make sure to only unload if custom asset is used and to not unload then reload vanilla asset. Have tested it with fados customequipment and confirmed that mirrorshield is working as intended. --- soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp | 1 + soh/soh/Enhancements/customequipment.cpp | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp index a02a658877..077732809b 100644 --- a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp +++ b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp @@ -2707,6 +2707,7 @@ void RegisterCosmeticHooks() { [](s16 sceneNum) { CosmeticsEditor_AutoRandomizeAll(); }); COND_HOOK(OnGameFrameUpdate, true, CosmeticsUpdateTick); + COND_HOOK(OnAssetAltChange, true, []() { ApplyOrResetCustomGfxPatches(true); }); } void RegisterCosmeticWidgets() { diff --git a/soh/soh/Enhancements/customequipment.cpp b/soh/soh/Enhancements/customequipment.cpp index 1ce4c79048..06e6453802 100644 --- a/soh/soh/Enhancements/customequipment.cpp +++ b/soh/soh/Enhancements/customequipment.cpp @@ -121,6 +121,8 @@ static bool IsDummyPlayer(const Player* player) { return player != nullptr && player->actor.update == DummyPlayer_Update; } +static bool sPrevAltAssetsEnabled = false; + void PatchOrUnpatch(const char* resource, const char* gfx, const char* dlist1, const char* dlist2, const char* dlist3, const char* alternateDL) { if (resource == NULL || gfx == NULL || dlist1 == NULL || dlist2 == NULL) { @@ -128,6 +130,7 @@ void PatchOrUnpatch(const char* resource, const char* gfx, const char* dlist1, c } const bool altAssetsRuntime = ResourceMgr_IsAltAssetsEnabled(); + const bool altAssetsChanged = (altAssetsRuntime != sPrevAltAssetsEnabled); if (!altAssetsRuntime) { // Alt assets are off; ensure any prior patches using these names are reverted. @@ -136,11 +139,16 @@ void PatchOrUnpatch(const char* resource, const char* gfx, const char* dlist1, c if (dlist3 != NULL) { ResourceMgr_UnpatchGfxByName(resource, dlist3); } - // Drop any cached version of the resource so it reloads clean (unpatched) next use. - ResourceMgr_UnloadResource(resource); + if (altAssetsChanged) { + ResourceMgr_UnloadResource(resource); + } return; } + if (altAssetsChanged) { + ResourceMgr_UnloadResource(resource); + } + if (!ResourceGetIsCustomByName(gfx)) { return; } @@ -510,4 +518,6 @@ void UpdatePatchCustomEquipmentDlists() { } ApplyCommonEquipmentPatches(); + + sPrevAltAssetsEnabled = ResourceMgr_IsAltAssetsEnabled(); } From 719bb87eda32de53cba570c12e7397e37b5e6161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:30:33 +0000 Subject: [PATCH 10/30] add hint text for sun song fairies in spirit temple (#6544) --- .../3drando/hint_list/hint_list_exclude_dungeon.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_dungeon.cpp b/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_dungeon.cpp index aa375716f6..c77441ee58 100644 --- a/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_dungeon.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_dungeon.cpp @@ -1318,6 +1318,14 @@ void StaticData::HintTable_Init_Exclude_Dungeon() { /*german*/ "Man erzählt sich, daß sich bewacht von einem #Ring der Flammen#, im Geistertempel #[[1]]# |befände|befänden|.", /*french*/ "Selon moi, protégé par un #cercle de flammes# dans le Temple de l'Esprit se trouve #[[1]]#.", {QM_RED, QM_GREEN})); + hintTextTable[RHT_SPIRIT_TEMPLE_BOULDER_ROOM_SUN_FAIRY] = HintText(CustomMessage("They say that #calling the sun past rolling boulders in Spirit Temple# reveals #[[1]]#.", + /*german*/ TODO_TRANSLATE, + /*french*/ TODO_TRANSLATE, {QM_RED, QM_GREEN})); + + hintTextTable[RHT_SPIRIT_TEMPLE_ARMOS_ROOM_SUN_FAIRY] = HintText(CustomMessage("They say that #calling the sun in the spotlight by statues# reveals #[[1]]#.", + /*german*/ TODO_TRANSLATE, + /*french*/ TODO_TRANSLATE, {QM_RED, QM_GREEN})); + hintTextTable[RHT_CRATE_SPIRIT_TEMPLE] = HintText(CustomMessage("They say that a #crate in Spirit Temple# contains #[[1]]#.", /*german*/ "Man erzählt sich, daß eine #Kiste im Geistertempel# #[[1]]# enthielte.", /*french*/ "Selon moi, une #caisse dans le Temple de l'Esprit# contient #[[1]]#.", {QM_RED, QM_GREEN})); From 69681e608f7597f13285bdb4cc11e804439ea373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:02:33 +0000 Subject: [PATCH 11/30] Fix swimvoid in grottos to just respawn in grotto (#6529) --- soh/soh/Enhancements/randomizer/hook_handlers.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index d6ff0dc5be..9ac629fe8f 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -2616,7 +2616,16 @@ void RandomizerOnPlayerUpdateHandler() { gSaveContext.respawn[RESPAWN_MODE_DOWN].yaw = respawn->second.yaw; } - Play_TriggerVoidOut(gPlayState); + if (gPlayState->sceneNum == SCENE_GROTTOS) { + // RESPAWN_MODE_DOWN isn't refreshed on grotto entry, reload grotto instead + gPlayState->nextEntranceIndex = gSaveContext.entranceIndex; + gPlayState->transitionTrigger = TRANS_TRIGGER_START; + gPlayState->transitionType = TRANS_TYPE_FADE_BLACK; + gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK; + gSaveContext.respawnFlag = 0; + } else { + Play_TriggerVoidOut(gPlayState); + } } } From 3221d8a988513618986423f36c8c02c8f4dc2435 Mon Sep 17 00:00:00 2001 From: Pepper0ni <93387759+Pepper0ni@users.noreply.github.com> Date: Wed, 22 Apr 2026 00:04:37 +0100 Subject: [PATCH 12/30] Fix bean fairies + start with beans generation (#6548) --- soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp b/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp index e7f875bf1c..ded9523a1e 100644 --- a/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp @@ -126,6 +126,7 @@ void GenerateStartingInventory() { AddItemToInventory(RG_NOCTURNE_OF_SHADOW, ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW) ? 1 : 0); AddItemToInventory(RG_PRELUDE_OF_LIGHT, ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT) ? 1 : 0); AddItemToInventory(RG_KOKIRI_SWORD, ctx->GetOption(RSK_STARTING_KOKIRI_SWORD) ? 1 : 0); + AddItemToInventory(RG_MAGIC_BEAN_PACK, ctx->GetOption(RSK_STARTING_BEANS) ? 1 : 0); // if (ProgressiveGoronSword) { // AddItemToInventory(RG_PROGRESSIVE_GORONSWORD, StartingBiggoronSword.Value()); // } else { From 806398a65b04c994d6472eb482e0de400d48cc72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:34:26 +0000 Subject: [PATCH 13/30] Fix OGC great fairy reward in vanilla with skip misc interactions when fish is not obtainable (#6556) Item_CheckObtainability should only be called with MOD_NONE GI For RG_DOUBLE_DEFENSE that became ITEM_FISH. Nonsense ensued To reproduce issue, create debug save & go straight to OGC great fairy with only magic/ocarina/lullaby --- soh/soh/Enhancements/randomizer/item.h | 1 - .../Enhancements/timesaver_hook_handlers.cpp | 62 +++++++++---------- soh/src/code/z_actor.c | 15 +++-- soh/src/code/z_player_lib.c | 1 - .../actors/ovl_player_actor/z_player.c | 9 +-- 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/item.h b/soh/soh/Enhancements/randomizer/item.h index ab4b3c1194..f484159102 100644 --- a/soh/soh/Enhancements/randomizer/item.h +++ b/soh/soh/Enhancements/randomizer/item.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include "3drando/text.hpp" diff --git a/soh/soh/Enhancements/timesaver_hook_handlers.cpp b/soh/soh/Enhancements/timesaver_hook_handlers.cpp index b609224349..204de040fa 100644 --- a/soh/soh/Enhancements/timesaver_hook_handlers.cpp +++ b/soh/soh/Enhancements/timesaver_hook_handlers.cpp @@ -24,7 +24,6 @@ extern "C" { #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_Spot06_Objects/z_bg_spot06_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" @@ -1241,41 +1240,41 @@ void TimeSaverOnFlagSetHandler(int16_t flagType, int16_t flag) { case FLAG_EVENT_CHECK_INF: switch (flag) { case EVENTCHKINF_SPOKE_TO_SARIA_ON_BRIDGE: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_FAIRY_OCARINA).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_FAIRY_OCARINA); break; case EVENTCHKINF_OBTAINED_KOKIRI_EMERALD_DEKU_TREE_DEAD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_KOKIRI_EMERALD).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_KOKIRI_EMERALD); break; case EVENTCHKINF_USED_DODONGOS_CAVERN_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_GORON_RUBY).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_GORON_RUBY); break; case EVENTCHKINF_USED_JABU_JABUS_BELLY_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_ZORA_SAPPHIRE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_ZORA_SAPPHIRE); break; case EVENTCHKINF_USED_FOREST_TEMPLE_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_FOREST_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_FOREST_MEDALLION); break; case EVENTCHKINF_USED_FIRE_TEMPLE_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_FIRE_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_FIRE_MEDALLION); break; case EVENTCHKINF_USED_WATER_TEMPLE_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_WATER_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_WATER_MEDALLION); break; case EVENTCHKINF_RETURNED_TO_TEMPLE_OF_TIME_WITH_ALL_MEDALLIONS: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_LIGHT_ARROWS).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_LIGHT_ARROWS); break; case EVENTCHKINF_TIME_TRAVELED_TO_ADULT: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_LIGHT_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_LIGHT_MEDALLION); break; } break; case FLAG_RANDOMIZER_INF: switch (flag) { case RAND_INF_DUNGEONS_DONE_SHADOW_TEMPLE: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SHADOW_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SHADOW_MEDALLION); break; case RAND_INF_DUNGEONS_DONE_SPIRIT_TEMPLE: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SPIRIT_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SPIRIT_MEDALLION); break; } break; @@ -1287,22 +1286,22 @@ void TimeSaverOnFlagSetHandler(int16_t flagType, int16_t flag) { case FLAG_RANDOMIZER_INF: switch (flag) { case RAND_INF_ZF_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_FARORES_WIND).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_FARORES_WIND); break; case RAND_INF_HC_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_DINS_FIRE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_DINS_FIRE); break; case RAND_INF_COLOSSUS_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_NAYRUS_LOVE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_NAYRUS_LOVE); break; case RAND_INF_DMT_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_MAGIC_SINGLE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_MAGIC_SINGLE); break; case RAND_INF_DMC_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_MAGIC_DOUBLE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_MAGIC_DOUBLE); break; case RAND_INF_OGC_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_DOUBLE_DEFENSE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_DOUBLE_DEFENSE); break; } break; @@ -1328,47 +1327,44 @@ void TimeSaverOnFlagSetHandler(int16_t flagType, int16_t flag) { case FLAG_EVENT_CHECK_INF: switch (flag) { case EVENTCHKINF_LEARNED_ZELDAS_LULLABY: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_ZELDAS_LULLABY).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_ZELDAS_LULLABY); break; case EVENTCHKINF_LEARNED_MINUET_OF_FOREST: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_MINUET_OF_FOREST).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_MINUET_OF_FOREST); break; case EVENTCHKINF_LEARNED_BOLERO_OF_FIRE: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_BOLERO_OF_FIRE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_BOLERO_OF_FIRE); break; case EVENTCHKINF_LEARNED_SERENADE_OF_WATER: - vanillaQueuedItemEntry = - Rando::StaticData::RetrieveItem(RG_SERENADE_OF_WATER).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SERENADE_OF_WATER); break; case EVENTCHKINF_LEARNED_REQUIEM_OF_SPIRIT: - vanillaQueuedItemEntry = - Rando::StaticData::RetrieveItem(RG_REQUIEM_OF_SPIRIT).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_REQUIEM_OF_SPIRIT); break; case EVENTCHKINF_BONGO_BONGO_ESCAPED_FROM_WELL: - vanillaQueuedItemEntry = - Rando::StaticData::RetrieveItem(RG_NOCTURNE_OF_SHADOW).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_NOCTURNE_OF_SHADOW); break; case EVENTCHKINF_LEARNED_PRELUDE_OF_LIGHT: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_PRELUDE_OF_LIGHT).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_PRELUDE_OF_LIGHT); break; case EVENTCHKINF_LEARNED_SARIAS_SONG: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SARIAS_SONG).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SARIAS_SONG); break; case EVENTCHKINF_LEARNED_SONG_OF_TIME: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SONG_OF_TIME).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SONG_OF_TIME); break; case EVENTCHKINF_LEARNED_SONG_OF_STORMS: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SONG_OF_STORMS).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SONG_OF_STORMS); break; case EVENTCHKINF_LEARNED_SUNS_SONG: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SUNS_SONG).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SUNS_SONG); break; } break; case FLAG_RANDOMIZER_INF: switch (flag) { case RAND_INF_LEARNED_EPONA_SONG: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_EPONAS_SONG).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_EPONAS_SONG); break; } break; diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index 54c33e2cfa..f69a7ea8cc 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -2032,8 +2032,10 @@ s32 GiveItemEntryWithoutActor(PlayState* play, GetItemEntry getItemEntry) { PLAYER_STATE1_CLIMBING_LEDGE | PLAYER_STATE1_JUMPING | PLAYER_STATE1_FREEFALL | PLAYER_STATE1_FIRST_PERSON | PLAYER_STATE1_CLIMBING_LADDER)) && Player_GetExplosiveHeld(player) < 0) { - if (((player->heldActor != NULL) && ((getItemEntry.getItemId > GI_NONE) && (getItemEntry.getItemId < GI_MAX)) || - (IS_RANDO && (getItemEntry.getItemId > RG_NONE) && (getItemEntry.getItemId < RG_MAX))) || + if (((player->heldActor != NULL && (getItemEntry.modIndex == MOD_NONE && getItemEntry.getItemId > GI_NONE && + getItemEntry.getItemId < GI_MAX)) || + (getItemEntry.modIndex == MOD_RANDOMIZER && getItemEntry.getItemId > RG_NONE && + getItemEntry.getItemId < RG_MAX)) || (!(player->stateFlags1 & (PLAYER_STATE1_CARRYING_ACTOR | PLAYER_STATE1_IN_CUTSCENE)))) { if ((getItemEntry.getItemId != GI_NONE)) { player->getItemEntry = getItemEntry; @@ -2073,8 +2075,10 @@ s32 GiveItemEntryFromActor(Actor* actor, PlayState* play, GetItemEntry getItemEn PLAYER_STATE1_CLIMBING_LADDER)) && Player_GetExplosiveHeld(player) < 0) { if ((((player->heldActor != NULL) || (actor == player->talkActor)) && - ((!IS_RANDO && ((getItemEntry.getItemId > GI_NONE) && (getItemEntry.getItemId < GI_MAX))) || - (IS_RANDO && ((getItemEntry.getItemId > RG_NONE) && (getItemEntry.getItemId < RG_MAX))))) || + ((getItemEntry.getItemId == MOD_NONE && + ((getItemEntry.getItemId > GI_NONE) && (getItemEntry.getItemId < GI_MAX))) || + (getItemEntry.getItemId == MOD_RANDOMIZER && + ((getItemEntry.getItemId > RG_NONE) && (getItemEntry.getItemId < RG_MAX))))) || (!(player->stateFlags1 & (PLAYER_STATE1_CARRYING_ACTOR | PLAYER_STATE1_IN_CUTSCENE)))) { if ((actor->xzDistToPlayer < xzRange) && (fabsf(actor->yDistToPlayer) < yRange)) { s16 yawDiff = actor->yawTowardsPlayer - player->actor.shape.rot.y; @@ -2118,8 +2122,7 @@ s32 Actor_OfferGetItem(Actor* actor, PlayState* play, s32 getItemId, f32 xzRange PLAYER_STATE1_CLIMBING_LADDER)) && Player_GetExplosiveHeld(player) < 0) { if ((((player->heldActor != NULL) || (actor == player->talkActor)) && - ((!IS_RANDO && ((getItemId > GI_NONE) && (getItemId < GI_MAX))) || - (IS_RANDO && ((getItemId > RG_NONE) && (getItemId < RG_MAX))))) || + ((getItemId > GI_NONE) && (getItemId < GI_MAX))) || (!(player->stateFlags1 & (PLAYER_STATE1_CARRYING_ACTOR | PLAYER_STATE1_IN_CUTSCENE)))) { if ((actor->xzDistToPlayer < xzRange) && (fabsf(actor->yDistToPlayer) < yRange)) { s16 yawDiff = actor->yawTowardsPlayer - player->actor.shape.rot.y; diff --git a/soh/src/code/z_player_lib.c b/soh/src/code/z_player_lib.c index 83dfd7608c..79bf0bfb6b 100644 --- a/soh/src/code/z_player_lib.c +++ b/soh/src/code/z_player_lib.c @@ -3,7 +3,6 @@ #include "objects/gameplay_field_keep/gameplay_field_keep.h" #include "objects/object_link_boy/object_link_boy.h" #include "objects/object_link_child/object_link_child.h" -#include "objects/object_triforce_spot/object_triforce_spot.h" #include "overlays/actors/ovl_Demo_Effect/z_demo_effect.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 5aa9d94704..804292a786 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -7339,8 +7339,9 @@ s32 Player_ActionHandler_2(Player* this, PlayState* play) { // getting bombchus need to show the cutscene) and whenever the player doesn't have the item yet. In // rando, we're overruling this because we need to keep showing the cutscene because those items can be // randomized and thus it's important to keep showing the cutscene. - uint8_t showItemCutscene = play->sceneNum == SCENE_BOMBCHU_BOWLING_ALLEY || - Item_CheckObtainability(giEntry.itemId) == ITEM_NONE || IS_RANDO; + uint8_t showItemCutscene = play->sceneNum == SCENE_BOMBCHU_BOWLING_ALLEY || IS_RANDO || + giEntry.modIndex == MOD_RANDOMIZER || + Item_CheckObtainability(giEntry.itemId) == ITEM_NONE; // Only skip cutscenes for drops when they're items/consumables from bushes/rocks/enemies. uint8_t isDropToSkip = @@ -7359,8 +7360,8 @@ s32 Player_ActionHandler_2(Player* this, PlayState* play) { // the player already has because those items could be a randomized item coming from scrubs, // freestanding PoH's and keys. So we need to once again overrule this specifically for items coming // from bushes/rocks/enemies when the player has already picked that item up. - uint8_t skipItemCutsceneRando = - IS_RANDO && Item_CheckObtainability(giEntry.itemId) != ITEM_NONE && isDropToSkip; + uint8_t skipItemCutsceneRando = IS_RANDO && giEntry.modIndex == MOD_NONE && + Item_CheckObtainability(giEntry.itemId) != ITEM_NONE && isDropToSkip; // Show cutscene when picking up a item. if (showItemCutscene && !skipItemCutscene && !skipItemCutsceneRando) { From ccfa31a245f0dabb1edf626e8e8b677f032d2025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:48:12 +0000 Subject: [PATCH 14/30] Fix drag&drop not updating excluded locations (#6559) --- soh/soh/OTRGlobals.cpp | 1 + soh/soh/SohGui/SohMenu.h | 1 + soh/soh/SohGui/SohMenuRandomizer.cpp | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 39f04ddbbd..b5227b4113 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -2536,6 +2536,7 @@ bool SoH_HandleConfigDrop(char* filePath) { ->ClearBindings(); Rando::Settings::GetInstance()->UpdateAllOptions(); + SohGui::MarkRandomizerMenusDirty(); gui->SaveConsoleVariablesNextFrame(); ShipInit::Init("*"); diff --git a/soh/soh/SohGui/SohMenu.h b/soh/soh/SohGui/SohMenu.h index 5b0b07827d..c50c436740 100644 --- a/soh/soh/SohGui/SohMenu.h +++ b/soh/soh/SohGui/SohMenu.h @@ -28,6 +28,7 @@ static std::map languages = { }; void UpdateMenuTricks(); void UpdateMenuLocations(); +void MarkRandomizerMenusDirty(); class SohMenu : public Ship::Menu { public: diff --git a/soh/soh/SohGui/SohMenuRandomizer.cpp b/soh/soh/SohGui/SohMenuRandomizer.cpp index b5b9ece986..5647e35910 100644 --- a/soh/soh/SohGui/SohMenuRandomizer.cpp +++ b/soh/soh/SohGui/SohMenuRandomizer.cpp @@ -181,6 +181,11 @@ void DrawLocationsMenu(WidgetInfo& info) { ImGui::EndDisabled(); } +void MarkRandomizerMenusDirty() { + locationsDirty = true; + tricksDirty = true; +} + void UpdateMenuLocations() { RandomizerCheckObjects::UpdateImGuiVisibility(); // todo: this efficiently when we build out cvar array support From ea76550fc703dbcfe9b3bfbda5d301cd554cce01 Mon Sep 17 00:00:00 2001 From: Pepper0ni <93387759+Pepper0ni@users.noreply.github.com> Date: Sat, 25 Apr 2026 16:30:27 +0100 Subject: [PATCH 15/30] Fix owl talk logic to include kokiri (#6564) --- .../location_access/overworld/death_mountain_trail.cpp | 2 +- .../randomizer/location_access/overworld/lake_hylia.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/death_mountain_trail.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/death_mountain_trail.cpp index 78d406403e..891318b262 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/death_mountain_trail.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/death_mountain_trail.cpp @@ -64,7 +64,7 @@ void RegionTable_Init_DeathMountainTrail() { //Exits ENTRANCE(RR_DEATH_MOUNTAIN_ROCKFALL, true), ENTRANCE(RR_DMC_UPPER_ENTRY, true), - ENTRANCE(RR_DMT_OWL_FLIGHT, logic->IsChild && (logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), + ENTRANCE(RR_DMT_OWL_FLIGHT, logic->IsChild && (logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_KOKIRI) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), ENTRANCE(RR_DMT_GREAT_FAIRY_FOUNTAIN, AnyAgeTime([]{return logic->BlastOrSmash();})), }); diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp index cdd1060502..5c99fd46ff 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp @@ -83,11 +83,11 @@ void RegionTable_Init_LakeHylia() { //Exits ENTRANCE(RR_HF_TO_LAKE_HYLIA, true), ENTRANCE(RR_LH_FROM_SHORTCUT, true), - ENTRANCE(RR_LH_OWL_FLIGHT, logic->IsChild && (logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), + ENTRANCE(RR_LH_OWL_FLIGHT, logic->IsChild && (logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_KOKIRI) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), ENTRANCE(RR_LH_FISHING_ISLAND, ((logic->IsChild || logic->Get(LOGIC_WATER_TEMPLE_CLEAR)) && logic->HasItem(RG_BRONZE_SCALE)) || (logic->IsAdult && (logic->ReachScarecrow() || CanPlantBean(RR_LAKE_HYLIA, RG_LAKE_HYLIA_BEAN_SOUL)))), ENTRANCE(RR_LH_LAB, logic->CanOpenOverworldDoor(RG_HYLIA_LAB_KEY)), ENTRANCE(RR_LH_FROM_WATER_TEMPLE, true), - ENTRANCE(RR_LH_GROTTO, logic->HasItem(RG_POWER_BRACELET) && (logic->IsAdult || logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), + ENTRANCE(RR_LH_GROTTO, logic->HasItem(RG_POWER_BRACELET) && (logic->IsAdult || logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_KOKIRI) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), }); areaTable[RR_LH_FROM_SHORTCUT] = Region("LH From Shortcut", SCENE_LAKE_HYLIA, TIME_DOESNT_PASS, {RA_LAKE_HYLIA}, {}, {}, { From 1a46d2ec96875fbd0679a9055ed10940f90ea282 Mon Sep 17 00:00:00 2001 From: Jameriquiah <42100286+Jameriquiah@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:37:20 -0400 Subject: [PATCH 16/30] hyrule field typo fix (#6574) --- .../3drando/hint_list/hint_list_exclude_overworld.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_overworld.cpp b/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_overworld.cpp index 487c761584..aca29a0146 100644 --- a/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_overworld.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_overworld.cpp @@ -2128,7 +2128,7 @@ void StaticData::HintTable_Init_Exclude_Overworld() { /*french*/ "Selon moi, un #arbre au Ranch Lon Lon# cache #[[1]]#.", { QM_RED, QM_GREEN })); hintTextTable[RHT_BUSH_HYRULE_FIELD] = - HintText(CustomMessage("They say that a #bush in Hyrle Field# contains #[[1]]#.", + HintText(CustomMessage("They say that a #bush in Hyrule Field# contains #[[1]]#.", /*german*/ "", /*french*/ "Selon moi, un #buisson dans la Plaine d'Hyrule# cache #[[1]]#.", { QM_RED, QM_GREEN })); hintTextTable[RHT_BUSH_ZORAS_FOUNTAIN] = From 5b4d8edf5194c7581e514e715ec7c37e2ffc9fdd Mon Sep 17 00:00:00 2001 From: Pepper0ni <93387759+Pepper0ni@users.noreply.github.com> Date: Mon, 4 May 2026 03:02:21 +0100 Subject: [PATCH 17/30] fix bad merge with the suns song fairy in spirit (#6590) --- .../randomizer/location_access/dungeons/spirit_temple.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp index 8baa6abcf1..de04fda773 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp @@ -134,7 +134,6 @@ void RegionTable_Init_SpiritTemple() { LOCATION(RC_SPIRIT_TEMPLE_CHILD_CLIMB_EAST_CHEST, SpiritShared(RR_SPIRIT_TEMPLE_SUN_ON_FLOOR_2F, []{return logic->CanHitSwitch(ED_BOMB_THROW) && logic->HasItem(RG_OPEN_CHEST);})), LOCATION(RC_SPIRIT_TEMPLE_GS_SUN_ON_FLOOR_ROOM, SpiritShared(RR_SPIRIT_TEMPLE_SUN_ON_FLOOR_2F, []{return logic->CanKillEnemy(RE_GOLD_SKULLTULA, logic->TakeDamage() ? ED_SHORT_JUMPSLASH : ED_BOMB_THROW);}, false, RR_SPIRIT_TEMPLE_SUN_ON_FLOOR_1F, []{return logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG);})), - LOCATION(RC_SPIRIT_TEMPLE_BOULDER_ROOM_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG) && (logic->CanUse(RG_FAIRY_BOW) || logic->CanUse(RG_HOOKSHOT) || logic->CanUse(RG_FAIRY_SLINGSHOT) || logic->CanUse(RG_BOOMERANG) || logic->CanUse(RG_BOMBCHU_5) || (logic->CanUse(RG_BOMB_BAG) && logic->IsAdult && ctx->GetTrickOption(RT_SPIRIT_LOWER_ADULT_SWITCH))) && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanJumpslash())), }, { //Exits ENTRANCE(RR_SPIRIT_TEMPLE_SUN_ON_FLOOR_1F, true), From 37db03481526f2a2c1e0063ebd3ae6b4d3e4abeb Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 9 May 2026 18:25:09 -0400 Subject: [PATCH 18/30] Use singular on message for 1 token (#6567) --- soh/soh/Enhancements/InjectItemCounts.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/soh/soh/Enhancements/InjectItemCounts.cpp b/soh/soh/Enhancements/InjectItemCounts.cpp index 8b33a9b648..8d66ba2712 100644 --- a/soh/soh/Enhancements/InjectItemCounts.cpp +++ b/soh/soh/Enhancements/InjectItemCounts.cpp @@ -6,11 +6,11 @@ extern "C" { void BuildSkulltulaMessage(uint16_t* textId, bool* loadFromMessageTable) { CustomMessage msg = - CustomMessage("You got a %rGold Skulltula Token%w!&You've collected %r[[gsCount]]%w tokens&in total!", - "Ein %rGoldenes Skulltula-Symbol%w!&Du hast nun insgesamt %r[[gsCount]]&%wGoldene " - "Skulltula-Symbole&gesammelt!", - "Vous obtenez un %rSymbole de&Skulltula d'or%w! Vous avez&collecté %r[[gsCount]]%w symboles en " - "tout!", + CustomMessage("You got a %rGold Skulltula Token%w!&You've collected %r[[d]]%w |token|tokens|&in total!", + "Ein %rGoldenes Skulltula-Symbol%w!&Du hast nun insgesamt %r[[d]]&%w|Goldenes " + "Skulltula-Symbol|Goldene Skulltula-Symbole|&gesammelt!", + "Vous obtenez un %rSymbole de&Skulltula d'or%w! Vous avez&collecté %r[[d]]%w |symbole|symboles| " + "en tout!", TEXTBOX_TYPE_BLUE); // The freeze text cannot be manually dismissed and must be auto-dismissed. // This is fine and even wanted when skull tokens are not shuffled, but when @@ -25,7 +25,7 @@ void BuildSkulltulaMessage(uint16_t* textId, bool* loadFromMessageTable) { msg = msg + "\x0E\x3C"; } int16_t gsCount = gSaveContext.inventory.gsTokens; - msg.Replace("[[gsCount]]", std::to_string(gsCount)); + msg.InsertNumber(gsCount); msg.AutoFormat(ITEM_SKULL_TOKEN); msg.LoadIntoFont(); *loadFromMessageTable = false; From b728e671bffdc3c2c661328d7f0e289e1020fa05 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 24 May 2026 23:30:31 -0400 Subject: [PATCH 19/30] Fix blank text on carpet salesman (#6605) --- soh/soh/Enhancements/randomizer/Messages/MerchantMessages.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/soh/soh/Enhancements/randomizer/Messages/MerchantMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/MerchantMessages.cpp index 9a004f4abf..747d4a9ee0 100644 --- a/soh/soh/Enhancements/randomizer/Messages/MerchantMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/MerchantMessages.cpp @@ -110,6 +110,8 @@ void BuildCarpetGuyMessage(uint16_t* textId, bool* loadFromMessageTable) { BuildMerchantMessage(msg, RC_WASTELAND_BOMBCHU_SALESMAN, !RAND_GET_OPTION(RSK_MERCHANT_TEXT_HINT) || CVarGetInteger(CVAR_RANDOMIZER_ENHANCEMENT("MysteriousShuffle"), 0)); + } else { + return; } msg.AutoFormat(); msg.LoadIntoFont(); From e3ee258a928f4aa844efb7697433ad1954f641d8 Mon Sep 17 00:00:00 2001 From: Unreference <87878910+unreference@users.noreply.github.com> Date: Wed, 27 May 2026 07:02:32 -0700 Subject: [PATCH 20/30] fix(actors): Restore vanilla default for Kokiri Forest quest state hook (#6614) --- soh/src/overlays/actors/ovl_En_Ko/z_en_ko.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/src/overlays/actors/ovl_En_Ko/z_en_ko.c b/soh/src/overlays/actors/ovl_En_Ko/z_en_ko.c index 4b2280d7ed..e818a1c2c4 100644 --- a/soh/src/overlays/actors/ovl_En_Ko/z_en_ko.c +++ b/soh/src/overlays/actors/ovl_En_Ko/z_en_ko.c @@ -1183,7 +1183,7 @@ void func_80A99048(EnKo* this, PlayState* play) { if (ENKO_TYPE == ENKO_TYPE_CHILD_5) { this->collider.base.ocFlags1 |= 0x40; } - if (GameInteractor_Should(VB_KOKIRI_GET_FOREST_QUEST_STATE2, false, this)) { + if (GameInteractor_Should(VB_KOKIRI_GET_FOREST_QUEST_STATE2, true, this)) { this->forestQuestState = EnKo_GetForestQuestState2(this); } Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, sOsAnimeLookup[ENKO_TYPE][this->forestQuestState]); From e5ad4e6f114c245bf7ead2d5ecf04a62aa8d4880 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 27 May 2026 11:58:17 -0400 Subject: [PATCH 21/30] Fix ending audio shuffle (#6608) --- soh/soh/Enhancements/audio/AudioEditor.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/audio/AudioEditor.cpp b/soh/soh/Enhancements/audio/AudioEditor.cpp index 6139da7c0c..699ec522b1 100644 --- a/soh/soh/Enhancements/audio/AudioEditor.cpp +++ b/soh/soh/Enhancements/audio/AudioEditor.cpp @@ -53,6 +53,7 @@ extern std::shared_ptr mSohMenu; #define SEQ_COUNT_INSTRUMENT 6 #define SEQ_COUNT_SFX 57 #define SEQ_COUNT_VOICE 108 +#define SEQ_COUNT_ENDING 5 size_t AuthenticCountBySequenceType(SeqType type) { switch (type) { @@ -74,6 +75,8 @@ size_t AuthenticCountBySequenceType(SeqType type) { return SEQ_COUNT_INSTRUMENT; case SEQ_VOICE: return SEQ_COUNT_VOICE; + case SEQ_ENDING: + return SEQ_COUNT_ENDING; default: return 0; } @@ -806,7 +809,8 @@ void AudioEditor::DrawElement() { } std::vector allTypes = { - SEQ_BGM_WORLD, SEQ_BGM_EVENT, SEQ_BGM_BATTLE, SEQ_OCARINA, SEQ_FANFARE, SEQ_INSTRUMENT, SEQ_SFX, SEQ_VOICE, + SEQ_BGM_WORLD, SEQ_BGM_EVENT, SEQ_BGM_BATTLE, SEQ_OCARINA, SEQ_FANFARE, + SEQ_INSTRUMENT, SEQ_SFX, SEQ_VOICE, SEQ_ENDING, }; void AudioEditor_RandomizeAll() { From 0dcd52e5c730cce9bd127e250b193a46415c9c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Wed, 27 May 2026 18:28:43 +0000 Subject: [PATCH 22/30] Fix warp shuffle without warp hint text (#6551) Issue was needing to hook on RSK_SHUFFLE_WARP_SONGS, but cleaned up code --- .../randomizer/Messages/StaticHints.cpp | 87 ++++++------------- 1 file changed, 27 insertions(+), 60 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp b/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp index d62c2e0f14..31d7e64eab 100644 --- a/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp @@ -178,78 +178,43 @@ void BuildGregHintMessage(uint16_t* textId, bool* loadFromMessageTable) { } } -void BuildMysteriousWarpMessage() { - CustomMessage msg = CustomMessage( - "Warp to&%ra mysterious place?%w&" + CustomMessage::TWO_WAY_CHOICE() + "%gOK&No%w", - "Zu&%reinem mysteriösen Ort%w?&" + CustomMessage::TWO_WAY_CHOICE() + "%gOK&No%w", - "Se téléporter vers&%run endroit mystérieux%w?&" + CustomMessage::TWO_WAY_CHOICE() + "%rOK!&Non%w"); - msg.LoadIntoFont(); +static void BuildWarpMessage(RandomizerHint rh, bool* loadFromMessageTable) { + if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { + CustomMessage msg = CustomMessage( + "Warp to&%ra mysterious place?%w&" + CustomMessage::TWO_WAY_CHOICE() + "%gOK&No%w", + "Zu&%reinem mysteriösen Ort%w?&" + CustomMessage::TWO_WAY_CHOICE() + "%gOK&No%w", + "Se téléporter vers&%run endroit mystérieux%w?&" + CustomMessage::TWO_WAY_CHOICE() + "%rOK!&Non%w"); + msg.AutoFormat(); + msg.LoadIntoFont(); + } else { + CustomMessage msg = RAND_GET_HINT(rh)->GetHintMessage(MF_AUTO_FORMAT); + msg.LoadIntoFont(); + } + *loadFromMessageTable = false; } void BuildMinuetWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_MINUET_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_MINUET_WARP_LOC, loadFromMessageTable); } void BuildBoleroWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_BOLERO_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_BOLERO_WARP_LOC, loadFromMessageTable); } void BuildSerenadeWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_SERENADE_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_SERENADE_WARP_LOC, loadFromMessageTable); } void BuildRequiemWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_REQUIEM_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_REQUIEM_WARP_LOC, loadFromMessageTable); } void BuildNocturneWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_NOCTURNE_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_NOCTURNE_WARP_LOC, loadFromMessageTable); } void BuildPreludeWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_PRELUDE_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_PRELUDE_WARP_LOC, loadFromMessageTable); } void BuildFrogsHintMessage(uint16_t* textId, bool* loadFromMessageTable) { @@ -419,15 +384,17 @@ void RegisterStaticHints() { COND_ID_HOOK(OnOpenText, TEXT_CHEST_GAME_REAL_GAMBLER, RAND_GET_OPTION(RSK_GREG_HINT), BuildGregHintMessage); COND_ID_HOOK(OnOpenText, TEXT_CHEST_GAME_THANKS_A_LOT, RAND_GET_OPTION(RSK_GREG_HINT), BuildGregHintMessage); // Warp - COND_ID_HOOK(OnOpenText, TEXT_WARP_MINUET_OF_FOREST, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), BuildMinuetWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_BOLERO_OF_FIRE, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), BuildBoleroWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_SERENADE_OF_WATER, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), + COND_ID_HOOK(OnOpenText, TEXT_WARP_MINUET_OF_FOREST, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), + BuildMinuetWarpMessage); + COND_ID_HOOK(OnOpenText, TEXT_WARP_BOLERO_OF_FIRE, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), BuildBoleroWarpMessage); + COND_ID_HOOK(OnOpenText, TEXT_WARP_SERENADE_OF_WATER, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), BuildSerenadeWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_REQUIEM_OF_SPIRIT, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), + COND_ID_HOOK(OnOpenText, TEXT_WARP_REQUIEM_OF_SPIRIT, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), BuildRequiemWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_NOCTURNE_OF_SHADOW, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), + COND_ID_HOOK(OnOpenText, TEXT_WARP_NOCTURNE_OF_SHADOW, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), BuildNocturneWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_PRELUDE_OF_LIGHT, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), BuildPreludeWarpMessage); + COND_ID_HOOK(OnOpenText, TEXT_WARP_PRELUDE_OF_LIGHT, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), + BuildPreludeWarpMessage); // Frogs COND_ID_HOOK(OnOpenText, TEXT_FROGS_UNDERWATER, RAND_GET_OPTION(RSK_FROGS_HINT), BuildFrogsHintMessage); // Loach From dc4b27d65a8e09623110127a3478f0917250449e Mon Sep 17 00:00:00 2001 From: aMannus Date: Fri, 29 May 2026 04:14:17 +0200 Subject: [PATCH 23/30] Fix gossip stone check (#6648) --- soh/soh/Enhancements/randomizer/Messages/GossipStoneHints.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/Messages/GossipStoneHints.cpp b/soh/soh/Enhancements/randomizer/Messages/GossipStoneHints.cpp index 7e2a2e193a..70d2235a63 100644 --- a/soh/soh/Enhancements/randomizer/Messages/GossipStoneHints.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/GossipStoneHints.cpp @@ -13,7 +13,7 @@ extern PlayState* gPlayState; void BuildHintStoneMessage(uint16_t* textId, bool* loadFromMessageTable) { if ((RAND_GET_OPTION(RSK_GOSSIP_STONE_HINTS).Is(RO_GOSSIP_STONES_NEED_TRUTH) && - Player_GetMask(gPlayState) == PLAYER_MASK_TRUTH) || + Player_GetMask(gPlayState) != PLAYER_MASK_TRUTH) || (RAND_GET_OPTION(RSK_GOSSIP_STONE_HINTS).Is(RO_GOSSIP_STONES_NEED_STONE) && CHECK_QUEST_ITEM(QUEST_STONE_OF_AGONY) == 0)) { return; From 5bccc8a340915722d05b007ab58cb4d15bd320cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Fri, 29 May 2026 02:24:54 +0000 Subject: [PATCH 24/30] avoid ganon's castle blue warp dropping Link in lava (#6647) --- .../randomizer/randomizer_entrance.c | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.c b/soh/soh/Enhancements/randomizer/randomizer_entrance.c index 0154cd99fe..f1a70ab9ce 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance.c +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.c @@ -700,17 +700,18 @@ void Entrance_OverrideSpawnScene(s32 sceneNum, s32 spawn) { modifiedLinkActorEntry.rot = gPlayState->linkActorEntry->rot; modifiedLinkActorEntry.params = gPlayState->linkActorEntry->params; - if (Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES) == RO_DUNGEON_ENTRANCE_SHUFFLE_ON_PLUS_GANON) { - // Move Ganon's Castle exit spawn to be on the small ledge near the castle and not over the void - // to prevent Link from falling if the bridge isn't spawned - if (sceneNum == SCENE_OUTSIDE_GANONS_CASTLE && spawn == 1) { - modifiedLinkActorEntry.pos.x = 0xFEA8; - modifiedLinkActorEntry.pos.y = 0x065C; - modifiedLinkActorEntry.pos.z = 0x0290; - modifiedLinkActorEntry.rot.y = 0x0700; - modifiedLinkActorEntry.params = 0x0DFF; // stationary spawn - gPlayState->linkActorEntry = &modifiedLinkActorEntry; - } + // Move Ganon's Castle exit spawn to be on the small ledge near the castle and not over the void + // to prevent Link from falling if the bridge isn't spawned + if (sceneNum == SCENE_OUTSIDE_GANONS_CASTLE && spawn == 1 && + (Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES) == RO_DUNGEON_ENTRANCE_SHUFFLE_ON_PLUS_GANON || + Randomizer_GetSettingValue(RSK_SHUFFLE_BOSS_ENTRANCES) != RO_BOSS_ROOM_ENTRANCE_SHUFFLE_OFF || + Randomizer_GetSettingValue(RSK_SHUFFLE_GANONS_TOWER_ENTRANCE))) { + modifiedLinkActorEntry.pos.x = 0xFEA8; + modifiedLinkActorEntry.pos.y = 0x065C; + modifiedLinkActorEntry.pos.z = 0x0290; + modifiedLinkActorEntry.rot.y = 0x0700; + modifiedLinkActorEntry.params = 0x0DFF; // stationary spawn + gPlayState->linkActorEntry = &modifiedLinkActorEntry; } if (Randomizer_GetSettingValue(RSK_SHUFFLE_BOSS_ENTRANCES) != RO_BOSS_ROOM_ENTRANCE_SHUFFLE_OFF) { From a57cdc6f775e147b12469e10cd9835ba2e57fc09 Mon Sep 17 00:00:00 2001 From: Pepe20129 <72659707+Pepe20129@users.noreply.github.com> Date: Fri, 29 May 2026 04:39:56 +0200 Subject: [PATCH 25/30] Update Hell Mode Preset (#6570) --- .../presets/Rando Seed Settings - Hell Mode.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/soh/assets/custom/presets/Rando Seed Settings - Hell Mode.json b/soh/assets/custom/presets/Rando Seed Settings - Hell Mode.json index 5e0d7e5863..f5baa80047 100644 --- a/soh/assets/custom/presets/Rando Seed Settings - Hell Mode.json +++ b/soh/assets/custom/presets/Rando Seed Settings - Hell Mode.json @@ -4,7 +4,7 @@ "gRandoSettings": { "BigPoeTargetCount": 1, "BlueFireArrows": 1, - "BombchuBag": 1, + "BombchuBag": 2, "BossKeysanity": 5, "ClosedForest": 2, "CuccosToReturn": 1, @@ -21,11 +21,13 @@ "LacsRewardCount": 10, "LacsRewardOptions": 1, "LockOverworldDoors": 1, + "MedallionLockedTrials": 1, "MixBosses": 1, "MixDungeons": 1, "MixGrottos": 1, "MixInteriors": 1, "MixOverworld": 1, + "MixThievesHideout": 1, "MixedEntrances": 1, "RainbowBridge": 7, "ScrubsPrices": 2, @@ -35,12 +37,16 @@ "Shuffle100GSReward": 1, "ShuffleAdultTrade": 1, "ShuffleBeanFairies": 1, + "ShuffleBeanSouls": 1, "ShuffleBeehives": 1, "ShuffleBossEntrances": 2, "ShuffleBossSouls": 2, + "ShuffleBushes": 1, "ShuffleChildWallet": 1, + "ShuffleClimb": 1, "ShuffleCows": 1, "ShuffleCrates": 3, + "ShuffleCrawl": 1, "ShuffleDekuNutBag": 1, "ShuffleDekuStickBag": 1, "ShuffleDungeonsEntrances": 2, @@ -50,7 +56,9 @@ "ShuffleFreestanding": 3, "ShuffleFrogSongRupees": 1, "ShuffleGanonBossKey": 9, + "ShuffleGanonTowerEntrance": 1, "ShuffleGerudoToken": 1, + "ShuffleGrab": 1, "ShuffleGrass": 3, "ShuffleGrottosEntrances": 1, "ShuffleInteriorsEntrances": 2, @@ -59,18 +67,21 @@ "ShuffleMerchants": 3, "ShuffleOcarinaButtons": 1, "ShuffleOcarinas": 1, + "ShuffleOpenChest": 1, "ShuffleOverworldEntrances": 1, "ShuffleOverworldSpawns": 1, "ShuffleOwlDrops": 1, "ShufflePots": 3, "ShuffleScrubs": 2, "ShuffleSongs": 2, + "ShuffleSpeak": 1, "ShuffleStoneFairies": 1, "ShuffleSwim": 1, + "ShuffleThievesHideoutEntrances": 1, + "ShuffleTrees": 1, "ShuffleTokens": 3, "ShuffleWarpSongs": 1, "ShuffleWeirdEgg": 1, - "SkipEponaRace": 1, "StartingAge": 2, "StartingHearts": 0, "StartingMapsCompasses": 5, From d09cd4ffcda23d0413bca2f943a0d91961f93e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Fri, 29 May 2026 02:50:02 +0000 Subject: [PATCH 26/30] Fix sword scaling (#6558) Don't apply transforms when scale 1.0, adjust translation to be gradual --- soh/src/code/z_player_lib.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/soh/src/code/z_player_lib.c b/soh/src/code/z_player_lib.c index 79bf0bfb6b..89faa95ad5 100644 --- a/soh/src/code/z_player_lib.c +++ b/soh/src/code/z_player_lib.c @@ -1316,12 +1316,14 @@ s32 Player_OverrideLimbDrawGameplayCommon(PlayState* play, s32 limbIndex, Gfx** if (limbIndex == PLAYER_LIMB_HEAD) { if (CVarGetInteger(CVAR_COSMETIC("Link.HeadScale.Changed"), 0)) { f32 scale = CVarGetFloat(CVAR_COSMETIC("Link.HeadScale.Value"), 1.0f); - Matrix_Scale(scale, scale, scale, MTXMODE_APPLY); - if (scale > 1.2f) { - Matrix_Translate(-((LINK_IS_ADULT ? 320.0f : 200.0f) * scale), 0.0f, 0.0f, MTXMODE_APPLY); - } else if (scale < 1.0f) { - Matrix_Translate((LINK_IS_ADULT ? 3600.0f : 2900.0f) * ABS(scale - 1.0f), 0.0f, 0.0f, - MTXMODE_APPLY); + if (scale != 1.0f) { + Matrix_Scale(scale, scale, scale, MTXMODE_APPLY); + if (scale > 1.2f) { + Matrix_Translate(-((LINK_IS_ADULT ? 320.0f : 200.0f) * scale), 0.0f, 0.0f, MTXMODE_APPLY); + } else if (scale < 1.0f) { + Matrix_Translate((LINK_IS_ADULT ? 3600.0f : 2900.0f) * ABS(scale - 1.0f), 0.0f, 0.0f, + MTXMODE_APPLY); + } } } rot->x += this->headLimbRot.z; @@ -1330,8 +1332,10 @@ s32 Player_OverrideLimbDrawGameplayCommon(PlayState* play, s32 limbIndex, Gfx** } else if (limbIndex == PLAYER_LIMB_L_HAND) { if (CVarGetInteger(CVAR_COSMETIC("Link.SwordScale.Changed"), 0)) { f32 scale = CVarGetFloat(CVAR_COSMETIC("Link.SwordScale.Value"), 1.0f); - Matrix_Scale(scale, scale, scale, MTXMODE_APPLY); - Matrix_Translate(-((LINK_IS_ADULT ? 320.0f : 200.0f) * scale), 0.0f, 0.0f, MTXMODE_APPLY); + if (scale != 1.0f) { + Matrix_Scale(scale, scale, scale, MTXMODE_APPLY); + Matrix_Translate(-((LINK_IS_ADULT ? 320.0f : 200.0f) * (scale - 1.0f)), 0.0f, 0.0f, MTXMODE_APPLY); + } } } else if (limbIndex == PLAYER_LIMB_UPPER) { if (this->upperLimbYawSecondary != 0) { From 31cbeb929b16995be56dc943c7f50af605cc0bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Fri, 29 May 2026 02:51:15 +0000 Subject: [PATCH 27/30] Fix bean merchant check not being listed in tracker when bean merchant only shuffled (#6557) --- .../Enhancements/randomizer/randomizer_check_tracker.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp index 5c681d3076..3a835bd9f7 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp @@ -1565,7 +1565,8 @@ bool IsCheckShuffled(RandomizerCheck rc) { (loc->GetRCType() != RCTYPE_SCRUB || showScrubs || (showMajorScrubs && (rc == RC_LW_DEKU_SCRUB_NEAR_BRIDGE || // The 3 scrubs that are always randomized rc == RC_HF_DEKU_SCRUB_GROTTO || rc == RC_LW_DEKU_SCRUB_GROTTO_FRONT))) && - (loc->GetRCType() != RCTYPE_MERCHANT || showMerchants) && + ((loc->GetRCType() != RCTYPE_MERCHANT || showMerchants) || + (rc == RC_ZR_MAGIC_BEAN_SALESMAN && showBeans)) && (loc->GetRCType() != RCTYPE_SONG_LOCATION || showSongs) && (loc->GetRCType() != RCTYPE_BEEHIVE || showBeehives) && (loc->GetRCType() != RCTYPE_OCARINA || showOcarinas) && @@ -1603,8 +1604,7 @@ bool IsCheckShuffled(RandomizerCheck rc) { rc == RC_DMT_TRADE_CLAIM_CHECK // even when shuffle adult trade is off ) && (rc != RC_KF_KOKIRI_SWORD_CHEST || showKokiriSword) && (rc != RC_TOT_MASTER_SWORD || showMasterSword) && - (rc != RC_LH_HYRULE_LOACH || showHyruleLoach) && (rc != RC_ZR_MAGIC_BEAN_SALESMAN || showBeans) && - (rc != RC_HC_MALON_EGG || showWeirdEgg) && + (rc != RC_LH_HYRULE_LOACH || showHyruleLoach) && (rc != RC_HC_MALON_EGG || showWeirdEgg) && (loc->GetRCType() != RCTYPE_FROG_SONG || showFrogSongRupees) && ((loc->GetRCType() != RCTYPE_MAP && loc->GetRCType() != RCTYPE_COMPASS) || showStartingMapsCompasses) && (loc->GetRCType() != RCTYPE_FOUNTAIN_FAIRY || showFountainFairies) && From b8a3998c51e04e141aaebfb6baf3aaccaade8368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Fri, 29 May 2026 02:51:57 +0000 Subject: [PATCH 28/30] Fix master sword timing info (#6552) General fix: don't reset timestamp if already set, this is particularly important for retrieving master sword vs ganon Also fix master sword timing not being set when shuffled --- .../Enhancements/randomizer/randomizer.cpp | 47 +++++-------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 71f88e67d3..39a8308f3d 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -3589,49 +3589,28 @@ static std::unordered_map randomizerGetToS // Gameplay stat tracking: Update time the item was acquired // (special cases for rando items) void Randomizer_GameplayStats_SetTimestamp(uint16_t item) { - u32 time = static_cast(GAMEPLAYSTAT_TOTAL_TIME); - // Have items in Link's pocket shown as being obtained at 0.1 seconds if (time == 0) { time = 1; } - // Use ITEM_KEY_BOSS to timestamp Ganon's boss key - if (item == RG_GANONS_CASTLE_BOSS_KEY) { - gSaveContext.ship.stats.itemTimestamp[ITEM_KEY_BOSS] = time; - return; - } - - if (randomizerGetToStatsTimeStamp.contains((RandomizerGet)item)) { - gSaveContext.ship.stats.itemTimestamp[randomizerGetToStatsTimeStamp[(RandomizerGet)item]] = time; - return; - } - - // Count any bottled item as a bottle - if (item >= RG_EMPTY_BOTTLE && item <= RG_BOTTLE_WITH_BIG_POE) { - if (gSaveContext.ship.stats.itemTimestamp[ITEM_BOTTLE] == 0) { + if (gSaveContext.ship.stats.itemTimestamp[item] == 0) { + if (item == RG_GANONS_CASTLE_BOSS_KEY) { + gSaveContext.ship.stats.itemTimestamp[ITEM_KEY_BOSS] = time; + } else if (item == RG_MASTER_SWORD) { + gSaveContext.ship.stats.itemTimestamp[ITEM_SWORD_MASTER] = time; + } else if (randomizerGetToStatsTimeStamp.contains((RandomizerGet)item)) { + gSaveContext.ship.stats.itemTimestamp[randomizerGetToStatsTimeStamp[(RandomizerGet)item]] = time; + } else if (item >= RG_EMPTY_BOTTLE && item <= RG_BOTTLE_WITH_BIG_POE) { gSaveContext.ship.stats.itemTimestamp[ITEM_BOTTLE] = time; - } - return; - } - - // Count any bombchu pack as bombchus - if ((item >= RG_BOMBCHU_5 && item <= RG_BOMBCHU_20) || item == RG_PROGRESSIVE_BOMBCHU_BAG) { - if (gSaveContext.ship.stats.itemTimestamp[ITEM_BOMBCHU] = 0) { + } else if ((item >= RG_BOMBCHU_5 && item <= RG_BOMBCHU_20) || item == RG_PROGRESSIVE_BOMBCHU_BAG) { gSaveContext.ship.stats.itemTimestamp[ITEM_BOMBCHU] = time; + } else if (item == RG_MAGIC_SINGLE) { + gSaveContext.ship.stats.itemTimestamp[ITEM_SINGLE_MAGIC] = time; + } else if (item == RG_DOUBLE_DEFENSE) { + gSaveContext.ship.stats.itemTimestamp[ITEM_DOUBLE_DEFENSE] = time; } - return; - } - - if (item == RG_MAGIC_SINGLE) { - gSaveContext.ship.stats.itemTimestamp[ITEM_SINGLE_MAGIC] = time; - return; - } - - if (item == RG_DOUBLE_DEFENSE) { - gSaveContext.ship.stats.itemTimestamp[ITEM_DOUBLE_DEFENSE] = time; - return; } } From b61db770207664c17fb5c54f20b8547e56fde2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Fri, 5 Jun 2026 06:16:11 +0000 Subject: [PATCH 29/30] more hardening of code in Network/ (#6650) also unique_ptr --- soh/soh/Network/Anchor/JsonConversions.hpp | 3 + .../Network/Anchor/Packets/SetCheckStatus.cpp | 5 + soh/soh/Network/Anchor/Packets/SetFlag.cpp | 7 ++ soh/soh/Network/Anchor/Packets/TeleportTo.cpp | 6 ++ soh/soh/Network/Anchor/Packets/UnsetFlag.cpp | 7 ++ .../Anchor/Packets/UpdateDungeonItems.cpp | 7 ++ soh/soh/Network/CrowdControl/CrowdControl.cpp | 35 ++++--- soh/soh/Network/CrowdControl/CrowdControl.h | 4 +- soh/soh/Network/Network.cpp | 6 +- soh/soh/Network/Sail/Sail.cpp | 96 +++++++++---------- soh/soh/Network/Sail/Sail.h | 4 +- 11 files changed, 113 insertions(+), 67 deletions(-) diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp index 8b28ba40e1..0794662336 100644 --- a/soh/soh/Network/Anchor/JsonConversions.hpp +++ b/soh/soh/Network/Anchor/JsonConversions.hpp @@ -191,6 +191,9 @@ inline void from_json(const json& j, SaveContext& saveContext) { j.at("swordHealth").get_to(saveContext.swordHealth); std::vector sceneFlagsArray; j.at("sceneFlags").get_to(sceneFlagsArray); + if (sceneFlagsArray.size() < 124 * 4) { + sceneFlagsArray.resize(124 * 4, 0); + } for (int i = 0; i < 124; i++) { saveContext.sceneFlags[i].chest = sceneFlagsArray[i * 4]; saveContext.sceneFlags[i].swch = sceneFlagsArray[i * 4 + 1]; diff --git a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp index fb1c70dc9a..171ce3d39a 100644 --- a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp +++ b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp @@ -3,6 +3,7 @@ #include #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/OTRGlobals.h" +#include "soh/Enhancements/randomizer/randomizerEnums/RandomizerCheck.h" static bool isResultOfHandling = false; @@ -39,6 +40,10 @@ void Anchor::HandlePacket_SetCheckStatus(nlohmann::json payload) { auto randoContext = Rando::Context::GetInstance(); RandomizerCheck rc = payload["rc"].get(); + if (rc < 0 || rc >= RC_MAX) { + SPDLOG_ERROR("[Anchor] SET_CHECK_STATUS: rc {} out of range", (int)rc); + return; + } RandomizerCheckStatus status = payload["status"].get(); bool skipped = payload["skipped"].get(); diff --git a/soh/soh/Network/Anchor/Packets/SetFlag.cpp b/soh/soh/Network/Anchor/Packets/SetFlag.cpp index 3468bab016..8b12bf6487 100644 --- a/soh/soh/Network/Anchor/Packets/SetFlag.cpp +++ b/soh/soh/Network/Anchor/Packets/SetFlag.cpp @@ -41,6 +41,13 @@ void Anchor::HandlePacket_SetFlag(nlohmann::json payload) { s16 flagType = payload["flagType"].get(); s16 flag = payload["flag"].get(); + // sceneNum == SCENE_ID_MAX is a sentinel meaning "global flag" (handled below); only larger + // values would index gSaveContext.sceneFlags out of bounds. + if (sceneNum < 0 || sceneNum > SCENE_ID_MAX) { + SPDLOG_ERROR("[Anchor] SET_FLAG: sceneNum {} out of range", sceneNum); + return; + } + if (sceneNum == SCENE_ID_MAX) { auto effect = new GameInteractionEffect::SetFlag(); effect->parameters[0] = flagType; diff --git a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp index 1d50be4495..2c000b1ce5 100644 --- a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp +++ b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp @@ -39,6 +39,12 @@ void Anchor::HandlePacket_TeleportTo(nlohmann::json payload) { s32 entranceIndex = payload["entranceIndex"].get(); s8 roomIndex = payload["roomIndex"].get(); + + if (entranceIndex < 0 || roomIndex < 0) { + SPDLOG_ERROR("[Anchor] TELEPORT_TO: invalid entranceIndex {} or roomIndex {}", entranceIndex, (int)roomIndex); + return; + } + PosRot posRot = payload["posRot"].get(); gPlayState->nextEntranceIndex = entranceIndex; diff --git a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp index cf2fbf5d40..e5e7de7a8b 100644 --- a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp +++ b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp @@ -41,6 +41,13 @@ void Anchor::HandlePacket_UnsetFlag(nlohmann::json payload) { s16 flagType = payload["flagType"].get(); s16 flag = payload["flag"].get(); + // sceneNum == SCENE_ID_MAX is a sentinel meaning "global flag" (handled below); only larger + // values would index gSaveContext.sceneFlags out of bounds. + if (sceneNum < 0 || sceneNum > SCENE_ID_MAX) { + SPDLOG_ERROR("[Anchor] UNSET_FLAG: sceneNum {} out of range", sceneNum); + return; + } + if (sceneNum == SCENE_ID_MAX) { auto effect = new GameInteractionEffect::UnsetFlag(); effect->parameters[0] = flagType; diff --git a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp index 9e0a432009..ed44356e33 100644 --- a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp +++ b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp @@ -1,6 +1,7 @@ #include "soh/Network/Anchor/Anchor.h" #include #include +#include #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/OTRGlobals.h" @@ -33,6 +34,12 @@ void Anchor::HandlePacket_UpdateDungeonItems(nlohmann::json payload) { } u16 mapIndex = payload["mapIndex"].get(); + // dungeonKeys is shorter than dungeonItems (19 vs 20), so bound by the smaller of the two. + if (mapIndex >= ARRAY_COUNT(gSaveContext.inventory.dungeonItems) || + mapIndex >= ARRAY_COUNT(gSaveContext.inventory.dungeonKeys)) { + SPDLOG_ERROR("[Anchor] UPDATE_DUNGEON_ITEMS: mapIndex {} out of range", mapIndex); + return; + } gSaveContext.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get(); gSaveContext.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get(); } diff --git a/soh/soh/Network/CrowdControl/CrowdControl.cpp b/soh/soh/Network/CrowdControl/CrowdControl.cpp index 19ac74f276..7bbfb64b7e 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.cpp +++ b/soh/soh/Network/CrowdControl/CrowdControl.cpp @@ -29,19 +29,19 @@ void CrowdControl::OnDisconnected() { } void CrowdControl::OnIncomingJson(nlohmann::json payload) { - Effect* incomingEffect = ParseMessage(payload); + std::unique_ptr incomingEffect = ParseMessage(payload); if (!incomingEffect) { return; } // If effect is not a timed effect, execute and return result. if (!incomingEffect->timeRemaining) { - EffectResult result = CrowdControl::ExecuteEffect(incomingEffect); + EffectResult result = CrowdControl::ExecuteEffect(incomingEffect.get()); EmitMessage(incomingEffect->id, incomingEffect->timeRemaining, result); } else { // If another timed effect is already active that conflicts with the incoming effect. bool isConflictingEffectActive = false; - for (Effect* effect : activeEffects) { + for (const auto& effect : activeEffects) { if (effect != incomingEffect && effect->category == incomingEffect->category && effect->id < incomingEffect->id) { isConflictingEffectActive = true; @@ -52,14 +52,14 @@ void CrowdControl::OnIncomingJson(nlohmann::json payload) { if (!isConflictingEffectActive) { // Check if effect can be applied, if it can't, let CC know. - EffectResult result = CrowdControl::CanApplyEffect(incomingEffect); + EffectResult result = CrowdControl::CanApplyEffect(incomingEffect.get()); if (result == EffectResult::Retry || result == EffectResult::Failure) { EmitMessage(incomingEffect->id, incomingEffect->timeRemaining, result); return; } activeEffectsMutex.lock(); - activeEffects.push_back(incomingEffect); + activeEffects.push_back(std::move(incomingEffect)); activeEffectsMutex.unlock(); } } @@ -74,17 +74,15 @@ void CrowdControl::ProcessActiveEffects() { auto it = activeEffects.begin(); while (it != activeEffects.end()) { - Effect* effect = *it; + Effect* effect = it->get(); EffectResult result = CrowdControl::ExecuteEffect(effect); if (result == EffectResult::Success) { // If time remaining has reached 0, we have finished the effect. if (effect->timeRemaining <= 0) { - it = activeEffects.erase(std::remove(activeEffects.begin(), activeEffects.end(), effect), - activeEffects.end()); GameInteractor::RemoveEffect( *dynamic_cast(effect->giEffect.get())); - delete effect; + it = activeEffects.erase(it); } else { // If we have a success after previously being paused, tell CC to resume timer. if (effect->isPaused) { @@ -168,7 +166,7 @@ CrowdControl::EffectResult CrowdControl::TranslateGiEnum(GameInteractionEffectQu return result; } -CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { +std::unique_ptr CrowdControl::ParseMessage(nlohmann::json dataReceived) { if (!dataReceived.contains("id") || !dataReceived.contains("type")) { SPDLOG_ERROR("[CrowdControl] Invalid payload received:\n{}", dataReceived.dump()); return nullptr; @@ -176,13 +174,16 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { SPDLOG_INFO("[CrowdControl] Received payload:\n{}", dataReceived.dump()); - if (!dataReceived.contains("code")) { + // "parameters" is intentionally not required: most effects (spawn enemies, teleports, status + // effects, etc.) carry no parameters. Its absence is handled safely below, and any type error + // is caught by the guard in Network::HandleRemoteJson. + if (!dataReceived.contains("code") || !dataReceived.contains("viewer")) { // This seems to happen when the CC session ends - SPDLOG_ERROR("[CrowdControl] Payload does not contain code, ignoring."); + SPDLOG_ERROR("[CrowdControl] Payload does not contain code or viewer, ignoring."); return nullptr; } - Effect* effect = new Effect(); + auto effect = std::make_unique(); effect->lastExecutionResult = EffectResult::Initiate; effect->id = dataReceived["id"]; effect->viewerName = dataReceived["viewer"]; @@ -194,9 +195,15 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { receivedParameter = dataReceived["parameters"][0]; } + auto it = effectStringToEnum.find(effectName); + if (it == effectStringToEnum.end()) { + SPDLOG_ERROR("[CrowdControl] Unknown effect code: {}", effectName); + return nullptr; + } + // Assign GameInteractionEffect + values to CC effect. // Categories are mostly used for checking for conflicting timed effects. - switch (effectStringToEnum[effectName]) { + switch (it->second) { // Spawn Enemies and Objects case kEffectSpawnCuccoStorm: diff --git a/soh/soh/Network/CrowdControl/CrowdControl.h b/soh/soh/Network/CrowdControl/CrowdControl.h index 5cc0883d42..8effe1ebed 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.h +++ b/soh/soh/Network/CrowdControl/CrowdControl.h @@ -64,14 +64,14 @@ class CrowdControl : public Network { std::thread ccThreadProcess; - std::vector activeEffects; + std::vector> activeEffects; std::mutex activeEffectsMutex; void HandleRemoteData(nlohmann::json payload); void ProcessActiveEffects(); void EmitMessage(uint32_t eventId, long timeRemaining, EffectResult status); - Effect* ParseMessage(nlohmann::json payload); + std::unique_ptr ParseMessage(nlohmann::json payload); EffectResult ExecuteEffect(Effect* effect); EffectResult CanApplyEffect(Effect* effect); EffectResult TranslateGiEnum(GameInteractionEffectQueryResult giResult); diff --git a/soh/soh/Network/Network.cpp b/soh/soh/Network/Network.cpp index 6eae1f3f14..1145705079 100644 --- a/soh/soh/Network/Network.cpp +++ b/soh/soh/Network/Network.cpp @@ -157,5 +157,9 @@ void Network::HandleRemoteJson(std::string payload) { return; } - OnIncomingJson(jsonPayload); + try { + OnIncomingJson(jsonPayload); + } catch (const std::exception& e) { + SPDLOG_ERROR("[Network] Exception handling incoming JSON: {}", e.what()); + } catch (...) { SPDLOG_ERROR("[Network] Unknown exception handling incoming JSON"); } } diff --git a/soh/soh/Network/Sail/Sail.cpp b/soh/soh/Network/Sail/Sail.cpp index ddaf059c58..dcfcca8aca 100644 --- a/soh/soh/Network/Sail/Sail.cpp +++ b/soh/soh/Network/Sail/Sail.cpp @@ -2,8 +2,6 @@ #include #include #include -#include "soh/OTRGlobals.h" -#include "soh/util.h" template bool IsType(const SrcType* src) { return dynamic_cast(src) != nullptr; @@ -98,12 +96,12 @@ void Sail::OnIncomingJson(nlohmann::json payload) { return; } - GameInteractionEffectBase* giEffect = EffectFromJson(payload["effect"]); + auto giEffect = EffectFromJson(payload["effect"]); if (giEffect) { GameInteractionEffectQueryResult result; if (effectType == "remove") { - if (IsType(giEffect)) { - result = dynamic_cast(giEffect)->Remove(); + if (IsType(giEffect.get())) { + result = dynamic_cast(giEffect.get())->Remove(); } else { result = GameInteractionEffectQueryResult::NotPossible; } @@ -133,7 +131,7 @@ void Sail::OnIncomingJson(nlohmann::json payload) { } catch (...) { SPDLOG_ERROR("[Sail] Unknown exception handling remote JSON"); } } -GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { +std::unique_ptr Sail::EffectFromJson(nlohmann::json payload) { if (!payload.contains("name")) { return nullptr; } @@ -141,7 +139,7 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { std::string name = payload["name"].get(); if (name == "SetSceneFlag") { - auto effect = new GameInteractionEffect::SetSceneFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); @@ -149,7 +147,7 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { } return effect; } else if (name == "UnsetSceneFlag") { - auto effect = new GameInteractionEffect::UnsetSceneFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); @@ -157,171 +155,171 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { } return effect; } else if (name == "SetFlag") { - auto effect = new GameInteractionEffect::SetFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "UnsetFlag") { - auto effect = new GameInteractionEffect::UnsetFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "ModifyHeartContainers") { - auto effect = new GameInteractionEffect::ModifyHeartContainers(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "FillMagic") { - return new GameInteractionEffect::FillMagic(); + return std::make_unique(); } else if (name == "EmptyMagic") { - return new GameInteractionEffect::EmptyMagic(); + return std::make_unique(); } else if (name == "ModifyRupees") { - auto effect = new GameInteractionEffect::ModifyRupees(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "NoUI") { - return new GameInteractionEffect::NoUI(); + return std::make_unique(); } else if (name == "ModifyGravity") { - auto effect = new GameInteractionEffect::ModifyGravity(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyHealth") { - auto effect = new GameInteractionEffect::ModifyHealth(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetPlayerHealth") { - auto effect = new GameInteractionEffect::SetPlayerHealth(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "FreezePlayer") { - return new GameInteractionEffect::FreezePlayer(); + return std::make_unique(); } else if (name == "BurnPlayer") { - return new GameInteractionEffect::BurnPlayer(); + return std::make_unique(); } else if (name == "ElectrocutePlayer") { - return new GameInteractionEffect::ElectrocutePlayer(); + return std::make_unique(); } else if (name == "KnockbackPlayer") { - auto effect = new GameInteractionEffect::KnockbackPlayer(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyLinkSize") { - auto effect = new GameInteractionEffect::ModifyLinkSize(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "InvisibleLink") { - return new GameInteractionEffect::InvisibleLink(); + return std::make_unique(); } else if (name == "PacifistMode") { - return new GameInteractionEffect::PacifistMode(); + return std::make_unique(); } else if (name == "DisableZTargeting") { - return new GameInteractionEffect::DisableZTargeting(); + return std::make_unique(); } else if (name == "WeatherRainstorm") { - return new GameInteractionEffect::WeatherRainstorm(); + return std::make_unique(); } else if (name == "ReverseControls") { - return new GameInteractionEffect::ReverseControls(); + return std::make_unique(); } else if (name == "ForceEquipBoots") { - auto effect = new GameInteractionEffect::ForceEquipBoots(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyMovementSpeedMultiplier") { - auto effect = new GameInteractionEffect::ModifyMovementSpeedMultiplier(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "OneHitKO") { - return new GameInteractionEffect::OneHitKO(); + return std::make_unique(); } else if (name == "ModifyDefenseModifier") { - auto effect = new GameInteractionEffect::ModifyDefenseModifier(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "GiveOrTakeShield") { - auto effect = new GameInteractionEffect::GiveOrTakeShield(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "TeleportPlayer") { - auto effect = new GameInteractionEffect::TeleportPlayer(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ClearAssignedButtons") { - auto effect = new GameInteractionEffect::ClearAssignedButtons(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetTimeOfDay") { - auto effect = new GameInteractionEffect::SetTimeOfDay(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetCollisionViewer") { - return new GameInteractionEffect::SetCollisionViewer(); + return std::make_unique(); } else if (name == "RandomizeCosmetics") { - return new GameInteractionEffect::RandomizeCosmetics(); + return std::make_unique(); } else if (name == "PressButton") { - auto effect = new GameInteractionEffect::PressButton(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "PressRandomButton") { - auto effect = new GameInteractionEffect::PressRandomButton(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "AddOrTakeAmmo") { - auto effect = new GameInteractionEffect::AddOrTakeAmmo(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "RandomBombFuseTimer") { - return new GameInteractionEffect::RandomBombFuseTimer(); + return std::make_unique(); } else if (name == "DisableLedgeGrabs") { - return new GameInteractionEffect::DisableLedgeGrabs(); + return std::make_unique(); } else if (name == "RandomWind") { - return new GameInteractionEffect::RandomWind(); + return std::make_unique(); } else if (name == "RandomBonks") { - return new GameInteractionEffect::RandomBonks(); + return std::make_unique(); } else if (name == "PlayerInvincibility") { - return new GameInteractionEffect::PlayerInvincibility(); + return std::make_unique(); } else if (name == "SlipperyFloor") { - return new GameInteractionEffect::SlipperyFloor(); + return std::make_unique(); } else if (name == "SpawnEnemyWithOffset") { - auto effect = new GameInteractionEffect::SpawnEnemyWithOffset(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "SpawnActor") { - auto effect = new GameInteractionEffect::SpawnActor(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); diff --git a/soh/soh/Network/Sail/Sail.h b/soh/soh/Network/Sail/Sail.h index fa2f0ff581..418606e66c 100644 --- a/soh/soh/Network/Sail/Sail.h +++ b/soh/soh/Network/Sail/Sail.h @@ -2,12 +2,14 @@ #define NETWORK_SAIL_H #ifdef __cplusplus +#include + #include "soh/Network/Network.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" class Sail : public Network { private: - GameInteractionEffectBase* EffectFromJson(nlohmann::json payload); + std::unique_ptr EffectFromJson(nlohmann::json payload); void RegisterHooks(); public: From e93ea5b9192b0f3b55e5a7b333018e4976676a5c Mon Sep 17 00:00:00 2001 From: Pepe20129 <72659707+Pepe20129@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:52:25 +0200 Subject: [PATCH 30/30] Enemy rando cleanup 3 (#6518) * Fix bari's biris not respecting the seeded option * Randomize Peehat Larvas * Refactor `IsEnemyAllowedToSpawn` * Fix the issue where some enemies spawn above the ceiling * Partially fix twisted hallway issue * Prevent Baris from spawning Baris --- .../ExtraModes/EnemyRandomizer.cpp | 587 +++++++++++------- .../vanilla-behavior/GIVanillaBehavior.h | 11 +- .../actors/ovl_En_Peehat/z_en_peehat.c | 22 +- 3 files changed, 394 insertions(+), 226 deletions(-) diff --git a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp index e3d311535b..f518106c7c 100644 --- a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp +++ b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp @@ -1,6 +1,5 @@ #include "functions.h" #include "macros.h" -#include "soh/Enhancements/randomizer/3drando/random.hpp" #include "soh/Enhancements/randomizer/SeedContext.h" #include "soh/Enhancements/enhancementTypes.h" #include "soh/ObjectExtension/ObjectExtension.h" @@ -19,6 +18,7 @@ extern "C" { #include "src/overlays/actors/ovl_En_Blkobj/z_en_blkobj.h" #include "src/overlays/actors/ovl_En_Encount1/z_en_encount1.h" #include "src/overlays/actors/ovl_En_GeldB/z_en_geldb.h" +#include "src/overlays/actors/ovl_En_Peehat/z_en_peehat.h" #include "src/overlays/actors/ovl_En_Rr/z_en_rr.h" #include "src/overlays/actors/ovl_En_Vali/z_en_vali.h" @@ -37,8 +37,8 @@ extern std::shared_ptr mSohMenu; typedef struct EnemyEntry { const char* cvar; const char* name; - int16_t id; - int16_t params; + s16 id; + s16 params; } EnemyEntry; // clang-format off @@ -60,7 +60,7 @@ static EnemyEntry randomizedEnemySpawnTable[] = { { CVAR_ENHANCEMENT("RandomizedEnemyList.Dinolfos"), "Dinolfos", ACTOR_EN_ZF, -2 }, // Dinolfos { CVAR_ENHANCEMENT("RandomizedEnemyList.Dodongo"), "Dodongo", ACTOR_EN_DODONGO, -1 }, // Dodongo { CVAR_ENHANCEMENT("RandomizedEnemyList.FireKeese"), "Fire Keese", ACTOR_EN_FIREFLY, 1 }, // Fire Keese - // { CVAR_ENHANCEMENT("RandomizedEnemyList.FlareDancer"), "Flare Dancer", ACTOR_EN_FD, 0 }, // Flare Dancer (possible cause of crashes because of spawning flame actors on sloped ground) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.FlareDancer"), "Flare Dancer", ACTOR_EN_FD, 0 }, // Flare Dancer (possible cause of crashes because of spawning flame actors on sloped ground or overloading) { CVAR_ENHANCEMENT("RandomizedEnemyList.FloorTile"), "Floor Tile", ACTOR_EN_YUKABYUN, 0 }, // Flying Floor Tile { CVAR_ENHANCEMENT("RandomizedEnemyList.Floormaster"), "Floormaster", ACTOR_EN_FLOORMAS, 0 }, // Floormaster { CVAR_ENHANCEMENT("RandomizedEnemyList.FlyingPeahat"), "Flying Peahat", ACTOR_EN_PEEHAT, -1 }, // Flying Peahat (big grounded, doesn't spawn larva) @@ -80,16 +80,16 @@ static EnemyEntry randomizedEnemySpawnTable[] = { { CVAR_ENHANCEMENT("RandomizedEnemyList.InvisStalfos"), "Invisible Stalfos", ACTOR_EN_TEST, 0 }, // Stalfos (invisible) { CVAR_ENHANCEMENT("RandomizedEnemyList.Keese"), "Keese", ACTOR_EN_FIREFLY, 2 }, // Regular Keese { CVAR_ENHANCEMENT("RandomizedEnemyList.LargeBaba"), "Large Deku Baba", ACTOR_EN_DEKUBABA, 1 }, // Deku Baba (large) - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Leever"), "Leever", ACTOR_EN_REEBA, 0 }, // Leever Doesn't work (reliant on surface, without a spawner it kills itself too quickly) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Leever"), "Leever", ACTOR_EN_REEBA, 0 }, // Leever Doesn't work (reliant on surface, without a spawner it kills itself too quickly) { CVAR_ENHANCEMENT("RandomizedEnemyList.LikeLike"), "Like-Like", ACTOR_EN_RR, 0 }, // Like-Like { CVAR_ENHANCEMENT("RandomizedEnemyList.Lizalfos"), "Lizalfos", ACTOR_EN_ZF, -1 }, // Lizalfos { CVAR_ENHANCEMENT("RandomizedEnemyList.MadScrub"), "Mad Scrub", ACTOR_EN_DEKUNUTS, 768 }, // Mad Scrub (triple attack) (projectiles don't work) { CVAR_ENHANCEMENT("RandomizedEnemyList.NormalWolfos"), "Wolfos (Normal)", ACTOR_EN_WF, 0 }, // Wolfos (normal) - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Octorok"), "Octorok", ACTOR_EN_OKUTA, 0 }, // Octorok Doesn't work (actor directly uses water box collision to handle hiding/popping up) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Octorok"), "Octorok", ACTOR_EN_OKUTA, 0 }, // Octorok Doesn't work (actor directly uses water box collision to handle hiding/popping up) { CVAR_ENHANCEMENT("RandomizedEnemyList.PeahatLarva"), "Peahat Larva", ACTOR_EN_PEEHAT, 1 }, // Flying Peahat Larva - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 0 }, // Poe Doesn't work (Seems to rely on other objects?) - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 2 }, // Poe (composer Sharp) Doesn't work (Seems to rely on other objects?) - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 3 }, // Poe (composer Flat) Doesn't work (Seems to rely on other objects?) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 0 }, // Poe Doesn't work (Seems to rely on other objects?) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe.Sharp"), "Poe (Sharp)", ACTOR_EN_POH, 2 }, // Poe (composer Sharp) Doesn't work (Seems to rely on other objects?) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe.Flat"), "Poe (Flat)", ACTOR_EN_POH, 3 }, // Poe (composer Flat) Doesn't work (Seems to rely on other objects?) { CVAR_ENHANCEMENT("RandomizedEnemyList.Redead"), "Redead", ACTOR_EN_RD, 1 }, // Redead (standing) { CVAR_ENHANCEMENT("RandomizedEnemyList.RedTektite"), "Red Tektite", ACTOR_EN_TITE, -1 }, // Tektite (red) { CVAR_ENHANCEMENT("RandomizedEnemyList.Shabom"), "Shabom", ACTOR_EN_BUBBLE, 0 }, // Shabom (bubble) @@ -159,125 +159,205 @@ static int enemiesToRandomize[] = { // ACTOR_EN_REEBA, // Leever (reliant on spawner (z_en_encount1.c)) }; -bool IsEnemyAllowedToSpawn(int16_t sceneNum, int8_t roomNum, EnemyEntry enemy) { - uint32_t isMQ = ResourceMgr_IsSceneMasterQuest(sceneNum); - - // Freezard - Child Link can only kill this with Deku Stick jumpslash or other equipment like bombs. - // Beamos - Needs bombs. - // Anubis - Needs fire. - // Shell Blade & Spike - Child Link can't kill these with sword or Deku Stick. - // Flare dancer, Arwing & Dark Link - Both go out of bounds way too easily, softlocking the player. - // Wallmaster - Not easily visible, often makes players think they're softlocked and that there's no enemies left. - // Club Moblin - Many issues with them falling or placing out of bounds. Maybe fixable in the future? - bool enemiesToExcludeClearRooms = - enemy.id == ACTOR_EN_FZ || enemy.id == ACTOR_EN_VM || enemy.id == ACTOR_EN_SB || enemy.id == ACTOR_EN_NY || - enemy.id == ACTOR_EN_CLEAR_TAG || enemy.id == ACTOR_EN_WALLMAS || enemy.id == ACTOR_EN_TORCH2 || - (enemy.id == ACTOR_EN_MB && enemy.params == 0) || enemy.id == ACTOR_EN_FD || enemy.id == ACTOR_EN_ANUBICE_TAG; - - // Bari - Spawns 3 more enemies, potentially extremely difficult in timed rooms. - bool enemiesToExcludeTimedRooms = enemiesToExcludeClearRooms || enemy.id == ACTOR_EN_VALI; - - switch (sceneNum) { - // Deku Tree - case SCENE_DEKU_TREE: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 1 || roomNum == 9)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 4 || roomNum == 6 || roomNum == 9 || roomNum == 10))); - // Dodongo's Cavern - case SCENE_DODONGOS_CAVERN: - return (!(!isMQ && enemiesToExcludeClearRooms && roomNum == 15) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 5 || roomNum == 13 || roomNum == 14))); - // Jabu Jabu - case SCENE_JABU_JABU: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 8 || roomNum == 9)) && - !(!isMQ && enemiesToExcludeTimedRooms && roomNum == 12) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 11 || roomNum == 14))); - // Forest Temple - case SCENE_FOREST_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && - (roomNum == 6 || roomNum == 10 || roomNum == 18 || roomNum == 21)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 5 || roomNum == 6 || roomNum == 18 || roomNum == 21))); - // Fire Temple - case SCENE_FIRE_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && roomNum == 15) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 15 || roomNum == 17 || roomNum == 18))); - // Water Temple - case SCENE_WATER_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 13 || roomNum == 18 || roomNum == 19)) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 13 || roomNum == 18))); - // Spirit Temple - case SCENE_SPIRIT_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && - (roomNum == 1 || roomNum == 10 || roomNum == 17 || roomNum == 20)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 1 || roomNum == 2 || roomNum == 4 || roomNum == 10 || roomNum == 15 || - roomNum == 19 || roomNum == 20))); - // Shadow Temple - case SCENE_SHADOW_TEMPLE: - return ( - !(!isMQ && enemiesToExcludeClearRooms && - (roomNum == 1 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 16 || roomNum == 17 || - roomNum == 19 || roomNum == 20)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 1 || roomNum == 6 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 20))); - // Ganon's Castle Trials - case SCENE_INSIDE_GANONS_CASTLE: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 2 || roomNum == 5 || roomNum == 9)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 0 || roomNum == 2 || roomNum == 5 || roomNum == 9))); - // Ice Caverns - case SCENE_ICE_CAVERN: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 1 || roomNum == 7)) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 3 || roomNum == 7))); - // Bottom of the Well - // Exclude Dark Link from room with holes in the floor because it can pull you in a like-like making the player - // fall down. - case SCENE_BOTTOM_OF_THE_WELL: - return (!(!isMQ && enemy.id == ACTOR_EN_TORCH2 && roomNum == 3)); - // Don't allow Dark Link in areas with lava void out zones as it voids out the player as well. - // Gerudo Training Ground. - case SCENE_GERUDO_TRAINING_GROUND: - return (!(enemy.id == ACTOR_EN_TORCH2 && roomNum == 6) && - !(!isMQ && enemiesToExcludeTimedRooms && (roomNum == 1 || roomNum == 7)) && - !(!isMQ && enemiesToExcludeClearRooms && (roomNum == 3 || roomNum == 5 || roomNum == 10)) && - !(isMQ && enemiesToExcludeTimedRooms && - (roomNum == 1 || roomNum == 3 || roomNum == 5 || roomNum == 7)) && - !(isMQ && enemiesToExcludeClearRooms && roomNum == 10)); - // Don't allow certain enemies in Ganon's Tower because they would spawn up on the ceiling, - // becoming impossible to kill. - // Ganon's Tower. - case SCENE_GANONS_TOWER: - return (!(enemiesToExcludeClearRooms || enemy.id == ACTOR_EN_VALI || - (enemy.id == ACTOR_EN_ZF && enemy.params == -1))); - // Ganon's Tower Escape. - case SCENE_GANONS_TOWER_COLLAPSE_INTERIOR: - return (!((enemiesToExcludeTimedRooms || (enemy.id == ACTOR_EN_ZF && enemy.params == -1)) && roomNum == 1)); - // Don't allow big Stalchildren, big Peahats and the large Bari (jellyfish) during the Gohma fight because they - // can clip into Gohma and it crashes the game. Likely because Gohma on the ceiling can't handle collision with - // other enemies. - case SCENE_DEKU_TREE_BOSS: - return (!enemiesToExcludeTimedRooms && !(enemy.id == ACTOR_EN_SKB && enemy.params == 20) && - !(enemy.id == ACTOR_EN_PEEHAT && enemy.params == -1)); - // Grottos. - case SCENE_GROTTOS: - return (!(enemiesToExcludeClearRooms && (roomNum == 2 || roomNum == 7))); - // Royal Grave. - case SCENE_ROYAL_FAMILYS_TOMB: - return (!(enemiesToExcludeClearRooms && roomNum == 0)); - // Don't allow Dark Link in areas with lava void out zones as it voids out the player as well. - // Death Mountain Crater. - case SCENE_DEATH_MOUNTAIN_CRATER: - return (enemy.id != ACTOR_EN_TORCH2); +static bool IsExcludedFromClearRooms(s16 enemyId, s16 enemyParams) { + switch (enemyId) { + // Freezard - Child Link can only kill this with Deku Stick jumpslash or other equipment like bombs + case ACTOR_EN_FZ: + // Beamos - Needs bombs + case ACTOR_EN_VM: + // Shell Blade - It's annoying to kill these as Child Link with sword or Deku Stick + case ACTOR_EN_SB: + // Spike - Child Link can't kill these with sword or Deku Stick + case ACTOR_EN_NY: + // Arwing - Goes out of bounds way too easily, softlocking the player + case ACTOR_EN_CLEAR_TAG: + // Wallmaster - Not easily visible, often makes players think they're softlocked and that there's no enemies + // left + case ACTOR_EN_WALLMAS: + // Dark Link - Goes out of bounds way too easily, softlocking the player + case ACTOR_EN_TORCH2: + // Flare dancer - Goes out of bounds way too easily, softlocking the player + case ACTOR_EN_FD: + // Anubis - Needs fire + case ACTOR_EN_ANUBICE_TAG: + return true; + case ACTOR_EN_MB: + return enemyParams == 0; default: - return 1; + return false; } } +static bool IsExcludedFromTimedRooms(s16 enemyId, s16 enemyParams) { + switch (enemyId) { + // Bari - Spawns 3 more enemies, potentially extremely difficult in timed rooms + case ACTOR_EN_VALI: + return true; + default: + return IsExcludedFromClearRooms(enemyId, enemyParams); + } +} + +static bool IsClearRoom(bool mq, s16 sceneNum, s8 roomNum) { + switch (sceneNum) { + case SCENE_DEKU_TREE: + if (mq) { + return roomNum == 4 || roomNum == 6 || roomNum == 9 || roomNum == 10; + } else { + return roomNum == 1 || roomNum == 9; + } + case SCENE_DODONGOS_CAVERN: + if (mq) { + return roomNum == 5 || roomNum == 6 || roomNum == 13 || roomNum == 14; + } else { + return roomNum == 15; + } + case SCENE_JABU_JABU: + if (mq) { + return roomNum == 11 || roomNum == 13 || roomNum == 14; + } else { + return roomNum == 8 || roomNum == 9; + } + case SCENE_FOREST_TEMPLE: + if (mq) { + return roomNum == 5 || roomNum == 6 || roomNum == 18 || roomNum == 21; + } else { + return roomNum == 6 || roomNum == 10 || roomNum == 18 || roomNum == 21; + } + case SCENE_FIRE_TEMPLE: + if (mq) { + return roomNum == 15 || roomNum == 17 || roomNum == 18; + } else { + return roomNum == 15; + } + case SCENE_WATER_TEMPLE: + if (mq) { + return roomNum == 13 || roomNum == 18; + } else { + return roomNum == 13 || roomNum == 18 || roomNum == 19; + } + case SCENE_SPIRIT_TEMPLE: + if (mq) { + return roomNum == 1 || roomNum == 2 || roomNum == 4 || roomNum == 10 || roomNum == 15 || + roomNum == 19 || roomNum == 20; + } else { + return roomNum == 1 || roomNum == 10 || roomNum == 17 || roomNum == 20 || roomNum == 27; + } + case SCENE_SHADOW_TEMPLE: + if (mq) { + return roomNum == 1 || roomNum == 6 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 20; + } else { + return roomNum == 1 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 16 || + roomNum == 17 || roomNum == 19 || roomNum == 20; + } + case SCENE_INSIDE_GANONS_CASTLE: + if (mq) { + return roomNum == 0 || roomNum == 2 || roomNum == 5 || roomNum == 9; + } else { + return roomNum == 2 || roomNum == 5 || roomNum == 9; + } + case SCENE_ICE_CAVERN: + if (mq) { + return roomNum == 3 || roomNum == 7; + } else { + return roomNum == 1 || roomNum == 7; + } + case SCENE_GERUDO_TRAINING_GROUND: + if (mq) { + return roomNum == 10; + } else { + return roomNum == 3 || roomNum == 5 || roomNum == 10; + } + case SCENE_GANONS_TOWER: + return true; + case SCENE_GROTTOS: + return roomNum == 2 || roomNum == 7; + case SCENE_ROYAL_FAMILYS_TOMB: + return roomNum == 0; + default: + return false; + } +} + +static bool IsTimedRoom(bool mq, s16 sceneNum, s8 roomNum) { + switch (sceneNum) { + case SCENE_JABU_JABU: + return !mq && roomNum == 12; + case SCENE_GERUDO_TRAINING_GROUND: + if (mq) { + return roomNum == 1 || roomNum == 3 || roomNum == 5 || roomNum == 7; + } else { + return roomNum == 1 || roomNum == 7; + } + case SCENE_GANONS_TOWER_COLLAPSE_INTERIOR: + return roomNum == 1; + default: + return false; + } +} + +static bool IsEnemyAllowedToSpawn(s16 sceneNum, s8 roomNum, EnemyEntry enemy, s16 posY, bool fromBari) { + bool mq = ResourceMgr_IsSceneMasterQuest(sceneNum); + + if (IsExcludedFromClearRooms(enemy.id, enemy.params) && IsClearRoom(mq, sceneNum, roomNum)) { + return false; + } + + if (IsExcludedFromTimedRooms(enemy.id, enemy.params) && IsTimedRoom(mq, sceneNum, roomNum)) { + return false; + } + + // Don't allow Lizalfos or Baris in Ganon's Tower because they would spawn up on the ceiling, becoming impossible to + // kill. + if (sceneNum == SCENE_GANONS_TOWER && + (enemy.id == ACTOR_EN_VALI || (enemy.id == ACTOR_EN_ZF && enemy.params == -1))) { + return false; + } + + // Don't allow Lizalfos in the first room of the interior of the castle collapse + if (sceneNum == SCENE_GANONS_TOWER_COLLAPSE_INTERIOR && roomNum == 1 && enemy.id == ACTOR_EN_ZF && + enemy.params == -1) { + return false; + } + + // Don't allow big Stalchildren, big Peahats and Baris (big jellyfish) during the Gohma fight because they can clip + // into Gohma and it crashes the game. Likely because Gohma on the ceiling can't handle collision with other + // enemies. + if (sceneNum == SCENE_DEKU_TREE_BOSS && + ((enemy.id == ACTOR_EN_SKB && enemy.params == 20) || (enemy.id == ACTOR_EN_PEEHAT && enemy.params == -1) || + (enemy.id == ACTOR_EN_VALI))) { + return false; + } + + // Don't allow the following enemies in the first spawn of the first room in MQ Fire Temple loop as when spawned and + // they get stuck in the room above + // - Lizalfos/Dinolfos, Bari: they drop in + // - Skulltulla: they appear above + // - Flying Peehat: they rise above the ceiling + if (mq && sceneNum == SCENE_FIRE_TEMPLE && roomNum == 15 && posY == 64 && + (enemy.id == ACTOR_EN_ZF || enemy.id == ACTOR_EN_VALI || enemy.id == ACTOR_EN_ST || + enemy.id == ACTOR_EN_PEEHAT)) { + return false; + } + + // Don't allow Stalfos in the child spirit clear room as they jump out of bounds frequently + if (sceneNum == SCENE_SPIRIT_TEMPLE && roomNum == 1 && enemy.id == ACTOR_EN_TEST) { + return false; + } + + // Don't allow baris to spawn another bari + if (fromBari && enemy.id == ACTOR_EN_VALI) { + return false; + } + + return true; +} + static std::vector selectedEnemyList; -void GetSelectedEnemies() { +static void UpdateSelectedEnemies() { selectedEnemyList.clear(); + for (int i = 0; i < ARRAY_COUNT(randomizedEnemySpawnTable); i++) { if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemyList.All"), 0)) { selectedEnemyList.push_back(randomizedEnemySpawnTable[i]); @@ -285,108 +365,119 @@ void GetSelectedEnemies() { selectedEnemyList.push_back(randomizedEnemySpawnTable[i]); } } + if (selectedEnemyList.size() == 0) { selectedEnemyList.push_back(randomizedEnemySpawnTable[0]); } } -EnemyEntry GetRandomizedEnemyEntry(uint32_t seed, PlayState* play) { +static EnemyEntry GetRandomizedEnemyEntry(u32 seed, PlayState* play, s16 posY, bool fromBari) { std::vector filteredEnemyList = {}; + if (selectedEnemyList.size() == 0) { - GetSelectedEnemies(); + UpdateSelectedEnemies(); } + for (EnemyEntry enemy : selectedEnemyList) { - if (IsEnemyAllowedToSpawn(play->sceneNum, play->roomCtx.curRoom.num, enemy)) { + if (IsEnemyAllowedToSpawn(play->sceneNum, play->roomCtx.curRoom.num, enemy, posY, fromBari)) { filteredEnemyList.push_back(enemy); } } + if (filteredEnemyList.size() == 0) { filteredEnemyList = selectedEnemyList; } + if (CVAR_ENEMY_RANDOMIZER_VALUE == ENEMY_RANDOMIZER_RANDOM_SEEDED) { - uint32_t finalSeed = - seed + (IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : gSaveContext.ship.stats.fileCreatedAt); - Random_Init(finalSeed); - uint32_t randomNumber = Random(0, filteredEnemyList.size()); - return filteredEnemyList[randomNumber]; - } else { - uint32_t randomSelectedEnemy = Random(0, filteredEnemyList.size()); - return filteredEnemyList[randomSelectedEnemy]; + uint64_t randomState = 0; + + ShipUtils::RandInit( + seed + (IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : gSaveContext.ship.stats.fileCreatedAt), + &randomState); + + return ShipUtils::RandomElement(filteredEnemyList, false, &randomState); } + + return ShipUtils::RandomElement(filteredEnemyList, false); } -bool IsEnemyFoundToRandomize(int16_t sceneNum, int8_t roomNum, int16_t actorId, int16_t params, float posX) { - - uint32_t isMQ = ResourceMgr_IsSceneMasterQuest(sceneNum); +static bool IsEnemyFoundToRandomize(s16 sceneNum, s8 roomNum, s16 actorId, s16 params, f32 posX) { + u32 isMQ = ResourceMgr_IsSceneMasterQuest(sceneNum); for (int i = 0; i < ARRAY_COUNT(enemiesToRandomize); i++) { - if (actorId == enemiesToRandomize[i]) { - switch (actorId) { - // Only randomize the main component of Electric Tailparasans, not the tail segments they spawn. - case ACTOR_EN_TP: - return (params == -1); - // Only randomize the initial Deku Scrub actor (single and triple attack), not the flower they spawn. - case ACTOR_EN_DEKUNUTS: - return (params == -256 || params == 768); - // Don't randomize the OoB wallmaster in the Silver Rupee room because it's only there to - // not trigger unlocking the door after killing the other wallmaster in authentic gameplay. - case ACTOR_EN_WALLMAS: - return (!(!isMQ && sceneNum == SCENE_GERUDO_TRAINING_GROUND && roomNum == 2 && posX == -2345)); - // Only randomize initial Floormaster actor (it can split and does some spawning on init). - case ACTOR_EN_FLOORMAS: - return (params == 0 || params == -32768); - // Only randomize the initial eggs, not the enemies that spawn from them. - case ACTOR_EN_GOMA: - return (params >= 0 && params <= 9); - // Only randomize Skullwalltulas, not Golden Skulltulas. - case ACTOR_EN_SW: - return (params == 0); - // Don't randomize Nabooru because it'll break the cutscene and the door. - // Don't randomize Iron Knuckle in MQ Spirit Trial because it's needed to - // break the thrones in the room to access a button. - case ACTOR_EN_IK: - return (params != 1280 && !(isMQ && sceneNum == SCENE_INSIDE_GANONS_CASTLE && roomNum == 17)); - // Only randomize the initial spawn of the huge jellyfish. It spawns another copy when hit with a sword. - case ACTOR_EN_VALI: - return (params == -1); - // Don't randomize Lizalfos in Dodongo's Cavern because the gates won't work correctly otherwise. - case ACTOR_EN_ZF: - return (params != 1280 && params != 1281 && params != 1536 && params != 1537); - // Don't randomize the Wolfos in SFM because it's needed to open the gate. - case ACTOR_EN_WF: - return (params != 7936); - // Don't randomize the Stalfos in Forest Temple because other enemies fall through the hole and don't - // trigger the platform. Don't randomize the Stalfos spawning on the boat in Shadow Temple, as - // randomizing them places the new enemies down in the river. - case ACTOR_EN_TEST: - return (params != 1 && !(sceneNum == SCENE_SHADOW_TEMPLE && roomNum == 21)); - // Only randomize the enemy variant of Armos Statue. - // Leave one Armos unrandomized in the Spirit Temple room where an armos is needed to push down a - // button. - case ACTOR_EN_AM: - return ((params == -1 || params == 255) && !(sceneNum == SCENE_SPIRIT_TEMPLE && posX == 2141)); - // Don't randomize Shell Blades and Spikes in the underwater portion in Water Temple as it's impossible - // to kill most other enemies underwater with just hookshot and they're required to be killed for a - // grate to open. - case ACTOR_EN_SB: - case ACTOR_EN_NY: - return (!(!isMQ && sceneNum == SCENE_WATER_TEMPLE && roomNum == 2)); - case ACTOR_EN_SKJ: - return !(sceneNum == SCENE_LOST_WOODS && LINK_IS_CHILD); - default: - return 1; - } + if (actorId != enemiesToRandomize[i]) { + continue; + } + + switch (actorId) { + // Only randomize the main component of Electric Tailparasans, not the tail segments they spawn. + case ACTOR_EN_TP: + return params == -1; + // Only randomize the initial Deku Scrub actor (single and triple attack), not the flower they spawn. + case ACTOR_EN_DEKUNUTS: + return params == -256 || params == 768; + // Don't randomize the OoB wallmaster in the Silver Rupee room because it's only there to + // not trigger unlocking the door after killing the other wallmaster in authentic gameplay. + case ACTOR_EN_WALLMAS: + return !(!isMQ && sceneNum == SCENE_GERUDO_TRAINING_GROUND && roomNum == 2 && posX == -2345); + // Only randomize initial Floormaster actor (it can split and does some spawning on init). + case ACTOR_EN_FLOORMAS: + return params == 0 || params == -32768; + // Only randomize the initial eggs, not the enemies that spawn from them. + case ACTOR_EN_GOMA: + return params >= 0 && params <= 9; + // Only randomize Skullwalltulas, not Golden Skulltulas. + case ACTOR_EN_SW: + return params == 0; + // Don't randomize Nabooru because it'll break the cutscene and the door. + // Don't randomize Iron Knuckle in MQ Spirit Trial because it's needed to + // break the thrones in the room to access a button. + case ACTOR_EN_IK: + return params != 1280 && !(isMQ && sceneNum == SCENE_INSIDE_GANONS_CASTLE && roomNum == 17); + // Only randomize the initial spawn of the huge jellyfish. It spawns another copy when hit with a sword. + case ACTOR_EN_VALI: + return params == -1; + // Don't randomize Lizalfos in Dodongo's Cavern because the gates won't work correctly otherwise. + case ACTOR_EN_ZF: + return params != 1280 && params != 1281 && params != 1536 && params != 1537; + // Don't randomize the right baby dodongo on the first tunnel in Dodongo's Cavern as in vanilla you use them + // isntead of bombs to blow up a wall + case ACTOR_EN_DODOJR: + return !(sceneNum == SCENE_DODONGOS_CAVERN && roomNum == 1 && posX == 1972); + // Don't randomize the Wolfos in SFM because it's needed to open the gate. + case ACTOR_EN_WF: + return params != 7936; + // Don't randomize the Stalfos in Forest Temple because other enemies fall through the hole and don't + // trigger the platform. Don't randomize the Stalfos spawning on the boat in Shadow Temple, as + // randomizing them places the new enemies down in the river. + case ACTOR_EN_TEST: + return params != 1 && !(sceneNum == SCENE_SHADOW_TEMPLE && roomNum == 21); + // Only randomize the enemy variant of Armos Statue. + // Leave one Armos unrandomized in the Spirit Temple room where an armos is needed to push down a + // button. + case ACTOR_EN_AM: + return (params == -1 || params == 255) && !(sceneNum == SCENE_SPIRIT_TEMPLE && posX == 2141); + // Don't randomize Shell Blades and Spikes in the underwater portion in Water Temple as it's impossible + // to kill most other enemies underwater with just hookshot and they're required to be killed for a + // grate to open. + case ACTOR_EN_SB: + case ACTOR_EN_NY: + return !(!isMQ && sceneNum == SCENE_WATER_TEMPLE && roomNum == 2); + // Don't randomize Skull Kids in Lost Woods as child as they're not enemies + case ACTOR_EN_SKJ: + return !(sceneNum == SCENE_LOST_WOODS && LINK_IS_CHILD); + default: + return true; } } // If no enemy is found, don't randomize the actor. - return 0; + return false; } -uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* posY, s16* posZ, int16_t* rotX, - int16_t* rotY, int16_t* rotZ, int16_t* params) { - - uint32_t isMQ = ResourceMgr_IsSceneMasterQuest(play->sceneNum); +static u8 GetRandomizedEnemy(PlayState* play, s16* actorId, s16* posX, s16* posY, s16* posZ, s16* rotX, s16* rotY, + s16* rotZ, s16* params, s16 offset = 0, bool fromBari = false) { + u32 isMQ = ResourceMgr_IsSceneMasterQuest(play->sceneNum); // Hack to remove enemies that wrongfully spawn because of bypassing object dependency with enemy randomizer on. // This should probably be handled on OTR generation in the future when object dependency is fully removed. @@ -412,7 +503,6 @@ uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* po } if (IsEnemyFoundToRandomize(play->sceneNum, play->roomCtx.curRoom.num, *actorId, *params, *posX)) { - // When replacing Iron Knuckles in Spirit Temple, move them away from the throne because // some enemies can get stuck on the throne. if (*actorId == ACTOR_EN_IK && play->sceneNum == SCENE_SPIRIT_TEMPLE) { @@ -443,17 +533,27 @@ uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* po pos.x = *posX; pos.y = *posY + 50; pos.z = *posZ; - raycastResult = BgCheck_AnyRaycastFloor1(&play->colCtx, &poly, &pos); - // If ground is found below actor, move actor to that height. - if (raycastResult > BGCHECK_Y_MIN) { - *posY = raycastResult; + // the forest temple second twisted hallway spawns after the enemies so we need to "find the floor" manually + if (play->sceneNum == SCENE_FOREST_TEMPLE && play->roomCtx.curRoom.num == 20 && *posZ > -3000) { + // when hallway is twisted (play->actorCtx.flags.tempSwch & 1), one spawn has the floor at 1235.165 & + // the other at 1239.094 but that changes based on the player position + // when not twisted, the whole floor is at 1228 + + *posY = 1228.0; + } else { + raycastResult = BgCheck_AnyRaycastFloor1(&play->colCtx, &poly, &pos); + + // If ground is found below actor, move actor to that height. + if (raycastResult > BGCHECK_Y_MIN) { + *posY = raycastResult; + } } // Get randomized enemy ID and parameter. - uint32_t seed = - play->sceneNum + *actorId + (int)*posX + (int)*posY + (int)*posZ + *rotX + *rotY + *rotZ + *params; - EnemyEntry randomEnemy = GetRandomizedEnemyEntry(seed, play); + u32 seed = + play->sceneNum + *actorId + (int)*posX + (int)*posY + (int)*posZ + *rotX + *rotY + *rotZ + *params + offset; + EnemyEntry randomEnemy = GetRandomizedEnemyEntry(seed, play, *posY, fromBari); *actorId = randomEnemy.id; *params = randomEnemy.params; @@ -528,6 +628,25 @@ void CustomStalfosPairFightDestroy(Actor* thisx, PlayState* play) { ObjectExtension::GetInstance().Remove(thisx); } +struct CustomPeehatLarvaData { + EnPeehat* peehat = nullptr; + ActorFunc originalDestroy = nullptr; +}; + +static ObjectExtension::Register CustomPeehatLarvaDataRegister; + +void CustomPeehatLarvaDestroy(Actor* thisx, PlayState* play) { + assert(ObjectExtension::GetInstance().Has(thisx)); + + CustomPeehatLarvaData* customPeehatLarvaData = ObjectExtension::GetInstance().Get(thisx); + + customPeehatLarvaData->peehat->unk_2FA -= 1; + + customPeehatLarvaData->originalDestroy(thisx, play); + + ObjectExtension::GetInstance().Remove(thisx); +} + void RegisterEnemyRandomizer() { COND_ID_HOOK(OnActorInit, ACTOR_EN_MB, ENEMY_RANDOMIZER_ENABLED, FixClubMoblinScale); @@ -756,14 +875,18 @@ void RegisterEnemyRandomizer() { s16 rotZ = 0; s16 params = 0; - for (s32 i = 0; i < 3; i++) { - // Offset small jellyfish with Enemy Randomizer, otherwise it gets - // stuck in a loop spawning more big jellyfish with seeded spawns. - if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0)) { - rotY += rand() % 50; - } + s16 homePosX = vali->actor.home.pos.x; + s16 homePosY = vali->actor.home.pos.y; + s16 homePosZ = vali->actor.home.pos.z; - if (!GetRandomizedEnemy(play, &actorId, &posX, &posY, &posZ, &rotX, &rotY, &rotZ, ¶ms)) { + s16 homeRotX = vali->actor.home.rot.x; + s16 homeRotY = vali->actor.home.rot.y; + s16 homeRotZ = vali->actor.home.rot.z; + + for (s32 i = 0; i < 3; i++) { + // use the home pos & rot to make it consistent + if (!GetRandomizedEnemy(play, &actorId, &homePosX, &homePosY, &homePosZ, &homeRotX, &homeRotY, &homeRotZ, + ¶ms, i * 1000, true)) { assert(false); } @@ -919,9 +1042,43 @@ void RegisterEnemyRandomizer() { *should = false; }); + + COND_VB_SHOULD(VB_PEEHAT_SPAWN_LARVAS, ENEMY_RANDOMIZER_ENABLED, { + EnPeehat* peehat = va_arg(args, EnPeehat*); + PlayState* play = va_arg(args, PlayState*); + + s16 actorId = ACTOR_EN_PEEHAT; + s16 homePosX = peehat->actor.home.pos.x; + s16 homePosY = peehat->actor.home.pos.y + 50.0f; + s16 homePosZ = peehat->actor.home.pos.z; + s16 rotX = 0; + s16 rotY = 0; + s16 rotZ = 0; + s16 params = PEAHAT_TYPE_LARVA; + + // 3 is MAX_LARVA + for (s32 i = 3 - peehat->unk_2FA; i > 0; i--) { + if (!GetRandomizedEnemy(play, &actorId, &homePosX, &homePosY, &homePosZ, &rotX, &rotY, &rotZ, ¶ms, + i * 1000)) { + assert(false); + } + + Actor* enemy = + Actor_Spawn(&play->actorCtx, play, actorId, homePosX, homePosY, homePosZ, rotX, rotY, rotZ, params); + + if (enemy == NULL) { + assert(false); + } else { + peehat->unk_2FA++; + ObjectExtension::GetInstance().Set( + enemy, CustomPeehatLarvaData{ .peehat = peehat, .originalDestroy = enemy->destroy }); + enemy->destroy = CustomPeehatLarvaDestroy; + } + } + }); } -static const std::map enemyRandomizerModes = { +static const std::map enemyRandomizerModes = { { ENEMY_RANDOMIZER_OFF, "Disabled" }, { ENEMY_RANDOMIZER_RANDOM, "Random" }, { ENEMY_RANDOMIZER_RANDOM_SEEDED, "Random (Seeded)" }, @@ -932,7 +1089,7 @@ void RegisterEnemyRandomizerWidgets() { SohGui::mSohMenu->AddWidget(path, "Enemy Randomizer", WIDGET_CVAR_COMBOBOX) .CVar(CVAR_ENHANCEMENT("RandomizedEnemies")) - .Callback([](WidgetInfo& info) { GetSelectedEnemies(); }) + .Callback([](WidgetInfo& info) { UpdateSelectedEnemies(); }) .Options( UIWidgets::ComboboxOptions() .DefaultIndex(ENEMY_RANDOMIZER_OFF) @@ -963,7 +1120,7 @@ void RegisterEnemyRandomizerWidgets() { SohGui::mSohMenu->AddWidget(path, "Select all Enemies", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("RandomizedEnemyList.All")) .PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0); }) - .Callback([](WidgetInfo& info) { GetSelectedEnemies(); }); + .Callback([](WidgetInfo& info) { UpdateSelectedEnemies(); }); SohGui::mSohMenu->AddWidget(path, "Enemy List", WIDGET_SEPARATOR).PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0); @@ -978,7 +1135,7 @@ void RegisterEnemyRandomizerWidgets() { info.options->disabled = CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemyList.All"), 0); info.options->disabledTooltip = "These options are disabled because \"Select All Enemies\" is enabled."; }) - .Callback([](WidgetInfo& info) { GetSelectedEnemies(); }); + .Callback([](WidgetInfo& info) { UpdateSelectedEnemies(); }); } } diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index 09ce04580a..956711edc9 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -2784,7 +2784,16 @@ typedef enum { // ``` // #### `args` // - `*int32_t (camId)` - VB_SHOULD_LOAD_BG_IMAGE + VB_SHOULD_LOAD_BG_IMAGE, + + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*EnPeehat` + // - `*PlayState` + VB_PEEHAT_SPAWN_LARVAS, } GIVanillaBehavior; #endif diff --git a/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c b/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c index 0957398887..d48b611ea5 100644 --- a/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c +++ b/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c @@ -298,17 +298,19 @@ void EnPeehat_HitWhenGrounded(EnPeehat* this, PlayState* play) { s32 i; this->colCylinder.base.acFlags &= ~AC_HIT; - for (i = MAX_LARVA - this->unk_2FA; i > 0; i--) { - Actor* larva = - Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_PEEHAT, - Rand_CenteredFloat(25.0f) + this->actor.world.pos.x, - Rand_CenteredFloat(25.0f) + (this->actor.world.pos.y + 50.0f), - Rand_CenteredFloat(25.0f) + this->actor.world.pos.z, 0, 0, 0, PEAHAT_TYPE_LARVA); + if (GameInteractor_Should(VB_PEEHAT_SPAWN_LARVAS, true, this, play)) { + for (i = MAX_LARVA - this->unk_2FA; i > 0; i--) { + Actor* larva = + Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_PEEHAT, + Rand_CenteredFloat(25.0f) + this->actor.world.pos.x, + Rand_CenteredFloat(25.0f) + (this->actor.world.pos.y + 50.0f), + Rand_CenteredFloat(25.0f) + this->actor.world.pos.z, 0, 0, 0, PEAHAT_TYPE_LARVA); - if (larva != NULL) { - larva->velocity.y = 6.0f; - larva->shape.rot.y = larva->world.rot.y = Rand_CenteredFloat(0xFFFF); - this->unk_2FA++; + if (larva != NULL) { + larva->velocity.y = 6.0f; + larva->shape.rot.y = larva->world.rot.y = Rand_CenteredFloat(0xFFFF); + this->unk_2FA++; + } } } this->unk_2D4 = 8;