summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristopher Leggett <chris@leggett.dev>2026-02-10 00:07:34 +0000
committerGitHub <noreply@github.com>2026-02-10 00:07:34 +0000
commit6a31700820b85ff7a02e29d89e4bc885a1677d29 (patch)
tree0635dcf2daa6b72faf07e1429e1e85e1b7e84ecf
parenta2f2697d0dcc3f8e32c89b02fa09732c6b713043 (diff)
Fixes some Stone of Agony bugs and refactors it to a VB Hook (#6235)
Fixes #1727. Specifically, it removes a lot of old situations in which the stone of agony icon would've been hidden, such as shield and z-targeting. It will still be hidden while talking to an NPC or in cutscenes. This does also introduce a subtle change in behavior. It now behaves a bit more like the Shard of Agony from the 3ds version. Instead of always being visible after the stone of agony is obtained, it is now hidden until you get close to a secret spot, appearing at a slightly further range than when the rumble would start.
-rw-r--r--soh/soh/Enhancements/Graphics/VisualAgony.cpp136
-rw-r--r--soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h9
-rw-r--r--soh/src/overlays/actors/ovl_player_actor/z_player.c103
3 files changed, 146 insertions, 102 deletions
diff --git a/soh/soh/Enhancements/Graphics/VisualAgony.cpp b/soh/soh/Enhancements/Graphics/VisualAgony.cpp
new file mode 100644
index 000000000..de7fcee33
--- /dev/null
+++ b/soh/soh/Enhancements/Graphics/VisualAgony.cpp
@@ -0,0 +1,136 @@
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h"
+#include "soh/Enhancements/cosmetics/cosmeticsTypes.h"
+#include "soh/ShipInit.hpp"
+#include "textures/icon_item_24_static/icon_item_24_static.h"
+
+extern "C" {
+#include "macros.h"
+#include "variables.h"
+#include "functions.h"
+
+extern PlayState* gPlayState;
+int16_t OTRGetRectDimensionFromLeftEdge(float v);
+float OTRGetDimensionFromLeftEdge(float v);
+float OTRGetDimensionFromRightEdge(float v);
+void FrameInterpolation_RecordOpenChild(const void* a, int b);
+void FrameInterpolation_RecordCloseChild(void);
+}
+
+#define CVAR_VISUAL_AGONY_NAME CVAR_ENHANCEMENT("VisualAgony")
+#define CVAR_VISUAL_AGONY_VALUE CVarGetInteger(CVAR_VISUAL_AGONY_NAME, 0)
+#define SHOULD_HIDE_VISUAL_AGONY \
+ ((player->stateFlags1 & \
+ (PLAYER_STATE1_IN_CUTSCENE | PLAYER_STATE1_DEAD | PLAYER_STATE1_TALKING | PLAYER_STATE1_LOADING)) || \
+ GameInteractor_NoUIActive())
+
+void DrawVisualAgony(Player* player, double temp) {
+ if (SHOULD_HIDE_VISUAL_AGONY || player->closestSecretDistSq > 80000.0f) {
+ return;
+ }
+ Color_RGB8 stoneOfAgonyColor = { 255, 255, 255 };
+ if (CVarGetInteger(CVAR_COSMETIC("HUD.StoneOfAgony.Changed"), 0)) {
+ stoneOfAgonyColor = CVarGetColor24(CVAR_COSMETIC("HUD.StoneOfAgony.Value"), stoneOfAgonyColor);
+ }
+ s16 Top_Margins = (CVarGetInteger(CVAR_COSMETIC("HUD.Margin.T"), 0) * -1);
+ s16 Left_Margins = CVarGetInteger(CVAR_COSMETIC("HUD.Margin.L"), 0);
+ s16 Right_Margins = CVarGetInteger(CVAR_COSMETIC("HUD.Margin.R"), 0);
+ s16 X_Margins_VSOA;
+ s16 Y_Margins_VSOA;
+ if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.UseMargins"), 0) != 0) {
+ if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == ORIGINAL_LOCATION) {
+ X_Margins_VSOA = Left_Margins;
+ } else {
+ X_Margins_VSOA = 0;
+ }
+ Y_Margins_VSOA = Top_Margins;
+ } else {
+ X_Margins_VSOA = 0;
+ Y_Margins_VSOA = 0;
+ }
+ s16 PosX_VSOA_ori = OTRGetRectDimensionFromLeftEdge(26) + X_Margins_VSOA;
+ s16 PosY_VSOA_ori = 60 + Y_Margins_VSOA;
+ s16 PosX_VSOA;
+ s16 PosY_VSOA;
+ if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) != 0) {
+ PosY_VSOA = CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosY"), 0) + Y_Margins_VSOA;
+ if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == ANCHOR_LEFT) {
+ if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.UseMargins"), 0) != 0) {
+ X_Margins_VSOA = Left_Margins;
+ };
+ PosX_VSOA =
+ OTRGetDimensionFromLeftEdge(CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosX"), 0) + X_Margins_VSOA);
+ } else if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == ANCHOR_RIGHT) {
+ if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.UseMargins"), 0) != 0) {
+ X_Margins_VSOA = Right_Margins;
+ }
+ PosX_VSOA =
+ OTRGetDimensionFromRightEdge(CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosX"), 0) + X_Margins_VSOA);
+ } else if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == ANCHOR_NONE) {
+ PosX_VSOA = CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosX"), 0);
+ } else if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == HIDDEN) {
+ PosX_VSOA = -9999;
+ } else {
+ PosX_VSOA = PosX_VSOA_ori;
+ }
+ } else {
+ PosY_VSOA = PosY_VSOA_ori;
+ PosX_VSOA = PosX_VSOA_ori;
+ }
+
+ int rectLeft = PosX_VSOA; // Left X Pos
+ int rectTop = PosY_VSOA; // Top Y Pos
+ int rectWidth = 24; // Texture Width
+ int rectHeight = 24; // Texture Heigh
+ int DefaultIconA = 50; // Default icon alpha (55 on 255)
+
+ OPEN_DISPS(gPlayState->state.gfxCtx);
+ gDPPipeSync(OVERLAY_DISP++);
+
+ gDPSetPrimColor(OVERLAY_DISP++, 0, 0, stoneOfAgonyColor.r, stoneOfAgonyColor.g, stoneOfAgonyColor.b, DefaultIconA);
+
+ gDPSetCombineLERP(OVERLAY_DISP++, PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0, PRIMITIVE,
+ ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0);
+ if (player->unk_6A0 > 4000000.0f) {
+ gDPSetPrimColor(OVERLAY_DISP++, 0, 0, stoneOfAgonyColor.r, stoneOfAgonyColor.g, stoneOfAgonyColor.b, 255);
+ } else {
+ gDPSetPrimColor(OVERLAY_DISP++, 0, 0, stoneOfAgonyColor.r, stoneOfAgonyColor.g, stoneOfAgonyColor.b,
+ DefaultIconA);
+ }
+ if (temp == 0 || temp <= 0.1f) {
+ /*Fail check, it is used to draw off the icon when
+ link is standing out range but do not refresh unk_6A0.
+ Also used to make a default value in my case.*/
+ gDPSetPrimColor(OVERLAY_DISP++, 0, 0, stoneOfAgonyColor.r, stoneOfAgonyColor.g, stoneOfAgonyColor.b,
+ DefaultIconA);
+ }
+ gDPSetEnvColor(OVERLAY_DISP++, 0, 0, 0, 255);
+ gDPSetOtherMode(OVERLAY_DISP++,
+ G_AD_DISABLE | G_CD_DISABLE | G_CK_NONE | G_TC_FILT | G_TF_POINT | G_TT_IA16 | G_TL_TILE |
+ G_TD_CLAMP | G_TP_NONE | G_CYC_1CYCLE | G_PM_NPRIMITIVE,
+ G_AC_NONE | G_ZS_PRIM | G_RM_XLU_SURF | G_RM_XLU_SURF2);
+ gDPLoadTextureBlock(OVERLAY_DISP++, gQuestIconStoneOfAgonyTex, G_IM_FMT_RGBA, G_IM_SIZ_32b, 24, 24, 0,
+ G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD,
+ G_TX_NOLOD);
+ gDPSetOtherMode(OVERLAY_DISP++,
+ G_AD_DISABLE | G_CD_DISABLE | G_CK_NONE | G_TC_FILT | G_TF_BILERP | G_TT_IA16 | G_TL_TILE |
+ G_TD_CLAMP | G_TP_NONE | G_CYC_1CYCLE | G_PM_NPRIMITIVE,
+ G_AC_NONE | G_ZS_PRIM | G_RM_XLU_SURF | G_RM_XLU_SURF2);
+ gSPWideTextureRectangle(OVERLAY_DISP++, rectLeft << 2, rectTop << 2, (rectLeft + rectWidth) << 2,
+ (rectTop + rectHeight) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10);
+ CLOSE_DISPS(gPlayState->state.gfxCtx);
+}
+
+void RegisterVisualAgony() {
+ COND_VB_SHOULD(VB_RUMBLE_FOR_SECRET, CVAR_VISUAL_AGONY_VALUE, {
+ Player* player = va_arg(args, Player*);
+ double temp = va_arg(args, double);
+ DrawVisualAgony(player, temp);
+ if (*should == true && !SHOULD_HIDE_VISUAL_AGONY) {
+ Audio_PlaySoundGeneral(NA_SE_SY_MESSAGE_WOMAN, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
+ &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
+ }
+ });
+}
+
+static RegisterShipInitFunc initFunc(RegisterVisualAgony, { CVAR_VISUAL_AGONY_NAME }); \ No newline at end of file
diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h
index da262f6ea..0cf3fe92b 100644
--- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h
+++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h
@@ -2597,6 +2597,15 @@ typedef enum {
// - `s32` commonType
VB_SET_IDLE_ANIM,
+ // #### `result`
+ // ```c
+ // player->unk_6A0 > 4000000.0f
+ // ```
+ // #### `args`
+ // - `*Player`
+ // - `double` (temp - promoted from `f32`)
+ VB_RUMBLE_FOR_SECRET
+
} GIVanillaBehavior;
#endif
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 617b76fec..944a7bd82 100644
--- a/soh/src/overlays/actors/ovl_player_actor/z_player.c
+++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c
@@ -20,7 +20,6 @@
#include "overlays/misc/ovl_kaleido_scope/z_kaleido_scope.h"
#include "objects/gameplay_keep/gameplay_keep.h"
#include "objects/object_link_child/object_link_child.h"
-#include "textures/icon_item_24_static/icon_item_24_static.h"
#include <soh/Enhancements/custom-message/CustomMessageTypes.h>
#include "soh/Enhancements/item-tables/ItemTableTypes.h"
#include "soh/Enhancements/game-interactor/GameInteractor.h"
@@ -11763,108 +11762,8 @@ void Player_DetectRumbleSecrets(Player* this) {
this->unk_6A0 += temp;
- /*Prevent it on horse, while jumping and on title screen.
- If you fly around no stone of agony for you! */
- Color_RGB8 stoneOfAgonyColor = { 255, 255, 255 };
- if (CVarGetInteger(CVAR_COSMETIC("HUD.StoneOfAgony.Changed"), 0)) {
- stoneOfAgonyColor = CVarGetColor24(CVAR_COSMETIC("HUD.StoneOfAgony.Value"), stoneOfAgonyColor);
- }
- if (CVarGetInteger(CVAR_ENHANCEMENT("VisualAgony"), 0) && !this->stateFlags1 && !GameInteractor_NoUIActive()) {
- s16 Top_Margins = (CVarGetInteger(CVAR_COSMETIC("HUD.Margin.T"), 0) * -1);
- s16 Left_Margins = CVarGetInteger(CVAR_COSMETIC("HUD.Margin.L"), 0);
- s16 Right_Margins = CVarGetInteger(CVAR_COSMETIC("HUD.Margin.R"), 0);
- s16 X_Margins_VSOA;
- s16 Y_Margins_VSOA;
- if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.UseMargins"), 0) != 0) {
- if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == ORIGINAL_LOCATION) {
- X_Margins_VSOA = Left_Margins;
- };
- Y_Margins_VSOA = Top_Margins;
- } else {
- X_Margins_VSOA = 0;
- Y_Margins_VSOA = 0;
- }
- s16 PosX_VSOA_ori = OTRGetRectDimensionFromLeftEdge(26) + X_Margins_VSOA;
- s16 PosY_VSOA_ori = 60 + Y_Margins_VSOA;
- s16 PosX_VSOA;
- s16 PosY_VSOA;
- if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) != 0) {
- PosY_VSOA = CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosY"), 0) + Y_Margins_VSOA;
- if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == ANCHOR_LEFT) {
- if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.UseMargins"), 0) != 0) {
- X_Margins_VSOA = Left_Margins;
- };
- PosX_VSOA = OTRGetDimensionFromLeftEdge(CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosX"), 0) +
- X_Margins_VSOA);
- } else if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == ANCHOR_RIGHT) {
- if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.UseMargins"), 0) != 0) {
- X_Margins_VSOA = Right_Margins;
- };
- PosX_VSOA = OTRGetDimensionFromRightEdge(CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosX"), 0) +
- X_Margins_VSOA);
- } else if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == ANCHOR_NONE) {
- PosX_VSOA = CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosX"), 0);
- } else if (CVarGetInteger(CVAR_COSMETIC("HUD.VisualSoA.PosType"), 0) == HIDDEN) {
- PosX_VSOA = -9999;
- }
- } else {
- PosY_VSOA = PosY_VSOA_ori;
- PosX_VSOA = PosX_VSOA_ori;
- }
-
- int rectLeft = PosX_VSOA; // Left X Pos
- int rectTop = PosY_VSOA; // Top Y Pos
- int rectWidth = 24; // Texture Width
- int rectHeight = 24; // Texture Heigh
- int DefaultIconA = 50; // Default icon alpha (55 on 255)
-
- OPEN_DISPS(gPlayState->state.gfxCtx);
- gDPPipeSync(OVERLAY_DISP++);
-
- gDPSetPrimColor(OVERLAY_DISP++, 0, 0, stoneOfAgonyColor.r, stoneOfAgonyColor.g, stoneOfAgonyColor.b,
- DefaultIconA);
-
- gDPSetCombineLERP(OVERLAY_DISP++, PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0,
- PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0);
- if (this->unk_6A0 > 4000000.0f) {
- gDPSetPrimColor(OVERLAY_DISP++, 0, 0, stoneOfAgonyColor.r, stoneOfAgonyColor.g, stoneOfAgonyColor.b,
- 255);
- } else {
- gDPSetPrimColor(OVERLAY_DISP++, 0, 0, stoneOfAgonyColor.r, stoneOfAgonyColor.g, stoneOfAgonyColor.b,
- DefaultIconA);
- }
- if (temp == 0 || temp <= 0.1f) {
- /*Fail check, it is used to draw off the icon when
- link is standing out range but do not refresh unk_6A0.
- Also used to make a default value in my case.*/
- gDPSetPrimColor(OVERLAY_DISP++, 0, 0, stoneOfAgonyColor.r, stoneOfAgonyColor.g, stoneOfAgonyColor.b,
- DefaultIconA);
- }
- gDPSetEnvColor(OVERLAY_DISP++, 0, 0, 0, 255);
- gDPSetOtherMode(OVERLAY_DISP++,
- G_AD_DISABLE | G_CD_DISABLE | G_CK_NONE | G_TC_FILT | G_TF_POINT | G_TT_IA16 | G_TL_TILE |
- G_TD_CLAMP | G_TP_NONE | G_CYC_1CYCLE | G_PM_NPRIMITIVE,
- G_AC_NONE | G_ZS_PRIM | G_RM_XLU_SURF | G_RM_XLU_SURF2);
- gDPLoadTextureBlock(OVERLAY_DISP++, gQuestIconStoneOfAgonyTex, G_IM_FMT_RGBA, G_IM_SIZ_32b, 24, 24, 0,
- G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK,
- G_TX_NOLOD, G_TX_NOLOD);
- gDPSetOtherMode(OVERLAY_DISP++,
- G_AD_DISABLE | G_CD_DISABLE | G_CK_NONE | G_TC_FILT | G_TF_BILERP | G_TT_IA16 | G_TL_TILE |
- G_TD_CLAMP | G_TP_NONE | G_CYC_1CYCLE | G_PM_NPRIMITIVE,
- G_AC_NONE | G_ZS_PRIM | G_RM_XLU_SURF | G_RM_XLU_SURF2);
- gSPWideTextureRectangle(OVERLAY_DISP++, rectLeft << 2, rectTop << 2, (rectLeft + rectWidth) << 2,
- (rectTop + rectHeight) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10);
- CLOSE_DISPS(gPlayState->state.gfxCtx);
- }
-
- if (this->unk_6A0 > 4000000.0f) {
+ if (GameInteractor_Should(VB_RUMBLE_FOR_SECRET, this->unk_6A0 > 4000000.0f, this, temp)) {
this->unk_6A0 = 0.0f;
- if (CVarGetInteger(CVAR_ENHANCEMENT("VisualAgony"), 0) && !this->stateFlags1 &&
- !GameInteractor_NoUIActive()) {
- // This audio is placed here and not in previous CVar check to prevent ears ra.. :)
- Audio_PlaySoundGeneral(NA_SE_SY_MESSAGE_WOMAN, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
- &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale);
- }
Player_RequestRumble(this, 120, 20, 10, 0);
}
}