diff --git a/mm/2s2h/Rando/Logic/EntranceShuffle.cpp b/mm/2s2h/Rando/Logic/EntranceShuffle.cpp new file mode 100644 index 0000000000..1a313425a7 --- /dev/null +++ b/mm/2s2h/Rando/Logic/EntranceShuffle.cpp @@ -0,0 +1,223 @@ +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/ShipInit.hpp" +#include "EntranceShuffle.h" +#include "Logic.h" + +extern "C" { +#include "z64scene.h" +} + +namespace Rando { + +namespace EntranceShuffle { + +// Entrance mapping storage +std::map sEntranceMap; + +// Optimally, these lists are dynamically built from the maps we already have built for logic... + +std::set interiorEntrances = { + // Clock Town + ENTRANCE(ASTRAL_OBSERVATORY, 0), + ENTRANCE(TREASURE_CHEST_SHOP, 0), + ENTRANCE(HONEY_AND_DARLINGS_SHOP, 0), + ENTRANCE(MAYORS_RESIDENCE, 0), + ENTRANCE(TOWN_SHOOTING_GALLERY, 0), + ENTRANCE(STOCK_POT_INN, 0), + ENTRANCE(STOCK_POT_INN, 1), + ENTRANCE(MILK_BAR, 0), + ENTRANCE(CURIOSITY_SHOP, 1), + ENTRANCE(FAIRY_FOUNTAIN, 0), + ENTRANCE(CLOCK_TOWER_INTERIOR, 1), + ENTRANCE(SWORDMANS_SCHOOL, 0), + ENTRANCE(CURIOSITY_SHOP, 0), + ENTRANCE(TRADING_POST, 0), + ENTRANCE(BOMB_SHOP, 0), + ENTRANCE(POST_OFFICE, 0), + ENTRANCE(LOTTERY_SHOP, 0), + // Termina Field & Roads + ENTRANCE(SWAMP_SHOOTING_GALLERY, 0), + ENTRANCE(TOURIST_INFORMATION, 0), + ENTRANCE(MAGIC_HAGS_POTION_SHOP, 0), + // Milk Road + ENTRANCE(CUCCO_SHACK, 0), + ENTRANCE(DOGGY_RACETRACK, 0), + ENTRANCE(RANCH_HOUSE, 0), + ENTRANCE(RANCH_HOUSE, 1), + // Mountain Village + ENTRANCE(GORON_SHOP, 0), + ENTRANCE(MOUNTAIN_SMITHY, 0), + // Great Bay + ENTRANCE(FISHERMANS_HUT, 0), + ENTRANCE(MARINE_RESEARCH_LAB, 0), + ENTRANCE(ZORA_HALL_ROOMS, 0), + ENTRANCE(ZORA_HALL_ROOMS, 1), + ENTRANCE(ZORA_HALL_ROOMS, 2), + ENTRANCE(ZORA_HALL_ROOMS, 3), + ENTRANCE(ZORA_HALL_ROOMS, 5), + ENTRANCE(OCEANSIDE_SPIDER_HOUSE, 0), + // Ikana + ENTRANCE(GHOST_HUT, 0), + ENTRANCE(MUSIC_BOX_HOUSE, 0), + // Great Fairy Fountains + ENTRANCE(FAIRY_FOUNTAIN, 1), + ENTRANCE(FAIRY_FOUNTAIN, 2), + ENTRANCE(FAIRY_FOUNTAIN, 3), + ENTRANCE(FAIRY_FOUNTAIN, 4), + // Other + ENTRANCE(SWAMP_SPIDER_HOUSE, 0), +}; + +std::set dungeonEntrances = { + ENTRANCE(WOODFALL_TEMPLE, 0), + ENTRANCE(SNOWHEAD_TEMPLE, 0), + ENTRANCE(GREAT_BAY_TEMPLE, 0), +}; + +std::vector ConvertSetToEntrancePairs(const std::set& entranceSet) { + std::vector entrancePairs; + for (s32 entrance : entranceSet) { + auto randoRegionId = Rando::Logic::GetRegionIdFromEntrance(entrance); + for (const auto& [exitId, regionExit] : Rando::Logic::Regions[randoRegionId].exits) { + if (regionExit.returnEntrance == entrance) { + entrancePairs.push_back({ entrance, exitId }); + break; + } + } + } + + return entrancePairs; +} + +std::vector GetInteriorEntrances() { + return ConvertSetToEntrancePairs(interiorEntrances); +} + +std::vector GetGrottoEntrances() { + return {}; +} + +std::vector GetDungeonEntrances() { + return ConvertSetToEntrancePairs(dungeonEntrances); +} + +std::vector GetOverworldEntrances() { + return { + { ENTRANCE(EAST_CLOCK_TOWN, 3), ENTRANCE(SOUTH_CLOCK_TOWN, 2) }, + { ENTRANCE(WEST_CLOCK_TOWN, 2), ENTRANCE(SOUTH_CLOCK_TOWN, 3) }, + { ENTRANCE(NORTH_CLOCK_TOWN, 2), ENTRANCE(SOUTH_CLOCK_TOWN, 4) }, + { ENTRANCE(WEST_CLOCK_TOWN, 1), ENTRANCE(SOUTH_CLOCK_TOWN, 5) }, + { ENTRANCE(LAUNDRY_POOL, 0), ENTRANCE(SOUTH_CLOCK_TOWN, 6) }, + { ENTRANCE(EAST_CLOCK_TOWN, 1), ENTRANCE(SOUTH_CLOCK_TOWN, 7) }, + { ENTRANCE(NORTH_CLOCK_TOWN, 1), ENTRANCE(EAST_CLOCK_TOWN, 5) }, + }; +} + +std::vector GetEntrancePool(EntrancePoolType poolType) { + switch (poolType) { + case POOL_INTERIOR: + return GetInteriorEntrances(); + case POOL_GROTTO: + return GetGrottoEntrances(); + case POOL_DUNGEON: + return GetDungeonEntrances(); + case POOL_OVERWORLD: + return GetOverworldEntrances(); + default: + return {}; + } +} + +bool IsEntranceShuffleEnabled() { + return IS_RANDO && RANDO_SAVE_OPTIONS[RO_SHUFFLE_ENTRANCES] != RO_ENTRANCE_SHUFFLE_OFF; +} + +// Called on file load and in file creation prior to logic calculation +void ShuffleEntrances() { + sEntranceMap.clear(); + + if (!IsEntranceShuffleEnabled()) { + return; + } + + Ship_Random_Seed(gSaveContext.save.shipSaveInfo.rando.finalSeed); + + auto shuffleType = RANDO_SAVE_OPTIONS[RO_SHUFFLE_ENTRANCES]; + + std::vector poolsToShuffle; + + switch (shuffleType) { + case RO_ENTRANCE_SHUFFLE_INTERIORS_ONLY: + poolsToShuffle = { POOL_INTERIOR }; + break; + case RO_ENTRANCE_SHUFFLE_DUNGEONS_ONLY: + poolsToShuffle = { POOL_DUNGEON }; + break; + case RO_ENTRANCE_SHUFFLE_FULL: + poolsToShuffle = { POOL_INTERIOR, POOL_DUNGEON, POOL_OVERWORLD }; + break; + default: + return; + } + + // Shuffle each pool independently + for (auto poolType : poolsToShuffle) { + std::vector originalPool = GetEntrancePool(poolType); + auto pool = originalPool; // Make a copy to shuffle + + if (pool.size() < 2) { + continue; + } + + // Shuffle pool + for (size_t i = 0; i < pool.size(); i++) { + size_t j = Ship_Random(0, pool.size() - 1); + std::swap(pool[i], pool[j]); + } + + for (size_t i = 0; i < pool.size(); ++i) { + const auto& from = originalPool[i]; + const auto& to = pool[i]; + + if (from.entrance != to.entrance) { + sEntranceMap[from.entrance] = to.entrance; + } + + // if (from.isReversible && to.isReversible) { + if (to.exit != from.exit) { + sEntranceMap[to.exit] = from.exit; + } + // } + } + } +} + +s32 GetShuffledEntrance(s32 originalEntrance) { + if (!IsEntranceShuffleEnabled()) { + return originalEntrance; + } + + if (sEntranceMap.count(originalEntrance) > 0) { + return sEntranceMap[originalEntrance]; + } + + return originalEntrance; +} + +s32 GetOriginalEntrance(s32 shuffledEntrance) { + if (!IsEntranceShuffleEnabled()) { + return shuffledEntrance; + } + + for (const auto& [orig, mapped] : sEntranceMap) { + if (mapped == shuffledEntrance) { + return orig; + } + } + + return shuffledEntrance; +} + +} // namespace EntranceShuffle + +} // namespace Rando diff --git a/mm/2s2h/Rando/Logic/EntranceShuffle.h b/mm/2s2h/Rando/Logic/EntranceShuffle.h new file mode 100644 index 0000000000..10b5726ae4 --- /dev/null +++ b/mm/2s2h/Rando/Logic/EntranceShuffle.h @@ -0,0 +1,50 @@ +#ifndef RANDO_ENTRANCE_SHUFFLE_H +#define RANDO_ENTRANCE_SHUFFLE_H + +#include "Rando/Rando.h" +#include +#include + +extern "C" { +#include "functions.h" +#include "variables.h" +} + +namespace Rando { + +namespace EntranceShuffle { + +// Structure to represent an entrance pair +struct EntrancePair { + s32 entrance; // the entrance you take to enter Target area from source + s32 exit; // the exit you take to leave Target area to source +}; + +// Groups of entrances that can be shuffled together +enum EntrancePoolType { + POOL_INTERIOR, // Houses, shops, etc. + POOL_GROTTO, // Grottos and caves + POOL_DUNGEON, // Dungeon entrances + POOL_OVERWORLD, // Overworld area connections +}; + +// Generate entrance shuffle mappings for a new seed +void ShuffleEntrances(); + +// Get the shuffled destination for an entrance +s32 GetShuffledEntrance(s32 originalEntrance); + +// Get the original entrance from a shuffled one (for reverse lookups) +s32 GetOriginalEntrance(s32 shuffledEntrance); + +// Check if entrance shuffle is enabled +bool IsEntranceShuffleEnabled(); + +// Get all entrances in a specific pool +std::vector GetEntrancePool(EntrancePoolType poolType); + +} // namespace EntranceShuffle + +} // namespace Rando + +#endif // RANDO_ENTRANCE_SHUFFLE_H diff --git a/mm/2s2h/Rando/Logic/Logic.cpp b/mm/2s2h/Rando/Logic/Logic.cpp index b763975571..18335378ec 100644 --- a/mm/2s2h/Rando/Logic/Logic.cpp +++ b/mm/2s2h/Rando/Logic/Logic.cpp @@ -2,6 +2,7 @@ #include "2s2h/ShipInit.hpp" #include "Logic.h" +#include "EntranceShuffle.h" namespace Rando { @@ -44,7 +45,12 @@ void FindReachableRegions(RandoRegionId currentRegion, std::set& } for (auto& [exitId, regionExit] : randoRegion.exits) { - RandoRegionId connectedRegionId = GetRegionIdFromEntrance(exitId); + s32 lookupExit = exitId; + if (Rando::EntranceShuffle::IsEntranceShuffleEnabled()) { + lookupExit = Rando::EntranceShuffle::GetShuffledEntrance(lookupExit); + } + + RandoRegionId connectedRegionId = GetRegionIdFromEntrance(lookupExit); // Check if the region is accessible and hasn’t been visited yet if (reachableRegions.count(connectedRegionId) == 0 && regionExit.condition()) { reachableRegions.insert(connectedRegionId); // Mark region as visited diff --git a/mm/2s2h/Rando/Menu.cpp b/mm/2s2h/Rando/Menu.cpp index 9bdbc7eb48..de23909734 100644 --- a/mm/2s2h/Rando/Menu.cpp +++ b/mm/2s2h/Rando/Menu.cpp @@ -35,6 +35,13 @@ std::unordered_map accessTrialsOptions = { { RO_ACCESS_TRIALS_OPEN, "Open" }, }; +std::unordered_map entranceShuffleOptions = { + { RO_ENTRANCE_SHUFFLE_OFF, "Off" }, + { RO_ENTRANCE_SHUFFLE_INTERIORS_ONLY, "Interiors Only" }, + { RO_ENTRANCE_SHUFFLE_DUNGEONS_ONLY, "Dungeons Only" }, + { RO_ENTRANCE_SHUFFLE_FULL, "Full" }, +}; + std::vector incompatibleWithVanilla = { RO_SHUFFLE_BOSS_SOULS, RO_SHUFFLE_SWIM, @@ -356,6 +363,7 @@ static void DrawShufflesTab() { CVarCheckbox("Shuffle Freestanding Items", Rando::StaticData::Options[RO_SHUFFLE_FREESTANDING_ITEMS].cvar); CVarCheckbox("Shuffle Wonder Items", "gPlaceholderBool", CheckboxOptions({ { .disabled = true, .disabledTooltip = "Coming Soon" } })); + CVarCombobox("Shuffle Entrances", Rando::StaticData::Options[RO_SHUFFLE_ENTRANCES].cvar, &entranceShuffleOptions); ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("randoLocationsColumn3", ImVec2(columnWidth, halfHeight)); diff --git a/mm/2s2h/Rando/MiscBehavior/EntranceHooks.cpp b/mm/2s2h/Rando/MiscBehavior/EntranceHooks.cpp new file mode 100644 index 0000000000..4bd97d8798 --- /dev/null +++ b/mm/2s2h/Rando/MiscBehavior/EntranceHooks.cpp @@ -0,0 +1,39 @@ +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/ShipInit.hpp" +#include "2s2h/Rando/Logic/EntranceShuffle.h" + +extern "C" { +#include "functions.h" +#include "variables.h" +#include "z64scene.h" +} + +namespace Rando { + +namespace EntranceShuffle { + +// Hook into scene transitions to apply entrance shuffling +void OnPlayDestroy() { + if (!IsEntranceShuffleEnabled()) { + return; + } + + // Get the shuffled destination entrance + s32 originalEntrance = gSaveContext.save.entrance; + s32 shuffledEntrance = GetShuffledEntrance(originalEntrance); + + if (shuffledEntrance != originalEntrance) { + gSaveContext.save.entrance = shuffledEntrance; + } +} + +static RegisterShipInitFunc registerHooks( + []() { + // Hook into OnPlayDestroy which is called just before transitioning + GameInteractor::Instance->RegisterGameHook([]() { OnPlayDestroy(); }); + }, + {}); + +} // namespace EntranceShuffle + +} // namespace Rando diff --git a/mm/2s2h/Rando/MiscBehavior/MiscBehavior.cpp b/mm/2s2h/Rando/MiscBehavior/MiscBehavior.cpp index abef387215..c0a2247370 100644 --- a/mm/2s2h/Rando/MiscBehavior/MiscBehavior.cpp +++ b/mm/2s2h/Rando/MiscBehavior/MiscBehavior.cpp @@ -1,5 +1,6 @@ #include "MiscBehavior.h" #include "2s2h/Rando/Logic/Logic.h" +#include "2s2h/Rando/Logic/EntranceShuffle.h" extern "C" { #include "variables.h" @@ -16,6 +17,7 @@ void Rando::MiscBehavior::OnFileLoad() { Rando::MiscBehavior::CheckQueueReset(); Rando::MiscBehavior::InitKaleidoItemPage(); Rando::MiscBehavior::InitOfferGetItemBehavior(); + Rando::EntranceShuffle::ShuffleEntrances(); COND_HOOK(OnFlagSet, IS_RANDO, Rando::MiscBehavior::OnFlagSet); COND_HOOK(OnSceneFlagSet, IS_RANDO, Rando::MiscBehavior::OnSceneFlagSet); diff --git a/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp b/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp index 63ad54f952..65e4711b85 100644 --- a/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp +++ b/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp @@ -1,6 +1,7 @@ #include "MiscBehavior.h" #include "Rando/Spoiler/Spoiler.h" #include "Rando/Logic/Logic.h" +#include "Rando/Logic/EntranceShuffle.h" #include "2s2h/ShipUtils.h" #include #include @@ -172,6 +173,9 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) { // Grant the starting stuff GrantStarters(); + // Shuffle entrances if enabled + Rando::EntranceShuffle::ShuffleEntrances(); + if (RANDO_SAVE_OPTIONS[RO_LOGIC] == RO_LOGIC_VANILLA) { GiveItem(RI_SWORD_KOKIRI); GiveItem(RI_SHIELD_HERO); diff --git a/mm/2s2h/Rando/StaticData/Options.cpp b/mm/2s2h/Rando/StaticData/Options.cpp index eb05930b0f..543784e23f 100644 --- a/mm/2s2h/Rando/StaticData/Options.cpp +++ b/mm/2s2h/Rando/StaticData/Options.cpp @@ -40,6 +40,7 @@ std::map Options = { RO(RO_SHUFFLE_COWS, RO_GENERIC_OFF), RO(RO_SHUFFLE_CRATE_DROPS, RO_GENERIC_OFF), RO(RO_SHUFFLE_ENEMY_DROPS, RO_GENERIC_OFF), + RO(RO_SHUFFLE_ENTRANCES, RO_ENTRANCE_SHUFFLE_OFF), RO(RO_SHUFFLE_FREESTANDING_ITEMS, RO_GENERIC_OFF), RO(RO_SHUFFLE_FROGS, RO_GENERIC_OFF), RO(RO_SHUFFLE_GOLD_SKULLTULAS, RO_GENERIC_OFF), diff --git a/mm/2s2h/Rando/Types.h b/mm/2s2h/Rando/Types.h index 6c640e59e5..38f55b4aea 100644 --- a/mm/2s2h/Rando/Types.h +++ b/mm/2s2h/Rando/Types.h @@ -2827,6 +2827,7 @@ typedef enum { RO_SHUFFLE_COWS, RO_SHUFFLE_CRATE_DROPS, RO_SHUFFLE_ENEMY_DROPS, + RO_SHUFFLE_ENTRANCES, RO_SHUFFLE_FREESTANDING_ITEMS, RO_SHUFFLE_FROGS, RO_SHUFFLE_GRASS_DROPS, @@ -2880,6 +2881,13 @@ typedef enum { RO_ACCESS_TRIALS_OPEN, } RandoOptionAccessTrials; +typedef enum { + RO_ENTRANCE_SHUFFLE_OFF, + RO_ENTRANCE_SHUFFLE_INTERIORS_ONLY, + RO_ENTRANCE_SHUFFLE_DUNGEONS_ONLY, + RO_ENTRANCE_SHUFFLE_FULL, +} RandoOptionEntranceShuffle; + typedef enum { RANDO_INF_PURCHASED_BEANS_FROM_SOUTHERN_SWAMP_SCRUB, RANDO_INF_PURCHASED_BOMB_BAG_FROM_GORON_VILLAGE_SCRUB,