diff --git a/soh/soh/Enhancements/randomizer/option_descriptions.cpp b/soh/soh/Enhancements/randomizer/option_descriptions.cpp index 3eddec7e642..7a49b5672ef 100644 --- a/soh/soh/Enhancements/randomizer/option_descriptions.cpp +++ b/soh/soh/Enhancements/randomizer/option_descriptions.cpp @@ -50,6 +50,16 @@ void Settings::CreateOptionDescriptions() { "Choose which age Link will start as.\n\n" "Starting as adult means you start with the Master Sword in your inventory.\n" "The child option is forcefully set if it would conflict with other options."; + mOptionDescriptions[RSK_RANDOMIZE_SETTINGS] = + "Randomize settings each time a seed is generated using the seed RNG.\n\n" + "Off - Use the exact settings you selected.\n" + "On (No Entrance Rando) - Randomize settings, excluding entrance shuffle settings.\n" + "On (Entrance Rando) - Also randomize entrance shuffle settings.\n" + "On (Entrance Rando + Decoupled) - Same as above but Decouple Entrance Setting is also randomized.\n\n" + "Logic, Excluded Locations, Starting Items and Tricks are never randomized. Starting Age is always randomized"; + mOptionDescriptions[RSK_RANDOMIZE_SETTINGS_INCLUDE_MQ] = + "When Randomize Settings Per Seed is enabled, this also includes MQ dungeon-related settings in that " + "randomization."; mOptionDescriptions[RSK_GERUDO_FORTRESS] = "Sets the state of the carpenters captured by Gerudo " "in Gerudo Fortress, and with it the number of guards that spawn.\n" diff --git a/soh/soh/Enhancements/randomizer/randomizerTypes.h b/soh/soh/Enhancements/randomizer/randomizerTypes.h index 9a8b7fe4420..72ad9ae9cca 100644 --- a/soh/soh/Enhancements/randomizer/randomizerTypes.h +++ b/soh/soh/Enhancements/randomizer/randomizerTypes.h @@ -6667,6 +6667,8 @@ typedef enum { RSK_LOCK_OVERWORLD_DOORS, RSK_SHUFFLE_GRASS, RSK_ROCS_FEATHER, + RSK_RANDOMIZE_SETTINGS, + RSK_RANDOMIZE_SETTINGS_INCLUDE_MQ, RSK_MAX } RandomizerSettingKey; @@ -6729,6 +6731,14 @@ typedef enum { RO_AGE_RANDOM, } RandoOptionStartingAge; +// Randomize Settings settings (off, on with entrance handling modes) +typedef enum { + RO_RANDOMIZE_SETTINGS_OFF, + RO_RANDOMIZE_SETTINGS_EXCLUDE_ENTRANCES, + RO_RANDOMIZE_SETTINGS_INCLUDE_ENTRANCES, + RO_RANDOMIZE_SETTINGS_INCLUDE_ENTRANCES_DECOUPLED, +} RandoOptionRandomizeSettings; + // Fortress Carpenters settings (normal, fast, free) typedef enum { RO_GF_CARPENTERS_NORMAL, diff --git a/soh/soh/Enhancements/randomizer/settings.cpp b/soh/soh/Enhancements/randomizer/settings.cpp index 37c571b1459..0302aad547e 100644 --- a/soh/soh/Enhancements/randomizer/settings.cpp +++ b/soh/soh/Enhancements/randomizer/settings.cpp @@ -279,6 +279,13 @@ void Settings::CreateOptions() { OPT_U8(RSK_TRIAL_COUNT, "Ganon's Trials Count", {NumOpts(0, 6)}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("GanonTrialCount"), mOptionDescriptions[RSK_TRIAL_COUNT], WIDGET_CVAR_SLIDER_INT, 6, true); OPT_BOOL(RSK_MEDALLION_LOCKED_TRIALS, "Medallion Locked Trials", CVAR_RANDOMIZER_SETTING("MedallionLockedTrials"), mOptionDescriptions[RSK_MEDALLION_LOCKED_TRIALS]); OPT_U8(RSK_STARTING_AGE, "Starting Age", {"Child", "Adult", "Random"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("StartingAge"), mOptionDescriptions[RSK_STARTING_AGE], WIDGET_CVAR_COMBOBOX, RO_AGE_CHILD); + OPT_U8(RSK_RANDOMIZE_SETTINGS, "Randomize Settings Per Seed", + {"Off", "On", "On + Entrance Rando", "On + Entrance Rando + Decoupled"}, + OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("RandomizeSettings"), + mOptionDescriptions[RSK_RANDOMIZE_SETTINGS], WIDGET_CVAR_COMBOBOX, RO_RANDOMIZE_SETTINGS_OFF); + OPT_BOOL(RSK_RANDOMIZE_SETTINGS_INCLUDE_MQ, "Include MQ Dungeon Settings", + CVAR_RANDOMIZER_SETTING("RandomizeSettingsIncludeMQ"), + mOptionDescriptions[RSK_RANDOMIZE_SETTINGS_INCLUDE_MQ], IMFLAG_NONE, WIDGET_CVAR_CHECKBOX, false); OPT_U8(RSK_SELECTED_STARTING_AGE, "Selected Starting Age", {"Child", "Adult"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("SelectedStartingAge"), mOptionDescriptions[RSK_STARTING_AGE], WIDGET_CVAR_COMBOBOX, RO_AGE_CHILD); OPT_BOOL(RSK_SHUFFLE_ENTRANCES, "Shuffle Entrances"); OPT_U8(RSK_SHUFFLE_DUNGEON_ENTRANCES, "Dungeon Entrances", {"Off", "On", "On + Ganon"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("ShuffleDungeonsEntrances"), mOptionDescriptions[RSK_SHUFFLE_DUNGEON_ENTRANCES], WIDGET_CVAR_COMBOBOX, RO_DUNGEON_ENTRANCE_SHUFFLE_OFF); @@ -1280,6 +1287,7 @@ void Settings::CreateOptions() { OPT_U8(RSK_DAMAGE_MULTIPLIER, "Damage Multiplier", {"x1/2", "x1", "x2", "x4", "x8", "x16", "OHKO"}, OptionCategory::Setting, "", "", WIDGET_CVAR_SLIDER_INT, RO_DAMAGE_MULTIPLIER_DEFAULT); // Don't show any MQ options if both quests aren't available if (!(OTRGlobals::Instance->HasMasterQuest() && OTRGlobals::Instance->HasOriginal())) { + mOptions[RSK_RANDOMIZE_SETTINGS_INCLUDE_MQ].Disable("This Options has been disabled because only one type of OTR has been loaded"); mOptions[RSK_MQ_DUNGEON_RANDOM].Disable("This Options has been disabled because only one type of OTR has been loaded"); mOptions[RSK_MQ_DUNGEON_COUNT].Disable("This Options has been disabled because only one type of OTR has been loaded"); mOptions[RSK_MQ_DUNGEON_SET].Disable("This Options has been disabled because only one type of OTR has been loaded"); @@ -1297,6 +1305,7 @@ void Settings::CreateOptions() { mOptions[RSK_MQ_GANONS_CASTLE].Disable("This Options has been disabled because only one type of OTR has been loaded"); } else { // If any MQ Options are available, show the MQ Dungeon Randomization Combobox + mOptions[RSK_RANDOMIZE_SETTINGS_INCLUDE_MQ].Enable(); mOptions[RSK_MQ_DUNGEON_RANDOM].Enable(); mOptions[RSK_MQ_DUNGEON_COUNT].Enable(); mOptions[RSK_MQ_DUNGEON_SET].Enable(); @@ -2972,6 +2981,95 @@ void Settings::UpdateAllOptions() { void Context::FinalizeSettings(const std::set& excludedLocations, const std::set& enabledTricks) { + + // Randomize settings based on selected mode. + const uint8_t randomizeSettingsMode = mOptions[RSK_RANDOMIZE_SETTINGS].Get(); + if (randomizeSettingsMode != RO_RANDOMIZE_SETTINGS_OFF) { + const bool includeEntranceSettings = randomizeSettingsMode >= RO_RANDOMIZE_SETTINGS_INCLUDE_ENTRANCES; + const bool includeDecoupledEntrances = + randomizeSettingsMode == RO_RANDOMIZE_SETTINGS_INCLUDE_ENTRANCES_DECOUPLED; + const bool includeMqSettings = mOptions[RSK_RANDOMIZE_SETTINGS_INCLUDE_MQ] && + OTRGlobals::Instance->HasMasterQuest() && OTRGlobals::Instance->HasOriginal(); + + auto settings = Rando::Settings::GetInstance(); + const auto addGroupKeys = [settings](std::set& keySet, + const RandomizerSettingGroupKey groupKey) { + for (const auto* option : settings->GetOptionGroup(groupKey).GetOptions()) { + keySet.insert(option->GetKey()); + } + }; + + std::set alwaysRandomizedSettingKeys; + const std::array alwaysRandomizedGroups = { + RSG_MENU_SECTION_WINCON, RSG_MENU_SECTION_AREA_ACCESS, RSG_MENU_SECTION_DUNGEON_ITEMS, + RSG_MENU_SECTION_KEYRINGS, RSG_MENU_SECTION_BASIC_SHUFFLES, RSG_MENU_SECTION_SHOP_SHUFFLES, + RSG_MENU_SECTION_ADDITIONAL_ITEMS + }; + for (const auto groupKey : alwaysRandomizedGroups) { + addGroupKeys(alwaysRandomizedSettingKeys, groupKey); + } + alwaysRandomizedSettingKeys.insert(RSK_STARTING_AGE); + + std::set mqSettingKeys; + addGroupKeys(mqSettingKeys, RSG_MENU_SECTION_MQ); + + std::set entranceSettingKeys; + addGroupKeys(entranceSettingKeys, RSG_MENU_SECTION_ENTRANCES); + if (!includeDecoupledEntrances) { + entranceSettingKeys.erase(RSK_DECOUPLED_ENTRANCES); + mOptions[RSK_DECOUPLED_ENTRANCES].Set(RO_GENERIC_OFF); // Prevents setting leak if decoupled is not in use + } + + if (!includeEntranceSettings) { // Prevents setting leak if entrance rando is not in use + mOptions[RSK_SHUFFLE_DUNGEON_ENTRANCES].Set(RO_DUNGEON_ENTRANCE_SHUFFLE_OFF); + mOptions[RSK_SHUFFLE_BOSS_ENTRANCES].Set(RO_BOSS_ROOM_ENTRANCE_SHUFFLE_OFF); + mOptions[RSK_SHUFFLE_GANONS_TOWER_ENTRANCE].Set(RO_GENERIC_OFF); + mOptions[RSK_SHUFFLE_OVERWORLD_ENTRANCES].Set(RO_GENERIC_OFF); + mOptions[RSK_SHUFFLE_INTERIOR_ENTRANCES].Set(RO_INTERIOR_ENTRANCE_SHUFFLE_OFF); + mOptions[RSK_SHUFFLE_THIEVES_HIDEOUT_ENTRANCES].Set(RO_GENERIC_OFF); + mOptions[RSK_SHUFFLE_GROTTO_ENTRANCES].Set(RO_GENERIC_OFF); + mOptions[RSK_SHUFFLE_OWL_DROPS].Set(RO_GENERIC_OFF); + mOptions[RSK_SHUFFLE_WARP_SONGS].Set(RO_GENERIC_OFF); + mOptions[RSK_SHUFFLE_OVERWORLD_SPAWNS].Set(RO_GENERIC_OFF); + mOptions[RSK_MIXED_ENTRANCE_POOLS].Set(RO_GENERIC_OFF); + mOptions[RSK_MIX_DUNGEON_ENTRANCES].Set(RO_GENERIC_OFF); + mOptions[RSK_MIX_BOSS_ENTRANCES].Set(RO_GENERIC_OFF); + mOptions[RSK_MIX_OVERWORLD_ENTRANCES].Set(RO_GENERIC_OFF); + mOptions[RSK_MIX_INTERIOR_ENTRANCES].Set(RO_GENERIC_OFF); + mOptions[RSK_MIX_THIEVES_HIDEOUT_ENTRANCES].Set(RO_GENERIC_OFF); + mOptions[RSK_MIX_GROTTO_ENTRANCES].Set(RO_GENERIC_OFF); + mOptions[RSK_DECOUPLED_ENTRANCES].Set(RO_GENERIC_OFF); + } + + for (size_t i = 0; i < RSK_MAX; i++) { + const auto key = static_cast(i); + + const bool isEntranceSetting = entranceSettingKeys.contains(key); + if (isEntranceSetting && !includeEntranceSettings) { + continue; + } + + const bool shouldRandomizeThisSetting = alwaysRandomizedSettingKeys.contains(key) || + (isEntranceSetting && includeEntranceSettings) || + (includeMqSettings && mqSettingKeys.contains(key)); + if (!shouldRandomizeThisSetting) { + continue; + } + + auto& setting = settings->GetOption(key); + if (!setting.IsCategory(OptionCategory::Setting)) { + continue; + } + + const size_t optionCount = setting.GetOptionCount(); + if (optionCount <= 1) { + continue; + } + + mOptions[i].Set(Random(0, static_cast(optionCount))); + } + } + // if we skip child zelda, we start with zelda's letter, and malon starts // at the ranch, so we should *not* shuffle the weird egg if (mOptions[RSK_SKIP_CHILD_ZELDA]) { diff --git a/soh/soh/SohGui/SohMenuRandomizer.cpp b/soh/soh/SohGui/SohMenuRandomizer.cpp index a2d33154a6b..9306c6e5c48 100644 --- a/soh/soh/SohGui/SohMenuRandomizer.cpp +++ b/soh/soh/SohGui/SohMenuRandomizer.cpp @@ -20,6 +20,13 @@ static const std::map skipGetItemAnimationOptions = { { SGIA_ALL, "All Items" }, }; +static const std::map randomizeSettingsModeOptions = { + { RO_RANDOMIZE_SETTINGS_OFF, "Off" }, + { RO_RANDOMIZE_SETTINGS_EXCLUDE_ENTRANCES, "On" }, + { RO_RANDOMIZE_SETTINGS_INCLUDE_ENTRANCES, "On + Entrance Rando" }, + { RO_RANDOMIZE_SETTINGS_INCLUDE_ENTRANCES_DECOUPLED, "On + Entrance Rando + Decoupled" }, +}; + static bool locationsDirty = true; static bool tricksDirty = true; static int32_t prevMQDungeonSetting; @@ -530,6 +537,33 @@ void SohMenu::AddMenuRandomizer() { WIDGET_TEXT) .Options(TextOptions().Color(UIWidgets::Colors::Gray)); AddWidget(path, "Seed Entry", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Randomize Settings Per Seed", WIDGET_CVAR_COMBOBOX) + .CVar(CVAR_RANDOMIZER_SETTING("RandomizeSettings")) + .Options( + ComboboxOptions() + .ComboMap(randomizeSettingsModeOptions) + .DefaultIndex(RO_RANDOMIZE_SETTINGS_OFF) + .Tooltip( + "Randomize settings each time a seed is generated.\n\n" + "Off - Settings are not randomized.\n" + "On - Randomize settings.\n" + "On + Entrance Rando - Randomize settings + entrances.\n" + "On + Entrance Rando + Decoupled - Same as above, plus Decoupled Entrances can be randomized.\n\n" + "Logic, Excluded Locations, Starting Items and Tricks are never randomized.\n" + "Starting age is always randomized.")); + AddWidget(path, "Include MQ Dungeon Settings", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_RANDOMIZER_SETTING("RandomizeSettingsIncludeMQ")) + .PreFunc([](WidgetInfo& info) { + const bool hasBothOtrs = OTRGlobals::Instance->HasMasterQuest() && OTRGlobals::Instance->HasOriginal(); + const bool randomizeSettingsEnabled = + CVarGetInteger(CVAR_RANDOMIZER_SETTING("RandomizeSettings"), RO_RANDOMIZE_SETTINGS_OFF) != + RO_RANDOMIZE_SETTINGS_OFF; + info.isHidden = !hasBothOtrs || !randomizeSettingsEnabled; + if (info.isHidden) { + CVarSetInteger(CVAR_RANDOMIZER_SETTING("RandomizeSettingsIncludeMQ"), 0); + } + }) + .Options(CheckboxOptions().DefaultValue(false).Tooltip("If enabled, MQ dungeon settings are also randomized.")); AddWidget(path, "Manual seed entry", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_RANDOMIZER_SETTING("ManualSeedEntry")) .Options(CheckboxOptions().DefaultValue(true));