From 2ececa32067cdf32d3244484bb4960e164378be1 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 6 May 2026 03:27:14 -0700 Subject: [PATCH] update goal flags during play --- src/d/d_save.cpp | 2 +- .../randomizer/game/randomizer_context.cpp | 216 +++++++++++++++--- .../randomizer/game/randomizer_context.hpp | 34 ++- src/dusk/randomizer/game/tools.cpp | 25 ++ src/dusk/randomizer/game/tools.h | 3 + .../generator/data/settings_list.yaml | 2 +- .../randomizer/generator/seedgen/settings.cpp | 14 ++ .../randomizer/generator/seedgen/settings.hpp | 3 + 8 files changed, 270 insertions(+), 29 deletions(-) diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index 0f72129c76..4a5605a7f4 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -1398,7 +1398,7 @@ BOOL dSv_event_c::isEventBit(const u16 i_no) const { case CITY_IN_THE_SKY_CLEARED: // Would like to find where this is checked and patch it there. { if (!dComIfGs_isEventBit(FIXED_THE_MIRROR_OF_TWILIGHT)) { - if (randomizer_GetContext().mSettings.at("Palace of Twilight Requirements") == "Vanilla") { + if (randomizer_GetContext().mSettings[RandomizerContext::PALACE_OF_TWILIGHT_REQUIREMENTS] != RandomizerContext::VANILLA) { return false; } } diff --git a/src/dusk/randomizer/game/randomizer_context.cpp b/src/dusk/randomizer/game/randomizer_context.cpp index ca15869e61..24afdab322 100644 --- a/src/dusk/randomizer/game/randomizer_context.cpp +++ b/src/dusk/randomizer/game/randomizer_context.cpp @@ -17,10 +17,11 @@ #include "d/actor/d_a_alink.h" #include "d/d_com_inf_game.h" -#include "d/d_meter2_info.h" #include "d/d_meter2.h" #include "d/d_meter2_draw.h" +#include "d/d_meter2_info.h" #include "d/d_msg_flow.h" +#include "flags.h" std::optional RandomizerContext::WriteToFile() { @@ -31,8 +32,8 @@ std::optional RandomizerContext::WriteToFile() { YAML::Node out{}; - for (const auto& [settingName, option] : this->mSettings) { - out["mSettings"][settingName] = option; + for (const auto& [setting, option] : this->mSettings) { + out["mSettings"][setting] = option; } // NOTE: When dumping u8s, they must be converted to u16s (or higher), otherwise they get dumped @@ -105,9 +106,9 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha // Necessary settings for (const auto& settingNode : in["mSettings"] ) { - const auto& settingName = settingNode.first.as(); - const auto& option = settingNode.second.as(); - this->mSettings[settingName] = option; + const auto& setting = settingNode.first.as(); + const auto& option = settingNode.second.as(); + this->mSettings[setting] = option; } // Event flags @@ -235,6 +236,49 @@ std::string RandomizerContext::GetSeedDataPath() const { return std::string(SDL_GetPrefPath(dusk::OrgName, dusk::AppName)) + "randomizer/seeds/" + this->mHash + "/seed.dat"; } +s32 RandomizerContext::settingToEnum(const std::string& settingName) { + static const std::unordered_map nameToEnum = { + {"Hyrule Barrier Dungeons", HYRULE_BARRIER_DUNGEONS}, + {"Hyrule Barrier Requirements", HYRULE_BARRIER_REQUIREMENTS}, + {"Hyrule Barrier Fused Shadows", HYRULE_BARRIER_FUSED_SHADOWS}, + {"Hyrule Barrier Mirror Shards", HYRULE_BARRIER_MIRROR_SHARDS}, + {"Hyrule Castle Big Key Requirements", HYRULE_BIG_KEY_REQUIREMENTS}, + {"Hyrule Barrier Poe Souls", HYRULE_BARRIER_POE_SOULS}, + {"Hyrule Barrier Hearts", HYRULE_BARRIER_HEARTS}, + {"Hyrule Castle Big Key Mirror Shards", HYRULE_BIG_KEY_MIRROR_SHARDS}, + {"Hyrule Castle Big Key Fused Shadows", HYRULE_BIG_KEY_FUSED_SHADOWS}, + {"Hyrule Castle Big Key Dungeons", HYRULE_BIG_KEY_DUNGEONS}, + {"Hyrule Castle Big Key Poe Souls", HYRULE_BIG_KEY_POE_SOULS}, + {"Hyrule Castle Big Key Hearts", HYRULE_BIG_KEY_HEARTS}, + {"Palace of Twilight Requirements", PALACE_OF_TWILIGHT_REQUIREMENTS}, + }; + + if (nameToEnum.contains(settingName)) { + return nameToEnum.at(settingName); + } + + return -1; +} + +s32 RandomizerContext::optionToEnum(const std::string& optionName) { + static const std::unordered_map nameToEnum = { + {"None", NONE}, + {"Vanilla", VANILLA}, + {"Open", OPEN}, + {"Fused Shadows", FUSED_SHADOWS}, + {"Mirror Shards", MIRROR_SHARDS}, + {"Poe Souls", POE_SOULS}, + {"Hearts", HEARTS}, + {"Dungeons", DUNGEONS}, + }; + + if (nameToEnum.contains(optionName)) { + return nameToEnum.at(optionName); + } + + return -1; +} + RandomizerState g_randomizerState; int RandomizerState::_create() { @@ -253,6 +297,96 @@ int RandomizerState::_delete() { return 1; } +/* + * Updates flags for Hyrule Castle Barrier, Palace of Twilight Access, + * and Hyrule Castle Big Key chest. Maybe a bit overkill to check this every frame, but + * it keeps it all in one place for now. + */ +static void updateGoalFlags() { + auto& settings = randomizer_GetContext().mSettings; + + // Hyrule Castle Barrier + if (!dComIfGs_isEventBit(BARRIER_GONE)) { + bool destroyBarrier = false; + switch (settings[RandomizerContext::HYRULE_BARRIER_REQUIREMENTS]) { + case RandomizerContext::VANILLA: + destroyBarrier = dComIfGs_isEventBit(PALACE_OF_TWILIGHT_CLEARED); + break; + case RandomizerContext::FUSED_SHADOWS: + destroyBarrier = numFusedShadows() >= settings[RandomizerContext::HYRULE_BARRIER_FUSED_SHADOWS]; + break; + case RandomizerContext::MIRROR_SHARDS: + destroyBarrier = numMirrorShards() >= settings[RandomizerContext::HYRULE_BARRIER_MIRROR_SHARDS]; + break; + case RandomizerContext::DUNGEONS: + destroyBarrier = numCompletedDungeons() >= settings[RandomizerContext::HYRULE_BARRIER_DUNGEONS]; + break; + case RandomizerContext::POE_SOULS: + destroyBarrier = dComIfGs_getPohSpiritNum() >= settings[RandomizerContext::HYRULE_BARRIER_POE_SOULS]; + break; + case RandomizerContext::HEARTS: + destroyBarrier = dComIfGs_getMaxLife() >= 5 * settings[RandomizerContext::HYRULE_BARRIER_HEARTS]; + break; + default: + break; + } + + if (destroyBarrier) { + dComIfGs_onEventBit(BARRIER_GONE); + } + } + + // Hyrule Castle Big Key Gate + if (!dComIfGs_isStageSwitch(0x18, 0x4B)) { + bool openGate = false; + switch (settings[RandomizerContext::HYRULE_BIG_KEY_REQUIREMENTS]) { + case RandomizerContext::FUSED_SHADOWS: + openGate = numFusedShadows() >= settings[RandomizerContext::HYRULE_BIG_KEY_FUSED_SHADOWS]; + break; + case RandomizerContext::MIRROR_SHARDS: + openGate = numMirrorShards() >= settings[RandomizerContext::HYRULE_BIG_KEY_MIRROR_SHARDS]; + break; + case RandomizerContext::DUNGEONS: + openGate = numCompletedDungeons() >= settings[RandomizerContext::HYRULE_BIG_KEY_DUNGEONS]; + break; + case RandomizerContext::POE_SOULS: + openGate = dComIfGs_getPohSpiritNum() >= settings[RandomizerContext::HYRULE_BIG_KEY_POE_SOULS]; + break; + case RandomizerContext::HEARTS: + openGate = dComIfGs_getMaxLife() >= 5 * settings[RandomizerContext::HYRULE_BIG_KEY_HEARTS]; + break; + default: + break; + } + + if (openGate) { + dComIfGs_onStageSwitch(0x18, 0x4B); + } + } + + // Palace of Twlight Access + if (!dComIfGs_isEventBit(FIXED_THE_MIRROR_OF_TWILIGHT)) { + bool openPalace = false; + switch (settings[RandomizerContext::PALACE_OF_TWILIGHT_REQUIREMENTS]) { + case RandomizerContext::VANILLA: + openPalace = dComIfGs_isEventBit(CITY_IN_THE_SKY_CLEARED); + break; + case RandomizerContext::FUSED_SHADOWS: + openPalace = numFusedShadows() >= 3; + break; + case RandomizerContext::MIRROR_SHARDS: + openPalace = numMirrorShards() >= 4; + break; + default: + break; + } + + if (openPalace) { + dComIfGs_onEventBit(FIXED_THE_MIRROR_OF_TWILIGHT); + } + } +} + int RandomizerState::execute() { if (!mInitialized) { return 0; @@ -468,6 +602,9 @@ void RandomizerState::offLoad() // Clear the danBit that starts a conversation when entering the ranch so the player can do goats as needed. dComIfGs_offSaveDunSwitch(0x1); } + + // Check and update our goal flags + updateGoalFlags(); } bool checkFoolishItemEffectReady() @@ -566,30 +703,28 @@ u32 getActorCRC32(stage_actor_data_class* actor) { return zng_crc32(0, reinterpret_cast(actor), RandomizerContext::ACTOR_CRC_SIZE); } -/* - * Generates a seed and writes the necessary seed files to the players seed directory - */ -void GenerateAndWriteSeed(std::string& generationStatusMsg) { - const auto result = SDL_GetPrefPath(dusk::OrgName, dusk::AppName); - if (!result) { - DuskLog.fatal("Unable to get PrefPath: {}", SDL_GetError()); - } - randomizer::Randomizer r; - r.SetBaseOutputPath(result); - auto generationResult = r.Generate(); - if (generationResult.has_value()) { - generationStatusMsg = fmt::format("Generation failed with the following error:\n{}", generationResult.value()); - return; - } - - const auto& world = r.GetWorlds()[0]; - +RandomizerContext WriteSeedData(const std::unique_ptr& world) { RandomizerContext randoData{}; // Settings we need to check ingame for (const auto& [setting, info] : *randomizer::seedgen::settings::GetAllSettingsInfo()) { if (info->NeedInGame()) { - randoData.mSettings[setting] = world->Setting(setting).GetCurrentOption(); + auto settingEnum = RandomizerContext::settingToEnum(setting); + if (settingEnum == -1) { + throw std::runtime_error("Setting \"" + setting + "\" does not have an associated enum value"); + } + auto option = world->Setting(setting).GetCurrentOption(); + int optionEnum{}; + // If this setting's options are just numbers, get the numeric value + if (info->OptionsAreNumbers()) { + optionEnum = world->Setting(setting).GetCurrentOptionAsNumber(); + } else { + optionEnum = RandomizerContext::optionToEnum(option); + } + if (optionEnum == -1) { + throw std::runtime_error("Option \"" + option + "\" for setting \"" + setting + "\" does not have an associated enum value"); + } + randoData.mSettings[settingEnum] = optionEnum; } } @@ -920,11 +1055,40 @@ void GenerateAndWriteSeed(std::string& generationStatusMsg) { } } + return std::move(randoData); +} + +/* + * Generates a seed and writes the necessary seed files to the players seed directory + */ +void GenerateAndWriteSeed(std::string& generationStatusMsg) { + const auto result = SDL_GetPrefPath(dusk::OrgName, dusk::AppName); + if (!result) { + DuskLog.fatal("Unable to get PrefPath: {}", SDL_GetError()); + } + randomizer::Randomizer r; + r.SetBaseOutputPath(result); + auto generationResult = r.Generate(); + if (generationResult.has_value()) { + generationStatusMsg = fmt::format("Seed Generation failed. Reason:\n{}", generationResult.value()); + return; + } + + const auto& world = r.GetWorlds()[0]; + RandomizerContext randoData{}; + try { + randoData = WriteSeedData(world); + } catch (const std::runtime_error& e) { + generationStatusMsg = + fmt::format("Failed to write seed data. Reason:\n{}", e.what()); + return; + } + randoData.mHash = r.GetConfig().GetHash(); auto writeToFileResult = randoData.WriteToFile(); if (writeToFileResult.has_value()) { generationStatusMsg = - fmt::format("Failed to write seed data. Reason: {}", writeToFileResult.value()); + fmt::format("Failed to write seed data to file. Reason:\n{}", writeToFileResult.value()); return; } diff --git a/src/dusk/randomizer/game/randomizer_context.hpp b/src/dusk/randomizer/game/randomizer_context.hpp index 7502736a67..d82c1f8e9f 100644 --- a/src/dusk/randomizer/game/randomizer_context.hpp +++ b/src/dusk/randomizer/game/randomizer_context.hpp @@ -24,7 +24,8 @@ public: u32 mSeedID{0}; std::string mHash{""}; - std::unordered_map mSettings{}; + // Maps enum of necessary setting to enum of value + std::unordered_map mSettings{}; std::list mStartEventFlags{}; std::unordered_map> mStartRegionFlags{}; @@ -48,6 +49,37 @@ public: std::optional WriteToFile(); std::optional LoadFromHash(const std::string& hash); std::string GetSeedDataPath() const; + + enum Settings { + HYRULE_BARRIER_REQUIREMENTS, + HYRULE_BARRIER_FUSED_SHADOWS, + HYRULE_BARRIER_MIRROR_SHARDS, + HYRULE_BARRIER_POE_SOULS, + HYRULE_BARRIER_HEARTS, + HYRULE_BARRIER_DUNGEONS, + HYRULE_BIG_KEY_REQUIREMENTS, + HYRULE_BIG_KEY_FUSED_SHADOWS, + HYRULE_BIG_KEY_MIRROR_SHARDS, + HYRULE_BIG_KEY_POE_SOULS, + HYRULE_BIG_KEY_HEARTS, + HYRULE_BIG_KEY_DUNGEONS, + PALACE_OF_TWILIGHT_REQUIREMENTS, + }; + + enum Options { + NONE, + VANILLA, + OPEN, + FUSED_SHADOWS, + MIRROR_SHARDS, + POE_SOULS, + HEARTS, + DUNGEONS, + }; + + static int settingToEnum(const std::string& settingName); + + static int optionToEnum(const std::string& optionName); }; /* diff --git a/src/dusk/randomizer/game/tools.cpp b/src/dusk/randomizer/game/tools.cpp index 8f1769fd4f..96cc98bba2 100644 --- a/src/dusk/randomizer/game/tools.cpp +++ b/src/dusk/randomizer/game/tools.cpp @@ -124,4 +124,29 @@ u16 getItemMessageID(u8 itemId) { } return itemId + 0x65; +} + +int numCompletedDungeons() { + int numCompleted{0}; + // Loop through dungeon area node ids + for (int i = 0x10; i < 0x18; ++i) { + numCompleted += dComIfGs_isStageBossEnemy(i); + } + return numCompleted; +} + +int numFusedShadows() { + int numFusedShadows{0}; + for (int i = 0; i < 3; ++i) { + numFusedShadows += dComIfGs_isCollectCrystal(i); + } + return numFusedShadows; +} + +int numMirrorShards() { + int numMirrorShards{0}; + for (int i = 0; i < 4; ++i) { + numMirrorShards += dComIfGs_isCollectMirror(i); + } + return numMirrorShards; } \ No newline at end of file diff --git a/src/dusk/randomizer/game/tools.h b/src/dusk/randomizer/game/tools.h index 5826a5f7bf..728b304fb9 100644 --- a/src/dusk/randomizer/game/tools.h +++ b/src/dusk/randomizer/game/tools.h @@ -14,3 +14,6 @@ int initCreatePlayerItem(u32 item, u32 flag, const cXyz* pos, int roomNo, const int getStageID(const char* stage = NULL); bool playerIsOnTitleScreen(); u16 getItemMessageID(u8 itemId); +int numCompletedDungeons(); +int numFusedShadows(); +int numMirrorShards(); \ No newline at end of file diff --git a/src/dusk/randomizer/generator/data/settings_list.yaml b/src/dusk/randomizer/generator/data/settings_list.yaml index 761e8d7d32..3d25129826 100644 --- a/src/dusk/randomizer/generator/data/settings_list.yaml +++ b/src/dusk/randomizer/generator/data/settings_list.yaml @@ -69,7 +69,7 @@ Tracker Important: True Default Option: Vanilla Options: - - Open: "The barrier around Hyrule Castle is dispelled from the beginning." + - Open: "The Mirror of Twilight is open at the start of the game." - Fused Shadows: "The player must collect all 3 Fused Shadows." - Mirror Shards: "The player must collect all 4 Mirror Shards." - Vanilla: "The player must complete City in the Sky." diff --git a/src/dusk/randomizer/generator/seedgen/settings.cpp b/src/dusk/randomizer/generator/seedgen/settings.cpp index 9aa852715c..5c248bfeeb 100644 --- a/src/dusk/randomizer/generator/seedgen/settings.cpp +++ b/src/dusk/randomizer/generator/seedgen/settings.cpp @@ -66,6 +66,11 @@ namespace randomizer::seedgen::settings utility::str::Erase(logicOption, "'", ")", "("); this->_logicOptions.push_back(logicOption); } + + // Note: Assumes an option is never a negative number + this->_optionsAreNumbers = std::ranges::all_of(options, [](const std::string& option) { + return !option.empty() && std::ranges::all_of(option, ::isdigit); + }); } std::string SettingInfo::GetDefaultOption() const @@ -114,6 +119,15 @@ namespace randomizer::seedgen::settings return this->_info->GetOptions()[this->_currentOptionIndex]; } + int Setting::GetCurrentOptionAsNumber() const { + try { + return std::stoi(this->GetCurrentOption()); + } catch (...) { + throw std::runtime_error("Option \"" + GetCurrentOption() + "\" for setting \"" + this->GetInfo()->GetName() + + "\" cannot be turned into a number"); + } + } + void Setting::ResolveIfRandom() { if (this->GetCurrentOptionIndex() == this->GetInfo()->GetRandomOptionIndex()) diff --git a/src/dusk/randomizer/generator/seedgen/settings.hpp b/src/dusk/randomizer/generator/seedgen/settings.hpp index 33f604e6a9..555d54edc8 100644 --- a/src/dusk/randomizer/generator/seedgen/settings.hpp +++ b/src/dusk/randomizer/generator/seedgen/settings.hpp @@ -100,6 +100,7 @@ namespace randomizer::seedgen::settings int GetRandomHigh() const { return this->_randomHigh; } bool TrackerImportant() const { return this->_trackerImportant; } bool NeedInGame() const { return this->_needInGame; } + bool OptionsAreNumbers() const { return this->_optionsAreNumbers; } private: int _id = -1; @@ -114,6 +115,7 @@ namespace randomizer::seedgen::settings int _randomHigh = 0; // Upper bound when choosing a random option bool _trackerImportant = false; // Whether or not this setting can affect trackers bool _needInGame = false; // Whether or not we need to read this setting during gameplay + bool _optionsAreNumbers = false;// Whether this setting's options are all numbers // Variables that hold the setting's name and options when being checked // in a logical requirement string. @@ -135,6 +137,7 @@ namespace randomizer::seedgen::settings void SetCurrentOption(const std::string& newOption); void SetCurrentOption(const int& newOptionIndex); std::string GetCurrentOption() const; + int GetCurrentOptionAsNumber() const; const int& GetCurrentOptionIndex() const { return this->_currentOptionIndex; } const bool& IsUsingRandomOption() const { return this->_isUsingRandomOption; } SettingInfo* GetInfo() const { return this->_info; }