diff --git a/mm/2s2h/BenGui/BenGui.cpp b/mm/2s2h/BenGui/BenGui.cpp index f7c9d8d077..ec3d3adfbe 100644 --- a/mm/2s2h/BenGui/BenGui.cpp +++ b/mm/2s2h/BenGui/BenGui.cpp @@ -9,6 +9,7 @@ #include "CosmeticEditor.h" #include "Notification.h" #include "2s2h/Rando/CheckTracker/CheckTracker.h" +#include "2s2h/Rando/EntranceTracker/EntranceTracker.h" #ifdef __APPLE__ #include @@ -59,6 +60,8 @@ std::shared_ptr mBenMenu; std::shared_ptr mNotificationWindow; std::shared_ptr mRandoCheckTrackerWindow; std::shared_ptr mRandoCheckTrackerSettingsWindow; +std::shared_ptr mEntranceTrackerWindow; +std::shared_ptr mEntranceTrackerSettingsWindow; std::shared_ptr mItemTrackerWindow; std::shared_ptr mItemTrackerSettingsWindow; std::shared_ptr mDisplayOverlayWindow; @@ -176,6 +179,14 @@ void SetupGuiElements() { "gWindows.CheckTrackerSettings", "Check Tracker Settings"); gui->AddGuiWindow(mRandoCheckTrackerSettingsWindow); + mEntranceTrackerWindow = std::make_shared( + "gWindows.EntranceTracker", "Entrance Tracker", ImVec2(450, 500)); + gui->AddGuiWindow(mEntranceTrackerWindow); + + mEntranceTrackerSettingsWindow = std::make_shared( + "gWindows.EntranceTrackerSettings", "Entrance Tracker Settings"); + gui->AddGuiWindow(mEntranceTrackerSettingsWindow); + mInputViewer = std::make_shared("gWindows.InputViewer", "Input Viewer"); gui->AddGuiWindow(mInputViewer); mInputViewerSettings = std::make_shared("gWindows.InputViewerSettings", @@ -202,6 +213,8 @@ void Destroy() { mNotificationWindow = nullptr; mRandoCheckTrackerWindow = nullptr; mRandoCheckTrackerSettingsWindow = nullptr; + mEntranceTrackerWindow = nullptr; + mEntranceTrackerSettingsWindow = nullptr; mHookDebuggerWindow = nullptr; mSaveEditorWindow = nullptr; diff --git a/mm/2s2h/Rando/EntranceTracker/EntranceTracker.cpp b/mm/2s2h/Rando/EntranceTracker/EntranceTracker.cpp new file mode 100644 index 0000000000..e518c35b62 --- /dev/null +++ b/mm/2s2h/Rando/EntranceTracker/EntranceTracker.cpp @@ -0,0 +1,1111 @@ +#include "EntranceTracker.h" +#include "2s2h/Rando/Logic/Logic.h" +#include "2s2h/Rando/Logic/EntranceShuffle.h" +#include "2s2h/ShipUtils.h" +#include "2s2h/BenGui/UIWidgets.hpp" +#include "2s2h/BenPort.h" +#include "2s2h/ShipInit.hpp" +#include +#include +#include + +extern "C" { +#include "z64scene.h" +s16 Play_GetOriginalSceneId(s16 sceneId); +} + +namespace BenGui { +extern std::shared_ptr mEntranceTrackerWindow; +} + +#define WIDGET_COLOR UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)) + +// Scene sorting index (same as CheckTracker) +#define DEFINE_SCENE(_name, enumValue, _textId, _drawConfig, _restrictionFlags, _persistentCycleFlags, \ + _entranceSceneId, betterMapSelectIndex, _humanName) \ + { enumValue, betterMapSelectIndex }, +#define DEFINE_SCENE_UNSET(_enumValue) + +static std::unordered_map betterSceneIndex = { +#include "tables/scene_table.h" +}; + +#undef DEFINE_SCENE +#undef DEFINE_SCENE_UNSET + +// Visibility modes +typedef enum { + ENTRANCE_TRACKER_VISIBILITY_MODE_ALWAYS, + ENTRANCE_TRACKER_VISIBILITY_MODE_ONLY_ON_PAUSE_MENU, + ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_TOGGLE, + ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_HOLD, +} EntranceTrackerVisibilityMode; + +static std::unordered_map sVisibilityModes = { + { ENTRANCE_TRACKER_VISIBILITY_MODE_ALWAYS, "Always" }, + { ENTRANCE_TRACKER_VISIBILITY_MODE_ONLY_ON_PAUSE_MENU, "Only on Pause Menu" }, + { ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_TOGGLE, "Button Toggle" }, + { ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_HOLD, "Button Hold" }, +}; + +static bool sEntranceTrackerBtnState = false; + +// CVars +#define CVAR_NAME_SHOW_ENTRANCE_TRACKER "gWindows.EntranceTracker" +#define CVAR_NAME_VISIBILITY_MODE "gRando.EntranceTracker.VisibilityMode" +#define CVAR_NAME_VISIBILITY_BTN "gRando.EntranceTracker.VisibilityBtn" +#define CVAR_NAME_TRACKER_OPACITY "gRando.EntranceTracker.Opacity" +#define CVAR_NAME_TRACKER_SCALE "gRando.EntranceTracker.Scale" +#define CVAR_NAME_SPOILER_MODE "gRando.EntranceTracker.SpoilerMode" +#define CVAR_NAME_SHOW_SEARCH "gRando.EntranceTracker.ShowSearch" +#define CVAR_NAME_EXTENDED_ROUTING "gRando.EntranceTracker.ExtendedRouting" + +#define CVAR_SHOW_ENTRANCE_TRACKER CVarGetInteger(CVAR_NAME_SHOW_ENTRANCE_TRACKER, 0) +#define CVAR_VISIBILITY_MODE CVarGetInteger(CVAR_NAME_VISIBILITY_MODE, ENTRANCE_TRACKER_VISIBILITY_MODE_ALWAYS) +#define CVAR_VISIBILITY_BTN CVarGetInteger(CVAR_NAME_VISIBILITY_BTN, BTN_CUSTOM_MODIFIER1) +#define CVAR_TRACKER_OPACITY CVarGetFloat(CVAR_NAME_TRACKER_OPACITY, 0.5f) +#define CVAR_TRACKER_SCALE CVarGetFloat(CVAR_NAME_TRACKER_SCALE, 1.0f) +#define CVAR_SPOILER_MODE CVarGetInteger(CVAR_NAME_SPOILER_MODE, 1) // Default: show all +#define CVAR_SHOW_SEARCH CVarGetInteger(CVAR_NAME_SHOW_SEARCH, 1) +#define CVAR_EXTENDED_ROUTING CVarGetInteger(CVAR_NAME_EXTENDED_ROUTING, 0) + +static ImGuiTextFilter sEntranceTrackerFilter; +static float trackerScale = 1.0f; +static ImVec4 trackerBG = ImVec4{ 0, 0, 0, 0.5f }; + +// Entrance display data structures +struct EntranceConnection { + s32 originalEntrance; + s32 shuffledEntrance; + SceneId sourceScene; + std::string originalName; + std::string shuffledName; +}; + +static std::map> sEntrancesByArea; +static std::vector sSortedSceneIds; + +// Route finder state +static int sRouteFromIndex = 0; +static int sRouteToIndex = 0; +static std::string sRouteResult = ""; +static std::vector> sRegionList; +static ImGuiTextFilter sRouteFromFilter; +static ImGuiTextFilter sRouteToFilter; + +// Entrance name mapping - built from scene names with spawn-specific suffixes +static std::map sEntranceNames; + +// Helper to extract scene ID from entrance +static SceneId GetSceneFromEntrance(s32 entrance) { + return static_cast((entrance >> 9) & 0x7F); +} + +// Cache of all exits that are in entrance shuffle pools +static std::set sPoolExits; + +// Build the set of exits that are actually in entrance shuffle pools +static void BuildPoolExitSet() { + sPoolExits.clear(); + + std::vector pools = { Rando::EntranceShuffle::POOL_INTERIOR, + Rando::EntranceShuffle::POOL_DUNGEON, + Rando::EntranceShuffle::POOL_OVERWORLD }; + + for (auto poolType : pools) { + auto entrancePairs = Rando::EntranceShuffle::GetEntrancePool(poolType); + for (const auto& pair : entrancePairs) { + // Add both the entrance and exit from each pair + sPoolExits.insert(pair.entrance); + sPoolExits.insert(pair.exit); + } + } +} + +// Check if an exit is part of any entrance shuffle pool +static bool IsExitInShufflePool(s32 exitId) { + return sPoolExits.count(exitId) > 0; +} + +// Helper to extract spawn number from entrance +static s32 GetSpawnFromEntrance(s32 entrance) { + return (entrance >> 4) & 0x1F; +} + +// Build entrance name from scene name and spawn number +static std::string BuildEntranceName(s32 entrance) { + SceneId sceneId = GetSceneFromEntrance(entrance); + s32 spawn = GetSpawnFromEntrance(entrance); + + std::string baseName = Ship_GetSceneName(sceneId); + + // Add spawn suffix for multi-entrance scenes + if (spawn > 0) { + baseName += " (" + std::to_string(spawn + 1) + ")"; + } + + return baseName; +} + +std::string Rando::EntranceTracker::GetEntranceName(s32 entranceId) { + if (sEntranceNames.count(entranceId) > 0) { + return sEntranceNames[entranceId]; + } + return BuildEntranceName(entranceId); +} + +// Discovery tracking - uses save data for persistence +// Maps entrance IDs to compact indices (0-255) for bitfield storage +static std::map sEntranceToIndex; +static std::map sIndexToEntrance; +static u8 sNextEntranceIndex = 0; + +// Get or assign an index for an entrance ID +static u8 GetEntranceIndex(s32 entranceId) { + if (sEntranceToIndex.count(entranceId) == 0) { + if (sNextEntranceIndex >= 255) { + return 255; // Fallback if we run out of indices + } + sEntranceToIndex[entranceId] = sNextEntranceIndex; + sIndexToEntrance[sNextEntranceIndex] = entranceId; + sNextEntranceIndex++; + } + return sEntranceToIndex[entranceId]; +} + +// Build the entrance index mapping from known entrance pools +static void BuildEntranceIndexMap() { + sEntranceToIndex.clear(); + sIndexToEntrance.clear(); + sNextEntranceIndex = 0; + + // Add all entrances from the pools to ensure consistent indexing + std::vector pools = { Rando::EntranceShuffle::POOL_INTERIOR, + Rando::EntranceShuffle::POOL_DUNGEON, + Rando::EntranceShuffle::POOL_OVERWORLD }; + + for (auto poolType : pools) { + auto entrancePairs = Rando::EntranceShuffle::GetEntrancePool(poolType); + for (const auto& pair : entrancePairs) { + GetEntranceIndex(pair.entrance); + GetEntranceIndex(pair.exit); + } + } +} + +void Rando::EntranceTracker::SetEntranceDiscovered(s32 entranceId) { + if (!IS_RANDO) { + return; + } + + u8 index = GetEntranceIndex(entranceId); + if (index < 255) { + u8 byteIndex = index / 8; + u8 bitIndex = index % 8; + gSaveContext.save.shipSaveInfo.rando.discoveredEntrances[byteIndex] |= (1 << bitIndex); + } +} + +bool Rando::EntranceTracker::IsEntranceDiscovered(s32 entranceId) { + if (!IS_RANDO) { + return false; + } + + u8 index = GetEntranceIndex(entranceId); + if (index >= 255) { + return false; + } + + u8 byteIndex = index / 8; + u8 bitIndex = index % 8; + return (gSaveContext.save.shipSaveInfo.rando.discoveredEntrances[byteIndex] & (1 << bitIndex)) != 0; +} + +// List of generic region names that need scene context +static const std::set sGenericRegionNames = { + "Entrance", "Main Room", "Maze Room", + "Upper", "Lower", "Back", + "Front", "Boss Room", "Passage", + "Main Room Upper", "Main Room Lower", "Night 1 Boss", + "Night 2 Boss", "Higher", "Before Great Bay Coast" +}; + +// Get a display name for a region, using scene name as fallback or prefix for generic names +static std::string GetRegionDisplayName(RandoRegionId regionId) { + if (Rando::Logic::Regions.count(regionId) == 0) { + return ""; + } + + const auto& region = Rando::Logic::Regions.at(regionId); + std::string name = region.name; + std::string sceneName = Ship_GetSceneName(region.sceneId); + + // If name is empty, use scene name + if (name.empty()) { + return sceneName; + } + + // If name is generic, prefix with scene name + if (sGenericRegionNames.count(name) > 0) { + return sceneName + " - " + name; + } + + return name; +} + +// Build the entrance name mapping with better names for specific entrances +static void BuildEntranceNameMap() { + sEntranceNames.clear(); + + // Add custom names for specific entrances (more descriptive than just scene name) + sEntranceNames[ENTRANCE(STOCK_POT_INN, 0)] = "Stock Pot Inn (Main Entrance)"; + sEntranceNames[ENTRANCE(STOCK_POT_INN, 1)] = "Stock Pot Inn (Upper Floor)"; + sEntranceNames[ENTRANCE(CURIOSITY_SHOP, 0)] = "Curiosity Shop (Front Entrance)"; + sEntranceNames[ENTRANCE(CURIOSITY_SHOP, 1)] = "Curiosity Shop (Back Entrance)"; + sEntranceNames[ENTRANCE(RANCH_HOUSE, 0)] = "Romani Ranch House (Main Building)"; + sEntranceNames[ENTRANCE(RANCH_HOUSE, 1)] = "Romani Ranch House (Barn)"; + sEntranceNames[ENTRANCE(ZORA_HALL_ROOMS, 0)] = "Zora Hall (Evan's Room)"; + sEntranceNames[ENTRANCE(ZORA_HALL_ROOMS, 1)] = "Zora Hall (Lulu's Room)"; + sEntranceNames[ENTRANCE(ZORA_HALL_ROOMS, 2)] = "Zora Hall (Japas' Room)"; + sEntranceNames[ENTRANCE(ZORA_HALL_ROOMS, 3)] = "Zora Hall (Tijo's Room)"; + sEntranceNames[ENTRANCE(ZORA_HALL_ROOMS, 5)] = "Zora Hall (Shop)"; + sEntranceNames[ENTRANCE(FAIRY_FOUNTAIN, 0)] = "Great Fairy (Clock Town)"; + sEntranceNames[ENTRANCE(FAIRY_FOUNTAIN, 1)] = "Great Fairy (Woodfall)"; + sEntranceNames[ENTRANCE(FAIRY_FOUNTAIN, 2)] = "Great Fairy (Snowhead)"; + sEntranceNames[ENTRANCE(FAIRY_FOUNTAIN, 3)] = "Great Fairy (Great Bay)"; + sEntranceNames[ENTRANCE(FAIRY_FOUNTAIN, 4)] = "Great Fairy (Ikana)"; + sEntranceNames[ENTRANCE(CLOCK_TOWER_INTERIOR, 0)] = "Clock Tower Interior (Main)"; + sEntranceNames[ENTRANCE(CLOCK_TOWER_INTERIOR, 1)] = "Clock Tower Interior (Upper)"; + + // Temple entrances - be specific about which entrance/exit + sEntranceNames[ENTRANCE(GREAT_BAY_TEMPLE, 0)] = "Great Bay Temple (Main Entrance)"; + sEntranceNames[ENTRANCE(GREAT_BAY_TEMPLE, 1)] = "Great Bay Temple (Main Exit)"; + sEntranceNames[ENTRANCE(WOODFALL_TEMPLE, 0)] = "Woodfall Temple (Main Entrance)"; + sEntranceNames[ENTRANCE(WOODFALL_TEMPLE, 1)] = "Woodfall Temple (Main Exit)"; + sEntranceNames[ENTRANCE(SNOWHEAD_TEMPLE, 0)] = "Snowhead Temple (Main Entrance)"; + sEntranceNames[ENTRANCE(SNOWHEAD_TEMPLE, 1)] = "Snowhead Temple (Main Exit)"; + sEntranceNames[ENTRANCE(IKANA_CASTLE, 0)] = "Ikana Castle (Main Entrance)"; + sEntranceNames[ENTRANCE(IKANA_CASTLE, 1)] = "Ikana Castle (Main Exit)"; + sEntranceNames[ENTRANCE(STONE_TOWER_TEMPLE, 0)] = "Stone Tower Temple (Main Entrance)"; + sEntranceNames[ENTRANCE(STONE_TOWER_TEMPLE, 1)] = "Stone Tower Temple (Main Exit)"; + + // Overworld connections - use more descriptive names + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 0)] = "East Clock Town (to Termina Field)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 1)] = "East Clock Town (Chest Shop)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 2)] = "East Clock Town (Astral Observatory)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 3)] = "East Clock Town (from South)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 4)] = "East Clock Town (Treasure Chest Shop)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 5)] = "East Clock Town (Great Fairy)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 6)] = "East Clock Town (Honey and Darlings)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 7)] = "East Clock Town (Mayor's Residence)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 8)] = "East Clock Town (Shooting Gallery)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 9)] = "East Clock Town (Stock Pot Inn)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 10)] = "East Clock Town (Stock Pot Inn Upstairs)"; + sEntranceNames[ENTRANCE(EAST_CLOCK_TOWN, 11)] = "East Clock Town (Milk Bar)"; + + sEntranceNames[ENTRANCE(SOUTH_CLOCK_TOWN, 0)] = "South Clock Town (Clock Tower Interior)"; + sEntranceNames[ENTRANCE(SOUTH_CLOCK_TOWN, 2)] = "South Clock Town (from East)"; + sEntranceNames[ENTRANCE(SOUTH_CLOCK_TOWN, 3)] = "South Clock Town (from West)"; + sEntranceNames[ENTRANCE(SOUTH_CLOCK_TOWN, 4)] = "South Clock Town (from North)"; + sEntranceNames[ENTRANCE(SOUTH_CLOCK_TOWN, 5)] = "South Clock Town (Swordsman)"; + sEntranceNames[ENTRANCE(SOUTH_CLOCK_TOWN, 6)] = "South Clock Town (Laundry)"; + sEntranceNames[ENTRANCE(SOUTH_CLOCK_TOWN, 7)] = "South Clock Town (Chest Shop)"; + + sEntranceNames[ENTRANCE(WEST_CLOCK_TOWN, 0)] = "West Clock Town (to Termina Field)"; + sEntranceNames[ENTRANCE(WEST_CLOCK_TOWN, 1)] = "West Clock Town (to South Lower)"; + sEntranceNames[ENTRANCE(WEST_CLOCK_TOWN, 2)] = "West Clock Town (to South Upper)"; + sEntranceNames[ENTRANCE(WEST_CLOCK_TOWN, 3)] = "West Clock Town (Swordsman)"; + sEntranceNames[ENTRANCE(WEST_CLOCK_TOWN, 4)] = "West Clock Town (Curiosity Shop)"; + sEntranceNames[ENTRANCE(WEST_CLOCK_TOWN, 5)] = "West Clock Town (Trading Post)"; + sEntranceNames[ENTRANCE(WEST_CLOCK_TOWN, 6)] = "West Clock Town (Bomb Shop)"; + sEntranceNames[ENTRANCE(WEST_CLOCK_TOWN, 7)] = "West Clock Town (Post Office)"; + sEntranceNames[ENTRANCE(WEST_CLOCK_TOWN, 8)] = "West Clock Town (Lottery Shop)"; + + sEntranceNames[ENTRANCE(NORTH_CLOCK_TOWN, 1)] = "North Clock Town (Great Fairy)"; + sEntranceNames[ENTRANCE(NORTH_CLOCK_TOWN, 2)] = "North Clock Town (from South)"; + sEntranceNames[ENTRANCE(NORTH_CLOCK_TOWN, 3)] = "North Clock Town (Great Fairy Fountain)"; + + sEntranceNames[ENTRANCE(LAUNDRY_POOL, 0)] = "Laundry Pool (Entrance)"; + sEntranceNames[ENTRANCE(LAUNDRY_POOL, 1)] = "Laundry Pool (Back Alley)"; + + // Additional interior shops/buildings from Central.cpp + sEntranceNames[ENTRANCE(SWORDMANS_SCHOOL, 0)] = "Swordsman's School"; + sEntranceNames[ENTRANCE(TREASURE_CHEST_SHOP, 0)] = "Treasure Chest Shop"; + sEntranceNames[ENTRANCE(TOWN_SHOOTING_GALLERY, 0)] = "Shooting Gallery"; + sEntranceNames[ENTRANCE(DEKU_SCRUB_PLAYGROUND, 0)] = "Deku Playground"; + sEntranceNames[ENTRANCE(TRADING_POST, 0)] = "Trading Post"; + sEntranceNames[ENTRANCE(POST_OFFICE, 0)] = "Post Office"; + sEntranceNames[ENTRANCE(MILK_BAR, 0)] = "Milk Bar"; + sEntranceNames[ENTRANCE(LOTTERY_SHOP, 0)] = "Lottery Shop"; + sEntranceNames[ENTRANCE(MAYORS_RESIDENCE, 0)] = "Mayor's Residence"; + sEntranceNames[ENTRANCE(BOMB_SHOP, 0)] = "Bomb Shop"; + sEntranceNames[ENTRANCE(HONEY_AND_DARLINGS_SHOP, 0)] = "Honey and Darling's Shop"; + sEntranceNames[ENTRANCE(ASTRAL_OBSERVATORY, 0)] = "Astral Observatory"; + + // Termina Field & Roads + sEntranceNames[ENTRANCE(TOURIST_INFORMATION, 0)] = "Tourist Information"; + sEntranceNames[ENTRANCE(SWAMP_SHOOTING_GALLERY, 0)] = "Swamp Shooting Gallery"; + sEntranceNames[ENTRANCE(MAGIC_HAGS_POTION_SHOP, 0)] = "Magic Hag's Potion Shop"; + + // Milk Road + sEntranceNames[ENTRANCE(CUCCO_SHACK, 0)] = "Cucco Shack"; + sEntranceNames[ENTRANCE(DOGGY_RACETRACK, 0)] = "Doggy Racetrack"; + sEntranceNames[ENTRANCE(RANCH_HOUSE, 0)] = "Ranch House (Main)"; + sEntranceNames[ENTRANCE(RANCH_HOUSE, 1)] = "Ranch House (Barn)"; + + // Great Bay + sEntranceNames[ENTRANCE(FISHERMANS_HUT, 0)] = "Fisherman's Hut"; + sEntranceNames[ENTRANCE(MARINE_RESEARCH_LAB, 0)] = "Marine Research Lab"; + sEntranceNames[ENTRANCE(OCEANSIDE_SPIDER_HOUSE, 0)] = "Oceanside Spider House"; + sEntranceNames[ENTRANCE(PINNACLE_ROCK, 0)] = "Pinnacle Rock"; + + // Mountain Village + sEntranceNames[ENTRANCE(GORON_SHOP, 0)] = "Goron Shop"; + + // Swamp + sEntranceNames[ENTRANCE(SWAMP_SPIDER_HOUSE, 0)] = "Swamp Spider House"; + + // Stone Tower + sEntranceNames[ENTRANCE(STONE_TOWER, 0)] = "Stone Tower"; + sEntranceNames[ENTRANCE(STONE_TOWER, 1)] = "Stone Tower (Inverted Entrance)"; + sEntranceNames[ENTRANCE(STONE_TOWER, 2)] = "Stone Tower (to Temple)"; + + // Ghost Hut entrances (from Ikana Canyon) + sEntranceNames[ENTRANCE(GHOST_HUT, 0)] = "Ghost Hut"; + sEntranceNames[ENTRANCE(GHOST_HUT, 1)] = "Ghost Hut (Alternate)"; + sEntranceNames[ENTRANCE(GHOST_HUT, 2)] = "Ghost Hut (Another Route)"; + + // Ikana + sEntranceNames[ENTRANCE(MUSIC_BOX_HOUSE, 0)] = "Music Box House"; +} + +// Get the source scene for an entrance pair (where you are when you use the exit) +static SceneId GetSourceSceneForEntrancePair(s32 returnEntrance) { + // returnEntrance is the entrance ID that you return to when exiting a destination + // We need to find which region has this as a returnEntrance + // The returnEntrance field is in the value of the exits map + + for (const auto& [regionId, region] : Rando::Logic::Regions) { + for (const auto& [_, regionExit] : region.exits) { + if (regionExit.returnEntrance == returnEntrance) { + // Found it! This region has this return entrance + return region.sceneId; + } + } + } + + // Fallback: try to extract scene from the return entrance ID itself + return GetSceneFromEntrance(returnEntrance); +} + +// Build entrance data grouped by source area +static void BuildEntranceData() { + sEntrancesByArea.clear(); + sSortedSceneIds.clear(); + + // Build the set of pool exits for route finding + BuildPoolExitSet(); + + if (!Rando::EntranceShuffle::IsEntranceShuffleEnabled()) { + return; + } + + // Get all entrance pools and process each + std::vector pools = { Rando::EntranceShuffle::POOL_INTERIOR, + Rando::EntranceShuffle::POOL_DUNGEON, + Rando::EntranceShuffle::POOL_OVERWORLD }; + + for (auto poolType : pools) { + auto entrancePairs = Rando::EntranceShuffle::GetEntrancePool(poolType); + + for (const auto& pair : entrancePairs) { + s32 original = pair.entrance; + s32 shuffled = Rando::EntranceShuffle::GetShuffledEntrance(original); + s32 exitId = pair.exit; // This is the exit from the SOURCE region + + // Get source scene using the exit ID from the source region + SceneId sourceScene = GetSourceSceneForEntrancePair(exitId); + + EntranceConnection conn; + conn.originalEntrance = original; + conn.shuffledEntrance = shuffled; + conn.sourceScene = sourceScene; + conn.originalName = Rando::EntranceTracker::GetEntranceName(original); + conn.shuffledName = Rando::EntranceTracker::GetEntranceName(shuffled); + + sEntrancesByArea[sourceScene].push_back(conn); + } + } + + // Build sorted scene list + for (const auto& [sceneId, _] : sEntrancesByArea) { + sSortedSceneIds.push_back(sceneId); + } + + // Sort by betterSceneIndex + std::sort(sSortedSceneIds.begin(), sSortedSceneIds.end(), + [](SceneId a, SceneId b) { return betterSceneIndex[a] < betterSceneIndex[b]; }); +} + +// Helper: Find which region a shuffled entrance leads to by checking entrance pools +static RandoRegionId FindRegionFromShuffledEntrance(s32 shuffledEntrance) { + // First try the direct lookup (for entrances) + RandoRegionId direct = Rando::Logic::GetRegionIdFromEntrance(shuffledEntrance); + if (direct != RR_MAX) { + return direct; + } + + // If direct lookup fails, check all entrance pairs to find which original entrance/exit + // this shuffled entrance corresponds to, then look up that region + std::vector pools = { Rando::EntranceShuffle::POOL_INTERIOR, + Rando::EntranceShuffle::POOL_DUNGEON, + Rando::EntranceShuffle::POOL_OVERWORLD }; + + for (auto poolType : pools) { + auto entrancePairs = Rando::EntranceShuffle::GetEntrancePool(poolType); + for (const auto& pair : entrancePairs) { + // Check if shuffled entrance matches shuffled version of the original entrance + s32 shuffledFromEntrance = Rando::EntranceShuffle::GetShuffledEntrance(pair.entrance); + if (shuffledFromEntrance == shuffledEntrance) { + // Found it! The original entrance is pair.entrance + // Now look up that region + RandoRegionId region = Rando::Logic::GetRegionIdFromEntrance(pair.entrance); + if (region != RR_MAX) { + return region; + } + } + + // Also check if shuffled entrance matches shuffled version of the original exit + s32 shuffledFromExit = Rando::EntranceShuffle::GetShuffledEntrance(pair.exit); + if (shuffledFromExit == shuffledEntrance) { + // This is trickier - we have a shuffled exit. Find which region has this exit. + // The exit points TO a region, so look for the region that has this as an exit + for (const auto& [rid, rdata] : Rando::Logic::Regions) { + for (const auto& [exitId, regionExit] : rdata.exits) { + if (Rando::EntranceShuffle::GetShuffledEntrance(exitId) == shuffledEntrance) { + return rid; + } + } + } + } + } + } + + // Fallback for extended routing: try to find ANY region that has this entrance/exit + // This allows us to trace non-shuffled connections when extended routing is enabled + if (CVAR_EXTENDED_ROUTING) { + // Try looking for a region with this entrance + for (const auto& [rid, rdata] : Rando::Logic::Regions) { + for (const auto& [exitId, regionExit] : rdata.exits) { + if (exitId == shuffledEntrance || + Rando::EntranceShuffle::GetShuffledEntrance(exitId) == shuffledEntrance) { + return rid; + } + } + } + + // As a last resort, try by scene ID + SceneId targetScene = GetSceneFromEntrance(shuffledEntrance); + for (const auto& [rid, rdata] : Rando::Logic::Regions) { + if (rdata.sceneId == targetScene) { + return rid; + } + } + } + + return RR_MAX; +} +static void BuildRegionList() { + sRegionList.clear(); + + // Include all regions from the logic system + for (const auto& [regionId, region] : Rando::Logic::Regions) { + std::string displayName = GetRegionDisplayName(regionId); + if (!displayName.empty()) { + sRegionList.push_back({ regionId, displayName }); + } + } + + // Sort alphabetically + std::sort(sRegionList.begin(), sRegionList.end(), [](const auto& a, const auto& b) { return a.second < b.second; }); +} + +// Route finding using BFS +struct RouteStep { + RandoRegionId region; + s32 entranceUsed; // -1 for connections (same-scene transitions) + std::string description; +}; + +static std::vector FindRoute(RandoRegionId from, RandoRegionId to) { + std::vector result; + + if (from == to) { + return result; // Already there + } + + std::queue> queue; + std::set visited; + + // Start from the source region + queue.push({}); + visited.insert(from); + + RandoRegionId current = from; + + while (!queue.empty()) { + auto path = queue.front(); + queue.pop(); + + // Determine current region from path + current = path.empty() ? from : path.back().region; + + if (Rando::Logic::Regions.count(current) == 0) { + continue; + } + + auto& region = Rando::Logic::Regions[current]; + + // Check exits - only traverse exits that are in entrance shuffle pools (unless extended routing is enabled) + for (const auto& [exitId, regionExit] : region.exits) { + // Only traverse exits that are part of the entrance shuffle pools + // This prevents the BFS from exploring internal logic connections + // Extended routing bypasses this check for more complete (but messier) routes + if (!CVAR_EXTENDED_ROUTING && !IsExitInShufflePool(exitId)) { + continue; + } + + s32 actualExit = Rando::EntranceShuffle::GetShuffledEntrance(exitId); + + // Find the region this shuffled entrance leads to + RandoRegionId targetRegion = FindRegionFromShuffledEntrance(actualExit); + + // If we can't determine where this exit leads: + // - Normal mode: skip it + // - Extended routing: try scene-based lookup as fallback + if (targetRegion == RR_MAX) { + if (CVAR_EXTENDED_ROUTING) { + // Try to find any region in the target scene + SceneId targetScene = GetSceneFromEntrance(actualExit); + for (const auto& [rid, rdata] : Rando::Logic::Regions) { + if (rdata.sceneId == targetScene) { + targetRegion = rid; + break; + } + } + } + + // Still no region found, skip this exit + if (targetRegion == RR_MAX) { + continue; + } + } + + if (targetRegion == to) { + // Found the destination + result = path; + RouteStep step; + step.region = to; + step.entranceUsed = exitId; + // Use the source entrance name (what you use from current location), not the destination + step.description = Rando::EntranceTracker::GetEntranceName(exitId); + result.push_back(step); + return result; + } + + if (targetRegion != RR_MAX && visited.find(targetRegion) == visited.end()) { + visited.insert(targetRegion); + auto newPath = path; + RouteStep step; + step.region = targetRegion; + step.entranceUsed = exitId; + // Use the source entrance name (what you use from current location), not the destination + step.description = Rando::EntranceTracker::GetEntranceName(exitId); + newPath.push_back(step); + queue.push(newPath); + } + } + + // In extended routing mode, also allow traversing connections (same-scene transitions) + // Normal mode skips these to avoid garbage routes through internal regions + if (CVAR_EXTENDED_ROUTING) { + for (const auto& [connectedRegion, condition] : region.connections) { + if (connectedRegion == to) { + result = path; + RouteStep step; + step.region = to; + step.entranceUsed = -1; + step.description = "(internal transition)"; + result.push_back(step); + return result; + } + + if (visited.find(connectedRegion) == visited.end()) { + visited.insert(connectedRegion); + auto newPath = path; + RouteStep step; + step.region = connectedRegion; + step.entranceUsed = -1; + step.description = "(internal transition)"; + newPath.push_back(step); + queue.push(newPath); + } + } + } + } + + return result; // Empty if no route found +} + +// Format route for display (compact format) +static std::string FormatRoute(RandoRegionId from, const std::vector& route) { + if (route.empty()) { + return "No route found"; + } + + std::string result; + std::string fromName = GetRegionDisplayName(from); + + if (!fromName.empty()) { + result = fromName; + } + + for (const auto& step : route) { + // Show the entrance you take to get there + if (!step.description.empty()) { + result += " -> " + step.description; + } + } + + return result.empty() ? "No route found" : result; +} + +namespace Rando { + +namespace EntranceTracker { + +void EntranceTrackerWindow::Draw() { + if (!CVAR_SHOW_ENTRANCE_TRACKER) { + return; + } + + if (CVAR_VISIBILITY_MODE == ENTRANCE_TRACKER_VISIBILITY_MODE_ONLY_ON_PAUSE_MENU && + (!gPlayState || !gPlayState->pauseCtx.state)) { + return; + } + + if ((CVAR_VISIBILITY_MODE == ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_TOGGLE || + CVAR_VISIBILITY_MODE == ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_HOLD) && + !sEntranceTrackerBtnState) { + return; + } + + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, trackerBG); + ImGui::PushStyleColor(ImGuiCol_TitleBg, trackerBG); + ImGui::PushStyleColor(ImGuiCol_WindowBg, trackerBG); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + + ImGui::SetNextWindowSize(ImVec2(450.0f, 500.0f), ImGuiCond_FirstUseEver); + + ImGui::Begin("Entrance Tracker", nullptr, ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing); + + trackerBG.w = ImGui::IsWindowDocked() ? 1.0f : CVAR_TRACKER_OPACITY; + trackerScale = CVAR_TRACKER_SCALE; + ImGui::SetWindowFontScale(trackerScale); + + if (!gPlayState || !IS_RANDO) { + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize("No Rando Save Loaded").x) / 2); + ImGui::SetCursorPosY(ImGui::GetWindowHeight() / 2 - 10.0f); + ImGui::TextColored(UIWidgets::ColorValues.at(UIWidgets::Colors::Gray), "No Rando Save Loaded"); + ImGui::End(); + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(1); + return; + } + + if (!Rando::EntranceShuffle::IsEntranceShuffleEnabled()) { + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize("Entrance Shuffle Not Enabled").x) / 2); + ImGui::SetCursorPosY(ImGui::GetWindowHeight() / 2 - 10.0f); + ImGui::TextColored(UIWidgets::ColorValues.at(UIWidgets::Colors::Gray), "Entrance Shuffle Not Enabled"); + ImGui::End(); + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(1); + return; + } + + // Tab bar for Entrances and Route Finder + if (ImGui::BeginTabBar("EntranceTrackerTabs")) { + if (ImGui::BeginTabItem("Entrances")) { + ImGui::BeginChild("EntranceList"); + + // Search bar for entrances tab + if (CVAR_SHOW_SEARCH) { + UIWidgets::PushStyleInput(); + sEntranceTrackerFilter.Draw("##filter", ImGui::GetContentRegionAvail().x - 40.0f); + UIWidgets::PopStyleInput(); + + ImGui::SameLine(); + if (!sEntranceTrackerFilter.IsActive()) { + ImGui::Text("Search"); + } else { + if (UIWidgets::Button(ICON_FA_TIMES, UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { + sEntranceTrackerFilter.Clear(); + } + } + } + + bool spoilerMode = CVAR_SPOILER_MODE; + + for (SceneId sceneId : sSortedSceneIds) { + auto& entrances = sEntrancesByArea[sceneId]; + + // Filter entrances + std::vector filtered; + for (const auto& conn : entrances) { + // Check spoiler mode + if (!spoilerMode && !IsEntranceDiscovered(conn.shuffledEntrance)) { + continue; + } + + // Check search filter + std::string searchStr = conn.originalName + " " + conn.shuffledName; + if (!sEntranceTrackerFilter.PassFilter(searchStr.c_str())) { + continue; + } + + filtered.push_back(&conn); + } + + if (filtered.empty()) { + continue; + } + + ImGui::PushID(sceneId); + ImGui::Separator(); + + std::string headerText = Ship_GetSceneName(sceneId); + headerText += " (" + std::to_string(filtered.size()) + ")"; + + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0)); + + if (ImGui::CollapsingHeader(headerText.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(20.0f); + + for (const auto* conn : filtered) { + std::string line = conn->originalName + " -> " + conn->shuffledName; + ImGui::Text("%s", line.c_str()); + } + + ImGui::Unindent(20.0f); + } + + ImGui::PopStyleColor(); + ImGui::PopID(); + } + + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Route Finder")) { + ImGui::BeginChild("RouteFinder"); + + // Build region list if empty + if (sRegionList.empty()) { + BuildRegionList(); + } + + // From section with search + ImGui::Text("From:"); + ImGui::SameLine(); + + // Calculate available width for combo + float availWidth = ImGui::GetContentRegionAvail().x - 100.0f; + + if (ImGui::BeginCombo("##FromRegion", + sRouteFromIndex < (int)sRegionList.size() + ? sRegionList[sRouteFromIndex].second.c_str() + : "Select...", + ImGuiComboFlags_HeightLarge)) { + + // Search filter for From + sRouteFromFilter.Draw("##FromSearch", availWidth); + ImGui::Separator(); + + int visibleCount = 0; + for (int i = 0; i < (int)sRegionList.size(); i++) { + if (!sRouteFromFilter.PassFilter(sRegionList[i].second.c_str())) { + continue; + } + + visibleCount++; + ImGui::PushID(i); + bool selected = (i == sRouteFromIndex); + if (ImGui::Selectable(sRegionList[i].second.c_str(), selected)) { + sRouteFromIndex = i; + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + ImGui::PopID(); + } + + if (visibleCount == 0) { + ImGui::TextDisabled("No matches"); + } + + ImGui::EndCombo(); + } + + // "Use Current Location" button + ImGui::SameLine(); + if (UIWidgets::Button("Current", UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { + if (gPlayState) { + RandoRegionId currentRegion = Rando::Logic::GetRegionIdFromEntrance(gSaveContext.save.entrance); + bool found = false; + + // First try: find exact region match + if (currentRegion != RR_MAX) { + for (int i = 0; i < (int)sRegionList.size(); i++) { + if (sRegionList[i].first == currentRegion) { + sRouteFromIndex = i; + found = true; + break; + } + } + } + + // Fallback: find a region matching the current scene + if (!found) { + SceneId currentScene = static_cast(gPlayState->sceneId); + for (int i = 0; i < (int)sRegionList.size(); i++) { + RandoRegionId regionId = sRegionList[i].first; + if (Rando::Logic::Regions.count(regionId) > 0 && + Rando::Logic::Regions.at(regionId).sceneId == currentScene) { + sRouteFromIndex = i; + break; + } + } + } + } + } + UIWidgets::Tooltip("Use current location"); + + // To section with search + ImGui::Text("To:"); + ImGui::SameLine(); + + if (ImGui::BeginCombo("##ToRegion", + sRouteToIndex < (int)sRegionList.size() ? sRegionList[sRouteToIndex].second.c_str() + : "Select...", + ImGuiComboFlags_HeightLarge)) { + + // Search filter for To + sRouteToFilter.Draw("##ToSearch", availWidth); + ImGui::Separator(); + + int visibleCount = 0; + for (int i = 0; i < (int)sRegionList.size(); i++) { + if (!sRouteToFilter.PassFilter(sRegionList[i].second.c_str())) { + continue; + } + + visibleCount++; + ImGui::PushID(i); + bool selected = (i == sRouteToIndex); + if (ImGui::Selectable(sRegionList[i].second.c_str(), selected)) { + sRouteToIndex = i; + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + ImGui::PopID(); + } + + if (visibleCount == 0) { + ImGui::TextDisabled("No matches"); + } + + ImGui::EndCombo(); + } + + // Swap From/To button + ImGui::SameLine(); + if (UIWidgets::Button(ICON_FA_EXCHANGE, UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { + std::swap(sRouteFromIndex, sRouteToIndex); + } + UIWidgets::Tooltip("Swap From and To locations"); + + // Find Route button + if (UIWidgets::Button("Find Route", UIWidgets::ButtonOptions().Color(UIWidgets::Colors::Green))) { + if (sRouteFromIndex < (int)sRegionList.size() && sRouteToIndex < (int)sRegionList.size()) { + RandoRegionId from = sRegionList[sRouteFromIndex].first; + RandoRegionId to = sRegionList[sRouteToIndex].first; + + // Check if spoilers are off and destination is undiscovered + bool spoilerMode = CVAR_SPOILER_MODE; + bool destinationDiscovered = true; + + if (!spoilerMode && Rando::Logic::Regions.count(to) > 0) { + // Check if any entrance to this region has been discovered + SceneId destScene = Rando::Logic::Regions.at(to).sceneId; + destinationDiscovered = false; + + // Iterate through all regions to find entrances leading to the destination + for (const auto& [regionId, region] : Rando::Logic::Regions) { + for (const auto& [exitId, regionExit] : region.exits) { + if (regionExit.returnEntrance == ONE_WAY_EXIT) + continue; + if (GetSceneFromEntrance(regionExit.returnEntrance) == destScene) { + if (IsEntranceDiscovered(regionExit.returnEntrance)) { + destinationDiscovered = true; + break; + } + } + } + if (destinationDiscovered) + break; + } + } + + if (!destinationDiscovered) { + sRouteResult = "No route could be found"; + } else { + auto route = FindRoute(from, to); + sRouteResult = FormatRoute(from, route); + } + } + } + + // Route result + ImGui::Separator(); + ImGui::TextWrapped("%s", sRouteResult.c_str()); + + ImGui::EndChild(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::End(); + + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(1); +} + +void EntranceTrackerSettingsWindow::DrawElement() { + if (CVarGetInteger("gWindows.EntranceTracker", 0)) { + UIWidgets::WindowButton("Disable Entrance Tracker", "gWindows.EntranceTracker", BenGui::mEntranceTrackerWindow, + { .size = UIWidgets::Sizes::Inline, .color = UIWidgets::Colors::Red }); + } else { + UIWidgets::WindowButton("Enable Entrance Tracker", "gWindows.EntranceTracker", BenGui::mEntranceTrackerWindow, + { .size = UIWidgets::Sizes::Inline, .color = UIWidgets::Colors::Green }); + } + + if (ImGui::BeginTable("Settings Table", 2)) { + ImGui::TableSetupColumn("col1", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("col2", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextColumn(); + + ImGui::SeparatorText("Display Settings"); + + UIWidgets::CVarCheckbox("Show All Entrances (Spoilers)", CVAR_NAME_SPOILER_MODE, + UIWidgets::CheckboxOptions().DefaultValue(true)); + UIWidgets::Tooltip("When disabled, only shows entrances you have discovered by using them."); + + UIWidgets::CVarCheckbox("Show Search", CVAR_NAME_SHOW_SEARCH, UIWidgets::CheckboxOptions().DefaultValue(true)); + + UIWidgets::CVarCheckbox("Extended Routing (Experimental)", CVAR_NAME_EXTENDED_ROUTING); + UIWidgets::Tooltip("Enables routing through areas not yet in entrance shuffle pools.\n\n" + "Routes may include internal logic regions that don't correspond to " + "actual in-game locations. You'll need to use your knowledge of the " + "vanilla world layout to interpret these steps."); + + ImGui::TableNextColumn(); + ImGui::SeparatorText("Window Settings"); + + UIWidgets::CVarCombobox("Visibility", CVAR_NAME_VISIBILITY_MODE, &sVisibilityModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(ENTRANCE_TRACKER_VISIBILITY_MODE_ALWAYS) + .ComponentAlignment(UIWidgets::ComponentAlignment::Right) + .LabelPosition(UIWidgets::LabelPosition::Far)); + + if (CVAR_VISIBILITY_MODE == ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_TOGGLE || + CVAR_VISIBILITY_MODE == ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_HOLD) { + UIWidgets::CVarBtnSelector("Button Combination:", CVAR_NAME_VISIBILITY_BTN, + UIWidgets::BtnSelectorOptions().DefaultValue(BTN_CUSTOM_MODIFIER1)); + } + + if (UIWidgets::CVarSliderFloat("Opacity", CVAR_NAME_TRACKER_OPACITY, + { + .format = "Opacity: %.1f", + .step = 0.10f, + .min = 0.0f, + .max = 1.0f, + .defaultValue = 0.5f, + .labelPosition = UIWidgets::LabelPosition::None, + })) { + trackerBG.w = CVAR_TRACKER_OPACITY; + } + + if (UIWidgets::CVarSliderFloat("Scale", CVAR_NAME_TRACKER_SCALE, + { + .format = "Scale: %.1f", + .step = 0.10f, + .min = 0.7f, + .max = 2.5f, + .defaultValue = 1.0f, + .labelPosition = UIWidgets::LabelPosition::None, + })) { + trackerScale = CVAR_TRACKER_SCALE; + } + + ImGui::EndTable(); + } +} + +void Init() { + BuildEntranceNameMap(); + BuildEntranceIndexMap(); + + trackerBG = { 0, 0, 0, CVAR_TRACKER_OPACITY }; + trackerScale = CVAR_TRACKER_SCALE; +} + +static RegisterShipInitFunc initFunc( + []() { + COND_HOOK(OnGameStateMainStart, CVAR_VISIBILITY_MODE >= ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_TOGGLE, []() { + Input* input = CONTROLLER1(gGameState); + + if (CVAR_VISIBILITY_MODE == ENTRANCE_TRACKER_VISIBILITY_MODE_BUTTON_HOLD) { + if (CHECK_BTN_ALL(input->cur.button, CVAR_VISIBILITY_BTN)) { + sEntranceTrackerBtnState = true; + } else { + sEntranceTrackerBtnState = false; + } + } else { + if (CHECK_BTN_ALL(input->cur.button, CVAR_VISIBILITY_BTN) && + CHECK_BTN_ANY(input->press.button, CVAR_VISIBILITY_BTN)) { + sEntranceTrackerBtnState = !sEntranceTrackerBtnState; + } + } + }); + }, + { CVAR_NAME_VISIBILITY_MODE }); + +void OnFileLoad() { + if (!IS_RANDO) { + return; + } + + BuildEntranceIndexMap(); + BuildEntranceData(); + BuildRegionList(); + sRouteResult = ""; +} + +} // namespace EntranceTracker + +} // namespace Rando diff --git a/mm/2s2h/Rando/EntranceTracker/EntranceTracker.h b/mm/2s2h/Rando/EntranceTracker/EntranceTracker.h new file mode 100644 index 0000000000..c1c76640f2 --- /dev/null +++ b/mm/2s2h/Rando/EntranceTracker/EntranceTracker.h @@ -0,0 +1,44 @@ +#ifndef RANDO_ENTRANCE_TRACKER_H +#define RANDO_ENTRANCE_TRACKER_H + +#include "Rando/Rando.h" +#include + +namespace Rando { + +namespace EntranceTracker { + +void Init(); +void OnFileLoad(); + +// Get human-readable name for an entrance +std::string GetEntranceName(s32 entranceId); + +// Discovery tracking +void SetEntranceDiscovered(s32 entranceId); +bool IsEntranceDiscovered(s32 entranceId); + +class EntranceTrackerWindow : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void InitElement() override{}; + void DrawElement() override{}; + void Draw() override; + void UpdateElement() override{}; +}; + +class EntranceTrackerSettingsWindow : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void InitElement() override{}; + void DrawElement() override; + void UpdateElement() override{}; +}; + +} // namespace EntranceTracker + +} // namespace Rando + +#endif // RANDO_ENTRANCE_TRACKER_H 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 b72dde87a1..c502270bee 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 { @@ -122,7 +123,13 @@ void FindReachableRegions(RandoRegionId currentRegion, std::set& // Explore exits for (auto& [exitId, regionExit] : sourceRegion.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); diff --git a/mm/2s2h/Rando/Logic/Regions/MilkRoad.cpp b/mm/2s2h/Rando/Logic/Regions/MilkRoad.cpp index 9187e49287..a65e66596c 100644 --- a/mm/2s2h/Rando/Logic/Regions/MilkRoad.cpp +++ b/mm/2s2h/Rando/Logic/Regions/MilkRoad.cpp @@ -159,7 +159,8 @@ static RegisterShipInitFunc initFunc([]() { CONNECTION(RR_MILK_ROAD, (RANDO_EVENTS[RE_COWS_FROM_ALIENS] && IS_NIGHT2()) || FINAL_DAY()), }, }; - Regions[RR_RANCH_BARN] = RandoRegion{ .sceneId = SCENE_OMOYA, + Regions[RR_RANCH_BARN] = RandoRegion{ .name = "Mama's House (Barn)", + .sceneId = SCENE_OMOYA, .checks = { CHECK(RC_ROMANI_RANCH_BARN_COW_LEFT, CAN_PLAY_SONG(EPONA) && (BETWEEN(TIME_NIGHT1_PM_06_00, TIME_NIGHT1_AM_02_30) || RANDO_EVENTS[RE_COWS_FROM_ALIENS])), CHECK(RC_ROMANI_RANCH_BARN_COW_MIDDLE, CAN_PLAY_SONG(EPONA) && (BETWEEN(TIME_NIGHT1_PM_06_00, TIME_NIGHT1_AM_02_30) || RANDO_EVENTS[RE_COWS_FROM_ALIENS])), @@ -173,7 +174,8 @@ static RegisterShipInitFunc initFunc([]() { STAY(TIME_NIGHT3_PM_08_00, false), }, }; - Regions[RR_RANCH_HOUSE] = RandoRegion{ .sceneId = SCENE_OMOYA, + Regions[RR_RANCH_HOUSE] = RandoRegion{ .name = "Mama's House (Main)", + .sceneId = SCENE_OMOYA, .exits = { // TO FROM EXIT(ENTRANCE(ROMANI_RANCH, 3), ENTRANCE(RANCH_HOUSE, 1), true), }, diff --git a/mm/2s2h/Rando/Menu.cpp b/mm/2s2h/Rando/Menu.cpp index b0b63dc35e..f448109f29 100644 --- a/mm/2s2h/Rando/Menu.cpp +++ b/mm/2s2h/Rando/Menu.cpp @@ -2,6 +2,7 @@ #include "Rando/Spoiler/Spoiler.h" #include "2s2h/BenGui/UIWidgets.hpp" #include "Rando/CheckTracker/CheckTracker.h" +#include "Rando/EntranceTracker/EntranceTracker.h" #include "Rando/MiscBehavior/ClockShuffle.h" #include "build.h" #include "2s2h/BenGui/BenMenu.h" @@ -47,6 +48,13 @@ std::unordered_map trapItemsOptions = { }; // clang-format off +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, @@ -63,6 +71,8 @@ bool isExcludedInitialized = false; namespace BenGui { extern std::shared_ptr mRandoCheckTrackerWindow; extern std::shared_ptr mRandoCheckTrackerSettingsWindow; +extern std::shared_ptr mEntranceTrackerWindow; +extern std::shared_ptr mEntranceTrackerSettingsWindow; extern std::shared_ptr mBenMenu; } // namespace BenGui @@ -440,6 +450,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)); @@ -1082,6 +1093,12 @@ void Rando::RegisterMenu() { mBenMenu->AddWidget(path, "Popout Settings", WIDGET_WINDOW_BUTTON) .CVar("gWindows.CheckTrackerSettings") .WindowName("Check Tracker Settings"); + + mBenMenu->AddSidebarEntry("Rando", "Entrance Tracker", 1); + path.sidebarName = "Entrance Tracker"; + mBenMenu->AddWidget(path, "Popout Settings", WIDGET_WINDOW_BUTTON) + .CVar("gWindows.EntranceTrackerSettings") + .WindowName("Entrance Tracker Settings"); } static RegisterMenuInitFunc initFunc(Rando::RegisterMenu); diff --git a/mm/2s2h/Rando/MiscBehavior/EntranceHooks.cpp b/mm/2s2h/Rando/MiscBehavior/EntranceHooks.cpp new file mode 100644 index 0000000000..399dfe428c --- /dev/null +++ b/mm/2s2h/Rando/MiscBehavior/EntranceHooks.cpp @@ -0,0 +1,43 @@ +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/ShipInit.hpp" +#include "2s2h/Rando/Logic/EntranceShuffle.h" +#include "2s2h/Rando/EntranceTracker/EntranceTracker.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); + + // Record discovered entrance for the tracker + EntranceTracker::SetEntranceDiscovered(shuffledEntrance); + + 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 fad880ca4b..18b96850de 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" @@ -17,6 +18,7 @@ void Rando::MiscBehavior::OnFileLoad() { Rando::MiscBehavior::InitKaleidoItemPage(); Rando::MiscBehavior::InitOfferGetItemBehavior(); Rando::MiscBehavior::SariasSongHint(); + 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 be39bacd91..e506e78d25 100644 --- a/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp +++ b/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp @@ -2,6 +2,7 @@ #include "Rando/Rando.h" #include "Rando/Spoiler/Spoiler.h" #include "Rando/Logic/Logic.h" +#include "Rando/Logic/EntranceShuffle.h" #include "2s2h/ShipUtils.h" #include #include "ClockShuffle.h" @@ -143,6 +144,9 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) { } } + // 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/Rando.cpp b/mm/2s2h/Rando/Rando.cpp index 09cd90cbc8..49314e4827 100644 --- a/mm/2s2h/Rando/Rando.cpp +++ b/mm/2s2h/Rando/Rando.cpp @@ -5,6 +5,7 @@ #include "Rando/MiscBehavior/ClockShuffle.h" #include "Rando/Spoiler/Spoiler.h" #include "Rando/CheckTracker/CheckTracker.h" +#include "Rando/EntranceTracker/EntranceTracker.h" #include "2s2h/ShipInit.hpp" #include #include @@ -14,6 +15,7 @@ void OnSaveLoadHandler(s16 fileNum) { Rando::MiscBehavior::OnFileLoad(); Rando::ActorBehavior::OnFileLoad(); Rando::CheckTracker::OnFileLoad(); + Rando::EntranceTracker::OnFileLoad(); Rando::ClockShuffle::OnFileLoad(); // Re-initalizes enhancements that are effected by the save being rando or not @@ -26,6 +28,7 @@ void Rando::Init() { Rando::MiscBehavior::Init(); Rando::ActorBehavior::Init(); Rando::CheckTracker::Init(); + Rando::EntranceTracker::Init(); Ship::Context::GetInstance()->GetFileDropMgr()->RegisterDropHandler(Rando::Spoiler::HandleFileDropped); GameInteractor::Instance->RegisterGameHook(OnSaveLoadHandler); diff --git a/mm/2s2h/Rando/StaticData/Options.cpp b/mm/2s2h/Rando/StaticData/Options.cpp index 8317c0eec7..93840a902b 100644 --- a/mm/2s2h/Rando/StaticData/Options.cpp +++ b/mm/2s2h/Rando/StaticData/Options.cpp @@ -44,6 +44,7 @@ std::map Options = { RO(RO_SHUFFLE_CRATE_DROPS, RO_GENERIC_OFF), RO(RO_SHUFFLE_ENEMY_DROPS, RO_GENERIC_OFF), RO(RO_SHUFFLE_ENEMY_SOULS, 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 673309a612..050c11cd4e 100644 --- a/mm/2s2h/Rando/Types.h +++ b/mm/2s2h/Rando/Types.h @@ -2977,6 +2977,7 @@ typedef enum { RO_SHUFFLE_CRATE_DROPS, RO_SHUFFLE_ENEMY_DROPS, RO_SHUFFLE_ENEMY_SOULS, + RO_SHUFFLE_ENTRANCES, RO_SHUFFLE_FREESTANDING_ITEMS, RO_SHUFFLE_FROGS, RO_SHUFFLE_GOLD_SKULLTULAS, @@ -3043,6 +3044,13 @@ typedef enum { RO_CLOCK_SHUFFLE_DESCENDING, } RandoClockShuffleOptions; +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, diff --git a/mm/include/z64save.h b/mm/include/z64save.h index d5e265e778..cd19e5a647 100644 --- a/mm/include/z64save.h +++ b/mm/include/z64save.h @@ -388,6 +388,7 @@ 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 discoveredEntrances[32]; // Bitfield for 256 discovered entrance flags (for entrance tracker) } RandoSaveInfo; // These are values added by 2S2H that we need to be persisted to the save file