diff options
| author | Garrett Cox <garrettjcox@gmail.com> | 2026-07-11 06:41:53 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-11 06:41:53 -0500 |
| commit | 7aa885216a3cd4e1512bd62fcf331814f21cbc3f (patch) | |
| tree | 3e812a1522ce3de45573ebea96d247929ee34ce8 | |
| parent | a7e3e7c29be41cf67d89bdcb2f501d36d6106544 (diff) | |
Tweaks to sarias song behavior (#1780)
| -rw-r--r-- | mm/2s2h/BenGui/UIWidgets.cpp | 27 | ||||
| -rw-r--r-- | mm/2s2h/BenGui/UIWidgets.hpp | 1 | ||||
| -rw-r--r-- | mm/2s2h/BenJsonConversions.hpp | 4 | ||||
| -rw-r--r-- | mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp | 9 | ||||
| -rw-r--r-- | mm/2s2h/Rando/ConvertItem.cpp | 5 | ||||
| -rw-r--r-- | mm/2s2h/Rando/GiveItem.cpp | 4 | ||||
| -rw-r--r-- | mm/2s2h/Rando/Menu.cpp | 163 | ||||
| -rw-r--r-- | mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp | 4 | ||||
| -rw-r--r-- | mm/2s2h/Rando/MiscBehavior/SariasSongHint.cpp | 200 | ||||
| -rw-r--r-- | mm/2s2h/Rando/Rando.h | 9 | ||||
| -rw-r--r-- | mm/2s2h/Rando/RemoveItem.cpp | 6 | ||||
| -rw-r--r-- | mm/2s2h/Rando/Spoiler/Apply.cpp | 3 | ||||
| -rw-r--r-- | mm/2s2h/Rando/Spoiler/Generate.cpp | 3 | ||||
| -rw-r--r-- | mm/2s2h/SaveManager/SaveManager.cpp | 28 | ||||
| -rw-r--r-- | mm/2s2h/SaveManager/SaveManager.h | 1 | ||||
| -rw-r--r-- | mm/include/z64save.h | 2 |
16 files changed, 442 insertions, 27 deletions
diff --git a/mm/2s2h/BenGui/UIWidgets.cpp b/mm/2s2h/BenGui/UIWidgets.cpp index 481622390..a76247c74 100644 --- a/mm/2s2h/BenGui/UIWidgets.cpp +++ b/mm/2s2h/BenGui/UIWidgets.cpp @@ -181,6 +181,33 @@ bool Button(const char* label, const ButtonOptions& options) { return dirty; } +bool IconButton(const char* strId, const char* icon, const ButtonOptions& options) { + ImVec2 buttonScreenPos = ImGui::GetCursorScreenPos(); + bool clicked = Button(strId, options); + + ImGuiCol textColorIndex = options.disabled ? ImGuiCol_TextDisabled : ImGuiCol_Text; + ImU32 textColor = ImGui::GetColorU32(textColorIndex); + ImFont* font = ImGui::GetFont(); + + unsigned int codepoint = 0; + ImTextCharFromUtf8(&codepoint, icon, NULL); + const ImFontGlyph* glyph = font->FindGlyph((ImWchar)codepoint); + + if (glyph != NULL) { + float glyphWidth = glyph->X1 - glyph->X0; + float glyphHeight = glyph->Y1 - glyph->Y0; + ImVec2 penPos = ImVec2(buttonScreenPos.x + (options.size.x - glyphWidth) * 0.5f - glyph->X0, + buttonScreenPos.y + (options.size.y - glyphHeight) * 0.5f - glyph->Y0); + font->RenderChar(ImGui::GetWindowDrawList(), ImGui::GetFontSize(), penPos, textColor, (ImWchar)codepoint); + } else { + ImVec2 iconSize = ImGui::CalcTextSize(icon); + ImVec2 iconPos = ImVec2(buttonScreenPos.x + (options.size.x - iconSize.x) * 0.5f, + buttonScreenPos.y + (options.size.y - iconSize.y) * 0.5f); + ImGui::GetWindowDrawList()->AddText(iconPos, textColor, icon); + } + return clicked; +} + bool WindowButton(const char* label, const char* cvarName, std::shared_ptr<Ship::GuiWindow> windowPtr, const WindowButtonOptions& options) { ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); diff --git a/mm/2s2h/BenGui/UIWidgets.hpp b/mm/2s2h/BenGui/UIWidgets.hpp index e060c7b5c..e622a5005 100644 --- a/mm/2s2h/BenGui/UIWidgets.hpp +++ b/mm/2s2h/BenGui/UIWidgets.hpp @@ -615,6 +615,7 @@ void PushStyleButton(const ImVec4& color, ImVec2 padding = ImVec2(10.0f, 8.0f)); void PushStyleButton(Colors color = Colors::Gray, ImVec2 padding = ImVec2(10.0f, 8.0f)); void PopStyleButton(); bool Button(const char* label, const ButtonOptions& options = {}); +bool IconButton(const char* strId, const char* icon, const ButtonOptions& options = {}); bool WindowButton(const char* label, const char* cvarName, std::shared_ptr<Ship::GuiWindow> windowPtr, const WindowButtonOptions& options = {}); diff --git a/mm/2s2h/BenJsonConversions.hpp b/mm/2s2h/BenJsonConversions.hpp index f6447baea..dd26421bd 100644 --- a/mm/2s2h/BenJsonConversions.hpp +++ b/mm/2s2h/BenJsonConversions.hpp @@ -58,6 +58,8 @@ inline void to_json(json& j, const RandoSaveInfo& rando) { { "randoStartingItems", rando.randoStartingItems }, { "foundDungeonKeys", rando.foundDungeonKeys }, { "foundTriforcePieces", rando.foundTriforcePieces }, + { "sariaHintsAvailable", rando.sariaHintsAvailable }, + { "sariaPriorityItems", rando.sariaPriorityItems }, }; } @@ -70,6 +72,8 @@ inline void from_json(const json& j, RandoSaveInfo& rando) { j.at("randoStartingItems").get_to(rando.randoStartingItems); j.at("foundDungeonKeys").get_to(rando.foundDungeonKeys); j.at("foundTriforcePieces").get_to(rando.foundTriforcePieces); + j.at("sariaHintsAvailable").get_to(rando.sariaHintsAvailable); + j.at("sariaPriorityItems").get_to(rando.sariaPriorityItems); } inline void to_json(json& j, const Vec3f& vec) { diff --git a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp index 747f1933d..e8b28a91f 100644 --- a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp +++ b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp @@ -82,6 +82,9 @@ TrackerImageObject GetImageObject(TrackerItemType itemType, u32 itemId) { case RI_TRIFORCE_PIECE: { itemObtained = gSaveContext.save.shipSaveInfo.rando.foundTriforcePieces > 0; } break; + case RI_SONG_SARIA: { + itemObtained = gSaveContext.save.shipSaveInfo.rando.sariaHintsAvailable > 0; + } break; default: { itemObtained = !Rando::IsItemObtainable(randoItemId); } break; @@ -182,6 +185,12 @@ std::string GetItemCounts(TrackerItemType itemType, u32 itemId) { max > 999 ? "1k" : std::to_string(max)); } } break; + case RI_SONG_SARIA: { + auto count = gSaveContext.save.shipSaveInfo.rando.sariaHintsAvailable; + if (count > 1) { + countStr = std::to_string(count); + } + } break; case RI_GS_TOKEN_OCEAN: case RI_GS_TOKEN_SWAMP: { auto max = diff --git a/mm/2s2h/Rando/ConvertItem.cpp b/mm/2s2h/Rando/ConvertItem.cpp index cae0a67d0..13fc338d4 100644 --- a/mm/2s2h/Rando/ConvertItem.cpp +++ b/mm/2s2h/Rando/ConvertItem.cpp @@ -504,7 +504,10 @@ bool Rando::IsItemObtainable(RandoItemId randoItemId, RandoCheckId randoCheckId) case RI_SONG_OATH: return !CHECK_QUEST_ITEM(QUEST_SONG_OATH); case RI_SONG_SARIA: - return !CHECK_QUEST_ITEM(QUEST_SONG_SARIA); + if (hasObtainedCheck) { + return false; + } + return true; case RI_SONG_SOARING: return !CHECK_QUEST_ITEM(QUEST_SONG_SOARING); case RI_SONG_SONATA: diff --git a/mm/2s2h/Rando/GiveItem.cpp b/mm/2s2h/Rando/GiveItem.cpp index c42052655..10943ebba 100644 --- a/mm/2s2h/Rando/GiveItem.cpp +++ b/mm/2s2h/Rando/GiveItem.cpp @@ -380,6 +380,10 @@ void Rando::GiveItem(RandoItemId randoItemId) { case RI_SONG_INVERTED_TIME: Flags_SetRandoInf(RANDO_INF_OBTAINED_SONG_INVERTED_TIME); break; + case RI_SONG_SARIA: + gSaveContext.save.shipSaveInfo.rando.sariaHintsAvailable++; + Item_Give(gPlayState, Rando::StaticData::Items[randoItemId].itemId); + break; case RI_JUNK: case RI_NONE: break; diff --git a/mm/2s2h/Rando/Menu.cpp b/mm/2s2h/Rando/Menu.cpp index 2d26e834c..3ebf63ea3 100644 --- a/mm/2s2h/Rando/Menu.cpp +++ b/mm/2s2h/Rando/Menu.cpp @@ -1,6 +1,7 @@ #include "Rando/Rando.h" #include "Rando/Spoiler/Spoiler.h" #include "2s2h/BenGui/UIWidgets.hpp" +#include <ship/window/gui/IconsFontAwesome4.h> #include "Rando/CheckTracker/CheckTracker.h" #include "Rando/MiscBehavior/ClockShuffle.h" #include "build.h" @@ -548,6 +549,145 @@ static void DrawShufflesTab() { ImGui::EndChild(); } +static constexpr int SARIA_MAX_PRIORITY_ITEMS = 16; +static constexpr float PRIORITY_BUTTON_SIZE = 24.0f; + +static void PushPriorityListChildStyle() { + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8.0f, 8.0f)); +} + +static ButtonOptions PriorityRowButtonOptions(bool disabled = false) { + return ButtonOptions({ { .disabled = disabled } }) + .Size(ImVec2(PRIORITY_BUTTON_SIZE, PRIORITY_BUTTON_SIZE)) + .Padding(ImVec2(4.0f, 4.0f)); +} + +static void DrawPrioritySwapButton(const char* strId, const char* icon, bool disabled, + std::vector<RandoItemId>& priorityItemsList, size_t a, size_t b) { + if (IconButton(strId, icon, PriorityRowButtonOptions(disabled))) { + std::swap(priorityItemsList[a], priorityItemsList[b]); + Rando::SetSariaPriorityItemsInConfig(priorityItemsList); + } +} + +static void DrawPriorityItemsPopup() { + static std::vector<RandoItemId> priorityItemsList; + if (ImGui::IsWindowAppearing()) { + priorityItemsList = Rando::GetSariaPriorityItemsFromConfig(); + } + + std::string headerLabel = "Priority Items (" + std::to_string(priorityItemsList.size()) + "/" + + std::to_string(SARIA_MAX_PRIORITY_ITEMS) + ")"; + ImGui::SeparatorText(headerLabel.c_str()); + ImGui::TextWrapped("Saria's Song hints whichever of these is reachable and not yet found, checked in the " + "order listed below."); + if (Button( + ICON_FA_UNDO " Reset to Default", + ButtonOptions({ { .tooltip = "Replace this list with the default priority items" } }).Size(ImVec2(0, 0)))) { + priorityItemsList = Rando::GetDefaultSariaPriorityItems(); + Rando::SetSariaPriorityItemsInConfig(priorityItemsList); + } + + PushPriorityListChildStyle(); + if (ImGui::BeginChild("priorityItemsCurrentList", ImVec2(0, 180.0f))) { + if (priorityItemsList.empty()) { + ImGui::TextColored(ColorValues.at(Colors::Gray), "No priority items configured."); + } else if (ImGui::BeginTable("priorityItemsTable", 5, ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("icon", ImGuiTableColumnFlags_WidthFixed, PRIORITY_BUTTON_SIZE); + ImGui::TableSetupColumn("name", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("up", ImGuiTableColumnFlags_WidthFixed, 32.0f); + ImGui::TableSetupColumn("down", ImGuiTableColumnFlags_WidthFixed, 32.0f); + ImGui::TableSetupColumn("remove", ImGuiTableColumnFlags_WidthFixed, 32.0f); + + for (size_t index = 0; index < priorityItemsList.size(); index++) { + RandoItemId itemId = priorityItemsList[index]; + Rando::StaticData::RandoStaticItem randoStaticItem = Rando::StaticData::Items[itemId]; + ImGui::PushID((int)index); + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + const char* texturePath = Rando::StaticData::GetIconTexturePath(itemId); + ImTextureID textureId = + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(texturePath); + float iconOffsetY = (ImGui::GetFrameHeight() - PRIORITY_BUTTON_SIZE) * 0.5f; + if (iconOffsetY > 0.0f) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + iconOffsetY); + } + ImGui::Image(textureId, ImVec2(PRIORITY_BUTTON_SIZE, PRIORITY_BUTTON_SIZE), ImVec2(0, 0), ImVec2(1, 1), + Ship_GetItemColorTint(randoStaticItem.itemId), ImVec4(0, 0, 0, 0)); + + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(randoStaticItem.name); + + ImGui::TableNextColumn(); + DrawPrioritySwapButton("##up", ICON_FA_CHEVRON_UP, index == 0, priorityItemsList, index, index - 1); + + ImGui::TableNextColumn(); + DrawPrioritySwapButton("##down", ICON_FA_CHEVRON_DOWN, index + 1 == priorityItemsList.size(), + priorityItemsList, index, index + 1); + + ImGui::TableNextColumn(); + if (IconButton("##remove", ICON_FA_TIMES, PriorityRowButtonOptions().Color(Colors::Red))) { + priorityItemsList.erase(priorityItemsList.begin() + index); + Rando::SetSariaPriorityItemsInConfig(priorityItemsList); + } + + ImGui::PopID(); + } + ImGui::EndTable(); + } + } + ImGui::EndChild(); + ImGui::PopStyleVar(2); + + ImGui::Spacing(); + ImGui::SeparatorText("Add Item"); + + static ImGuiTextFilter addItemFilter; + UIWidgets::PushStyleCombobox(); + addItemFilter.Draw("##priorityItemFilter", ImGui::GetContentRegionAvail().x); + UIWidgets::PopStyleCombobox(); + if (!addItemFilter.IsActive()) { + ImGui::SameLine(18.0f); + ImGui::Text("Search"); + } + + bool atCap = priorityItemsList.size() >= SARIA_MAX_PRIORITY_ITEMS; + PushPriorityListChildStyle(); + if (ImGui::BeginChild("priorityItemsAddList", ImVec2(0, 0))) { + if (atCap) { + ImGui::PushStyleColor(ImGuiCol_Text, ColorValues.at(Colors::Orange)); + ImGui::TextWrapped("Priority list is full (%d/%d). Remove an item above to add a different one.", + SARIA_MAX_PRIORITY_ITEMS, SARIA_MAX_PRIORITY_ITEMS); + ImGui::PopStyleColor(); + } else { + for (RandoItemId candidateId : Rando::GetSariaPriorityItemCandidates()) { + if (setOfItemsInPool.count(candidateId) == 0) { + continue; + } + if (std::find(priorityItemsList.begin(), priorityItemsList.end(), candidateId) != + priorityItemsList.end()) { + continue; + } + + const char* name = Rando::StaticData::Items[candidateId].name; + if (!addItemFilter.PassFilter(name)) { + continue; + } + + if (ImGui::Selectable(name)) { + priorityItemsList.push_back(candidateId); + Rando::SetSariaPriorityItemsInConfig(priorityItemsList); + } + } + } + } + ImGui::EndChild(); + ImGui::PopStyleVar(2); +} + static void DrawItemsTab() { f32 columnWidth = ImGui::GetContentRegionAvail().x / 3 - (ImGui::GetStyle().ItemSpacing.x * 2); ImGui::BeginChild("randoItemsColumn1", ImVec2(columnWidth, ImGui::GetContentRegionAvail().y)); @@ -568,9 +708,26 @@ static void DrawItemsTab() { 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." } })); + { { .tooltip = "Adds Saria's Song to the item pool, playing it will give you a hint to a reachable " + "item, preferring items from your Priority Items list (configurable via the button to " + "the right) in order, and falling back to a random reachable major item or mask if none " + "of your priority items are currently available. The song is one time use, you will " + "lose it after using it." } })); + if (CVarGetInteger(Rando::StaticData::Options[RO_SHUFFLE_SONG_SARIA].cvar, 0)) { + ImGui::SameLine(); + if (Button(ICON_FA_COG, + ButtonOptions({ { .tooltip = "Configure the Priority Items list used by the Saria's Song hint" } }) + .Size(ImVec2(0, 0)))) { + ImGui::OpenPopup("PriorityItemsPopup"); + } + ImGui::SetNextWindowSize(ImVec2(400.0f, 520.0f), ImGuiCond_Always); + ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, 6.0f); + if (ImGui::BeginPopup("PriorityItemsPopup")) { + DrawPriorityItemsPopup(); + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); + } CVarCheckbox("Deku Stick Bag", "gPlaceholderBool", CheckboxOptions({ { .disabled = true, .disabledTooltip = "Coming Soon" } })); CVarCheckbox("Deku Nut Bag", "gPlaceholderBool", diff --git a/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp b/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp index a6f08ea32..c3d699b5e 100644 --- a/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp +++ b/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp @@ -73,6 +73,10 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) { auto startingItems = Rando::GetStartingItemsFromConfig(); Rando::SetStartingItemsInSave(gSaveContext.save.shipSaveInfo.rando, startingItems); + // Persist SariaPriorityItems to the save + auto priorityItems = Rando::GetSariaPriorityItemsFromConfig(); + Rando::SetSariaPriorityItemsInSave(gSaveContext.save.shipSaveInfo.rando, priorityItems); + std::vector<RandoCheckId> checkPool; std::vector<RandoItemId> itemPool; Rando::Logic::GeneratePools(gSaveContext.save.shipSaveInfo.rando, checkPool, itemPool); diff --git a/mm/2s2h/Rando/MiscBehavior/SariasSongHint.cpp b/mm/2s2h/Rando/MiscBehavior/SariasSongHint.cpp index 3f4c025d5..1271ce7f2 100644 --- a/mm/2s2h/Rando/MiscBehavior/SariasSongHint.cpp +++ b/mm/2s2h/Rando/MiscBehavior/SariasSongHint.cpp @@ -1,8 +1,12 @@ #include "2s2h/CustomMessage/CustomMessage.h" #include "MiscBehavior.h" #include "2s2h/ShipUtils.h" +#include <algorithm> #include <set> #include "2s2h/Rando/Logic/Logic.h" +#include "2s2h/SaveManager/SaveManager.h" +#include <libultraship/libultraship.h> +#include <libultraship/bridge/consolevariablebridge.h> extern "C" { #include <variables.h> @@ -12,14 +16,138 @@ extern s16 sOcarinaSongFanfares[17]; extern s16 sLastPlayedSong; } -static int playedSariasSongState = 0; +std::vector<RandoItemId> Rando::GetDefaultSariaPriorityItems() { + return { + 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, + }; +} + +std::vector<RandoItemId> Rando::GetSariaPriorityItemsFromSpoiler(nlohmann::json& spoiler) { + auto priorityItemsStrings = spoiler["sariaPriorityItems"].get<std::vector<std::string>>(); + std::vector<RandoItemId> priorityItems; + + for (auto& itemName : priorityItemsStrings) { + auto randoItemId = Rando::StaticData::GetItemIdFromName(itemName.c_str()); + if (randoItemId > RI_UNKNOWN && randoItemId < RI_MAX) { + priorityItems.push_back(randoItemId); + } + } + + return priorityItems; +} + +void Rando::SetSariaPriorityItemsInSpoiler(nlohmann::json& spoiler, std::vector<RandoItemId>& priorityItems) { + std::vector<std::string> priorityItemsJson; + for (auto& randoItemId : priorityItems) { + if (randoItemId > RI_UNKNOWN && randoItemId < RI_MAX) { + priorityItemsJson.push_back(Rando::StaticData::Items[randoItemId].spoilerName); + } + } + spoiler["sariaPriorityItems"] = priorityItemsJson; +} + +std::vector<RandoItemId> Rando::GetSariaPriorityItemsFromSave(RandoSaveInfo& randoSaveInfo) { + std::vector<RandoItemId> priorityItems; + + for (int i = 0; i < ARRAY_COUNT(randoSaveInfo.sariaPriorityItems); i++) { + if (randoSaveInfo.sariaPriorityItems[i] > RI_UNKNOWN && randoSaveInfo.sariaPriorityItems[i] < RI_MAX) { + priorityItems.push_back((RandoItemId)randoSaveInfo.sariaPriorityItems[i]); + } + } + + return priorityItems; +} + +void Rando::SetSariaPriorityItemsInSave(RandoSaveInfo& randoSaveInfo, std::vector<RandoItemId>& priorityItems) { + memset(&randoSaveInfo.sariaPriorityItems, 0, sizeof(randoSaveInfo.sariaPriorityItems)); + + size_t index = 0; + for (auto& randoItemId : priorityItems) { + if (index >= ARRAY_COUNT(randoSaveInfo.sariaPriorityItems)) { + break; + } + randoSaveInfo.sariaPriorityItems[index++] = randoItemId; + } +} + +std::vector<RandoItemId> Rando::GetSariaPriorityItemsFromConfig() { + auto allConfig = Ship::Context::GetInstance()->GetConfig()->GetNestedJson(); + std::vector<RandoItemId> priorityItems = Rando::GetDefaultSariaPriorityItems(); + + if (allConfig.find("CVars") != allConfig.end() && allConfig["CVars"].is_object() && + allConfig["CVars"].find("gRando") != allConfig["CVars"].end() && allConfig["CVars"]["gRando"].is_object() && + allConfig["CVars"]["gRando"].find("SariaPriorityItems") != allConfig["CVars"]["gRando"].end()) { -std::set<RandoItemId> 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, -}; + if (allConfig["CVars"]["gRando"]["SariaPriorityItems"].is_array()) { + priorityItems.clear(); + + auto priorityItemsStrings = + allConfig["CVars"]["gRando"]["SariaPriorityItems"].get<std::vector<std::string>>(); + for (auto& itemName : priorityItemsStrings) { + auto randoItemId = Rando::StaticData::GetItemIdFromName(itemName.c_str()); + if (randoItemId > RI_UNKNOWN && randoItemId < RI_MAX) { + priorityItems.push_back(randoItemId); + } + } + } else if (allConfig["CVars"]["gRando"]["SariaPriorityItems"].is_string()) { + CVarClear("gRando.SariaPriorityItems"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } else if (allConfig["CVars"]["gRando"]["SariaPriorityItems"].is_null()) { + priorityItems.clear(); + } + } + + return priorityItems; +} + +void Rando::SetSariaPriorityItemsInConfig(std::vector<RandoItemId>& priorityItems) { + auto priorityItemsJson = nlohmann::json::array(); + for (auto& randoItemId : priorityItems) { + if (randoItemId > RI_UNKNOWN && randoItemId < RI_MAX) { + priorityItemsJson.push_back(Rando::StaticData::Items[randoItemId].spoilerName); + } + } + // SetBlock() already persists to disk internally - no separate Save() call needed here. + Ship::Context::GetInstance()->GetConfig()->SetBlock("CVars.gRando.SariaPriorityItems", priorityItemsJson); +} + +static bool IsExcludedFromSariaPriorityItemCandidates(RandoItemId randoItemId) { + switch (randoItemId) { + case RI_PROGRESSIVE_SWORD: + case RI_PROGRESSIVE_BOW: + case RI_PROGRESSIVE_BOMB_BAG: + case RI_PROGRESSIVE_MAGIC: + case RI_PROGRESSIVE_WALLET: + case RI_PROGRESSIVE_LULLABY: + case RI_TIME_PROGRESSIVE: + case RI_TRIFORCE_PIECE_PREVIOUS: + return true; + default: + return false; + } +} + +// List of items for the UI +std::vector<RandoItemId> Rando::GetSariaPriorityItemCandidates() { + static const std::vector<RandoItemId> candidates = [] { + std::vector<RandoItemId> result; + for (auto& [randoItemId, randoStaticItem] : Rando::StaticData::Items) { + if ((randoStaticItem.randoItemType == RITYPE_MAJOR || randoStaticItem.randoItemType == RITYPE_MASK) && + !IsExcludedFromSariaPriorityItemCandidates(randoItemId)) { + result.push_back(randoItemId); + } + } + return result; + }(); + return candidates; +} + +static int playedSariasSongState = 0; RandoCheckId GetProgressiveCheckInLogic() { + std::vector<RandoItemId> priorityItems = Rando::GetSariaPriorityItemsFromSave(gSaveContext.save.shipSaveInfo.rando); + std::unordered_map<RandoRegionId, Rando::Logic::RegionTimeState> regionTimeStates = Rando::Logic::InitializeRegionTimeStates(RR_MAX); std::set<RandoRegionId> reachableRegions = {}; @@ -29,7 +157,7 @@ RandoCheckId GetProgressiveCheckInLogic() { Rando::Logic::FindReachableRegions(Rando::Logic::GetRegionIdFromEntrance(gSaveContext.save.entrance), reachableRegions, regionTimeStates); - std::vector<RandoCheckId> priorityChecks = {}; + std::unordered_map<RandoItemId, RandoCheckId> priorityItemChecks = {}; std::vector<RandoCheckId> otherChecks = {}; for (RandoRegionId regionId : reachableRegions) { @@ -40,8 +168,8 @@ RandoCheckId GetProgressiveCheckInLogic() { 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); + if (std::find(priorityItems.begin(), priorityItems.end(), itemId) != priorityItems.end()) { + priorityItemChecks.try_emplace(itemId, randoCheckId); } else if (type == RITYPE_MAJOR || type == RITYPE_MASK) { otherChecks.push_back(randoCheckId); } @@ -49,10 +177,17 @@ RandoCheckId GetProgressiveCheckInLogic() { } } - // 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())]) - : priorityChecks[0]; + // Walk the user-ordered priority list and return the check for the first entry that's actually available + // right now. + for (RandoItemId priorityItemId : priorityItems) { + auto priorityCheck = priorityItemChecks.find(priorityItemId); + if (priorityCheck != priorityItemChecks.end()) { + return priorityCheck->second; + } + } + + // None of the priority items are currently available; fall back to a random reachable major item/mask check. + return otherChecks.empty() ? RC_UNKNOWN : otherChecks[Ship_Random(0, otherChecks.size())]; } void Rando::MiscBehavior::SariasSongHint() { @@ -67,8 +202,8 @@ void Rando::MiscBehavior::SariasSongHint() { COND_VB_SHOULD(VB_SONG_AVAILABLE_TO_PLAY, shouldRegister, { uint8_t* songIndex = va_arg(args, uint8_t*); - if (*songIndex == OCARINA_SONG_SARIAS && CHECK_QUEST_ITEM(QUEST_SONG_SARIA)) { - *should = true; + if (*songIndex == OCARINA_SONG_SARIAS && *should) { + *should = gSaveContext.save.shipSaveInfo.rando.sariaHintsAvailable > 0; } }); @@ -76,12 +211,34 @@ void Rando::MiscBehavior::SariasSongHint() { 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 { + Input* input = CONTROLLER1(&gPlayState->state); + + if (playedSariasSongState == 1) { + // Check BTN_A directly instead of Message_ShouldAdvanceSilent(): its two/three-choice branch + // already reduces to exactly this (a fresh A press), but only once msgCtx->textboxEndType has + // actually been finalized to TEXTBOX_ENDTYPE_TWO_CHOICE. Until then it falls through to the + // plain-textbox branch instead, which (with Fast Text on) treats a still-held B as an advance - + // letting a button held from confirming the "call out" prompt moments earlier auto-confirm + // this choice before the player has seen it, always reading choiceIndex's default of 0 ("Yes"). + if (CHECK_BTN_ALL(input->press.button, BTN_A)) { + 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; + } + } + } else { + // playedSariasSongState == 2: the hint (or "no response") text is up. Require a genuine fresh + // press here instead of Message_ShouldAdvanceSilent(), which treats a held B as an advance when + // the Fast Text enhancement is on. Otherwise, still holding B from confirming the prompt above + // closes this text the instant it finishes drawing, before it can be read. + if (CHECK_BTN_ALL(input->press.button, BTN_A) || CHECK_BTN_ALL(input->press.button, BTN_B) || + CHECK_BTN_ALL(input->press.button, BTN_CUP)) { playedSariasSongState = 0; Audio_PlaySfx(NA_SE_SY_DECIDE); Message_CloseTextbox(gPlayState); @@ -120,9 +277,8 @@ void Rando::MiscBehavior::SariasSongHint() { CustomMessage::Replace(&entry.msg, "{{location}}", Rando::StaticData::GetLocationNameForHint(randoCheckId, true)); Rando::RemoveItem(RI_SONG_SARIA); + SaveManager_PersistSariaHintsAvailable(); } - - playedSariasSongState = 0; } else if (playedSariasSongState == 0) { return; } diff --git a/mm/2s2h/Rando/Rando.h b/mm/2s2h/Rando/Rando.h index 75ea214e6..1c7aa6297 100644 --- a/mm/2s2h/Rando/Rando.h +++ b/mm/2s2h/Rando/Rando.h @@ -32,6 +32,15 @@ void SetStartingItemsInSave(RandoSaveInfo& randoSaveInfo, std::vector<RandoItemI std::vector<RandoItemId> GetStartingItemsFromConfig(); void SetStartingItemsInConfig(std::vector<RandoItemId>& startingItems); +std::vector<RandoItemId> GetDefaultSariaPriorityItems(); +std::vector<RandoItemId> GetSariaPriorityItemsFromSpoiler(nlohmann::json& spoiler); +void SetSariaPriorityItemsInSpoiler(nlohmann::json& spoiler, std::vector<RandoItemId>& priorityItems); +std::vector<RandoItemId> GetSariaPriorityItemsFromSave(RandoSaveInfo& randoSaveInfo); +void SetSariaPriorityItemsInSave(RandoSaveInfo& randoSaveInfo, std::vector<RandoItemId>& priorityItems); +std::vector<RandoItemId> GetSariaPriorityItemsFromConfig(); +void SetSariaPriorityItemsInConfig(std::vector<RandoItemId>& priorityItems); +std::vector<RandoItemId> GetSariaPriorityItemCandidates(); + std::vector<RandoCheckId> FindMultiItemPlacement(RandoItemId randoItemId); } // namespace Rando diff --git a/mm/2s2h/Rando/RemoveItem.cpp b/mm/2s2h/Rando/RemoveItem.cpp index d5e55fce0..21c2f7e7a 100644 --- a/mm/2s2h/Rando/RemoveItem.cpp +++ b/mm/2s2h/Rando/RemoveItem.cpp @@ -360,7 +360,11 @@ void Rando::RemoveItem(RandoItemId randoItemId) { REMOVE_QUEST_ITEM(QUEST_SONG_OATH); break; case RI_SONG_SARIA: - REMOVE_QUEST_ITEM(QUEST_SONG_SARIA); + gSaveContext.save.shipSaveInfo.rando.sariaHintsAvailable = + MAX(gSaveContext.save.shipSaveInfo.rando.sariaHintsAvailable - 1, 0); + if (gSaveContext.save.shipSaveInfo.rando.sariaHintsAvailable == 0) { + REMOVE_QUEST_ITEM(QUEST_SONG_SARIA); + } break; case RI_SONG_SOARING: REMOVE_QUEST_ITEM(QUEST_SONG_SOARING); diff --git a/mm/2s2h/Rando/Spoiler/Apply.cpp b/mm/2s2h/Rando/Spoiler/Apply.cpp index 39fae9c14..bcce930e2 100644 --- a/mm/2s2h/Rando/Spoiler/Apply.cpp +++ b/mm/2s2h/Rando/Spoiler/Apply.cpp @@ -26,6 +26,9 @@ void ApplyToSaveContext(nlohmann::json spoiler) { auto startingItems = Rando::GetStartingItemsFromSpoiler(spoiler); Rando::SetStartingItemsInSave(gSaveContext.save.shipSaveInfo.rando, startingItems); + auto priorityItems = Rando::GetSariaPriorityItemsFromSpoiler(spoiler); + Rando::SetSariaPriorityItemsInSave(gSaveContext.save.shipSaveInfo.rando, priorityItems); + for (auto& [randoCheckId, randoStaticCheck] : Rando::StaticData::Checks) { if (randoStaticCheck.randoCheckId == RC_UNKNOWN) { continue; diff --git a/mm/2s2h/Rando/Spoiler/Generate.cpp b/mm/2s2h/Rando/Spoiler/Generate.cpp index 45c8d828c..7c9ca489a 100644 --- a/mm/2s2h/Rando/Spoiler/Generate.cpp +++ b/mm/2s2h/Rando/Spoiler/Generate.cpp @@ -19,6 +19,9 @@ nlohmann::json GenerateFromSaveContext() { auto startingItems = Rando::GetStartingItemsFromSave(gSaveContext.save.shipSaveInfo.rando); Rando::SetStartingItemsInSpoiler(spoiler, startingItems); + auto priorityItems = Rando::GetSariaPriorityItemsFromSave(gSaveContext.save.shipSaveInfo.rando); + Rando::SetSariaPriorityItemsInSpoiler(spoiler, priorityItems); + spoiler["checks"] = nlohmann::json::object(); for (auto& [randoCheckId, randoStaticCheck] : Rando::StaticData::Checks) { if (randoStaticCheck.randoCheckId == RC_UNKNOWN) { diff --git a/mm/2s2h/SaveManager/SaveManager.cpp b/mm/2s2h/SaveManager/SaveManager.cpp index a49bd949b..04a433874 100644 --- a/mm/2s2h/SaveManager/SaveManager.cpp +++ b/mm/2s2h/SaveManager/SaveManager.cpp @@ -147,6 +147,34 @@ int SaveManager_ReadSaveFile(const std::filesystem::path& fileName, nlohmann::js } } +// Special "auto save" to prevent save scumming Saria's Song hint. This can be more generic if we +// find another use case for this functionality. +void SaveManager_PersistSariaHintsAvailable() { + std::string fileName = SaveManager_GetFileName(gSaveContext.fileNum + 1); + nlohmann::json j; + + if (SaveManager_ReadSaveFile(fileName, j) != 0) { + return; + } + + try { + u8 hintsAvailable = gSaveContext.save.shipSaveInfo.rando.sariaHintsAvailable; + + if (j.contains("newCycleSave")) { + j["newCycleSave"]["save"]["shipSaveInfo"]["rando"]["sariaHintsAvailable"] = hintsAvailable; + } + + if (j.contains("owlSave")) { + j["owlSave"]["save"]["shipSaveInfo"]["rando"]["sariaHintsAvailable"] = hintsAvailable; + } + } catch (...) { + SPDLOG_ERROR("Failed to patch sariaHintsAvailable into save file"); + return; + } + + SaveManager_WriteSaveFile(fileName, j); +} + void SaveManager_MoveInvalidSaveFile(const std::filesystem::path& fileName, const std::string& message) { const std::filesystem::path filePath = savesFolderPath / fileName; const std::filesystem::path backupFilePath = diff --git a/mm/2s2h/SaveManager/SaveManager.h b/mm/2s2h/SaveManager/SaveManager.h index 2c82ca4b9..4cac20163 100644 --- a/mm/2s2h/SaveManager/SaveManager.h +++ b/mm/2s2h/SaveManager/SaveManager.h @@ -11,6 +11,7 @@ bool SaveManager_HandleFileDropped(char* filePath); bool BinarySaveConverter_HandleFileDropped(char* filePath); int SaveManager_GetOpenFileSlot(); void SaveManager_WriteSaveFile(const std::filesystem::path& fileName, nlohmann::json j); +void SaveManager_PersistSariaHintsAvailable(); #else void SaveManager_SysFlashrom_WriteData(u8* addr, u32 pageNum, u32 pageCount); s32 SaveManager_SysFlashrom_ReadData(void* addr, u32 pageNum, u32 pageCount); diff --git a/mm/include/z64save.h b/mm/include/z64save.h index 2f7bda353..20c3d2fe3 100644 --- a/mm/include/z64save.h +++ b/mm/include/z64save.h @@ -388,6 +388,8 @@ typedef struct RandoSaveInfo { u16 randoStartingItems[256]; // Max 256 starting items, using u16 in case we add more than 255 items s8 foundDungeonKeys[9]; // Tracks the number of dungeon keys found, opposed to the number of keys in the inventory u16 foundTriforcePieces; + u8 sariaHintsAvailable; + u16 sariaPriorityItems[16]; } RandoSaveInfo; // These are values added by 2S2H that we need to be persisted to the save file |
