Files
dusklight/src/dusk/randomizer/logic/world.cpp
T
2026-04-11 20:21:59 -07:00

1177 lines
49 KiB
C++

#include "world.hpp"
#include "search.hpp"
#include "../randomizer.hpp"
#include "../utility/exception.hpp"
#include "../utility/file.hpp"
#include "../utility/general.hpp"
#include "../utility/log.hpp"
#include "../utility/platform.hpp"
#include "../utility/random.hpp"
#include "../utility/string.hpp"
#include "../utility/yaml.hpp"
#include <iostream>
#include <unordered_set>
#include <filesystem>
namespace randomizer::logic::world
{
World::World(const int& id, Randomizer* randomizer) :
_id(id), _randomizer(randomizer)
{}
int World::GetID() const
{
return this->_id;
}
void World::SetSettings(const seedgen::settings::Settings& settings)
{
_settings = settings;
}
const seedgen::settings::Settings& World::GetSettings() const
{
return this->_settings;
}
void World::SetRandomizer(Randomizer* randomizer)
{
this->_randomizer = randomizer;
}
Randomizer* World::GetRandomizer() const
{
return this->_randomizer;
}
void World::ResolveRandomSettings()
{
for (auto& [name, setting] : this->_settings.GetMap())
{
setting.ResolveIfRandom();
}
}
void World::ResolveConflictingSettings()
{
// If Bonks Do Damage is On and the Damage Multiplier is OHKO and Eldin or Lanayru Twilight are not cleared, then
// this creates a logically impossible scenario. We can't guarantee repeatable access to a bottled fairy in twilight
// unless the player starts with the shadow crystal in their inventory. Turn off Bonks Do Damage in this case.
bool bonksDoDamage = this->Setting("Bonks Do Damage") == "On";
bool ohko = this->Setting("Damage Multiplier") == "OHKO";
bool eldinTwilightNotCleared = this->Setting("Eldin Twilight Cleared") == "Off";
bool lanayruTwilightNotCleared = this->Setting("Lanayru Twilight Cleared") == "Off";
if (bonksDoDamage && ohko && (eldinTwilightNotCleared || lanayruTwilightNotCleared))
{
this->Setting("Bonks Do Damage").SetCurrentOption("Off");
LOG_TO_DEBUG("Changing Bonks Do Damage to Off");
}
// If we're starting as wolf link, the prologue has to be skipped
if (this->Setting("Starting Form") == "Wolf" && this->Setting("Skip Prologue") == "Off")
{
this->Setting("Skip Prologue").SetCurrentOption("On");
LOG_TO_DEBUG("Turning off Prologue due to Wolf Start");
}
}
void World::Build()
{
randomizer::utility::platform::Log(std::string("Building World ") + std::to_string(this->GetID()));
this->BuildItemTable();
this->BuildLocationTable();
this->LoadLogicMacros();
this->LoadWorldGraph();
// TODO: Verify Hint Data
this->GenerateItemPools();
}
void World::BuildItemTable()
{
LOG_TO_DEBUG("Building Item Table for World " + std::to_string(this->GetID()));
// Check if we can open the file before parsing
auto filepath = RANDO_DATA_PATH "items.yaml";
randomizer::utility::file::Verify(filepath);
auto itemDataTree = LoadYAML(filepath);
// Process all nodes of the yaml file. Each node contains one item
for (const auto& itemNode : itemDataTree)
{
// Check to make sure all required fields are present
YAMLVerifyFields(itemNode, "Name", "Importance", "Id");
// Required Fields
auto id = itemNode["Id"].as<int>();
auto name = itemNode["Name"].as<std::string>();
auto importanceStr = itemNode["Importance"].as<std::string>();
auto importance = item::ImportanceFromStr(importanceStr);
if (importance == item::Importance::INVALID)
{
throw std::runtime_error(std::string("Unknown importance \"") + importanceStr + "\" from item node:\n" +
YAML::Dump(itemNode));
}
LOG_TO_DEBUG("Processing new item " + name + "\tid: " + std::to_string(id));
// Optional fields
auto gameWinningItem = itemNode["Game Winning Item"].as<bool>(false);
auto dungeonSmallKey = itemNode["Dungeon Small Key"].as<std::string>("");
auto dungeonBigKey = itemNode["Dungeon Big Key"].as<std::string>("");
auto dungeonCompass = itemNode["Dungeon Compass"].as<std::string>("");
auto dungeonMap = itemNode["Dungeon Map"].as<std::string>("");
// Make the item and insert it into the item table
auto item = std::make_unique<item::Item>(id,
name,
this,
importance,
gameWinningItem,
dungeonSmallKey != "",
dungeonBigKey != "",
dungeonCompass != "",
dungeonMap != "");
this->_itemTable.try_emplace(name, std::move(item));
// Assign dungeon items to dungeons
auto curItem = this->GetItem(name);
if (dungeonSmallKey != "")
{
this->GetDungeon(dungeonSmallKey)->SetSmallKey(curItem);
}
else if (dungeonBigKey != "")
{
this->GetDungeon(dungeonBigKey)->SetBigKey(curItem);
}
else if (dungeonCompass != "")
{
this->GetDungeon(dungeonCompass)->SetCompass(curItem);
}
else if (dungeonMap != "")
{
this->GetDungeon(dungeonMap)->SetDungeonMap(curItem);
}
}
}
void World::BuildLocationTable()
{
LOG_TO_DEBUG("Building Location Table for World " + std::to_string(this->GetID()));
// check if we can open the file before parsing because exceptions won't work on console
auto filepath = RANDO_DATA_PATH "locations.yaml";
randomizer::utility::file::Verify(filepath);
auto locationDataTree = LoadYAML(filepath);
// Process all nodes of the yaml file. Each node contains one location
int locationIdCounter = 0;
for (const auto& locationNode : locationDataTree)
{
// Check to make sure all required fields are present
YAMLVerifyFields(locationNode, "Name", "Categories");
// Required Fields
auto name = locationNode["Name"].as<std::string>();
std::unordered_set<std::string> categories = {};
for (const auto& category : locationNode["Categories"])
{
categories.insert(category.as<std::string>());
// Add the category to the registered location categories for this world.
// When checking a locations categories, we can check to make sure the category
// is in here to make sure proper categories are being checked.
this->_registeredLocationCategories.insert(category.as<std::string>());
}
// Optional Fields
auto originalItemName = locationNode["Original Item"].as<std::string>("Nothing");
// If the original item is a twilight tear for a twilight section that's been cleared, then don't include the
// location
if ((originalItemName == "Faron Twilight Tear" && this->Setting("Faron Twilight Cleared") == "On") ||
(originalItemName == "Eldin Twilight Tear" && this->Setting("Eldin Twilight Cleared") == "On") ||
(originalItemName == "Lanayru Twilight Tear" && this->Setting("Lanayru Twilight Cleared") == "On"))
{
LOG_TO_DEBUG("Removing " + name + " because it's corresponding twilight is already cleared");
this->_intentionallyRemovedLocations.insert(name);
continue;
}
auto originalItem = this->GetItem(originalItemName);
auto goalLocation = locationNode["Goal Location"].as<bool>(false);
auto hintPriority = locationNode["Hint Priority"].as<std::string>("Never");
auto location = std::make_unique<location::Location>(locationIdCounter++,
name,
categories,
this,
originalItem,
goalLocation,
hintPriority);
LOG_TO_DEBUG("Processing new location " + name + "\tid: " + std::to_string(locationIdCounter - 1) +
"\toriginal item: " + originalItemName);
location->SetRegisteredLocationCategories(&this->_registeredLocationCategories);
this->_locationTable.emplace(name, std::move(location));
}
}
void World::LoadLogicMacros()
{
LOG_TO_DEBUG("Loading Macros for World " + std::to_string(this->GetID()));
// check if we can open the file before parsing
auto filepath = RANDO_DATA_PATH "macros.yaml";
randomizer::utility::file::Verify(filepath);
auto macrosDataTree = LoadYAML(filepath);
// Process all nodes of the yaml file. Each node contains one macro
int macroIdCounter = 0;
for (const auto& macroNode : macrosDataTree)
{
auto macroName = macroNode.first.as<std::string>();
auto macroReqStr = macroNode.second.as<std::string>();
// Process the macro
this->_macros[macroIdCounter] = requirement::ParseRequirementString(macroReqStr,
this,
/*forceLogic = */ true);
// Store it
this->_macroIndexes[macroName] = macroIdCounter;
LOG_TO_DEBUG("\"" + macroName + "\" assigned macro index of " + std::to_string(macroIdCounter));
macroIdCounter += 1;
}
}
void World::LoadWorldGraph()
{
LOG_TO_DEBUG("Loading world graph for World " + std::to_string(this->GetID()));
std::unordered_set<int> definedEvents = {};
std::unordered_set<std::string> definedAreas = {};
// I don't know if directory iterator works on console so all logic files are manually specified here for now
std::string folder = RANDO_DATA_PATH "world/";
std::list<std::string> files = {
"Root.yaml",
"overworld/Ordona Province.yaml",
"overworld/Faron Province.yaml",
"overworld/Eldin Province.yaml",
"overworld/Lanayru Province.yaml",
"overworld/Gerudo Desert.yaml",
"overworld/Snowpeak Province.yaml",
"dungeons/Forest Temple.yaml",
"dungeons/Goron Mines.yaml",
"dungeons/Lakebed Temple.yaml",
"dungeons/Arbiters Grounds.yaml",
"dungeons/Snowpeak Ruins.yaml",
"dungeons/Temple of Time.yaml",
"dungeons/City in the Sky.yaml",
"dungeons/Palace of Twilight.yaml",
"dungeons/Hyrule Castle.yaml",
};
// Loop through and process all files
for (const auto& file : files)
{
auto filepath = folder + file;
randomizer::utility::file::Verify(filepath);
auto worldDataTree = LoadYAML(filepath);
for (const auto& areaNode : worldDataTree)
{
YAMLVerifyFields(areaNode, "Name");
// Required Fields
auto areaName = areaNode["Name"].as<std::string>();
// Optional Fields
auto mapSector = areaNode["Map Sector"].as<std::string>("");
auto region = areaNode["Region"].as<std::string>("");
auto twilight = areaNode["Twilight"].as<std::string>("");
auto dungeonStartArea = areaNode["Dungeon Start Area"].as<bool>(false);
auto canWarp = areaNode["Can Warp"].as<bool>(false);
auto canChangeTime = areaNode["Can Change Time"].as<bool>(false);
auto canTransformStr = areaNode["Can Transform"].as<std::string>("Always");
// Copy our events map so we can add autogenerated events to it
std::map<std::string, std::string> eventNodes = {};
if (areaNode["Events"])
{
for (const auto& eventNode : areaNode["Events"])
{
auto eventName = eventNode.first.as<std::string>();
auto eventReqStr = eventNode.second.as<std::string>();
eventNodes.emplace(eventName, eventReqStr);
}
}
// Add an event for accessing this area
eventNodes.emplace("Can Access " + areaName, "Nothing");
// If we can warp, add the Can Warp event
if (canWarp)
{
eventNodes.emplace("Can Warp", "Nothing");
}
// If this area unlocks a map sector, add the event for the map sector
if (mapSector != "")
{
eventNodes.emplace(mapSector + " Map Sector", "Nothing");
}
// Create and get the area object now so we can pass it to all the other things
// which need a pointer to it
auto area = this->GetArea(areaName, /*createIfNotFound = */ true);
definedAreas.emplace(areaName);
// Set if the area can change time
area->SetCanChangeTime(canChangeTime);
// If this area is in a dungeon, check and set the dungeon start area
if (this->_dungeons.contains(region))
{
auto dungeon = this->GetDungeon(region);
if (dungeonStartArea)
{
dungeon->SetStartingArea(area);
}
}
// Set hint region stuff
if (region != "")
{
area->SetHardAssignedRegion(region);
area->AddHintRegion(region);
}
// Set the transform status
// Check to make sure a valid string is used for Can Transform
const std::unordered_set<std::string> validTransformStatuses = {"Always", "If Transform Anywhere", "Never"};
if (!validTransformStatuses.contains(canTransformStr))
{
throw std::runtime_error("Unknown Can Transform Status \"" + canTransformStr + "\" in area \"" + areaName +
"\".");
}
auto canTransform = canTransformStr == "Always" ||
(this->Setting("Transform Anywhere") == "On" && canTransformStr == "If Transform Anywhere");
area->SetCanTransform(canTransform);
// Set the completed twilight macro index if necessary
if (twilight != "")
{
auto twilightMacro = "Can Complete " + twilight + " Twilight";
auto canCompleteTwilightMacroIndex = this->GetMacroIndex(twilightMacro);
if (canCompleteTwilightMacroIndex == -1)
{
throw std::runtime_error('\"' + twilightMacro +
"\" is not a macro that exists when trying to set twilight macro for " +
area->GetName());
}
// Only bother with this if the setting for clearing this twilight is off
if (this->Setting(twilight + " Twilight Cleared") == "Off")
{
area->SetTwilightCompletedMacroIndex(canCompleteTwilightMacroIndex);
}
}
// Lists of events, locations, and exits that we pass along to the area object
std::list<std::unique_ptr<area::EventAccess>> events = {};
std::list<std::unique_ptr<area::LocationAccess>> locations = {};
std::list<std::unique_ptr<entrance::Entrance>> exits = {};
// Process events
for (const auto& [eventName, eventReqStr] : eventNodes)
{
// Parse the requirement string
auto eventReq = requirement::ParseRequirementString(eventReqStr, this);
// Create the EventAccess wrapper and put it into the list of events for this area
auto eventIndex = this->GetEventIndex(eventName);
auto event = std::make_unique<area::EventAccess>(eventReq, area, eventIndex);
events.emplace_back(std::move(event));
definedEvents.emplace(eventIndex);
}
// Process locations
if (areaNode["Locations"])
{
for (const auto& locationNode : areaNode["Locations"])
{
// Get location name and requirement string
auto locationName = locationNode.first.as<std::string>();
auto locationReqStr = locationNode.second.as<std::string>();
// Ignore the location if it's been intentionally removed
if (this->_intentionallyRemovedLocations.contains(locationName))
{
continue;
}
auto location = this->GetLocation(locationName);
// If this location is in a twilight section, and is not a twilit insect, add the Not_Twilight macro.
// We can't assume repeatable access to non-insect locations in twilights.
if (twilight != "" && !location->GetOriginalItem()->GetName().ends_with("Twilight Tear"))
{
locationReqStr = "Not_Twilight and (" + locationReqStr + ")";
LOG_TO_DEBUG("Adding Not_Twilight check to requirement for " + locationName);
}
// Parse the requirement string
auto locationReq = requirement::ParseRequirementString(locationReqStr, this);
// Create the LocationAccess wrapper and put it into the list of locations for this area
auto locationAccess = std::make_unique<area::LocationAccess>(location, locationReq, area);
locations.emplace_back(std::move(locationAccess));
// Also add this LocationAccess to the locations list of access points
location->AddLocationAccess(locations.back().get());
}
}
// Process Exits
if (areaNode["Exits"])
{
for (const auto& exitNode : areaNode["Exits"])
{
// Get the connected area and requirement string
auto connectedAreaName = exitNode.first.as<std::string>();
auto entranceReqStr = exitNode.second.as<std::string>();
auto connectedArea = this->GetArea(connectedAreaName, /*createIfNotFound = */ true);
// Parse the requirement string
auto entranceReq = requirement::ParseRequirementString(entranceReqStr, this);
// Create the Entrance object and put it into the list of exits for this area
auto entrance =
std::make_unique<entrance::Entrance>(area, connectedArea, entranceReq, this);
exits.emplace_back(std::move(entrance));
}
}
area->SetEvents(events);
area->SetLocations(locations);
area->SetExits(exits);
}
}
// Make sure that all used events are defined
for (const auto& [eventName, eventIndex] : this->_eventIndexes)
{
if (!definedEvents.contains(eventIndex))
{
throw std::runtime_error("Event \"" + eventName + "\" is used but never defined.");
}
}
// Make sure all used areas are defined
for (const auto& [areaName, area] : this->_areaTable)
{
if (!definedAreas.contains(areaName))
{
throw std::runtime_error("Area \"" + areaName + "\" is used but never defined.");
}
}
// Pass a pointer for each exit to the entrance list for the area it connects to
for (const auto& [areaName, area] : this->_areaTable)
{
for (const auto& exit : area->GetExits())
{
exit->GetConnectedArea()->AddEntrance(exit);
}
}
}
bool World::EvaluateSettingCondition(const std::string& condition)
{
auto req = requirement::ParseRequirementString(condition, this, true);
return requirement::EvaluateSimpleRequirement(req, this);
}
void World::GenerateItemPools()
{
LOG_TO_DEBUG("Now building item pools");
item_pool::GenerateItemPool(this);
item_pool::GenerateStartingItemPool(this);
LOG_TO_DEBUG("Item Pool for world " + std::to_string(this->GetID()) + ":");
for (const auto& item : this->_itemPool)
{
LOG_TO_DEBUG("- " + item->GetName());
}
LOG_TO_DEBUG("Starting Inventory for world " + std::to_string(this->GetID()) + ":");
for (const auto& item : this->_startingItemPool)
{
LOG_TO_DEBUG("- " + item->GetName());
}
}
void World::PerformPreEntranceShuffleTasks()
{
this->PlaceVanillaItems();
this->SanitizeItemPool();
this->PlacePlandomizerItems();
this->SetNonProgressLocations();
}
void World::PlaceVanillaItems()
{
LOG_TO_DEBUG("Now placing vanilla items");
for (auto& [locationName, location] : this->_locationTable)
{
auto originalItem = location->GetOriginalItem();
auto originalItemName = originalItem->GetName();
// Place all vanilla items
// Vanilla Small Keys
if ((this->Setting("Small Keys") == "Vanilla" &&
(originalItem->IsDungeonSmallKey() ||
randomizer::utility::str::Contains(originalItemName, "Ordon Pumpkin", "Ordon Cheese"))) ||
// Vanilla Big Keys (only include Hyrule Castle Big Key if it has no requirements)
(this->Setting("Big Keys") == "Vanilla" && originalItem->IsBigKey() &&
(originalItemName != "Hyrule Castle Big Key" || this->Setting("Hyrule Castle Big Key Requirements") == "None")) ||
// Vanilla Maps and Compasses
(this->Setting("Maps and Compasses") == "Vanilla" &&
(originalItem->IsDungeonMap() || originalItem->IsCompass())) ||
// Hyrule Castle Big Key
(originalItemName == "Hyrule Castle Big Key" && this->Setting("Hyrule Castle Big Key Requirements") != "None") ||
// Vanilla Poe Souls
(originalItemName == "Poe Soul" &&
(this->Setting("Poe Souls") == "Vanilla" ||
(this->Setting("Poe Souls") == "Dungeon" && location->HasCategories("Dungeon")) ||
(this->Setting("Poe Souls") == "Overworld" && location->HasCategories("Overworld")))) ||
// Vanilla Golden Bugs
(this->Setting("Golden Bugs") == "Off" && location->HasCategories("Golden Bug")) ||
// Sky Characters
(this->Setting("Sky Characters") == "Off" && location->HasCategories("Sky Book")) ||
// NPC Gifts
(this->Setting("Gifts From NPCs") == "Off" && location->HasCategories("Npc")) ||
// Shop Items
(this->Setting("Shop Items") == "Off" && location->HasCategories("Shop")) ||
// Hidden Skills
(this->Setting("Hidden Skills") == "Off" && location->HasCategories("Hidden Skill")) ||
// Hidden Rupees
(this->Setting("Hidden Rupees") == "Off" && location->HasCategories("Rupee - Hidden")) ||
// Freestanding Rupees
(this->Setting("Freestanding Rupees") == "Off" && location->HasCategories("Rupee - Freestanding")) ||
// North Faron Woods Gate Key
(this->Setting("Skip Prologue") == "Off" && locationName == "Faron Mist Cave Open Chest") ||
// Some locations which will always be vanilla for the time being
(randomizer::utility::str::Contains(locationName,
"Renados Letter",
"Telma Invoice",
"Wooden Statue",
"Ilia Charm",
"Ilia Memory Reward",
"Defeat Ganondorf",
"Twilit Insect",
"Twilit Bloat")))
{
// Change bottled items to all be empty bottles. It's much easier logically to only have to worry about a single
// item as a bottle instead of all bottled items as bottles for the search algorithm. Other contents will
// replace the empty bottles after all items have been placed
if (originalItem->IsBottle())
{
originalItem = this->GetItem("Empty Bottle");
}
// Don't place stamps for now
if (originalItem->IsStamp())
{
originalItem = this->GetItem("Purple Rupee");
}
location->SetCurrentItem(originalItem);
location->SetKnownVanillaItem(true);
randomizer::utility::container::Erase(this->_itemPool, originalItem);
}
}
}
void World::PlacePlandomizerItems()
{
for (auto& [location, item] : this->_plandomizerLocations)
{
if (!location->IsEmpty())
{
throw std::runtime_error("Cannot plandomize \"" + item->GetName() + "\" at \"" + location->GetName() +
"\" because vanilla item \"" + location->GetCurrentItem()->GetName() +
"\" already exists there.");
}
location->SetCurrentItem(item);
randomizer::utility::container::Erase(this->_itemPool, item);
}
}
void World::SetNonProgressLocations()
{
LOG_TO_DEBUG("Now setting nonprogress locations for world " + std::to_string(this->GetID()));
// Any manually excluded locations are nonprogress
for (const auto& locationName : this->_settings.GetExcludedLocations())
{
auto location = this->GetLocation(locationName);
location->SetProgression(false);
}
// Some locations not being randomized can conflict with other settings. When
// the appropriate location and setting conflict, these locations should have their item
// removed and be set to nonprogress.
for (auto& [locationName, location] : this->_locationTable)
{
auto originalItem = location->GetOriginalItem();
auto originalItemName = originalItem->GetName();
// If an NPC gives a key when not randomized, but keys are keysy (keys shouldn't exist)
if ((this->Setting("Gifts From NPCs") == "Off" && location->HasCategories("Npc") &&
((this->Setting("Small Keys") == "Keysy" && originalItem->IsDungeonSmallKey()) ||
(this->Setting("Big Keys") == "Keysy" && originalItem->IsBigKey()) ||
(this->Setting("Maps and Compasses") == "Start With" &&
(originalItem->IsDungeonMap() || originalItem->IsCompass())))) ||
// Sky Characters are not randomized, but City in the Sky doesn't require Sky Book Characters (Sky characters
// shouldn't exist)
(this->Setting("Sky Characters") == "Off" && this->Setting("City Does Not Require Filled Skybook") == "On" &&
location->HasCategories("Sky Book")) ||
// We're starting with a shop item, but shop items aren't randomized
(this->Setting("Shop Items") == "Off" && location->HasCategories("Shop") &&
randomizer::utility::container::ElementInContainer(this->_startingItemPool, originalItem)))
{
location->RemoveCurrentItem();
location->SetKnownVanillaItem(false);
location->SetProgression(false);
}
}
}
void World::PerformPostEntranceShuffleTasks()
{
this->AssignAreaProperties();
this->AssignGoalLocations();
this->ChooseRequiredDungeons();
this->SetForbiddenItems();
}
void World::AssignAreaProperties()
{
for (auto& [areaName, area] : this->_areaTable)
{
area->AssignHintRegionsAndDungeonLocations();
// Also assign dungeons their starting entrance
for (const auto& exit : area->GetExits())
{
auto parentRegions = exit->GetParentArea()->GetHintRegions();
auto connectedRegions = exit->GetConnectedArea()->GetHintRegions();
if (!parentRegions.contains("None"))
{
for (auto& [dungeonName, dungeon] : this->_dungeons)
{
// If this exit leads into a dungeon and its parent area is not part of the dungeon
// then this is the entrance that leads into the dungeon
if (connectedRegions.contains(dungeonName) && !parentRegions.contains(dungeonName))
{
dungeon->AddStartingEntrance(exit);
}
}
}
}
}
}
void World::AssignGoalLocations()
{
std::unordered_map<std::string, location::LocationPool> dungeonGoalLocations = {};
for (const auto& [dungeonName, dungeon] : this->_dungeons)
{
dungeonGoalLocations[dungeonName] = {};
}
// Collect all the possible goal locations for each dungeon
for (auto& [areaName, area] : this->_areaTable)
{
for (const auto& locAcc : area->GetLocations())
{
auto location = locAcc->GetLocation();
if (location->IsGoalLocation())
{
for (const auto& region : area->GetHintRegions())
{
if (dungeonGoalLocations.contains(region))
{
dungeonGoalLocations.at(region).push_back(location);
}
}
}
}
}
// Set a single goal location for each dungeon
for (auto& [dungeonName, dungeon] : this->_dungeons)
{
auto& possibleGoalLocations = dungeonGoalLocations.at(dungeonName);
// If a goal location becomes unreachable due to beatable only logic, then it's possible a dungeon may not be
// assigned a goal location. Dungeons without a goal location cannot be chosen as required dungeons.
if (!possibleGoalLocations.empty())
{
dungeon->SetGoalLocation(randomizer::utility::random::RandomElement(possibleGoalLocations));
}
else
{
LOG_TO_DEBUG("No goal location could be chosen for " + dungeonName);
}
}
}
void World::SetForbiddenItems()
{
// Prevent small keys from appearing on bosses if the setting is on
if (this->Setting("No Small Keys on Bosses") == "On")
{
// Gather all boss locations (heart container and dungeon reward checks)
auto bossLocations = this->GetAllLocations();
randomizer::utility::container::FilterAndEraseFromVector(
bossLocations,
[](const auto& location)
{ return !randomizer::utility::str::Contains(location->GetName(), "Heart Container", "Dungeon Reward"); });
// Gather all small key items
item_pool::ItemPool smallKeys = {};
for (const auto& [itemName, item] : this->_itemTable)
{
if (item->IsDungeonSmallKey() || randomizer::utility::general::IsAnyOf(itemName,
"Ordon Pumpkin",
"Ordon Cheese",
"North Faron Woods Gate Key",
"Gerudo Desert Bulblin Camp Key"))
{
smallKeys.push_back(item.get());
}
}
// Set the small keys as forbidden on the boss locations
for (auto& location : bossLocations)
{
for (const auto& smallKey : smallKeys)
{
location->AddForbiddenItem(smallKey);
}
}
}
}
void World::ChooseRequiredDungeons()
{
// STUB
}
void World::DetermineRequiredDungeons()
{
for (const auto& [dungeonName, dungeon] : this->_dungeons)
{
// To determine if a dungeon is required, we're going to disable all of its entrances and then check to see
// that the game is still beatble. If the game is not beatable with the dungeon entrances disabled, then the
// dungeon is required.
// Disable the dungeon's starting entrances
for (auto& entrance : dungeon->GetStartingEntrances())
{
entrance->SetDisbled(true);
}
// Check if the game is beatable, set dungeon as required if so. If the dungeon is not required and barren
// unrequired dungeons is on, then set all the locations in the unrequired dungeon as nonprogress.
auto completeItemPool = item_pool::GetCompleteItemPool(this->_randomizer->GetWorlds());
if (!search::GameBeatable(&(this->_randomizer->GetWorlds()), completeItemPool))
{
dungeon->SetRequired(true);
}
else if (this->Setting("Unrequired Dungeons Are Barren") == "On")
{
for (auto& location : dungeon->GetLocations())
{
location->SetProgression(false);
}
}
// Re-enable the dungeon's entrances
for (auto& entrance : dungeon->GetStartingEntrances())
{
entrance->SetDisbled(false);
}
}
}
void World::SanitizeItemPool()
{
auto junkPool = item_pool::GetInitialJunkPool();
// Depending on the Trap item Frequency setting, add some amount of ice traps to the pool
if (this->Setting("Trap Item Frequency") == "Few")
{
junkPool.emplace("Foolish Item", 6);
}
else if (this->Setting("Trap Item Frequency") == "Many")
{
junkPool.emplace("Foolish Item", 27);
}
else if (this->Setting("Trap Item Frequency") == "Mayhem")
{
junkPool.emplace("Foolish Item", 64);
}
else if (this->Setting("Trap Item Frequency") == "Nightmare")
{
junkPool.clear();
junkPool.emplace("Foolish Item", 1);
}
// Create an actual item pool from the junk items
item_pool::ItemPool mainJunkPool = {};
for (const auto& [itemName, count] : junkPool)
{
auto item = this->GetItem(itemName);
for (auto i = 0; i < count; i++)
{
mainJunkPool.push_back(item);
}
}
auto allItemLocations = this->GetAllLocations();
int numEmptyLocations = std::count_if(allItemLocations.begin(),
allItemLocations.end(),
[](const auto& location) { return location->IsEmpty(); });
// Create a copy of the real pool we just made. When adding junk items we want to add all the items from the junk pool
// once if possible, then if there's more space left pick randomly from the full pool
auto mainJunkPoolCopy = mainJunkPool;
// Add items until the pool's size matches the number of empty locations
while (this->_itemPool.size() < numEmptyLocations)
{
item::Item* randomJunkItem;
if (!mainJunkPool.empty())
{
randomJunkItem = randomizer::utility::random::PopRandomElement(mainJunkPool);
}
else
{
randomJunkItem = randomizer::utility::random::RandomElement(mainJunkPoolCopy);
}
this->_itemPool.emplace_back(randomJunkItem);
LOG_TO_DEBUG("Added junk item \"" + randomJunkItem->GetName() + "\" to item pool for world " +
std::to_string(this->GetID()));
}
}
void World::SetSearchStartingProperties(search::Search* search) const
{
// Set the root area to have all player forms and times of day (necessary for entrance rando validation)
auto root = this->GetRootArea();
search->_areaFormTime[root] = requirement::FormTime::ALL;
}
void World::PerformPostFillTasks()
{
this->FinalizeBottleContents();
}
void World::FinalizeBottleContents()
{
// Replace 3 bottles with other bottle contents we currently use.
auto bottleWithGreatFairiesTears = this->GetItem("Bottle with Great Fairies Tears");
auto bottleWithHalfMilk = this->GetItem("Bottle with Half Milk");
auto bottleWithLanternOil = this->GetItem("Bottle with Lantern Oil");
auto emptyBottle = this->GetItem("Empty Bottle");
item_pool::ItemPool bottlePool = {bottleWithGreatFairiesTears,
bottleWithHalfMilk,
bottleWithLanternOil,
emptyBottle};
// If npc gifts are vanilla, then set those vanilla bottles appropriately
if (this->Setting("Gifts From NPCs") == "Off")
{
for (auto& [locationName, location] : this->_locationTable)
{
auto originalItem = location->GetOriginalItem();
if (location->HasCategories("Npc") && originalItem->IsBottle())
{
location->SetCurrentItem(originalItem);
}
}
}
// Otherwise gather all the locations which have a bottle and replace the bottles at those locations instead
else
{
// Gather the bottle locations
location::LocationPool bottleLocations = {};
for (auto& [locationName, location] : this->_locationTable)
{
auto originalItem = location->GetCurrentItem();
if (originalItem->IsBottle())
{
bottleLocations.push_back(location.get());
}
}
// Place the new bottle items
randomizer::utility::random::ShufflePool(bottleLocations);
for (auto& bottleLocation : bottleLocations)
{
bottleLocation->SetCurrentItem(randomizer::utility::random::PopRandomElement(bottlePool));
}
}
}
void World::AddPlandomizedLocation(location::Location* location, item::Item* item)
{
if (this->_plandomizerLocations.contains(location))
{
throw std::runtime_error("Plandomizer Error: multiple entries for \"" + location->GetName() + "\" in world " +
std::to_string(this->_id));
}
this->_plandomizerLocations[location] = item;
}
void World::AddPlandomizedEntrance(entrance::Entrance* entrance, entrance::Entrance* target)
{
for (const auto& [plandoEntrance, plandoTarget] : this->_plandomizerEntrances)
{
if (plandoEntrance == entrance)
{
throw std::runtime_error("Plandomizer Error: multiple entries for \"" + entrance->GetOriginalName() +
"\" in world " + std::to_string(this->_id));
}
if (plandoTarget == target)
{
throw std::runtime_error("Plandomizer Error: multiple entrances target \"" + target->GetOriginalName() +
"\" in world " + std::to_string(this->_id));
}
}
this->_plandomizerEntrances[entrance] = target;
}
std::unordered_map<entrance::Entrance*, entrance::Entrance*> World::GetPlandomizerEntrances()
{
return this->_plandomizerEntrances;
}
dungeon::Dungeon* World::GetDungeon(const std::string& name)
{
if (!this->_dungeons.contains(name))
{
this->_dungeons.emplace(name, std::make_unique<dungeon::Dungeon>(name, this));
LOG_TO_DEBUG("Added new dungeon \"" + name + "\" to world " + std::to_string(this->_id));
}
return this->_dungeons.at(name).get();
}
const std::map<std::string, std::unique_ptr<dungeon::Dungeon>>& World::GetDungeonTable() const
{
return this->_dungeons;
}
item::Item* World::GetItem(const std::string& name, const bool& ignoreError /*= false*/)
{
if (name == "Nothing")
{
return item::Nothing.get();
}
if (!this->_itemTable.contains(name))
{
if (!ignoreError)
{
throw std::runtime_error("Unknown item name \"" + name + "\"");
}
return nullptr;
}
return this->_itemTable.at(name).get();
}
item::Item* World::GetGameWinningItem() const
{
return this->_itemTable.at("Game Beatable").get();
}
item::Item* World::GetShadowCrystal()
{
return this->_itemTable.at("Shadow Crystal").get();
}
item_pool::ItemPool& World::GetItemPool()
{
return this->_itemPool;
}
item_pool::ItemPool& World::GetStartingItemPool()
{
return this->_startingItemPool;
}
location::Location* World::GetLocation(const std::string& name)
{
if (!this->_locationTable.contains(name))
{
throw std::runtime_error("Unknown location name \"" + name + "\"");
}
return this->_locationTable.at(name).get();
}
location::LocationPool World::GetAllLocations(const bool& includeNonItemLocations /* = false */)
{
location::LocationPool locationPool = {};
for (const auto& [locationName, location] : this->_locationTable)
{
if (includeNonItemLocations || !location->HasCategories("Non-Item Location"))
{
locationPool.emplace_back(location.get());
}
}
return locationPool;
}
area::Area* World::GetArea(const std::string& name, const bool& createIfNotFound /* = false */)
{
if (!this->_areaTable.contains(name))
{
if (createIfNotFound)
{
this->_areaTable.emplace(name, std::make_unique<area::Area>(name, this));
}
else
{
throw std::runtime_error("Unknown area name \"" + name + "\"");
}
}
return this->_areaTable.at(name).get();
}
area::Area* World::GetRootArea() const
{
return this->_areaTable.at("Root").get();
}
const std::map<std::string, std::unique_ptr<area::Area>>& World::GetAreaTable() const
{
return this->_areaTable;
}
entrance::Entrance* World::GetEntrance(const std::string& originalName)
{
auto [parentAreaName, connectedAreaName] = entrance::GetParentAndConnectedAreaNames(originalName);
auto parentArea = this->GetArea(parentAreaName);
auto connectedArea = this->GetArea(connectedAreaName);
for (const auto& exit : parentArea->GetExits())
{
if (exit->GetOriginalConnectedArea() == connectedArea)
{
return exit;
}
}
throw std::runtime_error("\"" + originalName + "\" is not a known connection");
}
int World::GetNewEntranceID()
{
return this->_entranceIdCounter++;
}
entrance::EntrancePool World::GetShuffleableEntrances(const entrance::Type& type,
const bool& onlyPrimary /* = false */)
{
entrance::EntrancePool shuffleableEntrances = {};
for (const auto& [areaName, area] : this->GetAreaTable())
{
for (const auto& exit : area->GetExits())
{
if ((type == exit->GetType() || type == entrance::Type::ALL) &&
(!onlyPrimary || exit->IsPrimary()) && exit->GetType() != entrance::Type::INVALID)
{
shuffleableEntrances.push_back(exit);
}
}
}
return shuffleableEntrances;
}
entrance::EntrancePool World::GetShuffledEntrances(
const entrance::Type& type /* = entrance::Type::ALL */,
const bool& onlyPrimary /* = false */)
{
auto entrances = this->GetShuffleableEntrances(type, onlyPrimary);
// Remove any entrances which aren't shuffled
randomizer::utility::container::FilterAndEraseFromVector(entrances, [](const auto& e) { return !e->IsShuffled(); });
return entrances;
}
std::unordered_map<entrance::Entrance*, int>& World::GetExitTimeFormCache()
{
return this->_exitTimeFormCache;
}
int World::GetMacroIndex(const std::string& macroName) const
{
if (this->_macroIndexes.contains(macroName))
{
return this->_macroIndexes.at(macroName);
}
return -1;
}
const requirement::Requirement& World::GetMacro(const int& macroIndex)
{
return this->_macros.at(macroIndex);
}
int World::GetEventIndex(const std::string& eventName, bool addIfNone /*= true*/)
{
// If the event doesn't exist
if (!this->_eventIndexes.contains(eventName))
{
if (addIfNone)
{
auto index = this->_randomizer->GetNewEventID();
this->_eventIndexes.emplace(eventName, index);
this->_eventNames.emplace(index, eventName);
LOG_TO_DEBUG("Event \"" + eventName + "\" was assigned eventIndex " + std::to_string(index));
}
else
{
throw std::runtime_error("Event \"" + eventName + "\" does not exist");
}
}
return this->_eventIndexes.at(eventName);
}
std::string World::GetEventName(const int& eventIndex)
{
if (!this->_eventNames.contains(eventIndex))
{
LOG_TO_ERROR("Invalid Event Index");
}
return this->_eventNames.at(eventIndex);
}
randomizer::seedgen::settings::Setting& World::Setting(const std::string& settingName)
{
auto& settings = this->_settings;
// Check to make sure the setting exists
if (!settings.GetMap().contains(settingName))
{
throw std::runtime_error("Setting \"" + settingName + "\" is not a known setting");
}
return settings.GetMap().at(settingName);
}
} // namespace randomizer::logic::world