diff --git a/mm/2s2h/CustomMessage/CustomMessage.cpp b/mm/2s2h/CustomMessage/CustomMessage.cpp index ff3a161528..3bc6c5f1fe 100644 --- a/mm/2s2h/CustomMessage/CustomMessage.cpp +++ b/mm/2s2h/CustomMessage/CustomMessage.cpp @@ -65,37 +65,70 @@ void CustomMessage::AddLineBreaks(std::string* msg) { for (size_t i = 0; i < msg->size(); ++i) { char currentChar = (*msg)[i]; - if ((uint8_t)currentChar >= 0x20 && (uint8_t)currentChar < 0x20 + ARRAY_COUNTU(sNESFontWidths)) { - currentLineWidth += sNESFontWidths[(uint8_t)currentChar - 0x20]; - } - - // Increment for existing new liens + // Handle existing newlines if (currentChar == 0x11) { currentLineWidth = 0.0f; lastSpaceIndex = std::string::npos; ++currentLineCount; + continue; + } + + // Handle existing box breaks + if (currentChar == 0x10) { + currentLineWidth = 0.0f; + lastSpaceIndex = std::string::npos; + currentLineCount = 0; + continue; } + // Track spaces for word wrapping if (currentChar == ' ') { lastSpaceIndex = i; } - if (currentLineWidth > MAX_TEXTBOX_WIDTH) { + + // Calculate width for printable characters + float charWidth = 0.0f; + if ((uint8_t)currentChar >= 0x20 && (uint8_t)currentChar < 0x20 + ARRAY_COUNTU(sNESFontWidths)) { + charWidth = sNESFontWidths[(uint8_t)currentChar - 0x20]; + } + + // Check if adding this character would exceed width + if (currentLineWidth + charWidth > MAX_TEXTBOX_WIDTH) { if (lastSpaceIndex != std::string::npos) { + // Break at the last space (*msg)[lastSpaceIndex] = 0x11; - i = lastSpaceIndex; + + // Recalculate width from after the space to current position + currentLineWidth = 0.0f; + for (size_t j = lastSpaceIndex + 1; j < i; ++j) { + char c = (*msg)[j]; + if ((uint8_t)c >= 0x20 && (uint8_t)c < 0x20 + ARRAY_COUNTU(sNESFontWidths)) { + currentLineWidth += sNESFontWidths[(uint8_t)c - 0x20]; + } + } + // Add current character's width + currentLineWidth += charWidth; + + lastSpaceIndex = std::string::npos; } else { + // No space found, force break before current character msg->insert(i, 1, 0x11); + currentLineWidth = charWidth; // Current char is now first on new line + ++i; // Skip the inserted newline character } - currentLineWidth = 0.0f; - lastSpaceIndex = std::string::npos; ++currentLineCount; if (currentLineCount >= MAX_LINES_PER_PAGE) { - // Replace the added new line for a box break instead - (*msg)[i] = 0x10; + // Find the newline we just added and replace it with box break + size_t newlinePos = (lastSpaceIndex != std::string::npos) ? lastSpaceIndex : i - 1; + (*msg)[newlinePos] = 0x10; currentLineCount = 0; + lastSpaceIndex = std::string::npos; } + } else { + // Character fits on current line + currentLineWidth += charWidth; } } } diff --git a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp index 59e9045ba9..285c935b49 100644 --- a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp +++ b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp @@ -288,6 +288,7 @@ void LoadAvailableWindows() { { TRACKER_ITEM_RANDO, RI_SONG_DOUBLE_TIME }, { TRACKER_ITEM_RANDO, RI_SONG_INVERTED_TIME }, { TRACKER_ITEM_RANDO, RI_SONG_SUN }, + { TRACKER_ITEM_RANDO, RI_SONG_SARIA }, }, }); diff --git a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h index 6dbb8e53e1..a1c657d1be 100644 --- a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h +++ b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h @@ -1257,6 +1257,22 @@ typedef enum { // - `*EnMnk` VB_MONKEY_WAIT_TO_TALK_AFTER_APPROACH, + // #### `result` + // ```c + // false + // ``` + // #### `args` + // - None + VB_MSG_CAPTURE_MSGMODE_TEXT_DONE, + + // #### `result` + // ```c + // false + // ``` + // #### `args` + // - None + VB_MSG_CAPTURE_MSGMODE_TEXT_CLOSING_OCARINA_ACTION, + // #### `result` // ```c // true diff --git a/mm/2s2h/Rando/DrawItem.cpp b/mm/2s2h/Rando/DrawItem.cpp index 6328f81722..c4592e4645 100644 --- a/mm/2s2h/Rando/DrawItem.cpp +++ b/mm/2s2h/Rando/DrawItem.cpp @@ -101,6 +101,7 @@ void DrawSong(RandoItemId randoItemId) { case RI_SONG_STORMS: gDPSetEnvColor(POLY_XLU_DISP++, 146, 146, 146, 255); break; + case RI_SONG_SARIA: case RI_SONG_SONATA: gDPSetEnvColor(POLY_XLU_DISP++, 98, 255, 98, 255); break; @@ -488,6 +489,7 @@ void Rando::DrawItem(RandoItemId randoItemId, RandoCheckId randoCheckId, Actor* case RI_SONG_STORMS: case RI_SONG_SUN: case RI_SONG_HEALING: + case RI_SONG_SARIA: case RI_SONG_SOARING: case RI_SONG_SONATA: case RI_SONG_ELEGY: diff --git a/mm/2s2h/Rando/Logic/GeneratePools.cpp b/mm/2s2h/Rando/Logic/GeneratePools.cpp index f90ed3e2aa..a08738d242 100644 --- a/mm/2s2h/Rando/Logic/GeneratePools.cpp +++ b/mm/2s2h/Rando/Logic/GeneratePools.cpp @@ -217,6 +217,9 @@ void GeneratePools(RandoSaveInfo& saveInfo, std::vector& checkPool if (saveInfo.randoSaveOptions[RO_SHUFFLE_SONG_INVERTED_TIME] == RO_GENERIC_YES) { itemPool.push_back(RI_SONG_INVERTED_TIME); } + if (saveInfo.randoSaveOptions[RO_SHUFFLE_SONG_SARIA] == RO_GENERIC_YES) { + itemPool.push_back(RI_SONG_SARIA); + } // Shuffle Triforce Pieces into the Pool if (saveInfo.randoSaveOptions[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) { diff --git a/mm/2s2h/Rando/Menu.cpp b/mm/2s2h/Rando/Menu.cpp index 1fa5865bf6..50bd2921f7 100644 --- a/mm/2s2h/Rando/Menu.cpp +++ b/mm/2s2h/Rando/Menu.cpp @@ -234,6 +234,7 @@ static RegisterShipInitFunc refreshMetricsInit(RefreshMetrics, { "gRando.Options.RO_SHUFFLE_SNOWBALL_DROPS", "gRando.Options.RO_SHUFFLE_SONG_DOUBLE_TIME", "gRando.Options.RO_SHUFFLE_SONG_INVERTED_TIME", + "gRando.Options.RO_SHUFFLE_SONG_SARIA", "gRando.Options.RO_SHUFFLE_SONG_SUN", "gRando.Options.RO_SHUFFLE_SWIM", "gRando.Options.RO_SHUFFLE_TINGLE_SHOPS", @@ -439,6 +440,12 @@ static void DrawItemsTab() { CVarCheckbox("Song of Double Time", Rando::StaticData::Options[RO_SHUFFLE_SONG_DOUBLE_TIME].cvar); CVarCheckbox("Inverted Song of Time", Rando::StaticData::Options[RO_SHUFFLE_SONG_INVERTED_TIME].cvar); CVarCheckbox("Sun's Song", Rando::StaticData::Options[RO_SHUFFLE_SONG_SUN].cvar); + CVarCheckbox( + "Saria's Song", Rando::StaticData::Options[RO_SHUFFLE_SONG_SARIA].cvar, + CheckboxOptions( + { { .tooltip = "Adds Saria's Song to the item pool, playing it will give you a hint to a progressive item " + "that is reachable in logic, weighted towards things like bow, bomb, and transformation " + "masks. The song is one time use, you will lose it after using it." } })); CVarCheckbox("Deku Stick Bag", "gPlaceholderBool", CheckboxOptions({ { .disabled = true, .disabledTooltip = "Coming Soon" } })); CVarCheckbox("Deku Nut Bag", "gPlaceholderBool", diff --git a/mm/2s2h/Rando/MiscBehavior/MiscBehavior.cpp b/mm/2s2h/Rando/MiscBehavior/MiscBehavior.cpp index abef387215..fad880ca4b 100644 --- a/mm/2s2h/Rando/MiscBehavior/MiscBehavior.cpp +++ b/mm/2s2h/Rando/MiscBehavior/MiscBehavior.cpp @@ -16,6 +16,7 @@ void Rando::MiscBehavior::OnFileLoad() { Rando::MiscBehavior::CheckQueueReset(); Rando::MiscBehavior::InitKaleidoItemPage(); Rando::MiscBehavior::InitOfferGetItemBehavior(); + Rando::MiscBehavior::SariasSongHint(); COND_HOOK(OnFlagSet, IS_RANDO, Rando::MiscBehavior::OnFlagSet); COND_HOOK(OnSceneFlagSet, IS_RANDO, Rando::MiscBehavior::OnSceneFlagSet); diff --git a/mm/2s2h/Rando/MiscBehavior/MiscBehavior.h b/mm/2s2h/Rando/MiscBehavior/MiscBehavior.h index d0f93f4360..372dd1893b 100644 --- a/mm/2s2h/Rando/MiscBehavior/MiscBehavior.h +++ b/mm/2s2h/Rando/MiscBehavior/MiscBehavior.h @@ -22,6 +22,7 @@ void OnFlagSet(FlagType flagType, u32 flag); void OnSceneFlagSet(s16 sceneId, FlagType flagType, u32 flag); void OnSceneInit(s16 sceneId, s8 spawnNum); void OfferTrapItem(); +void SariasSongHint(); } // namespace MiscBehavior diff --git a/mm/2s2h/Rando/MiscBehavior/SariasSongHint.cpp b/mm/2s2h/Rando/MiscBehavior/SariasSongHint.cpp new file mode 100644 index 0000000000..bde8d62eda --- /dev/null +++ b/mm/2s2h/Rando/MiscBehavior/SariasSongHint.cpp @@ -0,0 +1,133 @@ +#include "2s2h/CustomMessage/CustomMessage.h" +#include "MiscBehavior.h" +#include "2s2h/ShipUtils.h" +#include +#include "2s2h/Rando/Logic/Logic.h" + +extern "C" { +#include +#include +s32 Message_ShouldAdvanceSilent(PlayState* play); +extern s16 sOcarinaSongFanfares[17]; +extern s16 sLastPlayedSong; +} + +static int playedSariasSongState = 0; + +std::set priorityItems = { + RI_BOW, RI_HOOKSHOT, RI_MASK_BLAST, RI_BOMB_BAG_20, RI_MASK_DEKU, RI_MASK_GORON, + RI_MASK_ZORA, RI_MASK_FIERCE_DEITY, RI_SONG_SONATA, RI_SONG_LULLABY, RI_SONG_NOVA, RI_SONG_SOARING, +}; + +RandoCheckId GetProgressiveCheckInLogic() { + std::unordered_map regionTimeStates = + Rando::Logic::InitializeRegionTimeStates(RR_MAX); + std::set reachableRegions = {}; + // Get connected entrances from starting & warp points + Rando::Logic::FindReachableRegions(RR_MAX, reachableRegions, regionTimeStates); + // Get connected regions from current entrance (TODO: Make this optional) + Rando::Logic::FindReachableRegions(Rando::Logic::GetRegionIdFromEntrance(gSaveContext.save.entrance), + reachableRegions, regionTimeStates); + + std::vector priorityChecks = {}; + std::vector otherChecks = {}; + + for (RandoRegionId regionId : reachableRegions) { + auto& randoRegion = Rando::Logic::Regions[regionId]; + for (auto& [randoCheckId, accessLogicFunc] : randoRegion.checks) { + if (accessLogicFunc.first() && RANDO_SAVE_CHECKS[randoCheckId].shuffled && + !RANDO_SAVE_CHECKS[randoCheckId].obtained) { + RandoItemId itemId = Rando::ConvertItem(RANDO_SAVE_CHECKS[randoCheckId].randoItemId, randoCheckId); + auto type = Rando::StaticData::Items[itemId].randoItemType; + + if (priorityItems.contains(itemId)) { + priorityChecks.push_back(randoCheckId); + } else if (type == RITYPE_MAJOR || type == RITYPE_MASK) { + otherChecks.push_back(randoCheckId); + } + } + } + } + + // First, we try to return a priority check if one is available, in order of the priority items list. + // If no priority checks are available, we return a random major/mask check. + return priorityChecks.empty() + ? (otherChecks.empty() ? RC_UNKNOWN : otherChecks[Ship_Random(0, otherChecks.size() - 1)]) + : priorityChecks[0]; +} + +void Rando::MiscBehavior::SariasSongHint() { + bool shouldRegister = IS_RANDO && RANDO_SAVE_OPTIONS[RO_SHUFFLE_SONG_SARIA]; + + // Fix vanilla issue where saria's song plays the majoras lair fanfare + if (shouldRegister) { + sOcarinaSongFanfares[OCARINA_SONG_SARIAS] = NA_BGM_SARIAS_SONG; + } else { + sOcarinaSongFanfares[OCARINA_SONG_SARIAS] = NA_BGM_MAJORAS_LAIR; + } + + COND_VB_SHOULD(VB_SONG_AVAILABLE_TO_PLAY, shouldRegister, { + uint8_t* songIndex = va_arg(args, uint8_t*); + // If the currently played song is Sun's Song, set it to be available to be played. + if (*songIndex == OCARINA_SONG_SARIAS && CHECK_QUEST_ITEM(QUEST_SONG_SARIA)) { + *should = true; + playedSariasSongState = 1; + } + }); + + COND_VB_SHOULD(VB_MSG_CAPTURE_MSGMODE_TEXT_DONE, shouldRegister, { + if (playedSariasSongState && gPlayState->msgCtx.ocarinaMode == OCARINA_MODE_PROCESS_RESTRICTED_SONG) { + *should = true; + + if (Message_ShouldAdvanceSilent(gPlayState)) { + if (gPlayState->msgCtx.choiceIndex == 0) { + playedSariasSongState = 2; + Audio_PlaySfx(NA_SE_SY_DECIDE); + Message_ContinueTextbox(gPlayState, 0x1B95); + } else { + playedSariasSongState = 0; + Audio_PlaySfx(NA_SE_SY_DECIDE); + Message_CloseTextbox(gPlayState); + gPlayState->msgCtx.ocarinaMode = OCARINA_MODE_END; + } + } + } + }); + + COND_VB_SHOULD(VB_MSG_CAPTURE_MSGMODE_TEXT_CLOSING_OCARINA_ACTION, shouldRegister, { + MessageContext* msgCtx = &gPlayState->msgCtx; + + if (sLastPlayedSong == OCARINA_SONG_SARIAS) { + *should = true; + Message_StartTextbox(gPlayState, 0x1B95, NULL); + gPlayState->msgCtx.ocarinaMode = OCARINA_MODE_PROCESS_RESTRICTED_SONG; + } + }); + + COND_ID_HOOK(OnOpenText, 0x1B95, shouldRegister, [](u16* textId, bool* loadFromMessageTable) { + CustomMessage::Entry entry; + if (playedSariasSongState == 1) { + entry.nextMessageID = 0x1B95; + entry.msg = "Call out to an old friend for help? You can only do this once.\x02\x11\xC2Yes\x11No"; + } else if (playedSariasSongState == 2) { + RandoCheckId randoCheckId = GetProgressiveCheckInLogic(); + entry.textboxType = TEXTBOX_TYPE_2; + + if (randoCheckId == RC_UNKNOWN) { + entry.msg = "... You call out but there is no response ..."; + } else { + entry.msg = "%g... Link? Is that you? Where have you been..?! Zelda has been worried sick about you! " + "... You need my help?\x10 Alright but just this once. Search %y{{location}}%g, you will " + "find what you need. Hurry now!"; + CustomMessage::Replace(&entry.msg, "{{location}}", + Ship_GetSceneName(Rando::StaticData::Checks[randoCheckId].sceneId)); + Rando::RemoveItem(RI_SONG_SARIA); + } + + playedSariasSongState = 0; + } + + CustomMessage::LoadCustomMessageIntoFont(entry); + *loadFromMessageTable = false; + }); +} diff --git a/mm/2s2h/Rando/StaticData/Items.cpp b/mm/2s2h/Rando/StaticData/Items.cpp index fb3ad98246..c6dde1b361 100644 --- a/mm/2s2h/Rando/StaticData/Items.cpp +++ b/mm/2s2h/Rando/StaticData/Items.cpp @@ -173,6 +173,7 @@ std::map Items = { RI(RI_SONG_LULLABY, "the", "Goron Lullaby", RITYPE_MAJOR, ITEM_SONG_LULLABY, GI_NONE, GID_NONE), RI(RI_SONG_NOVA, "the", "New Wave Bossa Nova", RITYPE_MAJOR, ITEM_SONG_NOVA, GI_NONE, GID_NONE), RI(RI_SONG_OATH, "the", "Oath to Order", RITYPE_MAJOR, ITEM_SONG_OATH, GI_NONE, GID_NONE), + RI(RI_SONG_SARIA, "", "Saria's Song", RITYPE_MAJOR, ITEM_SONG_SARIA, GI_NONE, GID_NONE), RI(RI_SONG_SOARING, "the", "Song of Soaring", RITYPE_MAJOR, ITEM_SONG_SOARING, GI_NONE, GID_NONE), RI(RI_SONG_SONATA, "the", "Sonata of Awakening", RITYPE_MAJOR, ITEM_SONG_SONATA, GI_NONE, GID_NONE), RI(RI_SONG_STORMS, "the", "Song of Storms", RITYPE_MAJOR, ITEM_SONG_STORMS, GI_NONE, GID_NONE), @@ -282,7 +283,7 @@ std::map> StartingItemsMap = { RI_PROGRESSIVE_WALLET, RI_SONG_TIME, RI_SONG_HEALING, RI_SONG_EPONA, RI_SONG_SOARING, RI_SONG_STORMS, RI_SONG_SONATA, RI_PROGRESSIVE_LULLABY, RI_SONG_NOVA, RI_SONG_ELEGY, RI_SONG_OATH, - RI_SONG_DOUBLE_TIME, RI_SONG_INVERTED_TIME, RI_SONG_SUN + RI_SONG_DOUBLE_TIME, RI_SONG_INVERTED_TIME, RI_SONG_SUN, RI_SONG_SARIA, } }, { STARTING_ITEMS_TRADE, { RI_MOONS_TEAR, RI_DEED_LAND, RI_DEED_SWAMP, RI_DEED_MOUNTAIN, RI_DEED_OCEAN, RI_ROOM_KEY, RI_LETTER_TO_MAMA, @@ -510,6 +511,8 @@ const char* GetIconTexturePath(RandoItemId randoItemId) { case RI_SONG_DOUBLE_TIME: case RI_SONG_INVERTED_TIME: return (const char*)gItemIcons[ITEM_SONG_TIME]; + case RI_SONG_SARIA: + return (const char*)gItemIcons[ITEM_SONG_SARIA]; default: break; } diff --git a/mm/2s2h/Rando/StaticData/Options.cpp b/mm/2s2h/Rando/StaticData/Options.cpp index a863f39ee8..0aa955749a 100644 --- a/mm/2s2h/Rando/StaticData/Options.cpp +++ b/mm/2s2h/Rando/StaticData/Options.cpp @@ -54,6 +54,7 @@ std::map Options = { RO(RO_SHUFFLE_SNOWBALL_DROPS, RO_GENERIC_OFF), RO(RO_SHUFFLE_SONG_DOUBLE_TIME, RO_GENERIC_OFF), RO(RO_SHUFFLE_SONG_INVERTED_TIME, RO_GENERIC_OFF), + RO(RO_SHUFFLE_SONG_SARIA, RO_GENERIC_OFF), RO(RO_SHUFFLE_SONG_SUN, RO_GENERIC_OFF), RO(RO_SHUFFLE_SWIM, RO_GENERIC_OFF), RO(RO_SHUFFLE_TINGLE_SHOPS, RO_GENERIC_OFF), diff --git a/mm/2s2h/Rando/Types.h b/mm/2s2h/Rando/Types.h index 15caa3748d..3f105bea07 100644 --- a/mm/2s2h/Rando/Types.h +++ b/mm/2s2h/Rando/Types.h @@ -2985,6 +2985,7 @@ typedef enum { RO_SHUFFLE_SNOWBALL_DROPS, RO_SHUFFLE_SONG_DOUBLE_TIME, RO_SHUFFLE_SONG_INVERTED_TIME, + RO_SHUFFLE_SONG_SARIA, RO_SHUFFLE_SONG_SUN, RO_SHUFFLE_SWIM, RO_SHUFFLE_TINGLE_SHOPS, diff --git a/mm/src/code/z_message.c b/mm/src/code/z_message.c index af383621c4..04efc6f8c5 100644 --- a/mm/src/code/z_message.c +++ b/mm/src/code/z_message.c @@ -5819,8 +5819,10 @@ void Message_Update(PlayState* play) { break; } - if ((msgCtx->textboxEndType == TEXTBOX_ENDTYPE_TWO_CHOICE) && - (play->msgCtx.ocarinaMode == OCARINA_MODE_ACTIVE)) { + if (GameInteractor_Should(VB_MSG_CAPTURE_MSGMODE_TEXT_DONE, false)) { + // no-op + } else if ((msgCtx->textboxEndType == TEXTBOX_ENDTYPE_TWO_CHOICE) && + (play->msgCtx.ocarinaMode == OCARINA_MODE_ACTIVE)) { if (Message_ShouldAdvance(play)) { if (msgCtx->choiceIndex == 0) { play->msgCtx.ocarinaMode = OCARINA_MODE_WARP; @@ -6042,7 +6044,9 @@ void Message_Update(PlayState* play) { if (msgCtx->ocarinaAction != OCARINA_ACTION_CHECK_NOTIME_DONE) { s16 pad; - if (sLastPlayedSong == OCARINA_SONG_TIME) { + if (GameInteractor_Should(VB_MSG_CAPTURE_MSGMODE_TEXT_CLOSING_OCARINA_ACTION, false)) { + // no-op + } else if (sLastPlayedSong == OCARINA_SONG_TIME) { if (interfaceCtx->restrictions.songOfTime == 0) { Message_StartTextbox(play, 0x1B8A, NULL); play->msgCtx.ocarinaMode = OCARINA_MODE_PROCESS_SOT;