diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index 914c8828e..85fe16237 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -188,6 +188,8 @@ source_group("Header Files\\soh\\Enhancements\\debugger" FILES ${Header_Files__s set(Header_Files__soh__Enhancements__randomizer "soh/Enhancements/randomizer/randomizer.h" + "soh/Enhancements/randomizer/randomizer_entrance.h" + "soh/Enhancements/randomizer/randomizer_grotto.h" "soh/Enhancements/randomizer/randomizer_inf.h" "soh/Enhancements/randomizer/randomizer_item_tracker.h" "soh/Enhancements/randomizer/adult_trade_shuffle.h" @@ -314,6 +316,8 @@ source_group("Source Files\\soh\\Enhancements\\debugger" FILES ${Source_Files__s set(Source_Files__soh__Enhancements__randomizer "soh/Enhancements/randomizer/randomizer.cpp" + "soh/Enhancements/randomizer/randomizer_entrance.c" + "soh/Enhancements/randomizer/randomizer_grotto.c" "soh/Enhancements/randomizer/randomizer_item_tracker.cpp" "soh/Enhancements/randomizer/adult_trade_shuffle.c" "soh/Enhancements/randomizer/randomizer_check_objects.cpp" diff --git a/soh/include/z64.h b/soh/include/z64.h index f0cc888c1..c376470e1 100644 --- a/soh/include/z64.h +++ b/soh/include/z64.h @@ -1181,6 +1181,7 @@ typedef struct { /* */ s32 returnEntranceIndex; /* */ s8 roomIndex; /* */ s8 data; + /* */ s8 exitScene; /* */ Vec3f pos; } BetterSceneSelectGrottoData; diff --git a/soh/include/z64save.h b/soh/include/z64save.h index 77f7061ec..b29e0e6cf 100644 --- a/soh/include/z64save.h +++ b/soh/include/z64save.h @@ -6,6 +6,7 @@ #include "z64audio.h" #include "soh/Enhancements/randomizer/randomizerTypes.h" #include "soh/Enhancements/randomizer/randomizer_inf.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" typedef struct { /* 0x00 */ u8 buttonItems[8]; @@ -185,6 +186,7 @@ typedef struct { RandoSetting randoSettings[300]; ItemLocationRando itemLocations[RC_MAX]; HintLocationRando hintLocations[50]; + EntranceOverride entranceOverrides[ENTRANCE_OVERRIDES_MAX_COUNT]; char childAltarText[250]; char adultAltarText[750]; char ganonHintText[150]; diff --git a/soh/soh/Enhancements/randomizer/3drando/entrance.hpp b/soh/soh/Enhancements/randomizer/3drando/entrance.hpp index a9b29bca5..013f31dbe 100644 --- a/soh/soh/Enhancements/randomizer/3drando/entrance.hpp +++ b/soh/soh/Enhancements/randomizer/3drando/entrance.hpp @@ -7,60 +7,11 @@ #include #include +#include "../randomizer_entrance.h" + #define ENTRANCE_SHUFFLE_SUCCESS 0 #define ENTRANCE_SHUFFLE_FAILURE 1 - -typedef struct { - int16_t index; - int16_t destination; - int16_t blueWarp; - int16_t override; - int16_t overrideDestination; -} EntranceOverride; - -typedef enum { - ENTRANCE_GROUP_NO_GROUP, - ENTRANCE_GROUP_KOKIRI_FOREST, - ENTRANCE_GROUP_LOST_WOODS, - ENTRANCE_GROUP_KAKARIKO, - ENTRANCE_GROUP_GRAVEYARD, - ENTRANCE_GROUP_DEATH_MOUNTAIN_TRAIL, - ENTRANCE_GROUP_DEATH_MOUNTAIN_CRATER, - ENTRANCE_GROUP_GORON_CITY, - ENTRANCE_GROUP_ZORAS_DOMAIN, - ENTRANCE_GROUP_HYRULE_FIELD, - ENTRANCE_GROUP_LON_LON_RANCH, - ENTRANCE_GROUP_LAKE_HYLIA, - ENTRANCE_GROUP_GERUDO_VALLEY, - ENTRANCE_GROUP_HAUNTED_WASTELAND, - ENTRANCE_GROUP_MARKET, - ENTRANCE_GROUP_HYRULE_CASTLE, - SPOILER_ENTRANCE_GROUP_COUNT, -} SpoilerEntranceGroup; - -typedef enum { - ENTRANCE_TYPE_OVERWORLD, - ENTRANCE_TYPE_INTERIOR, - ENTRANCE_TYPE_GROTTO, - ENTRANCE_TYPE_DUNGEON, - ENTRANCE_TYPE_COUNT, -} TrackerEntranceType; - -typedef struct { - int16_t index; - char* name; - SpoilerEntranceGroup group; - TrackerEntranceType type; - uint8_t oneExit; -} EntranceData; - -typedef struct { - uint8_t EntranceCount; - uint16_t GroupEntranceCounts[SPOILER_ENTRANCE_GROUP_COUNT]; - uint16_t GroupOffsets[SPOILER_ENTRANCE_GROUP_COUNT]; -} EntranceTrackingData; - extern std::list entranceOverrides; enum class EntranceType { @@ -314,7 +265,6 @@ private: int ShuffleAllEntrances(); void CreateEntranceOverrides(); -EntranceTrackingData* GetEntranceTrackingData(); extern std::vector> playthroughEntrances; extern bool noRandomEntrances; diff --git a/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_death_mountain.cpp b/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_death_mountain.cpp index b7a23c316..6633c567b 100644 --- a/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_death_mountain.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_death_mountain.cpp @@ -122,7 +122,7 @@ void AreaTable_Init_DeathMountain() { Entrance(GC_SHOP, {[]{return (IsAdult && StopGCRollingGoronAsAdult) || (IsChild && (CanBlastOrSmash || GoronBracelet || GoronCityChildFire || CanUse(BOW)));}, /*Glitched*/[]{return IsChild && Sticks && CanDoGlitch(GlitchType::QPA, GlitchDifficulty::ADVANCED);}}), Entrance(GC_DARUNIAS_CHAMBER, {[]{return (IsAdult && StopGCRollingGoronAsAdult) || GCDaruniasDoorOpenChild;}}), - Entrance(GC_GROTTO, {[]{return IsAdult && ((CanPlay(SongOfTime) && ((EffectiveHealth > 2) || CanUse(GORON_TUNIC) || CanUse(LONGSHOT) || CanUse(NAYRUS_LOVE))) || (EffectiveHealth > 1 && CanUse(GORON_TUNIC) && CanUse(HOOKSHOT)) || (CanUse(NAYRUS_LOVE) && CanUse(HOOKSHOT)));}, + Entrance(GC_GROTTO_PLATFORM, {[]{return IsAdult && ((CanPlay(SongOfTime) && ((EffectiveHealth > 2) || CanUse(GORON_TUNIC) || CanUse(LONGSHOT) || CanUse(NAYRUS_LOVE))) || (EffectiveHealth > 1 && CanUse(GORON_TUNIC) && CanUse(HOOKSHOT)) || (CanUse(NAYRUS_LOVE) && CanUse(HOOKSHOT)));}, /*Glitched*/[]{return (HasBombchus && ((IsChild && CanDoGlitch(GlitchType::BombHover, GlitchDifficulty::NOVICE)) || CanDoGlitch(GlitchType::BombHover, GlitchDifficulty::INTERMEDIATE))) || (IsChild && CanUse(LONGSHOT));}}), }); diff --git a/soh/soh/Enhancements/randomizer/3drando/settings.cpp b/soh/soh/Enhancements/randomizer/3drando/settings.cpp index 78b2e03d5..f36dade6d 100644 --- a/soh/soh/Enhancements/randomizer/3drando/settings.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/settings.cpp @@ -2563,6 +2563,13 @@ namespace Settings { ShuffleKokiriSword.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_KOKIRI_SWORD]); ShuffleOcarinas.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_OCARINA]); + // Shuffle Entrances + ShuffleEntrances.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_ENTRANCES]); + ShuffleDungeonEntrances.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_DUNGEON_ENTRANCES]); + ShuffleOverworldEntrances.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_OVERWORLD_ENTRANCES]); + ShuffleInteriorEntrances.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_INTERIOR_ENTRANCES]); + ShuffleGrottoEntrances.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_GROTTO_ENTRANCES]); + // 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(cvarSettings[RSK_SKIP_CHILD_ZELDA]) { diff --git a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp index 9e326c168..b48f7bef9 100644 --- a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp @@ -296,26 +296,46 @@ static void WriteLocation( } //Writes a shuffled entrance to the specified node -static void WriteShuffledEntrance( - tinyxml2::XMLElement* parentNode, - Entrance* entrance, - const bool withPadding = false -) { - auto node = parentNode->InsertNewChildElement("entrance"); - node->SetAttribute("name", entrance->GetName().c_str()); - auto text = entrance->GetConnectedRegion()->regionName + " from " + entrance->GetReplacement()->GetParentRegion()->regionName; - node->SetText(text.c_str()); +static void WriteShuffledEntrance(std::string sphereString, Entrance* entrance) { + int16_t originalIndex = entrance->GetIndex(); + int16_t destinationIndex = entrance->GetReverse()->GetIndex(); + int16_t originalBlueWarp = entrance->GetBlueWarp(); + int16_t replacementBlueWarp = entrance->GetReplacement()->GetReverse()->GetBlueWarp(); + int16_t replacementIndex = entrance->GetReplacement()->GetIndex(); + int16_t replacementDestinationIndex = entrance->GetReplacement()->GetReverse()->GetIndex(); + std::string name = entrance->GetName(); + std::string text = entrance->GetConnectedRegion()->regionName + " from " + entrance->GetReplacement()->GetParentRegion()->regionName; - if (withPadding) { - constexpr int16_t LONGEST_NAME = 56; //The longest name of a vanilla entrance + switch (gSaveContext.language) { + case LANGUAGE_ENG: + case LANGUAGE_FRA: + default: + json entranceJson = json::object({ + {"index", originalIndex}, + {"destination", destinationIndex}, + {"blueWarp", originalBlueWarp}, + {"override", replacementIndex}, + {"overrideDestination", replacementDestinationIndex}, + }); - //Insert padding so we get a kind of table in the XML document - int16_t requiredPadding = LONGEST_NAME - entrance->GetName().length(); - if (requiredPadding > 0) { - std::string padding(requiredPadding, ' '); - node->SetAttribute("_", padding.c_str()); + jsonData["entrances"].push_back(entranceJson); + + // When decoupled entrances is off, handle saving reverse entrances with blue warps + if (!false) { // RANDOTODO: add check for decoupled entrances + json reverseEntranceJson = json::object({ + {"index", replacementDestinationIndex}, + {"destination", replacementIndex}, + {"blueWarp", replacementBlueWarp}, + {"override", destinationIndex}, + {"overrideDestination", originalIndex}, + }); + + jsonData["entrances"].push_back(reverseEntranceJson); + } + + jsonData["entrancesMap"][sphereString][name] = text; + break; } - } } // Writes the settings (without excluded locations, starting inventory and tricks) to the spoilerLog document. @@ -512,7 +532,7 @@ static void WritePlaythrough() { for (uint32_t i = 0; i < playthroughLocations.size(); ++i) { auto sphereNum = std::to_string(i); std::string sphereString = "sphere "; - if (sphereNum.length() == 1) sphereString += "0"; + if (i < 10) sphereString += "0"; sphereString += sphereNum; for (const uint32_t key : playthroughLocations[i]) { WriteLocation(sphereString, key, true); @@ -523,23 +543,16 @@ static void WritePlaythrough() { } //Write the randomized entrance playthrough to the spoiler log, if applicable -static void WriteShuffledEntrances(tinyxml2::XMLDocument& spoilerLog) { - if (!Settings::ShuffleEntrances || noRandomEntrances) { - return; +static void WriteShuffledEntrances() { + for (uint32_t i = 0; i < playthroughEntrances.size(); ++i) { + auto sphereNum = std::to_string(i); + std::string sphereString = "sphere "; + if (i < 10) sphereString += "0"; + sphereString += sphereNum; + for (Entrance* entrance : playthroughEntrances[i]) { + WriteShuffledEntrance(sphereString, entrance); } - - auto playthroughNode = spoilerLog.NewElement("entrance-playthrough"); - - for (uint32_t i = 0; i < playthroughEntrances.size(); ++i) { - auto sphereNode = playthroughNode->InsertNewChildElement("sphere"); - sphereNode->SetAttribute("level", i + 1); - - for (Entrance* entrance : playthroughEntrances[i]) { - WriteShuffledEntrance(sphereNode, entrance, true); - } - } - - spoilerLog.RootElement()->InsertEndChild(playthroughNode); + } } // Writes the WOTH locations to the spoiler log, if there are any. @@ -744,7 +757,7 @@ const char* SpoilerLog_Write(int language) { wothLocations.clear(); WriteHints(language); - //WriteShuffledEntrances(spoilerLog); + WriteShuffledEntrances(); WriteAllLocations(language); if (!std::filesystem::exists(Ship::Window::GetPathRelativeToAppDirectory("Randomizer"))) { diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 42faa3e4f..297ec0995 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -220,6 +220,11 @@ std::unordered_map SpoilerfileSettingNameToEn { "World Settings:Starting Age", RSK_STARTING_AGE }, { "World Settings:Ammo Drops", RSK_ENABLE_BOMBCHU_DROPS }, { "World Settings:Bombchus in Logic", RSK_BOMBCHUS_IN_LOGIC }, + { "World Settings:Shuffle Entrances", RSK_SHUFFLE_ENTRANCES }, + { "World Settings:Dungeon Entrances", RSK_SHUFFLE_DUNGEON_ENTRANCES }, + { "World Settings:Overworld Entrances", RSK_SHUFFLE_OVERWORLD_ENTRANCES }, + { "World Settings:Interior Entrances", RSK_SHUFFLE_INTERIOR_ENTRANCES }, + { "World Settings:Grottos Entrances", RSK_SHUFFLE_GROTTO_ENTRANCES }, { "Misc Settings:Gossip Stone Hints", RSK_GOSSIP_STONE_HINTS }, { "Misc Settings:Hint Clarity", RSK_HINT_CLARITY }, { "Misc Settings:Hint Distribution", RSK_HINT_DISTRIBUTION }, @@ -543,6 +548,12 @@ void Randomizer::LoadRequiredTrials(const char* spoilerFileName) { } } +void Randomizer::LoadEntranceOverrides(const char* spoilerFileName, bool silent){ + if (strcmp(spoilerFileName, "") != 0) { + ParseEntranceDataFile(spoilerFileName, silent); + } +} + void Randomizer::LoadMasterQuestDungeons(const char* spoilerFileName) { if (strcmp(spoilerFileName, "") != 0) { ParseMasterQuestDungeonsFile(spoilerFileName); @@ -704,6 +715,9 @@ void Randomizer::ParseRandomizerSettingsFile(const char* spoilerFileName) { case RSK_BLUE_FIRE_ARROWS: case RSK_SUNLIGHT_ARROWS: case RSK_BOMBCHUS_IN_LOGIC: + case RSK_SHUFFLE_ENTRANCES: + case RSK_SHUFFLE_OVERWORLD_ENTRANCES: + case RSK_SHUFFLE_GROTTO_ENTRANCES: if(it.value() == "Off") { gSaveContext.randoSettings[index].value = 0; } else if(it.value() == "On") { @@ -900,6 +914,24 @@ void Randomizer::ParseRandomizerSettingsFile(const char* spoilerFileName) { numericValueString = it.value(); gSaveContext.randoSettings[index].value = std::stoi(numericValueString); break; + case RSK_SHUFFLE_DUNGEON_ENTRANCES: + if (it.value() == "Off") { + gSaveContext.randoSettings[index].value = 0; + } else if (it.value() == "On") { + gSaveContext.randoSettings[index].value = 1; + } else if (it.value() == "On + Ganon") { + gSaveContext.randoSettings[index].value = 2; + } + break; + case RSK_SHUFFLE_INTERIOR_ENTRANCES: + if (it.value() == "Off") { + gSaveContext.randoSettings[index].value = 0; + } else if (it.value() == "Simple") { + gSaveContext.randoSettings[index].value = 1; + } else if (it.value() == "All") { + gSaveContext.randoSettings[index].value = 2; + } + break; } } } @@ -1165,6 +1197,49 @@ void Randomizer::ParseItemLocationsFile(const char* spoilerFileName, bool silent } } +void Randomizer::ParseEntranceDataFile(const char* spoilerFileName, bool silent) { + std::ifstream spoilerFileStream(sanitize(spoilerFileName)); + if (!spoilerFileStream) { + return; + } + + // set all the entrances to be 0 to indicate an unshuffled entrance + for (auto &entranceOveride : gSaveContext.entranceOverrides) { + entranceOveride.index = 0; + entranceOveride.destination = 0; + entranceOveride.blueWarp = 0; + entranceOveride.override = 0; + entranceOveride.overrideDestination = 0; + } + + try { + json spoilerFileJson; + spoilerFileStream >> spoilerFileJson; + json EntrancesJson = spoilerFileJson["entrances"]; + + size_t i = 0; + for (auto it = EntrancesJson.begin(); it != EntrancesJson.end(); ++it, i++) { + json entranceJson = *it; + + for (auto entranceIt = entranceJson.begin(); entranceIt != entranceJson.end(); ++entranceIt) { + if (entranceIt.key() == "index") { + gSaveContext.entranceOverrides[i].index = entranceIt.value(); + } else if (entranceIt.key() == "destination") { + gSaveContext.entranceOverrides[i].destination = entranceIt.value(); + } else if (entranceIt.key() == "blueWarp") { + gSaveContext.entranceOverrides[i].blueWarp = entranceIt.value(); + } else if (entranceIt.key() == "override") { + gSaveContext.entranceOverrides[i].override = entranceIt.value(); + } else if (entranceIt.key() == "overrideDestination") { + gSaveContext.entranceOverrides[i].overrideDestination = entranceIt.value(); + } + } + } + } catch (const std::exception& e) { + return; + } +} + bool Randomizer::IsTrialRequired(RandomizerInf trial) { return this->trialsRequired.contains(trial); } @@ -2553,6 +2628,7 @@ void GenerateRandomizerImgui() { // Link's Pocket has to have a dungeon reward if the other rewards are shuffled to end of dungeon. cvarSettings[RSK_LINKS_POCKET] = CVar_GetS32("gRandomizeShuffleDungeonReward", 0) != 0 ? CVar_GetS32("gRandomizeLinksPocket", 0) : 0; + if (OTRGlobals::Instance->HasMasterQuest() && OTRGlobals::Instance->HasOriginal()) { // If both OTRs are loaded. cvarSettings[RSK_RANDOM_MQ_DUNGEONS] = CVar_GetS32("gRandomizeMqDungeons", 0); @@ -2567,23 +2643,34 @@ void GenerateRandomizerImgui() { cvarSettings[RSK_MQ_DUNGEON_COUNT] = 0; } - // todo: this efficently when we build out cvar array support - std::set excludedLocations; - std::stringstream excludedLocationStringStream(CVar_GetString("gRandomizeExcludedLocations", "")); - std::string excludedLocationString; - while (getline(excludedLocationStringStream, excludedLocationString, ',')) { - excludedLocations.insert((RandomizerCheck)std::stoi(excludedLocationString)); - } + // Enable if any of the entrance rando options are enabled. + cvarSettings[RSK_SHUFFLE_ENTRANCES] = CVar_GetS32("gRandomizeShuffleDungeonsEntrances", 0) || + CVar_GetS32("gRandomizeShuffleOverworldEntrances", 0) || + CVar_GetS32("gRandomizeShuffleInteriorsEntrances", 0) || + CVar_GetS32("gRandomizeShuffleGrottosEntrances", 0); - RandoMain::GenerateRando(cvarSettings, excludedLocations); + cvarSettings[RSK_SHUFFLE_DUNGEON_ENTRANCES] = CVar_GetS32("gRandomizeShuffleDungeonsEntrances", 0); + cvarSettings[RSK_SHUFFLE_OVERWORLD_ENTRANCES] = CVar_GetS32("gRandomizeShuffleOverworldEntrances", 0); + cvarSettings[RSK_SHUFFLE_INTERIOR_ENTRANCES] = CVar_GetS32("gRandomizeShuffleInteriorsEntrances", 0); + cvarSettings[RSK_SHUFFLE_GROTTO_ENTRANCES] = CVar_GetS32("gRandomizeShuffleGrottosEntrances", 0); - CVar_SetS32("gRandoGenerating", 0); - CVar_Save(); - CVar_Load(); - - generated = 1; + // todo: this efficently when we build out cvar array support + std::set excludedLocations; + std::stringstream excludedLocationStringStream(CVar_GetString("gRandomizeExcludedLocations", "")); + std::string excludedLocationString; + while (getline(excludedLocationStringStream, excludedLocationString, ',')) { + excludedLocations.insert((RandomizerCheck)std::stoi(excludedLocationString)); } + RandoMain::GenerateRando(cvarSettings, excludedLocations); + + CVar_SetS32("gRandoGenerating", 0); + CVar_Save(); + CVar_Load(); + + generated = 1; +} + void DrawRandoEditor(bool& open) { if (generated) { generated = 0; @@ -2612,11 +2699,8 @@ void DrawRandoEditor(bool& open) { // World Settings const char* randoStartingAge[3] = { "Child", "Adult", "Random" }; - const char* randoShuffleEntrances[2] = { "Off", "On" }; - const char* randoShuffleDungeonsEntrances[2] = { "Off", "On" }; - const char* randoShuffleOverworldEntrances[2] = { "Off", "On" }; - const char* randoShuffleInteriorsEntrances[2] = { "Off", "On" }; - const char* randoShuffleGrottosEntrances[2] = { "Off", "On" }; + const char* randoShuffleDungeonsEntrances[3] = { "Off", "On", "On + Ganon" }; + const char* randoShuffleInteriorsEntrances[3] = { "Off", "Simple", "All" }; const char* randoBombchusInLogic[2] = { "Off", "On" }; const char* randoAmmoDrops[3] = { "On + Bombchu", "Off", "On" }; const char* randoHeartDropsAndRefills[4] = { "On", "No Drop", "No Refill", "Off" }; @@ -2934,10 +3018,57 @@ void DrawRandoEditor(bool& open) { ImGui::BeginChild("ChildShuffleEntrances", ImVec2(0, -8)); ImGui::PushItemWidth(-FLT_MIN); - ImGui::Text("Coming soon"); + // Shuffle Dungeon Entrances + ImGui::Text("Shuffle Dungeon Entrances"); + UIWidgets::InsertHelpHoverText( + "Shuffle the pool of dungeon entrances, including Bottom of the Well, Ice Cavern and Gerudo Training Grounds.\n" + "\n" + "Shuffling Ganon's Castle can be enabled separately.\n" + "\n" + "Additionally, the entrances of Deku Tree, Fire Temple, Bottom of the Well and Gerudo Training Ground are opened for both child and adult.\n" + "\n" + "- Deku Tree will be open for adult after Mido has seen child Link with a sword and shield.\n" + "- Bottom of the Well will be open for adult after playing Song of Storms to the Windmill guy as child.\n" + "- Gerudo Training Ground will be open for child after adult has paid to open the gate once." + ); + UIWidgets::EnhancementCombobox("gRandomizeShuffleDungeonsEntrances", randoShuffleDungeonsEntrances, 3, 0); UIWidgets::PaddedSeparator(); + // Shuffle Overworld Entrances + UIWidgets::EnhancementCheckbox("Shuffle Overworld Entrances", "gRandomizeShuffleOverworldEntrances"); + UIWidgets::InsertHelpHoverText( + "Shuffle the pool of Overworld entrances, which corresponds to almost all loading zones between overworld areas.\n" + "\n" + "Some entrances are unshuffled to avoid issues:\n" + "- Hyrule Castle Courtyard and Garden entrance\n" + "- Both Market Back Alley entrances\n" + "- Gerudo Valley to Lake Hylia (unless entrances are decoupled)" + ); + + UIWidgets::PaddedSeparator(); + + // Shuffle Interior Entrances + ImGui::Text("Shuffle Interior Entrances"); + UIWidgets::InsertHelpHoverText( + "Shuffle the pool of interior entrances which contains most Houses and all Great Fairies.\n" + "\n" + "All - An extended version of 'Simple' with some extra places:\n" + "- Windmill\n" + "- Link's House\n" + "- Temple of Time\n" + "- Kakariko Potion Shop" + ); + UIWidgets::EnhancementCombobox("gRandomizeShuffleInteriorsEntrances", randoShuffleInteriorsEntrances, 3, 0); + + UIWidgets::PaddedSeparator(); + + // Shuffle Grotto Entrances + UIWidgets::EnhancementCheckbox("Shuffle Grotto Entrances", "gRandomizeShuffleGrottosEntrances"); + UIWidgets::InsertHelpHoverText( + "Shuffle the pool of grotto entrances, including all graves, small Fairy fountains and the Deku Theatre." + ); + ImGui::PopItemWidth(); ImGui::EndChild(); ImGui::EndTable(); diff --git a/soh/soh/Enhancements/randomizer/randomizer.h b/soh/soh/Enhancements/randomizer/randomizer.h index a4e3873f8..d05b24b4d 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.h +++ b/soh/soh/Enhancements/randomizer/randomizer.h @@ -28,6 +28,7 @@ class Randomizer { void ParseRequiredTrialsFile(const char* spoilerFileName); void ParseMasterQuestDungeonsFile(const char* spoilerFileName); void ParseItemLocationsFile(const char* spoilerFileName, bool silent); + void ParseEntranceDataFile(const char* spoilerFileName, bool silent); bool IsItemVanilla(RandomizerGet randoGet); GetItemEntry GetItemEntryFromRGData(RandomizerGetData rgData, GetItemID ogItemId, bool checkObtainability = true); @@ -58,6 +59,7 @@ class Randomizer { void LoadRequiredTrials(const char* spoilerFileName); void LoadMasterQuestDungeons(const char* spoilerFileName); bool IsTrialRequired(RandomizerInf trial); + void LoadEntranceOverrides(const char* spoilerFileName, bool silent); u8 GetRandoSettingValue(RandomizerSettingKey randoSettingKey); RandomizerCheck GetCheckFromActor(s16 actorId, s16 sceneNum, s16 actorParams); RandomizerCheck GetCheckFromRandomizerInf(RandomizerInf randomizerInf); diff --git a/soh/soh/Enhancements/randomizer/randomizerTypes.h b/soh/soh/Enhancements/randomizer/randomizerTypes.h index 4ffa6313c..66e12f592 100644 --- a/soh/soh/Enhancements/randomizer/randomizerTypes.h +++ b/soh/soh/Enhancements/randomizer/randomizerTypes.h @@ -1052,6 +1052,20 @@ typedef enum { RSK_KEYRINGS_BOTTOM_OF_THE_WELL, RSK_KEYRINGS_GTG, RSK_KEYRINGS_GANONS_CASTLE, + RSK_SHUFFLE_ENTRANCES, + RSK_SHUFFLE_DUNGEON_ENTRANCES, + RSK_SHUFFLE_OVERWORLD_ENTRANCES, + RSK_SHUFFLE_INTERIOR_ENTRANCES, + RSK_SHUFFLE_GROTTO_ENTRANCES, + RSK_SHUFFLE_OWL_DROPS, + RSK_SHUFFLE_WARP_SONGS, + RSK_SHUFFLE_OVERWORLD_SPAWNS, + RSK_MIXED_ENTRANCE_POOLS, + RSK_MIX_DUNGEON_ENTRANCES, + RSK_MIX_OVERWORLD_ENTRANCES, + RSK_MIX_INTERIOR_ENTRANCES, + RSK_MIX_GROTTO_ENTRANCES, + RSK_DECOUPLED_ENTRANCES, RSK_MAX } RandomizerSettingKey; diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.c b/soh/soh/Enhancements/randomizer/randomizer_entrance.c new file mode 100644 index 000000000..c00f8d42c --- /dev/null +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.c @@ -0,0 +1,526 @@ +/* + * Much of the code here was borrowed from https://github.com/gamestabled/OoT3D_Randomizer/blob/main/code/src/entrance.c + * It's been adapted for SoH to use our gPlayState vs their gGlobalContext with slightly different named properties, and our enums for some scenes/entrances. + * + * Unlike 3DS rando, we need to be able to support the user loading up vanilla and rando saves, so the logic around + * modifying the entrance table requires that we save the original table and reset whenever loading a vanilla save. + * A modified dynamicExitList is manually included since we can't read it from addressing like 3ds rando. + */ + +#include "randomizer_entrance.h" +#include "randomizer_grotto.h" +#include + +#include "global.h" + +extern PlayState* gPlayState; + +//Overwrite the dynamic exit for the OGC Fairy Fountain to be 0x3E8 instead +//of 0x340 (0x340 will stay as the exit for the HC Fairy Fountain -> Castle Grounds) +s16 dynamicExitList[] = { 0x045B, 0x0482, 0x03E8, 0x044B, 0x02A2, 0x0201, 0x03B8, 0x04EE, 0x03C0, 0x0463, 0x01CD, 0x0394, 0x0340, 0x057C }; +// OGC Fairy HC Fairy + +// Warp Song indices array : 0x53C33C = { 0x0600, 0x04F6, 0x0604, 0x01F1, 0x0568, 0x05F4 } + +// Owl Flights : 0x492064 and 0x492080 + +static s16 entranceOverrideTable[ENTRANCE_TABLE_SIZE] = {0}; + +EntranceInfo originalEntranceTable[1556] = {0}; + +//These variables store the new entrance indices for dungeons so that +//savewarping and game overs respawn players at the proper entrance. +//By default, these will be their vanilla values. +static s16 newDekuTreeEntrance = DEKU_TREE_ENTRANCE; +static s16 newDodongosCavernEntrance = DODONGOS_CAVERN_ENTRANCE; +static s16 newJabuJabusBellyEntrance = JABU_JABUS_BELLY_ENTRANCE; +static s16 newForestTempleEntrance = FOREST_TEMPLE_ENTRANCE; +static s16 newFireTempleEntrance = FIRE_TEMPLE_ENTRANCE; +static s16 newWaterTempleEntrance = WATER_TEMPLE_ENTRANCE; +static s16 newSpiritTempleEntrance = SPIRIT_TEMPLE_ENTRANCE; +static s16 newShadowTempleEntrance = SHADOW_TEMPLE_ENTRANCE; +static s16 newBottomOfTheWellEntrance = BOTTOM_OF_THE_WELL_ENTRANCE; +static s16 newGerudoTrainingGroundsEntrance = GERUDO_TRAINING_GROUNDS_ENTRANCE; +static s16 newIceCavernEntrance = ICE_CAVERN_ENTRANCE; + +static s8 hasCopiedEntranceTable = 0; +static s8 hasModifiedEntranceTable = 0; + +u8 Entrance_EntranceIsNull(EntranceOverride* entranceOverride) { + return entranceOverride->index == 0 && entranceOverride->destination == 0 && entranceOverride->blueWarp == 0 + && entranceOverride->override == 0 && entranceOverride->overrideDestination == 0; +} + +static void Entrance_SeparateOGCFairyFountainExit(void) { + //Overwrite unused entrance 0x03E8 with values from 0x0340 to use it as the + //exit from OGC Great Fairy Fountain -> Castle Grounds + for (size_t i = 0; i < 4; ++i) { + gEntranceTable[0x3E8 + i] = gEntranceTable[0x340 + i]; + } +} + +void Entrance_CopyOriginalEntranceTable(void) { + if (!hasCopiedEntranceTable) { + memcpy(originalEntranceTable, gEntranceTable, sizeof(EntranceInfo) * 1556); + hasCopiedEntranceTable = 1; + } +} + +void Entrance_ResetEntranceTable(void) { + if (hasCopiedEntranceTable && hasModifiedEntranceTable) { + memcpy(gEntranceTable, originalEntranceTable, sizeof(EntranceInfo) * 1556); + hasModifiedEntranceTable = 0; + } +} + +void Entrance_Init(void) { + s32 index; + + Entrance_CopyOriginalEntranceTable(); + + // Skip Child Stealth if given by settings + if (Randomizer_GetSettingValue(RSK_SKIP_CHILD_STEALTH)) { + gEntranceTable[0x07A].scene = 0x4A; + gEntranceTable[0x07A].spawn = 0x00; + gEntranceTable[0x07A].field = 0x0183; + } + + // Delete the title card and add a fade in for Hyrule Field from Ocarina of Time cutscene + for (index = 0x50F; index < 0x513; ++index) { + gEntranceTable[index].field = 0x010B; + } + + Entrance_SeparateOGCFairyFountainExit(); + + // Initialize the entrance override table with each index leading to itself. An + // index referring to itself means that the entrance is not currently shuffled. + for (s16 i = 0; i < ENTRANCE_TABLE_SIZE; i++) { + entranceOverrideTable[i] = i; + } + + // Initialize the grotto exit and load lists + Grotto_InitExitAndLoadLists(); + + // Then overwrite the indices which are shuffled + for (size_t i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) { + + if (Entrance_EntranceIsNull(&gSaveContext.entranceOverrides[i])) { + break; + } + + s16 originalIndex = gSaveContext.entranceOverrides[i].index; + s16 blueWarpIndex = gSaveContext.entranceOverrides[i].blueWarp; + s16 overrideIndex = gSaveContext.entranceOverrides[i].override; + + //Overwrite grotto related indices + if (originalIndex >= 0x0800) { + Grotto_SetExitOverride(originalIndex, overrideIndex); + continue; + } + + if (originalIndex >= 0x0700 && originalIndex < 0x0800) { + Grotto_SetLoadOverride(originalIndex, overrideIndex); + continue; + } + + // Overwrite the indices which we want to shuffle, leaving the rest as they are + entranceOverrideTable[originalIndex] = overrideIndex; + + if (blueWarpIndex != 0) { + entranceOverrideTable[blueWarpIndex] = overrideIndex; + } + + //Override both land and water entrances for Hyrule Field -> ZR Front and vice versa + if (originalIndex == 0x00EA) { //Hyrule Field -> ZR Front land entrance + entranceOverrideTable[0x01D9] = overrideIndex; + } else if (originalIndex == 0x0181) { //ZR Front -> Hyrule Field land entrance + entranceOverrideTable[0x0311] = overrideIndex; + } + } + + // Stop playing background music during shuffled entrance transitions + // so that we don't get duplicated or overlapping music tracks + if (Randomizer_GetSettingValue(RSK_SHUFFLE_OVERWORLD_ENTRANCES)) { + + s16 indicesToSilenceBackgroundMusic[2] = { + // The lost woods music playing near the GC Woods Warp keeps playing + // in the next area if the bvackground music is allowed to keep playing + entranceOverrideTable[0x04D6], // Goron City -> Lost Woods override + + // If Malon is singing at night, then her singing will be transferred + // to the next area if it allows the background music to keep playing + entranceOverrideTable[0x025A], // Castle Grounds -> Market override + }; + + for (size_t j = 0; j < sizeof(indicesToSilenceBackgroundMusic) / sizeof(s16); j++) { + + s16 override = indicesToSilenceBackgroundMusic[j]; + for (s16 i = 0; i < 4; i++) { + // Zero out the bit in the field which tells the game to keep playing + // background music for all four scene setups at each index + gEntranceTable[override + i].field &= ~0x8000; + } + } + } + + hasModifiedEntranceTable = 1; +} + +s16 Entrance_GetOverride(s16 index) { + + // The game sometimes uses special indices from 0x7FF9 -> 0x7FFF for exiting + // grottos and fairy fountains. These aren't handled here since the game + // naturally handles them later. + if (index >= ENTRANCE_TABLE_SIZE) { + return index; + } + + return entranceOverrideTable[index]; +} + +s16 Entrance_OverrideNextIndex(s16 nextEntranceIndex) { + // When entering Spirit Temple, clear temp flags so they don't carry over to the randomized dungeon + if (nextEntranceIndex == 0x0082 && Entrance_GetOverride(nextEntranceIndex) != nextEntranceIndex && + gPlayState != NULL) { + gPlayState->actorCtx.flags.tempSwch = 0; + gPlayState->actorCtx.flags.tempCollect = 0; + } + + // Exiting through the crawl space from Hyrule Castle courtyard is the same exit as leaving Ganon's castle + // If we came from the Castle courtyard, then don't override the entrance to keep Link in Hyrule Castle area + if (gPlayState->sceneNum == 69 && nextEntranceIndex == 0x023D) { + return nextEntranceIndex; + } + + return Grotto_OverrideSpecialEntrance(Entrance_GetOverride(nextEntranceIndex)); +} + +s16 Entrance_OverrideDynamicExit(s16 dynamicExitIndex) { + return Grotto_OverrideSpecialEntrance(Entrance_GetOverride(dynamicExitList[dynamicExitIndex])); +} + +u32 Entrance_SceneAndSpawnAre(u8 scene, u8 spawn) { + EntranceInfo currentEntrance = gEntranceTable[gSaveContext.entranceIndex]; + return currentEntrance.scene == scene && currentEntrance.spawn == spawn; +} + +//Properly respawn the player after a game over, accounding for dungeon entrance +//randomizer. It's easier to rewrite this entirely compared to performing an ASM +//dance for just the boss rooms. Entrance Indexes can be found here: +//https://wiki.cloudmodding.com/oot/Entrance_Table_(Data) +void Entrance_SetGameOverEntrance(void) { + + //Set the current entrance depending on which entrance the player last came through + switch (gSaveContext.entranceIndex) { + case 0x040F : //Deku Tree Boss Room + gSaveContext.entranceIndex = newDekuTreeEntrance; + return; + case 0x040B : //Dodongos Cavern Boss Room + gSaveContext.entranceIndex = newDodongosCavernEntrance; + return; + case 0x0301 : //Jabu Jabus Belly Boss Room + gSaveContext.entranceIndex = newJabuJabusBellyEntrance; + return; + case 0x000C : //Forest Temple Boss Room + gSaveContext.entranceIndex = newForestTempleEntrance; + return; + case 0x0305 : //Fire Temple Boss Room + gSaveContext.entranceIndex = newFireTempleEntrance; + return; + case 0x0417 : //Water Temple Boss Room + gSaveContext.entranceIndex = newWaterTempleEntrance; + return; + case 0x008D : //Spirit Temple Boss Room + gSaveContext.entranceIndex = newSpiritTempleEntrance; + return; + case 0x0413 : //Shadow Temple Boss Room + gSaveContext.entranceIndex = newShadowTempleEntrance; + return; + case 0x041F : //Ganondorf Boss Room + gSaveContext.entranceIndex = 0x041B; // Inside Ganon's Castle -> Ganon's Tower Climb + return; + } +} + +//Properly savewarp the player accounting for dungeon entrance randomizer. +//It's easier to rewrite this entirely compared to performing an ASM +//dance for just the boss rooms. +//https://wiki.cloudmodding.com/oot/Entrance_Table_(Data) +void Entrance_SetSavewarpEntrance(void) { + + s16 scene = gSaveContext.savedSceneNum; + + if (scene == SCENE_YDAN || scene == SCENE_YDAN_BOSS) { + gSaveContext.entranceIndex = newDekuTreeEntrance; + } else if (scene == SCENE_DDAN || scene == SCENE_DDAN_BOSS) { + gSaveContext.entranceIndex = newDodongosCavernEntrance; + } else if (scene == SCENE_BDAN || scene == SCENE_BDAN_BOSS) { + gSaveContext.entranceIndex = newJabuJabusBellyEntrance; + } else if (scene == SCENE_BMORI1 || scene == SCENE_MORIBOSSROOM) { //Forest Temple Boss Room + gSaveContext.entranceIndex = newForestTempleEntrance; + } else if (scene == SCENE_HIDAN || scene == SCENE_FIRE_BS) { //Fire Temple Boss Room + gSaveContext.entranceIndex = newFireTempleEntrance; + } else if (scene == SCENE_MIZUSIN || scene == SCENE_MIZUSIN_BS) { //Water Temple Boss Room + gSaveContext.entranceIndex = newWaterTempleEntrance; + } else if (scene == SCENE_JYASINZOU || scene == SCENE_JYASINBOSS) { //Spirit Temple Boss Room + gSaveContext.entranceIndex = newSpiritTempleEntrance; + } else if (scene == SCENE_HAKADAN || scene == SCENE_HAKADAN_BS) { //Shadow Temple Boss Room + gSaveContext.entranceIndex = newShadowTempleEntrance; + } else if (scene == SCENE_HAKADANCH) { // BOTW + gSaveContext.entranceIndex = newBottomOfTheWellEntrance; + } else if (scene == SCENE_MEN) { // GTG + gSaveContext.entranceIndex = newGerudoTrainingGroundsEntrance; + } else if (scene == SCENE_ICE_DOUKUTO) { // Ice cavern + gSaveContext.entranceIndex = newIceCavernEntrance; + } else if (scene == SCENE_GANONTIKA) { + gSaveContext.entranceIndex = GANONS_CASTLE_ENTRANCE; + } else if (scene == SCENE_GANON || scene == SCENE_GANONTIKA_SONOGO || scene == SCENE_GANON_SONOGO || scene == SCENE_GANON_DEMO || scene == SCENE_GANON_FINAL) { + gSaveContext.entranceIndex = 0x041B; // Inside Ganon's Castle -> Ganon's Tower Climb + } else if (scene == SCENE_GERUDOWAY) { // Theives hideout + gSaveContext.entranceIndex = 0x0486; // Gerudo Fortress -> Thieve's Hideout spawn 0 + } else if (scene == SCENE_LINK_HOME) { + gSaveContext.entranceIndex = LINK_HOUSE_SAVEWARP_ENTRANCE; + } else if (LINK_IS_CHILD) { + gSaveContext.entranceIndex = Entrance_GetOverride(LINK_HOUSE_SAVEWARP_ENTRANCE); + } else { + gSaveContext.entranceIndex = Entrance_GetOverride(0x05F4); // Temple of Time Adult Spawn + } +} + +void Entrance_OverrideBlueWarp(void) { + switch (gPlayState->sceneNum) { + case SCENE_YDAN_BOSS: // Ghoma boss room + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(0x0457); + return; + case SCENE_DDAN_BOSS: // King Dodongo boss room + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(0x047A); + return; + case SCENE_BDAN_BOSS: // Barinade boss room + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(0x010E); + return; + case SCENE_MORIBOSSROOM: // Phantom Ganon boss room + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(0x0608); + return; + case SCENE_FIRE_BS: // Volvagia boss room + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(0x0564); + return; + case SCENE_MIZUSIN_BS: // Morpha boss room + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(0x060C); + return; + case SCENE_JYASINBOSS: // Bongo-Bongo boss room + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(0x0610); + return; + case SCENE_HAKADAN_BS: // Twinrova boss room + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(0x0580); + return; + } +} + +void Entrance_OverrideCutsceneEntrance(u16 cutsceneCmd) { + switch (cutsceneCmd) { + case 24: // Dropping a fish for Jabu Jabu + gPlayState->nextEntranceIndex = Entrance_OverrideNextIndex(newJabuJabusBellyEntrance); + gPlayState->sceneLoadFlag = 0x14; + gPlayState->fadeTransition = 2; + break; + } +} + +void Entrance_EnableFW(void) { + Player* player = GET_PLAYER(gPlayState); + // Leave restriction in Tower Collapse Interior, Castle Collapse, Treasure Box Shop, Tower Collapse Exterior, + // Grottos area, Fishing Pond, Ganon Battle and for states that disable buttons. + if (!false /* farores wind anywhere */ || + gPlayState->sceneNum == 14 || gPlayState->sceneNum == 15 || (gPlayState->sceneNum == 16 && !false /* shuffled chest mini game */) || + gPlayState->sceneNum == 26 || gPlayState->sceneNum == 62 || gPlayState->sceneNum == 73 || + gPlayState->sceneNum == 79 || + gSaveContext.eventInf[0] & 0x1 || // Ingo's Minigame state + player->stateFlags1 & 0x08A02000 || // Swimming, riding horse, Down A, hanging from a ledge + player->stateFlags2 & 0x00040000 // Blank A + // Shielding, spinning and getting skull tokens still disable buttons automatically + ) { + return; + } + + for (int i = 1; i < 5; i++) { + if (gSaveContext.equips.buttonItems[i] == 13) { + gSaveContext.buttonStatus[i] = BTN_ENABLED; + } + } +} + +// Check if Link should still be riding epona after changing entrances +void Entrance_HandleEponaState(void) { + s32 entrance = gPlayState->nextEntranceIndex; + Player* player = GET_PLAYER(gPlayState); + //If Link is riding Epona but he's about to go through an entrance where she can't spawn, + //unset the Epona flag to avoid Master glitch, and restore temp B. + if (Randomizer_GetSettingValue(RSK_SHUFFLE_OVERWORLD_ENTRANCES) && (player->stateFlags1 & PLAYER_STATE1_23)) { + + static const s16 validEponaEntrances[] = { + 0x0102, // Hyrule Field -> Lake Hylia + 0x0189, // Lake Hylia -> Hyrule Field + 0x0309, // LH Fishing Hole -> LH Fishing Island + 0x03CC, // LH Lab -> Lake Hylia + 0x0117, // Hyrule Field -> Gerudo Valley + 0x018D, // Gerudo Valley -> Hyrule Field + 0x0157, // Hyrule Field -> Lon Lon Ranch + 0x01F9, // Lon Lon Ranch -> Hyrule Field + 0x01FD, // Market Entrance -> Hyrule Field + 0x00EA, // ZR Front -> Hyrule Field + 0x0181, // LW Bridge -> Hyrule Field + 0x0129, // GV Fortress Side -> Gerudo Fortress + 0x022D, // Gerudo Fortress -> GV Fortress Side + 0x03D0, // GV Carpenter Tent -> GV Fortress Side + 0x0496, // Gerudo Fortress -> Thieves Hideout + 0x042F, // LLR Stables -> Lon Lon Ranch + 0x05D4, // LLR Tower -> Lon Lon Ranch + 0x0378, // LLR Talons House -> Lon Lon Ranch + 0x028A, // LLR Southern Fence Jump + 0x028E, // LLR Western Fence Jump + 0x0292, // LLR Eastern Fence Jump + 0x0476, // LLR Front Gate Jump + // The following indices currently aren't randomized, but we'll list + // them in case they ever are. They're all Theives Hideout -> Gerudo Fortress + 0x231, + 0x235, + 0x239, + 0x2BA, + }; + for (size_t i = 0; i < ARRAY_COUNT(validEponaEntrances); i++) { + // If the entrance is equal to any of the valid ones, return and + // don't change anything + if (entrance == validEponaEntrances[i]) { + return; + } + } + + // Update Link's status to no longer be riding epona so that the next time link + // enters an epona supported area, he isn't automatically placed on epona + player->stateFlags1 &= ~PLAYER_STATE1_23; + player->actor.parent = NULL; + AREG(6) = 0; + gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; //"temp B" + } +} + +// Certain weather states don't clear normally in entrance rando, so we need to reset and re-apply +// the correct weather state based on the current entrance and scene +void Entrance_OverrideWeatherState() { + gWeatherMode = 0; + gPlayState->envCtx.gloomySkyMode = 0; + + // Weather only applyies to adult link + if (LINK_IS_CHILD || gSaveContext.sceneSetupIndex >= 4) { + return; + } + + // Hyrule Market + if (gSaveContext.entranceIndex == 0x01FD) { // Hyrule Field by Market Entrance + gWeatherMode = 1; + return; + } + // Lon Lon Ranch (No Epona) + if (!Flags_GetEventChkInf(0x18)){ // if you don't have Epona + switch (gSaveContext.entranceIndex) { + case 0x0157: // Lon Lon Ranch from HF + case 0x01F9: // Hyrule Field from LLR + gWeatherMode = 2; + return; + } + } + // Water Temple + if (!(gSaveContext.eventChkInf[4] & 0x0400)) { // have not beaten Water Temple + switch (gSaveContext.entranceIndex) { + case 0x019D: // Zora River from behind waterfall + case 0x01DD: // Zora River from LW water shortcut + case 0x04DA: // Lost Woods water shortcut from ZR + gWeatherMode = 3; + return; + } + switch (gPlayState->sceneNum) { + case 88: // Zora's Domain + case 89: // Zora's Fountain + gWeatherMode = 3; + return; + } + } + // Kakariko Thunderstorm + if (((gSaveContext.inventory.questItems & 0x7) == 0x7) && // Have forest, fire, and water medallion + !(gSaveContext.sceneFlags[24].clear & 0x02)) { // have not beaten Bongo Bongo + switch (gPlayState->sceneNum) { + case 82: // Kakariko + case 83: // Graveyard + gPlayState->envCtx.gloomySkyMode = 2; + switch (gSaveContext.entranceIndex) { + case 0x00DB: // Kakariko from HF + case 0x0191: // Kakariko from Death Mountain Trail + case 0x0205: // Graveyard from Shadow Temple + break; + default: + gWeatherMode = 5; + return; + } + } + } + // Death Mountain Cloudy + if (!(gSaveContext.eventChkInf[4] & 0x0200)) { // have not beaten Fire Temple + if (gPlayState->nextEntranceIndex == 0x04D6) { // Lost Woods Goron City Shortcut + gWeatherMode = 2; + return; + } + switch (gPlayState->sceneNum) { + case 82: // Kakariko + case 83: // Graveyard + case 96: // Death Mountain Trail + case 97: // Death Mountain Crater + if (!gPlayState->envCtx.gloomySkyMode) { + gPlayState->envCtx.gloomySkyMode = 1; + } + switch (gSaveContext.entranceIndex) { + case 0x00DB: // Kakariko from HF + case 0x0195: // Kakariko from Graveyard + break; + default: + gWeatherMode = 2; + return; + } + } + } +} + +// Rectify the "Getting Caught By Gerudo" entrance index if necessary, based on the age and current scene +// In ER, Adult should be placed at the fortress entrance when getting caught in the fortress without a hookshot, instead of being thrown in the valley +// Child should always be thrown in the stream when caught in the valley, and placed at the fortress entrance from valley when caught in the fortress +void Entrance_OverrideGeurdoGuardCapture(void) { + if (LINK_IS_CHILD) { + gPlayState->nextEntranceIndex = 0x1A5; + } + + if ((LINK_IS_CHILD || Randomizer_GetSettingValue(RSK_SHUFFLE_OVERWORLD_ENTRANCES)) && + gPlayState->nextEntranceIndex == 0x1A5) { // Geurdo Valley thrown out + if (gPlayState->sceneNum != 0x5A) { // Geurdo Valley + gPlayState->nextEntranceIndex = 0x129; // Gerudo Fortress + } + } +} + +void Entrance_OverrideSpawnScene(s32 sceneNum, s32 spawn) { + if (Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES) == 2) { // Shuffle Ganon's Castle + // Move Hyrule's Castle Courtyard exit spawn to be before the crates so players don't skip Talon + if (sceneNum == 95 && spawn == 1) { + gPlayState->linkActorEntry->pos.x = 0x033A; + gPlayState->linkActorEntry->pos.y = 0x0623; + gPlayState->linkActorEntry->pos.z = 0xFF22; + } + + // Move Ganon's Castle exit spawn to be on the small ledge near the castle and not over the void + if (sceneNum == 100 && spawn == 1) { + gPlayState->linkActorEntry->pos.x = 0xFEA8; + gPlayState->linkActorEntry->pos.y = 0x065C; + gPlayState->linkActorEntry->pos.z = 0x0290; + gPlayState->linkActorEntry->rot.y = 0x0700; + gPlayState->linkActorEntry->params = 0x0DFF; // stationary spawn + } + } +} diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.h b/soh/soh/Enhancements/randomizer/randomizer_entrance.h new file mode 100644 index 000000000..3836770e7 --- /dev/null +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.h @@ -0,0 +1,50 @@ +#ifndef _RANDO_ENTRANCE_H_ +#define _RANDO_ENTRANCE_H_ + +#include + +//Entrance Table Data: https://wiki.cloudmodding.com/oot/Entrance_Table_(Data) +//Accessed June 2021, published content date at the time was 14 March 2020, at 21:47 + +#define ENTRANCE_TABLE_SIZE 0x0614 + +#define DEKU_TREE_ENTRANCE 0x0000 +#define DODONGOS_CAVERN_ENTRANCE 0x0004 +#define JABU_JABUS_BELLY_ENTRANCE 0x0028 +#define FOREST_TEMPLE_ENTRANCE 0x169 +#define FIRE_TEMPLE_ENTRANCE 0x165 +#define WATER_TEMPLE_ENTRANCE 0x0010 +#define SPIRIT_TEMPLE_ENTRANCE 0x0082 +#define SHADOW_TEMPLE_ENTRANCE 0x0037 +#define BOTTOM_OF_THE_WELL_ENTRANCE 0x0098 +#define GERUDO_TRAINING_GROUNDS_ENTRANCE 0x0008 +#define ICE_CAVERN_ENTRANCE 0x0088 +#define GANONS_CASTLE_ENTRANCE 0x0467 +#define LINK_HOUSE_SAVEWARP_ENTRANCE 0x00BB + +#define ENTRANCE_OVERRIDES_MAX_COUNT 256 + +typedef struct { + int16_t index; + int16_t destination; + int16_t blueWarp; + int16_t override; + int16_t overrideDestination; +} EntranceOverride; + +void Entrance_Init(void); +void Entrance_ResetEntranceTable(void); +uint8_t Entrance_EntranceIsNull(EntranceOverride* entranceOverride); +int16_t Entrance_GetOverride(int16_t index); +int16_t Entrance_OverrideNextIndex(int16_t nextEntranceIndex); +int16_t Entrance_OverrideDynamicExit(int16_t dynamicExitIndex); +uint32_t Entrance_SceneAndSpawnAre(uint8_t scene, uint8_t spawn); +void Entrance_SetSavewarpEntrance(void); +void Entrance_OverrideBlueWarp(void); +void Entrance_OverrideCutsceneEntrance(uint16_t cutsceneCmd); +void Entrance_HandleEponaState(void); +void Entrance_OverrideWeatherState(void); +void Entrance_OverrideGeurdoGuardCapture(void); +void Entrance_OverrideSpawnScene(int32_t sceneNum, int32_t spawn); + +#endif //_RANDO_ENTRANCE_H_ diff --git a/soh/soh/Enhancements/randomizer/randomizer_grotto.c b/soh/soh/Enhancements/randomizer/randomizer_grotto.c new file mode 100644 index 000000000..f45384211 --- /dev/null +++ b/soh/soh/Enhancements/randomizer/randomizer_grotto.c @@ -0,0 +1,261 @@ +/* + * Much of the code here was borrowed from https://github.com/gamestabled/OoT3D_Randomizer/blob/main/code/src/grotto.c + * It's been adapted for SoH to use our gPlayState vs their gGlobalContext and slightly different named properties. + */ + +#include "randomizer_grotto.h" + +#include "global.h" + +extern PlayState* gPlayState; + +// Information necessary for entering each grotto +static const GrottoLoadInfo grottoLoadTable[NUM_GROTTOS] = { + {.entranceIndex = 0x05BC, .content = 0xFD, .scene = 0x5C}, // Desert Colossus -> Colossus Grotto + {.entranceIndex = 0x05A4, .content = 0xEF, .scene = 0x57}, // Lake Hylia -> LH Grotto + {.entranceIndex = 0x05BC, .content = 0xEB, .scene = 0x54}, // Zora River -> ZR Storms Grotto + {.entranceIndex = 0x036D, .content = 0xE6, .scene = 0x54}, // Zora River -> ZR Fairy Grotto + {.entranceIndex = 0x003F, .content = 0x29, .scene = 0x54}, // Zora River -> ZR Open Grotto + {.entranceIndex = 0x05A4, .content = 0xF9, .scene = 0x61}, // DMC Lower Nearby -> DMC Hammer Grotto + {.entranceIndex = 0x003F, .content = 0x7A, .scene = 0x61}, // DMC Upper Nearby -> DMC Upper Grotto + {.entranceIndex = 0x05A4, .content = 0xFB, .scene = 0x62}, // GC Grotto Platform -> GC Grotto + {.entranceIndex = 0x003F, .content = 0x57, .scene = 0x60}, // Death Mountain -> DMT Storms Grotto + {.entranceIndex = 0x05FC, .content = 0xF8, .scene = 0x60}, // Death Mountain Summit -> DMT Cow Grotto + {.entranceIndex = 0x003F, .content = 0x28, .scene = 0x52}, // Kak Backyard -> Kak Open Grotto + {.entranceIndex = 0x05A0, .content = 0xE7, .scene = 0x52}, // Kakariko Village -> Kak Redead Grotto + {.entranceIndex = 0x05B8, .content = 0xF6, .scene = 0x5F}, // Hyrule Castle Grounds -> HC Storms Grotto + {.entranceIndex = 0x05C0, .content = 0xE1, .scene = 0x51}, // Hyrule Field -> HF Tektite Grotto + {.entranceIndex = 0x0598, .content = 0xE5, .scene = 0x51}, // Hyrule Field -> HF Near Kak Grotto + {.entranceIndex = 0x036D, .content = 0xFF, .scene = 0x51}, // Hyrule Field -> HF Fairy Grotto + {.entranceIndex = 0x003F, .content = 0x00, .scene = 0x51}, // Hyrule Field -> HF Near Market Grotto + {.entranceIndex = 0x05A8, .content = 0xE4, .scene = 0x51}, // Hyrule Field -> HF Cow Grotto + {.entranceIndex = 0x059C, .content = 0xE6, .scene = 0x51}, // Hyrule Field -> HF Inside Fence Grotto + {.entranceIndex = 0x003F, .content = 0x03, .scene = 0x51}, // Hyrule Field -> HF Open Grotto + {.entranceIndex = 0x003F, .content = 0x22, .scene = 0x51}, // Hyrule Field -> HF Southeast Grotto + {.entranceIndex = 0x05A4, .content = 0xFC, .scene = 0x63}, // Lon Lon Ranch -> LLR Grotto + {.entranceIndex = 0x05B4, .content = 0xED, .scene = 0x56}, // SFM Entryway -> SFM Wolfos Grotto + {.entranceIndex = 0x05BC, .content = 0xEE, .scene = 0x56}, // Sacred Forest Meadow -> SFM Storms Grotto + {.entranceIndex = 0x036D, .content = 0xFF, .scene = 0x56}, // Sacred Forest Meadow -> SFM Fairy Grotto + {.entranceIndex = 0x05B0, .content = 0xF5, .scene = 0x5B}, // LW Beyond Mido -> LW Scrubs Grotto + {.entranceIndex = 0x003F, .content = 0x14, .scene = 0x5B}, // Lost Woods -> LW Near Shortcuts Grotto + {.entranceIndex = 0x003F, .content = 0x2C, .scene = 0x55}, // Kokiri Forest -> KF Storms Grotto + {.entranceIndex = 0x036D, .content = 0xFF, .scene = 0x58}, // Zoras Domain -> ZD Storms Grotto + {.entranceIndex = 0x036D, .content = 0xFF, .scene = 0x5D}, // Gerudo Fortress -> GF Storms Grotto + {.entranceIndex = 0x05BC, .content = 0xF0, .scene = 0x5A}, // GV Fortress Side -> GV Storms Grotto + {.entranceIndex = 0x05AC, .content = 0xF2, .scene = 0x5A}, // GV Grotto Ledge -> GV Octorok Grotto + {.entranceIndex = 0x05C4, .content = 0xF3, .scene = 0x5B}, // LW Beyond Mido -> Deku Theater +}; + +// Information necessary for setting up returning from a grotto +static const GrottoReturnInfo grottoReturnTable[NUM_GROTTOS] = { + {.entranceIndex = 0x0123, .room = 0x00, .angle = 0xA71C, .pos = {.x = 62.5078f, .y = -32.0f, .z = -1296.2f}}, // Colossus Grotto -> Desert Colossus + {.entranceIndex = 0x0102, .room = 0x00, .angle = 0x0000, .pos = {.x = -3039.34f, .y = -1033.0f, .z = 6080.74f}}, // LH Grotto -> Lake Hylia + {.entranceIndex = 0x00EA, .room = 0x00, .angle = 0x0000, .pos = {.x = -1630.05f, .y = 100.0f, .z = -132.104f}}, // ZR Storms Grotto -> Zora River + {.entranceIndex = 0x00EA, .room = 0x00, .angle = 0xE000, .pos = {.x = 649.507f, .y = 570.0f, .z = -346.853f}}, // ZR Fairy Grotto -> Zora River + {.entranceIndex = 0x00EA, .room = 0x00, .angle = 0x8000, .pos = {.x = 362.29f, .y = 570.0f, .z = 111.48f}}, // ZR Open Grotto -> Zora River + {.entranceIndex = 0x0246, .room = 0x01, .angle = 0x31C7, .pos = {.x = -1666.73f, .y = 721.0f, .z = -459.21f}}, // DMC Hammer Grotto -> DMC Lower Local + {.entranceIndex = 0x0147, .room = 0x01, .angle = 0x238E, .pos = {.x = 63.723f, .y = 1265.0f, .z = 1791.39f}}, // DMC Upper Grotto -> DMC Upper Local + {.entranceIndex = 0x014D, .room = 0x03, .angle = 0x0000, .pos = {.x = 1104.73f, .y = 580.0f, .z = -1159.95f}}, // GC Grotto -> GC Grotto Platform + {.entranceIndex = 0x01B9, .room = 0x00, .angle = 0x8000, .pos = {.x = -387.584f, .y = 1386.0f, .z = -1213.05f}}, // DMT Storms Grotto -> Death Mountain + {.entranceIndex = 0x01B9, .room = 0x00, .angle = 0x8000, .pos = {.x = -691.022f, .y = 1946.0f, .z = -312.969f}}, // DMT Cow Grotto -> Death Mountain Summit + {.entranceIndex = 0x00DB, .room = 0x00, .angle = 0x0000, .pos = {.x = 855.238f, .y = 80.0f, .z = -234.095f}}, // Kak Open Grotto -> Kak Backyard + {.entranceIndex = 0x00DB, .room = 0x00, .angle = 0x0000, .pos = {.x = -401.873f, .y = 0.0f, .z = 402.792f}}, // Kak Redead Grotto -> Kakariko Village + {.entranceIndex = 0x0138, .room = 0x00, .angle = 0x9555, .pos = {.x = 1009.02f, .y = 1571.0f, .z = 855.532f}}, // HC Storms Grotto -> Castle Grounds + {.entranceIndex = 0x01F9, .room = 0x00, .angle = 0x1555, .pos = {.x = -4949.58f, .y = -300.0f, .z = 2837.59f}}, // HF Tektite Grotto -> Hyrule Field + {.entranceIndex = 0x01F9, .room = 0x00, .angle = 0xC000, .pos = {.x = 2050.6f, .y = 20.0f, .z = -160.397f}}, // HF Near Kak Grotto -> Hyrule Field + {.entranceIndex = 0x01F9, .room = 0x00, .angle = 0x0000, .pos = {.x = -4447.66f, .y = -300.0f, .z = -393.191f}}, // HF Fairy Grotto -> Hyrule Field + {.entranceIndex = 0x01F9, .room = 0x00, .angle = 0xE000, .pos = {.x = -1446.56f, .y = 0.0f, .z = 830.775f}}, // HF Near Market Grotto -> Hyrule Field + {.entranceIndex = 0x01F9, .room = 0x00, .angle = 0x0000, .pos = {.x = -7874.07f, .y = -300.0f, .z = 6921.31f}}, // HF Cow Grotto -> Hyrule Field + {.entranceIndex = 0x01F9, .room = 0x00, .angle = 0xEAAB, .pos = {.x = -4989.13f, .y = -700.0f, .z = 13821.1f}}, // HF Inside Fence Grotto -> Hyrule Field + {.entranceIndex = 0x01F9, .room = 0x00, .angle = 0x8000, .pos = {.x = -4032.61f, .y = -700.0f, .z = 13831.5f}}, // HF Open Grotto -> Hyrule Field + {.entranceIndex = 0x01F9, .room = 0x00, .angle = 0x9555, .pos = {.x = -288.313f, .y = -500.0f, .z = 12320.2f}}, // HF Southeast Grotto -> Hyrule Field + {.entranceIndex = 0x0157, .room = 0x00, .angle = 0xAAAB, .pos = {.x = 1775.92f, .y = 0.0f, .z = 1486.82f}}, // LLR Grotto -> Lon Lon Ranch + {.entranceIndex = 0x00FC, .room = 0x00, .angle = 0x8000, .pos = {.x = -189.861f, .y = 0.0f, .z = 1898.09f}}, // SFM Wolfos Grotto -> SFM Entryway + {.entranceIndex = 0x00FC, .room = 0x00, .angle = 0xAAAB, .pos = {.x = 314.853f, .y = 480.0f, .z = -2300.39f}}, // SFM Storms Grotto -> Sacred Forest Meadow + {.entranceIndex = 0x00FC, .room = 0x00, .angle = 0x0000, .pos = {.x = 55.034f, .y = 0.0f, .z = 250.595f}}, // SFM Fairy Grotto -> Sacred Forest Meadow + {.entranceIndex = 0x01A9, .room = 0x08, .angle = 0x2000, .pos = {.x = 691.994f, .y = 0.0f, .z = -2502.2f}}, // LW Scrubs Grotto -> LW Beyond Mido + {.entranceIndex = 0x011E, .room = 0x02, .angle = 0xE000, .pos = {.x = 905.755f, .y = 0.0f, .z = -901.43f}}, // LW Near Shortcuts Grotto -> Lost Woods + {.entranceIndex = 0x0286, .room = 0x00, .angle = 0x4000, .pos = {.x = -507.065f, .y = 380.0f, .z = -1220.43f}}, // KF Storms Grotto -> Kokiri Forest + {.entranceIndex = 0x0108, .room = 0x01, .angle = 0xD555, .pos = {.x = -855.68f, .y = 14.0f, .z = -474.422f}}, // ZD Storms Grotto -> Zoras Domain + {.entranceIndex = 0x0129, .room = 0x00, .angle = 0x4000, .pos = {.x = 380.521f, .y = 333.0f, .z = -1560.74f}}, // GF Storms Grotto -> Gerudo Fortress + {.entranceIndex = 0x022D, .room = 0x00, .angle = 0x9555, .pos = {.x = -1326.34f, .y = 15.0f, .z = -983.994f}}, // GV Storms Grotto -> GV Fortress Side + {.entranceIndex = 0x0117, .room = 0x00, .angle = 0x8000, .pos = {.x = 291.513f, .y = -555.0f, .z = 1478.39f}}, // GV Octorok Grotto -> GV Grotto Ledge + {.entranceIndex = 0x01A9, .room = 0x06, .angle = 0x4000, .pos = {.x = 109.281f, .y = -20.0f, .z = -1601.42f}}, // Deku Theater -> LW Beyond Mido +}; + +static s16 grottoExitList[NUM_GROTTOS] = {0}; +static s16 grottoLoadList[NUM_GROTTOS] = {0}; +static s8 grottoId = 0xFF; +static s8 lastEntranceType = NOT_GROTTO; + +// Initialize both lists so that each index refers to itself. An index referring +// to itself means that the entrance is not shuffled. Indices will be overwritten +// later in Entrance_Init() in entrance.c if entrance shuffle is enabled. +// For the grotto load list, the entrance index is 0x0700 + the grotto id +// For the grotto exit list, the entrance index is 0x0800 + the grotto id +void Grotto_InitExitAndLoadLists(void) { + for (u8 i = 0; i < NUM_GROTTOS; i++) { + grottoLoadList[i] = 0x0700 + i; + grottoExitList[i] = 0x0800 + i; + } +} + +void Grotto_SetExitOverride(s16 originalIndex, s16 overrideIndex) { + s16 id = originalIndex & 0x00FF; + grottoExitList[id] = overrideIndex; +} + +void Grotto_SetLoadOverride(s16 originalIndex, s16 overrideIndex) { + s16 id = originalIndex & 0x00FF; + grottoLoadList[id] = overrideIndex; +} + +static void Grotto_SetupReturnInfo(GrottoReturnInfo grotto, RespawnMode respawnMode) { + // Set necessary grotto return data to the Entrance Point, so that voiding out and setting FW work correctly + gSaveContext.respawn[respawnMode].entranceIndex = grotto.entranceIndex; + gSaveContext.respawn[respawnMode].roomIndex = grotto.room; + + if (false /*mixGrottos == ON*/ || false /*decoupledEntrances == ON*/) { + gSaveContext.respawn[respawnMode].playerParams = 0x04FF; // exiting grotto with no initial camera focus + } + + gSaveContext.respawn[respawnMode].yaw = grotto.angle; + gSaveContext.respawn[respawnMode].pos = grotto.pos; + //TODO If Mixed Entrance Pools or decoupled entrances are active, set these flags to 0 instead of restoring them + if (false /*mixGrottos == ON*/ || false /*decoupledEntrances == ON*/) { + gSaveContext.respawn[respawnMode].tempSwchFlags = 0; + gSaveContext.respawn[respawnMode].tempCollectFlags = 0; + } else { + gSaveContext.respawn[respawnMode].tempSwchFlags = gSaveContext.respawn[RESPAWN_MODE_RETURN].tempSwchFlags; + gSaveContext.respawn[respawnMode].tempCollectFlags = gSaveContext.respawn[RESPAWN_MODE_RETURN].tempCollectFlags; + } +} + +// Translates and overrides the passed in entrance index if it corresponds to a +// special grotto entrance (grotto load or returnpoint) +s16 Grotto_OverrideSpecialEntrance(s16 nextEntranceIndex) { + + // Don't change anything unless grotto shuffle has been enabled + if (!Randomizer_GetSettingValue(RSK_SHUFFLE_GROTTO_ENTRANCES)) { + return nextEntranceIndex; + } + + // If Link hits a grotto exit, load the entrance index from the grotto exit list + // based on the current grotto ID + if (nextEntranceIndex == 0x7FFF) { + // SaveFile_SetEntranceDiscovered(0x0800 + grottoId); + nextEntranceIndex = grottoExitList[grottoId]; + } + + // Get the new grotto id from the next entrance + grottoId = nextEntranceIndex & 0x00FF; + + // Grotto Returns + if (nextEntranceIndex >= 0x0800 && nextEntranceIndex < 0x0800 + NUM_GROTTOS) { + + GrottoReturnInfo grotto = grottoReturnTable[grottoId]; + Grotto_SetupReturnInfo(grotto, RESPAWN_MODE_RETURN); + Grotto_SetupReturnInfo(grotto, RESPAWN_MODE_DOWN); + + // When the nextEntranceIndex is determined by a dynamic exit, we have + // to set the respawn information and nextEntranceIndex manually + if (gPlayState != NULL && gPlayState->nextEntranceIndex != -1) { + gSaveContext.respawnFlag = 2; + nextEntranceIndex = grotto.entranceIndex; + gPlayState->fadeTransition = 3; + gSaveContext.nextTransition = 3; + // Otherwise return 0x7FFF and let the game handle it + } else { + nextEntranceIndex = 0x7FFF; + } + + lastEntranceType = GROTTO_RETURN; + // Grotto Loads + } else if (nextEntranceIndex >= 0x0700 && nextEntranceIndex < 0x0800) { + + // Set the respawn data to load the correct grotto + GrottoLoadInfo grotto = grottoLoadTable[grottoId]; + gSaveContext.respawn[RESPAWN_MODE_RETURN].data = grotto.content; + nextEntranceIndex = grotto.entranceIndex; + + lastEntranceType = NOT_GROTTO; + // Otherwise just unset the current grotto ID + } else { + grottoId = 0xFF; + lastEntranceType = NOT_GROTTO; + } + + return nextEntranceIndex; +} + +// Override the entrance index when entering into a grotto actor +// thisx - pointer to the grotto actor +void Grotto_OverrideActorEntrance(Actor* thisx) { + + // Vanilla Behavior if grottos aren't shuffled + if (!Randomizer_GetSettingValue(RSK_SHUFFLE_GROTTO_ENTRANCES)) { + return; + } + + s8 grottoContent = thisx->params & 0x00FF; + + // Loop through the grotto load table until we find the matching index based + // on content and scene + for (s16 index = 0; index < NUM_GROTTOS; index++) { + + if (grottoContent == grottoLoadTable[index].content && gPlayState->sceneNum == grottoLoadTable[index].scene) { + // Find the override for the matching index from the grotto Load List + // SaveFile_SetEntranceDiscovered(0x0700 + index); + index = grottoLoadList[index]; + + // Run the index through the special entrances override check + lastEntranceType = GROTTO_LOAD; + gPlayState->nextEntranceIndex = Grotto_OverrideSpecialEntrance(index); + return; + } + } +} + +// Set the respawn flag for when we want to return from a grotto entrance +// Used for Sun's Song and Game Over, which usually don't restore saved position data +void Grotto_ForceGrottoReturn(void) { + if (lastEntranceType == GROTTO_RETURN && Randomizer_GetSettingValue(RSK_SHUFFLE_GROTTO_ENTRANCES)) { + gSaveContext.respawnFlag = 2; + gSaveContext.respawn[RESPAWN_MODE_RETURN].playerParams = 0x0DFF; + gSaveContext.respawn[RESPAWN_MODE_RETURN].pos = grottoReturnTable[grottoId].pos; + //Save the current temp flags in the grotto return point, so they'll properly keep their values. + gSaveContext.respawn[RESPAWN_MODE_RETURN].tempSwchFlags = gPlayState->actorCtx.flags.tempSwch; + gSaveContext.respawn[RESPAWN_MODE_RETURN].tempCollectFlags = gPlayState->actorCtx.flags.tempCollect; + } +} + +// Used for the DMT special voids, which usually don't restore saved position data +void Grotto_ForceRegularVoidOut(void) { + if (lastEntranceType == GROTTO_RETURN && Randomizer_GetSettingValue(RSK_SHUFFLE_GROTTO_ENTRANCES)) { + gSaveContext.respawn[RESPAWN_MODE_DOWN] = gSaveContext.respawn[RESPAWN_MODE_RETURN]; + gSaveContext.respawn[RESPAWN_MODE_DOWN].playerParams = 0x0DFF; + gSaveContext.respawn[RESPAWN_MODE_DOWN].pos = grottoReturnTable[grottoId].pos; + gSaveContext.respawnFlag = 1; + } +} + +// If returning to a FW point saved at a grotto exit, copy the FW data to the Grotto Return Point +// so that Sun's Song and Game Over will behave correctly +void Grotto_SetupReturnInfoOnFWReturn(void) { + if (Randomizer_GetSettingValue(RSK_SHUFFLE_GROTTO_ENTRANCES)) { + gSaveContext.respawn[RESPAWN_MODE_RETURN] = gSaveContext.respawn[RESPAWN_MODE_TOP]; + gSaveContext.respawn[RESPAWN_MODE_RETURN].playerParams = 0x0DFF; + lastEntranceType = GROTTO_RETURN; + } +} + +// Get the renamed entrance index based on the grotto contents and exit scene number +s16 Grotto_GetRenamedGrottoIndexFromOriginal(s8 content, s8 scene) { + for (s16 index = 0; index < NUM_GROTTOS; index++) { + if (content == grottoLoadTable[index].content && scene == grottoLoadTable[index].scene) { + return 0x0700 | index; + } + } + + return 0x0700; +} diff --git a/soh/soh/Enhancements/randomizer/randomizer_grotto.h b/soh/soh/Enhancements/randomizer/randomizer_grotto.h new file mode 100644 index 000000000..b8f4b06fd --- /dev/null +++ b/soh/soh/Enhancements/randomizer/randomizer_grotto.h @@ -0,0 +1,32 @@ +#ifndef _RANDO_GROTTO_H_ +#define _RANDO_GROTTO_H_ + +#include "z64math.h" + +#define NUM_GROTTOS 33 +#define NOT_GROTTO 0 +#define GROTTO_LOAD 1 +#define GROTTO_RETURN 2 + +typedef struct { + s16 entranceIndex; + s8 content; + s8 scene; +} GrottoLoadInfo; + +typedef struct { + s16 entranceIndex; + s8 room; + s16 angle; + Vec3f pos; +} GrottoReturnInfo; + +void Grotto_InitExitAndLoadLists(void); +void Grotto_SetExitOverride(s16 originalIndex, s16 overrideIndex); +void Grotto_SetLoadOverride(s16 originalIndex, s16 overrideIndex); +s16 Grotto_OverrideSpecialEntrance(s16 nextEntranceIndex); +void Grotto_ForceGrottoReturn(void); +void Grotto_ForceRegularVoidOut(void); +s16 Grotto_GetRenamedGrottoIndexFromOriginal(s8 content, s8 scene); + +#endif //_RANDO_GROTTO_H_ diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index cc4cca6b5..dc9e3c236 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -1836,6 +1836,10 @@ extern "C" bool Randomizer_IsTrialRequired(RandomizerInf trial) { return OTRGlobals::Instance->gRandomizer->IsTrialRequired(trial); } +extern "C" void Randomizer_LoadEntranceOverrides(const char* spoilerFileName, bool silent) { + OTRGlobals::Instance->gRandomizer->LoadEntranceOverrides(spoilerFileName, silent); +} + extern "C" u32 SpoilerFileExists(const char* spoilerFileName) { return OTRGlobals::Instance->gRandomizer->SpoilerFileExists(spoilerFileName); } diff --git a/soh/soh/OTRGlobals.h b/soh/soh/OTRGlobals.h index 237ebf933..22a168377 100644 --- a/soh/soh/OTRGlobals.h +++ b/soh/soh/OTRGlobals.h @@ -120,6 +120,7 @@ void Randomizer_LoadMerchantMessages(const char* spoilerFileName); void Randomizer_LoadRequiredTrials(const char* spoilerFileName); void Randomizer_LoadMasterQuestDungeons(const char* spoilerFileName); void Randomizer_LoadItemLocations(const char* spoilerFileName, bool silent); +void Randomizer_LoadEntranceOverrides(const char* spoilerFileName, bool silent); bool Randomizer_IsTrialRequired(RandomizerInf trial); GetItemEntry Randomizer_GetItemFromActor(s16 actorId, s16 sceneNum, s16 actorParams, GetItemID ogId); GetItemEntry Randomizer_GetItemFromActorWithoutObtainabilityCheck(s16 actorId, s16 sceneNum, s16 actorParams, GetItemID ogId); diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 6312f668a..1a825fd99 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -137,6 +137,16 @@ void SaveManager::LoadRandomizerVersion2() { }); }); + SaveManager::Instance->LoadArray("entrances", ARRAY_COUNT(gSaveContext.entranceOverrides), [&](size_t i) { + SaveManager::Instance->LoadStruct("", [&]() { + SaveManager::Instance->LoadData("index", gSaveContext.entranceOverrides[i].index); + SaveManager::Instance->LoadData("destination", gSaveContext.entranceOverrides[i].destination); + SaveManager::Instance->LoadData("blueWarp", gSaveContext.entranceOverrides[i].blueWarp); + SaveManager::Instance->LoadData("override", gSaveContext.entranceOverrides[i].override); + SaveManager::Instance->LoadData("overrideDestination", gSaveContext.entranceOverrides[i].overrideDestination); + }); + }); + SaveManager::Instance->LoadArray("seed", ARRAY_COUNT(gSaveContext.seedIcons), [&](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.seedIcons[i]); }); @@ -209,6 +219,16 @@ void SaveManager::SaveRandomizer() { }); }); + SaveManager::Instance->SaveArray("entrances", ARRAY_COUNT(gSaveContext.entranceOverrides), [&](size_t i) { + SaveManager::Instance->SaveStruct("", [&]() { + SaveManager::Instance->SaveData("index", gSaveContext.entranceOverrides[i].index); + SaveManager::Instance->SaveData("destination", gSaveContext.entranceOverrides[i].destination); + SaveManager::Instance->SaveData("blueWarp", gSaveContext.entranceOverrides[i].blueWarp); + SaveManager::Instance->SaveData("override", gSaveContext.entranceOverrides[i].override); + SaveManager::Instance->SaveData("overrideDestination", gSaveContext.entranceOverrides[i].overrideDestination); + }); + }); + SaveManager::Instance->SaveArray("seed", ARRAY_COUNT(gSaveContext.seedIcons), [&](size_t i) { SaveManager::Instance->SaveData("", gSaveContext.seedIcons[i]); }); diff --git a/soh/src/code/z_demo.c b/soh/src/code/z_demo.c index 8f5a5fd31..4133d4dab 100644 --- a/soh/src/code/z_demo.c +++ b/soh/src/code/z_demo.c @@ -31,6 +31,8 @@ #include "scenes/misc/hakaana_ouke/hakaana_ouke_scene.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" + u16 D_8011E1C0 = 0; u16 D_8011E1C4 = 0; @@ -1236,6 +1238,10 @@ void Cutscene_Command_Terminator(PlayState* play, CutsceneContext* csCtx, CsCmdB play->fadeTransition = 3; break; } + + if (randoCsSkip) { + Entrance_OverrideCutsceneEntrance(cmd->base); + } } } diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 0a8d11a4b..0c3afc651 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -4,6 +4,7 @@ #include "textures/do_action_static/do_action_static.h" #include "textures/icon_item_static/icon_item_static.h" #include "soh/Enhancements/randomizer/adult_trade_shuffle.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" #ifdef _MSC_VER #include @@ -6361,6 +6362,12 @@ void Interface_Update(PlayState* play) { gSaveContext.respawnFlag = -2; play->nextEntranceIndex = gSaveContext.entranceIndex; + + // In ER, handle sun song respawn from last entrance from grottos + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_ENTRANCES)) { + Grotto_ForceGrottoReturn(); + } + play->sceneLoadFlag = 0x14; gSaveContext.sunsSongState = SUNSSONG_INACTIVE; func_800F6964(30); diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index 8bc51042e..9f5b35447 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -7,6 +7,7 @@ #include #include "soh/frame_interpolation.h" #include "soh/Enhancements/debugconsole.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" #include #include @@ -158,6 +159,11 @@ void Play_Destroy(GameState* thisx) { PlayState* play = (PlayState*)thisx; Player* player = GET_PLAYER(play); + // In ER, remove link from epona when entering somewhere that doesn't support epona + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_OVERWORLD_ENTRANCES)) { + Entrance_HandleEponaState(); + } + play->state.gfxCtx->callback = NULL; play->state.gfxCtx->callbackParam = 0; SREG(91) = 0; @@ -512,6 +518,7 @@ void Play_Init(GameState* thisx) { play, gEntranceTable[((void)0, gSaveContext.entranceIndex) + ((void)0, gSaveContext.sceneSetupIndex)].scene, gEntranceTable[((void)0, gSaveContext.sceneSetupIndex) + ((void)0, gSaveContext.entranceIndex)].spawn); + osSyncPrintf("\nSCENE_NO=%d COUNTER=%d\n", ((void)0, gSaveContext.entranceIndex), gSaveContext.sceneSetupIndex); Cutscene_HandleEntranceTriggers(play); @@ -1754,6 +1761,10 @@ void* Play_LoadFile(PlayState* play, RomFile* file) { } void Play_InitEnvironment(PlayState* play, s16 skyboxId) { + // For entrance rando, ensure the correct weather state and sky mode is applied + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_ENTRANCES)) { + Entrance_OverrideWeatherState(); + } Skybox_Init(&play->state, &play->skyboxCtx, skyboxId); Environment_Init(play, &play->envCtx, 0); } @@ -1781,6 +1792,10 @@ void Play_InitScene(PlayState* play, s32 spawn) void Play_SpawnScene(PlayState* play, s32 sceneNum, s32 spawn) { OTRPlay_SpawnScene(play, sceneNum, spawn); + + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_ENTRANCES)) { + Entrance_OverrideSpawnScene(sceneNum, spawn); + } } void func_800C016C(PlayState* play, Vec3f* src, Vec3f* dest) { diff --git a/soh/src/code/z_sram.c b/soh/src/code/z_sram.c index a2017ce9a..406cba1fb 100644 --- a/soh/src/code/z_sram.c +++ b/soh/src/code/z_sram.c @@ -4,6 +4,7 @@ #include #include #include +#include "soh/Enhancements/randomizer/randomizer_entrance.h" #include "soh/Enhancements/randomizer/adult_trade_shuffle.h" #define NUM_DUNGEONS 8 @@ -196,6 +197,19 @@ void Sram_OpenSave() { } } + // Setup the modified entrance table and entrance shuffle table for rando + if (gSaveContext.n64ddFlag) { + Entrance_Init(); + if (!CVar_GetS32("gRememberSaveLocation", 0) || gSaveContext.savedSceneNum == SCENE_YOUSEI_IZUMI_TATE || + gSaveContext.savedSceneNum == SCENE_KAKUSIANA) { + Entrance_SetSavewarpEntrance(); + } + } else { + // When going from a rando save to a vanilla save within the same game instance + // we need to reset the entrance table back to its vanilla state + Entrance_ResetEntranceTable(); + } + osSyncPrintf("scene_no = %d\n", gSaveContext.entranceIndex); osSyncPrintf(VT_RST); diff --git a/soh/src/overlays/actors/ovl_Bg_Spot01_Idosoko/z_bg_spot01_idosoko.c b/soh/src/overlays/actors/ovl_Bg_Spot01_Idosoko/z_bg_spot01_idosoko.c index c525f08df..c41e29942 100644 --- a/soh/src/overlays/actors/ovl_Bg_Spot01_Idosoko/z_bg_spot01_idosoko.c +++ b/soh/src/overlays/actors/ovl_Bg_Spot01_Idosoko/z_bg_spot01_idosoko.c @@ -47,7 +47,10 @@ void BgSpot01Idosoko_Init(Actor* thisx, PlayState* play) { Actor_ProcessInitChain(&this->dyna.actor, sInitChain); CollisionHeader_GetVirtual(&gKakarikoBOTWStoneCol, &colHeader); this->dyna.bgId = DynaPoly_SetBgActor(play, &play->colCtx.dyna, &this->dyna.actor, colHeader); - if (!LINK_IS_ADULT) { + // If dungeon entrance randomizer is on, remove the well stone as adult Link when + // child Link has drained the water to the well + if (!LINK_IS_ADULT || gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES) && + Flags_GetEventChkInf(0x67)) { Actor_Kill(&this->dyna.actor); } else { BgSpot01Idosoko_SetupAction(this, func_808ABF54); diff --git a/soh/src/overlays/actors/ovl_Bg_Spot12_Saku/z_bg_spot12_saku.c b/soh/src/overlays/actors/ovl_Bg_Spot12_Saku/z_bg_spot12_saku.c index d19ab61b4..768991f45 100644 --- a/soh/src/overlays/actors/ovl_Bg_Spot12_Saku/z_bg_spot12_saku.c +++ b/soh/src/overlays/actors/ovl_Bg_Spot12_Saku/z_bg_spot12_saku.c @@ -58,6 +58,12 @@ void func_808B3420(BgSpot12Saku* this, PlayState* play, CollisionHeader* collisi void BgSpot12Saku_Init(Actor* thisx, PlayState* play) { BgSpot12Saku* this = (BgSpot12Saku*)thisx; + // If ER is on, force the gate to always use its permanent flag + // (which it only uses in Child Gerudo Fortress in the vanilla game) + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES)) { + thisx->params = 0x0002; + } + func_808B3420(this, play, &gGerudoFortressGTGShutterCol, DPM_UNK); Actor_ProcessInitChain(&this->dyna.actor, sInitChain); if (Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) { @@ -125,6 +131,12 @@ void func_808B37AC(BgSpot12Saku* this, PlayState* play) { void BgSpot12Saku_Update(Actor* thisx, PlayState* play) { BgSpot12Saku* this = (BgSpot12Saku*)thisx; + // If ER is on, when the guard opens the GtG gate its permanent flag will be set. + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES) && + Flags_GetSwitch(play, 0x3A)) { + Flags_SetSwitch(play, 0x2); + } + if (this->timer > 0) { this->timer--; } diff --git a/soh/src/overlays/actors/ovl_Bg_Treemouth/z_bg_treemouth.c b/soh/src/overlays/actors/ovl_Bg_Treemouth/z_bg_treemouth.c index dbd4bc9f8..831e03a61 100644 --- a/soh/src/overlays/actors/ovl_Bg_Treemouth/z_bg_treemouth.c +++ b/soh/src/overlays/actors/ovl_Bg_Treemouth/z_bg_treemouth.c @@ -73,7 +73,9 @@ void BgTreemouth_Init(Actor* thisx, PlayState* play) { if ((gSaveContext.sceneSetupIndex < 4) && !LINK_IS_ADULT) { BgTreemouth_SetupAction(this, func_808BC8B8); - } else if (LINK_IS_ADULT || (gSaveContext.sceneSetupIndex == 7)) { + // If dungeon entrance randomizer is on, keep the tree mouth open when link is adult and sword & shield have been shown to mido + } else if ((LINK_IS_ADULT && (!gSaveContext.n64ddFlag || !Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES)) || + !Flags_GetEventChkInf(0x4)) || (gSaveContext.sceneSetupIndex == 7)) { this->unk_168 = 0.0f; BgTreemouth_SetupAction(this, BgTreemouth_DoNothing); } else { diff --git a/soh/src/overlays/actors/ovl_Demo_Im/z_demo_im.c b/soh/src/overlays/actors/ovl_Demo_Im/z_demo_im.c index b87e7a90d..d1753aee1 100644 --- a/soh/src/overlays/actors/ovl_Demo_Im/z_demo_im.c +++ b/soh/src/overlays/actors/ovl_Demo_Im/z_demo_im.c @@ -867,7 +867,12 @@ void func_80986B2C(PlayState* play) { if (Message_GetState(&play->msgCtx) == TEXT_STATE_CLOSING) { Player* player = GET_PLAYER(play); - play->nextEntranceIndex = 0xCD; + // In entrance rando have impa bring link back to the front of castle grounds + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_OVERWORLD_ENTRANCES)) { + play->nextEntranceIndex = 0x0138; + } else { + play->nextEntranceIndex = 0xCD; + } play->fadeTransition = 38; play->sceneLoadFlag = 0x14; func_8002DF54(play, &player->actor, 8); @@ -911,7 +916,12 @@ void GivePlayerRandoRewardImpa(Actor* impa, PlayState* play, RandomizerCheck che play->sceneLoadFlag = 0x14; play->fadeTransition = 3; gSaveContext.nextTransition = 3; - play->nextEntranceIndex = 0x0594; + // In entrance rando have impa bring link back to the front of castle grounds + if (Randomizer_GetSettingValue(RSK_SHUFFLE_OVERWORLD_ENTRANCES)) { + play->nextEntranceIndex = 0x0138; + } else { + play->nextEntranceIndex = 0x0594; + } gSaveContext.nextCutsceneIndex = 0; } } diff --git a/soh/src/overlays/actors/ovl_Door_Ana/z_door_ana.c b/soh/src/overlays/actors/ovl_Door_Ana/z_door_ana.c index 2d7356bb4..7d3dfa9da 100644 --- a/soh/src/overlays/actors/ovl_Door_Ana/z_door_ana.c +++ b/soh/src/overlays/actors/ovl_Door_Ana/z_door_ana.c @@ -6,6 +6,7 @@ #include "z_door_ana.h" #include "objects/gameplay_field_keep/gameplay_field_keep.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" #define FLAGS ACTOR_FLAG_25 @@ -139,6 +140,12 @@ void DoorAna_WaitOpen(DoorAna* this, PlayState* play) { destinationIdx = this->actor.home.rot.z + 1; } play->nextEntranceIndex = entrances[destinationIdx]; + + // In ER, load the correct entrance based on the grotto link is falling into + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_ENTRANCES)) { + Grotto_OverrideActorEntrance(&this->actor); + } + DoorAna_SetupAction(this, DoorAna_GrabPlayer); } else { if (!Player_InCsMode(play) && !(player->stateFlags1 & 0x8800000) && diff --git a/soh/src/overlays/actors/ovl_Door_Warp1/z_door_warp1.c b/soh/src/overlays/actors/ovl_Door_Warp1/z_door_warp1.c index 2ea62559a..bc54d7bd6 100644 --- a/soh/src/overlays/actors/ovl_Door_Warp1/z_door_warp1.c +++ b/soh/src/overlays/actors/ovl_Door_Warp1/z_door_warp1.c @@ -1,5 +1,6 @@ #include "z_door_warp1.h" #include "objects/object_warp1/object_warp1.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" #define FLAGS 0 @@ -580,6 +581,11 @@ void DoorWarp1_ChildWarpOut(DoorWarp1* this, PlayState* play) { play->nextEntranceIndex = 0x10E; gSaveContext.nextCutsceneIndex = 0; } + + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES)) { + Entrance_OverrideBlueWarp(); + } + osSyncPrintf("\n\n\nおわりおわり"); play->sceneLoadFlag = 0x14; play->fadeTransition = 7; @@ -682,6 +688,10 @@ void DoorWarp1_RutoWarpOut(DoorWarp1* this, PlayState* play) { gSaveContext.nextCutsceneIndex = 0xFFF0; } + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES)) { + Entrance_OverrideBlueWarp(); + } + play->sceneLoadFlag = 0x14; play->fadeTransition = 7; } @@ -890,6 +900,11 @@ void DoorWarp1_AdultWarpOut(DoorWarp1* this, PlayState* play) { gSaveContext.nextCutsceneIndex = 0; } } + + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES)) { + Entrance_OverrideBlueWarp(); + } + play->sceneLoadFlag = 0x14; play->fadeTransition = 3; gSaveContext.nextTransition = 7; diff --git a/soh/src/overlays/actors/ovl_En_Ge1/z_en_ge1.c b/soh/src/overlays/actors/ovl_En_Ge1/z_en_ge1.c index ddaa79292..0ee47a595 100644 --- a/soh/src/overlays/actors/ovl_En_Ge1/z_en_ge1.c +++ b/soh/src/overlays/actors/ovl_En_Ge1/z_en_ge1.c @@ -7,6 +7,7 @@ #include "z_en_ge1.h" #include "vt.h" #include "objects/object_ge1/object_ge1.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" #define FLAGS (ACTOR_FLAG_0 | ACTOR_FLAG_3) @@ -93,6 +94,20 @@ void EnGe1_Init(Actor* thisx, PlayState* play) { s32 pad; EnGe1* this = (EnGe1*)thisx; + // When spawning the gate operator, also spawn an extra gate operator on the wasteland side + if (gSaveContext.n64ddFlag && (Randomizer_GetSettingValue(RSK_SHUFFLE_GERUDO_MEMBERSHIP_CARD) || + Randomizer_GetSettingValue(RSK_SHUFFLE_OVERWORLD_ENTRANCES)) && (this->actor.params & 0xFF) == GE1_TYPE_GATE_OPERATOR) { + // Spawn the extra gaurd with params matching the custom type added (0x0300 + 0x02) + Actor_Spawn(&play->actorCtx, play, ACTOR_EN_GE1, -1358.0f, 88.0f, -3018.0f, 0, 0x95B0, 0, + 0x0300 | GE1_TYPE_EXTRA_GATE_OPERATOR); + } + + // Convert the "extra" gate operator into a normal one so it matches the same params + if ((this->actor.params & 0xFF) == GE1_TYPE_EXTRA_GATE_OPERATOR) { + this->actor.params &= ~0xFF; + this->actor.params |= GE1_TYPE_GATE_OPERATOR; + } + ActorShape_Init(&this->actor.shape, 0.0f, ActorShadow_DrawCircle, 30.0f); SkelAnime_InitFlex(play, &this->skelAnime, &gGerudoWhiteSkel, &gGerudoWhiteIdleAnim, this->jointTable, this->morphTable, GE1_LIMB_MAX); @@ -169,7 +184,12 @@ void EnGe1_Init(Actor* thisx, PlayState* play) { this->hairstyle = GE1_HAIR_STRAIGHT; if (EnGe1_CheckCarpentersFreed()) { - this->actionFunc = EnGe1_CheckForCard_GTGGuard; + // If the gtg gate is permanently open, don't let the gaurd charge to open it again + if (gSaveContext.n64ddFlag && gSaveContext.sceneFlags[93].swch & 0x00000004) { + this->actionFunc = EnGe1_SetNormalText; + } else { + this->actionFunc = EnGe1_CheckForCard_GTGGuard; + } } else { this->actionFunc = EnGe1_WatchForPlayerFrontOnly; } @@ -247,6 +267,10 @@ void EnGe1_KickPlayer(EnGe1* this, PlayState* play) { play->nextEntranceIndex = 0x3B4; } + if (gSaveContext.n64ddFlag) { + Entrance_OverrideGeurdoGuardCapture(); + } + play->fadeTransition = 0x26; play->sceneLoadFlag = 0x14; } diff --git a/soh/src/overlays/actors/ovl_En_Ge1/z_en_ge1.h b/soh/src/overlays/actors/ovl_En_Ge1/z_en_ge1.h index f286ce314..c3c79d7e4 100644 --- a/soh/src/overlays/actors/ovl_En_Ge1/z_en_ge1.h +++ b/soh/src/overlays/actors/ovl_En_Ge1/z_en_ge1.h @@ -12,6 +12,7 @@ typedef void (*EnGe1ActionFunc)(struct EnGe1*, PlayState*); typedef enum { /* 0x00 */ GE1_TYPE_GATE_GUARD, /* 0x01 */ GE1_TYPE_GATE_OPERATOR, + /* 0x02 */ GE1_TYPE_EXTRA_GATE_OPERATOR, // Custom guard type for entrance randomizer to open the gate /* 0x04 */ GE1_TYPE_NORMAL = 4, /* 0x05 */ GE1_TYPE_VALLEY_FLOOR, /* 0x45 */ GE1_TYPE_HORSEBACK_ARCHERY = 0x45, diff --git a/soh/src/overlays/actors/ovl_En_Ge2/z_en_ge2.c b/soh/src/overlays/actors/ovl_En_Ge2/z_en_ge2.c index 3cfe4b51f..0d49cbab1 100644 --- a/soh/src/overlays/actors/ovl_En_Ge2/z_en_ge2.c +++ b/soh/src/overlays/actors/ovl_En_Ge2/z_en_ge2.c @@ -7,6 +7,7 @@ #include "z_en_ge2.h" #include "vt.h" #include "objects/object_gla/object_gla.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" #define FLAGS (ACTOR_FLAG_0 | ACTOR_FLAG_3 | ACTOR_FLAG_4) @@ -253,6 +254,10 @@ void EnGe2_CaptureClose(EnGe2* this, PlayState* play) { play->nextEntranceIndex = 0x3B4; } + if (gSaveContext.n64ddFlag) { + Entrance_OverrideGeurdoGuardCapture(); + } + play->fadeTransition = 0x26; play->sceneLoadFlag = 0x14; } @@ -279,6 +284,10 @@ void EnGe2_CaptureCharge(EnGe2* this, PlayState* play) { play->nextEntranceIndex = 0x3B4; } + if (gSaveContext.n64ddFlag) { + Entrance_OverrideGeurdoGuardCapture(); + } + play->fadeTransition = 0x26; play->sceneLoadFlag = 0x14; } diff --git a/soh/src/overlays/actors/ovl_En_Ishi/z_en_ishi.c b/soh/src/overlays/actors/ovl_En_Ishi/z_en_ishi.c index b222e1f19..5741f49f2 100644 --- a/soh/src/overlays/actors/ovl_En_Ishi/z_en_ishi.c +++ b/soh/src/overlays/actors/ovl_En_Ishi/z_en_ishi.c @@ -330,6 +330,12 @@ void EnIshi_Init(Actor* thisx, PlayState* play) { Actor_Kill(&this->actor); return; } + // If dungeon entrance randomizer is on, remove the grey boulders that normally + // block child Link from reaching the Fire Temple entrance. + if (type == ROCK_LARGE && gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES) && + play->sceneNum == 0x061) { // Death Mountain Creater + Actor_Kill(&this->actor); + } EnIshi_SetupWait(this); } diff --git a/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c b/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c index 3f1db6e49..041104ef3 100644 --- a/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c +++ b/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c @@ -2,6 +2,7 @@ #include "vt.h" #include "overlays/actors/ovl_En_Syateki_Itm/z_en_syateki_itm.h" #include "objects/object_ossan/object_ossan.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" #define FLAGS (ACTOR_FLAG_0 | ACTOR_FLAG_3 | ACTOR_FLAG_4 | ACTOR_FLAG_27) @@ -154,6 +155,15 @@ void EnSyatekiMan_Init(Actor* thisx, PlayState* play) { s32 pad; EnSyatekiMan* this = (EnSyatekiMan*)thisx; + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_INTERIOR_ENTRANCES)) { + // If child is in the adult shooting gallery or adult in the child shooting gallery, then despawn the shooting gallery man + if ((LINK_IS_CHILD && Entrance_SceneAndSpawnAre(0x42, 0x00)) || //Kakariko Village -> Adult Shooting Gallery, index 003B in the entrance table + (LINK_IS_ADULT && Entrance_SceneAndSpawnAre(0x42, 0x01))) { //Market -> Child Shooting Gallery, index 016D in the entrance table + Actor_Kill(thisx); + return; + } + } + osSyncPrintf("\n\n"); // "Old man appeared!! Muhohohohohohohon" osSyncPrintf(VT_FGCOL(GREEN) "☆☆☆☆☆ 親父登場!!むほほほほほほほーん ☆☆☆☆☆ \n" VT_RST); diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 7fe844aa3..7f9ebb4ca 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -23,6 +23,7 @@ #include #include "soh/Enhancements/item-tables/ItemTableTypes.h" #include "soh/Enhancements/debugconsole.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" typedef enum { /* 0x00 */ KNOB_ANIM_ADULT_L, @@ -4221,14 +4222,26 @@ s32 func_80839034(PlayState* play, Player* this, CollisionPoly* poly, u32 bgId) func_800994A0(play); } else { play->nextEntranceIndex = play->setupExitList[sp3C - 1]; + + // Main override for entrance rando and entrance skips + if (gSaveContext.n64ddFlag) { + play->nextEntranceIndex = Entrance_OverrideNextIndex(play->nextEntranceIndex); + } + if (play->nextEntranceIndex == 0x7FFF) { gSaveContext.respawnFlag = 2; play->nextEntranceIndex = gSaveContext.respawn[RESPAWN_MODE_RETURN].entranceIndex; play->fadeTransition = 3; gSaveContext.nextTransition = 3; } else if (play->nextEntranceIndex >= 0x7FF9) { - play->nextEntranceIndex = - D_808544F8[D_80854514[play->nextEntranceIndex - 0x7FF9] + play->curSpawn]; + // handle dynamic exits + if (gSaveContext.n64ddFlag) { + play->nextEntranceIndex = Entrance_OverrideDynamicExit(D_80854514[play->nextEntranceIndex - 0x7FF9] + play->curSpawn); + } else { + play->nextEntranceIndex = + D_808544F8[D_80854514[play->nextEntranceIndex - 0x7FF9] + play->curSpawn]; + } + func_800994A0(play); } else { if (SurfaceType_GetSlope(&play->colCtx, poly, bgId) == 2) { @@ -13400,6 +13413,10 @@ void func_8084F88C(Player* this, PlayState* play) { play->nextEntranceIndex = 0x0088; } else if (this->unk_84F < 0) { Play_TriggerRespawn(play); + // In ER, handle DMT and other special void outs to respawn from last entrance from grotto + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_ENTRANCES)) { + Grotto_ForceRegularVoidOut(); + } } else { Play_TriggerVoidOut(play); } diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c index dd1d93f73..ece2ff48b 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c +++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c @@ -458,6 +458,7 @@ void FileChoose_UpdateRandomizer() { Randomizer_LoadItemLocations(fileLoc, silent); Randomizer_LoadMerchantMessages(fileLoc); Randomizer_LoadMasterQuestDungeons(fileLoc); + Randomizer_LoadEntranceOverrides(fileLoc, silent); fileSelectSpoilerFileLoaded = true; } } @@ -2136,6 +2137,7 @@ void FileChoose_LoadGame(GameState* thisx) { Randomizer_LoadRequiredTrials(""); Randomizer_LoadMerchantMessages(""); Randomizer_LoadMasterQuestDungeons(""); + Randomizer_LoadEntranceOverrides("", true); gSaveContext.respawn[0].entranceIndex = -1; gSaveContext.respawnFlag = 0; diff --git a/soh/src/overlays/gamestates/ovl_select/z_select.c b/soh/src/overlays/gamestates/ovl_select/z_select.c index 601af217c..278975528 100644 --- a/soh/src/overlays/gamestates/ovl_select/z_select.c +++ b/soh/src/overlays/gamestates/ovl_select/z_select.c @@ -9,6 +9,8 @@ #include "vt.h" #include "alloca.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" + void Select_LoadTitle(SelectContext* this) { this->state.running = false; SET_NEXT_GAMESTATE(&this->state, Title_Init, TitleContext); @@ -32,6 +34,12 @@ void Select_LoadGame(SelectContext* this, s32 entranceIndex) { Audio_QueueSeqCmd(SEQ_PLAYER_BGM_MAIN << 24 | NA_BGM_STOP); gSaveContext.entranceIndex = entranceIndex; + // Check the entrance to see if the exit should be overriden to a grotto return point for entrance rando + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_ENTRANCES)) { + // Ignore return value as we want to load into the entrance specified by the debug menu + Grotto_OverrideSpecialEntrance(Entrance_GetOverride(entranceIndex)); + } + if (CVar_GetS32("gBetterDebugWarpScreen", 0)) { CVar_SetS32("gBetterDebugWarpScreenCurrentScene", this->currentScene); CVar_SetS32("gBetterDebugWarpScreenTopDisplayedScene", this->topDisplayedScene); @@ -74,6 +82,14 @@ void Select_Grotto_LoadGame(SelectContext* this, s32 grottoIndex) { gSaveContext.respawn[RESPAWN_MODE_RETURN].playerParams = 0x4ff; gSaveContext.respawn[RESPAWN_MODE_RETURN].pos = this->betterGrottos[grottoIndex].pos; + // Check the entrance to see if the exit should be overriden to a grotto return point for entrance rando + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_ENTRANCES)) { + // Use grotto content and parent scene num to identify the right grotto + s16 grottoEntrance = Grotto_GetRenamedGrottoIndexFromOriginal(this->betterGrottos[grottoIndex].data, this->betterGrottos[grottoIndex].exitScene); + // Ignore return value as we want to load into the entrance specified by the debug menu + Grotto_OverrideSpecialEntrance(Entrance_GetOverride(grottoEntrance)); + } + if (CVar_GetS32("gBetterDebugWarpScreen", 0)) { CVar_SetS32("gBetterDebugWarpScreenCurrentScene", this->currentScene); CVar_SetS32("gBetterDebugWarpScreenTopDisplayedScene", this->topDisplayedScene); @@ -623,34 +639,34 @@ static BetterSceneSelectEntry sBetterScenes[] = { }; static BetterSceneSelectGrottoData sBetterGrottos[] = { - { 0x003F, 0x00EE, 0, 0x2C, { -504.0, 380.0, -1224.0 }}, - { 0x003F, 0x04D6, 2, 0x14, { 922.0, 0.0, -933.0 }}, - { 0x05B4, 0x00FC, 0, 0xFFFFFFED, { -201.0, 0.0, 1906.0 }}, - { 0x003F, 0x00CD, 0, 0x00, { -1428.0, 0.0, 790.0 }}, - { 0x003F, 0x0189, 0, 0x03, { -4026.0, -700.0, 13858.0 }}, - { 0x003F, 0x0189, 0, 0x22, { -259.0, -500.0, 12356.0 }}, - { 0x003F, 0x034D, 0, 0x28, { 861.0, 80.0, -253.0 }}, - { 0x05A0, 0x034D, 0, 0xFFFFFFE7, { -400.0, 0.0, 408.0 }}, - { 0x003F, 0x01B9, 0, 0x57, { -389.0, 1386.0, -1202.0 }}, - { 0x003F, 0x0147, 1, 0x7A, { 50.0, 1233.0, 1776.0 }}, - { 0x003F, 0x019D, 0, 0x29, { 369.0, 570.0, 128.0 }}, - { 0x059C, 0x0189, 0, 0xFFFFFFE6, { -5002.0, -700.0, 13823.0 }}, - { 0x05A4, 0x0246, 1, 0xFFFFFFF9, { -1703.0, 722.0, -481.0 }}, - { 0x05A4, 0x014D, 3, 0xFFFFFFFB, { 1091.0, 580.0, -1192.0 }}, - { 0x05A4, 0x05D4, 0, 0xFFFFFFFC, { 1798.0, 0.0, 1498.0 }}, - { 0x05A4, 0x021D, 0, 0xFFFFFFEF, { -3044.0, -1033.0, 6070.0 }}, - { 0x05B0, 0x01A9, 8, 0xFFFFFFF5, { 677.0, 0.0, -2515.0 }}, - { 0x05BC, 0x00EA, 0, 0xFFFFFFEB, { -1632.0, 100.0, -123.0 }}, - { 0x05BC, 0x0215, 0, 0xFFFFFFEE, { 317.0, 480.0, -2303.0 }}, - { 0x05BC, 0x03D0, 0, 0xFFFFFFF0, { -1321.0, 15.0, -968.0 }}, - { 0x05BC, 0x01F1, 0, 0xFFFFFFFD, { 71.0, -32.0, -1303.0 }}, - { 0x05C4, 0x04D6, 6, 0xFFFFFFF3, { 75.0, -20.0, -1596.0 }}, - { 0x0598, 0x017D, 0, 0xFFFFFFE5, { 2059.0, 20.0, -174.0 }}, - { 0x05B8, 0x023D, 0, 0xFFFFFFF6, { 986.0, 1571.0, 837.0 }}, - { 0x05A8, 0x018D, 0, 0xFFFFFFE4, { -7873.0, -300.0, 6916.0 }}, - { 0x05FC, 0x01B9, 0, 0xFFFFFFF8, { -678.0, 1946.0, -284.0 }}, - { 0x05AC, 0x0117, 0, 0xFFFFFFF2, { 271.0, -555.0, 1465.0 }}, - { 0x05C0, 0x00CD, 0, 0xFFFFFFE1, { -4945.0, -300.0, 2841.0 }}, + { 0x003F, 0x00EE, 0, 0x2C, 0x55, { -504.0, 380.0, -1224.0 }}, // Kokiri Forest -> KF Storms Grotto + { 0x003F, 0x04D6, 2, 0x14, 0x5B, { 922.0, 0.0, -933.0 }}, // Lost Woods -> LW Near Shortcuts Grotto + { 0x05B4, 0x00FC, 0, 0xED, 0x56, { -201.0, 0.0, 1906.0 }}, // SFM Entryway -> SFM Wolfos Grotto + { 0x003F, 0x00CD, 0, 0x00, 0x51, { -1428.0, 0.0, 790.0 }}, // Hyrule Field -> HF Near Market Grotto + { 0x003F, 0x0189, 0, 0x03, 0x51, { -4026.0, -700.0, 13858.0 }}, // Hyrule Field -> HF Open Grotto + { 0x003F, 0x0189, 0, 0x22, 0x51, { -259.0, -500.0, 12356.0 }}, // Hyrule Field -> HF Southeast Grotto + { 0x003F, 0x034D, 0, 0x28, 0x52, { 861.0, 80.0, -253.0 }}, // Kak Backyard -> Kak Open Grotto + { 0x05A0, 0x034D, 0, 0xE7, 0x52, { -400.0, 0.0, 408.0 }}, // Kakariko Village -> Kak Redead Grotto + { 0x003F, 0x01B9, 0, 0x57, 0x60, { -389.0, 1386.0, -1202.0 }}, // Death Mountain -> DMT Storms Grotto + { 0x003F, 0x0147, 1, 0x7A, 0x61, { 50.0, 1233.0, 1776.0 }}, // DMC Upper Nearby -> DMC Upper Grotto + { 0x003F, 0x019D, 0, 0x29, 0x54, { 369.0, 570.0, 128.0 }}, // Zora River -> ZR Open Grotto + { 0x059C, 0x0189, 0, 0xE6, 0x51, { -5002.0, -700.0, 13823.0 }}, // Hyrule Field -> HF Inside Fence Grotto + { 0x05A4, 0x0246, 1, 0xF9, 0x61, { -1703.0, 722.0, -481.0 }}, // DMC Lower Nearby -> DMC Hammer Grotto + { 0x05A4, 0x014D, 3, 0xFB, 0x62, { 1091.0, 580.0, -1192.0 }}, // GC Grotto Platform -> GC Grotto + { 0x05A4, 0x05D4, 0, 0xFC, 0x63, { 1798.0, 0.0, 1498.0 }}, // Lon Lon Ranch -> LLR Grotto + { 0x05A4, 0x021D, 0, 0xEF, 0x57, { -3044.0, -1033.0, 6070.0 }}, // Lake Hylia -> LH Grotto + { 0x05B0, 0x01A9, 8, 0xF5, 0x5B, { 677.0, 0.0, -2515.0 }}, // LW Beyond Mido -> LW Scrubs Grotto + { 0x05BC, 0x00EA, 0, 0xEB, 0x54, { -1632.0, 100.0, -123.0 }}, // Zora River -> ZR Storms Grotto + { 0x05BC, 0x0215, 0, 0xEE, 0x56, { 317.0, 480.0, -2303.0 }}, // Sacred Forest Meadow -> SFM Storms Grotto + { 0x05BC, 0x03D0, 0, 0xF0, 0x5A, { -1321.0, 15.0, -968.0 }}, // GV Fortress Side -> GV Storms Grotto + { 0x05BC, 0x01F1, 0, 0xFD, 0x5C, { 71.0, -32.0, -1303.0 }}, // Desert Colossus -> Colossus Grotto + { 0x05C4, 0x04D6, 6, 0xF3, 0x5B, { 75.0, -20.0, -1596.0 }}, // LW Beyond Mido -> Deku Theater + { 0x0598, 0x017D, 0, 0xE5, 0x51, { 2059.0, 20.0, -174.0 }}, // Hyrule Field -> HF Near Kak Grotto + { 0x05B8, 0x023D, 0, 0xF6, 0x5F, { 986.0, 1571.0, 837.0 }}, // Hyrule Castle Grounds -> HC Storms Grotto + { 0x05A8, 0x018D, 0, 0xE4, 0x51, { -7873.0, -300.0, 6916.0 }}, // Hyrule Field -> HF Cow Grotto + { 0x05FC, 0x01B9, 0, 0xF8, 0x60, { -678.0, 1946.0, -284.0 }}, // Death Mountain Summit -> DMT Cow Grotto + { 0x05AC, 0x0117, 0, 0xF2, 0x5A, { 271.0, -555.0, 1465.0 }}, // GV Grotto Ledge -> GV Octorok Grotto + { 0x05C0, 0x00CD, 0, 0xE1, 0x51, { -4945.0, -300.0, 2841.0 }}, // Hyrule Field -> HF Tektite Grotto }; void Select_UpdateMenu(SelectContext* this) { diff --git a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c index d955b1492..c13ec7908 100644 --- a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c +++ b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c @@ -14,6 +14,7 @@ #include "vt.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" static void* sEquipmentFRATexs[] = { gPauseEquipment00FRATex, gPauseEquipment01Tex, gPauseEquipment02Tex, gPauseEquipment03Tex, gPauseEquipment04Tex, @@ -4213,6 +4214,10 @@ void KaleidoScope_Update(PlayState* play) if (pauseCtx->promptChoice == 0) { Play_TriggerRespawn(play); gSaveContext.respawnFlag = -2; + // In ER, handle death warp to last entrance from grottos + if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SHUFFLE_ENTRANCES)) { + Grotto_ForceGrottoReturn(); + } gSaveContext.nextTransition = 2; gSaveContext.health = 0x30; Audio_QueueSeqCmd(0xF << 28 | SEQ_PLAYER_BGM_MAIN << 24 | 0xA);