summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormckinlee <mckinlee@ymail.com>2026-03-19 21:41:24 -0400
committerGitHub <noreply@github.com>2026-03-19 21:41:24 -0400
commitcfd5cc483ae2da52ccc8e00082ef2f8e3d969863 (patch)
tree7f949e637b9627975e6e08bfdec8bbfb32df5565
parentf7ccda83aab44cf532798aba306b2cf12a3ea8fb (diff)
[Enhancement] Song Items (#1566)
* equip songs * feedback
-rw-r--r--mm/2s2h/BenGui/BenMenu.cpp4
-rw-r--r--mm/2s2h/Enhancements/Songs/SongItems.cpp522
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h40
-rw-r--r--mm/src/code/z_message.c33
-rw-r--r--mm/src/code/z_parameter.c74
-rw-r--r--mm/src/code/z_player_lib.c19
6 files changed, 648 insertions, 44 deletions
diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp
index 310475b86..ca2d247c6 100644
--- a/mm/2s2h/BenGui/BenMenu.cpp
+++ b/mm/2s2h/BenGui/BenMenu.cpp
@@ -1353,6 +1353,10 @@ void BenMenu::AddEnhancements() {
AddWidget(path, "Right Stick Ocarina", WIDGET_CVAR_CHECKBOX)
.CVar("gEnhancements.Playback.RightStickOcarina")
.Options(CheckboxOptions().Tooltip("Enables using the Right Stick for Ocarina playback."));
+ AddWidget(path, "Song Items", WIDGET_CVAR_CHECKBOX)
+ .CVar("gEnhancements.Songs.SongItems")
+ .Options(CheckboxOptions().Tooltip("Equip songs to C/D-Pad buttons from the Quest Status page. "
+ "Songs auto-play when used, skipping manual note input."));
AddWidget(path, "Pause Owl Warp", WIDGET_CVAR_CHECKBOX)
.CVar("gEnhancements.Songs.PauseOwlWarp")
.Options(CheckboxOptions().Tooltip(
diff --git a/mm/2s2h/Enhancements/Songs/SongItems.cpp b/mm/2s2h/Enhancements/Songs/SongItems.cpp
new file mode 100644
index 000000000..8fa1e0f3d
--- /dev/null
+++ b/mm/2s2h/Enhancements/Songs/SongItems.cpp
@@ -0,0 +1,522 @@
+#include <libultraship/bridge/consolevariablebridge.h>
+#include "2s2h/GameInteractor/GameInteractor.h"
+#include "2s2h/ShipInit.hpp"
+#include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h"
+
+extern "C" {
+#include "archives/icon_item_static/icon_item_static_yar.h"
+#include "interface/parameter_static/parameter_static.h"
+extern s16 sEquipState;
+extern s16 sEquipAnimTimer;
+char* ResourceMgr_LoadTexOrDListByName(const char* filePath);
+void Message_ResetOcarinaButtonAlphas(void);
+}
+
+#define CVAR_NAME "gEnhancements.Songs.SongItems"
+#define CVAR CVarGetInteger(CVAR_NAME, 0)
+
+static u8 sPendingAutoPlaySong = 0xFF;
+static bool sHideOcarinaStaff = false;
+static bool sOcarinaErrorPlayed = false;
+static u32 sSongIconsRGBA32[13][32 * 32];
+
+static struct {
+ s32 btn;
+ bool isDpad;
+ ItemId item;
+ s32 slot;
+} sPendingSongSwap = { -1, false, ITEM_NONE, SLOT_NONE };
+
+static bool IsSongItem(ItemId item) {
+ return (item >= ITEM_SONG_SONATA && item <= ITEM_SONG_SUN) || item == ITEM_SONG_LULLABY_INTRO;
+}
+
+static u8 SongItemToOcarinaId(ItemId item) {
+ return item == ITEM_SONG_LULLABY_INTRO ? OCARINA_SONG_GORON_LULLABY_INTRO : (u8)(item - ITEM_SONG_SONATA);
+}
+
+static TexturePtr GetSongIconTexture(ItemId item) {
+ if (item == ITEM_SONG_LULLABY_INTRO) {
+ return (TexturePtr)sSongIconsRGBA32[12];
+ }
+ return (TexturePtr)sSongIconsRGBA32[item - ITEM_SONG_SONATA];
+}
+
+template <typename F> static void ForEachEquipSlot(F&& func) {
+ for (int i = EQUIP_SLOT_C_LEFT; i <= EQUIP_SLOT_C_RIGHT; i++) {
+ func(i, false);
+ }
+ if (CVarGetInteger("gEnhancements.Dpad.DpadEquips", 0)) {
+ for (int i = EQUIP_SLOT_D_RIGHT; i <= EQUIP_SLOT_D_UP; i++) {
+ func(i, true);
+ }
+ }
+}
+
+static ItemId GetButtonItem(s32 btn, bool isDpad) {
+ return (ItemId)(isDpad ? DPAD_GET_CUR_FORM_BTN_ITEM(btn) : GET_CUR_FORM_BTN_ITEM(btn));
+}
+
+static s32 GetButtonSlot(s32 btn, bool isDpad) {
+ return isDpad ? DPAD_GET_CUR_FORM_BTN_SLOT(btn) : GET_CUR_FORM_BTN_SLOT(btn);
+}
+
+static void SetIconItemSegment(PlayState* play, s32 btn, bool isDpad, TexturePtr tex) {
+ if (isDpad)
+ play->interfaceCtx.iconItemSegment[DPAD_BUTTON(btn) + EQUIP_SLOT_MAX] = (char*)tex;
+ else
+ play->interfaceCtx.iconItemSegment[btn] = (char*)tex;
+}
+
+static void SetButtonItem(PlayState* play, s32 btn, bool isDpad, ItemId item, s32 itemSlot) {
+ if (isDpad) {
+ DPAD_SET_CUR_FORM_BTN_ITEM(btn, item);
+ DPAD_SET_CUR_FORM_BTN_SLOT(btn, itemSlot);
+ Interface_Dpad_LoadItemIconImpl(play, btn);
+ } else {
+ SET_CUR_FORM_BTN_ITEM(btn, item);
+ SET_CUR_FORM_BTN_SLOT(btn, itemSlot);
+ Interface_LoadItemIconImpl(play, btn);
+ }
+ if (IsSongItem(item)) {
+ SetIconItemSegment(play, btn, isDpad, GetSongIconTexture(item));
+ }
+}
+
+// 4x5 pixel font for song acronym text on icons.
+// Each glyph is 4 pixels wide x 5 pixels tall. Each byte = one row, bit 3 = leftmost pixel.
+// clang-format off
+static const u8 sFont4x5[][5] = {
+ { 0b0110, 0b1001, 0b1111, 0b1001, 0b1001 }, // A
+ { 0b1110, 0b1001, 0b1110, 0b1001, 0b1110 }, // B
+ { 0b1111, 0b1000, 0b1110, 0b1000, 0b1111 }, // E
+ { 0b0111, 0b1000, 0b1011, 0b1001, 0b0110 }, // G
+ { 0b1001, 0b1001, 0b1111, 0b1001, 0b1001 }, // H
+ { 0b1110, 0b0100, 0b0100, 0b0100, 0b1110 }, // I
+ { 0b1000, 0b1000, 0b1000, 0b1000, 0b1111 }, // L
+ { 0b1001, 0b1111, 0b1111, 0b1001, 0b1001 }, // M
+ { 0b1001, 0b1101, 0b1111, 0b1011, 0b1001 }, // N
+ { 0b0110, 0b1001, 0b1001, 0b1001, 0b0110 }, // O
+ { 0b1110, 0b1001, 0b1110, 0b1000, 0b1000 }, // P
+ { 0b1110, 0b1001, 0b1110, 0b1010, 0b1001 }, // R
+ { 0b0111, 0b1000, 0b0110, 0b0001, 0b1110 }, // S
+ { 0b1110, 0b0100, 0b0100, 0b0100, 0b0100 }, // T
+ { 0b1001, 0b1001, 0b1001, 0b1001, 0b0110 }, // U
+};
+// clang-format on
+static const char sFontChars[] = "ABEGHILMNOPRSTU";
+
+static const char* sSongAcronyms[13] = {
+ "SOA", "GL", "BN", "EOE", "OTO", "SS", "SOT", "SOH", "ES", "SOAR", "STORM", "SUS", "GLI",
+};
+
+static int CharToFontIndex(char c) {
+ for (int i = 0; sFontChars[i]; i++) {
+ if (sFontChars[i] == c)
+ return i;
+ }
+ return -1;
+}
+
+static void RenderTextOnIcon(u32* iconBuf, const char* text) {
+ int len = (int)strlen(text);
+ int textWidth = len * 5 - 1;
+ int startX = (32 - textWidth) / 2;
+ int startY = 26;
+
+ // Pass 1: dark outline around each glyph pixel.
+ // Writes unconditionally so the outline is visible against both transparent areas and bright
+ // note pixels (important for white songs). The foreground pass overwrites glyph positions after.
+ for (int ci = 0; ci < len; ci++) {
+ int fi = CharToFontIndex(text[ci]);
+ if (fi < 0)
+ continue;
+ for (int gy = 0; gy < 5; gy++) {
+ u8 row = sFont4x5[fi][gy];
+ for (int gx = 0; gx < 4; gx++) {
+ if (!(row & (1 << (3 - gx))))
+ continue;
+ for (int dy = -1; dy <= 1; dy++) {
+ for (int dx = -1; dx <= 1; dx++) {
+ if (dx == 0 && dy == 0)
+ continue;
+ int px = startX + ci * 5 + gx + dx;
+ int py = startY + gy + dy;
+ if (px >= 0 && px < 32 && py >= 0 && py < 32) {
+ iconBuf[py * 32 + px] = 0xC0000000;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Pass 2: white foreground pixels
+ for (int ci = 0; ci < len; ci++) {
+ int fi = CharToFontIndex(text[ci]);
+ if (fi < 0)
+ continue;
+ for (int gy = 0; gy < 5; gy++) {
+ u8 row = sFont4x5[fi][gy];
+ for (int gx = 0; gx < 4; gx++) {
+ if (row & (1 << (3 - gx))) {
+ int px = startX + ci * 5 + gx;
+ int py = startY + gy;
+ if (px >= 0 && px < 32 && py >= 0 && py < 32) {
+ iconBuf[py * 32 + px] = 0xFFFFFFFF;
+ }
+ }
+ }
+ }
+ }
+}
+
+static void InitSongIcons() {
+ static const Color_RGB8 sColors[] = {
+ { 150, 255, 100 }, { 255, 80, 40 }, { 100, 150, 255 }, { 255, 160, 0 }, { 255, 100, 255 },
+ { 255, 240, 100 }, { 255, 255, 255 }, { 255, 255, 255 }, { 255, 255, 255 }, { 255, 255, 255 },
+ { 255, 255, 255 }, { 255, 255, 255 }, { 255, 80, 40 },
+ };
+
+ memset(sSongIconsRGBA32, 0, sizeof(sSongIconsRGBA32));
+
+ u8* src = (u8*)ResourceMgr_LoadTexOrDListByName(dgItemIconSongNoteTex);
+ if (src == nullptr) {
+ return;
+ }
+
+ for (int s = 0; s < 13; s++) {
+ Color_RGB8 c = sColors[s];
+ for (int y = 0; y < 24; y++) {
+ for (int x = 0; x < 16; x++) {
+ u8 px = src[y * 16 + x];
+ u8 intensity = (px >> 4) * 0x11;
+ u8 alpha = (px & 0xF) * 0x11;
+ sSongIconsRGBA32[s][(7 + y) * 32 + (9 + x)] = (alpha << 24) | ((intensity * c.b / 255) << 16) |
+ ((intensity * c.g / 255) << 8) | (intensity * c.r / 255);
+ }
+ }
+ RenderTextOnIcon(sSongIconsRGBA32[s], sSongAcronyms[s]);
+ }
+}
+
+static void HandleSongEquip(PauseContext* pauseCtx) {
+ PlayState* play = gPlayState;
+ Input* input = &play->state.input[0];
+
+ if (pauseCtx->state != PAUSE_STATE_MAIN || pauseCtx->pageIndex != PAUSE_QUEST ||
+ (pauseCtx->mainState != PAUSE_MAIN_STATE_IDLE_CURSOR_ON_SONG && pauseCtx->mainState != PAUSE_MAIN_STATE_IDLE) ||
+ pauseCtx->cursorSpecialPos != 0) {
+ return;
+ }
+
+ s16 cursorPoint = pauseCtx->cursorPoint[PAUSE_QUEST];
+ bool isHoveringSong = false;
+
+ bool isDpadEquipsEnabled = CVarGetInteger("gEnhancements.Dpad.DpadEquips", 0);
+
+ if ((cursorPoint >= QUEST_SONG_SONATA && cursorPoint <= QUEST_SONG_SUN) ||
+ cursorPoint == QUEST_SONG_LULLABY_INTRO) {
+ if (CHECK_QUEST_ITEM(cursorPoint) ||
+ (cursorPoint == QUEST_SONG_LULLABY && CHECK_QUEST_ITEM(QUEST_SONG_LULLABY_INTRO))) {
+ isHoveringSong = true;
+ }
+ }
+
+ auto updateButtonStatus = [&](u8 status) {
+ if (gSaveContext.buttonStatus[EQUIP_SLOT_C_LEFT] == status)
+ return;
+ ForEachEquipSlot([&](s32 i, bool isDpadBtn) {
+ if (isDpadBtn)
+ gSaveContext.shipSaveContext.dpad.status[i] = status;
+ else
+ gSaveContext.buttonStatus[i] = status;
+ });
+ gSaveContext.hudVisibility = HUD_VISIBILITY_IDLE;
+ Interface_SetHudVisibility(HUD_VISIBILITY_ALL);
+ };
+
+ updateButtonStatus((isHoveringSong && play->msgCtx.msgLength == 0) ? BTN_ENABLED : BTN_DISABLED);
+
+ if (!isHoveringSong) {
+ return;
+ }
+
+ // Convert the QuestItem cursor index to an ItemId.
+ // ITEM_WALLET_GIANT is the base offset such that (ITEM_WALLET_GIANT + QUEST_SONG_*) == ITEM_SONG_*
+ // due to the enum layout. Lullaby Intro is non-contiguous and must be handled separately.
+ ItemId songItem;
+ if (cursorPoint == QUEST_SONG_LULLABY && !CHECK_QUEST_ITEM(QUEST_SONG_LULLABY) &&
+ CHECK_QUEST_ITEM(QUEST_SONG_LULLABY_INTRO)) {
+ songItem = ITEM_SONG_LULLABY_INTRO;
+ } else {
+ songItem = (ItemId)(ITEM_WALLET_GIANT + cursorPoint);
+ }
+ s32 targetBtn = -1;
+ bool isDpad = false;
+
+ static const u16 sEquipButtons[][4] = { { BTN_CLEFT, BTN_CDOWN, BTN_CRIGHT, 0 },
+ { BTN_DRIGHT, BTN_DLEFT, BTN_DDOWN, BTN_DUP } };
+
+ ForEachEquipSlot([&](s32 i, bool isDpadBtn) {
+ if (targetBtn < 0 &&
+ CHECK_BTN_ALL(input->press.button, sEquipButtons[isDpadBtn][isDpadBtn ? i : i - EQUIP_SLOT_C_LEFT])) {
+ targetBtn = i;
+ isDpad = isDpadBtn;
+ }
+ });
+
+ if (targetBtn < 0) {
+ return;
+ }
+
+ ItemId targetItem = GetButtonItem(targetBtn, isDpad);
+ s32 targetSlot = GetButtonSlot(targetBtn, isDpad);
+
+ // Clear this song from any other button to prevent duplicates (and swap if needed)
+ // The actual swap is deferred until the vanilla equip animation finishes (PAUSE_MAIN_STATE_IDLE)
+ ForEachEquipSlot([&](s32 i, bool isDpadBtn) {
+ if (i == targetBtn && isDpadBtn == isDpad)
+ return;
+ if (GetButtonItem(i, isDpadBtn) == songItem) {
+ sPendingSongSwap = { i, isDpadBtn, targetItem, targetSlot };
+ targetItem = ITEM_NONE;
+ targetSlot = SLOT_NONE;
+ }
+ });
+
+ // Set up the animation for KaleidoScope_UpdateItemEquip
+ pauseCtx->equipTargetItem = songItem;
+ // Use a unique fake slot so UpdateItemEquip doesn't accidentally swap empty buttons
+ pauseCtx->equipTargetSlot = 100 + (cursorPoint - QUEST_SONG_SONATA);
+
+ // EQUIP_SLOT_C -> PAUSE_EQUIP_C (-1), EQUIP_SLOT_D -> PAUSE_EQUIP_D (+3)
+ pauseCtx->equipTargetCBtn = targetBtn + (isDpad ? 3 : -1);
+
+ pauseCtx->mainState = PAUSE_MAIN_STATE_EQUIP_ITEM;
+ pauseCtx->equipAnimX = pauseCtx->questVtx[cursorPoint * 4].v.ob[0] * 10;
+ pauseCtx->equipAnimY = pauseCtx->questVtx[cursorPoint * 4].v.ob[1] * 10;
+ pauseCtx->equipAnimAlpha = 255;
+ sEquipState = 3; // EQUIP_STATE_MOVE_TO_C_BTN
+ sEquipAnimTimer = 10;
+ pauseCtx->equipAnimScale = 320;
+ pauseCtx->equipAnimShrinkRate = 40;
+
+ Audio_PlaySfx(NA_SE_SY_DECIDE);
+}
+
+static void RegisterSongItems() {
+ InitSongIcons();
+
+ COND_HOOK(OnKaleidoUpdate, CVAR, [](PauseContext* pauseCtx) {
+ HandleSongEquip(pauseCtx);
+ if (sPendingSongSwap.btn != -1 && pauseCtx->mainState == PAUSE_MAIN_STATE_IDLE) {
+ SetButtonItem(gPlayState, sPendingSongSwap.btn, sPendingSongSwap.isDpad, sPendingSongSwap.item,
+ sPendingSongSwap.slot);
+ sPendingSongSwap.btn = -1;
+ }
+ });
+
+ COND_HOOK(BeforeKaleidoDrawPage, CVAR, [](PauseContext* pauseCtx, u16 pauseIndex) {
+ if (pauseCtx->state != PAUSE_STATE_MAIN || pauseIndex != PAUSE_QUEST) {
+ return;
+ }
+
+ PlayState* play = gPlayState;
+ bool hasSongEquipped = false;
+
+ ForEachEquipSlot([&](s32 i, bool isDpadBtn) {
+ if (IsSongItem(GetButtonItem(i, isDpadBtn))) {
+ hasSongEquipped = true;
+ }
+ });
+
+ if (!hasSongEquipped) {
+ return;
+ }
+
+ OPEN_DISPS(play->state.gfxCtx);
+ Gfx_SetupDL42_Opa(play->state.gfxCtx);
+
+ // Use a custom combine mode to blend the dark translucent interior of the texture
+ // to a yellowish-brown color matching the Quest Status background (180, 180, 120),
+ // while keeping the pure white outline intact.
+ gDPSetCombineLERP(POLY_OPA_DISP++, PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0,
+ PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0);
+
+ gDPSetEnvColor(POLY_OPA_DISP++, 60, 70, 40, 0);
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, pauseCtx->alpha);
+
+ auto drawBorder = [&](ItemId item) {
+ if (!IsSongItem(item))
+ return;
+ s32 questIndex = (item == ITEM_SONG_LULLABY_INTRO) ? QUEST_SONG_LULLABY : (s32)(item - ITEM_WALLET_GIANT);
+ s32 vtxIndex = questIndex * 4;
+
+ Vtx* nvtx = (Vtx*)GRAPH_ALLOC(play->state.gfxCtx, 4 * sizeof(Vtx));
+ for (int v = 0; v < 4; v++) {
+ nvtx[v] = pauseCtx->questVtx[vtxIndex + v];
+ }
+
+ // By default, width is 16 and height is 24. Expanding width by 4 on each side natively aligns to 24x24
+ // border
+ for (int v = 0; v < 4; v++) {
+ nvtx[v].v.ob[0] += (v & 1) ? 4 : -4;
+ nvtx[v].v.ob[1] += 1;
+ nvtx[v].v.tc[0] = (v & 1) ? (32 << 5) : 0;
+ nvtx[v].v.tc[1] = (v & 2) ? (32 << 5) : 0;
+ }
+
+ gSPVertex(POLY_OPA_DISP++, (uintptr_t)nvtx, 4, 0);
+ POLY_OPA_DISP = Gfx_DrawTexQuadIA8(POLY_OPA_DISP, (TexturePtr)gEquippedItemOutlineTex, 32, 32, 0);
+ };
+
+ ForEachEquipSlot([&](s32 i, bool isDpadBtn) { drawBorder(GetButtonItem(i, isDpadBtn)); });
+
+ CLOSE_DISPS(play->state.gfxCtx);
+ });
+
+ COND_VB_SHOULD(VB_KALEIDO_SWITCH_PAGE_WITH_DPAD, CVAR, {
+ u16 button = va_arg(args, int);
+ Input* input = &gPlayState->state.input[0];
+
+ if (CVarGetInteger("gEnhancements.Dpad.DpadEquips", 0) && CHECK_BTN_ALL(input->cur.button, button)) {
+ PauseContext* pauseCtx = &gPlayState->pauseCtx;
+
+ if (pauseCtx->pageIndex == PAUSE_QUEST && pauseCtx->mainState == PAUSE_MAIN_STATE_IDLE_CURSOR_ON_SONG &&
+ CHECK_QUEST_ITEM(pauseCtx->cursorPoint[PAUSE_QUEST])) {
+ *should = false;
+ }
+ }
+ });
+
+ COND_VB_SHOULD(VB_KALEIDO_DRAW_EQUIP_ANIM_ICON, CVAR, {
+ ItemId* equipAnimDrawItem = va_arg(args, ItemId*);
+ if (IsSongItem(*equipAnimDrawItem)) {
+ TexturePtr tex = GetSongIconTexture(*equipAnimDrawItem);
+ OPEN_DISPS(gPlayState->state.gfxCtx);
+ gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, gPlayState->pauseCtx.equipAnimAlpha);
+ gSPVertex(OVERLAY_DISP++, (uintptr_t)&gPlayState->pauseCtx.cursorVtx[16], 4, 0);
+ gDPLoadTextureBlock(OVERLAY_DISP++, tex, G_IM_FMT_RGBA, G_IM_SIZ_32b, 32, 32, 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);
+ gSP1Quadrangle(OVERLAY_DISP++, 0, 2, 3, 1, 0);
+ CLOSE_DISPS(gPlayState->state.gfxCtx);
+ *should = false;
+ }
+ });
+
+ COND_VB_SHOULD(VB_DRAW_ITEM_EQUIPPED_OUTLINE, CVAR, {
+ ItemId* equipAnimDrawItem = va_arg(args, ItemId*);
+ if (IsSongItem(*equipAnimDrawItem)) {
+ *should = false;
+ }
+ });
+
+ COND_VB_SHOULD(VB_INTERFACE_LOAD_ITEM_ICON, CVAR, {
+ u8 btn = va_arg(args, int);
+ ItemId item = GetButtonItem(btn, false);
+ if (IsSongItem(item)) {
+ SetIconItemSegment(gPlayState, btn, false, GetSongIconTexture(item));
+ *should = false;
+ }
+ });
+
+ COND_VB_SHOULD(VB_INTERFACE_LOAD_DPAD_ITEM_ICON, CVAR, {
+ u8 btn = va_arg(args, int);
+ ItemId item = GetButtonItem(btn, true);
+ if (IsSongItem(item)) {
+ SetIconItemSegment(gPlayState, btn, true, GetSongIconTexture(item));
+ *should = false;
+ }
+ });
+
+ COND_VB_SHOULD(VB_ITEM_BE_RESTRICTED, CVAR, {
+ ItemId* itemId = va_arg(args, ItemId*);
+ if (IsSongItem(*itemId)) {
+ *should = false;
+ }
+ });
+
+ COND_VB_SHOULD(VB_GET_ITEM_ON_BUTTON, CVAR, {
+ EquipSlot slot = (EquipSlot)va_arg(args, int);
+ ItemId* item = va_arg(args, ItemId*);
+
+ if (IsSongItem(*item)) {
+ if (INV_CONTENT(ITEM_OCARINA_OF_TIME) != ITEM_OCARINA_OF_TIME) {
+ if (!sOcarinaErrorPlayed) {
+ Audio_PlaySfx(NA_SE_SY_OCARINA_ERROR);
+ sOcarinaErrorPlayed = true;
+ }
+ *item = ITEM_NONE;
+ return;
+ }
+ sPendingAutoPlaySong = SongItemToOcarinaId(*item);
+ sHideOcarinaStaff = true;
+ *item = ITEM_OCARINA_OF_TIME;
+ }
+ });
+
+ COND_VB_SHOULD(VB_OVERRIDE_OCARINA_STAFF_STATE, CVAR, {
+ if (sPendingAutoPlaySong != 0xFF) {
+ OcarinaStaff* staff = va_arg(args, OcarinaStaff*);
+ staff->state = sPendingAutoPlaySong;
+ sPendingAutoPlaySong = 0xFF;
+ Message_ResetOcarinaButtonAlphas();
+ *should = true;
+ }
+ });
+
+ COND_VB_SHOULD(VB_DRAW_OCARINA_STAFF, CVAR, {
+ if (sHideOcarinaStaff) {
+ *should = false;
+ }
+ });
+
+ COND_HOOK(OnSceneInit, CVAR, [](s8 sceneId, s8 spawnNum) {
+ sPendingAutoPlaySong = 0xFF;
+ sHideOcarinaStaff = false;
+ sOcarinaErrorPlayed = false;
+ sPendingSongSwap.btn = -1;
+ });
+
+ COND_HOOK(OnGameStateUpdate, CVAR, []() {
+ if (gPlayState == nullptr) {
+ return;
+ }
+
+ // Reset error sound flag when no item buttons are held
+ Input* input = &gPlayState->state.input[0];
+ if (!CHECK_BTN_ANY(input->cur.button,
+ BTN_CLEFT | BTN_CDOWN | BTN_CRIGHT | BTN_DRIGHT | BTN_DLEFT | BTN_DDOWN | BTN_DUP)) {
+ sOcarinaErrorPlayed = false;
+ }
+
+ // Maintain colored song icons on HUD buttons across scene transitions / form changes
+ ForEachEquipSlot([&](s32 i, bool isDpadBtn) {
+ ItemId item = GetButtonItem(i, isDpadBtn);
+ if (!IsSongItem(item)) {
+ return;
+ }
+
+ // Auto-upgrade Lullaby Intro to full Lullaby when the player learns it
+ if (item == ITEM_SONG_LULLABY_INTRO && CHECK_QUEST_ITEM(QUEST_SONG_LULLABY)) {
+ item = ITEM_SONG_LULLABY;
+ SetButtonItem(gPlayState, i, isDpadBtn, item, GetButtonSlot(i, isDpadBtn));
+ }
+
+ SetIconItemSegment(gPlayState, i, isDpadBtn, GetSongIconTexture(item));
+ });
+
+ // Hide ocarina staff during auto-play
+ if (!sHideOcarinaStaff || sPendingAutoPlaySong != 0xFF) {
+ return;
+ }
+
+ u8 mode = gPlayState->msgCtx.msgMode;
+ if (mode < MSGMODE_OCARINA_STARTING || mode > MSGMODE_40) {
+ sHideOcarinaStaff = false;
+ }
+ });
+}
+
+static RegisterShipInitFunc initFunc(RegisterSongItems, { CVAR_NAME });
diff --git a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h
index db0a8877b..ad46a61a2 100644
--- a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h
+++ b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h
@@ -545,6 +545,14 @@ typedef enum {
// true
// ```
// #### `args`
+ // - None
+ VB_DRAW_OCARINA_STAFF,
+
+ // #### `result`
+ // ```c
+ // true
+ // ```
+ // #### `args`
// - `*EnSlime`
VB_DRAW_SLIME_BODY_ITEM,
@@ -1110,6 +1118,22 @@ typedef enum {
// #### `result`
// ```c
+ // true
+ // ```
+ // #### `args`
+ // - `u8` (btn)
+ VB_INTERFACE_LOAD_DPAD_ITEM_ICON,
+
+ // #### `result`
+ // ```c
+ // true
+ // ```
+ // #### `args`
+ // - `u8` (btn)
+ VB_INTERFACE_LOAD_ITEM_ICON,
+
+ // #### `result`
+ // ```c
// !gPlayerFormItemRestrictions[GET_PLAYER_FORM][itemId]
// ```
// #### `args`
@@ -1164,6 +1188,14 @@ typedef enum {
// true
// ```
// #### `args`
+ // - `ItemId*` (draw item, mutable)
+ VB_KALEIDO_DRAW_EQUIP_ANIM_ICON,
+
+ // #### `result`
+ // ```c
+ // true
+ // ```
+ // #### `args`
// - `u16` (cursor slot)
// - `u16` (cursor item)
VB_KALEIDO_EQUIP_ITEM_TO_BUTTON,
@@ -1455,6 +1487,14 @@ typedef enum {
// #### `result`
// ```c
+ // false
+ // ```
+ // #### `args`
+ // - `*OcarinaStaff` (the currently playing staff; state can be overridden to force a song)
+ VB_OVERRIDE_OCARINA_STAFF_STATE,
+
+ // #### `result`
+ // ```c
// true
// ```
// #### `args`
diff --git a/mm/src/code/z_message.c b/mm/src/code/z_message.c
index 04efc6f8c..1e0a6eed6 100644
--- a/mm/src/code/z_message.c
+++ b/mm/src/code/z_message.c
@@ -4298,6 +4298,10 @@ void Message_DrawOcarinaButtons(PlayState* play, Gfx** gfxP) {
}
void Message_DrawText(PlayState* play, Gfx** gfxP) {
+ if (!GameInteractor_Should(VB_DRAW_OCARINA_STAFF, true)) {
+ return;
+ }
+
if ((gSaveContext.options.language == LANGUAGE_JPN) && !play->msgCtx.textIsCredits) {
Message_DrawTextDefault(play, gfxP);
} else if (play->msgCtx.textIsCredits) {
@@ -4424,7 +4428,9 @@ void Message_DrawMain(PlayState* play, Gfx** gfxP) {
(((msgCtx->msgMode >= MSGMODE_TEXT_BOX_GROWING) && (msgCtx->msgMode <= MSGMODE_TEXT_DONE)) ||
((msgCtx->msgMode >= MSGMODE_NEW_CYCLE_0) && (msgCtx->msgMode <= MSGMODE_OWL_SAVE_2))) &&
(D_801CFC78[msgCtx->textBoxType] != 0xE)) {
- Message_DrawTextBox(play, &gfx);
+ if (GameInteractor_Should(VB_DRAW_OCARINA_STAFF, true)) {
+ Message_DrawTextBox(play, &gfx);
+ }
}
}
}
@@ -4495,8 +4501,10 @@ void Message_DrawMain(PlayState* play, Gfx** gfxP) {
case MSGMODE_TEXT_AWAIT_INPUT:
case MSGMODE_TEXT_AWAIT_NEXT:
Message_DrawText(play, &gfx);
- Message_DrawTextboxIcon(play, &gfx, 158,
- (s16)(D_801D03A8[msgCtx->textBoxType] + msgCtx->textboxYTarget));
+ if (GameInteractor_Should(VB_DRAW_OCARINA_STAFF, true)) {
+ Message_DrawTextboxIcon(play, &gfx, 158,
+ (s16)(D_801D03A8[msgCtx->textBoxType] + msgCtx->textboxYTarget));
+ }
break;
case MSGMODE_OCARINA_STARTING:
@@ -4611,6 +4619,8 @@ void Message_DrawMain(PlayState* play, Gfx** gfxP) {
msgCtx->songPlayed = msgCtx->ocarinaStaff->state;
+ GameInteractor_Should(VB_OVERRIDE_OCARINA_STAFF_STATE, false, msgCtx->ocarinaStaff);
+
bool vanillaOwnedSongCheck = (msgCtx->ocarinaStaff->state == OCARINA_SONG_SCARECROW_SPAWN) ||
(msgCtx->ocarinaStaff->state == OCARINA_SONG_INVERTED_TIME) ||
(msgCtx->ocarinaStaff->state == OCARINA_SONG_DOUBLE_TIME) ||
@@ -5317,13 +5327,17 @@ void Message_DrawMain(PlayState* play, Gfx** gfxP) {
case TEXTBOX_ENDTYPE_INPUT_BANK:
case TEXTBOX_ENDTYPE_INPUT_DOGGY_RACETRACK_BET:
default:
- Message_DrawTextboxIcon(play, &gfx, 158,
- (s16)(D_801D03A8[msgCtx->textBoxType] + msgCtx->textboxYTarget));
+ if (GameInteractor_Should(VB_DRAW_OCARINA_STAFF, true)) {
+ Message_DrawTextboxIcon(play, &gfx, 158,
+ (s16)(D_801D03A8[msgCtx->textBoxType] + msgCtx->textboxYTarget));
+ }
break;
case TEXTBOX_ENDTYPE_EVENT2:
- Message_DrawTextboxIcon(play, &gfx, 158,
- (s16)(D_801D03A8[msgCtx->textBoxType] + msgCtx->textboxYTarget));
+ if (GameInteractor_Should(VB_DRAW_OCARINA_STAFF, true)) {
+ Message_DrawTextboxIcon(play, &gfx, 158,
+ (s16)(D_801D03A8[msgCtx->textBoxType] + msgCtx->textboxYTarget));
+ }
break;
}
break;
@@ -5354,7 +5368,10 @@ void Message_DrawMain(PlayState* play, Gfx** gfxP) {
break;
}
}
- Message_DrawOcarinaButtons(play, &gfx);
+ if (GameInteractor_Should(VB_DRAW_OCARINA_STAFF, true)) {
+ Message_DrawOcarinaButtons(play, &gfx);
+ }
+
*gfxP = gfx;
}
diff --git a/mm/src/code/z_parameter.c b/mm/src/code/z_parameter.c
index 534d9bb33..8cd3ea70c 100644
--- a/mm/src/code/z_parameter.c
+++ b/mm/src/code/z_parameter.c
@@ -4028,6 +4028,10 @@ void Interface_InitMinigame(PlayState* play) {
void Interface_Dpad_LoadItemIconImpl(PlayState* play, u8 btn) {
InterfaceContext* interfaceCtx = &play->interfaceCtx;
+ if (!GameInteractor_Should(VB_INTERFACE_LOAD_DPAD_ITEM_ICON, true, btn)) {
+ return;
+ }
+
if (DPAD_GET_CUR_FORM_BTN_ITEM(btn) < ARRAY_COUNT(gItemIcons)) {
interfaceCtx->iconItemSegment[DPAD_BUTTON(btn) + EQUIP_SLOT_MAX] = gItemIcons[DPAD_GET_CUR_FORM_BTN_ITEM(btn)];
} else {
@@ -4065,6 +4069,10 @@ void Interface_DrawAutosaveIcon(PlayState* play, uint16_t opacity) {
void Interface_LoadItemIconImpl(PlayState* play, u8 btn) {
InterfaceContext* interfaceCtx = &play->interfaceCtx;
+ if (!GameInteractor_Should(VB_INTERFACE_LOAD_ITEM_ICON, true, btn)) {
+ return;
+ }
+
// #region 2S2H [Port]
// CmpDma_LoadFile(SEGMENT_ROM_START(icon_item_static_yar), GET_CUR_FORM_BTN_ITEM(btn),
// &interfaceCtx->iconItemSegment[(u32)btn * ICON_ITEM_TEX_SIZE], ICON_ITEM_TEX_SIZE);
@@ -6684,38 +6692,42 @@ void Interface_DrawPauseMenuEquippingIcons(PlayState* play) {
pauseCtx->cursorVtx[18].v.ob[1] = pauseCtx->cursorVtx[19].v.ob[1] =
pauseCtx->cursorVtx[16].v.ob[1] - (pauseCtx->equipAnimScale / 10);
- if (pauseCtx->equipTargetItem < 0xB5) {
- // Normal Equip (icon goes from the inventory slot to the C button when equipping it)
- gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, pauseCtx->equipAnimAlpha);
- gSPVertex(OVERLAY_DISP++, &pauseCtx->cursorVtx[16], 4, 0);
- gDPLoadTextureBlock(OVERLAY_DISP++, gItemIcons[pauseCtx->equipTargetItem], G_IM_FMT_RGBA, G_IM_SIZ_32b,
- ICON_ITEM_TEX_WIDTH, ICON_ITEM_TEX_HEIGHT, 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);
- } else {
- // Magic Arrow Equip Effect
- temp = pauseCtx->equipTargetItem - 0xB5;
- gDPSetPrimColor(OVERLAY_DISP++, 0, 0, sMagicArrowEffectsR[temp], sMagicArrowEffectsG[temp],
- sMagicArrowEffectsB[temp], pauseCtx->equipAnimAlpha);
-
- if ((pauseCtx->equipAnimAlpha > 0) && (pauseCtx->equipAnimAlpha < 255)) {
- temp = (pauseCtx->equipAnimAlpha / 8) / 2;
- pauseCtx->cursorVtx[16].v.ob[0] = pauseCtx->cursorVtx[18].v.ob[0] =
- pauseCtx->cursorVtx[16].v.ob[0] - temp;
- pauseCtx->cursorVtx[17].v.ob[0] = pauseCtx->cursorVtx[19].v.ob[0] =
- pauseCtx->cursorVtx[16].v.ob[0] + temp * 2 + 32;
- pauseCtx->cursorVtx[16].v.ob[1] = pauseCtx->cursorVtx[17].v.ob[1] =
- pauseCtx->cursorVtx[16].v.ob[1] + temp;
- pauseCtx->cursorVtx[18].v.ob[1] = pauseCtx->cursorVtx[19].v.ob[1] =
- pauseCtx->cursorVtx[16].v.ob[1] - temp * 2 - 32;
- }
-
- gSPVertex(OVERLAY_DISP++, &pauseCtx->cursorVtx[16], 4, 0);
- gDPLoadTextureBlock(OVERLAY_DISP++, gMagicArrowEquipEffectTex, G_IM_FMT_IA, G_IM_SIZ_8b, 32, 32, 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);
- }
+ ItemId equipAnimDrawItem = pauseCtx->equipTargetItem;
+ if (GameInteractor_Should(VB_KALEIDO_DRAW_EQUIP_ANIM_ICON, true, &equipAnimDrawItem)) {
- gSP1Quadrangle(OVERLAY_DISP++, 0, 2, 3, 1, 0);
+ if (pauseCtx->equipTargetItem < 0xB5) {
+ // Normal Equip (icon goes from the inventory slot to the C button when equipping it)
+ gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, pauseCtx->equipAnimAlpha);
+ gSPVertex(OVERLAY_DISP++, &pauseCtx->cursorVtx[16], 4, 0);
+ gDPLoadTextureBlock(OVERLAY_DISP++, gItemIcons[equipAnimDrawItem], G_IM_FMT_RGBA, G_IM_SIZ_32b,
+ ICON_ITEM_TEX_WIDTH, ICON_ITEM_TEX_HEIGHT, 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);
+ } else {
+ // Magic Arrow Equip Effect
+ temp = pauseCtx->equipTargetItem - 0xB5;
+ gDPSetPrimColor(OVERLAY_DISP++, 0, 0, sMagicArrowEffectsR[temp], sMagicArrowEffectsG[temp],
+ sMagicArrowEffectsB[temp], pauseCtx->equipAnimAlpha);
+
+ if ((pauseCtx->equipAnimAlpha > 0) && (pauseCtx->equipAnimAlpha < 255)) {
+ temp = (pauseCtx->equipAnimAlpha / 8) / 2;
+ pauseCtx->cursorVtx[16].v.ob[0] = pauseCtx->cursorVtx[18].v.ob[0] =
+ pauseCtx->cursorVtx[16].v.ob[0] - temp;
+ pauseCtx->cursorVtx[17].v.ob[0] = pauseCtx->cursorVtx[19].v.ob[0] =
+ pauseCtx->cursorVtx[16].v.ob[0] + temp * 2 + 32;
+ pauseCtx->cursorVtx[16].v.ob[1] = pauseCtx->cursorVtx[17].v.ob[1] =
+ pauseCtx->cursorVtx[16].v.ob[1] + temp;
+ pauseCtx->cursorVtx[18].v.ob[1] = pauseCtx->cursorVtx[19].v.ob[1] =
+ pauseCtx->cursorVtx[16].v.ob[1] - temp * 2 - 32;
+ }
+
+ gSPVertex(OVERLAY_DISP++, &pauseCtx->cursorVtx[16], 4, 0);
+ gDPLoadTextureBlock(OVERLAY_DISP++, gMagicArrowEquipEffectTex, G_IM_FMT_IA, G_IM_SIZ_8b, 32, 32, 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);
+ }
+
+ gSP1Quadrangle(OVERLAY_DISP++, 0, 2, 3, 1, 0);
+ }
}
CLOSE_DISPS(play->state.gfxCtx);
diff --git a/mm/src/code/z_player_lib.c b/mm/src/code/z_player_lib.c
index 5ba73fc3f..3ab319529 100644
--- a/mm/src/code/z_player_lib.c
+++ b/mm/src/code/z_player_lib.c
@@ -760,7 +760,9 @@ ItemId Player_Dpad_GetItemOnButton(PlayState* play, Player* player, DpadEquipSlo
return ITEM_NONE;
}
- return DPAD_BTN_ITEM(slot);
+ ItemId item = DPAD_BTN_ITEM(slot);
+ GameInteractor_Should(VB_GET_ITEM_ON_BUTTON, item, slot, &item);
+ return item;
}
// #endregion
@@ -797,16 +799,23 @@ ItemId Player_GetItemOnButton(PlayState* play, Player* player, EquipSlot slot) {
}
if (slot == EQUIP_SLOT_C_LEFT) {
- return C_BTN_ITEM(EQUIP_SLOT_C_LEFT);
+ ItemId item = C_BTN_ITEM(EQUIP_SLOT_C_LEFT);
+ GameInteractor_Should(VB_GET_ITEM_ON_BUTTON, item, slot, &item);
+ return item;
}
if (slot == EQUIP_SLOT_C_DOWN) {
- return C_BTN_ITEM(EQUIP_SLOT_C_DOWN);
+ ItemId item = C_BTN_ITEM(EQUIP_SLOT_C_DOWN);
+ GameInteractor_Should(VB_GET_ITEM_ON_BUTTON, item, slot, &item);
+ return item;
}
// EQUIP_SLOT_C_RIGHT
-
- return C_BTN_ITEM(EQUIP_SLOT_C_RIGHT);
+ {
+ ItemId item = C_BTN_ITEM(EQUIP_SLOT_C_RIGHT);
+ GameInteractor_Should(VB_GET_ITEM_ON_BUTTON, item, slot, &item);
+ return item;
+ }
}
// #region 2S2H [Dpad]