diff options
| author | mckinlee <mckinlee@ymail.com> | 2026-03-19 21:41:24 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-19 21:41:24 -0400 |
| commit | cfd5cc483ae2da52ccc8e00082ef2f8e3d969863 (patch) | |
| tree | 7f949e637b9627975e6e08bfdec8bbfb32df5565 | |
| parent | f7ccda83aab44cf532798aba306b2cf12a3ea8fb (diff) | |
[Enhancement] Song Items (#1566)
* equip songs
* feedback
| -rw-r--r-- | mm/2s2h/BenGui/BenMenu.cpp | 4 | ||||
| -rw-r--r-- | mm/2s2h/Enhancements/Songs/SongItems.cpp | 522 | ||||
| -rw-r--r-- | mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h | 40 | ||||
| -rw-r--r-- | mm/src/code/z_message.c | 33 | ||||
| -rw-r--r-- | mm/src/code/z_parameter.c | 74 | ||||
| -rw-r--r-- | mm/src/code/z_player_lib.c | 19 |
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] |
