diff --git a/soh/soh/Enhancements/AlwaysOnFixes.cpp b/soh/soh/Enhancements/AlwaysOnFixes.cpp index fe1bc2ffed..bfc1e05a8f 100644 --- a/soh/soh/Enhancements/AlwaysOnFixes.cpp +++ b/soh/soh/Enhancements/AlwaysOnFixes.cpp @@ -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); } }); diff --git a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp index 20431a15ea..e11718c8c2 100644 --- a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp +++ b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp @@ -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); diff --git a/soh/soh/Enhancements/Restorations/ChildHookshotSoftlock.cpp b/soh/soh/Enhancements/Restorations/ChildHookshotSoftlock.cpp new file mode 100644 index 0000000000..863c61d6a6 --- /dev/null +++ b/soh/soh/Enhancements/Restorations/ChildHookshotSoftlock.cpp @@ -0,0 +1,23 @@ +#include + +#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 }); diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index 4780451082..146e5fd36c 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -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 diff --git a/soh/soh/ShipUtils.cpp b/soh/soh/ShipUtils.cpp index 32c14ff17f..aab4b901dd 100644 --- a/soh/soh/ShipUtils.cpp +++ b/soh/soh/ShipUtils.cpp @@ -5,15 +5,18 @@ #include "soh_assets.h" #include #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'; } diff --git a/soh/soh/ShipUtils.h b/soh/soh/ShipUtils.h index 7e5f515c72..3f55a54220 100644 --- a/soh/soh/ShipUtils.h +++ b/soh/soh/ShipUtils.h @@ -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); diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp index 27c7f9f475..3bf31116af 100644 --- a/soh/soh/SohGui/SohMenuEnhancements.cpp +++ b/soh/soh/SohGui/SohMenuEnhancements.cpp @@ -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() diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index 5f309948cb..77c5abce37 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -17,6 +17,7 @@ #include "soh/ActorDB.h" #include "soh/OTRGlobals.h" +#include "soh/ShipUtils.h" #include #include @@ -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; } diff --git a/soh/src/code/z_en_item00.c b/soh/src/code/z_en_item00.c index c9775c60c6..e06d8fdcfd 100644 --- a/soh/src/code/z_en_item00.c +++ b/soh/src/code/z_en_item00.c @@ -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); }