From 430f7cde07e5bcededd27050afc6c1ff16f80b50 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 7 Oct 2026 10:14:54 -0400 Subject: [PATCH 01/29] Fix name of Fire Trial silver rupee (#7328) --- soh/soh/Enhancements/randomizer/ShuffleSilver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/ShuffleSilver.cpp b/soh/soh/Enhancements/randomizer/ShuffleSilver.cpp index 0d32ba0473..f8c71a35ae 100644 --- a/soh/soh/Enhancements/randomizer/ShuffleSilver.cpp +++ b/soh/soh/Enhancements/randomizer/ShuffleSilver.cpp @@ -378,7 +378,7 @@ void Rando::StaticData::RegisterSilverLocations() { locationTable[RC_GANONS_CASTLE_UNDER_LEDGE_FOREST_SILVER] = Location::Collectable(RC_GANONS_CASTLE_UNDER_LEDGE_FOREST_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_INSIDE_GANONS_CASTLE, TWO_ACTOR_PARAMS(1651, 2021), "Under Ledge Forest Trial Silver", RHT_GANONS_CASTLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_GANONS_CASTLE_UNDER_LEDGE_FOREST_SILVER)); locationTable[RC_GANONS_CASTLE_ON_TARGET_FOREST_SILVER] = Location::Collectable(RC_GANONS_CASTLE_ON_TARGET_FOREST_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_INSIDE_GANONS_CASTLE, TWO_ACTOR_PARAMS(1634, 1550), "On Target Forest Trial Silver", RHT_GANONS_CASTLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_GANONS_CASTLE_ON_TARGET_FOREST_SILVER)); locationTable[RC_GANONS_CASTLE_TORCH_SLUG_FIRE_SILVER] = Location::Collectable(RC_GANONS_CASTLE_TORCH_SLUG_FIRE_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_INSIDE_GANONS_CASTLE, TWO_ACTOR_PARAMS(-854, -3679), "Torch Slug Fire Trial Silver", RHT_GANONS_CASTLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_GANONS_CASTLE_TORCH_SLUG_FIRE_SILVER)); - locationTable[RC_GANONS_CASTLE_FLAME_JETS_FIRE_SILVER] = Location::Collectable(RC_GANONS_CASTLE_FLAME_JETS_FIRE_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_INSIDE_GANONS_CASTLE, TWO_ACTOR_PARAMS(-1555, -2317), "Flame Jets Fire Temple Silver", RHT_GANONS_CASTLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_GANONS_CASTLE_FLAME_JETS_FIRE_SILVER)); + locationTable[RC_GANONS_CASTLE_FLAME_JETS_FIRE_SILVER] = Location::Collectable(RC_GANONS_CASTLE_FLAME_JETS_FIRE_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_INSIDE_GANONS_CASTLE, TWO_ACTOR_PARAMS(-1555, -2317), "Flame Jets Fire Trial Silver", RHT_GANONS_CASTLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_GANONS_CASTLE_FLAME_JETS_FIRE_SILVER)); locationTable[RC_GANONS_CASTLE_DISTANT_PLATFORM_FIRE_SILVER] = Location::Collectable(RC_GANONS_CASTLE_DISTANT_PLATFORM_FIRE_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_INSIDE_GANONS_CASTLE, TWO_ACTOR_PARAMS(-515, -3253), "Distant Platform Fire Trial Silver", RHT_GANONS_CASTLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_GANONS_CASTLE_DISTANT_PLATFORM_FIRE_SILVER)); locationTable[RC_GANONS_CASTLE_CLOSE_PLATFORM_FIRE_SILVER] = Location::Collectable(RC_GANONS_CASTLE_CLOSE_PLATFORM_FIRE_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_INSIDE_GANONS_CASTLE, TWO_ACTOR_PARAMS(-604, -2592), "Close Platform Fire Trial Silver", RHT_GANONS_CASTLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_GANONS_CASTLE_CLOSE_PLATFORM_FIRE_SILVER)); locationTable[RC_GANONS_GANONS_CASTLE_UNDER_PILLAR_FIRE_SILVER] = Location::Collectable(RC_GANONS_GANONS_CASTLE_UNDER_PILLAR_FIRE_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_INSIDE_GANONS_CASTLE, TWO_ACTOR_PARAMS(-1867, -2754), "Under Pillar Fire Trial Silver", RHT_GANONS_CASTLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_GANONS_GANONS_CASTLE_UNDER_PILLAR_FIRE_SILVER)); From 4ec74a4dd2b5c0860d28cfba75c53386bb50ca11 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 7 Oct 2026 21:29:15 -0400 Subject: [PATCH 02/29] Unfreeze Link on forced text skip (#7338) --- soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp index 010c409d93..80e8c0b22b 100644 --- a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp +++ b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp @@ -468,7 +468,7 @@ void TimeSaverOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_li *should = false; // We should still come out of a softlock if the setting was changed during forced dialogue - if (GET_PLAYER(gPlayState)->csAction != 7) { + if (GET_PLAYER(gPlayState)->csAction == 8) { Player_SetCsAction(gPlayState, NULL, 7); } } From 0f1f31a1e9f2b45f3b7dbdc3c918137958bd5e66 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 7 Oct 2026 21:30:13 -0400 Subject: [PATCH 03/29] Show message when no Linux file picker is found (#7339) --- soh/soh/Extractor/Extract.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/soh/soh/Extractor/Extract.cpp b/soh/soh/Extractor/Extract.cpp index 89da6e1b78..e21f83744b 100644 --- a/soh/soh/Extractor/Extract.cpp +++ b/soh/soh/Extractor/Extract.cpp @@ -318,6 +318,18 @@ bool Extractor::GetRomPathFromBox() { } mCurrentRomPath = nameBuffer; #else + if (!pfd::settings::available()) { + std::string msg = + "No file picker was found on your system. Ship of Harkinian needs one of these to select a ROM:\n\n" + " - GNOME, Xfce, Cinnamon and other GTK desktops: install \"zenity\"\n" + " - KDE Plasma: install \"kdialog\"\n\n" + "On other distributions or window managers, search your package manager.\n\n" + "Alternatively, place your ROM in:\n" + + mSearchPath + "\nand restart."; + ShowErrorBox("File Picker Unavailable", msg.c_str()); + return false; + } + auto selection = pfd::open_file("Select a file", mSearchPath, { "N64 Roms", "*.z64 *.n64 *.v64" }).result(); if (selection.empty()) { From 41702557d0316bf5d9be4b35cb0bf2aedb54603e Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 7 Oct 2026 21:31:00 -0400 Subject: [PATCH 04/29] Prevent crash when fairy spawn is unknown (#7340) --- soh/soh/Enhancements/randomizer/ShuffleFairies.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/ShuffleFairies.cpp b/soh/soh/Enhancements/randomizer/ShuffleFairies.cpp index 4eef04b33c..450dd0387e 100644 --- a/soh/soh/Enhancements/randomizer/ShuffleFairies.cpp +++ b/soh/soh/Enhancements/randomizer/ShuffleFairies.cpp @@ -64,6 +64,7 @@ CheckIdentity ShuffleFairies_GetFairyIdentity(int32_t params, ActorID id) { CheckIdentity fairyIdentity; s16 sceneNum = gPlayState->sceneNum; fairyIdentity.randomizerInf = RAND_INF_MAX; + fairyIdentity.randomizerCheck = RC_UNKNOWN_CHECK; if (sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_NIGHT || sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_RUINS) { sceneNum = SCENE_TEMPLE_OF_TIME_EXTERIOR_DAY; @@ -73,7 +74,6 @@ CheckIdentity ShuffleFairies_GetFairyIdentity(int32_t params, ActorID id) { if (location->GetRandomizerCheck() == RC_UNKNOWN_CHECK) { SPDLOG_WARN("FairyGetIdentity did not receive a valid RC value ({}).", location->GetRandomizerCheck()); - assert(false); } else { fairyIdentity.randomizerInf = static_cast(location->GetCollectionCheck().flag); fairyIdentity.randomizerCheck = location->GetRandomizerCheck(); @@ -84,6 +84,9 @@ CheckIdentity ShuffleFairies_GetFairyIdentity(int32_t params, ActorID id) { static bool SpawnFairy(f32 posX, f32 posY, f32 posZ, int32_t params, FairyType fairyType, ActorID id) { CheckIdentity fairyIdentity = ShuffleFairies_GetFairyIdentity(params, id); + if (fairyIdentity.randomizerCheck == RC_UNKNOWN_CHECK) { + return false; + } if (!Flags_GetRandomizerInf(fairyIdentity.randomizerInf)) { Actor* fairy = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_ELF, posX, posY - 30.0f, posZ, 0, 0, 0, fairyType); From c724b412b9ca49572dfadb274af4f6d73a28dc01 Mon Sep 17 00:00:00 2001 From: A Green Spoon <121978037+A-Green-Spoon@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:35:22 -0400 Subject: [PATCH 05/29] Win condition tooltip + altar hint + draw full triforce for anywhere wincon (#7341) --- soh/assets/custom/lang/en_US.json | 38 ++++++++++++++++--- soh/include/z64item.h | 5 ++- .../randomizer/3drando/hint_list.cpp | 5 +++ soh/soh/Enhancements/randomizer/draw.cpp | 4 ++ soh/soh/Enhancements/randomizer/hint.cpp | 4 +- soh/soh/Enhancements/randomizer/item_list.cpp | 2 +- .../randomizerEnums/RandomizerHintTextKey.h | 1 + soh/src/code/z_draw.c | 36 ++++++++++++++---- 8 files changed, 77 insertions(+), 18 deletions(-) diff --git a/soh/assets/custom/lang/en_US.json b/soh/assets/custom/lang/en_US.json index 4e9bf0c42c..2d989173cc 100644 --- a/soh/assets/custom/lang/en_US.json +++ b/soh/assets/custom/lang/en_US.json @@ -1159,6 +1159,38 @@ "rewards on slider does not change." ] }, + "wincon": { + "name": "Win Condition", + "description": [ + "Defeat Ganon - Defeat Ganon to complete the game.\n", + "\n", + "Anywhere - Collect the Triforce to complete the game. It can appear anywhere in the world.\n", + "\n", + "Trigger - These settings put the completing the game on a trigger, ", + "playing the credits when the requirements are met:\n", + "- Stones: Obtain the specified amount of Spiritual Stones.\n", + "- Medallions: Obtain the specified amount of medallions.\n", + "- Dungeon rewards: Obtain the specified total sum of Spiritual Stones or medallions.\n", + "- Dungeons: Complete the specified amount of dungeons. Dungeons are considered complete after stepping into ", + "the blue warp after the boss.\n", + "- Tokens: Obtain the specified amount of Skulltula tokens.\n", + "- Triforce Pieces: Obtain the specified number of Triforce Pieces." + ] + }, + "wincon_options": { + "name": "Win Condition Options", + "description": [ + "Standard Rewards - Greg does not change logic, Greg does not help obtain Ganon's Soul, max ", + "number of rewards on slider does not change.\n", + "\n", + "Greg as Reward - Greg does change logic (can be part of expected path for obtaining ", + "Ganon's Soul), Greg helps obtain Ganon's Soul, max number of rewards on slider increases by 1 to ", + "account for Greg.\n", + "\n", + "Greg as Wildcard - Greg does not change logic, Greg helps obtain Ganon's Soul, max number of ", + "rewards on slider does not change." + ] + }, "big_poe_count": { "name": "Big Poe Target Count", "description": "The Poe collector will give a reward for turning in this many Big Poes." @@ -1549,12 +1581,6 @@ "ganons_soul_triforce_count": { "name": "Ganon's Soul Triforce Piece Count" }, - "wincon": { - "name": "Win Condition" - }, - "wincon_options": { - "name": "Win Condition Reward Options" - }, "wincon_medallion_count": { "name": "Win Condition Medallion Count" }, diff --git a/soh/include/z64item.h b/soh/include/z64item.h index 137ed09a85..2e21a9c2fd 100644 --- a/soh/include/z64item.h +++ b/soh/include/z64item.h @@ -594,8 +594,9 @@ typedef enum { /* 0x7A */ GID_SONG_TIME, /* 0x7B */ GID_SONG_STORM, /* 0x7C */ GID_TRIFORCE_PIECE, - /* 0x7D */ GID_FISHING_POLE, - /* 0x7E */ GID_MAXIMUM + /* 0x7D */ GID_TRIFORCE, + /* 0x7E */ GID_FISHING_POLE, + /* 0x7F */ GID_MAXIMUM } GetItemDrawID; diff --git a/soh/soh/Enhancements/randomizer/3drando/hint_list.cpp b/soh/soh/Enhancements/randomizer/3drando/hint_list.cpp index 2cbff54b26..38db0742cc 100644 --- a/soh/soh/Enhancements/randomizer/3drando/hint_list.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/hint_list.cpp @@ -2152,6 +2152,11 @@ void StaticData::HintTable_Init() { | WINCON HINT TEXT | ---------------------------*/ + hintTextTable[RHT_WINCON_DEFEAT_GANON_HINT] = HintText(CustomMessage("$wAnd the #Triforce# will be granted once #the evil one# is defeated.^", + /*german*/ "$wUnd das #Triforce# wird gewährt, sobald #der Böse# besiegt wurde.^", + /*french*/ "$wAussi, la #Triforce# sera accordée une fois que #le Malin# aura été vaincu.^", + {QM_YELLOW, QM_PINK})); + hintTextTable[RHT_WINCON_ANYWHERE_HINT] = HintText(CustomMessage("$wAnd the #Triforce# will be hidden somewhere&#in Hyrule#.^", /*german*/ "$wUnd das #Triforce# wird irgendwo #in Hyrule# zu finden sein.^", /*french*/ "$wAussi, la #Triforce# se trouve quelque part #dans Hyrule#.^", diff --git a/soh/soh/Enhancements/randomizer/draw.cpp b/soh/soh/Enhancements/randomizer/draw.cpp index 334f84e8eb..370f2ba47f 100644 --- a/soh/soh/Enhancements/randomizer/draw.cpp +++ b/soh/soh/Enhancements/randomizer/draw.cpp @@ -357,6 +357,10 @@ extern "C" void Randomizer_DrawTriforcePieceGI(PlayState* play, GetItemEntry get if (rando->GetRandoSettingValue(RSK_GANONS_SOUL) == RO_GANONS_SOUL_TRIFORCE_PIECES) { fullTriforce = fullTriforce || (rando->GetRandoSettingValue(RSK_GANONS_SOUL_TRIFORCE_COUNT) == current); } + if (getItemEntry.getItemId == RG_TRIFORCE) { + fullTriforce = true; + triforcePieceScale = 0.035f; + } Matrix_Scale(triforcePieceScale, triforcePieceScale, triforcePieceScale, MTXMODE_APPLY); diff --git a/soh/soh/Enhancements/randomizer/hint.cpp b/soh/soh/Enhancements/randomizer/hint.cpp index d5f52a4dde..45639dd476 100644 --- a/soh/soh/Enhancements/randomizer/hint.cpp +++ b/soh/soh/Enhancements/randomizer/hint.cpp @@ -650,7 +650,9 @@ CustomMessage Hint::GetWinconText() { auto ctx = Rando::Context::GetInstance(); CustomMessage winconMessage; - if (ctx->GetOption(RSK_WINCON).Is(RO_WINCON_ANYWHERE)) { + if (ctx->GetOption(RSK_WINCON).Is(RO_WINCON_DEFEAT_GANON)) { + return StaticData::hintTextTable[RHT_WINCON_DEFEAT_GANON_HINT].GetHintMessage(); + } else if (ctx->GetOption(RSK_WINCON).Is(RO_WINCON_ANYWHERE)) { return StaticData::hintTextTable[RHT_WINCON_ANYWHERE_HINT].GetHintMessage(); } else if (ctx->GetOption(RSK_WINCON).Is(RO_WINCON_STONES)) { winconMessage = StaticData::hintTextTable[RHT_WINCON_STONES_HINT].GetHintMessage(); diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index 7f4e8b1d05..b54ee3d5f5 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -472,7 +472,7 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_DEKU_NUT_BAG] = Item(RG_DEKU_NUT_BAG, Text{ "Deku Nut Bag", "Sac de Noix Mojo", "Deku-Nuß-Tasche" }, ITEMTYPE_ITEM, GI_NUT_UPGRADE_30, true, LOGIC_PROGRESSIVE_NUT_BAG, RHT_NONE, RG_DEKU_NUT_BAG, OBJECT_GI_NUTS, GID_NUTS, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "eine "}).CustomIcon(gItemIconDekuNutTex); itemTable[RG_DEKU_NUT_BAG].SetCustomDrawFunc(Randomizer_DrawNutBag); - itemTable[RG_TRIFORCE] = Item(RG_TRIFORCE, Text{ "Triforce", "Triforce", "Triforce" }, ITEMTYPE_ITEM, RG_TRIFORCE, true, LOGIC_NONE, RHT_NONE, RG_TRIFORCE, OBJECT_GI_BOMB_2, GID_TRIFORCE_PIECE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"the ", "la ", "die "}).CustomIcon(gTriforcePieceTex);; + itemTable[RG_TRIFORCE] = Item(RG_TRIFORCE, Text{ "Triforce", "Triforce", "Triforce" }, ITEMTYPE_ITEM, RG_TRIFORCE, true, LOGIC_NONE, RHT_NONE, RG_TRIFORCE, OBJECT_GI_BOMB_2, GID_TRIFORCE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"the ", "la ", "die "}).CustomIcon(gTriforcePieceTex);; itemTable[RG_HINT] = Item(RG_HINT, Text{ "Hint", "Indice", "Hinweis" }, ITEMTYPE_EVENT, RG_HINT, false, LOGIC_NONE, RHT_NONE, ITEM_CATEGORY_LESSER); // Individual stages of progressive items (only here for GetItemEntry purposes, not for use in seed gen) itemTable[RG_HOOKSHOT] = Item(RG_HOOKSHOT, Text{ "Hookshot", "Grappin", "Fanghaken" }, ITEMTYPE_ITEM, GI_HOOKSHOT, true, LOGIC_PROGRESSIVE_HOOKSHOT, RHT_HOOKSHOT, ITEM_HOOKSHOT, OBJECT_GI_HOOKSHOT, GID_HOOKSHOT, 0x36, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"the ", "le ", "den "}); diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerHintTextKey.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerHintTextKey.h index a17a71818a..46b0e52474 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerHintTextKey.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerHintTextKey.h @@ -1258,6 +1258,7 @@ RANDO_ENUM_ITEM(RHT_GANONS_SOUL_DUNGEONS_HINT) RANDO_ENUM_ITEM(RHT_GANONS_SOUL_TOKENS_HINT) RANDO_ENUM_ITEM(RHT_GANONS_SOUL_TRIFORCE_PIECES_HINT) // Wincon +RANDO_ENUM_ITEM(RHT_WINCON_DEFEAT_GANON_HINT) RANDO_ENUM_ITEM(RHT_WINCON_ANYWHERE_HINT) RANDO_ENUM_ITEM(RHT_WINCON_STONES_HINT) RANDO_ENUM_ITEM(RHT_WINCON_MEDALLIONS_HINT) diff --git a/soh/src/code/z_draw.c b/soh/src/code/z_draw.c index 4706aaf7a7..06a7190788 100644 --- a/soh/src/code/z_draw.c +++ b/soh/src/code/z_draw.c @@ -114,6 +114,7 @@ void GetItem_DrawJewelGoron(PlayState* play, s16 drawId); void GetItem_DrawJewelZora(PlayState* play, s16 drawId); void GetItem_DrawGenericMusicNote(PlayState* play, s16 drawId); void GetItem_DrawTriforcePiece(PlayState* play, s16 drawId); +void GetItem_DrawTriforce(PlayState* play, s16 drawId); void GetItem_DrawFishingPole(PlayState* play, s16 drawId); typedef struct { @@ -384,14 +385,15 @@ DrawItemTableEntry sDrawItemTable[] = { { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Generic - { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Zelda's Lullaby - { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Epona's song - { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Saria's song - { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Sun's song - { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Song of time - { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Song of storms - { GetItem_DrawTriforcePiece, { gTriforcePiece0DL } }, // Triforce Piece - { GetItem_DrawFishingPole, { gGiFishingPoleDL } }, // Fishing Pole + { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Zelda's Lullaby + { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Epona's song + { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Saria's song + { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Sun's song + { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Song of time + { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, // Song of storms + { GetItem_DrawTriforcePiece, { gTriforcePiece0DL } }, // Triforce Piece + { GetItem_DrawTriforce, { gTriforcePieceCompletedDL } }, // Triforce + { GetItem_DrawFishingPole, { gGiFishingPoleDL } }, // Fishing Pole }; /** @@ -1027,6 +1029,24 @@ void GetItem_DrawTriforcePiece(PlayState* play, s16 drawId) { CLOSE_DISPS(play->state.gfxCtx); } +void GetItem_DrawTriforce(PlayState* play, s16 drawId) { + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL_25Opa(play->state.gfxCtx); + + Matrix_Scale(0.035f, 0.035f, 0.035f, MTXMODE_APPLY); + + uint8_t index = gSaveContext.ship.quest.data.randomizer.triforcePiecesCollected % 3; + Gfx* triforceDL = (Gfx*)gTriforcePieceCompletedDL; + + gSPMatrix(POLY_OPA_DISP++, Matrix_NewMtx(play->state.gfxCtx, (char*)__FILE__, __LINE__), + G_MTX_MODELVIEW | G_MTX_LOAD); + + gSPDisplayList(POLY_OPA_DISP++, triforceDL); + + CLOSE_DISPS(play->state.gfxCtx); +} + void GetItem_DrawFishingPole(PlayState* play, s16 drawId) { Vec3f pos; OPEN_DISPS(play->state.gfxCtx); From 2f9e96c7f3b72d4665bcc3c6e6ebe3ea2cfac259 Mon Sep 17 00:00:00 2001 From: Jordan Longstaff Date: Fri, 9 Oct 2026 09:28:17 -0400 Subject: [PATCH 06/29] Allow skipping Mweep cutscene in progress (#7349) --- .../TimeSavers/SkipCutscene/Story/SkipKingZoraMoveOver.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipKingZoraMoveOver.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipKingZoraMoveOver.cpp index 836fec9860..811917e250 100644 --- a/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipKingZoraMoveOver.cpp +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipKingZoraMoveOver.cpp @@ -8,6 +8,7 @@ extern "C" { s32 EnKz_FollowPath(EnKz* enKz, PlayState* play); s32 EnKz_SetMovedPos(EnKz* enKz, PlayState* play); +void EnKz_Mweep(EnKz* enKz, PlayState* play); void EnKz_SetupMweep(EnKz* enKz, PlayState* play); void EnKz_Wait(EnKz* enKz, PlayState* play); } @@ -35,7 +36,11 @@ static void EnKz_MweepNoCutscene(EnKz* enKz, PlayState* play) { static void EnKz_InterceptMweep(void* actorPtr) { EnKz* enKz = (EnKz*)actorPtr; - if (enKz->actionFunc != EnKz_SetupMweep) { + if (enKz->actionFunc == EnKz_Mweep) { + Play_ChangeCameraStatus(gPlayState, enKz->gameplayCamera, CAM_STAT_ACTIVE); + Play_ClearCamera(gPlayState, enKz->cutsceneCamera); + Player_SetCsActionWithHaltedActors(gPlayState, &enKz->actor, 7); + } else if (enKz->actionFunc != EnKz_SetupMweep) { return; } From 4a1b846c90d8858c0da38f7ca5237f58d92cac5b Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 10 Oct 2026 11:38:33 -0400 Subject: [PATCH 07/29] Fix typos in randomizer (#7357) --- soh/assets/custom/lang/en_US.json | 4 ++-- soh/soh/Enhancements/randomizer/ShuffleSilver.cpp | 4 ++-- soh/soh/Enhancements/randomizer/location_access.cpp | 2 +- .../randomizer/location_access/dungeons/fire_temple.cpp | 2 +- .../randomizer/location_access/dungeons/forest_temple.cpp | 2 +- .../randomizer/location_access/dungeons/ice_cavern.cpp | 2 +- .../randomizer/location_access/dungeons/shadow_temple.cpp | 2 +- .../randomizer/location_access/dungeons/spirit_temple.cpp | 8 ++++---- .../randomizer/location_access/dungeons/water_temple.cpp | 8 ++++---- soh/soh/Enhancements/randomizer/logic.cpp | 8 ++++---- .../randomizer/randomizerEnums/RandomizerTrick.h | 2 +- soh/soh/Enhancements/randomizer/settings.cpp | 8 ++++---- 12 files changed, 26 insertions(+), 26 deletions(-) diff --git a/soh/assets/custom/lang/en_US.json b/soh/assets/custom/lang/en_US.json index 2d989173cc..06699d067c 100644 --- a/soh/assets/custom/lang/en_US.json +++ b/soh/assets/custom/lang/en_US.json @@ -2684,7 +2684,7 @@ }, "water_dragon_jump_dive": { "name": "Water Temple Dragon Statue Jump Dive", - "description": "If you come into the dragon statue room from the serpent river, you can sidehop down from above and get into the tunnel without needing either Iron Boots or a Scale. Alternativly, you can do this while equipping irons in middair to get into the tunnel without swim. This trick applies to both Vanilla and Master Quest. In Vanilla, you must shoot the switch from above with the Bow, and then quickly get through the tunnel before the gate closes." + "description": "If you come into the dragon statue room from the serpent river, you can sidehop down from above and get into the tunnel without needing either Iron Boots or a Scale. Alternatively, you can do this while equipping irons in midair to get into the tunnel without swim. This trick applies to both Vanilla and Master Quest. In Vanilla, you must shoot the switch from above with the Bow, and then quickly get through the tunnel before the gate closes." }, "water_adult_dragon": { "name": "Water Temple Dragon Statue Switch from Above the Water as Adult", @@ -2916,7 +2916,7 @@ }, "gtg_mq_with_hookshot": { "name": "Gerudo Training Ground MQ Left Side Silver Rupees with Hookshot", - "description": "The highest Silver Rupee can be obtained by hookshooting the target and then immediately jumpslashing toward the Rupee." + "description": "The highest Silver Rupee can be obtained by hookshotting the target and then immediately jumpslashing toward the Rupee." }, "gtg_mq_without_hookshot": { "name": "Gerudo Training Ground MQ Left Side Silver Rupees without Hookshot", diff --git a/soh/soh/Enhancements/randomizer/ShuffleSilver.cpp b/soh/soh/Enhancements/randomizer/ShuffleSilver.cpp index f8c71a35ae..c4fb0e4756 100644 --- a/soh/soh/Enhancements/randomizer/ShuffleSilver.cpp +++ b/soh/soh/Enhancements/randomizer/ShuffleSilver.cpp @@ -334,8 +334,8 @@ void Rando::StaticData::RegisterSilverLocations() { locationTable[RC_SPIRIT_NEAR_DOOR_SUN_SILVER] = Location::Collectable(RC_SPIRIT_NEAR_DOOR_SUN_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_SPIRIT_TEMPLE, TWO_ACTOR_PARAMS(-984, -450), "Near Door Sun Silver", RHT_SPIRIT_TEMPLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_SPIRIT_NEAR_DOOR_SUN_SILVER)); locationTable[RC_SPIRIT_NW_BOULDER_SILVER] = Location::Collectable(RC_SPIRIT_NW_BOULDER_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_SPIRIT_TEMPLE, TWO_ACTOR_PARAMS(1284, -1355), "Northwest Boulder Silver", RHT_SPIRIT_TEMPLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_SPIRIT_NW_BOULDER_SILVER)); locationTable[RC_SPIRIT_SW_BOULDER_SILVER] = Location::Collectable(RC_SPIRIT_SW_BOULDER_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_SPIRIT_TEMPLE, TWO_ACTOR_PARAMS(1284, -813), "Southwest Boulder Silver", RHT_SPIRIT_TEMPLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_SPIRIT_SW_BOULDER_SILVER)); - locationTable[RC_SPIRIT_SE_BOULDER_SILVER] = Location::Collectable(RC_SPIRIT_SE_BOULDER_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_SPIRIT_TEMPLE, TWO_ACTOR_PARAMS(1856, -944), "Southeast Boulder Super", RHT_SPIRIT_TEMPLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_SPIRIT_SE_BOULDER_SILVER)); - locationTable[RC_SPIRIT_NE_BOULDER_SILVER] = Location::Collectable(RC_SPIRIT_NE_BOULDER_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_SPIRIT_TEMPLE, TWO_ACTOR_PARAMS(1856, -1219), "Notheast Boulder Silver", RHT_SPIRIT_TEMPLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_SPIRIT_NE_BOULDER_SILVER)); + locationTable[RC_SPIRIT_SE_BOULDER_SILVER] = Location::Collectable(RC_SPIRIT_SE_BOULDER_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_SPIRIT_TEMPLE, TWO_ACTOR_PARAMS(1856, -944), "Southeast Boulder Silver", RHT_SPIRIT_TEMPLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_SPIRIT_SE_BOULDER_SILVER)); + locationTable[RC_SPIRIT_NE_BOULDER_SILVER] = Location::Collectable(RC_SPIRIT_NE_BOULDER_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_SPIRIT_TEMPLE, TWO_ACTOR_PARAMS(1856, -1219), "Northeast Boulder Silver", RHT_SPIRIT_TEMPLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_SPIRIT_NE_BOULDER_SILVER)); locationTable[RC_SPIRIT_AIRBORNE_BOULDER_SILVER] = Location::Collectable(RC_SPIRIT_AIRBORNE_BOULDER_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_SPIRIT_TEMPLE, TWO_ACTOR_PARAMS(1573, -920), "Airborne Boulder Silver", RHT_SPIRIT_TEMPLE_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_SPIRIT_AIRBORNE_BOULDER_SILVER)); locationTable[RC_BOTW_B1_LADDER_SILVER] = Location::Collectable(RC_BOTW_B1_LADDER_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_BOTTOM_OF_THE_WELL, TWO_ACTOR_PARAMS(-796, -150), "B1 Ladder Silver", RHT_BOTW_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_BOTW_B1_LADDER_SILVER)); locationTable[RC_BOTW_B2_LADDER_SILVER] = Location::Collectable(RC_BOTW_B2_LADDER_SILVER, RCQUEST_VANILLA, RCTYPE_SILVER, ACTOR_EN_G_SWITCH, SCENE_BOTTOM_OF_THE_WELL, TWO_ACTOR_PARAMS(-614, -297), "B2 Ladder Silver", RHT_BOTW_SILVER, RG_BLUE_RUPEE, SpoilerCollectionCheck::RandomizerInf(RAND_INF_BOTW_B2_LADDER_SILVER)); diff --git a/soh/soh/Enhancements/randomizer/location_access.cpp b/soh/soh/Enhancements/randomizer/location_access.cpp index f5b489a350..6052dc57c4 100644 --- a/soh/soh/Enhancements/randomizer/location_access.cpp +++ b/soh/soh/Enhancements/randomizer/location_access.cpp @@ -43,7 +43,7 @@ bool LocationAccess::CheckConditionAtAgeTime(bool& age, bool& time) const { } bool LocationAccess::ConditionsMet(Region* parentRegion, bool calculatingAvailableChecks) const { - // WARNING enterance validation can run this after resetting the access for sphere 0 validation + // WARNING entrance validation can run this after resetting the access for sphere 0 validation // When refactoring ToD access, either fix the above or do not assume that we // have any access at all just because this is being run bool conditionsMet = false; diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/fire_temple.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/fire_temple.cpp index 81fdc1cd25..d2bf2d8d45 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/fire_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/fire_temple.cpp @@ -630,7 +630,7 @@ void RegionTable_Init_FireTemple() { //Only adult is tall enough to use a bow shot without climbing to UPPER //Logical ways to cross are hovers, longshot or running through the lava and then climbing back up as adult (child can't reach the ledge). //The Damage logic here is for jumping down and running across the lava to get in dins range of the south torch - //Fairies cannot be used for this as it is time sensetive, and NL is only useful with sticks as it disables other magic while in use, so it's tunic or raw damage taking ability. + //Fairies cannot be used for this as it is time sensitive, and NL is only useful with sticks as it disables other magic while in use, so it's tunic or raw damage taking ability. //testing tells me you take 3 ticks of lava damage, which is 12 internal damage or 3/4 of a heart at x1 damage multiplier, performing this run LOCATION(RC_FIRE_TEMPLE_MQ_NEAR_BOSS_CHEST, logic->CanUse(RG_DINS_FIRE) && logic->HasItem(RG_OPEN_CHEST) && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanUse(RG_LONGSHOT) || diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/forest_temple.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/forest_temple.cpp index 7cc1e0a5ac..037a61f11b 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/forest_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/forest_temple.cpp @@ -723,7 +723,7 @@ void RegionTable_Init_ForestTemple() { LOCATION(RC_FOREST_TEMPLE_MQ_RAISED_ISLAND_COURTYARD_LOWER_CHEST, logic->HasItem(RG_OPEN_CHEST)), }, { //Exits - //Skipping swim here is non-trival, needs a roll-jump. If a swim lock is added it's probably wise to copy deku baba events here + //Skipping swim here is non-trivial, needs a roll-jump. If a swim lock is added it's probably wise to copy deku baba events here ENTRANCE(RR_FOREST_TEMPLE_MQ_NE_COURTYARD, true), ENTRANCE(RR_FOREST_TEMPLE_MQ_NE_COURTYARD_LEDGE_ABOVE_ISLAND, logic->CanUse(RG_SONG_OF_TIME)), }); diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/ice_cavern.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/ice_cavern.cpp index 10510c7419..b6fa7a8cb0 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/ice_cavern.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/ice_cavern.cpp @@ -313,7 +313,7 @@ void RegionTable_Init_IceCavern() { LOCATION(RC_ICE_CAVERN_MQ_MAP_RED_ICE, logic->BlueFire()), }, {}); - //RANDOTODO the default state of the toggle switchs in MQ ice are wierd, investigate for doordsanity + //RANDOTODO the default state of the toggle switches in MQ ice are weird, investigate for doorsanity areaTable[RR_ICE_CAVERN_MQ_SCARECROW_ROOM] = Region("Ice Cavern MQ Scarecrow Room", SCENE_ICE_CAVERN, { //Events EVENT_ACCESS(LOGIC_BLUE_FIRE_ACCESS, logic->CanUse(RG_SONG_OF_TIME) || logic->BunnyHovers() || (logic->IsAdult && (logic->CanGroundJump() || ctx->GetTrickOption(RT_SLIDE_JUMP)))), diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/shadow_temple.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/shadow_temple.cpp index b911530f65..fd3b7987c5 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/shadow_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/shadow_temple.cpp @@ -716,7 +716,7 @@ void RegionTable_Init_ShadowTemple() { ENTRANCE(RR_SHADOW_TEMPLE_MQ_SPIKES_DOOR_PLATFORM, (((ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->CanUse(logic->IsAdult && AnyAgeTime([]{return logic->CanKillEnemy(RE_REDEAD);}) ? RG_HOOKSHOT : RG_LONGSHOT)) && //regardless, we can't assume the player does not have the silvers, so we need to cross from the glass to the platform with hovers or a jumpslash jump (as adult) (logic->CanUse(RG_HOVER_BOOTS) || (logic->IsAdult && logic->CanJumpslash()))) || - //alternativly, a middair groundjump from the invisible spikes takes adult directly there + //alternatively, a midair groundjump from the invisible spikes takes adult directly there ((ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->IsAdult && logic->CanMiddairGroundJump())) }); 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 104978b240..518be8835a 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp @@ -256,7 +256,7 @@ void RegionTable_Init_SpiritTemple() { RR_SPIRIT_TEMPLE_STATUE_ROOM_CHILD, []{return logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_LONGSHOT);})), }, { //Exits - //Adult can barely make it with just a jump, but it's a bit tight with a bad retry if it is relevent, so it would be it's own trick. + //Adult can barely make it with just a jump, but it's a bit tight with a bad retry if it is relevant, so it would be it's own trick. ENTRANCE(RR_SPIRIT_TEMPLE_STATUE_ROOM_CHILD, logic->CanUse(RG_HOVER_BOOTS) || logic->BunnyHood() || logic->CanJumpslash() || logic->CanUse(RG_HOOKSHOT)), ENTRANCE(RR_SPIRIT_TEMPLE_STATUE_ROOM, true), ENTRANCE(RR_SPIRIT_TEMPLE_GS_LEDGE, logic->IsAdult && (ctx->GetTrickOption(RT_SPIRIT_STATUE_JUMP) || logic->BunnyHood())), @@ -570,7 +570,7 @@ void RegionTable_Init_SpiritTemple() { // Exits ENTRANCE(RR_SPIRIT_TEMPLE_STATUE_ROOM, true), //Bunny hood jumps with a jumpslash can reach either hand and with good timing the platform as child. - //The latter is definitely a trick, the former may not be but I am dissalowing it because I would have to spend a week writing a new branch of spirit key logic. + //The latter is definitely a trick, the former may not be but I am disallowing it because I would have to spend a week writing a new branch of spirit key logic. //If this interaction with the hands is added, SpiritSharedLogic needs updating for it ENTRANCE(RR_SPIRIT_TEMPLE_INNER_WEST_HAND, logic->CanUse(RG_HOVER_BOOTS) || (logic->BunnyHood() && logic->CanJumpslash() && logic->IsAdult)), ENTRANCE(RR_SPIRIT_TEMPLE_INNER_LEFT_HAND, logic->CanUse(RG_HOVER_BOOTS) || (logic->BunnyHood() && logic->CanJumpslash() && logic->IsAdult)), @@ -659,7 +659,7 @@ void RegionTable_Init_SpiritTemple() { LOCATION(RC_SPIRIT_TEMPLE_MQ_GIBDO_BOULDER, logic->BlastOrSmash()), }, { //Exits - //climbing the grate through the celing allows you to go backwards here, but that's a clip not visible collision + //climbing the grate through the ceiling allows you to go backwards here, but that's a clip not visible collision ENTRANCE(RR_SPIRIT_TEMPLE_MQ_TURNTABLE, logic->Get(LOGIC_SPIRIT_MQ_GIBDOS_CLEARED)), }); @@ -1192,7 +1192,7 @@ void RegionTable_Init_SpiritTemple() { // Exits ENTRANCE(RR_SPIRIT_TEMPLE_MQ_STATUE_ROOM, true), //Bunny hood jumps with a jumpslash can reach either hand and with good timing the platform as child. - //The latter is definitely a trick, the former may not be but I am dissalowing it because I would have to spend a week writing a new branch of spirit key logic. + //The latter is definitely a trick, the former may not be but I am disallowing it because I would have to spend a week writing a new branch of spirit key logic. //If this interaction with the hands is added, SpiritSharedLogic needs updating for it ENTRANCE(RR_SPIRIT_TEMPLE_MQ_INNER_RIGHT_HAND, logic->CanUse(RG_HOVER_BOOTS)), ENTRANCE(RR_SPIRIT_TEMPLE_MQ_INNER_LEFT_HAND, logic->CanUse(RG_HOVER_BOOTS)), diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/water_temple.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/water_temple.cpp index 20b17d59c4..f075bb3150 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/water_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/water_temple.cpp @@ -254,11 +254,11 @@ void RegionTable_Init_WaterTemple() { //Implies CanAvoid(RE_STINGER) //the full logic for the puzzle, as it is cut down here for optimisation because of how the water rising starts requiring scale //EVENT_ACCESS(LOGIC_WATER_PUSHED_B1_BLOCK, logic->HasItem(RG_GORONS_BRACELET) && logic->HasExplosives() && - // (logic->CanUse(RG_HOOKSHOT) || logic->HasItem(RG_BRONZE_SCALE) || logic->CanUse(RG_HOVER_BOOTS) || (logic->IsAdult && logiic->BunnyHood()))), + // (logic->CanUse(RG_HOOKSHOT) || logic->HasItem(RG_BRONZE_SCALE) || logic->CanUse(RG_HOVER_BOOTS) || (logic->IsAdult && logic->BunnyHood()))), }, { //Locations //we can always get the pots by shooting them from afar and diving for the item... LOCATION(RC_WATER_TEMPLE_BASEMENT_BLOCK_PUZZLE_POT_1, (logic->CanBreakPots(ED_LONGSHOT, true, true) && logic->HasItem(RG_BRONZE_SCALE)) || - //or we can use irons and hookshot the pot, hookshoting to the target door if needed to cross the gap + //or we can use irons and hookshot the pot, hookshotting to the target door if needed to cross the gap (logic->CanUse(RG_IRON_BOOTS) && logic->CanUse(RG_HOOKSHOT) && logic->WaterTimer() >= 8)), LOCATION(RC_WATER_TEMPLE_BASEMENT_BLOCK_PUZZLE_POT_2, (logic->CanBreakPots(ED_LONGSHOT, true, true) && logic->HasItem(RG_BRONZE_SCALE)) || (logic->CanUse(RG_IRON_BOOTS) && logic->CanUse(RG_HOOKSHOT) && logic->WaterTimer() >= 8)), @@ -1113,7 +1113,7 @@ void RegionTable_Init_WaterTemple() { LOCATION(RC_WATER_TEMPLE_MQ_WONDER_HOOKSHOT_STAIRCASE_LEFT_3, logic->CanUse(RG_HOOKSHOT)), }, { //Exits - //you can also make it with hover boots and good timing, using the momentom of the slope, but it's a trick + //you can also make it with hover boots and good timing, using the momentum of the slope, but it's a trick ENTRANCE(RR_WATER_TEMPLE_MQ_WATERFALL, logic->CanUse(RG_LONGSHOT) && logic->CanHitSwitch(ED_FAR)), ENTRANCE(RR_WATER_TEMPLE_MQ_STALFOS_PIT, true), }); @@ -1133,7 +1133,7 @@ void RegionTable_Init_WaterTemple() { //Exits ENTRANCE(RR_WATER_TEMPLE_MQ_STALFOS_PIT, logic->CanUse(RG_HOOKSHOT) && (logic->IsAdult || (logic->CanUse(RG_IRON_BOOTS) && logic->WaterTimer() >= 8))), //Child can use the second method (going via the wall target near the half raised target) with bunny hood, but while the first jump is just wall assisted, the second is inconsistent - //It is also possible for them to jump around the statue with bunny, or even climb up sometimes. This needs more investigation before catagorisation as they also seem inconsistent without a setup + //It is also possible for them to jump around the statue with bunny, or even climb up sometimes. This needs more investigation before categorisation as they also seem inconsistent without a setup ENTRANCE(RR_WATER_TEMPLE_MQ_STALFOS_PIT_POTS, (logic->IsAdult && logic->CanUse(RG_HOOKSHOT)) || (logic->CanUse(RG_HOOKSHOT) && (logic->IsAdult || logic->CanUse(RG_IRON_BOOTS) && logic->WaterTimer() >= 8) && (logic->CanUse(RG_HOVER_BOOTS) || logic->Get(LOGIC_WATER_MQ_STALFOS_PIT)))), ENTRANCE(RR_WATER_TEMPLE_MQ_STALFOS_PIT_UPPER, logic->Get(LOGIC_WATER_MQ_STALFOS_PIT) && (logic->IsAdult || logic->CanUse(RG_IRON_BOOTS) && logic->WaterTimer() >= 8) && logic->CanUse(RG_HOOKSHOT)), }); diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index 7c1ad8a0e2..458583c575 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -1048,7 +1048,7 @@ bool Logic::CanKillEnemy(RandomizerEnemy enemy, EnemyDistance distance, bool wal // RANDOTODO make a function to track our ammo vs his HP when ammo capacity is taken into account in logic // all swords can at least trade blows with dark link, and even with 1 damage a slash it works out return CanUseSword() || - // Boomerang is a relaible, infinite ammo stun, so it enables any way to get enough damage with the + // Boomerang is a reliable, infinite ammo stun, so it enables any way to get enough damage with the // ammo we have Max HP dark link has 40 HP, bows and bombs do 2 so 20 ammo, stick jumpslash does 4 so // 10 sticks (CanUse(RG_BOOMERANG) && @@ -1178,7 +1178,7 @@ bool Logic::CanPassEnemy(RandomizerEnemy enemy, EnemyDistance distance, bool wal } // Can we avoid this enemy while climbing up a wall, or doing a difficult platforming challenge? -// use grounded if the challenge is such that the enemy interfears even if it cannot hit link out of the air +// use grounded if the challenge is such that the enemy interferes even if it cannot hit link out of the air bool Logic::CanAvoidEnemy(RandomizerEnemy enemy, EnemyDistance distance, bool grounded, uint8_t quantity) { // DISTANCE AND WALL ASSUMED, add more arguments later if needed if (CanKillEnemy(enemy, distance, true, quantity)) { @@ -1430,7 +1430,7 @@ bool Logic::WaterRisingTargetTo3FCentral() { * changed out of logic * * Extending from these 3, LOGIC_WATER_COULD_LOW_FROM_HIGH and LOGIC_WATER_COULD_HIGH_FROM_MID tell us if we can move - * from 1 level to the next, without us first having to confirm we can always do the preceeding level first. These allow + * from 1 level to the next, without us first having to confirm we can always do the preceding level first. These allow * us to check for conditions where we can complete a water level loop and reach any level from any level before we know * for sure we have real access. MIDDLE_EMBLEM always requires low water, so FROM_LOW is implied in * LOGIC_WATER_COULD_MIDDLE. @@ -1763,7 +1763,7 @@ uint16_t Logic::FireTimer() { } // Tunic is not required if you are using irons to do something that a simple gold scale dive could do, and you are not -// in water temple. (celing swimming and long walks through water do not count) +// in water temple. (ceiling swimming and long walks through water do not count) uint16_t Logic::WaterTimer() { if (CanUse(RG_ZORA_TUNIC)) { return UINT16_MAX; diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerTrick.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerTrick.h index c23a5b7a64..7f400438ac 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerTrick.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerTrick.h @@ -13,7 +13,7 @@ // Randomizer tricks and glitches (beta) based on knowledge of what is achievable in Ship of Harkinian randomizer, // excludes Item manipulation (e.g. RBA/GIM/SRM) and Wrong Warping and Ganon BK Skip -// TODO Fill and alphabetize (area based tricks must have area in name and come alphetized after general tricks) +// TODO Fill and alphabetize (area based tricks must have area in name and come alphabetized after general tricks) // TODO test commented out tricks to see if consistent with console on SoH // TODO add area specific tricks RANDO_ENUM_BEGIN(RandomizerTrick) diff --git a/soh/soh/Enhancements/randomizer/settings.cpp b/soh/soh/Enhancements/randomizer/settings.cpp index e751addebb..f345401940 100644 --- a/soh/soh/Enhancements/randomizer/settings.cpp +++ b/soh/soh/Enhancements/randomizer/settings.cpp @@ -447,7 +447,7 @@ void Settings::CreateOptions() { OPT_U8(RSK_MQ_DUNGEON_RANDOM, {"None", "Set Number", "Random", "Selection Only"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("MQDungeons"), WIDGET_CVAR_COMBOBOX, RO_MQ_DUNGEONS_NONE, false, nullptr, IMFLAG_NONE); OPT_CALLBACK(RSK_MQ_DUNGEON_RANDOM, { switch (CVarGetInteger(CVAR_RANDOMIZER_SETTING("MQDungeons"), RO_MQ_DUNGEONS_NONE)) { - // If No MQ Dungeons, add a separator after the combobx and hide + // If No MQ Dungeons, add a separator after the combobox and hide // the count slider and the toggle for individual dungeon selections. case RO_MQ_DUNGEONS_NONE: mOptions[RSK_MQ_DUNGEON_COUNT].Hide(); @@ -1097,7 +1097,7 @@ void Settings::CreateOptions() { mOptions[RSK_LOACH_HINT].Enable(); } else { mOptions[RSK_LOACH_HINT].Disable( - "Loach hint is only avaliable with \"Fishsanity\" set to \"Shuffle only Hyrule Loach\"\nas that's the only " + "Loach hint is only available with \"Fishsanity\" set to \"Shuffle only Hyrule Loach\"\nas that's the only " "setting where you present the loach to the fishing pond owner."); } }); @@ -2770,7 +2770,7 @@ void Context::FinalizeSettings(const std::set& excludedLocation mqSet += 1; break; case RO_MQ_SET_RANDOM: - // 50% per dungeon, rolled separatly so people can either have a linear distribtuion + // 50% per dungeon, rolled separately so people can either have a linear distribution // or a bell curve for the number of MQ dungeons per seed. if (Random(0, 2)) { dungeon->SetMQ(); @@ -2814,7 +2814,7 @@ void Context::FinalizeSettings(const std::set& excludedLocation } // otherwise, every dungeon is possible } else { - // if count is MAX_MQ_DUNGEON_COUNT, we know everything is MQ, so can skip some setps and not set Known + // if count is MAX_MQ_DUNGEON_COUNT, we know everything is MQ, so can skip some steps and not set Known if (mOptions[RSK_MQ_DUNGEON_RANDOM].Is(RO_MQ_DUNGEONS_SET_NUMBER) && mqCount == MAX_MQ_DUNGEON_COUNT) { randMQOption.resize(MAX_MQ_DUNGEON_COUNT); for (int i = 0; i < MAX_MQ_DUNGEON_COUNT; i++) { From eea5129a8c5d462706116a958a6a4574e2f7357b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:58:19 +0000 Subject: [PATCH 08/29] Add TCG keys to item tracker (#7361) Refactor dungeon item layout code to not involve blitting magic numbers in dungeonItems --- .../randomizer/randomizer_item_tracker.cpp | 211 ++++++++++-------- .../randomizer/randomizer_item_tracker.h | 5 - 2 files changed, 113 insertions(+), 103 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index e1e5892e1b..dc01c4168a 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include #include @@ -280,65 +282,83 @@ std::vector silverRupeeItems = { ITEM_TRACKER_RG_CUSTOM(RG_GANONS_CASTLE_MQ_SILVER_SHADOW, "ShaTri", ITEM_RUPEE_SILVER, 0, DrawItem), }; -std::vector itemTrackerDungeonsWithMapsHorizontal = { - { SCENE_DEKU_TREE, { ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_DODONGOS_CAVERN, { ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_JABU_JABU, { ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_FOREST_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_FIRE_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_WATER_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_SPIRIT_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_SHADOW_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_INSIDE_GANONS_CASTLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_BOTTOM_OF_THE_WELL, { ITEM_KEY_SMALL, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_ICE_CAVERN, { ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_GERUDO_TRAINING_GROUND, { ITEM_KEY_SMALL } }, -}; +// Dungeon items stacked top to bottom in one grid column, scene in data. Unused slots have no drawFunc +using ItemTrackerDungeonColumn = std::array; -std::vector itemTrackerDungeonsHorizontal = { - { SCENE_FOREST_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_FIRE_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_WATER_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_SPIRIT_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_SHADOW_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_INSIDE_GANONS_CASTLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_BOTTOM_OF_THE_WELL, { ITEM_KEY_SMALL } }, - { SCENE_GERUDO_TRAINING_GROUND, { ITEM_KEY_SMALL } }, -}; +static ItemTrackerItem Key(SceneID scene) { + return ITEM_TRACKER_ITEM(ITEM_KEY_SMALL, "", static_cast(scene), DrawDungeonItem); +} +static ItemTrackerItem Boss(SceneID scene) { + return ITEM_TRACKER_ITEM(ITEM_KEY_BOSS, "", static_cast(scene), DrawDungeonItem); +} +static ItemTrackerItem Map(SceneID scene) { + return ITEM_TRACKER_ITEM(ITEM_DUNGEON_MAP, "", static_cast(scene), DrawDungeonItem); +} +static ItemTrackerItem Compass(SceneID scene) { + return ITEM_TRACKER_ITEM(ITEM_COMPASS, "", static_cast(scene), DrawDungeonItem); +} -std::vector itemTrackerDungeonsWithMapsCompact = { - { SCENE_FOREST_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_FIRE_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_WATER_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_SPIRIT_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_SHADOW_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_BOTTOM_OF_THE_WELL, { ITEM_KEY_SMALL, ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_DEKU_TREE, { ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_DODONGOS_CAVERN, { ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_JABU_JABU, { ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_ICE_CAVERN, { ITEM_DUNGEON_MAP, ITEM_COMPASS } }, - { SCENE_INSIDE_GANONS_CASTLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_GERUDO_TRAINING_GROUND, { ITEM_KEY_SMALL } }, -}; +static const auto itemTrackerDungeonsWithMapsHorizontal = std::to_array({ + { Map(SCENE_DEKU_TREE), Compass(SCENE_DEKU_TREE) }, + { Map(SCENE_DODONGOS_CAVERN), Compass(SCENE_DODONGOS_CAVERN) }, + { Map(SCENE_JABU_JABU), Compass(SCENE_JABU_JABU) }, + { Key(SCENE_FOREST_TEMPLE), Boss(SCENE_FOREST_TEMPLE), Map(SCENE_FOREST_TEMPLE), Compass(SCENE_FOREST_TEMPLE) }, + { Key(SCENE_FIRE_TEMPLE), Boss(SCENE_FIRE_TEMPLE), Map(SCENE_FIRE_TEMPLE), Compass(SCENE_FIRE_TEMPLE) }, + { Key(SCENE_WATER_TEMPLE), Boss(SCENE_WATER_TEMPLE), Map(SCENE_WATER_TEMPLE), Compass(SCENE_WATER_TEMPLE) }, + { Key(SCENE_SPIRIT_TEMPLE), Boss(SCENE_SPIRIT_TEMPLE), Map(SCENE_SPIRIT_TEMPLE), Compass(SCENE_SPIRIT_TEMPLE) }, + { Key(SCENE_SHADOW_TEMPLE), Boss(SCENE_SHADOW_TEMPLE), Map(SCENE_SHADOW_TEMPLE), Compass(SCENE_SHADOW_TEMPLE) }, + { Key(SCENE_INSIDE_GANONS_CASTLE), Boss(SCENE_GANONS_TOWER) }, + { Key(SCENE_BOTTOM_OF_THE_WELL), Map(SCENE_BOTTOM_OF_THE_WELL), Compass(SCENE_BOTTOM_OF_THE_WELL) }, + { Map(SCENE_ICE_CAVERN), Compass(SCENE_ICE_CAVERN) }, + { Key(SCENE_GERUDO_TRAINING_GROUND), Key(SCENE_THIEVES_HIDEOUT), Key(SCENE_TREASURE_BOX_SHOP) }, +}); -std::vector itemTrackerDungeonsCompact = { - { SCENE_FOREST_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_FIRE_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_WATER_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_SPIRIT_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_SHADOW_TEMPLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_INSIDE_GANONS_CASTLE, { ITEM_KEY_SMALL, ITEM_KEY_BOSS } }, - { SCENE_BOTTOM_OF_THE_WELL, { ITEM_KEY_SMALL } }, - { SCENE_GERUDO_TRAINING_GROUND, { ITEM_KEY_SMALL } }, - { SCENE_THIEVES_HIDEOUT, { ITEM_KEY_SMALL } }, -}; +static const auto itemTrackerDungeonsHorizontal = std::to_array({ + { Key(SCENE_FOREST_TEMPLE), Boss(SCENE_FOREST_TEMPLE) }, + { Key(SCENE_FIRE_TEMPLE), Boss(SCENE_FIRE_TEMPLE) }, + { Key(SCENE_WATER_TEMPLE), Boss(SCENE_WATER_TEMPLE) }, + { Key(SCENE_SPIRIT_TEMPLE), Boss(SCENE_SPIRIT_TEMPLE) }, + { Key(SCENE_SHADOW_TEMPLE), Boss(SCENE_SHADOW_TEMPLE) }, + { Key(SCENE_INSIDE_GANONS_CASTLE), Boss(SCENE_GANONS_TOWER) }, + { Key(SCENE_BOTTOM_OF_THE_WELL), Key(SCENE_TREASURE_BOX_SHOP) }, + { Key(SCENE_GERUDO_TRAINING_GROUND), Key(SCENE_THIEVES_HIDEOUT) }, +}); + +static const auto itemTrackerDungeonsWithMapsCompact = std::to_array({ + { Key(SCENE_FOREST_TEMPLE), Boss(SCENE_FOREST_TEMPLE), Map(SCENE_FOREST_TEMPLE), Compass(SCENE_FOREST_TEMPLE) }, + { Key(SCENE_FIRE_TEMPLE), Boss(SCENE_FIRE_TEMPLE), Map(SCENE_FIRE_TEMPLE), Compass(SCENE_FIRE_TEMPLE) }, + { Key(SCENE_WATER_TEMPLE), Boss(SCENE_WATER_TEMPLE), Map(SCENE_WATER_TEMPLE), Compass(SCENE_WATER_TEMPLE) }, + { Key(SCENE_SPIRIT_TEMPLE), Boss(SCENE_SPIRIT_TEMPLE), Map(SCENE_SPIRIT_TEMPLE), Compass(SCENE_SPIRIT_TEMPLE) }, + { Key(SCENE_SHADOW_TEMPLE), Boss(SCENE_SHADOW_TEMPLE), Map(SCENE_SHADOW_TEMPLE), Compass(SCENE_SHADOW_TEMPLE) }, + { Key(SCENE_BOTTOM_OF_THE_WELL), Map(SCENE_BOTTOM_OF_THE_WELL), Compass(SCENE_BOTTOM_OF_THE_WELL), + Key(SCENE_TREASURE_BOX_SHOP) }, + { Map(SCENE_DEKU_TREE), Compass(SCENE_DEKU_TREE) }, + { Map(SCENE_DODONGOS_CAVERN), Compass(SCENE_DODONGOS_CAVERN) }, + { Map(SCENE_JABU_JABU), Compass(SCENE_JABU_JABU) }, + { Map(SCENE_ICE_CAVERN), Compass(SCENE_ICE_CAVERN) }, + { Key(SCENE_INSIDE_GANONS_CASTLE), Boss(SCENE_GANONS_TOWER) }, + { Key(SCENE_GERUDO_TRAINING_GROUND), Key(SCENE_THIEVES_HIDEOUT) }, +}); + +static const auto itemTrackerDungeonsCompact = std::to_array({ + { Key(SCENE_FOREST_TEMPLE), Boss(SCENE_FOREST_TEMPLE) }, + { Key(SCENE_FIRE_TEMPLE), Boss(SCENE_FIRE_TEMPLE) }, + { Key(SCENE_WATER_TEMPLE), Boss(SCENE_WATER_TEMPLE) }, + { Key(SCENE_SPIRIT_TEMPLE), Boss(SCENE_SPIRIT_TEMPLE) }, + { Key(SCENE_SHADOW_TEMPLE), Boss(SCENE_SHADOW_TEMPLE) }, + { Key(SCENE_INSIDE_GANONS_CASTLE), Boss(SCENE_GANONS_TOWER) }, + { Key(SCENE_BOTTOM_OF_THE_WELL) }, + { Key(SCENE_GERUDO_TRAINING_GROUND) }, + { Key(SCENE_THIEVES_HIDEOUT) }, + { Key(SCENE_TREASURE_BOX_SHOP) }, +}); std::map itemTrackerDungeonShortNames = { { SCENE_FOREST_TEMPLE, "FRST" }, { SCENE_FIRE_TEMPLE, "FIRE" }, { SCENE_WATER_TEMPLE, "WATR" }, { SCENE_SPIRIT_TEMPLE, "SPRT" }, { SCENE_SHADOW_TEMPLE, "SHDW" }, { SCENE_BOTTOM_OF_THE_WELL, "BOTW" }, { SCENE_DEKU_TREE, "DEKU" }, { SCENE_DODONGOS_CAVERN, "DCVN" }, { SCENE_JABU_JABU, "JABU" }, { SCENE_ICE_CAVERN, "ICE" }, { SCENE_INSIDE_GANONS_CASTLE, "GANON" }, { SCENE_GERUDO_TRAINING_GROUND, "GTG" }, - { SCENE_THIEVES_HIDEOUT, "HIDE" }, + { SCENE_THIEVES_HIDEOUT, "HIDE" }, { SCENE_TREASURE_BOX_SHOP, "TCG" }, }; std::vector dungeonItems = {}; @@ -1424,55 +1444,56 @@ void DrawItemsInACircle(std::vector items) { } } +static bool ShowDungeonItem(const ItemTrackerItem& item) { + if (item.data == SCENE_TREASURE_BOX_SHOP) { + return IS_RANDO && RAND_GET_OPTION(RSK_SHUFFLE_CHEST_MINIGAME); + } + if (item.data == SCENE_THIEVES_HIDEOUT) { + return !IS_RANDO || RAND_GET_OPTION(RSK_GERUDO_FORTRESS).IsNot(RO_GF_CARPENTERS_FREE); + } + return true; +} + /** * GetDungeonItemsVector - * Loops over dungeons and creates vectors of items in the correct order - * to then call DrawItemsInRows + * Lays out columns in blocks of `columns` wide, each block as tall as its tallest column, + * and flattens them row by row for DrawItemsInRows */ -std::vector GetDungeonItemsVector(std::vector dungeons, size_t columns = 6) { - std::vector dungeonItems = {}; - - size_t rowCount = 0; - for (size_t i = 0; i < dungeons.size(); i++) { - if (dungeons[i].items.size() > rowCount) - rowCount = static_cast(dungeons[i].items.size()); - } - - for (size_t i = 0; i < rowCount; i++) { - for (size_t j = 0; j < std::min(dungeons.size(), columns); j++) { - if (dungeons[j].items.size() > i) { - switch (dungeons[j].items[i]) { - case ITEM_KEY_SMALL: - dungeonItems.push_back(ITEM_TRACKER_ITEM(ITEM_KEY_SMALL, "", dungeons[j].id, DrawDungeonItem)); - break; - case ITEM_KEY_BOSS: - // Swap Ganon's Castle boss key to the right scene ID manually - if (dungeons[j].id == SCENE_INSIDE_GANONS_CASTLE) { - dungeonItems.push_back( - ITEM_TRACKER_ITEM(ITEM_KEY_BOSS, "", SCENE_GANONS_TOWER, DrawDungeonItem)); - } else { - dungeonItems.push_back( - ITEM_TRACKER_ITEM(ITEM_KEY_BOSS, "", dungeons[j].id, DrawDungeonItem)); - } - break; - case ITEM_DUNGEON_MAP: - dungeonItems.push_back( - ITEM_TRACKER_ITEM(ITEM_DUNGEON_MAP, "", dungeons[j].id, DrawDungeonItem)); - break; - case ITEM_COMPASS: - dungeonItems.push_back(ITEM_TRACKER_ITEM(ITEM_COMPASS, "", dungeons[j].id, DrawDungeonItem)); - break; - } - } else { - dungeonItems.push_back(ITEM_TRACKER_ITEM(ITEM_NONE, "", 0, DrawItem)); +std::vector GetDungeonItemsVector(std::span layout, + size_t columns = 6) { + struct ShownColumn { + ItemTrackerDungeonColumn items; + size_t size = 0; + }; + std::vector shown; + for (const auto& column : layout) { + ShownColumn kept; + for (const auto& item : column) { + if (item.drawFunc != nullptr && ShowDungeonItem(item)) { + kept.items[kept.size++] = item; } } + if (kept.size > 0) { + shown.push_back(kept); + } } - if (dungeons.size() > columns) { - std::vector nextDungeonItems = - GetDungeonItemsVector(std::vector(dungeons.begin() + columns, dungeons.end()), columns); - dungeonItems.insert(dungeonItems.end(), nextDungeonItems.begin(), nextDungeonItems.end()); + std::vector dungeonItems = {}; + for (size_t start = 0; start < shown.size(); start += columns) { + size_t end = std::min(start + columns, shown.size()); + size_t rowCount = 0; + for (size_t j = start; j < end; j++) { + rowCount = std::max(rowCount, shown[j].size); + } + for (size_t i = 0; i < rowCount; i++) { + for (size_t j = start; j < end; j++) { + if (i < shown[j].size) { + dungeonItems.push_back(shown[j].items[i]); + } else { + dungeonItems.push_back(ITEM_TRACKER_ITEM(ITEM_NONE, "", 0, DrawItem)); + } + } + } } return dungeonItems; @@ -1548,18 +1569,12 @@ void UpdateVectors() { SECTION_DISPLAY_SEPARATE) { if (CVarGetInteger(CVAR_TRACKER_ITEM("DungeonItems.DisplayMaps"), 1)) { dungeonItems = GetDungeonItemsVector(itemTrackerDungeonsWithMapsHorizontal, 12); - // Manually adding Thieves Hideout to an open spot so we don't get an additional row for one item - dungeonItems[23] = ITEM_TRACKER_ITEM(ITEM_KEY_SMALL, "", SCENE_THIEVES_HIDEOUT, DrawDungeonItem); } else { - // Manually adding Thieves Hideout to an open spot so we don't get an additional row for one item dungeonItems = GetDungeonItemsVector(itemTrackerDungeonsHorizontal, 8); - dungeonItems[15] = ITEM_TRACKER_ITEM(ITEM_KEY_SMALL, "", SCENE_THIEVES_HIDEOUT, DrawDungeonItem); } } else { if (CVarGetInteger(CVAR_TRACKER_ITEM("DungeonItems.DisplayMaps"), 1)) { dungeonItems = GetDungeonItemsVector(itemTrackerDungeonsWithMapsCompact); - // Manually adding Thieves Hideout to an open spot so we don't get an additional row for one item - dungeonItems[35] = ITEM_TRACKER_ITEM(ITEM_KEY_SMALL, "", SCENE_THIEVES_HIDEOUT, DrawDungeonItem); } else { dungeonItems = GetDungeonItemsVector(itemTrackerDungeonsCompact); } diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.h b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.h index 5e9b136bfb..c1d2146599 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.h +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.h @@ -72,11 +72,6 @@ static std::array itemTrackerWindowIDs = { "Item Tracker", "Total Checks" }; void ItemTracker_LoadFromPreset(const nlohmann::json& trackerInfo); -typedef struct ItemTrackerDungeon { - uint32_t id; - std::vector items; -} ItemTrackerDungeon; - class ItemTrackerSettingsWindow final : public Ship::GuiWindow { public: using GuiWindow::GuiWindow; From 7e510f69ff17b0eeb23abd3d5d900add7e26065e Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 10 Oct 2026 22:35:34 -0400 Subject: [PATCH 09/29] [Bugfix] Restore Ganondorf Lights Hint (#7363) --- .../TimeSavers/SkipCutscene/GanonIntros.cpp | 10 +++++++++- .../vanilla-behavior/GIVanillaBehavior.h | 1 + soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c | 6 +++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp index 1f634a8425..e7dc93f1e6 100644 --- a/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp @@ -2,7 +2,9 @@ #include +#include "soh/Enhancements/custom-message/CustomMessageTypes.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/randomizer/SeedContext.h" #include "soh/ShipInit.hpp" extern "C" { @@ -29,7 +31,13 @@ void RegisterGanonIntros() { } }); - COND_VB_SHOULD(VB_PLAY_GANONDORF_INTRO_CS, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, { *should = false; }); + COND_VB_SHOULD(VB_PLAY_GANONDORF_INTRO_CS, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, { + va_arg(args, void*); + u16 textId = va_arg(args, int); + if (textId != TEXT_GANONDORF || !IS_RANDO || !RAND_GET_OPTION(RSK_GANONDORF_HINT)) { + *should = false; + } + }); // Skip Ganon rising from the rubble, transforming and landing. Skip to knocking sword away. COND_VB_SHOULD(VB_PLAY_GANON_INTRO_CS, CVAR_VALUE, { diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index 41f34ca4e8..dac11a2939 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -2234,6 +2234,7 @@ typedef enum { // ``` // #### `args` // - `*BossGanon` + // - `u16` text ID VB_PLAY_GANONDORF_INTRO_CS, // #### `result` diff --git a/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c b/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c index 6311e0bdbf..58bd1b0164 100644 --- a/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c +++ b/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c @@ -918,7 +918,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { this->csTimer = 0; this->csCamFov = 60.0f; BossGanon_SetIntroCsCamera(this, 12); - if (GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this)) { + if (GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this, 0x70CB)) { Message_StartTextbox(play, 0x70CB, NULL); } } @@ -942,7 +942,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { this->csState = 19; this->csTimer = 0; - if (GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this)) { + if (GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this, 0x70CC)) { Message_StartTextbox(play, 0x70CC, NULL); } Animation_MorphToPlayOnce(&this->skelAnime, &gGanondorfRaiseHandStartAnim, -5.0f); @@ -985,7 +985,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { } if ((this->csTimer > 80) && (Message_GetState(&play->msgCtx) == TEXT_STATE_NONE)) { - if (!GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this)) { + if (!GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this, 0)) { this->timers[2] = 30; this->csCamAt.x = this->unk_1FC.x - 10.0f; this->csCamAt.y = this->unk_1FC.y + 30.0f; From 0a8b53a740acf5e7e6bc947e86a77c79c8bfb342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:12:20 +0000 Subject: [PATCH 10/29] Fix missing open chest logic in Jabu MQ (#7368) --- .../randomizer/location_access/dungeons/jabujabus_belly.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/jabujabus_belly.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/jabujabus_belly.cpp index f3e0da03c6..ce5313ce0d 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/jabujabus_belly.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/jabujabus_belly.cpp @@ -594,7 +594,7 @@ void RegionTable_Init_JabuJabusBelly() { EVENT_ACCESS(LOGIC_FAIRY_ACCESS, logic->CanBreakPots()), }, { //Locations - LOCATION(RC_JABU_JABUS_BELLY_MQ_NEAR_BOSS_CHEST, logic->CanUse(RG_FAIRY_SLINGSHOT)), + LOCATION(RC_JABU_JABUS_BELLY_MQ_NEAR_BOSS_CHEST, logic->CanUse(RG_FAIRY_SLINGSHOT) && logic->HasItem(RG_OPEN_CHEST)), LOCATION(RC_JABU_JABUS_BELLY_MQ_GS_NEAR_BOSS, (logic->HasItem(RG_CLIMB) && (logic->CanUse(RG_BOOMERANG) || (ctx->GetTrickOption(RT_JABU_NEAR_BOSS_RANGED) && logic->CanUse(RG_HOOKSHOT)))) || (ctx->GetTrickOption(RT_JABU_NEAR_BOSS_RANGED) && logic->CanUse(RG_LONGSHOT))), LOCATION(RC_JABU_JABUS_BELLY_MQ_BEFORE_BOSS_POT_1, logic->CanBreakPots()), LOCATION(RC_JABU_JABUS_BELLY_MQ_BEFORE_BOSS_GRASS_1, logic->CanCutShrubs()), From c2325bd751ac3cac4d1854d0b95f3eb56e0dacae Mon Sep 17 00:00:00 2001 From: Reppan <72985260+Jepvid@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:50:58 +0200 Subject: [PATCH 11/29] Add function to customequipment to fix shooting gallery (#7370) --- soh/soh/Enhancements/customequipment.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/customequipment.cpp b/soh/soh/Enhancements/customequipment.cpp index a162d3d10b..3e08c536d9 100644 --- a/soh/soh/Enhancements/customequipment.cpp +++ b/soh/soh/Enhancements/customequipment.cpp @@ -61,6 +61,16 @@ static Gfx* LoadCustomGfx(const char* path) { return ResourceMgr_LoadGfxByName(path); } +// Shooting Gallery being weird giving bow and then load slingshot model when child Link +// This function is used to determine if bow or slingshot model should be loaded +static bool UsesBowModel(Player* player) { + if (CVarGetInteger(CVAR_ENHANCEMENT("BowSlingshotAmmoFix"), 0) || + CVarGetInteger(CVAR_ENHANCEMENT("EquipmentAlwaysVisible"), 0)) { + return !Player_HoldsSlingshot(player); + } + return LINK_IS_ADULT; +} + static u8 sLastValidSwordEquip = EQUIP_VALUE_SWORD_NONE; static u8 GetEquippedSwordValue(PlayState* play) { @@ -317,7 +327,7 @@ static void RegisterCustomEquipment() { case PLAYER_MODELTYPE_RH_BOW_SLINGSHOT_2: fpsHand = isAdult ? gCustomAdultFPSHandDL : gCustomChildFPSHandDL; fpsWeapon = - Player_HoldsBow(player) + UsesBowModel(player) ? ResolveCustomChain({ gCustomFPSBowDL, gCustomBowDL, nullptr }) : ResolveCustomChain({ gCustomFPSSlingshotDL, gCustomSlingshotDL, nullptr }); break; @@ -391,7 +401,7 @@ static void RegisterCustomEquipment() { break; case PLAYER_MODELTYPE_RH_BOW_SLINGSHOT: case PLAYER_MODELTYPE_RH_BOW_SLINGSHOT_2: - customDL = Player_HoldsBow(player) ? gCustomBowDL : gCustomSlingshotDL; + customDL = UsesBowModel(player) ? gCustomBowDL : gCustomSlingshotDL; break; case PLAYER_MODELTYPE_RH_HOOKSHOT: customDL = (player->heldItemAction == PLAYER_IA_HOOKSHOT) ? gCustomHookshotDL From 88f98c63b60dd2eeb6eb8bb490a9726cf5807982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:54:54 +0000 Subject: [PATCH 12/29] Fix anchor issues (#7362) Avoid use-after-free with dummy players, Sync silver rupees, sync autoplanted beans --- .../Enhancements/randomizer/hook_handlers.cpp | 4 +++ soh/soh/Network/Anchor/DummyPlayer.cpp | 6 ++++ soh/soh/Network/Anchor/JsonConversions.hpp | 32 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index c9fd6f7089..a49b4dfe9c 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -11,6 +11,7 @@ #include "soh/Enhancements/randomizer/fishsanity.h" #include "soh/Enhancements/randomizer/static_data.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "soh/SohGui/ImGuiUtils.h" #include "soh/Notification/Notification.h" #include "soh/SaveManager.h" @@ -478,6 +479,9 @@ static void PlantAllBeans() { gSaveContext.sceneFlags[patch.scene].swch |= 1 << patch.swchFlag; if (gPlayState->sceneNum == patch.scene) { Flags_SetSwitch(gPlayState, patch.swchFlag); + } else { + // Flags_SetSwitch fires this for the current scene, other scenes need it so Anchor syncs them + GameInteractor_ExecuteOnSceneFlagSet(patch.scene, FLAG_SCENE_SWITCH, patch.swchFlag); } } ObjBean* bean = (ObjBean*)Actor_Find(&gPlayState->actorCtx, ACTOR_OBJ_BEAN, ACTORCAT_BG); diff --git a/soh/soh/Network/Anchor/DummyPlayer.cpp b/soh/soh/Network/Anchor/DummyPlayer.cpp index fae13d5849..71943745c4 100644 --- a/soh/soh/Network/Anchor/DummyPlayer.cpp +++ b/soh/soh/Network/Anchor/DummyPlayer.cpp @@ -254,4 +254,10 @@ void DummyPlayer_Destroy(Actor* actor, PlayState* play) { // asserts. Set the id back to ACTOR_PLAYER so that `numLoaded` will be decremented // correctly. actor->id = ACTOR_PLAYER; + + // Drop client's pointer so packet handlers don't use after free + uint32_t clientId = Anchor::Instance->GetDummyPlayerClientId(actor); + if (Anchor::Instance->clients.contains(clientId) && Anchor::Instance->clients[clientId].player == (Player*)actor) { + Anchor::Instance->clients[clientId].player = nullptr; + } } diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp index 83c2f77304..bb8e9df9d5 100644 --- a/soh/soh/Network/Anchor/JsonConversions.hpp +++ b/soh/soh/Network/Anchor/JsonConversions.hpp @@ -104,18 +104,50 @@ inline void from_json(const json& j, SohStats& sohStats) { j.at("fileCreatedAt").get_to(sohStats.fileCreatedAt); } +#define SILVER_RUPEE_FIELDS(X) \ + X(silverShadowBlades) \ + X(silverShadowPit) \ + X(silverShadowSpikes) \ + X(silverSpiritChild) \ + X(silverSpiritSun) \ + X(silverSpiritBoulders) \ + X(silverBotw) \ + X(silverIceCavernBlades) \ + X(silverIceCavernBlock) \ + X(silverGtgSlope) \ + X(silverGtgLava) \ + X(silverGtgWater) \ + X(silverGanonLight) \ + X(silverGanonForest) \ + X(silverGanonFire) \ + X(silverGanonSpirit) \ + X(silverMqDodongosCavern) \ + X(silverMqShadowInvisibleBlades) \ + X(silverMqSpiritLobby) \ + X(silverMqSpiritBigWall) \ + X(silverMqGanonWater) \ + X(silverMqGanonShadow) + inline void to_json(json& j, const ShipRandomizerSaveContextData& shipRandomizerSaveContextData) { j = json{ { "triforcePiecesCollected", shipRandomizerSaveContextData.triforcePiecesCollected }, { "bombchuUpgradeLevel", shipRandomizerSaveContextData.bombchuUpgradeLevel }, }; +#define SILVER_TO_JSON(field) j[#field] = shipRandomizerSaveContextData.field; + SILVER_RUPEE_FIELDS(SILVER_TO_JSON) +#undef SILVER_TO_JSON } inline void from_json(const json& j, ShipRandomizerSaveContextData& shipRandomizerSaveContextData) { j.at("triforcePiecesCollected").get_to(shipRandomizerSaveContextData.triforcePiecesCollected); j.at("bombchuUpgradeLevel").get_to(shipRandomizerSaveContextData.bombchuUpgradeLevel); +#define SILVER_FROM_JSON(field) shipRandomizerSaveContextData.field = j.value(#field, (s8)0); + SILVER_RUPEE_FIELDS(SILVER_FROM_JSON) +#undef SILVER_FROM_JSON } +#undef SILVER_RUPEE_FIELDS + inline void to_json(json& j, const ShipQuestSpecificSaveContextData& shipQuestSpecificSaveContextData) { j = json{ { "randomizer", shipQuestSpecificSaveContextData.randomizer }, From 5c6006ee0fdaf37879fac3a19a7899252a934e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:08:14 +0000 Subject: [PATCH 13/29] Fix extracting from MQ rom padded to 64MB (#7376) Also show error message instead of just proceeding to start if vanilla o2r already extracted --- soh/soh/Extractor/Extract.cpp | 9 +++++- soh/soh/OTRGlobals.cpp | 58 ++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/soh/soh/Extractor/Extract.cpp b/soh/soh/Extractor/Extract.cpp index e21f83744b..de16a265aa 100644 --- a/soh/soh/Extractor/Extract.cpp +++ b/soh/soh/Extractor/Extract.cpp @@ -366,7 +366,7 @@ size_t Extractor::GetCurRomSize() const { bool Extractor::ValidateAndFixRom() { // The MQ debug rom sometimes has the header patched to look like a US rom. - // Check the crc as if it were changed back, but leave mRomData as the original dump for torch. + // Check the crc as if it were changed back. CallTorch changes it back for real. const uint8_t region = mRomData[0x3E]; if (GetRomVerCrc() == OOT_PAL_GC_MQ_DBG) { mRomData[0x3E] = 'P'; @@ -666,6 +666,13 @@ bool Extractor::CallTorch(std::string installPath, std::string exportdir, std::a *totalExtract = SohTorch::CountAssetFiles(srcDir + "/" + GetTorchVersionDir()); *extractCount = 0; + // MQ debug dumps only hold 54MB of data, the rest is dump-specific padding, and some have + // the region byte changed. Trim and restore it so every dump matches the one config.yml hash. + if (GetRomVerCrc() == OOT_PAL_GC_MQ_DBG && mRomData.size() == MB64) { + mRomData.resize(MB54); + mRomData[0x3E] = 'P'; + } + // config.yml decides whether this is oot.o2r or oot-mq.o2r. std::string archiveName = SohTorch::Extract(std::move(mRomData), srcDir, tempdir, portVersion, extractCount); bool success = !archiveName.empty(); diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 1b4f62c475..67bd1777d5 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -430,7 +430,12 @@ void OTRGlobals::RunExtract(int argc, char* argv[]) { Extractor extract; PromptSteps promptStep = PS_FILE_CHECK; bool generatedIsMQ = false; + // Whether generatedIsMQ is set, i.e. the first prompted rom extracted fine + bool generatedFirst = false; + bool lastExtractFailed = false; std::atomic extractCount = 0, totalExtract = 0; + // Set by the extraction thread, shown as a popup by the main thread once the task finishes + std::atomic extractFailed = false; std::string installPath = std::filesystem::absolute(Ship::Context::GetAppBundlePath()).string(); std::string dataPath = std::filesystem::absolute(Ship::Context::GetAppDirectoryPath(appShortName)).string(); @@ -613,15 +618,17 @@ void OTRGlobals::RunExtract(int argc, char* argv[]) { std::string msg = "Archive for current ROM, " + archive + ", already exists.\nExtract again?"; SohGui::RegisterPopup("Confirm Re-extract", msg.c_str(), "Yes", "No", [&]() { extractionTask = threadPool->submit_task([&]() -> void { - extract.CallTorch(installPath, Ship::Context::GetAppDirectoryPath(appShortName), - &extractCount, &totalExtract); + extractFailed = + !extract.CallTorch(installPath, Ship::Context::GetAppDirectoryPath(appShortName), + &extractCount, &totalExtract); extractCount = totalExtract = 0; }); }); } else { extractionTask = threadPool->submit_task([&]() -> void { - extract.CallTorch(installPath, Ship::Context::GetAppDirectoryPath(appShortName), - &extractCount, &totalExtract); + extractFailed = + !extract.CallTorch(installPath, Ship::Context::GetAppDirectoryPath(appShortName), + &extractCount, &totalExtract); extractCount = totalExtract = 0; }); } @@ -677,9 +684,13 @@ void OTRGlobals::RunExtract(int argc, char* argv[]) { continue; } extractionTask = threadPool->submit_task([&]() -> void { - extract.CallTorch(installPath, Ship::Context::GetAppDirectoryPath(appShortName), - &extractCount, &totalExtract); - generatedIsMQ = extract.IsMasterQuest(); + extractFailed = + !extract.CallTorch(installPath, Ship::Context::GetAppDirectoryPath(appShortName), + &extractCount, &totalExtract); + if (!extractFailed) { + generatedIsMQ = extract.IsMasterQuest(); + generatedFirst = true; + } promptStep = PS_SECOND; extractCount = 0; totalExtract = 0; @@ -687,17 +698,28 @@ void OTRGlobals::RunExtract(int argc, char* argv[]) { continue; } case PS_SECOND: { + // Keep asking until the user says no, so a failed or cancelled rom can be retried SohGui::RegisterPopup( - "Extraction Complete", "ROM Extracted. Extract another?", "Yes", "No", + lastExtractFailed ? "Extract Another" : "Extraction Complete", + lastExtractFailed ? "Try extracting another ROM?" : "ROM Extracted. Extract another?", + "Yes", "No", [&]() { - if (!extract.ManuallySearchForRomMatchingType(generatedIsMQ ? RomSearchMode::Vanilla - : RomSearchMode::MQ)) { - extractStep = ES_VERIFY; - } else { + RomSearchMode mode = RomSearchMode::Both; + if (generatedFirst) { + mode = generatedIsMQ ? RomSearchMode::Vanilla : RomSearchMode::MQ; + } + if (extract.ManuallySearchForRomMatchingType(mode)) { extractionTask = threadPool->submit_task([&]() -> void { - extract.CallTorch(installPath, Ship::Context::GetAppDirectoryPath(appShortName), - &extractCount, &totalExtract); - extractStep = ES_VERIFY; + extractFailed = !extract.CallTorch( + installPath, Ship::Context::GetAppDirectoryPath(appShortName), + &extractCount, &totalExtract); + if (!extractFailed && generatedFirst) { + extractStep = ES_VERIFY; + } else if (!extractFailed) { + // A retry of the first rom worked, so still offer the other type + generatedIsMQ = extract.IsMasterQuest(); + generatedFirst = true; + } extractCount = 0; totalExtract = 0; }); @@ -754,6 +776,12 @@ void OTRGlobals::RunExtract(int argc, char* argv[]) { } catch (const std::exception& e) { SohGui::RegisterPopup("Extraction Crashed", e.what(), "Close", "", []() { exit(1); }); } + lastExtractFailed = extractFailed; + if (extractFailed) { + SohGui::RegisterPopup("Extraction Failed", + "The ROM could not be extracted. Check the log for details.", "OK", ""); + extractFailed = false; + } extractionTask.reset(); } else { if (!ImGui::IsPopupOpen("ROM Extraction")) { From e3ff1530b38713af5ce95a268b1743d581ba10d0 Mon Sep 17 00:00:00 2001 From: David Racine Date: Mon, 12 Oct 2026 19:01:03 -0400 Subject: [PATCH 14/29] Pin latest libultraship's port-maintenance branch (#7379) --- libultraship | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libultraship b/libultraship index 62e973aeb4..b374560b73 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit 62e973aeb4a53ad4d22bb91e2d9373ecdfcd246c +Subproject commit b374560b73b9756ddf5913dfe0473db131c78f6b From 5c59f8474ea41848cab3c034f8882a1a366ab622 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 12 Oct 2026 22:02:41 -0400 Subject: [PATCH 15/29] [Bugfix] Use game encoding for name in save editor (#7383) --- .../Enhancements/debugger/debugSaveEditor.cpp | 54 ++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp index e2dd08762e..438b490f8e 100644 --- a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp +++ b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp @@ -436,6 +436,34 @@ std::string decodeNTSCPlayerNameChar(int code) { return ret; } +char decodePlayerNameChar(uint8_t code, uint8_t language) { + if (language == NAME_LANGUAGE_PAL) { + return z2ASCII(code); + } + const std::string decoded = decodeNTSCPlayerNameChar(code); + return (decoded.length() == 1 && decoded[0] >= 32 && decoded[0] <= 126) ? decoded[0] : ' '; +} + +uint8_t encodePlayerNameChar(char c, uint8_t language) { + const bool pal = language == NAME_LANGUAGE_PAL; + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'Z') { + return (pal ? 10 : 171) + (c - 'A'); + } + if (c >= 'a' && c <= 'z') { + return (pal ? 36 : 197) + (c - 'a'); + } + if (c == '-') { + return pal ? 0x3F : 0xE4; + } + if (c == '.') { + return pal ? 0x40 : 0xEA; + } + return pal ? 0x3E : 0xDF; +} + enum MagicLevel { MAGIC_LEVEL_NONE, MAGIC_LEVEL_SINGLE, MAGIC_LEVEL_DOUBLE }; std::map magicLevelMap = { @@ -528,13 +556,8 @@ void DrawGeneralTab() { // Convert current name to ASCII for display if (!nameInputInitialized || ImGui::IsWindowFocused()) { for (int i = 0; i < 8; i++) { - if (gSaveContext.ship.filenameLanguage == NAME_LANGUAGE_PAL) { - playerNameInput[i] = z2ASCII(gSaveContext.playerName[i]); - } else { - const std::string decoded = decodeNTSCPlayerNameChar(gSaveContext.playerName[i]); - playerNameInput[i] = - (decoded.length() == 1 && decoded[0] >= 32 && decoded[0] <= 126) ? decoded[0] : ' '; - } + playerNameInput[i] = + decodePlayerNameChar(gSaveContext.playerName[i], gSaveContext.ship.filenameLanguage); } playerNameInput[8] = '\0'; nameInputInitialized = true; @@ -545,12 +568,11 @@ void DrawGeneralTab() { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 8.0f)); if (ImGui::InputText("##PlayerNameInput", playerNameInput, 9)) { // Convert ASCII input back to game encoding + bool ended = false; for (int i = 0; i < 8; i++) { - if (playerNameInput[i] == '\0') { - gSaveContext.playerName[i] = 223; - } else { - gSaveContext.playerName[i] = playerNameInput[i]; - } + ended = ended || playerNameInput[i] == '\0'; + gSaveContext.playerName[i] = + encodePlayerNameChar(ended ? ' ' : playerNameInput[i], gSaveContext.ship.filenameLanguage); } nameInputInitialized = false; } @@ -563,6 +585,7 @@ void DrawGeneralTab() { // Filename encoding const bool hasPAL = (sGerMessageEntryTablePtr != nullptr) && (sFraMessageEntryTablePtr != nullptr); const bool hasNTSC = (sJpnMessageEntryTablePtr != nullptr); + const uint8_t oldNameLanguage = gSaveContext.ship.filenameLanguage; if (hasPAL && hasNTSC) { Combobox("Player Name Language", &gSaveContext.ship.filenameLanguage, filenameLanguageMap, comboboxOptionsBase.Tooltip("Encoding used for Player Name")); @@ -576,6 +599,13 @@ void DrawGeneralTab() { comboboxOptionsBase.Tooltip("Encoding used for Player Name")); ImGui::EndDisabled(); } + if (canEditName && gSaveContext.ship.filenameLanguage != oldNameLanguage) { + for (int i = 0; i < 8; i++) { + gSaveContext.playerName[i] = encodePlayerNameChar( + decodePlayerNameChar(gSaveContext.playerName[i], oldNameLanguage), gSaveContext.ship.filenameLanguage); + } + nameInputInitialized = false; + } UIWidgets::EndCard(); UIWidgets::BeginCard("healthCard"); From 8ccdbb98a0b5bbb89bb67bf62e4219acdac6457b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:30:45 +0000 Subject: [PATCH 16/29] stop timer upon lifting triforce, not upon warping to credits (#7385) --- soh/soh/Enhancements/gameplaystats.cpp | 2 ++ soh/soh/Enhancements/randomizer/hook_handlers.cpp | 3 --- soh/soh/Enhancements/randomizer/randomizer.cpp | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/soh/soh/Enhancements/gameplaystats.cpp b/soh/soh/Enhancements/gameplaystats.cpp index 71ae03c92d..66d90c74a4 100644 --- a/soh/soh/Enhancements/gameplaystats.cpp +++ b/soh/soh/Enhancements/gameplaystats.cpp @@ -293,6 +293,7 @@ void LoadStatsVersion1() { SaveManager::Instance->LoadData("fileCreatedAt", gSaveContext.ship.stats.fileCreatedAt); SaveManager::Instance->LoadData("playTimer", gSaveContext.ship.stats.playTimer); SaveManager::Instance->LoadData("pauseTimer", gSaveContext.ship.stats.pauseTimer); + SaveManager::Instance->LoadData("gameComplete", gSaveContext.ship.stats.gameComplete); SaveManager::Instance->LoadArray( "itemTimestamps", ARRAY_COUNT(gSaveContext.ship.stats.itemTimestamp), [](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.ship.stats.itemTimestamp[i]); }); @@ -343,6 +344,7 @@ void SaveStats(const SaveContext& saveContext, int sectionID, bool fullSave) { SaveManager::Instance->SaveData("fileCreatedAt", saveContext.ship.stats.fileCreatedAt); SaveManager::Instance->SaveData("playTimer", saveContext.ship.stats.playTimer); SaveManager::Instance->SaveData("pauseTimer", saveContext.ship.stats.pauseTimer); + SaveManager::Instance->SaveData("gameComplete", saveContext.ship.stats.gameComplete); SaveManager::Instance->SaveArray( "itemTimestamps", ARRAY_COUNT(saveContext.ship.stats.itemTimestamp), [&](size_t i) { SaveManager::Instance->SaveData("", saveContext.ship.stats.itemTimestamp[i]); }); diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index a49b4dfe9c..6d329f823d 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -2743,9 +2743,6 @@ void RandomizerOnPlayerUpdateHandler() { // 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; diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 9920897368..6d10ccf77e 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -1398,6 +1398,9 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { } break; case RG_TRIFORCE: + gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] = + static_cast(GAMEPLAYSTAT_TOTAL_TIME); + gSaveContext.ship.stats.gameComplete = 1; GameInteractor_SetTriforceHuntCreditsWarpActive(true); break; case RG_TRIFORCE_PIECE: From e65fa741db8a85797a2b822bd925fb606b20a16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:36:27 +0000 Subject: [PATCH 17/29] Fix OOB check in audio_seqplayer.c (#7387) --- soh/src/code/audio_seqplayer.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/soh/src/code/audio_seqplayer.c b/soh/src/code/audio_seqplayer.c index 92efecd02e..b3bfdc5715 100644 --- a/soh/src/code/audio_seqplayer.c +++ b/soh/src/code/audio_seqplayer.c @@ -1186,12 +1186,12 @@ void AudioSeq_SequenceChannelProcessScript(SequenceChannel* channel) { u16 seqId = AudioEditor_GetReplacementSeq(seqPlayer->seqId); SequenceData sDat = ResourceMgr_LoadSeqByName(sequenceMap[seqId]); - // The game apparantely would sometimes do negative array lookups, the result of which would + // The game apparently would sometimes do negative array lookups, the result of which would // get rejected by AudioHeap_SearchCaches, never changing the actual fontid. - if (result > sDat.numFonts) + if (result >= sDat.numFonts) break; - fontId = sDat.fonts[(sDat.numFonts - result - 1)]; + fontId = sDat.fonts[sDat.numFonts - result - 1]; } if (AudioHeap_SearchCaches(FONT_TABLE, CACHE_EITHER, fontId)) { From 7146143058fcf358580947cc3c1339af098587f8 Mon Sep 17 00:00:00 2001 From: Jordan Longstaff Date: Tue, 13 Oct 2026 09:39:15 -0400 Subject: [PATCH 18/29] Bombchu Bowling ammo changeable during minigame (#7350) --- .../Enhancements/Minigames/BombchuBowling.cpp | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/soh/soh/Enhancements/Minigames/BombchuBowling.cpp b/soh/soh/Enhancements/Minigames/BombchuBowling.cpp index 8ce959e02c..84f08b146e 100644 --- a/soh/soh/Enhancements/Minigames/BombchuBowling.cpp +++ b/soh/soh/Enhancements/Minigames/BombchuBowling.cpp @@ -88,11 +88,22 @@ static void RegisterBombchuBowlingNoBigCucco() { } static void RegisterBombchuBowlingAmmo() { - COND_VB_SHOULD(VB_SET_BOMBCHU_BOWLING_AMMO, - CVAR_BOWLING_VALUE && (CVAR_BOWLING_AMMO_VALUE != CUCCO_BOWLING_AMMO_DEFAULT), { - gPlayState->bombchuBowlingStatus = CVAR_BOWLING_AMMO_VALUE; - *should = false; - }); + static s32 prevAmmoValue = CUCCO_BOWLING_AMMO_DEFAULT; + s32 nextAmmoValue = CVAR_BOWLING_VALUE ? CVAR_BOWLING_AMMO_VALUE : CUCCO_BOWLING_AMMO_DEFAULT; + + if ((prevAmmoValue != nextAmmoValue) && (gPlayState->bombchuBowlingStatus > 0)) { + gPlayState->bombchuBowlingStatus += nextAmmoValue - prevAmmoValue; + if (gPlayState->bombchuBowlingStatus <= 0) { + gPlayState->bombchuBowlingStatus = -1; + } + prevAmmoValue = nextAmmoValue; + } + + COND_VB_SHOULD(VB_SET_BOMBCHU_BOWLING_AMMO, nextAmmoValue != CUCCO_BOWLING_AMMO_DEFAULT, { + prevAmmoValue = CVAR_BOWLING_AMMO_VALUE; + gPlayState->bombchuBowlingStatus = prevAmmoValue; + *should = false; + }); } static void RegisterBombchuBowlingFirstPrize() { From 28ab1206742e86db6cbf8b3f334e7c66a4e7869d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:30:20 +0000 Subject: [PATCH 19/29] Fix logic errors due to unused regions (#7396) --- .../location_access/dungeons/ganons_castle.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/ganons_castle.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/ganons_castle.cpp index f34ebc7d3a..635e97f696 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/ganons_castle.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/ganons_castle.cpp @@ -623,7 +623,7 @@ void RegionTable_Init_GanonsCastle() { LOCATION(RC_GANONS_CASTLE_MQ_FIRE_TRIAL_POT_2, logic->CanBreakPots()), }, { //Exits - ENTRANCE(RR_GANONS_CASTLE_MQ_FIRE_TRIAL_BARRED_DOOR, true), + ENTRANCE(RR_GANONS_CASTLE_MQ_FIRE_TRIAL_FROM_BARRED, true), }); areaTable[RR_GANONS_CASTLE_MQ_WATER_TRIAL_GEYSER_ROOM] = Region("Ganon's Castle MQ Water Trial Geyser Room", SCENE_INSIDE_GANONS_CASTLE, { @@ -919,7 +919,7 @@ void RegionTable_Init_GanonsCastle() { //Exits //I got the trick going backwards, but only while taking damage. this isn't relevant anyway though ENTRANCE(RR_GANONS_CASTLE_MQ_LIGHT_TRIAL_BOULDER_ROOM_FRONT, logic->CanUse(RG_HOOKSHOT) || ctx->GetTrickOption(RT_GANON_MQ_LIGHT_TRIAL) || (logic->IsAdult && logic->CanGroundJump())), - ENTRANCE(RR_GANONS_CASTLE_MQ_LIGHT_TRIAL_FINAL_ROOM, logic->SmallKeys(SCENE_INSIDE_GANONS_CASTLE, 3)), + ENTRANCE(RR_GANONS_CASTLE_MQ_LIGHT_TRIAL_FAKE_FINAL_ROOM, logic->SmallKeys(SCENE_INSIDE_GANONS_CASTLE, 3)), }); areaTable[RR_GANONS_CASTLE_MQ_LIGHT_TRIAL_FAKE_FINAL_ROOM] = Region("Ganon's Castle MQ Light Trial Fake Final Room", SCENE_INSIDE_GANONS_CASTLE, {}, {}, { @@ -935,10 +935,7 @@ void RegionTable_Init_GanonsCastle() { //Locations LOCATION(RC_GANONS_CASTLE_MQ_LIGHT_TRIAL_POT_1, logic->CanBreakPots()), LOCATION(RC_GANONS_CASTLE_MQ_LIGHT_TRIAL_POT_2, logic->CanBreakPots()), - }, { - //Exits - ENTRANCE(RR_GANONS_CASTLE_MQ_LIGHT_TRIAL_BOULDER_ROOM_BACK, logic->SmallKeys(SCENE_INSIDE_GANONS_CASTLE, 3)), - }); + }, {}); #pragma endregion From 7aa53de9f2c0f4470ac36d2895648116f73193b5 Mon Sep 17 00:00:00 2001 From: Pepper0ni <93387759+Pepper0ni@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:02:34 +0100 Subject: [PATCH 20/29] Try to fix windows builds (#7397) --- CMake/automate-vcpkg.cmake | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CMake/automate-vcpkg.cmake b/CMake/automate-vcpkg.cmake index 4091949594..238b9ddcd0 100644 --- a/CMake/automate-vcpkg.cmake +++ b/CMake/automate-vcpkg.cmake @@ -82,11 +82,7 @@ if (WIN32) endif() if(NOT DEFINED VCPKG_ROOT) - if(NOT DEFINED ENV{VCPKG_ROOT}) - set(VCPKG_ROOT ${VCPKG_FALLBACK_ROOT}) - else() - set(VCPKG_ROOT $ENV{VCPKG_ROOT}) - endif() + set(VCPKG_ROOT ${VCPKG_FALLBACK_ROOT}) endif() # Installs a new copy of Vcpkg or updates an existing one From 9ffa4c86175f13863abe877e352fdb444973d46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:33:50 +0000 Subject: [PATCH 21/29] Fix defeating Ganon when wincon is not Defeat Ganon warping outside Hyrule Castle (#7404) --- .../Enhancements/GameplayStats/BossDefeatTimestamps.cpp | 5 ++++- soh/soh/Enhancements/gameplaystats.h | 8 ++++---- soh/soh/Enhancements/randomizer/hook_handlers.cpp | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/soh/soh/Enhancements/GameplayStats/BossDefeatTimestamps.cpp b/soh/soh/Enhancements/GameplayStats/BossDefeatTimestamps.cpp index 7c57ed7dca..f8ca31b56f 100644 --- a/soh/soh/Enhancements/GameplayStats/BossDefeatTimestamps.cpp +++ b/soh/soh/Enhancements/GameplayStats/BossDefeatTimestamps.cpp @@ -1,5 +1,6 @@ #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/ShipInit.hpp" +#include "soh/Enhancements/randomizer/SeedContext.h" extern "C" { #include "z64save.h" @@ -23,7 +24,9 @@ static void RegisterBossDefeatTimestamps() { BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_GANON, TIMESTAMP_DEFEAT_GANONDORF); COND_ID_HOOK(OnBossDefeat, ACTOR_BOSS_GANON2, true, [](void* refActor) { gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] = static_cast(GAMEPLAYSTAT_TOTAL_TIME); - gSaveContext.ship.stats.gameComplete = true; + if (!IS_RANDO || RAND_GET_OPTION(RSK_WINCON).Is(RO_WINCON_DEFEAT_GANON)) { + gSaveContext.ship.stats.gameComplete = true; + } }); } diff --git a/soh/soh/Enhancements/gameplaystats.h b/soh/soh/Enhancements/gameplaystats.h index bf9d651a6c..e9121bfc43 100644 --- a/soh/soh/Enhancements/gameplaystats.h +++ b/soh/soh/Enhancements/gameplaystats.h @@ -25,10 +25,10 @@ char* GameplayStats_GetCurrentTime(); ? (!gSaveContext.ship.stats.firstInput \ ? 0 \ : ((GetUnixTimestamp() - gSaveContext.ship.stats.firstInput) / 100)) \ - : (gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] \ - ? gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] \ - : (gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] \ - ? gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] \ + : (gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] \ + ? gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] \ + : (gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] \ + ? gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] \ : gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TIMESPLITS_COMPLETED]))) \ : (gSaveContext.ship.stats.playTimer / 2 + gSaveContext.ship.stats.pauseTimer / 3)) #define CURRENT_MODE_TIMER \ diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index 6d329f823d..d501c8e5a3 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -1872,7 +1872,7 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_GANONS_TOWER); randomizerQueuedChecks.push(RC_GANON); CheckTriggers(); - gPlayState->nextEntranceIndex = ENTR_OUTSIDE_GANONS_CASTLE_1_2; + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(ENTR_OUTSIDE_GANONS_CASTLE_1_2); gPlayState->transitionTrigger = TRANS_TRIGGER_START; gPlayState->transitionType = TRANS_TYPE_FADE_WHITE; gSaveContext.nextTransitionType = TRANS_TYPE_FADE_WHITE_SLOW; From 2064618b0f70d0d8bf83e1c3ee6a5539991bae5d Mon Sep 17 00:00:00 2001 From: Reppan <72985260+Jepvid@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:33:13 +0200 Subject: [PATCH 22/29] Add Function to custom equipment to guard against cutscene shenanigans (#7409) --- soh/soh/Enhancements/customequipment.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/soh/soh/Enhancements/customequipment.cpp b/soh/soh/Enhancements/customequipment.cpp index 3e08c536d9..94f5bda98e 100644 --- a/soh/soh/Enhancements/customequipment.cpp +++ b/soh/soh/Enhancements/customequipment.cpp @@ -71,6 +71,16 @@ static bool UsesBowModel(Player* player) { return LINK_IS_ADULT; } +// Detects cutscenes that swap the left hand DL without updating leftHandType. +static bool IsLeftHandSwappedByCutscene(Player* player) { + Gfx** dLists = player->leftHandDLists; + if (dLists == &gPlayerLeftHandOpenDLs[0] || dLists == &gPlayerLeftHandOpenDLs[1] || + dLists == &gPlayerLeftHandClosedDLs[0] || dLists == &gPlayerLeftHandClosedDLs[1]) { + return true; + } + return dLists == &gPlayerLeftHandBgsDLs[1] && player->leftHandType != PLAYER_MODELTYPE_LH_BGS; +} + static u8 sLastValidSwordEquip = EQUIP_VALUE_SWORD_NONE; static u8 GetEquippedSwordValue(PlayState* play) { @@ -260,6 +270,8 @@ static void RegisterCustomEquipment() { } break; } + if (IsLeftHandSwappedByCutscene(player)) + break; switch ((u8)player->leftHandType) { case PLAYER_MODELTYPE_LH_SWORD: { if (player == GET_PLAYER(play)) { From ce6edf821d4861f2da9e1be6b9b0d5e323574fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:45:33 +0000 Subject: [PATCH 23/29] Fix tracker showing too any keys with skeleton key after using key (#7403) --- soh/soh/Enhancements/randomizer/dungeon.cpp | 5 +++++ soh/soh/Enhancements/randomizer/dungeon.h | 2 ++ soh/soh/Enhancements/randomizer/randomizer.cpp | 11 ++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/dungeon.cpp b/soh/soh/Enhancements/randomizer/dungeon.cpp index e0fb53132e..d730287635 100644 --- a/soh/soh/Enhancements/randomizer/dungeon.cpp +++ b/soh/soh/Enhancements/randomizer/dungeon.cpp @@ -177,6 +177,11 @@ int8_t GetSceneTotalSmallKeys(const SaveContext* saveContext, const SceneID scen return FindCurrentSmallKeys(saveContext, scene); } +int8_t GetSceneUnspentSmallKeyMax(const SaveContext* saveContext, const SceneID scene) { + int8_t used = GetSceneTotalSmallKeys(saveContext, scene) - FindCurrentSmallKeys(saveContext, scene); + return GetSceneSmallKeyMax(scene) - used; +} + int8_t DungeonInfo::GetUsedSmallKeys(SaveContext* saveContext) const { return FindUsedSmallKeys(saveContext, scene, GetDoorFlags()); } diff --git a/soh/soh/Enhancements/randomizer/dungeon.h b/soh/soh/Enhancements/randomizer/dungeon.h index f956b7bfe3..4e5ca931b7 100644 --- a/soh/soh/Enhancements/randomizer/dungeon.h +++ b/soh/soh/Enhancements/randomizer/dungeon.h @@ -83,6 +83,8 @@ int8_t FindTotalSmallKeys(const SaveContext* saveContext, const SceneID scene, s uint8_t GetSceneSmallKeyMax(SceneID scene); /// Small keys held for the scene plus the ones already spent there. int8_t GetSceneTotalSmallKeys(const SaveContext* saveContext, SceneID scene); +/// The scene's small key max, minus keys already used on doors. Key rings and skeleton key set held keys to this. +int8_t GetSceneUnspentSmallKeyMax(const SaveContext* saveContext, SceneID scene); typedef enum { DEKU_TREE, diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 6d10ccf77e..03b209b0a1 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -1169,13 +1169,13 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { // This isn't technically necessary, because keys will no longer be consumed, // but for the player's sanity we display that they _have_ keys. for (Rando::DungeonInfo* dungeon : Rando::Context::GetInstance()->GetDungeons()->GetDungeonList()) { - uint8_t keys = Rando::GetSceneSmallKeyMax(dungeon->GetScene()); - if (keys > 0) { - gSaveContext.inventory.dungeonKeys[dungeon->GetScene()] = keys; + if (Rando::GetSceneSmallKeyMax(dungeon->GetScene()) > 0) { + gSaveContext.inventory.dungeonKeys[dungeon->GetScene()] = + Rando::GetSceneUnspentSmallKeyMax(&gSaveContext, dungeon->GetScene()); } } gSaveContext.inventory.dungeonKeys[SCENE_THIEVES_HIDEOUT] = - Rando::GetSceneSmallKeyMax(SCENE_THIEVES_HIDEOUT); + Rando::GetSceneUnspentSmallKeyMax(&gSaveContext, SCENE_THIEVES_HIDEOUT); } else if (item >= RG_KEATON_MASK && item <= RG_MASK_OF_TRUTH) { if (!ChildTradeSlotOccupied()) { INV_CONTENT(ITEM_TRADE_CHILD) = (int)ITEM_MASK_KEATON + (item - RG_KEATON_MASK); @@ -1348,7 +1348,8 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { if ((item >= RG_FOREST_TEMPLE_KEY_RING) && (item <= RG_TREASURE_GAME_KEY_RING)) { u8 keysOnRing = Rando::GetSceneSmallKeyMax((SceneID)mapIndex); gSaveContext.ship.stats.dungeonKeys[mapIndex] = keysOnRing; - gSaveContext.inventory.dungeonKeys[mapIndex] = keysOnRing; + gSaveContext.inventory.dungeonKeys[mapIndex] = + Rando::GetSceneUnspentSmallKeyMax(&gSaveContext, (SceneID)mapIndex); return Return_Item_Entry(giEntry, RG_NONE); } From d657862aad769081cf93c871def89323ff5d6d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:40:33 +0000 Subject: [PATCH 24/29] LC_NUMERIC C (#7413) --- soh/src/code/main.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/soh/src/code/main.c b/soh/src/code/main.c index 98c632665b..4e68d5da15 100644 --- a/soh/src/code/main.c +++ b/soh/src/code/main.c @@ -54,6 +54,8 @@ int SDL_main(int argc, char* argv[]) { #endif // Allow non-ascii characters for Windows setlocale(LC_ALL, ".UTF8"); + // Keep '.' as the decimal point, user locale may use ',' + setlocale(LC_NUMERIC, "C"); #else //_WIN32 int main(int argc, char* argv[]) { From 62ddf348ce4b3508163671d290a47b796142aef6 Mon Sep 17 00:00:00 2001 From: Pepper0ni <93387759+Pepper0ni@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:41:06 +0100 Subject: [PATCH 25/29] push latest LUS to dewey (#7416) --- libultraship | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libultraship b/libultraship index b374560b73..da6daf1a50 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit b374560b73b9756ddf5913dfe0473db131c78f6b +Subproject commit da6daf1a50a5e46d6641408d52227e31e85e46df From 156c2a4a0a2da9812db768a64a865dc4f504de9c Mon Sep 17 00:00:00 2001 From: Pepper0ni <93387759+Pepper0ni@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:47:34 +0100 Subject: [PATCH 26/29] Some logic fixes for dewey (#7417) add outside GTG sign to guards with nothing trick. --- soh/assets/custom/lang/en_US.json | 2 +- .../dungeons/forest_temple.cpp | 4 +- .../dungeons/jabujabus_belly.cpp | 2 +- .../dungeons/spirit_temple.cpp | 4 +- .../overworld/gerudo_fortress.cpp | 4 +- .../overworld/gerudo_valley.cpp | 10 +- .../overworld/hyrule_field.cpp | 96 +++++++++---------- .../location_access/overworld/lake_hylia.cpp | 84 ++++++++-------- 8 files changed, 103 insertions(+), 103 deletions(-) diff --git a/soh/assets/custom/lang/en_US.json b/soh/assets/custom/lang/en_US.json index 06699d067c..ee3365a5c8 100644 --- a/soh/assets/custom/lang/en_US.json +++ b/soh/assets/custom/lang/en_US.json @@ -2310,7 +2310,7 @@ }, "pass_guards_with_nothing": { "name": "Sneak Past Moving Gerudo Guards with No Items", - "description": "The logic normally guarantees Bow or Hookshot to stun them from a distance, but every moving guard can be passed with basic movement and AI manipulation" + "description": "The logic normally guarantees Bow or Hookshot to stun them from a distance, but every moving guard can be passed with basic movement and AI manipulation. This trick also enables reading the sign outside GTG without Gerudo card, which is barely possible." }, "gf_wasteland_gate_sidehop_skip": { "name": "Gerudo's Fortress Gate Skip with Sidehop", diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/forest_temple.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/forest_temple.cpp index 037a61f11b..4bf05eea7d 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/forest_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/forest_temple.cpp @@ -153,7 +153,7 @@ void RegionTable_Init_ForestTemple() { }, {}, { //Exits ENTRANCE(RR_FOREST_TEMPLE_NE_COURTYARD_LOWER, true), - ENTRANCE(RR_FOREST_TEMPLE_NE_COURTYARD_SWITCH, logic->BunnyHood() || logic->CanUse(RG_CLIMB) || (logic->IsAdult && ctx->GetTrickOption(RT_UNINTUITIVE_JUMPS)) || logic->CanUse(RG_HOOKSHOT) ||logic->CanUse(RG_HOVER_BOOTS)), + ENTRANCE(RR_FOREST_TEMPLE_NE_COURTYARD_SWITCH, logic->BunnyHood() || logic->HasItem(RG_CLIMB) || (logic->IsAdult && ctx->GetTrickOption(RT_UNINTUITIVE_JUMPS)) || logic->CanUse(RG_HOOKSHOT) ||logic->CanUse(RG_HOVER_BOOTS)), ENTRANCE(RR_FOREST_TEMPLE_NE_COURTYARD_DOORFRAME, logic->CanRecoilHover(RECOIL_HAMMER) || ((ctx->GetTrickOption(RT_FOREST_DOORFRAME) && logic->CanUse(RG_HOVER_BOOTS) && (logic->BunnyHood() || logic->CanJumpslash())))), ENTRANCE(RR_FOREST_TEMPLE_MAP_ROOM, true), }); @@ -704,7 +704,7 @@ void RegionTable_Init_ForestTemple() { ENTRANCE(RR_FOREST_TEMPLE_MQ_NE_COURTYARD_DOORFRAME, logic->CanRecoilHover(RECOIL_HAMMER) || (logic->IsAdult && logic->BunnyHovers()) || ((ctx->GetTrickOption(RT_FOREST_DOORFRAME) && logic->CanUse(RG_HOVER_BOOTS) && (logic->CanJumpslash() || logic->BunnyHood()))) || (logic->IsChild && (ctx->GetTrickOption(RT_FOREST_MQ_CHILD_DOORFRAME) || logic->CanMiddairGroundJump()))), - ENTRANCE(RR_FOREST_TEMPLE_MQ_COURTYARD_TOP_CHEST, logic->BunnyHood() || logic->CanUse(RG_CLIMB) || (logic->IsAdult && ctx->GetTrickOption(RT_UNINTUITIVE_JUMPS)) || logic->CanUse(RG_HOOKSHOT) ||logic->CanUse(RG_HOVER_BOOTS)), + ENTRANCE(RR_FOREST_TEMPLE_MQ_COURTYARD_TOP_CHEST, logic->BunnyHood() || logic->HasItem(RG_CLIMB) || (logic->IsAdult && ctx->GetTrickOption(RT_UNINTUITIVE_JUMPS)) || logic->CanUse(RG_HOOKSHOT) ||logic->CanUse(RG_HOVER_BOOTS)), }); areaTable[RR_FOREST_TEMPLE_MQ_COURTYARD_TOP_CHEST] = Region("Forest Temple MQ Courtyard Top Chest", SCENE_FOREST_TEMPLE, {}, { diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/jabujabus_belly.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/jabujabus_belly.cpp index ce5313ce0d..5565f11d84 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/jabujabus_belly.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/jabujabus_belly.cpp @@ -396,7 +396,7 @@ void RegionTable_Init_JabuJabusBelly() { //Exits //without swim, jump from rang chest to the other side //you can also bunnyhovers to force your way through but that's a trick - ENTRANCE(RR_JABU_JABUS_BELLY_MQ_WATER_SWITCH_ROOM_PAST_GEYSER, AnyAgeTime([]{return logic->CanKillEnemy(RE_LIZALFOS) && (logic->IsAdult || logic->CanUse(RG_CLIMB) || logic->HasItem(RG_BRONZE_SCALE));})), + ENTRANCE(RR_JABU_JABUS_BELLY_MQ_WATER_SWITCH_ROOM_PAST_GEYSER, AnyAgeTime([]{return logic->CanKillEnemy(RE_LIZALFOS) && (logic->IsAdult || logic->HasItem(RG_CLIMB) || logic->HasItem(RG_BRONZE_SCALE));})), ENTRANCE(RR_JABU_JABUS_BELLY_MQ_HOLES_ROOM, (logic->IsAdult || logic->HasItem(RG_BRONZE_SCALE)) && AnyAgeTime([]{return logic->CanKillEnemy(RE_LIZALFOS);})), }); 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 518be8835a..4faf6296bc 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp @@ -724,7 +724,7 @@ void RegionTable_Init_SpiritTemple() { //Needs 2 keys if hover recoils are on because you can recoil past the jet and waste a key without having Statue Room Access. //If the clip through the grate in 3 suns room is added to logic, being able to perform that will reduce the requirement as well. ENTRANCE(RR_SPIRIT_TEMPLE_MQ_UNDER_LIKE_LIKE, logic->SmallKeys(SCENE_SPIRIT_TEMPLE, - (ctx->GetTrickOption(RT_HOVER_BOOST_SIMPLE) && !((logic->Get(LOGIC_SPIRIT_MQ_3SUNS_ENEMIES) && (logic->CanUse(RG_CLIMB) || logic->CanUse(RG_LONGSHOT))) || logic->Get(LOGIC_REVERSE_SPIRIT_ADULT))) ? 2 : 1)), + (ctx->GetTrickOption(RT_HOVER_BOOST_SIMPLE) && !((logic->Get(LOGIC_SPIRIT_MQ_3SUNS_ENEMIES) && (logic->HasItem(RG_CLIMB) || logic->CanUse(RG_LONGSHOT))) || logic->Get(LOGIC_REVERSE_SPIRIT_ADULT))) ? 2 : 1)), }); areaTable[RR_SPIRIT_TEMPLE_MQ_UNDER_LIKE_LIKE] = Region("Spirit Temple MQ Under Like Like", SCENE_SPIRIT_TEMPLE, {}, { @@ -748,7 +748,7 @@ void RegionTable_Init_SpiritTemple() { ENTRANCE(RR_SPIRIT_TEMPLE_MQ_UNDER_LIKE_LIKE, logic->CanHitSwitch()), //This exit only governs child forwards access, adult and reverse access starts on the other side so never checks this ENTRANCE(RR_SPIRIT_TEMPLE_MQ_STATUE_ROOM_CHILD, logic->SmallKeys(SCENE_SPIRIT_TEMPLE, - (ctx->GetTrickOption(RT_HOVER_BOOST_SIMPLE) && !((logic->Get(LOGIC_SPIRIT_MQ_3SUNS_ENEMIES) && (logic->CanUse(RG_CLIMB) || logic->CanUse(RG_LONGSHOT))) || logic->Get(LOGIC_REVERSE_SPIRIT_ADULT))) ? 3 : 2)), + (ctx->GetTrickOption(RT_HOVER_BOOST_SIMPLE) && !((logic->Get(LOGIC_SPIRIT_MQ_3SUNS_ENEMIES) && (logic->HasItem(RG_CLIMB) || logic->CanUse(RG_LONGSHOT))) || logic->Get(LOGIC_REVERSE_SPIRIT_ADULT))) ? 3 : 2)), }); areaTable[RR_SPIRIT_TEMPLE_MQ_STATUE_ROOM_CHILD] = Region("Spirit Temple MQ Statue Room Child", SCENE_SPIRIT_TEMPLE, {}, { diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/gerudo_fortress.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/gerudo_fortress.cpp index be32703841..8de600382f 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/gerudo_fortress.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/gerudo_fortress.cpp @@ -49,7 +49,7 @@ void RegionTable_Init_GerudoFortress() { EVENT_ACCESS(LOGIC_GTG_GATE_OPEN, logic->IsAdult && logic->HasItem(RG_GERUDO_MEMBERSHIP_CARD) && logic->HasItem(RG_CHILD_WALLET) && logic->HasItem(RG_SPEAK_GERUDO)), }, { //Locations - LOCATION(RC_GF_GTG_ENTRANCE_RECTANGLE_SIGN, logic->IsAdult && logic->CanRead()), + LOCATION(RC_GF_GTG_ENTRANCE_RECTANGLE_SIGN, logic->IsAdult && logic->CanRead() && (ctx->GetTrickOption(RT_PASS_GUARDS_WITH_NOTHING) || logic->HasItem(RG_GERUDO_MEMBERSHIP_CARD))), }, { //Exits ENTRANCE(RR_GF_TO_GTG, logic->Get(LOGIC_GTG_GATE_OPEN) && (logic->IsAdult || ctx->GetOption(RSK_SHUFFLE_DUNGEON_ENTRANCES))), @@ -57,7 +57,7 @@ void RegionTable_Init_GerudoFortress() { ENTRANCE(RR_GF_JAIL_WINDOW, logic->CanUse(RG_HOOKSHOT)), ENTRANCE(RR_GF_OUTSKIRTS, true), ENTRANCE(RR_GF_NEAR_GROTTO, logic->IsChild || logic->CanPassEnemy(RE_GERUDO_GUARD)), - // RANDTODO: Add tricks for getting past the gerudo guarding the hba range + // RANDOTODO: Add tricks for getting past the gerudo guarding the hba range ENTRANCE(RR_GF_ABOVE_GTG, logic->IsChild || logic->CanPassEnemy(RE_GERUDO_GUARD)), ENTRANCE(RR_GF_TOP_OF_UPPER_VINES, logic->HasItem(RG_GERUDO_MEMBERSHIP_CARD) && logic->CanUse(RG_LONGSHOT)), ENTRANCE(RR_GF_HBA_RANGE, logic->IsChild || logic->HasItem(RG_GERUDO_MEMBERSHIP_CARD)), diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/gerudo_valley.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/gerudo_valley.cpp index 81e4ca5fb4..9cedc4272f 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/gerudo_valley.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/gerudo_valley.cpp @@ -118,10 +118,10 @@ void RegionTable_Init_GerudoValley() { LOCATION(RC_GV_CRATE_BRIDGE_2, logic->IsChild && logic->CanBreakCrates()), LOCATION(RC_GV_CRATE_BRIDGE_3, logic->IsChild && logic->CanBreakCrates()), LOCATION(RC_GV_CRATE_BRIDGE_4, logic->IsChild && logic->CanBreakCrates()), - LOCATION(RC_GV_ROCK_ACROSS_BRIDGE_1, logic->IsAdult), - LOCATION(RC_GV_ROCK_ACROSS_BRIDGE_2, logic->IsAdult), - LOCATION(RC_GV_ROCK_ACROSS_BRIDGE_3, logic->IsAdult), - LOCATION(RC_GV_ROCK_ACROSS_BRIDGE_4, logic->IsAdult), + LOCATION(RC_GV_ROCK_ACROSS_BRIDGE_1, logic->IsAdult && logic->CanBreakRocks()), + LOCATION(RC_GV_ROCK_ACROSS_BRIDGE_2, logic->IsAdult && logic->CanBreakRocks()), + LOCATION(RC_GV_ROCK_ACROSS_BRIDGE_3, logic->IsAdult && logic->CanBreakRocks()), + LOCATION(RC_GV_ROCK_ACROSS_BRIDGE_4, logic->IsAdult && logic->CanBreakRocks()), LOCATION(RC_GV_BOULDER_ACROSS_BRIDGE, logic->IsAdult && logic->BlastOrSmash()), LOCATION(RC_GV_BRONZE_BOULDER_ACROSS_BRIDGE_1, logic->IsAdult && logic->CanUse(RG_MEGATON_HAMMER)), LOCATION(RC_GV_BRONZE_BOULDER_ACROSS_BRIDGE_2, logic->IsAdult && logic->CanUse(RG_MEGATON_HAMMER)), @@ -134,7 +134,7 @@ void RegionTable_Init_GerudoValley() { ENTRANCE(RR_GF_OUTSKIRTS, true), ENTRANCE(RR_GV_UPPER_STREAM, logic->TakeDamage()), ENTRANCE(RR_GV_UPPER_STREAM_WATER, true), - ENTRANCE(RR_GERUDO_VALLEY, logic->IsChild || logic->SummonEpona() || logic->CanUse(RG_LONGSHOT) || ctx->GetOption(RSK_GERUDO_FORTRESS).Is(RO_GF_CARPENTERS_FREE) || logic->Get(LOGIC_TH_RESCUED_ALL_CARPENTERS) || logic->CanRecoilHover(RECOIL_HAMMER) || (logic->BunnyHovers() && logic->CanUse(RG_CLIMB))), + ENTRANCE(RR_GERUDO_VALLEY, logic->IsChild || logic->SummonEpona() || logic->CanUse(RG_LONGSHOT) || ctx->GetOption(RSK_GERUDO_FORTRESS).Is(RO_GF_CARPENTERS_FREE) || logic->Get(LOGIC_TH_RESCUED_ALL_CARPENTERS) || logic->CanRecoilHover(RECOIL_HAMMER) || (logic->BunnyHovers() && logic->HasItem(RG_CLIMB))), ENTRANCE(RR_GV_CARPENTER_TENT, logic->IsAdult || ctx->GetTrickOption(RT_GV_CHILD_TENT)), ENTRANCE(RR_GV_STORMS_GROTTO, logic->IsAdult && logic->CanOpenStormsGrotto()), ENTRANCE(RR_GV_CRATE_LEDGE, (ctx->GetTrickOption(RT_DAMAGE_BOOST_SIMPLE) && logic->HasExplosives()) || (ctx->GetTrickOption(RT_GV_CRATE_HOVERS) && logic->TakeDamage() && logic->CanUse(RG_HOVER_BOOTS) && (logic->CanUse(RG_MASTER_SWORD) || logic->CanUse(RG_BIGGORON_SWORD)))), diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/hyrule_field.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/hyrule_field.cpp index 6bfc887ae1..422317af68 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/hyrule_field.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/hyrule_field.cpp @@ -14,54 +14,54 @@ void RegionTable_Init_HyruleField() { LOCATION(RC_HF_OCARINA_OF_TIME_ITEM, logic->IsChild && logic->StoneCount() == 3 && logic->HasItem(RG_BRONZE_SCALE)), LOCATION(RC_SONG_FROM_OCARINA_OF_TIME, logic->IsChild && logic->StoneCount() == 3 && logic->HasItem(RG_BRONZE_SCALE)), LOCATION(RC_HF_POND_STORMS_FAIRY, logic->CanUse(RG_SONG_OF_STORMS)), - LOCATION(RC_HF_CENTRAL_GRASS_1, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_2, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_3, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_4, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_5, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_6, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_7, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_8, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_9, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_10, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_11, logic->CanCutShrubs()), - LOCATION(RC_HF_CENTRAL_GRASS_12, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_1, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_2, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_3, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_4, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_5, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_6, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_7, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_8, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_9, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_10, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_11, logic->CanCutShrubs()), - LOCATION(RC_HF_SOUTH_GRASS_12, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_1, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_2, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_3, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_4, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_5, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_6, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_7, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_8, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_9, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_10, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_11, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_MARKET_GRASS_12, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_1, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_2, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_3, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_4, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_5, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_6, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_7, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_8, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_9, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_10, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_11, logic->CanCutShrubs()), - LOCATION(RC_HF_NEAR_KF_GRASS_12, logic->CanCutShrubs()), + LOCATION(RC_HF_CENTRAL_GRASS_1, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_2, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_3, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_4, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_5, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_6, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_7, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_8, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_9, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_10, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_11, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_CENTRAL_GRASS_12, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_1, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_2, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_3, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_4, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_5, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_6, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_7, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_8, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_9, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_10, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_11, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_SOUTH_GRASS_12, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_1, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_2, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_3, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_4, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_5, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_6, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_7, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_8, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_9, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_10, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_11, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_MARKET_GRASS_12, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_1, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_2, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_3, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_4, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_5, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_6, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_7, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_8, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_9, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_10, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_11, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_HF_NEAR_KF_GRASS_12, logic->CanCutShrubs() || logic->SummonEpona()), LOCATION(RC_HF_SILVER_BOULDER, logic->CanUse(RG_SILVER_GAUNTLETS)), LOCATION(RC_HF_ROCK_1, logic->CanBreakRocks()), LOCATION(RC_HF_ROCK_2, logic->CanBreakRocks()), 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 db854e7340..05f5603f7a 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp @@ -39,48 +39,48 @@ void RegionTable_Init_LakeHylia() { LOCATION(RC_LH_LAB_GOSSIP_STONE, true), LOCATION(RC_LH_SOUTHEAST_GOSSIP_STONE, true), LOCATION(RC_LH_SOUTHWEST_GOSSIP_STONE, true), - LOCATION(RC_LH_GRASS_1, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_2, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_3, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_4, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_5, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_6, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_7, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_8, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_9, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_10, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_11, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_12, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_13, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_14, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_15, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_16, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_17, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_18, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_19, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_20, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_21, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_22, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_23, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_24, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_25, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_26, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_27, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_28, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_29, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_30, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_31, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_32, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_33, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_34, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_35, logic->CanCutShrubs()), - LOCATION(RC_LH_GRASS_36, logic->CanCutShrubs()), - LOCATION(RC_LH_CHILD_GRASS_1, logic->IsChild && logic->CanCutShrubs()), - LOCATION(RC_LH_CHILD_GRASS_2, logic->IsChild && logic->CanCutShrubs()), - LOCATION(RC_LH_CHILD_GRASS_3, logic->IsChild && logic->CanCutShrubs()), - LOCATION(RC_LH_CHILD_GRASS_4, logic->IsChild && logic->CanCutShrubs()), - LOCATION(RC_LH_WARP_PAD_GRASS_1, logic->CanCutShrubs()), - LOCATION(RC_LH_WARP_PAD_GRASS_2, logic->CanCutShrubs()), + LOCATION(RC_LH_GRASS_1, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_2, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_3, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_4, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_5, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_6, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_7, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_8, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_9, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_10, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_11, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_12, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_13, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_14, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_15, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_16, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_17, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_18, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_19, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_20, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_21, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_22, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_23, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_24, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_25, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_26, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_27, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_28, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_29, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_30, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_31, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_32, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_33, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_34, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_35, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_GRASS_36, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_CHILD_GRASS_1, logic->IsChild && (logic->CanCutShrubs() || logic->SummonEpona())), + LOCATION(RC_LH_CHILD_GRASS_2, logic->IsChild && (logic->CanCutShrubs() || logic->SummonEpona())), + LOCATION(RC_LH_CHILD_GRASS_3, logic->IsChild && (logic->CanCutShrubs() || logic->SummonEpona())), + LOCATION(RC_LH_CHILD_GRASS_4, logic->IsChild && (logic->CanCutShrubs() || logic->SummonEpona())), + LOCATION(RC_LH_WARP_PAD_GRASS_1, logic->CanCutShrubs() || logic->SummonEpona()), + LOCATION(RC_LH_WARP_PAD_GRASS_2, logic->CanCutShrubs() || logic->SummonEpona()), LOCATION(RC_LH_SCARECROW_BUTTERFLY_FAIRY, logic->IsChild && logic->CanUse(RG_STICKS)), LOCATION(RC_LH_LAB_RECTANGLE_SIGN, logic->CanRead()), LOCATION(RC_LH_NORTH_EXIT_ARROW_SIGN, logic->CanRead()), From b55a687fa0372c4033b7cca74779499e539d16f4 Mon Sep 17 00:00:00 2001 From: Pepper0ni <93387759+Pepper0ni@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:28:58 +0100 Subject: [PATCH 27/29] Fix readme links (#7418) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 70a4fafb0d..ba3b6dd857 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ If you want to playtest a continuous integration build, you can find them at the * [Windows](https://nightly.link/HarbourMasters/Shipwright/workflows/generate-builds/develop/soh-windows.zip) * [macOS](https://nightly.link/HarbourMasters/Shipwright/workflows/generate-builds/develop/soh-mac.zip) -* [Linux](https://nightly.link/HarbourMasters/Shipwright/workflows/generate-builds/develop/soh-linux.zip) +* [Linux X86_64](https://nightly.link/HarbourMasters/Shipwright/workflows/generate-builds/develop/soh-linux-x86_64.zip) +* [Linux ARM](https://nightly.link/HarbourMasters/Shipwright/workflows/generate-builds/develop/soh-linux-aarch64.zip) ### Further Reading More detailed documentation can be found in the 'docs' directory, including the aforementioned [building instructions](docs/BUILDING.md). From 80f2ad38f8b97ac278863b1a163024b67d36a6f5 Mon Sep 17 00:00:00 2001 From: Pepper0ni <93387759+Pepper0ni@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:30:01 +0100 Subject: [PATCH 28/29] fix the jump speed limiter while wearing bunny hood (#7419) --- soh/soh/Enhancements/SpeedModifiers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/soh/soh/Enhancements/SpeedModifiers.cpp b/soh/soh/Enhancements/SpeedModifiers.cpp index 17f0689ba9..4d6ab8feb4 100644 --- a/soh/soh/Enhancements/SpeedModifiers.cpp +++ b/soh/soh/Enhancements/SpeedModifiers.cpp @@ -64,6 +64,9 @@ static void RegisterSpeedModifiers() { COND_VB_SHOULD(VB_PLAYER_LIMIT_JUMP_SPEED, speedModifierActive || bunnyHoodActive, { Player* player = va_arg(args, Player*); if (ShouldAmplifyJump(player)) { + f32 maxSpeed = + (R_RUN_SPEED_LIMIT / 100.0f) * Ship_GetBunnyHoodJumpFactor(player) * GetSpeedModifierJumpFactor(); + player->linearVelocity = CLAMP(player->linearVelocity, -maxSpeed, maxSpeed); *should = false; } }); From f93e3cfbc88534b4304652f73c80fadbbe81c3aa Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 18 Oct 2026 16:19:43 -0400 Subject: [PATCH 29/29] [Bugfix] Randomizer flag can set vanilla infinite magic (#7415) Also hookify barinade soul --- .../vanilla-behavior/GIVanillaBehavior.h | 24 +++++++ .../Enhancements/randomizer/hook_handlers.cpp | 18 ++++++ soh/src/code/z_parameter.c | 29 ++------- .../overlays/actors/ovl_Boss_Va/z_boss_va.c | 64 +++++++++---------- 4 files changed, 78 insertions(+), 57 deletions(-) diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index dac11a2939..6f38baef1d 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -41,6 +41,14 @@ typedef enum { // - None VB_AMY_SOLVE, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*Color_RGB8` + VB_APPLY_MAGIC_METER_FILL_COLOR, + // #### `result` // ```c // true @@ -50,6 +58,14 @@ typedef enum { // - `s8*` sCsState VB_BARINADE_DEATH_SCENE, + // #### `result` + // ```c + // Flags_GetEventChkInf(EVENTCHKINF_BEGAN_BARINA_BATTLE) + // ``` + // #### `args` + // - `*BossVa` + VB_BARINADE_PLAY_REMATCH_INTRO, + // #### `result` // ```c // this->actor.textId == 0x401A @@ -1837,6 +1853,14 @@ typedef enum { // - `*EnMs` VB_MAGIC_BEAN_SALESMAN_TAKE_MONEY, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - None + VB_MAGIC_METER_DEPLETE_WHILE_CONSUMING, + // #### `result` // ```c // CHECK_QUEST_ITEM(QUEST_SONG_EPONA) diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index d501c8e5a3..9d6d517155 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -1022,6 +1022,24 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l case VB_CRAWL: *should = *should && Flags_GetRandomizerInf(RAND_INF_CAN_CRAWL); break; + case VB_BARINADE_PLAY_REMATCH_INTRO: + *should = + *should && (!RAND_GET_OPTION(RSK_SHUFFLE_BOSS_SOULS) || Flags_GetRandomizerInf(RAND_INF_BARINADE_SOUL)); + break; + case VB_MAGIC_METER_DEPLETE_WHILE_CONSUMING: + *should = *should && !Flags_GetRandomizerInf(RAND_INF_HAS_INFINITE_MAGIC_METER); + break; + case VB_APPLY_MAGIC_METER_FILL_COLOR: { + Color_RGB8* color = va_arg(args, Color_RGB8*); + + if (Flags_GetRandomizerInf(RAND_INF_HAS_INFINITE_MAGIC_METER)) { + *color = { 0, 0, 200 }; + if (CVarGetInteger(CVAR_COSMETIC("Consumable_MagicInfinite.Changed"), 0)) { + *color = CVarGetColor24(CVAR_COSMETIC("Consumable_MagicInfinite.Value"), *color); + } + } + break; + } case VB_CAN_BUY_SHOP_SHIELD_OR_TUNIC: { // Gate non-randomized shop shields/tunics behind finding a non-shop copy. if (RAND_GET_OPTION(RSK_SHOP_SHIELDS_AND_TUNICS_ONLY_REFILL).Is(RO_GENERIC_ON)) { diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 524ecd89f0..3681efd1c3 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -3263,7 +3263,7 @@ void Interface_UpdateMagicBar(PlayState* play) { break; case MAGIC_STATE_CONSUME: - if (!Flags_GetRandomizerInf(RAND_INF_HAS_INFINITE_MAGIC_METER)) { + if (GameInteractor_Should(VB_MAGIC_METER_DEPLETE_WHILE_CONSUMING, true)) { gSaveContext.magic -= 2; if (gSaveContext.magic <= 0) { gSaveContext.magic = 0; @@ -3454,7 +3454,6 @@ void Interface_DrawMagicBar(PlayState* play) { Color_RGB8 magicbar_yellow = { 250, 250, 0 }; // Magic bar being used Color_RGB8 magicbar_green = { R_MAGIC_FILL_COLOR(0), R_MAGIC_FILL_COLOR(1), R_MAGIC_FILL_COLOR(2) }; // Magic bar fill - Color_RGB8 magicbar_blue = { 0, 0, 200 }; // Infinite magic bar if (CVarGetInteger(CVAR_COSMETIC("Consumable.MagicActive.Changed"), 0)) { magicbar_yellow = CVarGetColor24(CVAR_COSMETIC("Consumable.MagicActive.Value"), magicbar_yellow); @@ -3462,9 +3461,7 @@ void Interface_DrawMagicBar(PlayState* play) { if (CVarGetInteger(CVAR_COSMETIC("Consumable.Magic.Changed"), 0)) { magicbar_green = CVarGetColor24(CVAR_COSMETIC("Consumable.Magic.Value"), magicbar_green); } - if (CVarGetInteger("gCosmetics.Consumable_MagicInfinite.Changed", 0)) { - magicbar_blue = CVarGetColor24("gCosmetics.Consumable_MagicInfinite.Value", magicbar_blue); - } + GameInteractor_Should(VB_APPLY_MAGIC_METER_FILL_COLOR, true, &magicbar_green); OPEN_DISPS(play->state.gfxCtx); @@ -3591,30 +3588,16 @@ void Interface_DrawMagicBar(PlayState* play) { // Fill the rest of the bar with the normal magic color gDPPipeSync(OVERLAY_DISP++); - if (Flags_GetRandomizerInf(RAND_INF_HAS_INFINITE_MAGIC_METER)) { - // Blue magic - gDPSetPrimColor(OVERLAY_DISP++, 0, 0, magicbar_blue.r, magicbar_blue.g, magicbar_blue.b, - interfaceCtx->magicAlpha); - } else { - // Green magic (default) - gDPSetPrimColor(OVERLAY_DISP++, 0, 0, magicbar_green.r, magicbar_green.g, magicbar_green.b, - interfaceCtx->magicAlpha); - } + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, magicbar_green.r, magicbar_green.g, magicbar_green.b, + interfaceCtx->magicAlpha); gSPWideTextureRectangle(OVERLAY_DISP++, rMagicFillX << 2, (magicBarY + 3) << 2, (rMagicFillX + gSaveContext.magicTarget) << 2, (magicBarY + 10) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); } else { // Fill the whole bar with the normal magic color - if (Flags_GetRandomizerInf(RAND_INF_HAS_INFINITE_MAGIC_METER)) { - // Blue magic - gDPSetPrimColor(OVERLAY_DISP++, 0, 0, magicbar_blue.r, magicbar_blue.g, magicbar_blue.b, - interfaceCtx->magicAlpha); - } else { - // Green magic (default) - gDPSetPrimColor(OVERLAY_DISP++, 0, 0, magicbar_green.r, magicbar_green.g, magicbar_green.b, - interfaceCtx->magicAlpha); - } + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, magicbar_green.r, magicbar_green.g, magicbar_green.b, + interfaceCtx->magicAlpha); gDPLoadMultiBlock_4b(OVERLAY_DISP++, gMagicMeterFillTex, 0, G_TX_RENDERTILE, G_IM_FMT_I, 16, 16, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, diff --git a/soh/src/overlays/actors/ovl_Boss_Va/z_boss_va.c b/soh/src/overlays/actors/ovl_Boss_Va/z_boss_va.c index dc434fbd9e..73337ce6bb 100644 --- a/soh/src/overlays/actors/ovl_Boss_Va/z_boss_va.c +++ b/soh/src/overlays/actors/ovl_Boss_Va/z_boss_va.c @@ -672,43 +672,39 @@ void BossVa_Init(Actor* thisx, PlayState* play2) { } else { this->actor.colChkInfo.damageTable = sDamageTable; sPhase2Timer = 0xFFFF; - if (Flags_GetEventChkInf(EVENTCHKINF_BEGAN_BARINA_BATTLE)) { - if (Randomizer_GetSettingValue(RSK_SHUFFLE_BOSS_SOULS) && - !Flags_GetRandomizerInf(RAND_INF_BARINADE_SOUL)) { - sCsState = BOSSVA_BATTLE; - } else { - sCsState = INTRO_CALL_BARI; - sDoorState = 100; - Player_SetCsActionWithHaltedActors(play, &this->actor, 1); - play->envCtx.screenFillColor[0] = 0xDC; - play->envCtx.screenFillColor[1] = 0xDC; - play->envCtx.screenFillColor[2] = 0xBE; - play->envCtx.screenFillColor[3] = 0xD2; - func_80064520(play, &play->csCtx); - sSubCamId = Play_CreateSubCamera(play); - Play_ChangeCameraStatus(play, CAM_ID_MAIN, CAM_STAT_WAIT); - Play_ChangeCameraStatus(play, sSubCamId, CAM_STAT_ACTIVE); - sSubCamEyeNext.x = sSubCamEye.x = 140.0f; - sSubCamEyeNext.y = sSubCamEye.y = 205.0f; - sSubCamEyeNext.z = sSubCamEye.z = -20.0f; - sSubCamAtNext.x = sSubCamAt.x = 10.0f; - sSubCamAtNext.y = sSubCamAt.y = 50.0f; - sSubCamAtNext.z = sSubCamAt.z = -220.0f; - Play_CameraSetAtEye(play, sSubCamId, &sSubCamAt, &sSubCamEye); - this->timer = 20; + if (GameInteractor_Should(VB_BARINADE_PLAY_REMATCH_INTRO, + Flags_GetEventChkInf(EVENTCHKINF_BEGAN_BARINA_BATTLE), this)) { + sCsState = INTRO_CALL_BARI; + sDoorState = 100; + Player_SetCsActionWithHaltedActors(play, &this->actor, 1); + play->envCtx.screenFillColor[0] = 0xDC; + play->envCtx.screenFillColor[1] = 0xDC; + play->envCtx.screenFillColor[2] = 0xBE; + play->envCtx.screenFillColor[3] = 0xD2; + func_80064520(play, &play->csCtx); + sSubCamId = Play_CreateSubCamera(play); + Play_ChangeCameraStatus(play, CAM_ID_MAIN, CAM_STAT_WAIT); + Play_ChangeCameraStatus(play, sSubCamId, CAM_STAT_ACTIVE); + sSubCamEyeNext.x = sSubCamEye.x = 140.0f; + sSubCamEyeNext.y = sSubCamEye.y = 205.0f; + sSubCamEyeNext.z = sSubCamEye.z = -20.0f; + sSubCamAtNext.x = sSubCamAt.x = 10.0f; + sSubCamAtNext.y = sSubCamAt.y = 50.0f; + sSubCamAtNext.z = sSubCamAt.z = -220.0f; + Play_CameraSetAtEye(play, sSubCamId, &sSubCamAt, &sSubCamEye); + this->timer = 20; - for (i = BOSSVA_BARI_LOWER_5; i >= BOSSVA_BARI_UPPER_1; i--) { - Actor_SpawnAsChild( - &play->actorCtx, &this->actor, play, ACTOR_BOSS_VA, - sInitPosOffsets[i].x + this->actor.world.pos.x, - sInitPosOffsets[i].y + this->actor.world.pos.y, - sInitPosOffsets[i].z + this->actor.world.pos.z, sInitRot[i].x + this->actor.world.rot.x, - sInitRot[i].y + this->actor.world.rot.y, sInitRot[i].z + this->actor.world.rot.z, i); - } - - sSubCamAtMaxVelFrac = sSubCamEyeMaxVelFrac = sZeroVec; + for (i = BOSSVA_BARI_LOWER_5; i >= BOSSVA_BARI_UPPER_1; i--) { + Actor_SpawnAsChild( + &play->actorCtx, &this->actor, play, ACTOR_BOSS_VA, + sInitPosOffsets[i].x + this->actor.world.pos.x, + sInitPosOffsets[i].y + this->actor.world.pos.y, + sInitPosOffsets[i].z + this->actor.world.pos.z, sInitRot[i].x + this->actor.world.rot.x, + sInitRot[i].y + this->actor.world.rot.y, sInitRot[i].z + this->actor.world.rot.z, i); } + sSubCamAtMaxVelFrac = sSubCamEyeMaxVelFrac = sZeroVec; + } else { sCsState = INTRO_START; sDoorState = 5;