Add restoration for hookshot as child to softlock (#7219)

Requested for speedrun mode

Also hookify more enemy rando stuff while I'm in the neighbourhood
This commit is contained in:
Philip Dubé
2026-09-19 14:00:06 +00:00
committed by GitHub
parent 2bc50f6056
commit 27a66925bf
9 changed files with 106 additions and 19 deletions
+2 -1
View File
@@ -54,7 +54,8 @@ void RegisterAlwaysOnFixes() {
// spawn (Player_InitItemAction removes the ranged weapon state elsewhere).
COND_VB_SHOULD(VB_INIT_HOOKSHOT_IA, true, {
Player* player = va_arg(args, Player*);
if (player->heldActor == NULL) {
if (player->heldActor == NULL && !(CVarGetInteger(CVAR_ENHANCEMENT("ChildHookshotSoftlock"), 0) &&
Object_GetIndex(&gPlayState->objectCtx, OBJECT_LINK_BOY) < 0)) {
Player_UseItem(gPlayState, player, 0xFF);
}
});
@@ -29,6 +29,7 @@ extern "C" {
#include "src/overlays/actors/ovl_En_Tp/z_en_tp.h"
extern PlayState* gPlayState;
extern int gMapLoading;
}
namespace SohGui {
@@ -745,6 +746,26 @@ void RegisterEnemyRandomizer() {
// If Random Gerudo Fighters are defeated, drop some items
COND_ID_HOOK(OnEnemyDefeat, ACTOR_EN_GELDB, ENEMY_RANDOMIZER_ENABLED, OnGerudoFighterDefeat);
// Spawn random enemies even if their object isn't loaded
COND_VB_SHOULD(VB_SPAWN_ACTOR_WITHOUT_OBJECT, ENEMY_RANDOMIZER_ENABLED, {
if (gMapLoading) {
*should = true;
}
});
// The following enemies break when the parent actor isn't the same as what would happen in authentic gameplay.
// As such, don't assign a parent to them at all when spawned with Enemy Randomizer.
// Gohma (z_boss_goma.c) and the falling platform spawning Stalfos in
// Forest Temple (z_bg_mori_bigst.c) that normally rely on this behaviour are changed when
// Enemy Rando is on so they still work properly even without assigning a parent.
COND_VB_SHOULD(VB_SET_CHILD_ACTOR_PARENT, ENEMY_RANDOMIZER_ENABLED, {
Actor* spawnedActor = va_arg(args, Actor*);
if (spawnedActor->id == ACTOR_EN_FLOORMAS || spawnedActor->id == ACTOR_EN_PEEHAT) {
*should = false;
}
});
COND_VB_SHOULD(VB_SPAWN_ACTOR_ENTRY, ENEMY_RANDOMIZER_ENABLED, {
ActorContext* actorCtx = va_arg(args, ActorContext*);
ActorEntry* actorEntry = va_arg(args, ActorEntry*);
@@ -1209,5 +1230,19 @@ void RegisterEnemyRandomizerWidgets() {
}
}
// Remove object dependency for Enemy Randomizer and Crowd Control to allow Like-likes to
// drop equipment correctly in rooms where Like-likes normally don't spawn.
static void RegisterItem00WithoutObject() {
bool required = ENEMY_RANDOMIZER_ENABLED || CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0);
COND_VB_SHOULD(VB_ITEM00_REQUIRE_OBJECT, required, {
EnItem00* item = va_arg(args, EnItem00*);
*should = false;
item->actor.objBankIndex = 0;
});
}
static RegisterShipInitFunc initFunc(RegisterEnemyRandomizer, { CVAR_ENEMY_RANDOMIZER_NAME });
static RegisterShipInitFunc initFuncItem00(RegisterItem00WithoutObject,
{ CVAR_ENEMY_RANDOMIZER_NAME, CVAR_REMOTE_CROWD_CONTROL("Enabled") });
static RegisterMenuInitFunc menuInitFunc(RegisterEnemyRandomizerWidgets);
@@ -0,0 +1,23 @@
#include <libultraship/bridge/consolevariablebridge.h>
#include "soh/cvar_prefixes.h"
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/ShipInit.hpp"
extern "C" {
#include "z64actor_enum.h"
}
static constexpr const char* CVAR_CHILD_HOOKSHOT_SOFTLOCK = CVAR_ENHANCEMENT("ChildHookshotSoftlock");
// As child object_link_boy isn't loaded, so vanilla fails to spawn the hookshot and Link is stuck holding it
static void RegisterChildHookshotSoftlock() {
COND_VB_SHOULD(VB_SPAWN_ACTOR_WITHOUT_OBJECT, CVarGetInteger(CVAR_CHILD_HOOKSHOT_SOFTLOCK, 0), {
s16 actorId = va_arg(args, int);
if (actorId == ACTOR_ARMS_HOOK) {
*should = false;
}
});
}
static RegisterShipInitFunc initFunc(RegisterChildHookshotSoftlock, { CVAR_CHILD_HOOKSHOT_SOFTLOCK });
@@ -1643,6 +1643,14 @@ typedef enum {
// - `*EnItem00`
VB_ITEM00_DESPAWN,
// #### `result`
// ```c
// true
// ```
// #### `args`
// - `*EnItem00`
VB_ITEM00_REQUIRE_OBJECT,
// #### `result`
// ```c
// this->unk_15A > 0
@@ -2741,6 +2749,14 @@ typedef enum {
// - `int32_t` (button - promoted from `s16`)
VB_SET_BUTTON_ITEM_FROM_C_BUTTON_SLOT,
// #### `result`
// ```c
// true
// ```
// #### `args`
// - `*Actor` (spawnedActor)
VB_SET_CHILD_ACTOR_PARENT,
// #### `result`
// ```c
// true
@@ -2968,6 +2984,14 @@ typedef enum {
// - None
VB_SKIP_SCARECROWS_SONG,
// #### `result`
// ```c
// !gMapLoading
// ```
// #### `args`
// - `s16` (actorId)
VB_SPAWN_ACTOR_WITHOUT_OBJECT,
// #### `result`
// ```c
// true
+13
View File
@@ -5,15 +5,18 @@
#include "soh_assets.h"
#include <libultraship/bridge/consolevariablebridge.h>
#include "spdlog/common.h"
#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
extern "C" {
#include "z64.h"
#include "macros.h"
#include "functions.h"
extern float OTRGetAspectRatio();
extern f32 sFontWidths[144];
extern const char* fontTbl[140];
extern int gMapLoading;
}
extern std::string Ship_FormatTimeDisplay(uint32_t value) {
@@ -59,6 +62,16 @@ extern "C" void Ship_ExtendedCullingActorAdjustProjectedX(Actor* actor) {
// Actor_GetProjectedPos(play, &actor->world.pos, &actor->projectedPos, &invW);
//}
// Vanilla fails to spawn an actor whose object isn't loaded
// SoH instead gives it gameplay_keep's slot, except while the room's actors are loading
extern "C" s32 Ship_GetActorSpawnObjectIndex(PlayState* play, s16 objectId, s16 actorId) {
s32 objBankIndex = Object_GetIndex(&play->objectCtx, objectId);
if (objBankIndex < 0 && GameInteractor_Should(VB_SPAWN_ACTOR_WITHOUT_OBJECT, !gMapLoading, actorId)) {
return 0;
}
return objBankIndex;
}
extern "C" bool Ship_IsCStringEmpty(const char* str) {
return str == NULL || str[0] == '\0';
}
+2
View File
@@ -23,6 +23,8 @@ void Ship_ExtendedCullingActorAdjustProjectedZ(Actor* actor);
void Ship_ExtendedCullingActorAdjustProjectedX(Actor* actor);
void Ship_ExtendedCullingActorRestoreProjectedPos(PlayState* play, Actor* actor);
s32 Ship_GetActorSpawnObjectIndex(PlayState* play, s16 objectId, s16 actorId);
bool Ship_IsCStringEmpty(const char* str);
void Ship_CreateQuadVertexGroup(Vtx* vtxList, s32 xStart, s32 yStart, s32 width, s32 height, u8 flippedH);
f32 Ship_GetCharFontWidth(u8 character);
+3
View File
@@ -1381,6 +1381,9 @@ void SohMenu::AddMenuEnhancements() {
.Options(CheckboxOptions().Tooltip(
"Restores a bug from NTSC 1.0/1.1 that allows you to obtain the eyeball frog from King Zora "
"instead of the Zora Tunic by Holding Shield."));
AddWidget(path, "Child Hookshot Softlock", WIDGET_CVAR_CHECKBOX)
.CVar(CVAR_ENHANCEMENT("ChildHookshotSoftlock"))
.Options(CheckboxOptions().Tooltip("Using the Hookshot as child softlocks."));
AddWidget(path, "Get Item Manipulation", WIDGET_CVAR_COMBOBOX)
.CVar(CVAR_ENHANCEMENT("GetItemManipulation"))
.Options(ComboboxOptions()
+3 -12
View File
@@ -17,6 +17,7 @@
#include "soh/ActorDB.h"
#include "soh/OTRGlobals.h"
#include "soh/ShipUtils.h"
#include <string.h>
#include <stdlib.h>
@@ -3346,11 +3347,7 @@ Actor* Actor_Spawn(ActorContext* actorCtx, PlayState* play, s16 actorId, f32 pos
return NULL;
}
objBankIndex = Object_GetIndex(&gPlayState->objectCtx, dbEntry->objectId);
if (objBankIndex < 0 && (!gMapLoading || CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0))) {
objBankIndex = 0;
}
objBankIndex = Ship_GetActorSpawnObjectIndex(play, dbEntry->objectId, actorId);
if ((objBankIndex < 0) ||
((dbEntry->category == ACTORCAT_ENEMY) && Flags_GetClear(play, play->roomCtx.curRoom.num))) {
@@ -3428,13 +3425,7 @@ Actor* Actor_SpawnAsChild(ActorContext* actorCtx, Actor* parent, PlayState* play
return NULL;
}
// The following enemies break when the parent actor isn't the same as what would happen in authentic gameplay.
// As such, don't assign a parent to them at all when spawned with Enemy Randomizer.
// Gohma (z_boss_goma.c) and the falling platform spawning Stalfos in
// Forest Temple (z_bg_mori_bigst.c) that normally rely on this behaviour are changed when
// Enemy Rando is on so they still work properly even without assigning a parent.
if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) &&
(spawnedActor->id == ACTOR_EN_FLOORMAS || spawnedActor->id == ACTOR_EN_PEEHAT)) {
if (!GameInteractor_Should(VB_SET_CHILD_ACTOR_PARENT, true, spawnedActor)) {
return spawnedActor;
}
+1 -6
View File
@@ -334,12 +334,7 @@ void EnItem00_SetupAction(EnItem00* this, EnItem00ActionFunc actionFunc) {
}
void EnItem00_SetObjectDependency(EnItem00* this, PlayState* play, s16 objectIndex) {
// Remove object dependency for Enemy Randomizer and Crowd Control to allow Like-likes to
// drop equipment correctly in rooms where Like-likes normally don't spawn.
if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) ||
(CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0))) {
this->actor.objBankIndex = 0;
} else {
if (GameInteractor_Should(VB_ITEM00_REQUIRE_OBJECT, true, this)) {
this->actor.objBankIndex = Object_GetIndex(&play->objectCtx, objectIndex);
Actor_SetObjectDependency(play, &this->actor);
}