summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGarrett Cox <garrettjcox@gmail.com>2026-07-11 06:41:53 -0500
committerGitHub <noreply@github.com>2026-07-11 06:41:53 -0500
commit7aa885216a3cd4e1512bd62fcf331814f21cbc3f (patch)
tree3e812a1522ce3de45573ebea96d247929ee34ce8
parenta7e3e7c29be41cf67d89bdcb2f501d36d6106544 (diff)
Tweaks to sarias song behavior (#1780)
-rw-r--r--mm/2s2h/BenGui/UIWidgets.cpp27
-rw-r--r--mm/2s2h/BenGui/UIWidgets.hpp1
-rw-r--r--mm/2s2h/BenJsonConversions.hpp4
-rw-r--r--mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp9
-rw-r--r--mm/2s2h/Rando/ConvertItem.cpp5
-rw-r--r--mm/2s2h/Rando/GiveItem.cpp4
-rw-r--r--mm/2s2h/Rando/Menu.cpp163
-rw-r--r--mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp4
-rw-r--r--mm/2s2h/Rando/MiscBehavior/SariasSongHint.cpp200
-rw-r--r--mm/2s2h/Rando/Rando.h9
-rw-r--r--mm/2s2h/Rando/RemoveItem.cpp6
-rw-r--r--mm/2s2h/Rando/Spoiler/Apply.cpp3
-rw-r--r--mm/2s2h/Rando/Spoiler/Generate.cpp3
-rw-r--r--mm/2s2h/SaveManager/SaveManager.cpp28
-rw-r--r--mm/2s2h/SaveManager/SaveManager.h1
-rw-r--r--mm/include/z64save.h2
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