diff --git a/src/dusk/randomizer/generator/logic/entrance.cpp b/src/dusk/randomizer/generator/logic/entrance.cpp index e6e0475903..b27d8d2c47 100644 --- a/src/dusk/randomizer/generator/logic/entrance.cpp +++ b/src/dusk/randomizer/generator/logic/entrance.cpp @@ -7,7 +7,6 @@ namespace randomizer::logic::entrance { - std::unordered_set NON_ASSUMED_TYPES = {Type::SPAWN, Type::WARP_PORTAL}; Type TypeFromStr(const std::string& str) { diff --git a/src/dusk/randomizer/generator/logic/entrance.hpp b/src/dusk/randomizer/generator/logic/entrance.hpp index 13fa2080ef..8159d4353c 100644 --- a/src/dusk/randomizer/generator/logic/entrance.hpp +++ b/src/dusk/randomizer/generator/logic/entrance.hpp @@ -49,7 +49,7 @@ namespace randomizer::logic::entrance ALL, }; - extern std::unordered_set NON_ASSUMED_TYPES; + static const std::unordered_set NON_ASSUMED_TYPES = {SPAWN, WARP_PORTAL}; /** * @brief Takes a string representation of a Type and returns the diff --git a/src/dusk/randomizer/generator/logic/entrance_shuffle.cpp b/src/dusk/randomizer/generator/logic/entrance_shuffle.cpp index 036b946c1e..68605ab208 100644 --- a/src/dusk/randomizer/generator/logic/entrance_shuffle.cpp +++ b/src/dusk/randomizer/generator/logic/entrance_shuffle.cpp @@ -2,11 +2,11 @@ #include "item_pool.hpp" #include "search.hpp" - -#include "../utility/file.hpp" #include "../utility/random.hpp" #include "../utility/yaml.hpp" +#include + using namespace randomizer::logic::entrance; namespace randomizer::logic::entrance_shuffle @@ -19,7 +19,11 @@ namespace randomizer::logic::entrance_shuffle auto targetEntrancePools = CreateTargetPools(entrancePools); // Set plando entrances first - SetPlandomizedEntrances(world, entrancePools, targetEntrancePools); + try { + SetPlandomizedEntrances(world, entrancePools, targetEntrancePools); + } catch (std::runtime_error& e) { + throw std::runtime_error("Plandomizer Error: " + std::string(e.what())); + } // Then shuffle non-assumed types (currently this is just spawn) ShuffleNonAssumedEntrancesPools(world, entrancePools, targetEntrancePools); @@ -360,20 +364,20 @@ namespace randomizer::logic::entrance_shuffle // Throw error if entrance/target types are not shuffleable if (entranceType == Type::INVALID) { - throw std::runtime_error("Plandomizer Error: " + entranceToConnect->GetOriginalName() + + throw std::runtime_error(entranceToConnect->GetOriginalName() + " is not an entrance that can be shuffled"); } if (plandoTarget->GetType() == Type::INVALID) { - throw std::runtime_error("Plandomizer Error: " + plandoTarget->GetOriginalName() + + throw std::runtime_error(plandoTarget->GetOriginalName() + " is not an entrance that can be shuffled"); } // Throw error if entrance type is shuffleable, but the type itself is not randomized currently if (!entrancePools.contains(entranceType)) { - throw std::runtime_error("Plandomizer Error: " + entranceToConnect->GetOriginalName() + "'s type " + - TypeToStr(entranceType) + " is not being shuffled and thus can't be plandomized."); + throw std::runtime_error("Entrance type " + TypeToStr(entranceType) + " for " + + entranceToConnect->GetOriginalName() + " is not being shuffled and thus can't be plandomized."); } // Get the appropriate pools @@ -383,13 +387,13 @@ namespace randomizer::logic::entrance_shuffle // If entrances are coupled, but the user tries to plandomize a non-primary connection, get the primary connection // instead if (world->Setting("Decouple Entrances") == "Off" && - randomizer::utility::container::ElementInContainer(entrancePool, entranceToConnect->GetReverse())) + utility::container::ElementInContainer(entrancePool, entranceToConnect->GetReverse())) { entranceToConnect = entranceToConnect->GetReverse(); targetToConnect = targetToConnect->GetReverse(); } - if (randomizer::utility::container::ElementInContainer(entrancePool, entranceToConnect)) + if (utility::container::ElementInContainer(entrancePool, entranceToConnect)) { bool validTargetFound = false; for (auto& target : targetPool) @@ -411,7 +415,7 @@ namespace randomizer::logic::entrance_shuffle } catch(const EntranceShuffleError& e) { - throw std::runtime_error("Could not connect plandomized entrance " + + throw std::runtime_error("Could not connect entrance " + entranceToConnect->GetOriginalName() + " to " + target->GetOriginalName() + " Reason:\n" + e.what()); } @@ -425,8 +429,8 @@ namespace randomizer::logic::entrance_shuffle // If we found our target, delete the entrance and it's now connected target from their respective pools if (validTargetFound) { - randomizer::utility::container::Erase(entrancePool, entranceToConnect); - randomizer::utility::container::Erase(targetPool, targetToConnect->GetAssumed()); + utility::container::Erase(entrancePool, entranceToConnect); + utility::container::Erase(targetPool, targetToConnect->GetAssumed()); } // Otherwise, the target is invalid else @@ -438,7 +442,7 @@ namespace randomizer::logic::entrance_shuffle else { throw std::runtime_error("Plandomizer Error: " + entranceToConnect->GetOriginalName() + - " for some reason could not be found."); + " could not be found."); } } @@ -449,15 +453,22 @@ namespace randomizer::logic::entrance_shuffle EntrancePools& entrancePools, EntrancePools& targetEntrancePools) { + // If we aren't shuffling any non-assumed types, return early + if (std::ranges::none_of(entrancePools | std::ranges::views::keys, [](const auto& type) { + return NON_ASSUMED_TYPES.contains(type); + })) { + return; + } + auto& worlds = world->GetRandomizer()->GetWorlds(); auto completeItemPool = item_pool::GetCompleteItemPool(worlds); // The idea here is we want to try shuffling all the non-assumed entrances - // at the same time since we can't validate the world after each one individually - // (That would require assuming access to entrances which we can't guarantee access to) + // at the same time since we can't validate the world after each one individually. + // (That would require assuming access to entrances which we can't guarantee access to.) // Realistically, this should never take more than 1 or 2 tries unless there's some wacky - // plandomizer stuff going on. Currently the only non-assumed entrance we're shuffling is the randomized spawn - // but if we ever shuffle warp portals, they'll go here too. + // plandomizer stuff going on. Currently, the only non-assumed entrance we're shuffling + // is the randomized spawn, but if we ever shuffle warp portals, they'll go here too. int retries = 20; while (retries > 0) @@ -501,7 +512,7 @@ namespace randomizer::logic::entrance_shuffle for (auto& [entrance, target] : rollbacks) { ConfirmReplacement(entrance, target); - randomizer::utility::container::Erase(targetEntrancePools[entrance->GetType()], target); + utility::container::Erase(targetEntrancePools[entrance->GetType()], target); } // Once we've made a valid world, delete all other targets that didn't get used for (auto& [entranceType, targetPool] : targetEntrancePools) diff --git a/src/dusk/randomizer/generator/logic/plandomizer.cpp b/src/dusk/randomizer/generator/logic/plandomizer.cpp index 991f708555..a0cd43cee4 100644 --- a/src/dusk/randomizer/generator/logic/plandomizer.cpp +++ b/src/dusk/randomizer/generator/logic/plandomizer.cpp @@ -4,14 +4,12 @@ #include "../utility/yaml.hpp" #include "../utility/file.hpp" -#include "../utility/log.hpp" namespace randomizer::logic::plandomizer { void LoadPlandomizerData(world::WorldPool& worlds, const fspath& filepath, const bool& ignoreErrors /*false*/) { // Verify the file exists before trying to open it - // TODO: TRY CATCH HERE utility::file::Verify(filepath); auto plandoTree = LoadYAML(filepath); @@ -33,8 +31,8 @@ namespace randomizer::logic::plandomizer return; } - throw std::runtime_error("Plandomizer file locations for " + worldStr + - " is not a map. Please check your syntax before trying again."); + throw std::runtime_error("Locations for " + worldStr + + " is not a map. Please check your plandomizer file syntax."); } for (const auto& locationNode : locations) @@ -50,7 +48,7 @@ namespace randomizer::logic::plandomizer } else { - throw std::runtime_error("Plandomizer Error: Missing key \"item\" in node:\n" + + throw std::runtime_error("Missing key \"item\" in node:\n" + YAML::Dump(locationNode)); } @@ -59,7 +57,7 @@ namespace randomizer::logic::plandomizer worldId = locationNode.second["World"].as(); if (worldId < 1 || worldId > worlds.size()) { - std::string errorMsg = "Plandomizer Error: Bad World ID \"" + std::to_string(worldId) + + std::string errorMsg = "Bad World ID \"" + std::to_string(worldId) + "\"\nOnly " + std::to_string(worlds.size()) + " worlds are being generated."; throw std::runtime_error(errorMsg); diff --git a/src/dusk/randomizer/generator/logic/world.cpp b/src/dusk/randomizer/generator/logic/world.cpp index d0caf8d041..7c924a7529 100644 --- a/src/dusk/randomizer/generator/logic/world.cpp +++ b/src/dusk/randomizer/generator/logic/world.cpp @@ -633,6 +633,21 @@ namespace randomizer::logic::world location->SetCurrentItem(item); utility::container::Erase(this->_itemPool, item); } + + // If no world has entrance randomizer enabled, check to see if our plandomized item placements work + if (std::ranges::none_of(this->GetRandomizer()->GetWorlds(), [](const auto& world) { + return world->AnyEntranceRandomizerEnabled(); + })) { + if (!this->_plandomizerLocations.empty() && Setting("Logic Rules") != "No Logic") { + auto& worlds = this->GetRandomizer()->GetWorlds(); + auto completeItemPool = item_pool::GetCompleteItemPool(worlds); + auto verifyLogicError = search::VerifyLogic(&worlds, completeItemPool); + if (verifyLogicError.has_value()) + { + throw std::runtime_error("Plandomizer item placements do not work! Reason:\n" + verifyLogicError.value()); + } + } + } } void World::SetNonProgressLocations() @@ -1194,7 +1209,7 @@ namespace randomizer::logic::world } entrance::EntrancePool World::GetShuffleableEntrances(const entrance::Type& type, - const bool& onlyPrimary /* = false */) + bool onlyPrimary /* = false */) { entrance::EntrancePool shuffleableEntrances = {}; for (const auto& [areaName, area] : this->GetAreaTable()) @@ -1213,7 +1228,7 @@ namespace randomizer::logic::world entrance::EntrancePool World::GetShuffledEntrances( const entrance::Type& type /* = entrance::Type::ALL */, - const bool& onlyPrimary /* = false */) + bool onlyPrimary /* = false */) { auto entrances = this->GetShuffleableEntrances(type, onlyPrimary); @@ -1282,4 +1297,14 @@ namespace randomizer::logic::world } return settings.GetMap().at(settingName); } + + bool World::AnyEntranceRandomizerEnabled() { + return Setting("Randomize Starting Spawn") != "Off" || + Setting("Randomize Dungeon Entrances") != "Off" || + Setting("Randomize Boss Entrances") != "Off" || + Setting("Randomize Grotto Entrances") != "Off" || + Setting("Randomize Cave Entrances") != "Off" || + Setting("Randomize Interior Entrances") != "Off" || + Setting("Randomize Overworld Entrances") != "Off"; + } } // namespace randomizer::logic::world diff --git a/src/dusk/randomizer/generator/logic/world.hpp b/src/dusk/randomizer/generator/logic/world.hpp index d944e62e15..caf996df80 100644 --- a/src/dusk/randomizer/generator/logic/world.hpp +++ b/src/dusk/randomizer/generator/logic/world.hpp @@ -137,10 +137,10 @@ namespace randomizer::logic::world entrance::Entrance* GetEntrance(const std::string& originalName); int GetNewEntranceID(); entrance::EntrancePool GetShuffleableEntrances(const entrance::Type& type, - const bool& onlyPrimary = false); + bool onlyPrimary = false); entrance::EntrancePool GetShuffledEntrances( const entrance::Type& type = entrance::Type::ALL, - const bool& onlyPrimary = false); + bool onlyPrimary = false); std::unordered_map& GetExitTimeFormCache(); int GetMacroIndex(const std::string& macroName) const; @@ -149,6 +149,7 @@ namespace randomizer::logic::world std::string GetEventName(const int& eventIndex); seedgen::settings::Setting& Setting(const std::string& settingName); + bool AnyEntranceRandomizerEnabled(); TextDatabase& GetTextDatabase() { return this->_textDatabase; } const std::string& GetText(const std::string& name, Text::Type type = Text::STANDARD, Text::Language language = Text::ENGLISH) { diff --git a/src/dusk/randomizer/generator/randomizer.cpp b/src/dusk/randomizer/generator/randomizer.cpp index 449ed7945c..6d9307b3e1 100644 --- a/src/dusk/randomizer/generator/randomizer.cpp +++ b/src/dusk/randomizer/generator/randomizer.cpp @@ -106,7 +106,11 @@ namespace randomizer // Process Plando Data for all worlds if (this->_config.IsUsingPlandomizer()) { - logic::plandomizer::LoadPlandomizerData(this->_worlds, this->_config.GetPlandomizerPath()); + try { + logic::plandomizer::LoadPlandomizerData(this->_worlds, this->_config.GetPlandomizerPath()); + } catch (const std::runtime_error& e) { + throw std::runtime_error("Plandomizer Error: " + std::string(e.what())); + } } // Pre Entrance Shuffle Tasks @@ -118,7 +122,7 @@ namespace randomizer utility::platform::Log("Shuffling Entrances..."); for (auto& world : this->_worlds) { - logic::entrance_shuffle::ShuffleWorldEntrances(world.get(), this->_worlds); + logic::entrance_shuffle::ShuffleWorldEntrances(world.get()); } // Post Entrance Shuffle Tasks diff --git a/src/dusk/ui/rando_config.cpp b/src/dusk/ui/rando_config.cpp index a54d84e41d..da9a4487f2 100644 --- a/src/dusk/ui/rando_config.cpp +++ b/src/dusk/ui/rando_config.cpp @@ -421,6 +421,8 @@ Modal* RandomizerWindow::show_seed_gen_modal(std::string_view message) { }, .icon = "verifying", }))); + // Allow manual line breaks in this modal for error messages + modal->root()->SetProperty("white-space", "pre-line"); if (auto* doc = top_document()) { doc->focus(); @@ -1189,7 +1191,7 @@ void RandomizerWindow::update() { m_genSeedModal->set_icon("error"); } - m_genSeedModal->set_body(generationStatusMsg); + m_genSeedModal->set_body(escape(generationStatusMsg)); m_genSeedModal->add_action({ .label = "OK", .onPressed = [this](Modal& modal) { diff --git a/src/dusk/ui/window.hpp b/src/dusk/ui/window.hpp index 5d732da70b..955ebeeb07 100644 --- a/src/dusk/ui/window.hpp +++ b/src/dusk/ui/window.hpp @@ -65,6 +65,7 @@ public: void show() override; void hide(bool close) override; bool visible() const override; + Rml::Element* root() { return mRoot;} protected: Rml::Element* mRoot = nullptr;