From 360f4882d21dc84ded570baf017aa075e5eea87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:21:16 +0000 Subject: [PATCH] rando: option to allow swordless epona items (#6727) --- soh/include/functions.h | 1 - .../vanilla-behavior/GIVanillaBehavior.h | 43 +++++++++++++ .../Enhancements/randomizer/hook_handlers.cpp | 37 +++++++++++ .../randomizer/option_descriptions.cpp | 6 ++ .../randomizerEnums/RandomizerSettingKey.h | 1 + .../randomizer/randomizer_entrance.c | 2 +- soh/soh/Enhancements/randomizer/settings.cpp | 2 + soh/src/code/z_game_over.c | 2 +- soh/src/code/z_parameter.c | 64 ++++++------------- soh/src/code/z_play.c | 2 +- .../ovl_kaleido_scope/z_kaleido_scope_PAL.c | 2 +- 11 files changed, 112 insertions(+), 50 deletions(-) diff --git a/soh/include/functions.h b/soh/include/functions.h index 54f2c2b5dd..066b1d1d9e 100644 --- a/soh/include/functions.h +++ b/soh/include/functions.h @@ -2459,7 +2459,6 @@ void Font_LoadOrderedFontNTSC(Font* font); // #endregion // #region SOH [General] -void Interface_RandoRestoreSwordless(void); s32 Ship_CalcShouldDrawAndUpdate(PlayState* play, Actor* actor, Vec3f* projectedPos, f32 projectedW, bool* shouldDraw, bool* shouldUpdate); diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index d7a66640d8..b0f44446dd 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -3016,6 +3016,49 @@ typedef enum { // - `*EnPeehat` // - `*PlayState` VB_PEEHAT_SPAWN_LARVAS, + + // #### `result` + // ```c + // gSaveContext.equips.buttonItems[0] != ITEM_NONE + // ``` + // Whether the B button slot should be treated as holding an item when entering the + // horseback/minigame "temporary B" force path. Rando returns `true` for a swordless + // player so the swordless-on-Epona item glitch can be blocked. + // #### `args` + // - `*PlayState` + VB_TEMP_B_TREAT_AS_OCCUPIED, + + // #### `result` + // ```c + // true + // ``` + // Side-effect hook (return value ignored): fired right after the vanilla + // `buttonStatus[0] = buttonItems[0]` stash so rando can relocate it to its swordless + // sentinel for later restoration. + // #### `args` + // - `*PlayState` + VB_TEMP_B_STASH_SWORDLESS, + + // #### `result` + // ```c + // (gSaveContext.equips.buttonItems[0] != ITEM_NONE) || (gSaveContext.infTable[29] == 0) + // ``` + // Whether the "temporary B" item should be restored to the B button. Rando also returns + // `true` when it had stashed a swordless sentinel. + // #### `args` + // - None + VB_TEMP_B_SHOULD_RESTORE, + + // #### `result` + // ```c + // true + // ``` + // Side-effect hook (return value ignored): fired right after the vanilla + // `buttonItems[0] = buttonStatus[0]` restore so rando can convert its swordless sentinel + // back into an empty (swordless) B button. + // #### `args` + // - None + VB_TEMP_B_RESTORE_SWORDLESS, } GIVanillaBehavior; #endif diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index 2cae55af02..1d01488a77 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -962,6 +962,22 @@ static ScrubIdentity IdentifyScrub(s32 sceneNum, s32 actorParams, s32 respawnDat return scrubIdentity; } +// buttonStatus[0] doubles as "B disabled" (BTN_DISABLED == 255 == ITEM_NONE) and as temp-B +// storage during minigames/Epona. We use ITEM_NONE_FE (254) as a sentinel so a swordless rando +// player can be funneled through that same temp-B machinery and restored to an empty B later. +#define SWORDLESS_STATUS ITEM_NONE_FE + +// true when a swordless player should be funneled through temporary-B force path +// (so their empty B is treated as "occupied", blocking swordless-on-Epona item glitch). +static bool RandoCanTrackSwordless(PlayState* play) { + Player* player = GET_PLAYER(play); + // Child is always assumed swordless until the Kokiri Sword is found; adult only with MS shuffle. + bool isSwordless = (LINK_IS_CHILD || RAND_GET_OPTION(RSK_SHUFFLE_MASTER_SWORD)) && + gSaveContext.equips.buttonItems[0] == ITEM_NONE && Flags_GetInfTable(INFTABLE_SWORDLESS); + bool wasSwordlessBefore = gSaveContext.buttonStatus[0] == SWORDLESS_STATUS; + return isSwordless && !wasSwordlessBefore && !RAND_GET_OPTION(RSK_SWORDLESS_EPONA_ITEMS); +} + void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_list originalArgs) { va_list args; va_copy(args, originalArgs); @@ -1469,6 +1485,27 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l } break; } + case VB_TEMP_B_TREAT_AS_OCCUPIED: + // Treat a swordless player's empty B as occupied so they enter the temp-B force path. + *should = *should || RandoCanTrackSwordless(va_arg(args, PlayState*)); + break; + case VB_TEMP_B_STASH_SWORDLESS: + // Relocate the just-stashed temp-B to the swordless sentinel for later restoration. + if (RandoCanTrackSwordless(va_arg(args, PlayState*))) { + gSaveContext.buttonStatus[0] = SWORDLESS_STATUS; + } + break; + case VB_TEMP_B_SHOULD_RESTORE: + // Also restore the B button when a swordless sentinel was stashed. + *should = *should || gSaveContext.buttonStatus[0] == SWORDLESS_STATUS; + break; + case VB_TEMP_B_RESTORE_SWORDLESS: + // Convert the swordless sentinel back into an empty (swordless) B button. + if (gSaveContext.buttonStatus[0] == SWORDLESS_STATUS) { + gSaveContext.equips.buttonItems[0] = ITEM_NONE; + gSaveContext.buttonStatus[0] = BTN_ENABLED; + } + break; case VB_TRADE_POCKET_CUCCO: { EnNiwLady* enNiwLady = va_arg(args, EnNiwLady*); Flags_UnsetRandomizerInf(RAND_INF_ADULT_TRADES_HAS_POCKET_CUCCO); diff --git a/soh/soh/Enhancements/randomizer/option_descriptions.cpp b/soh/soh/Enhancements/randomizer/option_descriptions.cpp index cc9a780086..a6b4b5a12c 100644 --- a/soh/soh/Enhancements/randomizer/option_descriptions.cpp +++ b/soh/soh/Enhancements/randomizer/option_descriptions.cpp @@ -243,6 +243,12 @@ void Settings::CreateOptionDescriptions() { "\n" "Adult Link will start with a second free item instead of the Master Sword.\n" "If you haven't found the Master Sword before facing Ganon, you won't receive it during the fight."; + mOptionDescriptions[RSK_SWORDLESS_EPONA_ITEMS] = + "Restores the vanilla glitch that lets a swordless player use C-button items (bottles, bombs, " + "magic, etc.) while riding Epona.\n" + "\n" + "When disabled, the B button is forced to the bow and the C buttons are disabled while swordless " + "on Epona, blocking the glitch."; mOptionDescriptions[RSK_SHUFFLE_CHILD_WALLET] = "Enabling this shuffles the Child's Wallet into the item pool.\n" "\n" "You will not be able to carry any rupees until you find a wallet."; diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h index 915fb6e230..764fa916ed 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h @@ -59,6 +59,7 @@ RANDO_ENUM_ITEM(RSK_STARTING_NOCTURNE_OF_SHADOW) RANDO_ENUM_ITEM(RSK_STARTING_PRELUDE_OF_LIGHT) RANDO_ENUM_ITEM(RSK_SHUFFLE_KOKIRI_SWORD) RANDO_ENUM_ITEM(RSK_SHUFFLE_MASTER_SWORD) +RANDO_ENUM_ITEM(RSK_SWORDLESS_EPONA_ITEMS) RANDO_ENUM_ITEM(RSK_SHUFFLE_CHILD_WALLET) RANDO_ENUM_ITEM(RSK_INCLUDE_TYCOON_WALLET) RANDO_ENUM_ITEM(RSK_SHUFFLE_DUNGEON_REWARDS) diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.c b/soh/soh/Enhancements/randomizer/randomizer_entrance.c index 6daf6f4768..a460a4fb4c 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance.c +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.c @@ -579,7 +579,7 @@ void Entrance_HandleEponaState(void) { player->actor.parent = NULL; AREG(6) = 0; gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; //"temp B" - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); } } diff --git a/soh/soh/Enhancements/randomizer/settings.cpp b/soh/soh/Enhancements/randomizer/settings.cpp index 552c602c8b..af347bef07 100644 --- a/soh/soh/Enhancements/randomizer/settings.cpp +++ b/soh/soh/Enhancements/randomizer/settings.cpp @@ -830,6 +830,7 @@ void Settings::CreateOptions() { }); OPT_BOOL(RSK_SHUFFLE_KOKIRI_SWORD, "Shuffle Kokiri Sword", CVAR_RANDOMIZER_SETTING("ShuffleKokiriSword"), mOptionDescriptions[RSK_SHUFFLE_KOKIRI_SWORD]); OPT_BOOL(RSK_SHUFFLE_MASTER_SWORD, "Shuffle Master Sword", CVAR_RANDOMIZER_SETTING("ShuffleMasterSword"), mOptionDescriptions[RSK_SHUFFLE_MASTER_SWORD]); + OPT_BOOL(RSK_SWORDLESS_EPONA_ITEMS, "Swordless Epona Items", CVAR_RANDOMIZER_SETTING("SwordlessEponaItems"), mOptionDescriptions[RSK_SWORDLESS_EPONA_ITEMS]); OPT_BOOL(RSK_SHUFFLE_CHILD_WALLET, "Shuffle Child's Wallet", CVAR_RANDOMIZER_SETTING("ShuffleChildWallet"), mOptionDescriptions[RSK_SHUFFLE_CHILD_WALLET], IMFLAG_NONE); OPT_BOOL(RSK_INCLUDE_TYCOON_WALLET, "Include Tycoon Wallet", CVAR_RANDOMIZER_SETTING("IncludeTycoonWallet"), mOptionDescriptions[RSK_INCLUDE_TYCOON_WALLET]); OPT_BOOL(RSK_SHUFFLE_OCARINA, "Shuffle Ocarinas", CVAR_RANDOMIZER_SETTING("ShuffleOcarinas"), mOptionDescriptions[RSK_SHUFFLE_OCARINA]); @@ -1750,6 +1751,7 @@ void Settings::CreateOptions() { &mOptions[RSK_SUNLIGHT_ARROWS], &mOptions[RSK_FULL_WALLETS], &mOptions[RSK_SLINGBOW_BREAK_BEEHIVES], + &mOptions[RSK_SWORDLESS_EPONA_ITEMS], &mOptions[RSK_SKIP_CHILD_ZELDA], &mOptions[RSK_MASK_QUEST], &mOptions[RSK_SKIP_CHILD_STEALTH], diff --git a/soh/src/code/z_game_over.c b/soh/src/code/z_game_over.c index 5b16bbc38f..e57e5a3a24 100644 --- a/soh/src/code/z_game_over.c +++ b/soh/src/code/z_game_over.c @@ -60,7 +60,7 @@ void GameOver_Update(PlayState* play) { if (gSaveContext.buttonStatus[0] != BTN_ENABLED) { gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); } else { gSaveContext.equips.buttonItems[0] = ITEM_NONE; } diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 2064527e6e..e96929a109 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -796,20 +796,6 @@ void func_80082850(PlayState* play, s16 maxAlpha) { } } -// buttonStatus[0] is used to represent if the B button is disabled, but also tracks -// the last active B button item during mini-games/epona (temp B) -// Since ITEM_NONE is the same as BTN_DISABLED (255), we need a different value to help us track -// that the player was swordless before like ITEM_NONE_FE (254) -#define SWORDLESS_STATUS ITEM_NONE_FE - -// Restores swordless state when using the custom value for temp B and then clears temp B -void Interface_RandoRestoreSwordless(void) { - if (IS_RANDO && gSaveContext.buttonStatus[0] == SWORDLESS_STATUS) { - gSaveContext.equips.buttonItems[0] = ITEM_NONE; - gSaveContext.buttonStatus[0] = BTN_ENABLED; - } -} - void func_80083108(PlayState* play) { MessageContext* msgCtx = &play->msgCtx; Player* player = GET_PLAYER(play); @@ -817,20 +803,14 @@ void func_80083108(PlayState* play) { s16 i; s16 sp28 = 0; - // Check for the player being swordless in rando (no item on B and swordless flag set) - // Child is always assumed due to not finding kokiri sword yet. Adult is only checked with MS shuffle on. - u8 randoIsSwordless = IS_RANDO && (LINK_IS_CHILD || Randomizer_GetSettingValue(RSK_SHUFFLE_MASTER_SWORD)) && - gSaveContext.equips.buttonItems[0] == ITEM_NONE && Flags_GetInfTable(INFTABLE_SWORDLESS); - u8 randoWasSwordlessBefore = IS_RANDO && gSaveContext.buttonStatus[0] == SWORDLESS_STATUS; - u8 randoCanTrackSwordless = randoIsSwordless && !randoWasSwordlessBefore; - if ((gSaveContext.cutsceneIndex < 0xFFF0) || ((play->sceneNum == SCENE_LON_LON_RANCH) && (gSaveContext.cutsceneIndex == 0xFFF0))) { gSaveContext.forceRisingButtonAlphas = 0; if ((player->stateFlags1 & PLAYER_STATE1_ON_HORSE) || (play->shootingGalleryStatus > 1) || ((play->sceneNum == SCENE_BOMBCHU_BOWLING_ALLEY) && Flags_GetSwitch(play, 0x38))) { - if (gSaveContext.equips.buttonItems[0] != ITEM_NONE || randoCanTrackSwordless) { + if (GameInteractor_Should(VB_TEMP_B_TREAT_AS_OCCUPIED, gSaveContext.equips.buttonItems[0] != ITEM_NONE, + play)) { gSaveContext.forceRisingButtonAlphas = 1; if (gSaveContext.buttonStatus[0] == BTN_DISABLED) { @@ -843,13 +823,10 @@ void func_80083108(PlayState* play) { if ((gSaveContext.equips.buttonItems[0] != ITEM_SLINGSHOT) && (gSaveContext.equips.buttonItems[0] != ITEM_BOW) && (gSaveContext.equips.buttonItems[0] != ITEM_BOMBCHU) && - (gSaveContext.equips.buttonItems[0] != ITEM_NONE || randoCanTrackSwordless)) { + GameInteractor_Should(VB_TEMP_B_TREAT_AS_OCCUPIED, gSaveContext.equips.buttonItems[0] != ITEM_NONE, + play)) { gSaveContext.buttonStatus[0] = gSaveContext.equips.buttonItems[0]; - - // Track swordless status for restoration later - if (randoCanTrackSwordless) { - gSaveContext.buttonStatus[0] = SWORDLESS_STATUS; - } + GameInteractor_Should(VB_TEMP_B_STASH_SWORDLESS, true, play); if ((play->sceneNum == SCENE_BOMBCHU_BOWLING_ALLEY) && Flags_GetSwitch(play, 0x38)) { gSaveContext.equips.buttonItems[0] = ITEM_BOMBCHU; @@ -902,12 +879,7 @@ void func_80083108(PlayState* play) { if (play->interfaceCtx.unk_260 != 0) { if (gSaveContext.equips.buttonItems[0] != ITEM_FISHING_POLE) { gSaveContext.buttonStatus[0] = gSaveContext.equips.buttonItems[0]; - - // Track swordless status for restoration later - if (randoCanTrackSwordless) { - gSaveContext.buttonStatus[0] = SWORDLESS_STATUS; - } - + GameInteractor_Should(VB_TEMP_B_STASH_SWORDLESS, true, play); gSaveContext.equips.buttonItems[0] = ITEM_FISHING_POLE; gSaveContext.unk_13EA = 0; Interface_LoadItemIcon1(play, 0); @@ -921,7 +893,7 @@ void func_80083108(PlayState* play) { gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; gSaveContext.unk_13EA = 0; - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); if (gSaveContext.equips.buttonItems[0] != ITEM_NONE) { Interface_LoadItemIcon1(play, 0); @@ -1030,7 +1002,7 @@ void func_80083108(PlayState* play) { (gSaveContext.equips.buttonItems[0] != ITEM_SWORD_KNIFE)) { gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); } else { gSaveContext.buttonStatus[0] = gSaveContext.equips.buttonItems[0]; } @@ -1070,11 +1042,12 @@ void func_80083108(PlayState* play) { (gSaveContext.equips.buttonItems[0] == ITEM_BOW) || (gSaveContext.equips.buttonItems[0] == ITEM_BOMBCHU) || (gSaveContext.equips.buttonItems[0] == ITEM_NONE)) { - if ((gSaveContext.equips.buttonItems[0] != ITEM_NONE) || (gSaveContext.infTable[29] == 0) || - randoWasSwordlessBefore) { + if (GameInteractor_Should(VB_TEMP_B_SHOULD_RESTORE, + (gSaveContext.equips.buttonItems[0] != ITEM_NONE) || + (gSaveContext.infTable[29] == 0))) { gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); sp28 = 1; @@ -1097,11 +1070,12 @@ void func_80083108(PlayState* play) { (gSaveContext.equips.buttonItems[0] == ITEM_BOW) || (gSaveContext.equips.buttonItems[0] == ITEM_BOMBCHU) || (gSaveContext.equips.buttonItems[0] == ITEM_NONE)) { - if ((gSaveContext.equips.buttonItems[0] != ITEM_NONE) || (gSaveContext.infTable[29] == 0) || - randoWasSwordlessBefore) { + if (GameInteractor_Should(VB_TEMP_B_SHOULD_RESTORE, + (gSaveContext.equips.buttonItems[0] != ITEM_NONE) || + (gSaveContext.infTable[29] == 0))) { gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); sp28 = 1; @@ -1762,13 +1736,13 @@ void func_80084BF4(PlayState* play, u16 flag) { (gSaveContext.equips.buttonItems[0] == ITEM_BOMBCHU) || (gSaveContext.equips.buttonItems[0] == ITEM_FISHING_POLE)) { gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); Interface_LoadItemIcon1(play, 0); } } else if (gSaveContext.equips.buttonItems[0] == ITEM_NONE) { if ((gSaveContext.equips.buttonItems[0] != ITEM_NONE) || (gSaveContext.infTable[29] == 0)) { gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); Interface_LoadItemIcon1(play, 0); } } @@ -5924,7 +5898,7 @@ void Interface_Draw(PlayState* play) { (gSaveContext.equips.buttonItems[0] != ITEM_SWORD_KNIFE)) { if (gSaveContext.buttonStatus[0] != BTN_ENABLED) { gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); } else { gSaveContext.equips.buttonItems[0] = ITEM_NONE; } diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index 00d1066a25..7da3f61cb9 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -2225,7 +2225,7 @@ void Play_PerformSave(PlayState* play) { (gSaveContext.equips.buttonItems[0] == ITEM_NONE && !Flags_GetInfTable(INFTABLE_SWORDLESS))) { gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); } Save_SaveFile(); diff --git a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c index b47f06fb20..d4d66c5253 100644 --- a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c +++ b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c @@ -4866,7 +4866,7 @@ void KaleidoScope_Update(PlayState* play) { } // Used to clear swordless temp B after unpause so minigame/epona handling restarts - Interface_RandoRestoreSwordless(); + GameInteractor_Should(VB_TEMP_B_RESTORE_SWORDLESS, true); interfaceCtx->unk_1FA = interfaceCtx->unk_1FC = 0; osSyncPrintf(VT_FGCOL(YELLOW));