From 97fb13cbee0e2b484830c0c8792f78116b97703f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:49:50 +0000 Subject: [PATCH] Hookify FP movement (#7189) Apply consistent movement effects to first person movement --- soh/soh/Enhancements/FirstPerson.cpp | 73 +++++++++++++++++++ .../vanilla-behavior/GIVanillaBehavior.h | 27 +++++++ .../actors/ovl_player_actor/z_player.c | 50 +++---------- 3 files changed, 111 insertions(+), 39 deletions(-) create mode 100644 soh/soh/Enhancements/FirstPerson.cpp diff --git a/soh/soh/Enhancements/FirstPerson.cpp b/soh/soh/Enhancements/FirstPerson.cpp new file mode 100644 index 0000000000..25e4c6a613 --- /dev/null +++ b/soh/soh/Enhancements/FirstPerson.cpp @@ -0,0 +1,73 @@ +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "global.h" +extern PlayState* gPlayState; +void func_8083DF68(Player* player, f32 arg1, s16 arg2); +void func_8084029C(Player* player, f32 arg1); +s32 Player_GetMovementSpeedAndYaw(Player* player, f32* outSpeedTarget, s16* outYawTarget, f32 speedMode, + PlayState* play); +} + +#define CVAR_MOVE_IN_FIRST_PERSON_NAME CVAR_SETTING("MoveInFirstPerson") +#define CVAR_RIGHT_STICK_AIM_NAME CVAR_SETTING("Controls.RightStickAim") + +// from z_player.c +#define SPEED_MODE_CURVED 0.018f + +static void MoveInFirstPerson(Player* player) { + f32 speedTarget; + s16 yawTarget; + + // Normal movement ignores the stick while unk_6AD says Link is in first person + uint8_t firstPersonMode = player->unk_6AD; + player->unk_6AD = 0; + bool moving = Player_GetMovementSpeedAndYaw(player, &speedTarget, &yawTarget, SPEED_MODE_CURVED, gPlayState); + player->unk_6AD = firstPersonMode; + + if ((player->stateFlags1 & PLAYER_STATE1_IN_WATER) && (player->currentBoots != PLAYER_BOOTS_IRON)) { + // Swim speed cap from func_8084AEEC. Its speed step only runs on swim stroke frames, so it isn't used here. + f32 swimSpeed = R_RUN_SPEED_LIMIT / 100.0f * 0.8f; + GameInteractor_Should(VB_PLAYER_MODIFY_SWIM_SPEED, true, player, &swimSpeed, true); + speedTarget = CLAMP_MAX(speedTarget, swimSpeed); + } else { + GameInteractor_Should(VB_PLAYER_MODIFY_FIRST_PERSON_SPEED, true, player, &speedTarget); + } + + // Turn instantly so strafing feels direct + if (moving) { + player->yaw = yawTarget; + } + func_8083DF68(player, speedTarget, player->yaw); + + if (!(player->stateFlags1 & PLAYER_STATE1_IN_WATER) && (player->linearVelocity > 0.0f)) { + func_8084029C(player, (REG(30) / 1000.0f) + ((REG(32) / 1000.0f) * player->linearVelocity)); + } +} + +static void RegisterFirstPerson() { + bool moveInFirstPerson = + CVarGetInteger(CVAR_MOVE_IN_FIRST_PERSON_NAME, 0) && CVarGetInteger(CVAR_RIGHT_STICK_AIM_NAME, 0); + + // Left stick moves instead of aiming; right stick aims + COND_VB_SHOULD(VB_PLAYER_AIM_WITH_LEFT_STICK, moveInFirstPerson, { *should = false; }); + + COND_VB_SHOULD(VB_PLAYER_FIRST_PERSON_DECELERATE, moveInFirstPerson, { + Player* player = va_arg(args, Player*); + MoveInFirstPerson(player); + *should = false; + }); + + // Keep moving where stick points rather than where Link faces. Once stopped, face forward as normal. + COND_VB_SHOULD(VB_PLAYER_FIRST_PERSON_ALIGN_YAW, moveInFirstPerson, { + Player* player = va_arg(args, Player*); + if (player->linearVelocity != 0.0f) { + *should = false; + } + }); +} + +static RegisterShipInitFunc initFunc(RegisterFirstPerson, + { CVAR_MOVE_IN_FIRST_PERSON_NAME, CVAR_RIGHT_STICK_AIM_NAME }); diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index d53d15c5b9..9eb6fb5fb6 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -2323,6 +2323,15 @@ typedef enum { // - `*DemoIm` VB_PLAY_ZELDAS_LULLABY_CS, + // #### `result` + // ```c + // true + // ``` + // Whether the left stick aims in first person. Off when it moves Link instead. + // #### `args` + // - `*Player` + VB_PLAYER_AIM_WITH_LEFT_STICK, + // #### `result` // ```c // true @@ -2342,6 +2351,24 @@ typedef enum { // - `PlayState*` play VB_PLAYER_DRAW_BOTTLE, + // #### `result` + // ```c + // true + // ``` + // Whether Link's movement direction is set to where he faces in first person. + // #### `args` + // - `*Player` + VB_PLAYER_FIRST_PERSON_ALIGN_YAW, + + // #### `result` + // ```c + // true + // ``` + // Whether Link slows to a stop in first person. Hooks may set his speed instead. + // #### `args` + // - `*Player` + VB_PLAYER_FIRST_PERSON_DECELERATE, + // #### `result` // ```c // true 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 d8c16755aa..76b7183503 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -12582,8 +12582,7 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { if (!func_8002DD78(this) && !func_808334B4(this) && (arg2 == 0)) { // First person without weapon // Y Axis - if (!(CVarGetInteger(CVAR_SETTING("MoveInFirstPerson"), 0) && - CVarGetInteger(CVAR_SETTING("Controls.RightStickAim"), 0))) { + if (GameInteractor_Should(VB_PLAYER_AIM_WITH_LEFT_STICK, true, this)) { temp2 += sControlInput->rel.stick_y * 240.0f * invertYAxisMulti * yAxisMulti; } if (CVarGetInteger(CVAR_SETTING("Controls.RightStickAim"), 0)) { @@ -12601,8 +12600,7 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { // X Axis temp2 = 0; - if (!(CVarGetInteger(CVAR_SETTING("MoveInFirstPerson"), 0) && - CVarGetInteger(CVAR_SETTING("Controls.RightStickAim"), 0))) { + if (GameInteractor_Should(VB_PLAYER_AIM_WITH_LEFT_STICK, true, this)) { temp2 += sControlInput->rel.stick_x * -16.0f * invertXAxisMulti * xAxisMulti; } if (CVarGetInteger(CVAR_SETTING("Controls.RightStickAim"), 0)) { @@ -12617,8 +12615,7 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { // Y Axis temp1 = (this->stateFlags1 & PLAYER_STATE1_ON_HORSE) ? 3500 : 14000; - if (!(CVarGetInteger(CVAR_SETTING("MoveInFirstPerson"), 0) && - CVarGetInteger(CVAR_SETTING("Controls.RightStickAim"), 0))) { + if (GameInteractor_Should(VB_PLAYER_AIM_WITH_LEFT_STICK, true, this)) { temp3 += ((sControlInput->rel.stick_y >= 0) ? 1 : -1) * (s32)((1.0f - Math_CosS(sControlInput->rel.stick_y * 200)) * 1500.0f) * invertYAxisMulti * yAxisMulti; @@ -12638,8 +12635,7 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { temp1 = 19114; temp2 = this->actor.focus.rot.y - this->actor.shape.rot.y; temp3 = 0; - if (!(CVarGetInteger(CVAR_SETTING("MoveInFirstPerson"), 0) && - CVarGetInteger(CVAR_SETTING("Controls.RightStickAim"), 0))) { + if (GameInteractor_Should(VB_PLAYER_AIM_WITH_LEFT_STICK, true, this)) { temp3 = ((sControlInput->rel.stick_x >= 0) ? 1 : -1) * (s32)((1.0f - Math_CosS(sControlInput->rel.stick_x * 200)) * -1500.0f) * invertXAxisMulti * xAxisMulti; @@ -12656,34 +12652,6 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { this->actor.focus.rot.y = CLAMP(temp2, -temp1, temp1) + this->actor.shape.rot.y; } - if (CVarGetInteger(CVAR_SETTING("MoveInFirstPerson"), 0) && - CVarGetInteger(CVAR_SETTING("Controls.RightStickAim"), 0)) { - f32 movementSpeed = LINK_IS_ADULT ? 9.0f : 8.25f; - GameInteractor_Should(VB_PLAYER_MODIFY_FIRST_PERSON_SPEED, true, this, &movementSpeed); - - f32 relX = - (sControlInput->rel.stick_x * (CVarGetInteger(CVAR_ENHANCEMENT("MirroredWorld"), 0) ? 1 : -1)) / 10.0f; - f32 relY = sControlInput->rel.stick_y / 10.0f; - - // Normalize so that diagonal movement isn't faster - f32 relMag = sqrtf((relX * relX) + (relY * relY)); - if (relMag > 1.0f) { - relX /= relMag; - relY /= relMag; - } - - // Determine what left and right mean based on camera angle - f32 relX2 = relX * Math_CosS(this->actor.focus.rot.y) + relY * Math_SinS(this->actor.focus.rot.y); - f32 relY2 = relY * Math_CosS(this->actor.focus.rot.y) - relX * Math_SinS(this->actor.focus.rot.y); - - // Calculate distance for footstep sound - f32 distance = sqrtf((relX2 * relX2) + (relY2 * relY2)) * movementSpeed; - func_8084029C(this, distance / 4.5f); - - this->actor.world.pos.x += (relX2 * movementSpeed) + this->actor.colChkInfo.displacement.x; - this->actor.world.pos.z += (relY2 * movementSpeed) + this->actor.colChkInfo.displacement.z; - } - this->unk_6AE_rotFlags |= UNK6AE_ROT_FOCUS_Y; return func_80836AB8(this, (play->shootingGalleryStatus != 0) || func_8002DD78(this) || func_808334B4(this)) - arg3; } @@ -12809,8 +12777,10 @@ void func_8084B158(PlayState* play, Player* this, Input* input, f32 arg3) { void Player_Action_8084B1D8(Player* this, PlayState* play) { if (this->stateFlags1 & PLAYER_STATE1_IN_WATER) { func_8084B000(this); - func_8084AEEC(this, &this->linearVelocity, 0, this->actor.shape.rot.y); - } else { + if (GameInteractor_Should(VB_PLAYER_FIRST_PERSON_DECELERATE, true, this)) { + func_8084AEEC(this, &this->linearVelocity, 0, this->actor.shape.rot.y); + } + } else if (GameInteractor_Should(VB_PLAYER_FIRST_PERSON_DECELERATE, true, this)) { Player_DecelerateToZero(this); } @@ -12838,7 +12808,9 @@ void Player_Action_8084B1D8(Player* this, PlayState* play) { } } - this->yaw = this->actor.shape.rot.y; + if (GameInteractor_Should(VB_PLAYER_FIRST_PERSON_ALIGN_YAW, true, this)) { + this->yaw = this->actor.shape.rot.y; + } } s32 func_8084B3CC(PlayState* play, Player* this) {