From 1c58bcc3e9f65c5d924188e1eb647785dcd6a369 Mon Sep 17 00:00:00 2001 From: Shishu the Dragon <183069616+ShishuTheDragon@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:41:38 +1200 Subject: [PATCH] Ivan: Hookify and improve spawning (#6707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spawn/despawn Ivan immediately (not on next scene) Move Ivan’s boomerang code out of z_player.c --- soh/soh/Enhancements/ExtraModes/IvanCoop.cpp | 48 +++++++++++++++++++ soh/soh/SohGui/SohMenuEnhancements.cpp | 4 +- soh/src/code/z_play.c | 6 --- .../actors/ovl_En_Partner/z_en_partner.c | 26 +++++++++- .../actors/ovl_player_actor/z_player.c | 21 -------- 5 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 soh/soh/Enhancements/ExtraModes/IvanCoop.cpp diff --git a/soh/soh/Enhancements/ExtraModes/IvanCoop.cpp b/soh/soh/Enhancements/ExtraModes/IvanCoop.cpp new file mode 100644 index 0000000000..2acd60a521 --- /dev/null +++ b/soh/soh/Enhancements/ExtraModes/IvanCoop.cpp @@ -0,0 +1,48 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "macros.h" +#include "functions.h" +extern PlayState* gPlayState; +extern s16 gEnPartnerId; +} + +#define CVAR_NAME CVAR_ENHANCEMENT("IvanCoopModeEnabled") +#define CVAR_VALUE CVarGetInteger(CVAR_NAME, 0) + +static void SpawnIvan() { + if (!gPlayState) + return; + + Player* player = GET_PLAYER(gPlayState); + if (!player) + return; + + if (Actor_Find(&gPlayState->actorCtx, gEnPartnerId, ACTORCAT_ITEMACTION)) + return; + + PosRot& world = player->actor.world; + Actor_Spawn(&gPlayState->actorCtx, gPlayState, gEnPartnerId, world.pos.x, + world.pos.y + Player_GetHeight(player) + 5.0f, world.pos.z, 0, world.rot.y, 0, 1); +} + +static void KillIvan() { + if (!gPlayState) + return; + + Actor* ivan = Actor_Find(&gPlayState->actorCtx, gEnPartnerId, ACTORCAT_ITEMACTION); + if (ivan) + Actor_Kill(ivan); +} + +static void RegisterIvanCoop() { + if (CVAR_VALUE) + SpawnIvan(); + else + KillIvan(); + + COND_ID_HOOK(OnActorSpawn, ACTOR_PLAYER, CVAR_VALUE, [](void*) { SpawnIvan(); }); +} + +static RegisterShipInitFunc initFunc(RegisterIvanCoop, { CVAR_NAME }); diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp index c06230a9e8..d854c621a6 100644 --- a/soh/soh/SohGui/SohMenuEnhancements.cpp +++ b/soh/soh/SohGui/SohMenuEnhancements.cpp @@ -1641,8 +1641,8 @@ void SohMenu::AddMenuEnhancements() { AddWidget(path, "Ivan the Fairy (Coop Mode)", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("IvanCoopModeEnabled")) .Options(CheckboxOptions().Tooltip( - "Enables Ivan the Fairy upon the next map change. Player 2 can control Ivan and press the C-Buttons to " - "use items and mess with Player 1!")); + "Enables Ivan the Fairy. Player 2 can control Ivan and press the C-Buttons to use items and mess with " + "Player 1!")); AddWidget(path, "Dogs Follow You Everywhere", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("DogFollowsEverywhere")) .Options(CheckboxOptions().Tooltip("Allows dogs to follow you anywhere you go, even if you leave the Market.")); diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index f73d076de4..00d1066a25 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -683,12 +683,6 @@ void Play_Init(GameState* thisx) { } #endif - if (CVarGetInteger(CVAR_ENHANCEMENT("IvanCoopModeEnabled"), 0)) { - Actor_Spawn(&play->actorCtx, play, gEnPartnerId, GET_PLAYER(play)->actor.world.pos.x, - GET_PLAYER(play)->actor.world.pos.y + Player_GetHeight(GET_PLAYER(play)) + 5.0f, - GET_PLAYER(play)->actor.world.pos.z, 0, 0, 0, 1); - } - // nextEntranceIndex was not initialized, so the previous value was carried over during soft resets. gPlayState->nextEntranceIndex = gSaveContext.entranceIndex; } diff --git a/soh/src/overlays/actors/ovl_En_Partner/z_en_partner.c b/soh/src/overlays/actors/ovl_En_Partner/z_en_partner.c index 4b7e50e877..1d6ac2ddbe 100644 --- a/soh/src/overlays/actors/ovl_En_Partner/z_en_partner.c +++ b/soh/src/overlays/actors/ovl_En_Partner/z_en_partner.c @@ -25,7 +25,6 @@ void EnPartner_Draw(Actor* thisx, PlayState* play); void EnPartner_SpawnSparkles(EnPartner* this, PlayState* play, s32 sparkleLife); void Player_RequestQuake(PlayState* play, s32 speed, s32 y, s32 countdown); -s32 spawn_boomerang_ivan(EnPartner* this, PlayState* play); static InitChainEntry sInitChain[] = { ICHAIN_VEC3F_DIV1000(scale, 8, ICHAIN_STOP), @@ -114,6 +113,17 @@ void EnPartner_Destroy(Actor* thisx, PlayState* play) { s32 pad; EnPartner* this = (EnPartner*)thisx; + if (this->hookshotTarget != NULL) { + Actor_Kill(this->hookshotTarget); + this->hookshotTarget = NULL; + } + + Player* player = GET_PLAYER(play); + if (player) { + player->ivanFloating = 0; + player->ivanDamageMultiplier = 1; + } + LightContext_RemoveLight(play, &play->lightCtx, this->lightNodeGlow); LightContext_RemoveLight(play, &play->lightCtx, this->lightNodeNoGlow); @@ -408,7 +418,19 @@ void UseBoomerang(Actor* thisx, PlayState* play, u8 started) { if (this->itemTimer <= 0) { if (started == 1) { this->itemTimer = 20; - spawn_boomerang_ivan(&this->actor, play); + + f32 posX = (Math_SinS(this->actor.shape.rot.y) * 1.0f) + this->actor.world.pos.x; + f32 posZ = (Math_CosS(this->actor.shape.rot.y) * 1.0f) + this->actor.world.pos.z; + s32 yaw = this->actor.shape.rot.y; + EnBoom* boomerang = + (EnBoom*)Actor_Spawn(&play->actorCtx, play, ACTOR_EN_BOOM, posX, this->actor.world.pos.y + 7.0f, posZ, + this->actor.focus.rot.x, yaw, 0, 0); + + this->boomerangActor = &boomerang->actor; + if (boomerang != NULL) { + boomerang->returnTimer = 20; + Audio_PlayActorSound2(&this->actor, NA_SE_IT_BOOMERANG_THROW); + } } } } diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index bb18a2da13..ab005d510b 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -24,7 +24,6 @@ #include "soh/Enhancements/item-tables/ItemTableTypes.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/Enhancements/randomizer/randomizer_entrance.h" -#include #include "soh/Enhancements/cosmetics/cosmeticsTypes.h" #include "soh/Enhancements/enhancementTypes.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" @@ -346,26 +345,6 @@ void Player_Action_CsAction(Player* this, PlayState* play); #pragma region[SoH] u8 gWalkSpeedToggle; -s32 spawn_boomerang_ivan(EnPartner* this, PlayState* play) { - if (!CVarGetInteger(CVAR_ENHANCEMENT("IvanCoopModeEnabled"), 0)) { - return 0; - } - - f32 posX = (Math_SinS(this->actor.shape.rot.y) * 1.0f) + this->actor.world.pos.x; - f32 posZ = (Math_CosS(this->actor.shape.rot.y) * 1.0f) + this->actor.world.pos.z; - s32 yaw = this->actor.shape.rot.y; - EnBoom* boomerang = (EnBoom*)Actor_Spawn(&play->actorCtx, play, ACTOR_EN_BOOM, posX, this->actor.world.pos.y + 7.0f, - posZ, this->actor.focus.rot.x, yaw, 0, 0); - - this->boomerangActor = &boomerang->actor; - if (boomerang != NULL) { - boomerang->returnTimer = 20; - Audio_PlayActorSound2(&this->actor, NA_SE_IT_BOOMERANG_THROW); - } - - return 1; -} - // Sets a flag according to which type of flag is specified in player->pendingFlag.flagType // and which flag is specified in player->pendingFlag.flagID. void Player_SetPendingFlag(Player* this, PlayState* play) {