Files
Shipwright/soh/soh/SohGui/SohMenuStartingItems.cpp
T
Philip Dubé a7cd6c8113 Avoid randomizer generation races with file select (#7136)
Fixes both starting file while generating,
& starting generation in file loading transition

Remove RandoGenerating cvar
2026-08-31 14:04:50 +00:00

356 lines
16 KiB
C++

#include <vector>
#include <fast/Fast3dGui.h>
#include "soh/SohGui/SohGui.hpp"
#include "soh/SohGui/UIWidgets.hpp"
#include "soh/SohGui/ImGuiUtils.h"
#include "soh/OTRGlobals.h"
#include "soh/cvar_prefixes.h"
#include "soh/Enhancements/randomizer/randomizer.h"
#include "soh/Enhancements/randomizer/settings.h"
namespace SohGui {
using namespace UIWidgets;
static constexpr float kIconSize = 48.0f;
static const ImVec2 kSongSize = ImVec2(32.0f, 48.0f);
static std::shared_ptr<Fast::Fast3dGui> GetFast3dGui() {
return std::dynamic_pointer_cast<Fast::Fast3dGui>(Ship::Context::GetRawInstance()->GetWindow()->GetGui());
}
static void SaveStartingItemCVars() {
Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
}
// Toggleable item icon (lit when the starting CVar is set, faded when not), mirroring
// DrawQuestItemButton in the save editor. The CVar name and tooltip come from the Option itself.
static void StartingItemToggle(RandomizerSettingKey rsk, uint32_t itemId, bool forceFaded = false) {
Rando::Option& option = Rando::Settings::GetInstance()->GetOption(rsk);
const char* cvar = option.GetCVarName().c_str();
const ItemMapEntry& entry = itemMapping[itemId];
bool on = !forceFaded && CVarGetInteger(cvar, 0) != 0;
ImGui::PushID(static_cast<int32_t>(rsk));
PushStyleButton(Colors::DarkGray);
if (ImGui::ImageButton(entry.name.c_str(), GetFast3dGui()->GetTextureByName(on ? entry.name : entry.nameFaded),
ImVec2(kIconSize, kIconSize), ImVec2(0, 0), ImVec2(1, 1))) {
CVarSetInteger(cvar, on ? 0 : 1);
SaveStartingItemCVars();
}
PopStyleButton();
Tooltip(option.GetName().c_str());
ImGui::PopID();
}
// Tiered item icon with a popup picker (None + one icon per tier), mirroring DrawUpgradeIcon.
// `itemIds[i]` is the icon shown for CVar value `i + 1`; value 0 shows itemIds[0] faded.
static void StartingItemTiered(RandomizerSettingKey rsk, const std::vector<uint32_t>& itemIds) {
Rando::Option& option = Rando::Settings::GetInstance()->GetOption(rsk);
const char* cvar = option.GetCVarName().c_str();
int32_t value = CVarGetInteger(cvar, 0);
ImGui::PushID(static_cast<int32_t>(rsk));
const ItemMapEntry& displayEntry = itemMapping[itemIds[value > 0 ? value - 1 : 0]];
bool lit = value > 0;
PushStyleButton(Colors::DarkGray);
if (ImGui::ImageButton(displayEntry.name.c_str(),
GetFast3dGui()->GetTextureByName(lit ? displayEntry.name : displayEntry.nameFaded),
ImVec2(kIconSize, kIconSize), ImVec2(0, 0), ImVec2(1, 1))) {
ImGui::OpenPopup("tieredPicker");
}
PopStyleButton();
Tooltip(option.GetName().c_str());
if (ImGui::BeginPopup("tieredPicker")) {
PushStyleButton(Colors::DarkGray);
if (ImGui::Button("##none", ImVec2(kIconSize, kIconSize) + ImGui::GetStyle().FramePadding * 2)) {
CVarSetInteger(cvar, 0);
SaveStartingItemCVars();
ImGui::CloseCurrentPopup();
}
PopStyleButton();
Tooltip("None");
for (size_t i = 0; i < itemIds.size(); i++) {
ImGui::SameLine();
const ItemMapEntry& entry = itemMapping[itemIds[i]];
PushStyleButton(Colors::DarkGray);
if (ImGui::ImageButton(entry.name.c_str(), GetFast3dGui()->GetTextureByName(entry.name),
ImVec2(kIconSize, kIconSize), ImVec2(0, 0), ImVec2(1, 1))) {
CVarSetInteger(cvar, static_cast<int32_t>(i) + 1);
SaveStartingItemCVars();
ImGui::CloseCurrentPopup();
}
PopStyleButton();
Tooltip(option.GetOptionText(i + 1).c_str());
}
ImGui::EndPopup();
}
ImGui::PopID();
}
// Toggleable 32x48 song icon, mirroring the songMapping loop in DrawQuestStatusTab.
static void StartingSongToggle(RandomizerSettingKey rsk, QuestItem song) {
Rando::Option& option = Rando::Settings::GetInstance()->GetOption(rsk);
const char* cvar = option.GetCVarName().c_str();
const SongMapEntry& entry = songMapping[song];
bool on = CVarGetInteger(cvar, 0) != 0;
ImGui::PushID(static_cast<int32_t>(rsk));
PushStyleButton(Colors::DarkGray);
if (ImGui::ImageButton(entry.name.c_str(), GetFast3dGui()->GetTextureByName(on ? entry.name : entry.nameFaded),
kSongSize, ImVec2(0, 0), ImVec2(1, 1))) {
CVarSetInteger(cvar, on ? 0 : 1);
SaveStartingItemCVars();
}
PopStyleButton();
Tooltip(option.GetName().c_str());
ImGui::PopID();
}
// Item icon followed by a count slider, like the ammo rows in DrawInventoryTab. The slider
// mirrors the Option's own semantics: the CVar stores the option index and the displayed
// number comes from the option text (matching how WIDGET_CVAR_SLIDER_INT renders these).
static void StartingItemCount(RandomizerSettingKey rsk, uint32_t itemId) {
Rando::Option& option = Rando::Settings::GetInstance()->GetOption(rsk);
const char* cvar = option.GetCVarName().c_str();
const ItemMapEntry& entry = itemMapping[itemId];
ImGui::PushID(static_cast<int32_t>(rsk));
ImGui::Image(GetFast3dGui()->GetTextureByName(entry.name), ImVec2(kIconSize, kIconSize));
ImGui::SameLine();
int32_t value = CVarGetInteger(cvar, option.GetMenuOptionDefault());
CVarSliderInt(option.GetName().c_str(), cvar,
IntSliderOptions()
.Color(THEME_COLOR)
.Min(0)
.Max(static_cast<int32_t>(option.GetOptionCount()) - 1)
.DefaultValue(option.GetMenuOptionDefault())
.Format(option.GetOptionText(value).c_str())
.LabelPosition(LabelPositions::Near));
ImGui::PopID();
}
// Plain combobox bound to an Option's CVar, with option text pulled from the Option.
static void StartingItemCombobox(RandomizerSettingKey rsk) {
Rando::Option& option = Rando::Settings::GetInstance()->GetOption(rsk);
std::vector<const char*> entries;
for (size_t i = 0; i < option.GetOptionCount(); i++) {
entries.push_back(option.GetOptionText(i).c_str());
}
CVarCombobox(option.GetName().c_str(), option.GetCVarName().c_str(), entries,
ComboboxOptions().Color(THEME_COLOR).LabelPosition(LabelPositions::Above));
}
void DrawStartingItemsMenu(WidgetInfo& info) {
bool generating = IsRandoGenerating();
bool disableEditingRandoSettings = generating || CVarGetInteger(CVAR_GENERAL("OnFileSelectNameEntry"), 0);
ImGui::BeginDisabled(CVarGetInteger(CVAR_SETTING("DisableChanges"), 0) || disableEditingRandoSettings);
ImGui::SeparatorText("Equipment");
StartingItemToggle(RSK_STARTING_KOKIRI_SWORD, ITEM_SWORD_KOKIRI);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_MASTER_SWORD, ITEM_SWORD_MASTER);
ImGui::SameLine();
StartingItemTiered(RSK_STARTING_BIGGORON_SWORD, { ITEM_SWORD_KNIFE, ITEM_SWORD_BGS });
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_DEKU_SHIELD, ITEM_SHIELD_DEKU);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_HYLIAN_SHIELD, ITEM_SHIELD_HYLIAN);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_MIRROR_SHIELD, ITEM_SHIELD_MIRROR);
StartingItemToggle(RSK_STARTING_GORON_TUNIC, ITEM_TUNIC_GORON);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_ZORA_TUNIC, ITEM_TUNIC_ZORA);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_IRON_BOOTS, ITEM_BOOTS_IRON);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_HOVER_BOOTS, ITEM_BOOTS_HOVER);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_STONE_OF_AGONY, ITEM_STONE_OF_AGONY);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_GERUDO_CARD, ITEM_GERUDO_CARD);
// Starting Strength/Scale/Wallet have no effect when Grab/Swim/Child's Wallet are shuffled; the generator
// force-disables them (settings.cpp), so gray them out to match.
bool grabShuffled =
CVarGetInteger(Rando::Settings::GetInstance()->GetOption(RSK_SHUFFLE_GRAB).GetCVarName().c_str(), 0) != 0;
ImGui::BeginDisabled(grabShuffled);
StartingItemTiered(RSK_STARTING_STRENGTH, { ITEM_BRACELET, ITEM_GAUNTLETS_SILVER, ITEM_GAUNTLETS_GOLD });
ImGui::EndDisabled();
if (grabShuffled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("Disabled because Shuffle Grab is on.");
}
ImGui::SameLine();
bool swimShuffled =
CVarGetInteger(Rando::Settings::GetInstance()->GetOption(RSK_SHUFFLE_SWIM).GetCVarName().c_str(), 0) != 0;
ImGui::BeginDisabled(swimShuffled);
StartingItemTiered(RSK_STARTING_SCALE, { ITEM_SCALE_SILVER, ITEM_SCALE_GOLDEN });
ImGui::EndDisabled();
if (swimShuffled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("Disabled because Shuffle Swim is on.");
}
ImGui::SameLine();
StartingItemTiered(RSK_STARTING_MAGIC_METER, { ITEM_MAGIC_SMALL, ITEM_MAGIC_LARGE });
ImGui::SameLine();
bool childsWalletShuffled =
CVarGetInteger(Rando::Settings::GetInstance()->GetOption(RSK_SHUFFLE_CHILD_WALLET).GetCVarName().c_str(), 0) !=
0;
ImGui::BeginDisabled(childsWalletShuffled);
StartingItemTiered(RSK_STARTING_WALLET, { ITEM_WALLET_ADULT, ITEM_WALLET_GIANT });
ImGui::EndDisabled();
if (childsWalletShuffled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("Disabled because Shuffle Child's Wallet is on.");
}
ImGui::SeparatorText("Items");
bool stickBagShuffled =
CVarGetInteger(Rando::Settings::GetInstance()->GetOption(RSK_SHUFFLE_DEKU_STICK_BAG).GetCVarName().c_str(),
0) != 0;
ImGui::BeginDisabled(stickBagShuffled);
StartingItemToggle(RSK_STARTING_STICKS, ITEM_STICK, stickBagShuffled);
ImGui::EndDisabled();
if (stickBagShuffled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("Disabled because the Deku Stick Bag is being shuffled.");
}
ImGui::SameLine();
bool nutBagShuffled =
CVarGetInteger(Rando::Settings::GetInstance()->GetOption(RSK_SHUFFLE_DEKU_NUT_BAG).GetCVarName().c_str(), 0) !=
0;
ImGui::BeginDisabled(nutBagShuffled);
StartingItemToggle(RSK_STARTING_NUTS, ITEM_NUT, nutBagShuffled);
ImGui::EndDisabled();
if (nutBagShuffled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("Disabled because the Deku Nut Bag is being shuffled.");
}
ImGui::SameLine();
StartingItemTiered(RSK_STARTING_BOMB_BAG, { ITEM_BOMB_BAG_20, ITEM_BOMB_BAG_30, ITEM_BOMB_BAG_40 });
ImGui::SameLine();
StartingItemTiered(RSK_STARTING_BOW, { ITEM_QUIVER_30, ITEM_QUIVER_40, ITEM_QUIVER_50 });
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_FIRE_ARROWS, ITEM_ARROW_FIRE);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_DINS_FIRE, ITEM_DINS_FIRE);
StartingItemTiered(RSK_STARTING_SLINGSHOT, { ITEM_BULLET_BAG_30, ITEM_BULLET_BAG_40, ITEM_BULLET_BAG_50 });
ImGui::SameLine();
StartingItemTiered(RSK_STARTING_OCARINA, { ITEM_OCARINA_FAIRY, ITEM_OCARINA_TIME });
ImGui::SameLine();
// The bombchu bag start option only makes sense when the bombchu bag setting enables bombchu bags;
// single-bag mode is a simple toggle, progressive mode is tiered, and None disables it entirely.
{
Rando::Option& bombchuOption = Rando::Settings::GetInstance()->GetOption(RSK_BOMBCHU_BAG);
int32_t bombchuMode = CVarGetInteger(bombchuOption.GetCVarName().c_str(), RO_BOMBCHU_BAG_NONE);
if (bombchuMode == RO_BOMBCHU_BAG_PROGRESSIVE) {
StartingItemTiered(RSK_STARTING_BOMBCHU_BAG, { ITEM_BOMBCHU, ITEM_BOMBCHU, ITEM_BOMBCHU });
} else if (bombchuMode == RO_BOMBCHU_BAG_SINGLE) {
StartingItemToggle(RSK_STARTING_BOMBCHU_BAG, ITEM_BOMBCHU);
} else {
ImGui::BeginDisabled();
StartingItemToggle(RSK_STARTING_BOMBCHU_BAG, ITEM_BOMBCHU, true);
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("Enable the Bombchu Bag setting to start with bombchus.");
}
}
}
ImGui::SameLine();
StartingItemTiered(RSK_STARTING_HOOKSHOT, { ITEM_HOOKSHOT, ITEM_LONGSHOT });
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_ICE_ARROWS, ITEM_ARROW_ICE);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_FARORES_WIND, ITEM_FARORES_WIND);
StartingItemToggle(RSK_STARTING_BOOMERANG, ITEM_BOOMERANG);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_LENS_OF_TRUTH, ITEM_LENS);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_BEANS, ITEM_BEAN);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_MEGATON_HAMMER, ITEM_HAMMER);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_LIGHT_ARROWS, ITEM_ARROW_LIGHT);
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_NAYRUS_LOVE, ITEM_NAYRUS_LOVE);
StartingItemTiered(RSK_STARTING_BOTTLE_1, { ITEM_BOTTLE, ITEM_BIG_POE, ITEM_LETTER_RUTO });
ImGui::SameLine();
StartingItemTiered(RSK_STARTING_BOTTLE_2, { ITEM_BOTTLE, ITEM_BIG_POE });
ImGui::SameLine();
StartingItemTiered(RSK_STARTING_BOTTLE_3, { ITEM_BOTTLE, ITEM_BIG_POE });
ImGui::SameLine();
StartingItemTiered(RSK_STARTING_BOTTLE_4, { ITEM_BOTTLE, ITEM_BIG_POE });
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_CLAIM_CHECK, ITEM_CLAIM_CHECK);
ImGui::SameLine();
bool weirdEggShuffled =
CVarGetInteger(Rando::Settings::GetInstance()->GetOption(RSK_SHUFFLE_WEIRD_EGG).GetCVarName().c_str(),
RO_WEIRD_EGG_VANILLA) == RO_WEIRD_EGG_SHUFFLED;
ImGui::BeginDisabled(!weirdEggShuffled);
StartingItemToggle(RSK_STARTING_WEIRD_EGG, ITEM_WEIRD_EGG, !weirdEggShuffled);
ImGui::EndDisabled();
if (!weirdEggShuffled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("Enable Shuffle Weird Egg to start with the Weird Egg.");
}
ImGui::SameLine();
bool zeldasLetterShuffled =
CVarGetInteger(Rando::Settings::GetInstance()->GetOption(RSK_SHUFFLE_ZELDAS_LETTER).GetCVarName().c_str(), 0) !=
0;
StartingItemToggle(RSK_STARTING_ZELDAS_LETTER, ITEM_LETTER_ZELDA);
if (ImGui::IsItemHovered()) {
if (zeldasLetterShuffled) {
ImGui::SetTooltip("Start with Zelda's Letter\n\n"
"The Kakariko gate starts open.");
} else {
ImGui::SetTooltip("Start with Zelda's Letter\n\n"
"The Kakariko gate starts open.\n"
"While the letter isn't shuffled this also skips child Zelda:\n"
"you get the item Impa would give and skip everything up to meeting Zelda.");
}
}
ImGui::SameLine();
StartingItemToggle(RSK_STARTING_BUNNY_HOOD, ITEM_MASK_BUNNY);
ImGui::SeparatorText("Songs");
StartingSongToggle(RSK_STARTING_ZELDAS_LULLABY, QUEST_SONG_LULLABY);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_EPONAS_SONG, QUEST_SONG_EPONA);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_SARIAS_SONG, QUEST_SONG_SARIA);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_SUNS_SONG, QUEST_SONG_SUN);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_SONG_OF_TIME, QUEST_SONG_TIME);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_SONG_OF_STORMS, QUEST_SONG_STORMS);
StartingSongToggle(RSK_STARTING_MINUET_OF_FOREST, QUEST_SONG_MINUET);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_BOLERO_OF_FIRE, QUEST_SONG_BOLERO);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_SERENADE_OF_WATER, QUEST_SONG_SERENADE);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_REQUIEM_OF_SPIRIT, QUEST_SONG_REQUIEM);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_NOCTURNE_OF_SHADOW, QUEST_SONG_NOCTURNE);
ImGui::SameLine();
StartingSongToggle(RSK_STARTING_PRELUDE_OF_LIGHT, QUEST_SONG_PRELUDE);
ImGui::SeparatorText("Other");
StartingItemCombobox(RSK_LINKS_POCKET);
// The reward type only applies when Link's Pocket grants a dungeon reward.
if (CVarGetInteger(Rando::Settings::GetInstance()->GetOption(RSK_LINKS_POCKET).GetCVarName().c_str(),
RO_LINKS_POCKET_DUNGEON_REWARD) == RO_LINKS_POCKET_DUNGEON_REWARD) {
StartingItemCombobox(RSK_LINKS_POCKET_REWARD);
}
StartingItemCount(RSK_STARTING_SKULLTULA_TOKEN, ITEM_SKULL_TOKEN);
StartingItemCount(RSK_STARTING_HEARTS, ITEM_HEART_CONTAINER);
ImGui::EndDisabled();
}
} // namespace SohGui