Files
Shipwright/soh/soh/Enhancements/randomizer/3drando/fill.cpp
T
Kenix3 c7ccd6dbff LUS Cleanup: Strips out the logging system created for the console
Properly routes SPDLog to the console.
Creates an API to be able to send command responses back to the console.
Cleans up the console UI, hiding options when not needed.
Removes stdout console sink for Windows.
2022-08-09 22:34:16 -04:00

1075 lines
45 KiB
C++

#include "fill.hpp"
#include "custom_messages.hpp"
#include "dungeon.hpp"
#include "item_location.hpp"
#include "item_pool.hpp"
#include "location_access.hpp"
#include "logic.hpp"
#include "random.hpp"
#include "spoiler_log.hpp"
#include "starting_inventory.hpp"
#include "hints.hpp"
#include "hint_list.hpp"
#include "entrance.hpp"
#include "shops.hpp"
#include "debug.hpp"
#include <vector>
#include <list>
#include <Lib/spdlog/include/spdlog/spdlog.h>
using namespace CustomMessages;
using namespace Logic;
using namespace Settings;
static bool placementFailure = false;
static void RemoveStartingItemsFromPool() {
for (uint32_t startingItem : StartingInventory) {
for (size_t i = 0; i < ItemPool.size(); i++) {
if (startingItem == BIGGORON_SWORD) {
if (ItemPool[i] == GIANTS_KNIFE || ItemPool[i] == BIGGORON_SWORD) {
ItemPool[i] = GetJunkItem();
}
continue;
} else if (startingItem == ItemPool[i] || (ItemTable(startingItem).IsBottleItem() && ItemTable(ItemPool[i]).IsBottleItem())) {
if (AdditionalHeartContainers > 0 && (startingItem == PIECE_OF_HEART || startingItem == TREASURE_GAME_HEART)) {
ItemPool[i] = HEART_CONTAINER;
AdditionalHeartContainers--;
} else {
ItemPool[i] = GetJunkItem();
}
break;
}
}
}
}
//This function will propogate Time of Day access through the entrance
static bool UpdateToDAccess(Entrance* entrance, SearchMode mode) {
bool ageTimePropogated = false;
//propogate childDay, childNight, adultDay, and adultNight separately
Area* parent = entrance->GetParentRegion();
Area* connection = entrance->GetConnectedRegion();
if (!connection->childDay && parent->childDay && entrance->CheckConditionAtAgeTime(Logic::IsChild, AtDay)) {
connection->childDay = true;
ageTimePropogated = true;
}
if (!connection->childNight && parent->childNight && entrance->CheckConditionAtAgeTime(Logic::IsChild, AtNight)) {
connection->childNight = true;
ageTimePropogated = true;
}
if (!connection->adultDay && parent->adultDay && entrance->CheckConditionAtAgeTime(IsAdult, AtDay)) {
connection->adultDay = true;
ageTimePropogated = true;
}
if (!connection->adultNight && parent->adultNight && entrance->CheckConditionAtAgeTime(IsAdult, AtNight)) {
connection->adultNight = true;
ageTimePropogated = true;
}
//special check for temple of time
bool propogateTimeTravel = mode != SearchMode::TimePassAccess && mode != SearchMode::TempleOfTimeAccess;
if (!AreaTable(ROOT)->Adult() && AreaTable(TOT_BEYOND_DOOR_OF_TIME)->Child() && propogateTimeTravel) {
AreaTable(ROOT)->adultDay = AreaTable(TOT_BEYOND_DOOR_OF_TIME)->childDay;
AreaTable(ROOT)->adultNight = AreaTable(TOT_BEYOND_DOOR_OF_TIME)->childNight;
} else if (!AreaTable(ROOT)->Child() && AreaTable(TOT_BEYOND_DOOR_OF_TIME)->Adult() && propogateTimeTravel){
AreaTable(ROOT)->childDay = AreaTable(TOT_BEYOND_DOOR_OF_TIME)->adultDay;
AreaTable(ROOT)->childNight = AreaTable(TOT_BEYOND_DOOR_OF_TIME)->adultNight;
}
return ageTimePropogated;
}
// Various checks that need to pass for the world to be validated as completable
static void ValidateWorldChecks(SearchMode& mode, bool checkPoeCollectorAccess, bool checkOtherEntranceAccess, std::vector<uint32_t>& areaPool) {
// Condition for validating Temple of Time Access
if (mode == SearchMode::TempleOfTimeAccess && ((Settings::ResolvedStartingAge == AGE_CHILD && AreaTable(TEMPLE_OF_TIME)->Adult()) || (Settings::ResolvedStartingAge == AGE_ADULT && AreaTable(TEMPLE_OF_TIME)->Child()) || !checkOtherEntranceAccess)) {
mode = SearchMode::ValidStartingRegion;
}
// Condition for validating a valid starting region
if (mode == SearchMode::ValidStartingRegion) {
bool childAccess = Settings::ResolvedStartingAge == AGE_CHILD || AreaTable(TOT_BEYOND_DOOR_OF_TIME)->Child();
bool adultAccess = Settings::ResolvedStartingAge == AGE_ADULT || AreaTable(TOT_BEYOND_DOOR_OF_TIME)->Adult();
Area* kokiri = AreaTable(KOKIRI_FOREST);
Area* kakariko = AreaTable(KAKARIKO_VILLAGE);
if ((childAccess && (kokiri->Child() || kakariko->Child())) ||
(adultAccess && (kokiri->Adult() || kakariko->Adult())) ||
!checkOtherEntranceAccess) {
mode = SearchMode::PoeCollectorAccess;
ApplyStartingInventory();
Logic::NoBottles = true;
}
}
// Condition for validating Poe Collector Access
if (mode == SearchMode::PoeCollectorAccess && (AreaTable(MARKET_GUARD_HOUSE)->Adult() || !checkPoeCollectorAccess)) {
// Apply all items that are necessary for checking all location access
std::vector<uint32_t> itemsToPlace =
FilterFromPool(ItemPool, [](const auto i) { return ItemTable(i).IsAdvancement(); });
for (uint32_t unplacedItem : itemsToPlace) {
ItemTable(unplacedItem).ApplyEffect();
}
// Reset access as the non-starting age
if (Settings::ResolvedStartingAge == AGE_CHILD) {
for (uint32_t areaKey : areaPool) {
AreaTable(areaKey)->adultDay = false;
AreaTable(areaKey)->adultNight = false;
}
} else {
for (uint32_t areaKey : areaPool) {
AreaTable(areaKey)->childDay = false;
AreaTable(areaKey)->childNight = false;
}
}
mode = SearchMode::AllLocationsReachable;
} else {
Logic::NoBottles = false;
}
}
//Get the max number of tokens that can possibly be useful
static int GetMaxGSCount() {
//If bridge or LACS is set to tokens, get how many are required
int maxBridge = 0;
int maxLACS = 0;
if (Settings::Bridge.Is(RAINBOWBRIDGE_TOKENS)) {
maxBridge = Settings::BridgeTokenCount.Value<uint8_t>();
}
if (Settings::GanonsBossKey.Is(GANONSBOSSKEY_LACS_TOKENS)) {
maxLACS = Settings::LACSTokenCount.Value<uint8_t>();
}
maxBridge = std::max(maxBridge, maxLACS);
//Get the max amount of GS which could be useful from token reward locations
int maxUseful = 0;
//If the highest advancement item is a token, we know it is useless since it won't lead to an otherwise useful item
if (Location(KAK_50_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() && Location(KAK_50_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 50;
}
else if (Location(KAK_40_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() && Location(KAK_40_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 40;
}
else if (Location(KAK_30_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() && Location(KAK_30_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 30;
}
else if (Location(KAK_20_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() && Location(KAK_20_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 20;
}
else if (Location(KAK_10_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() && Location(KAK_10_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 10;
}
//Return max of the two possible reasons tokens could be important
return std::max(maxUseful, maxBridge);
}
std::string GetShopItemBaseName(std::string itemName) {
std::string baseName = itemName.erase(0, 4); //Delete "Buy "
//Delete amount, if present (so when it looks like Buy Deku Nut (10) remove the (10))
if (baseName.find("(") != std::string::npos) {
baseName = baseName.erase(baseName.find("("));
}
//Do the same for [] (only applies to red potions, other things with [] also have a ())
if (baseName.find("[") != std::string::npos) {
baseName = baseName.erase(baseName.find("["));
}
return baseName;
}
std::vector<uint32_t> GetEmptyLocations(std::vector<uint32_t> allowedLocations) {
return FilterFromPool(allowedLocations, [](const auto loc){ return Location(loc)->GetPlaceduint32_t() == NONE;});
}
std::vector<uint32_t> GetAllEmptyLocations() {
return FilterFromPool(allLocations, [](const auto loc) { return Location(loc)->GetPlaceduint32_t() == NONE; });
}
//This function will return a vector of ItemLocations that are accessible with
//where items have been placed so far within the world. The allowedLocations argument
//specifies the pool of locations that we're trying to search for an accessible location in
std::vector<uint32_t> GetAccessibleLocations(const std::vector<uint32_t>& allowedLocations, SearchMode mode /* = SearchMode::ReachabilitySearch*/, std::string ignore /*= ""*/, bool checkPoeCollectorAccess /*= false*/, bool checkOtherEntranceAccess /*= false*/) {
std::vector<uint32_t> accessibleLocations;
// Reset all access to begin a new search
if (mode < SearchMode::ValidateWorld) {
ApplyStartingInventory();
}
Areas::AccessReset();
LocationReset();
std::vector<uint32_t> areaPool = {ROOT};
if (mode == SearchMode::ValidateWorld) {
mode = SearchMode::TimePassAccess;
AreaTable(ROOT)->childNight = true;
AreaTable(ROOT)->adultNight = true;
AreaTable(ROOT)->childDay = true;
AreaTable(ROOT)->adultDay = true;
allLocationsReachable = false;
}
//Variables for playthrough
int gsCount = 0;
const int maxGsCount = mode == SearchMode::GeneratePlaythrough ? GetMaxGSCount() : 0; //If generating playthrough want the max that's possibly useful, else doesn't matter
bool bombchusFound = false;
std::vector<std::string> buyIgnores;
//Variables for search
std::vector<ItemLocation*> newItemLocations;
bool updatedEvents = false;
bool ageTimePropogated = false;
bool firstIteration = true;
//Variables for Time Pass access
bool timePassChildDay = false;
bool timePassChildNight = false;
bool timePassAdultDay = false;
bool timePassAdultNight = false;
// Main access checking loop
while (newItemLocations.size() > 0 || updatedEvents || ageTimePropogated || firstIteration) {
firstIteration = false;
ageTimePropogated = false;
updatedEvents = false;
for (ItemLocation* location : newItemLocations) {
location->ApplyPlacedItemEffect();
}
newItemLocations.clear();
std::vector<uint32_t> itemSphere;
std::list<Entrance*> entranceSphere;
for (size_t i = 0; i < areaPool.size(); i++) {
Area* area = AreaTable(areaPool[i]);
if (area->UpdateEvents(mode)){
updatedEvents = true;
}
// If we're checking for TimePass access do that for each area as it's being updated.
// TimePass Access is satisfied when every AgeTime can reach an area with TimePass
// without the aid of TimePass. During this mode, TimePass won't update ToD access
// in any area.
if (mode == SearchMode::TimePassAccess) {
if (area->timePass) {
if (area->childDay) {
timePassChildDay = true;
}
if (area->childNight) {
timePassChildNight = true;
}
if (area->adultDay) {
timePassAdultDay = true;
}
if (area->adultNight) {
timePassAdultNight = true;
}
}
// Condition for validating that all startring AgeTimes have timepass access
// Once satisifed, change the mode to begin checking for Temple of Time Access
if ((timePassChildDay && timePassChildNight && timePassAdultDay && timePassAdultNight) || !checkOtherEntranceAccess) {
mode = SearchMode::TempleOfTimeAccess;
}
}
//for each exit in this area
for (auto& exit : area->exits) {
//Update Time of Day Access for the exit
if (UpdateToDAccess(&exit, mode)) {
ageTimePropogated = true;
ValidateWorldChecks(mode, checkPoeCollectorAccess, checkOtherEntranceAccess, areaPool);
}
//If the exit is accessible and hasn't been added yet, add it to the pool
Area* exitArea = exit.GetConnectedRegion();
if (!exitArea->addedToPool && exit.ConditionsMet()) {
exitArea->addedToPool = true;
areaPool.push_back(exit.Getuint32_t());
}
// Add shuffled entrances to the entrance playthrough
if (mode == SearchMode::GeneratePlaythrough && exit.IsShuffled() && !exit.IsAddedToPool() && !noRandomEntrances) {
entranceSphere.push_back(&exit);
exit.AddToPool();
// Don't list a coupled entrance from both directions
if (exit.GetReplacement()->GetReverse() != nullptr /*&& !DecoupleEntrances*/) {
exit.GetReplacement()->GetReverse()->AddToPool();
}
}
}
//for each ItemLocation in this area
if (mode < SearchMode::ValidateWorld) {
for (size_t k = 0; k < area->locations.size(); k++) {
LocationAccess& locPair = area->locations[k];
uint32_t loc = locPair.GetLocation();
ItemLocation* location = Location(loc);
if (!location->IsAddedToPool() && locPair.ConditionsMet()) {
location->AddToPool();
if (location->GetPlaceduint32_t() == NONE) {
accessibleLocations.push_back(loc); //Empty location, consider for placement
} else {
//If ignore has a value, we want to check if the item location should be considered or not
//This is necessary due to the below preprocessing for playthrough generation
if (ignore != "") {
ItemType type = location->GetPlacedItem().GetItemType();
std::string itemName(location->GetPlacedItemName().GetEnglish());
//If we want to ignore tokens, only add if not a token
if (ignore == "Tokens" && type != ITEMTYPE_TOKEN) {
newItemLocations.push_back(location);
}
//If we want to ignore bombchus, only add if bombchu is not in the name
else if (ignore == "Bombchus" && itemName.find("Bombchu") == std::string::npos) {
newItemLocations.push_back(location);
}
//We want to ignore a specific Buy item name
else if (ignore != "Tokens" && ignore != "Bombchus") {
if ((type == ITEMTYPE_SHOP && ignore != GetShopItemBaseName(itemName)) || type != ITEMTYPE_SHOP) {
newItemLocations.push_back(location);
}
}
}
//If it doesn't, we can just add the location
else {
newItemLocations.push_back(location); //Add item to cache to be considered in logic next iteration
}
}
//Playthrough stuff
//Generate the playthrough, so we want to add advancement items, unless we know to ignore them
if (mode == SearchMode::GeneratePlaythrough) {
//Item is an advancement item, figure out if it should be added to this sphere
if (!playthroughBeatable && location->GetPlacedItem().IsAdvancement()) {
ItemType type = location->GetPlacedItem().GetItemType();
std::string itemName(location->GetPlacedItemName().GetEnglish());
bool bombchus = itemName.find("Bombchu") != std::string::npos; //Is a bombchu location
//Decide whether to exclude this location
//This preprocessing is done to reduce the amount of searches performed in PareDownPlaythrough
//Want to exclude:
//1) Tokens after the last potentially useful one (the last one that gives an advancement item or last for token bridge)
//2) Bombchus after the first (including buy bombchus)
//3) Buy items of the same type, after the first (So only see Buy Deku Nut of any amount once)
bool exclude = true;
//Exclude tokens after the last possibly useful one
if (type == ITEMTYPE_TOKEN && gsCount < maxGsCount) {
gsCount++;
exclude = false;
}
//Only print first bombchu location found
else if (bombchus && !bombchusFound) {
bombchusFound = true;
exclude = false;
}
//Handle buy items
//If ammo drops are off, don't do this step, since buyable ammo becomes logically important
else if (AmmoDrops.IsNot(AMMODROPS_NONE) && !(bombchus && bombchusFound) && type == ITEMTYPE_SHOP) {
//Only check each buy item once
std::string buyItem = GetShopItemBaseName(itemName);
//Buy item not in list to ignore, add it to list and write to playthrough
if (std::find(buyIgnores.begin(), buyIgnores.end(), buyItem) == buyIgnores.end()) {
exclude = false;
buyIgnores.push_back(buyItem);
}
}
//Add all other advancement items
else if (!bombchus && type != ITEMTYPE_TOKEN && (AmmoDrops.Is(AMMODROPS_NONE) || type != ITEMTYPE_SHOP)) {
exclude = false;
}
//Has not been excluded, add to playthrough
if (!exclude) {
itemSphere.push_back(loc);
}
}
//Triforce has been found, seed is beatable, nothing else in this or future spheres matters
else if (location->GetPlaceduint32_t() == TRIFORCE) {
itemSphere.clear();
itemSphere.push_back(loc);
playthroughBeatable = true;
}
}
//All we care about is if the game is beatable, used to pare down playthrough
else if (location->GetPlaceduint32_t() == TRIFORCE && mode == SearchMode::CheckBeatable) {
playthroughBeatable = true;
return {}; //Return early for efficiency
}
}
}
}
}
if (mode == SearchMode::GeneratePlaythrough && itemSphere.size() > 0) {
playthroughLocations.push_back(itemSphere);
}
if (mode == SearchMode::GeneratePlaythrough && entranceSphere.size() > 0 && !noRandomEntrances) {
playthroughEntrances.push_back(entranceSphere);
}
}
//Check to see if all locations were reached
if (mode == SearchMode::AllLocationsReachable) {
allLocationsReachable = true;
for (const uint32_t loc : allLocations) {
if (!Location(loc)->IsAddedToPool()) {
allLocationsReachable = false;
auto message = "Location " + Location(loc)->GetName() + " not reachable\n";
SPDLOG_DEBUG(message);
#ifndef ENABLE_DEBUG
break;
#endif
}
}
return {};
}
erase_if(accessibleLocations, [&allowedLocations](uint32_t loc){
for (uint32_t allowedLocation : allowedLocations) {
if (loc == allowedLocation || Location(loc)->GetPlaceduint32_t() != NONE) {
return false;
}
}
return true;
});
return accessibleLocations;
}
static void GeneratePlaythrough() {
playthroughBeatable = false;
LogicReset();
GetAccessibleLocations(allLocations, SearchMode::GeneratePlaythrough);
}
//Remove unnecessary items from playthrough by removing their location, and checking if game is still beatable
//To reduce searches, some preprocessing is done in playthrough generation to avoid adding obviously unnecessary items
static void PareDownPlaythrough() {
std::vector<uint32_t> toAddBackItem;
//Start at sphere before Ganon's and count down
for (int i = playthroughLocations.size() - 2; i >= 0; i--) {
//Check each item location in sphere
std::vector<int> erasableIndices;
std::vector<uint32_t> sphere = playthroughLocations.at(i);
for (int j = sphere.size() - 1; j >= 0; j--) {
uint32_t loc = sphere.at(j);
uint32_t copy = Location(loc)->GetPlaceduint32_t(); //Copy out item
Location(loc)->SetPlacedItem(NONE); //Write in empty item
playthroughBeatable = false;
LogicReset();
std::string ignore = "";
if (ItemTable(copy).GetItemType() == ITEMTYPE_TOKEN) {
ignore = "Tokens";
}
else if (ItemTable(copy).GetName().GetEnglish().find("Bombchu") != std::string::npos) {
ignore = "Bombchus";
}
else if (ItemTable(copy).GetItemType() == ITEMTYPE_SHOP) {
ignore = GetShopItemBaseName(ItemTable(copy).GetName().GetEnglish());
}
GetAccessibleLocations(allLocations, SearchMode::CheckBeatable, ignore); //Check if game is still beatable
//Playthrough is still beatable without this item, therefore it can be removed from playthrough section.
if (playthroughBeatable) {
//Uncomment to print playthrough deletion log in citra
// std::string itemname(ItemTable(copy).GetName().GetEnglish());
// std::string locationname(Location(loc)->GetName());
// std::string removallog = itemname + " at " + locationname + " removed from playthrough";
// CitraPrint(removallog);
playthroughLocations[i].erase(playthroughLocations[i].begin() + j);
Location(loc)->SetDelayedItem(copy); //Game is still beatable, don't add back until later
toAddBackItem.push_back(loc);
}
else {
Location(loc)->SetPlacedItem(copy); //Immediately put item back so game is beatable again
}
}
}
//Some spheres may now be empty, remove these
for (int i = playthroughLocations.size() - 2; i >= 0; i--) {
if (playthroughLocations.at(i).size() == 0) {
playthroughLocations.erase(playthroughLocations.begin() + i);
}
}
//Now we can add back items which were removed previously
for (uint32_t loc : toAddBackItem) {
Location(loc)->SaveDelayedItem();
}
}
//Very similar to PareDownPlaythrough except it creates the list of Way of the Hero items
//Way of the Hero items are more specific than playthrough items in that they are items which *must*
// be obtained to logically be able to complete the seed, rather than playthrough items which
// are just possible items you *can* collect to complete the seed.
static void CalculateWotH() {
//First copy locations from the 2-dimensional playthroughLocations into the 1-dimensional wothLocations
//size - 1 so Triforce is not counted
for (size_t i = 0; i < playthroughLocations.size() - 1; i++) {
for (size_t j = 0; j < playthroughLocations[i].size(); j++) {
if (Location(playthroughLocations[i][j])->IsHintable()) {
wothLocations.push_back(playthroughLocations[i][j]);
}
}
}
//Now go through and check each location, seeing if it is strictly necessary for game completion
for (int i = wothLocations.size() - 1; i >= 0; i--) {
uint32_t loc = wothLocations[i];
uint32_t copy = Location(loc)->GetPlaceduint32_t(); //Copy out item
Location(loc)->SetPlacedItem(NONE); //Write in empty item
playthroughBeatable = false;
LogicReset();
GetAccessibleLocations(allLocations, SearchMode::CheckBeatable); //Check if game is still beatable
Location(loc)->SetPlacedItem(copy); //Immediately put item back
//If removing this item and no other item caused the game to become unbeatable, then it is strictly necessary, so keep it
//Else, delete from wothLocations
if (playthroughBeatable) {
wothLocations.erase(wothLocations.begin() + i);
}
}
playthroughBeatable = true;
LogicReset();
GetAccessibleLocations(allLocations);
}
//Will place things completely randomly, no logic checks are performed
static void FastFill(std::vector<uint32_t> items, std::vector<uint32_t> locations, bool endOnItemsEmpty = false) {
//Loop until locations are empty, or also end if items are empty and the parameters specify to end then
while (!locations.empty() && (!endOnItemsEmpty || !items.empty())) {
uint32_t loc = RandomElement(locations, true);
Location(loc)->SetAsHintable();
PlaceItemInLocation(loc, RandomElement(items, true));
if (items.empty() && !endOnItemsEmpty) {
items.push_back(GetJunkItem());
}
}
}
/*
| The algorithm places items in the world in reverse.
| This means we first assume we have every item in the item pool and
| remove an item and try to place it somewhere that is still reachable
| This method helps distribution of items locked behind many requirements.
| - OoT Randomizer
*/
static void AssumedFill(const std::vector<uint32_t>& items, const std::vector<uint32_t>& allowedLocations,
bool setLocationsAsHintable = false) {
if (items.size() > allowedLocations.size()) {
printf("\x1b[2;2HERROR: MORE ITEMS THAN LOCATIONS IN GIVEN LISTS");
SPDLOG_DEBUG("Items:\n");
for (const uint32_t item : items) {
SPDLOG_DEBUG("\t");
SPDLOG_DEBUG(ItemTable(item).GetName().GetEnglish());
SPDLOG_DEBUG("\n");
}
SPDLOG_DEBUG("\nAllowed Locations:\n");
for (const uint32_t loc : allowedLocations) {
SPDLOG_DEBUG("\t");
SPDLOG_DEBUG(Location(loc)->GetName());
SPDLOG_DEBUG("\n");
}
placementFailure = true;
return;
}
if (Settings::Logic.Is(LOGIC_NONE)) {
FastFill(items, GetEmptyLocations(allowedLocations), true);
return;
}
// keep retrying to place everything until it works or takes too long
int retries = 10;
bool unsuccessfulPlacement = false;
std::vector<uint32_t> attemptedLocations;
do {
retries--;
if (retries <= 0) {
placementFailure = true;
return;
}
unsuccessfulPlacement = false;
std::vector<uint32_t> itemsToPlace = items;
// copy all not yet placed advancement items so that we can apply their effects for the fill algorithm
std::vector<uint32_t> itemsToNotPlace =
FilterFromPool(ItemPool, [](const auto i) { return ItemTable(i).IsAdvancement(); });
// shuffle the order of items to place
Shuffle(itemsToPlace);
while (!itemsToPlace.empty()) {
uint32_t item = std::move(itemsToPlace.back());
ItemTable(item).SetAsPlaythrough();
itemsToPlace.pop_back();
// assume we have all unplaced items
LogicReset();
for (uint32_t unplacedItem : itemsToPlace) {
ItemTable(unplacedItem).ApplyEffect();
}
for (uint32_t unplacedItem : itemsToNotPlace) {
ItemTable(unplacedItem).ApplyEffect();
}
// get all accessible locations that are allowed
const std::vector<uint32_t> accessibleLocations = GetAccessibleLocations(allowedLocations);
// retry if there are no more locations to place items
if (accessibleLocations.empty()) {
SPDLOG_DEBUG("\nCANNOT PLACE ");
SPDLOG_DEBUG(ItemTable(item).GetName().GetEnglish());
SPDLOG_DEBUG(". TRYING AGAIN...\n");
#ifdef ENABLE_DEBUG
Areas::DumpWorldGraph(ItemTable(item).GetName().GetEnglish());
PlacementLog_Write();
#endif
// reset any locations that got an item
for (uint32_t loc : attemptedLocations) {
Location(loc)->SetPlacedItem(NONE);
itemsPlaced--;
}
attemptedLocations.clear();
unsuccessfulPlacement = true;
break;
}
// place the item within one of the allowed locations
uint32_t selectedLocation = RandomElement(accessibleLocations);
PlaceItemInLocation(selectedLocation, item);
attemptedLocations.push_back(selectedLocation);
// This tells us the location went through the randomization algorithm
// to distinguish it from locations which did not or that the player already
// knows
if (setLocationsAsHintable) {
Location(selectedLocation)->SetAsHintable();
}
// If ALR is off, then we check beatability after placing the item.
// If the game is beatable, then we can stop placing items with logic.
if (!LocationsReachable) {
playthroughBeatable = false;
LogicReset();
GetAccessibleLocations(allLocations, SearchMode::CheckBeatable);
if (playthroughBeatable) {
SPDLOG_DEBUG("Game beatable, now placing items randomly. " + std::to_string(itemsToPlace.size()) +
" major items remaining.\n\n");
FastFill(itemsToPlace, GetEmptyLocations(allowedLocations), true);
return;
}
}
}
} while (unsuccessfulPlacement);
}
//This function will specifically randomize dungeon rewards for the End of Dungeons
//setting, or randomize one dungeon reward to Link's Pocket if that setting is on
static void RandomizeDungeonRewards() {
//quest item bit mask of each stone/medallion for the savefile
static constexpr std::array<uint32_t, 9> bitMaskTable = {
0x00040000, //Kokiri Emerald
0x00080000, //Goron Ruby
0x00100000, //Zora Sapphire
0x00000001, //Forest Medallion
0x00000002, //Fire Medallion
0x00000004, //Water Medallion
0x00000008, //Spirit Medallion
0x00000010, //Shadow Medallion
0x00000020, //Light Medallion
};
int baseOffset = ItemTable(KOKIRI_EMERALD).GetItemID();
//End of Dungeons includes Link's Pocket
if (ShuffleRewards.Is(REWARDSHUFFLE_END_OF_DUNGEON)) {
//get stones and medallions
std::vector<uint32_t> rewards = FilterAndEraseFromPool(ItemPool, [](const auto i) {return ItemTable(i).GetItemType() == ITEMTYPE_DUNGEONREWARD;});
// If there are less than 9 dungeon rewards, prioritize the actual dungeons
// for placement instead of Link's Pocket
if (rewards.size() < 9) {
PlaceItemInLocation(LINKS_POCKET, GREEN_RUPEE);
}
if (Settings::Logic.Is(LOGIC_VANILLA)) { //Place dungeon rewards in vanilla locations
for (uint32_t loc : dungeonRewardLocations) {
Location(loc)->PlaceVanillaItem();
}
} else { //Randomize dungeon rewards with assumed fill
AssumedFill(rewards, dungeonRewardLocations);
}
for (size_t i = 0; i < dungeonRewardLocations.size(); i++) {
const auto index = Location(dungeonRewardLocations[i])->GetPlacedItem().GetItemID() - baseOffset;
rDungeonRewardOverrides[i] = index;
//set the player's dungeon reward on file creation instead of pushing it to them at the start.
//This is done mainly because players are already familiar with seeing their dungeon reward
//before opening up their file
if (i == dungeonRewardLocations.size()-1) {
LinksPocketRewardBitMask = bitMaskTable[index];
}
}
} else if (LinksPocketItem.Is(LINKSPOCKETITEM_DUNGEON_REWARD)) {
//get 1 stone/medallion
std::vector<uint32_t> rewards = FilterFromPool(ItemPool, [](const auto i) {return ItemTable(i).GetItemType() == ITEMTYPE_DUNGEONREWARD;});
// If there are no remaining stones/medallions, then Link's pocket won't get one
if (rewards.empty()) {
PlaceItemInLocation(LINKS_POCKET, GREEN_RUPEE);
return;
}
uint32_t startingReward = RandomElement(rewards, true);
LinksPocketRewardBitMask = bitMaskTable[ItemTable(startingReward).GetItemID() - baseOffset];
PlaceItemInLocation(LINKS_POCKET, startingReward);
//erase the stone/medallion from the Item Pool
FilterAndEraseFromPool(ItemPool, [startingReward](const uint32_t i) {return i == startingReward;});
}
}
//Fills any locations excluded by the player with junk items so that advancement items
//can't be placed there.
static void FillExcludedLocations() {
//Only fill in excluded locations that don't already have something and are forbidden
std::vector<uint32_t> excludedLocations = FilterFromPool(allLocations, [](const auto loc){
return Location(loc)->IsExcluded();
});
for (uint32_t loc : excludedLocations) {
PlaceJunkInExcludedLocation(loc);
}
}
//Function to handle the Own Dungeon setting
static void RandomizeOwnDungeon(const Dungeon::DungeonInfo* dungeon) {
std::vector<uint32_t> dungeonLocations = dungeon->GetDungeonLocations();
std::vector<uint32_t> dungeonItems;
//filter out locations that may be required to have songs placed at them
dungeonLocations = FilterFromPool(dungeonLocations, [](const auto loc){
if (ShuffleSongs.Is(SONGSHUFFLE_SONG_LOCATIONS)) {
return !(Location(loc)->IsCategory(Category::cSong));
}
if (ShuffleSongs.Is(SONGSHUFFLE_DUNGEON_REWARDS)) {
return !(Location(loc)->IsCategory(Category::cSongDungeonReward));
}
return true;
});
//Add specific items that need be randomized within this dungeon
if (Keysanity.Is(KEYSANITY_OWN_DUNGEON) && dungeon->GetSmallKey() != NONE) {
std::vector<uint32_t> dungeonSmallKeys = FilterAndEraseFromPool(ItemPool, [dungeon](const uint32_t i){ return i == dungeon->GetSmallKey();});
AddElementsToPool(dungeonItems, dungeonSmallKeys);
}
if ((BossKeysanity.Is(BOSSKEYSANITY_OWN_DUNGEON) && dungeon->GetBossKey() != GANONS_CASTLE_BOSS_KEY) ||
(GanonsBossKey.Is(GANONSBOSSKEY_OWN_DUNGEON) && dungeon->GetBossKey() == GANONS_CASTLE_BOSS_KEY)) {
auto dungeonBossKey = FilterAndEraseFromPool(ItemPool, [dungeon](const uint32_t i){ return i == dungeon->GetBossKey();});
AddElementsToPool(dungeonItems, dungeonBossKey);
}
//randomize boss key and small keys together for even distribution
AssumedFill(dungeonItems, dungeonLocations);
//randomize map and compass separately since they're not progressive
if (MapsAndCompasses.Is(MAPSANDCOMPASSES_OWN_DUNGEON) && dungeon->GetMap() != NONE && dungeon->GetCompass() != NONE) {
auto dungeonMapAndCompass = FilterAndEraseFromPool(ItemPool, [dungeon](const uint32_t i){ return i == dungeon->GetMap() || i == dungeon->GetCompass();});
AssumedFill(dungeonMapAndCompass, dungeonLocations);
}
}
/*Randomize items restricted to a certain set of locations.
The fill order of location groups is as follows:
- Own Dungeon
- Any Dungeon
- Overworld
Small Keys, Gerudo Keys, Boss Keys, Ganon's Boss Key, and/or dungeon rewards
will be randomized together if they have the same setting. Maps and Compasses
are randomized separately once the dungeon advancement items have all been placed.*/
static void RandomizeDungeonItems() {
using namespace Dungeon;
//Get Any Dungeon and Overworld group locations
std::vector<uint32_t> anyDungeonLocations = FilterFromPool(allLocations, [](const auto loc){return Location(loc)->IsDungeon();});
//overworldLocations defined in item_location.cpp
//Create Any Dungeon and Overworld item pools
std::vector<uint32_t> anyDungeonItems;
std::vector<uint32_t> overworldItems;
for (auto dungeon : dungeonList) {
if (Keysanity.Is(KEYSANITY_ANY_DUNGEON)) {
auto dungeonKeys = FilterAndEraseFromPool(ItemPool, [dungeon](const uint32_t i){return i == dungeon->GetSmallKey();});
AddElementsToPool(anyDungeonItems, dungeonKeys);
} else if (Keysanity.Is(KEYSANITY_OVERWORLD)) {
auto dungeonKeys = FilterAndEraseFromPool(ItemPool, [dungeon](const uint32_t i){return i == dungeon->GetSmallKey();});
AddElementsToPool(overworldItems, dungeonKeys);
}
if (BossKeysanity.Is(BOSSKEYSANITY_ANY_DUNGEON) && dungeon->GetBossKey() != GANONS_CASTLE_BOSS_KEY) {
auto bossKey = FilterAndEraseFromPool(ItemPool, [dungeon](const uint32_t i){return i == dungeon->GetBossKey();});
AddElementsToPool(anyDungeonItems, bossKey);
} else if (BossKeysanity.Is(BOSSKEYSANITY_OVERWORLD) && dungeon->GetBossKey() != GANONS_CASTLE_BOSS_KEY) {
auto bossKey = FilterAndEraseFromPool(ItemPool, [dungeon](const uint32_t i){return i == dungeon->GetBossKey();});
AddElementsToPool(overworldItems, bossKey);
}
if (GanonsBossKey.Is(GANONSBOSSKEY_ANY_DUNGEON)) {
auto ganonBossKey = FilterAndEraseFromPool(ItemPool, [](const auto i){return i == GANONS_CASTLE_BOSS_KEY;});
AddElementsToPool(anyDungeonItems, ganonBossKey);
} else if (GanonsBossKey.Is(GANONSBOSSKEY_OVERWORLD)) {
auto ganonBossKey = FilterAndEraseFromPool(ItemPool, [](const auto i) { return i == GANONS_CASTLE_BOSS_KEY; });
AddElementsToPool(overworldItems, ganonBossKey);
}
}
if (GerudoKeys.Is(GERUDOKEYS_ANY_DUNGEON)) {
auto gerudoKeys = FilterAndEraseFromPool(ItemPool, [](const auto i) { return i == GERUDO_FORTRESS_SMALL_KEY; });
AddElementsToPool(anyDungeonItems, gerudoKeys);
} else if (GerudoKeys.Is(GERUDOKEYS_OVERWORLD)) {
auto gerudoKeys = FilterAndEraseFromPool(ItemPool, [](const auto i) { return i == GERUDO_FORTRESS_SMALL_KEY; });
AddElementsToPool(overworldItems, gerudoKeys);
}
if (ShuffleRewards.Is(REWARDSHUFFLE_ANY_DUNGEON)) {
auto rewards = FilterAndEraseFromPool(
ItemPool, [](const auto i) { return ItemTable(i).GetItemType() == ITEMTYPE_DUNGEONREWARD; });
AddElementsToPool(anyDungeonItems, rewards);
} else if (ShuffleRewards.Is(REWARDSHUFFLE_OVERWORLD)) {
auto rewards = FilterAndEraseFromPool(
ItemPool, [](const auto i) { return ItemTable(i).GetItemType() == ITEMTYPE_DUNGEONREWARD; });
AddElementsToPool(overworldItems, rewards);
}
//Randomize Any Dungeon and Overworld pools
AssumedFill(anyDungeonItems, anyDungeonLocations, true);
AssumedFill(overworldItems, overworldLocations, true);
//Randomize maps and compasses after since they're not advancement items
for (auto dungeon : dungeonList) {
if (MapsAndCompasses.Is(MAPSANDCOMPASSES_ANY_DUNGEON)) {
auto mapAndCompassItems = FilterAndEraseFromPool(ItemPool, [dungeon](const uint32_t i){return i == dungeon->GetMap() || i == dungeon->GetCompass();});
AssumedFill(mapAndCompassItems, anyDungeonLocations, true);
} else if (MapsAndCompasses.Is(MAPSANDCOMPASSES_OVERWORLD)) {
auto mapAndCompassItems = FilterAndEraseFromPool(ItemPool, [dungeon](const uint32_t i){return i == dungeon->GetMap() || i == dungeon->GetCompass();});
AssumedFill(mapAndCompassItems, overworldLocations, true);
}
}
}
static void RandomizeLinksPocket() {
if (LinksPocketItem.Is(LINKSPOCKETITEM_ADVANCEMENT)) {
//Get all the advancement items don't include tokens
std::vector<uint32_t> advancementItems = FilterAndEraseFromPool(ItemPool, [](const auto i) {
return ItemTable(i).IsAdvancement() && ItemTable(i).GetItemType() != ITEMTYPE_TOKEN;
});
//select a random one
uint32_t startingItem = RandomElement(advancementItems, true);
//add the others back
AddElementsToPool(ItemPool, advancementItems);
PlaceItemInLocation(LINKS_POCKET, startingItem);
} else if (LinksPocketItem.Is(LINKSPOCKETITEM_NOTHING)) {
PlaceItemInLocation(LINKS_POCKET, GREEN_RUPEE);
}
}
void VanillaFill() {
//Perform minimum needed initialization
AreaTable_Init();
GenerateLocationPool();
GenerateItemPool();
GenerateStartingInventory();
//Place vanilla item in each location
RandomizeDungeonRewards();
for (uint32_t loc : allLocations) {
Location(loc)->PlaceVanillaItem();
}
//If necessary, handle ER stuff
if (ShuffleEntrances) {
printf("\x1b[7;10HShuffling Entrances...");
ShuffleAllEntrances();
printf("\x1b[7;32HDone");
}
//Finish up
CreateItemOverrides();
CreateEntranceOverrides();
CreateAlwaysIncludedMessages();
}
void ClearProgress() {
printf("\x1b[7;32H "); // Done
printf("\x1b[8;10H "); // Placing Items...Done
printf("\x1b[9;10H "); // Calculating Playthrough...Done
printf("\x1b[10;10H "); // Creating Hints...Done
printf("\x1b[11;10H "); // Writing Spoiler Log...Done
}
int Fill() {
int retries = 0;
while(retries < 5) {
placementFailure = false;
showItemProgress = false;
playthroughLocations.clear();
playthroughEntrances.clear();
wothLocations.clear();
AreaTable_Init(); //Reset the world graph to intialize the proper locations
ItemReset(); //Reset shops incase of shopsanity random
GenerateLocationPool();
GenerateItemPool();
GenerateStartingInventory();
RemoveStartingItemsFromPool();
FillExcludedLocations();
//Temporarily add shop items to the ItemPool so that entrance randomization
//can validate the world using deku/hylian shields
AddElementsToPool(ItemPool, GetMinVanillaShopItems(32)); //assume worst case shopsanity 4
if (ShuffleEntrances) {
printf("\x1b[7;10HShuffling Entrances");
if (ShuffleAllEntrances() == ENTRANCE_SHUFFLE_FAILURE) {
retries++;
ClearProgress();
continue;
}
printf("\x1b[7;32HDone");
}
//erase temporary shop items
FilterAndEraseFromPool(ItemPool, [](const auto item) { return ItemTable(item).GetItemType() == ITEMTYPE_SHOP; });
showItemProgress = true;
//Place shop items first, since a buy shield is needed to place a dungeon reward on Gohma due to access
NonShopItems = {};
if (Shopsanity.Is(SHOPSANITY_OFF)) {
PlaceVanillaShopItems(); //Place vanilla shop items in vanilla location
} else {
int total_replaced = 0;
if (Shopsanity.IsNot(SHOPSANITY_ZERO)) { //Shopsanity 1-4, random
//Initialize NonShopItems
ItemAndPrice init;
init.Name = Text{"No Item", "Sin objeto", "Pas d'objet"};
init.Price = -1;
init.Repurchaseable = false;
NonShopItems.assign(32, init);
//Indices from OoTR. So shopsanity one will overwrite 7, three will overwrite 7, 5, 8, etc.
const std::array<int, 4> indices = {7, 5, 8, 6};
//Overwrite appropriate number of shop items
for (size_t i = 0; i < ShopLocationLists.size(); i++) {
int num_to_replace = GetShopsanityReplaceAmount(); //1-4 shop items will be overwritten, depending on settings
total_replaced += num_to_replace;
for (int j = 0; j < num_to_replace; j++) {
int itemindex = indices[j];
int shopsanityPrice = GetRandomShopPrice();
NonShopItems[TransformShopIndex(i*8+itemindex-1)].Price = shopsanityPrice; //Set price to be retrieved by the patch and textboxes
Location(ShopLocationLists[i][itemindex - 1])->SetShopsanityPrice(shopsanityPrice);
}
}
}
//Get all locations and items that don't have a shopsanity price attached
std::vector<uint32_t> shopLocations = {};
//Get as many vanilla shop items as the total number of shop items minus the number of replaced items
//So shopsanity 0 will get all 64 vanilla items, shopsanity 4 will get 32, etc.
std::vector<uint32_t> shopItems = GetMinVanillaShopItems(total_replaced);
for (size_t i = 0; i < ShopLocationLists.size(); i++) {
for (size_t j = 0; j < ShopLocationLists[i].size(); j++) {
uint32_t loc = ShopLocationLists[i][j];
if (!(Location(loc)->HasShopsanityPrice())) {
shopLocations.push_back(loc);
}
}
}
//Place the shop items which will still be at shop locations
AssumedFill(shopItems, shopLocations);
}
//Place dungeon rewards
RandomizeDungeonRewards();
//Place dungeon items restricted to their Own Dungeon
for (auto dungeon : Dungeon::dungeonList) {
RandomizeOwnDungeon(dungeon);
}
//Then Place songs if song shuffle is set to specific locations
if (ShuffleSongs.IsNot(SONGSHUFFLE_ANYWHERE)) {
//Get each song
std::vector<uint32_t> songs =
FilterAndEraseFromPool(ItemPool, [](const auto i) { return ItemTable(i).GetItemType() == ITEMTYPE_SONG; });
//Get each song location
std::vector<uint32_t> songLocations;
if (ShuffleSongs.Is(SONGSHUFFLE_SONG_LOCATIONS)) {
songLocations =
FilterFromPool(allLocations, [](const auto loc) { return Location(loc)->IsCategory(Category::cSong); });
} else if (ShuffleSongs.Is(SONGSHUFFLE_DUNGEON_REWARDS)) {
songLocations = FilterFromPool(
allLocations, [](const auto loc) { return Location(loc)->IsCategory(Category::cSongDungeonReward); });
}
AssumedFill(songs, songLocations, true);
}
//Then place dungeon items that are assigned to restrictive location pools
RandomizeDungeonItems();
//Then place Link's Pocket Item if it has to be an advancement item
RandomizeLinksPocket();
//Then place the rest of the advancement items
std::vector<uint32_t> remainingAdvancementItems =
FilterAndEraseFromPool(ItemPool, [](const auto i) { return ItemTable(i).IsAdvancement(); });
AssumedFill(remainingAdvancementItems, allLocations, true);
//Fast fill for the rest of the pool
std::vector<uint32_t> remainingPool = FilterAndEraseFromPool(ItemPool, [](const auto i) { return true; });
FastFill(remainingPool, GetAllEmptyLocations(), false);
GeneratePlaythrough();
//Successful placement, produced beatable result
if(playthroughBeatable && !placementFailure) {
printf("Done");
printf("\x1b[9;10HCalculating Playthrough...");
PareDownPlaythrough();
CalculateWotH();
printf("Done");
CreateItemOverrides();
CreateEntranceOverrides();
CreateAlwaysIncludedMessages();
if (GossipStoneHints.IsNot(HINTS_NO_HINTS)) {
printf("\x1b[10;10HCreating Hints...");
CreateAllHints();
printf("Done");
}
if (ShuffleMerchants.Is(SHUFFLEMERCHANTS_HINTS)) {
CreateMerchantsHints();
}
return 1;
}
//Unsuccessful placement
if(retries < 4) {
SPDLOG_DEBUG("\nGOT STUCK. RETRYING...\n");
Areas::ResetAllLocations();
LogicReset();
ClearProgress();
}
retries++;
}
//All retries failed
return -1;
}