Entrance Rando (#1760)

This commit is contained in:
Adam Bird 2022-11-14 06:13:21 -05:00 committed by GitHub
parent 8be2c4ddd7
commit 15a9975200
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1341 additions and 142 deletions

View File

@ -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"

View File

@ -1181,6 +1181,7 @@ typedef struct {
/* */ s32 returnEntranceIndex;
/* */ s8 roomIndex;
/* */ s8 data;
/* */ s8 exitScene;
/* */ Vec3f pos;
} BetterSceneSelectGrottoData;

View File

@ -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];

View File

@ -7,60 +7,11 @@
#include <string>
#include <list>
#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<EntranceOverride> entranceOverrides;
enum class EntranceType {
@ -314,7 +265,6 @@ private:
int ShuffleAllEntrances();
void CreateEntranceOverrides();
EntranceTrackingData* GetEntranceTrackingData();
extern std::vector<std::list<Entrance*>> playthroughEntrances;
extern bool noRandomEntrances;

View File

@ -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));}}),
});

View File

@ -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]) {

View File

@ -296,25 +296,45 @@ 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;
}
}
@ -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;
}
auto playthroughNode = spoilerLog.NewElement("entrance-playthrough");
static void WriteShuffledEntrances() {
for (uint32_t i = 0; i < playthroughEntrances.size(); ++i) {
auto sphereNode = playthroughNode->InsertNewChildElement("sphere");
sphereNode->SetAttribute("level", i + 1);
auto sphereNum = std::to_string(i);
std::string sphereString = "sphere ";
if (i < 10) sphereString += "0";
sphereString += sphereNum;
for (Entrance* entrance : playthroughEntrances[i]) {
WriteShuffledEntrance(sphereNode, entrance, true);
WriteShuffledEntrance(sphereString, entrance);
}
}
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"))) {

View File

@ -220,6 +220,11 @@ std::unordered_map<std::string, RandomizerSettingKey> 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,6 +2643,17 @@ void GenerateRandomizerImgui() {
cvarSettings[RSK_MQ_DUNGEON_COUNT] = 0;
}
// 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);
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);
// todo: this efficently when we build out cvar array support
std::set<RandomizerCheck> excludedLocations;
std::stringstream excludedLocationStringStream(CVar_GetString("gRandomizeExcludedLocations", ""));
@ -2582,7 +2669,7 @@ void GenerateRandomizerImgui() {
CVar_Load();
generated = 1;
}
}
void DrawRandoEditor(bool& open) {
if (generated) {
@ -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();

View File

@ -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);

View File

@ -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;

View File

@ -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 <string.h>
#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
}
}
}

View File

@ -0,0 +1,50 @@
#ifndef _RANDO_ENTRANCE_H_
#define _RANDO_ENTRANCE_H_
#include <stdint.h>
//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_

View File

@ -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;
}

View File

@ -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_

View File

@ -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);
}

View File

@ -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);

View File

@ -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]);
});

View File

@ -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);
}
}
}

View File

@ -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 <stdlib.h>
@ -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);

View File

@ -7,6 +7,7 @@
#include <ImGuiImpl.h>
#include "soh/frame_interpolation.h"
#include "soh/Enhancements/debugconsole.h"
#include "soh/Enhancements/randomizer/randomizer_entrance.h"
#include <overlays/actors/ovl_En_Niw/z_en_niw.h>
#include <time.h>
@ -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) {

View File

@ -4,6 +4,7 @@
#include <string.h>
#include <soh/Enhancements/randomizer/randomizerTypes.h>
#include <soh/Enhancements/randomizer/randomizer_inf.h>
#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);

View File

@ -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);

View File

@ -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--;
}

View File

@ -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 {

View File

@ -867,7 +867,12 @@ void func_80986B2C(PlayState* play) {
if (Message_GetState(&play->msgCtx) == TEXT_STATE_CLOSING) {
Player* player = GET_PLAYER(play);
// 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;
// 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;
}
}

View File

@ -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) &&

View File

@ -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;

View File

@ -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()) {
// 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;
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);

View File

@ -23,6 +23,7 @@
#include <soh/Enhancements/custom-message/CustomMessageTypes.h>
#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) {
// 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);
}

View File

@ -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;

View File

@ -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) {

View File

@ -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);