#include "rando_config.hpp" #include #include #include "../session.hpp" #include "../tools.h" #include "../paths.hpp" #include "../../generator/seedgen/seed.hpp" #include "../../generator/utility/string.hpp" #include "../../generator/utility/text.hpp" #include "../../generator/utility/yaml.hpp" #include "rando_seed_generation.hpp" #include "config_store.hpp" #include #include #include #include #include "../randomizer_context.hpp" #include "d/d_file_select.h" namespace randomizer::ui { seedgen::settings::Setting* FindSetting(const std::string& key) { if (key.empty()) { mods::log::error("Key is empty! Unable to find setting."); } // TODO: handle multi-world selection auto& settings = GetRandomizerConfig().GetSettings(); try { return &settings.GetMap().at(key); } catch (std::exception e) { mods::log::error("Failed to get Settings Key: {}", key); } } const std::vector>& GetStartingInventoryLayoutOrder() { static const std::vector> layoutOrder = { // { display name , logic item name } {"Shadow Crystal", "Shadow Crystal"}, {"Horse Call", "Horse Call"}, {"Fishing Rod", "Progressive Fishing Rod"}, {"Slingshot", "Slingshot"}, {"Lantern", "Lantern"}, {"Gale Boomerang", "Gale Boomerang"}, {"Iron Boots", "Iron Boots"}, {"Bow", "Progressive Bow"}, {"Hawkeye", "Hawkeye"}, {"Bomb Bags", "Bomb Bag"}, {"Giant Bomb Bags", "Giant Bomb Bag"}, {"Clawshot", "Progressive Clawshot"}, {"Spinner", "Spinner"}, {"Ball and Chain", "Ball and Chain"}, {"Dominion Rod", "Progressive Dominion Rod"}, {"Empty Bottle", "Empty Bottle"}, {"Auru's Memo", "Aurus Memo"}, {"Ashei's Sketch", "Asheis Sketch"}, {"Sky Book", "Progressive Sky Book"}, {"Sword", "Progressive Sword"}, {"Ordon Shield", "Ordon Shield"}, {"Hylian Shield", "Hylian Shield"}, {"Zora Armor", "Zora Armor"}, {"Magic Armor", "Magic Armor"}, {"Wallet", "Progressive Wallet"}, {"Hidden Skills", "Progressive Hidden Skill"}, {"Poe Souls", "Poe Soul"}, {"Fused Shadows", "Progressive Fused Shadow"}, {"Mirror Shards", "Progressive Mirror Shard"}, {"Gate Keys", "Gate Keys"}, {"Gerudo Desert Bulblin Camp Key", "Gerudo Desert Bulblin Camp Key"}, {"Forest Temple Small Keys", "Forest Temple Small Key"}, {"Goron Mines Small Keys", "Goron Mines Small Key"}, {"Lakebed Temple Small Keys", "Lakebed Temple Small Key"}, {"Arbiter's Grounds Small Keys", "Arbiters Grounds Small Key"}, {"Snowpeak Ruins Small Keys", "Snowpeak Ruins Small Key"}, {"Ordon Pumpkin", "Ordon Pumpkin"}, {"Ordon Cheese", "Ordon Cheese"}, {"Temple of Time Small Keys", "Temple of Time Small Key"}, {"City in the Sky Small Keys", "City in the Sky Small Key"}, {"Palace of Twilight Small Keys", "Palace of Twilight Small Key"}, {"Hyrule Castle Small Keys", "Hyrule Castle Small Key"}, {"Forest Temple Big Key", "Forest Temple Big Key"}, {"Goron Mines Key Shards", "Goron Mines Key Shard"}, {"Lakebed Temple Big Key", "Lakebed Temple Big Key"}, {"Arbiter's Grounds Big Key", "Arbiters Grounds Big Key"}, {"Snowpeak Ruins Bedroom Key", "Snowpeak Ruins Bedroom Key"}, {"Temple of Time Big Key", "Temple of Time Big Key"}, {"City in the Sky Big Key", "City in the Sky Big Key"}, {"Palace of Twilight Big Key", "Palace of Twilight Big Key"}, {"Hyrule Castle Big Key", "Hyrule Castle Big Key"}, {"Gerudo Desert Portal", "Gerudo Desert Portal"}, {"Mirror Chamber Portal", "Mirror Chamber Portal"}, {"Snowpeak Portal", "Snowpeak Portal"}, {"Sacred Grove Portal", "Sacred Grove Portal"}, {"Bridge of Eldin Portal", "Bridge of Eldin Portal"}, {"Upper Zora's River Portal", "Upper Zoras River Portal"} }; return layoutOrder; } UiMenuTabHandle g_menu_tab{}; FileSelectGateWindowCtx g_file_select_window_ctx{}; namespace { // Control Helpers ModResult add_button(UiElementHandle pane, const char* label, const char* help_rml, UiPressedFn on_pressed, void* userdata = nullptr, UiElementHandle* out_handle = nullptr) { UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_BUTTON; desc.label = label; desc.help_rml = help_rml; desc.on_pressed = on_pressed; desc.user_data = userdata; return session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } void add_section(UiElementHandle pane, const char* label) { session::svc_mng.ui->pane_add_section(session::svc_mng.mod_ctx, pane, label); } void add_string_input(UiElementHandle pane, const char* label, const char* help_rml, int32_t max_length, UiControlGetFn getFn, UiControlSetFn setFn, UiElementHandle* out_handle = nullptr) { UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_STRING; desc.label = label; desc.help_rml = help_rml; desc.binding = UI_BINDING_CALLBACKS; desc.get = getFn; desc.set = setFn; desc.max_length = max_length; session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } void add_select(UiElementHandle pane, const char* label, const char* help_rml, const char** options, size_t option_count, UiControlGetFn getFn, UiControlSetFn setFn, UiElementHandle* out_handle = nullptr) { UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_SELECT; desc.label = label; desc.help_rml = help_rml; desc.binding = UI_BINDING_CALLBACKS; desc.get = getFn; desc.set = setFn; desc.options = options; desc.option_count = option_count; session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } void add_number_input(UiElementHandle pane, const char* label, const char* help_rml, int64_t min, int64_t max, int64_t step, UiControlGetFn getFn, UiControlSetFn setFn, UiElementHandle* out_handle = nullptr) { UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_NUMBER; desc.label = label; desc.help_rml = help_rml; desc.binding = UI_BINDING_CALLBACKS; desc.get = getFn; desc.set = setFn; desc.min = min; desc.max = max; desc.step = step; session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } void add_select_setting(UiElementHandle pane, const char* key, UiElementHandle* out_handle = nullptr) { auto setting = FindSetting(key); auto info = setting->GetInfo(); std::vector optionsList; std::string help_rml = ""; for (size_t i = 0; i < info->GetOptions().size(); ++i) { optionsList.push_back(info->GetOptions()[i].c_str()); help_rml += fmt::format("
{}: {}", info->GetOptions()[i], info->GetDescriptions()[i]); } auto getFn = [](ModContext*, void* user_data, UiControlValue* out_value) { auto setting = FindSetting(static_cast(user_data)); const auto& options = setting->GetInfo()->GetOptions(); for (size_t i = 0; i < options.size(); ++i) { if (options[i] == setting->GetCurrentOption()) { out_value->int_value = i; return; } } // default out_value->int_value = 0; }; auto setFn = [](ModContext*, void* user_data, const UiControlValue* value) { auto setting = FindSetting(static_cast(user_data)); const auto& options = setting->GetInfo()->GetOptions(); if (value->int_value >= 0 && value->int_value < options.size()) { setting->SetCurrentOption(options[value->int_value]); SaveRandomizerConfig(); } }; UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_SELECT; desc.label = key; desc.help_rml = help_rml.c_str(); desc.binding = UI_BINDING_CALLBACKS; desc.get = getFn; desc.set = setFn; desc.user_data = (void*)key; desc.options = optionsList.data(); desc.option_count = optionsList.size(); session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } void add_select_number_setting(UiElementHandle pane, const char* key, UiElementHandle* out_handle = nullptr) { auto setting = FindSetting(key); auto info = setting->GetInfo(); std::vector optionsList; for (size_t i = 0; i < info->GetOptions().size(); ++i) { optionsList.push_back(info->GetOptions()[i].c_str()); } auto getFn = [](ModContext*, void* user_data, UiControlValue* out_value) { auto setting = FindSetting(static_cast(user_data)); const auto& options = setting->GetInfo()->GetOptions(); for (size_t i = 0; i < options.size(); ++i) { if (options[i] == setting->GetCurrentOption()) { out_value->int_value = i; return; } } // default out_value->int_value = 0; }; auto setFn = [](ModContext*, void* user_data, const UiControlValue* value) { auto setting = FindSetting(static_cast(user_data)); const auto& options = setting->GetInfo()->GetOptions(); if (value->int_value >= 0 && value->int_value < options.size()) { setting->SetCurrentOption(options[value->int_value]); SaveRandomizerConfig(); } }; UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_SELECT; desc.label = key; desc.help_rml = ""; desc.binding = UI_BINDING_CALLBACKS; desc.get = getFn; desc.set = setFn; desc.user_data = (void*)key; desc.options = optionsList.data(); desc.option_count = optionsList.size(); session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } void add_number_setting(UiElementHandle pane, const char* label, const char* key, UiPredicateFn isDisabledFn, UiElementHandle* out_handle = nullptr) { std::string fullOptionalKey = key; // check if setting exists auto randoSettings = seedgen::settings::GetAllSettingsInfo(); if (!randoSettings->contains(fullOptionalKey)) { return; } auto curSetting = FindSetting(fullOptionalKey); const auto& options = curSetting->GetInfo()->GetOptions(); int64_t min = std::stoi(options.front()); int64_t max = std::stoi(options.back()); auto getFn = [](ModContext*, void* user_data, UiControlValue* out_value) { std::string fullOptionalKey = static_cast(user_data); // check if setting exists auto randoSettings = seedgen::settings::GetAllSettingsInfo(); if (!randoSettings->contains(fullOptionalKey)) { return; } auto setting = FindSetting(fullOptionalKey); out_value->int_value = setting->GetCurrentOptionAsNumber(); }; auto setFn = [](ModContext*, void* user_data, const UiControlValue* value) { std::string fullOptionalKey = static_cast(user_data); // check if setting exists auto randoSettings = seedgen::settings::GetAllSettingsInfo(); if (!randoSettings->contains(fullOptionalKey)) { return; } auto setting = FindSetting(fullOptionalKey); setting->SetCurrentOption(std::to_string(value->int_value)); }; UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_NUMBER; desc.label = label; desc.help_rml = ""; desc.binding = UI_BINDING_CALLBACKS; desc.get = getFn; desc.set = setFn; desc.is_disabled = isDisabledFn; desc.user_data = (void*)key; desc.min = min; desc.max = max; desc.step = 1; session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } // Seed Management Tab ModResult buildSeedManagementTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { add_button(leftPane, "Generate Seed", "Generate a Randomizer seed using the current configuration options, and the supplied seed string.", [](ModContext*, void*) { if (TryCreateRandomSeed()) { mods::log::info("Created new Seed for generator."); } GenerateRandomizerSeed(); }); add_string_input(leftPane, "Seed String", "Current value of the seed used by the randomizer for generation. Leave blank for a random value.", 31, [](ModContext*, void*, UiControlValue* out_value) { static char buffer[32]; strncpy(buffer,GetRandomizerConfig().GetSeed().c_str(),31); out_value->string_value = buffer; }, [](ModContext*, void*, const UiControlValue* value) { GetRandomizerConfig().SetSeed(value->string_value); SaveRandomizerConfig(); }); { std::filesystem::path seed_dir = paths::GetRandomizerSeedsPath(); if (!std::filesystem::exists(seed_dir)) std::filesystem::create_directory(seed_dir); std::string help_rml = "Select a seed above to delete it."; std::vector seedHashes; for (const auto& entry : std::filesystem::directory_iterator(seed_dir)) { if (entry.is_directory()) { seedHashes.push_back(entry.path().filename().string()); } } std::vector availableSeeds; for (const auto& hash : seedHashes) { availableSeeds.push_back(hash.c_str()); } add_select(leftPane, "Delete Seeds", help_rml.c_str(), availableSeeds.data(), availableSeeds.size(), [](ModContext*, void*, UiControlValue* out_value) { out_value->int_value = 0; }, [](ModContext*, void*, const UiControlValue* value) { int idx = 0; for (const auto& entry : std::filesystem::directory_iterator(paths::GetRandomizerSeedsPath())) { if (entry.is_directory()) { if (idx == value->int_value) { std::string hash = entry.path().filename().string(); if (randomizer_GetContext().mHash == hash) { randomizer_GetContext() = RandomizerContext{}; } std::filesystem::remove_all(entry); break; } idx++; } } }); } add_section(leftPane, "Permalink"); { std::string help_rml = "Copy your current settings permalink to share with others.
"; help_rml += fmt::format( "
Current Permalink:
{}", GetRandomizerConfig().GetPermalink()); add_button(leftPane, "Copy Permalink", help_rml.c_str(), [](ModContext*, void*) { mods::ui::set_clipboard_text(GetRandomizerConfig().GetPermalink().data()); UiToastDesc desc = UI_TOAST_DESC_INIT; desc.title_rml = "Randomizer"; desc.body_rml = "Permalink Copied"; desc.duration_ms = 3000; session::svc_mng.ui->push_toast(session::svc_mng.mod_ctx, &desc); }); } add_button(leftPane, "Paste Permalink", "Paste in a permalink from your clipboard. This will overwrite your current settings.", [](ModContext*, void*) { std::string text; ModResult rt = mods::ui::get_clipboard_text(text); if (rt != MOD_OK) { return; } auto result = GetRandomizerConfig().LoadFromPermalink(text); if (result.has_value()) { mods::log::error("Failed to load permalink: {}", result.value()); return; } SaveRandomizerConfig(); UiToastDesc desc = UI_TOAST_DESC_INIT; desc.title_rml = "Randomizer"; desc.body_rml = "Applied Permalink"; desc.duration_ms = 3000; session::svc_mng.ui->push_toast(session::svc_mng.mod_ctx, &desc); }); add_section(leftPane, "Presets"); add_button(leftPane, "Save Current Settings as Preset", "Save the current settings to your list of presets.", [](ModContext*, void*) { // TODO }); add_button(leftPane, "Load Preset", "Choose an existing preset to load from.", [](ModContext*, void*) { // TODO }); return MOD_OK; } ModResult updateSeedManagementTab(ModContext* ctx, void*, ModError*) { return MOD_OK; } // Seed Options Tab ModResult buildSeedOptionsTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { add_button(leftPane, "Reset Settings to Default", "Reset all settings to their default values. This will also clear starting items and excluded locations.", [](ModContext*, void*) { GetRandomizerConfig().ResetSettingsToDefault(); SaveRandomizerConfig(); }); add_section(leftPane, "Logic Settings"); add_select_setting(leftPane, "Logic Rules"); add_section(leftPane, "Access Options"); add_select_setting(leftPane, "Hyrule Barrier Requirements"); // pretty ugly way of doing this, but it works for now add_number_setting(leftPane, "Required Fused Shadows", "Hyrule Barrier Fused Shadows", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Barrier Requirements"); if (setting->GetCurrentOption() != "Fused Shadows") { return true; } return false; }); add_number_setting(leftPane, "Required Mirror Shards", "Hyrule Barrier Mirror Shards", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Barrier Requirements"); if (setting->GetCurrentOption() != "Mirror Shards") { return true; } return false; }); add_number_setting(leftPane, "Required Dungeons", "Hyrule Barrier Dungeons", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Barrier Requirements"); if (setting->GetCurrentOption() != "Dungeons") { return true; } return false; }); add_number_setting(leftPane, "Required Poe Souls", "Hyrule Barrier Poe Souls", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Barrier Requirements"); if (setting->GetCurrentOption() != "Poe Souls") { return true; } return false; }); add_number_setting(leftPane, "Required Hearts", "Hyrule Barrier Hearts", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Barrier Requirements"); if (setting->GetCurrentOption() != "Hearts") { return true; } return false; }); add_select_setting(leftPane, "Palace of Twilight Requirements"); add_select_setting(leftPane, "Faron Woods Logic"); add_select_setting(leftPane, "Mirror Chamber Access"); add_section(leftPane, "Shuffles"); add_select_setting(leftPane, "Golden Bugs"); add_select_setting(leftPane, "Sky Characters"); add_select_setting(leftPane, "Gifts From NPCs"); add_select_setting(leftPane, "Shop Items"); add_select_setting(leftPane, "Hidden Skills"); add_select_setting(leftPane, "Hidden Rupees"); add_select_setting(leftPane, "Freestanding Rupees"); add_select_setting(leftPane, "Poe Souls"); add_select_setting(leftPane, "Ilia Memory Quest"); add_select_setting(leftPane, "Item Scarcity"); add_select_setting(leftPane, "Trap Item Frequency"); add_section(leftPane, "Dungeon Items"); add_select_setting(leftPane, "Small Keys"); add_select_setting(leftPane, "Big Keys"); add_select_setting(leftPane, "Maps and Compasses"); add_select_setting(leftPane, "Hyrule Castle Big Key Requirements"); // pretty ugly way of doing this, but it works for now add_number_setting(leftPane, "Required Fused Shadows", "Hyrule Castle Big Key Fused Shadows", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Castle Big Key Requirements"); if (setting->GetCurrentOption() != "Fused Shadows") { return true; } return false; }); add_number_setting(leftPane, "Required Mirror Shards", "Hyrule Castle Big Key Mirror Shards", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Castle Big Key Requirements"); if (setting->GetCurrentOption() != "Mirror Shards") { return true; } return false; }); add_number_setting(leftPane, "Required Dungeons", "Hyrule Castle Big Key Dungeons", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Castle Big Key Requirements"); if (setting->GetCurrentOption() != "Dungeons") { return true; } return false; }); add_number_setting(leftPane, "Required Poe Souls", "Hyrule Castle Big Key Poe Souls", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Castle Big Key Requirements"); if (setting->GetCurrentOption() != "Poe Souls") { return true; } return false; }); add_number_setting(leftPane, "Required Hearts", "Hyrule Castle Big Key Hearts", [](ModContext*, void*) { auto setting = FindSetting("Hyrule Castle Big Key Requirements"); if (setting->GetCurrentOption() != "Hearts") { return true; } return false; }); add_select_setting(leftPane, "Dungeon Rewards Can Be Anywhere"); add_select_setting(leftPane, "No Small Keys on Bosses"); add_select_setting(leftPane, "Unrequired Dungeons Are Barren"); add_section(leftPane, "Timesavers"); add_select_setting(leftPane, "Skip Prologue"); add_select_setting(leftPane, "Faron Twilight Cleared"); add_select_setting(leftPane, "Eldin Twilight Cleared"); add_select_setting(leftPane, "Lanayru Twilight Cleared"); add_select_setting(leftPane, "Skip Midna's Desparate Hour"); add_select_setting(leftPane, "Skip Minor Cutscenes"); add_select_setting(leftPane, "Skip Major Cutscenes"); add_select_setting(leftPane, "Unlock Map Regions"); add_select_setting(leftPane, "Open Door of Time"); add_select_setting(leftPane, "Active Goron Mines Magnets"); add_select_setting(leftPane, "Lower Hyrule Castle Chandelier"); add_select_setting(leftPane, "Skip Bridge Donation"); add_section(leftPane, "Entrance Randomizer Settings"); add_select_setting(leftPane, "Randomize Starting Spawn"); add_select_setting(leftPane, "Randomize Dungeon Entrances"); add_select_setting(leftPane, "Randomize Boss Entrances"); add_select_setting(leftPane, "Randomize Grotto Entrances"); add_select_setting(leftPane, "Randomize Cave Entrances"); add_select_setting(leftPane, "Randomize Interior Entrances"); add_select_setting(leftPane, "Randomize Overworld Entrances"); add_select_setting(leftPane, "Decouple Double Door Entrances"); add_select_setting(leftPane, "Decouple Entrances"); add_section(leftPane, "Additional Settings"); add_select_setting(leftPane, "Starting Time of Day"); add_select_setting(leftPane, "Logic Transform Anywhere"); add_select_setting(leftPane, "Logic Increase Wallet Capacity"); add_select_setting(leftPane, "Logic Damage Multiplier"); add_section(leftPane, "Dungeon Entrance Settings"); add_select_setting(leftPane, "Lakebed Does Not Require Water Bombs"); add_select_setting(leftPane, "Arbiters Does Not Require Bulblin Camp"); add_select_setting(leftPane, "Snowpeak Does Not Require Reekfish Scent"); add_select_setting(leftPane, "Sacred Grove Does Not Require Skull Kid"); add_select_setting(leftPane, "City Does Not Require Filled Skybook"); add_select_setting(leftPane, "Goron Mines Entrance"); add_select_setting(leftPane, "Temple of Time Sword Requirement"); add_section(leftPane, "Tricks"); add_select_setting(leftPane, "Back Slice as Sword"); add_select_setting(leftPane, "Ball and Chain Webs"); return MOD_OK; } // Hints Tab ModResult buildHintsTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { add_section(leftPane, "Path Hints"); add_select_number_setting(leftPane, "Number of Path Hints"); add_select_setting(leftPane, "Path Hints on Midna"); add_select_setting(leftPane, "Path Hints on Hint Signs"); add_section(leftPane, "Barren Hints"); add_select_number_setting(leftPane, "Number of Barren Hints"); add_select_setting(leftPane, "Barren Hints on Midna"); add_select_setting(leftPane, "Barren Hints on Hint Signs"); add_section(leftPane, "Item Hints"); add_select_number_setting(leftPane, "Number of Item Hints"); add_select_setting(leftPane, "Item Hints on Midna"); add_select_setting(leftPane, "Item Hints on Hint Signs"); add_section(leftPane, "Location Hints"); add_select_number_setting(leftPane, "Number of Location Hints"); add_select_setting(leftPane, "Location Hints on Midna"); add_select_setting(leftPane, "Location Hints on Hint Signs"); add_select_setting(leftPane, "Prioritize Remote Location Hints"); return MOD_OK; } // Starting Inventory Tab UiElementHandle startingInventoryRmlElem = 0; template void on_pressed_starting_inventory_item(ModContext*, void* userdata) { const char* itemName = static_cast(userdata); auto& inventory = GetRandomizerConfig().GetSettings().GetModifiableStartingInventory(); int newCount = inventory[itemName] + 1; if (newCount > Max) { inventory.erase(itemName); } else { inventory.at(itemName) = newCount; } SaveRandomizerConfig(); mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR); } template void add_starting_inventory_item(UiElementHandle leftPane, const char* displayName, const char* itemName = "") { if (std::strcmp(itemName, "") == 0) { itemName = displayName; } UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_BUTTON; desc.label = displayName; desc.help_rml = ""; desc.binding = UI_BINDING_CALLBACKS; desc.on_pressed = on_pressed_starting_inventory_item; desc.user_data = (void*)itemName; session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, leftPane, &desc, nullptr); }; ModResult buildStartingInventoryTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { add_button(leftPane, "Clear Selected Starting Items", "", [](ModContext*, void*) { auto& inventory = GetRandomizerConfig().GetSettings().GetModifiableStartingInventory(); inventory.clear(); SaveRandomizerConfig(); }); add_section(leftPane, "Main Items"); add_starting_inventory_item(leftPane, "Shadow Crystal"); add_starting_inventory_item(leftPane, "Horse Call"); add_starting_inventory_item<2>(leftPane, "Fishing Rod", "Progressive Fishing Rod"); add_starting_inventory_item(leftPane, "Slingshot"); add_starting_inventory_item(leftPane, "Lantern"); add_starting_inventory_item(leftPane, "Gale Boomerang"); add_starting_inventory_item(leftPane, "Iron Boots"); add_starting_inventory_item<3>(leftPane, "Bow", "Progressive Bow"); add_starting_inventory_item(leftPane, "Hawkeye"); add_starting_inventory_item<3>(leftPane, "Bomb Bags", "Bomb Bag"); add_starting_inventory_item(leftPane, "Giant Bomb Bags", "Giant Bomb Bag"); add_starting_inventory_item<2>(leftPane, "Clawshot", "Progressive Clawshot"); add_starting_inventory_item(leftPane, "Spinner"); add_starting_inventory_item(leftPane, "Ball and Chain"); add_starting_inventory_item<2>(leftPane, "Dominion Rod", "Progressive Dominion Rod"); add_starting_inventory_item(leftPane, "Empty Bottle"); add_starting_inventory_item(leftPane, "Auru's Memo", "Aurus Memo"); add_starting_inventory_item(leftPane, "Ashei's Sketch", "Asheis Sketch"); add_starting_inventory_item<7>(leftPane, "Sky Book", "Progressive Sky Book"); add_section(leftPane, "Gear Screen"); add_starting_inventory_item<4>(leftPane, "Sword", "Progressive Sword"); add_starting_inventory_item(leftPane, "Ordon Shield"); add_starting_inventory_item(leftPane, "Hylian Shield"); add_starting_inventory_item(leftPane, "Zora Armor"); add_starting_inventory_item(leftPane, "Magic Armor"); add_starting_inventory_item<2>(leftPane, "Wallet", "Progressive Wallet"); add_starting_inventory_item<7>(leftPane, "Hidden Skills", "Progressive Hidden Skill"); add_starting_inventory_item<60>(leftPane, "Poe Souls", "Poe Soul"); add_starting_inventory_item<3>(leftPane, "Fused Shadows", "Progressive Fused Shadow"); add_starting_inventory_item<4>(leftPane, "Mirror Shards", "Progressive Mirror Shard"); add_section(leftPane, "Overworld Keys"); add_starting_inventory_item(leftPane, "Gate Keys"); add_starting_inventory_item(leftPane, "Gerudo Desert Bulblin Camp Key"); add_section(leftPane, "Dungeon Items"); add_starting_inventory_item<4>(leftPane, "Forest Temple Small Keys", "Forest Temple Small Key"); add_starting_inventory_item<3>(leftPane, "Goron Mines Small Keys", "Goron Mines Small Key"); add_starting_inventory_item<3>(leftPane, "Lakebed Temple Small Keys", "Lakebed Temple Small Key"); add_starting_inventory_item<5>(leftPane, "Arbiter's Grounds Small Keys", "Arbiters Grounds Small Key"); add_starting_inventory_item<4>(leftPane, "Snowpeak Ruins Small Keys", "Snowpeak Ruins Small Key"); add_starting_inventory_item(leftPane, "Ordon Pumpkin"); add_starting_inventory_item(leftPane, "Ordon Cheese"); add_starting_inventory_item<4>(leftPane, "Temple of Time Small Keys", "Temple of Time Small Key"); add_starting_inventory_item<1>(leftPane, "City in the Sky Small Keys", "City in the Sky Small Key"); add_starting_inventory_item<7>(leftPane, "Palace of Twilight Small Keys", "Palace of Twilight Small Key"); add_starting_inventory_item<3>(leftPane, "Hyrule Castle Small Keys", "Hyrule Castle Small Key"); add_starting_inventory_item(leftPane, "Forest Temple Big Key"); add_starting_inventory_item<3>(leftPane, "Goron Mines Key Shards", "Goron Mines Key Shard"); add_starting_inventory_item(leftPane, "Lakebed Temple Big Key"); add_starting_inventory_item(leftPane, "Arbiter's Grounds Big Key", "Arbiters Grounds Big Key"); add_starting_inventory_item(leftPane, "Snowpeak Ruins Bedroom Key"); add_starting_inventory_item(leftPane, "Temple of Time Big Key"); add_starting_inventory_item(leftPane, "City in the Sky Big Key"); add_starting_inventory_item(leftPane, "Palace of Twilight Big Key"); add_starting_inventory_item(leftPane, "Hyrule Castle Big Key"); add_section(leftPane, "Warp Portals"); add_starting_inventory_item(leftPane, "Gerudo Desert Portal"); add_starting_inventory_item(leftPane, "Mirror Chamber Portal"); add_starting_inventory_item(leftPane, "Snowpeak Portal"); add_starting_inventory_item(leftPane, "Sacred Grove Portal"); add_starting_inventory_item(leftPane, "Bridge of Eldin Portal"); add_starting_inventory_item(leftPane, "Upper Zora's River Portal", "Upper Zoras River Portal"); add_section(rightPane, "Selected Starting Items"); session::svc_mng.ui->pane_add_rml(session::svc_mng.mod_ctx, rightPane, "", &startingInventoryRmlElem); return MOD_OK; } ModResult updateStartingInventoryTab(ModContext* ctx, void*, ModError*) { const auto& inventory = GetRandomizerConfig().GetSettings().GetStartingInventory(); const auto& layoutOrder = GetStartingInventoryLayoutOrder(); std::string rightPaneRml = ""; for (const auto& [itemText, itemName] : layoutOrder) { if (!inventory.contains(itemName)) { continue; } int count = inventory.at(itemName); if (count <= 0) { continue; } // If we have a prettier name for the item, prioritize that std::string prettyItemName = fmt::format("{} x{}", itemName, count); if (randomizer::textObjectExists(prettyItemName)) { rightPaneRml += fmt::format("• {}
", randomizer::getTextStr(prettyItemName)); } // Display the count before the itemname for these items else if (itemName.find("Small Key") != std::string::npos || itemName.find("Shard") != std::string::npos || itemName.find("Fused Shadow") != std::string::npos || itemName.find("Hidden Skill") != std::string::npos || itemName == "Poe Soul" || itemName == "Bomb Bag") { rightPaneRml += fmt::format("• {} {}
", count, itemText); } else { rightPaneRml += fmt::format("• {}
", itemName); } } session::svc_mng.ui->elem_set_rml(session::svc_mng.mod_ctx, startingInventoryRmlElem, rightPaneRml.c_str()); return MOD_OK; } // Excluded Locations Tab UiElementHandle exlocHeaderElem = 0; UiElementHandle exlocSubHeaderElem = 0; UiElementHandle exlocRightPaneElem = 0; UiElementHandle exlocLeftPaneElem = 0; UiElementHandle exlocClearBtnElem = 0; // UiElementHandle exlocFilterInputElem = 0; std::string exlocFilter{}; struct ExcludedTabLocData { std::string name {}; std::string lowercaseName{}; std::unordered_set categories{}; }; auto& getExcludedLocationsList() { static std::list locationsForExcludedTab; // If we haven't loaded the locations to display for the excluded locations tab, load them up if (locationsForExcludedTab.empty()) { auto locationDataTree = LOAD_EMBED_YAML(RANDO_DATA_PATH "locations.yaml"); for (const auto& locationNode : locationDataTree) { ExcludedTabLocData excludedTabLocData{}; auto& name = excludedTabLocData.name; auto& lowercaseName = excludedTabLocData.lowercaseName; name = locationNode["Name"].as(); lowercaseName = name; std::transform(lowercaseName.begin(), lowercaseName.end(), lowercaseName.begin(), [](unsigned char c) { return std::tolower(c); }); for (const auto& category : locationNode["Categories"]) { excludedTabLocData.categories.insert(category.as()); } if (locationNode["Metadata"].IsMap()) { for (const auto& data : locationNode["Metadata"]) { excludedTabLocData.categories.insert(data.first.as()); } } // Don't include warp portals if (excludedTabLocData.categories.contains("Warp Portal")) { continue; } // Certain locations we don't include for now if (randomizer::utility::str::Contains(excludedTabLocData.name, "Renados Letter", "Telma Invoice", "Wooden Statue", "Ilia Charm", "Defeat Ganondorf", "Twilit Insect", "Twilit Bloat", "Hint")) { continue; } locationsForExcludedTab.push_back(excludedTabLocData); } locationsForExcludedTab.sort([](const auto& a, const auto& b) { return a.name < b.name; }); } // Create the vector we're going to return static std::vector locationNames{}; locationNames.clear(); // Get settings values auto& randoSettings = GetRandomizerConfig().GetSettings().GetMap(); bool goldenBugs = randoSettings.at("Golden Bugs") == "On"; bool skyCharacters = randoSettings.at("Sky Characters") == "On"; bool npcs = randoSettings.at("Gifts From NPCs") == "On"; bool shops = randoSettings.at("Shop Items") == "On"; bool goldenWolves = randoSettings.at("Hidden Skills") == "On"; bool hiddenRupees = randoSettings.at("Hidden Rupees") == "On"; bool freestandingRupees = randoSettings.at("Freestanding Rupees") == "On"; bool overworldPoes = randoSettings.at("Poe Souls").IsAnyOf("Overworld", "All"); bool dungeonPoes = randoSettings.at("Poe Souls").IsAnyOf("Dungeon", "All"); // Create lowercase filter std::string lowercaseFilter = exlocFilter; std::transform(lowercaseFilter.begin(), lowercaseFilter.end(), lowercaseFilter.begin(), [](unsigned char c) { return std::tolower(c); }); // Add relevant location names for (const auto& locData : locationsForExcludedTab) { // Skip categories that aren't shuffled auto& cats = locData.categories; if ((!goldenBugs && cats.contains("Golden Bug")) || (!skyCharacters && cats.contains("Sky Character")) || (!npcs && cats.contains("Npc")) || (!shops && cats.contains("Shop")) || (!goldenWolves && cats.contains("Golden Wolf")) || (!hiddenRupees && cats.contains("Rupee - Hidden")) || (!freestandingRupees && cats.contains("Rupee - Freestanding")) || (!overworldPoes && cats.contains("Poe") && cats.contains("Overworld")) || (!dungeonPoes && cats.contains("Poe") && cats.contains("Dungeon"))) { continue; } // Don't add this location if it doesn't match the current filter if (locData.lowercaseName.find(lowercaseFilter) == std::string::npos) { continue; } locationNames.push_back(&locData.name); } return locationNames; } ModResult buildExcludedLocationsTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { using namespace session; auto mod_ctx = svc_mng.mod_ctx; svc_mng.ui->elem_set_class(mod_ctx, leftPane, "excluded-locations-pane-left", true); svc_mng.ui->elem_set_class(mod_ctx, rightPane, "excluded-locations-pane-right", true); add_button(leftPane, "Clear All", "", [](ModContext*, void*) { GetRandomizerConfig().GetSettings().GetModifiableExcludedLocations().clear(); }, nullptr, &exlocClearBtnElem); svc_mng.ui->elem_set_class(mod_ctx, exlocClearBtnElem, "clear-all-button", true); // TODO: leaving out filter for now, since we're building all the buttons at tab init. // try to reimplement later when UiService is able to support something closer to the original impl /* add_string_input(leftPane, "Filter", "", 256, [](ModContext*, void*, UiControlValue* out_value) { out_value->string_value = exlocFilter.c_str(); }, [](ModContext*, void*, const UiControlValue* value) { exlocFilter = value->string_value; }, &exlocFilterInputElem); svc_mng.ui->elem_set_class(mod_ctx, exlocFilterInputElem, "filter-input", true); */ svc_mng.ui->pane_add_rml(mod_ctx, leftPane, "", &exlocLeftPaneElem); svc_mng.ui->elem_set_class(mod_ctx, exlocLeftPaneElem, "excluded-locations-pane", true); auto exlocList = getExcludedLocationsList(); for (auto e : exlocList) { UiElementHandle handle{}; add_button(leftPane, e->c_str(), "", [](ModContext*, void* userdata) { std::string locationName = static_cast(userdata); auto& excludedLocations = GetRandomizerConfig().GetSettings().GetModifiableExcludedLocations(); if (excludedLocations.contains(locationName)) { excludedLocations.erase(locationName); } else { excludedLocations.insert(locationName); } SaveRandomizerConfig(); }, (void*)e->data(), &handle); svc_mng.ui->elem_set_class(mod_ctx, handle, "excluded-location-button", true); } svc_mng.ui->pane_add_text(mod_ctx, rightPane, "Current Excluded Locations", &exlocHeaderElem); svc_mng.ui->elem_set_class(mod_ctx, exlocHeaderElem, "excluded-locations-header", true); svc_mng.ui->pane_add_text(mod_ctx, rightPane, "Re-select a location to remove it", &exlocSubHeaderElem); svc_mng.ui->elem_set_class(mod_ctx, exlocSubHeaderElem, "excluded-locations-subheader", true); svc_mng.ui->pane_add_rml(mod_ctx, rightPane, "", &exlocRightPaneElem); svc_mng.ui->elem_set_class(mod_ctx, exlocRightPaneElem, "excluded-locations-inner-pane-right", true); return MOD_OK; } ModResult updateExcludedLocationsTab(ModContext* ctx, void*, ModError*) { using namespace session; std::string rml = ""; for (const auto& location : GetRandomizerConfig().GetSettings().GetExcludedLocations()) { rml += fmt::format("• {}
", location); } svc_mng.ui->elem_set_rml(svc_mng.mod_ctx, exlocRightPaneElem, rml.c_str()); return MOD_OK; } // Menu Tab void OnMenuTabSelected(ModContext* ctx, void*) { UiTabDesc tabs[5]{}; tabs[0].struct_size = sizeof(UiTabDesc); tabs[0].title = "Seed Management"; tabs[0].build = buildSeedManagementTab; tabs[0].update = updateSeedManagementTab; tabs[1].struct_size = sizeof(UiTabDesc); tabs[1].title = "Seed Options"; tabs[1].build = buildSeedOptionsTab; tabs[2].struct_size = sizeof(UiTabDesc); tabs[2].title = "Hints"; tabs[2].build = buildHintsTab; tabs[3].struct_size = sizeof(UiTabDesc); tabs[3].title = "Starting Inventory"; tabs[3].build = buildStartingInventoryTab; tabs[3].update = updateStartingInventoryTab; tabs[4].struct_size = sizeof(UiTabDesc); tabs[4].title = "Excluded Locations"; tabs[4].build = buildExcludedLocationsTab; tabs[4].update = updateExcludedLocationsTab; UiWindowDesc desc = UI_WINDOW_DESC_INIT; desc.tabs = tabs; desc.tab_count = 5; UiWindowHandle window{}; session::svc_mng.ui->window_push(ctx, &desc, &window); } // Play Tab ModResult buildPlayTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { std::filesystem::path seed_dir = paths::GetRandomizerSeedsPath(); if (!std::filesystem::exists(seed_dir)) std::filesystem::create_directory(seed_dir); std::string help_rml = ""; if (std::filesystem::is_empty(seed_dir)) { help_rml = "No seeds generated! You can generate a seed from the Seed Management Tab."; } else { help_rml = "Choose which seed you want to play."; } std::vector seedHashes; for (const auto& entry : std::filesystem::directory_iterator(seed_dir)) { if (entry.is_directory()) { seedHashes.push_back(entry.path().filename().string()); } } std::vector availableSeeds; for (const auto& hash : seedHashes) { availableSeeds.push_back(hash.c_str()); } add_select(leftPane, "Selected Seed", help_rml.c_str(), availableSeeds.data(), availableSeeds.size(), [](ModContext*, void*, UiControlValue* out_value) { int idx = 0; for (const auto& entry : std::filesystem::directory_iterator(paths::GetRandomizerSeedsPath())) { if (entry.is_directory()) { std::string hash = entry.path().filename().string(); if (session::g_pending_seed_hash == hash) { break; } idx++; } } out_value->int_value = idx; }, [](ModContext*, void*, const UiControlValue* value) { int idx = 0; for (const auto& entry : std::filesystem::directory_iterator(paths::GetRandomizerSeedsPath())) { if (entry.is_directory()) { if (idx == value->int_value) { session::g_pending_seed_hash = entry.path().filename().string(); break; } idx++; } } }); { UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_BUTTON; desc.label = "Start Randomizer"; desc.help_rml = ""; desc.on_pressed = [](ModContext*, void* userdata) { // set flag to move to name screen after window close g_file_select_window_ctx.is_proceed = true; session::svc_mng.ui->window_close(session::svc_mng.mod_ctx, *static_cast(userdata)); mDoAud_seStartMenu(Z2SE_SY_NEW_FILE); }; desc.user_data = &g_file_select_window_ctx.window_handle; desc.is_disabled = [](ModContext*, void*) { return session::g_pending_seed_hash.empty(); }; session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, leftPane, &desc, nullptr); } return MOD_OK; } } ModResult buildMenuTab() { UiMenuTabDesc desc = UI_MENU_TAB_DESC_INIT; desc.label = "Randomizer"; desc.on_selected = OnMenuTabSelected; return session::svc_mng.ui->register_menu_tab(session::svc_mng.mod_ctx, &desc, &g_menu_tab); } ModResult buildFileSelectGateMenu(dFile_select_c* fileSelect) { UiTabDesc tabs[6]{}; tabs[0].struct_size = sizeof(UiTabDesc); tabs[0].title = "Play"; tabs[0].build = buildPlayTab; tabs[1].struct_size = sizeof(UiTabDesc); tabs[1].title = "Seed Management"; tabs[1].build = buildSeedManagementTab; tabs[1].update = updateSeedManagementTab; tabs[2].struct_size = sizeof(UiTabDesc); tabs[2].title = "Seed Options"; tabs[2].build = buildSeedOptionsTab; tabs[3].struct_size = sizeof(UiTabDesc); tabs[3].title = "Hints"; tabs[3].build = buildHintsTab; tabs[4].struct_size = sizeof(UiTabDesc); tabs[4].title = "Starting Inventory"; tabs[4].build = buildStartingInventoryTab; tabs[4].update = updateStartingInventoryTab; tabs[5].struct_size = sizeof(UiTabDesc); tabs[5].title = "Excluded Locations"; tabs[5].build = buildExcludedLocationsTab; tabs[5].update = updateExcludedLocationsTab; UiWindowDesc desc = UI_WINDOW_DESC_INIT; desc.tabs = tabs; desc.tab_count = 6; desc.user_data = fileSelect; desc.on_closed = [](ModContext*, UiWindowHandle, void* userdata) { dFile_select_c* i_this = static_cast(userdata); // if closing the window through backing out, return to file select if (!g_file_select_window_ctx.is_proceed) { i_this->headerTxtSet(0x43, 1, 0); i_this->fileRecScaleAnmInitSet2(0.0f, 1.0f); i_this->nameMoveAnmInitSet(0xd29, 0xd1f); i_this->modoruTxtDispAnmInit(0); i_this->mDataSelProc = dFile_select_c::DATASELPROC_NAME_TO_DATA_SELECT_MOVE; } g_dialogSelectModeState = SelectReady; }; return session::svc_mng.ui->window_push(session::svc_mng.mod_ctx, &desc, &g_file_select_window_ctx.window_handle); } } // namespace dusk::ui