Files
Shiip-of-Hakinian-Espanol/soh/soh/Enhancements/randomizer/3drando/fill.cpp
Pepper0ni 0a84d15d9d Implement some basic benchmarking for seed generation (#4353)
* messy first implementation, pushing for verification

* push to test other platforms, add benchmark preset

* more other compiler fixes

* Finish implementing benchmarks

* forgot to reset the timers each run

* do it better

* move timers to thier own file

* forgot to add files
2024-09-21 17:03:48 -07:00

1351 lines
59 KiB
C++

#include "fill.hpp"
#include "custom_messages.hpp"
#include "../dungeon.h"
#include "../context.h"
#include "item_pool.hpp"
#include "location_access.hpp"
#include "random.hpp"
#include "spoiler_log.hpp"
#include "starting_inventory.hpp"
#include "hints.hpp"
#include "../entrance.h"
#include "shops.hpp"
#include "pool_functions.hpp"
//#include "debug.hpp"
#include "soh/Enhancements/randomizer/static_data.h"
#include "../../debugger/performanceTimer.h"
#include <vector>
#include <list>
#include <spdlog/spdlog.h>
using namespace CustomMessages;
using namespace Rando;
//RANDOTODO merge into Logic once Logic is a class passed to logic funtions
struct GetAccessableLocationsStruct {
std::vector<RandomizerCheck> accessibleLocations;
std::vector<RandomizerRegion> regionPool;
//Variables for playthrough
int gsCount;
int maxGsCount;
std::vector<LogicVal> buyIgnores;
//Variables for search
std::vector<Rando::ItemLocation*> newItemLocations;
bool updatedEvents;
bool ageTimePropogated;
bool firstIteration;
//Variables For Validating Entrences
bool haveTimeAccess;
bool foundTempleOfTime;
bool validatedStartingRegion;
bool sphereZeroComplete;
std::vector<RandomizerCheck> itemSphere;
std::list<Entrance*> entranceSphere;
GetAccessableLocationsStruct(int _maxGsCount){
regionPool = {RR_ROOT};
gsCount = 0;
maxGsCount = _maxGsCount;
updatedEvents = false;
ageTimePropogated = false;
firstIteration = true;
haveTimeAccess = false;
foundTempleOfTime = false;
validatedStartingRegion = false;
sphereZeroComplete = false;
}
void InitLoop(){
firstIteration = false;
ageTimePropogated = false;
updatedEvents = false;
for (Rando::ItemLocation* location : newItemLocations) {
location->ApplyPlacedItemEffect();
}
newItemLocations.clear();
itemSphere.clear();
entranceSphere.clear();
}
};
static bool placementFailure = false;
static void RemoveStartingItemsFromPool() {
for (RandomizerGet startingItem : StartingInventory) {
for (size_t i = 0; i < ItemPool.size(); i++) {
if (startingItem == RG_BIGGORON_SWORD) {
if (ItemPool[i] == RG_GIANTS_KNIFE || ItemPool[i] == RG_BIGGORON_SWORD) {
ItemPool[i] = GetJunkItem();
}
continue;
} else if (startingItem == ItemPool[i] || (Rando::StaticData::RetrieveItem(startingItem).IsBottleItem() &&
Rando::StaticData::RetrieveItem(ItemPool[i]).IsBottleItem())) {
if (AdditionalHeartContainers > 0 &&
(startingItem == RG_PIECE_OF_HEART || startingItem == RG_TREASURE_GAME_HEART)) {
ItemPool[i] = RG_HEART_CONTAINER;
AdditionalHeartContainers--;
} else {
ItemPool[i] = GetJunkItem();
}
break;
}
}
}
}
//This function will propagate Time of Day access through the entrance
static bool UpdateToDAccess(Entrance* entrance, bool propagateTimeTravel) {
StartPerformanceTimer(PT_TOD_ACCESS);
bool ageTimePropogated = false;
//propagate childDay, childNight, adultDay, and adultNight separately
Area* parent = entrance->GetParentRegion();
Area* connection = entrance->GetConnectedRegion();
if (!connection->childDay && parent->childDay && entrance->CheckConditionAtAgeTime(logic->IsChild, logic->AtDay)) {
connection->childDay = true;
ageTimePropogated = true;
}
if (!connection->childNight && parent->childNight && entrance->CheckConditionAtAgeTime(logic->IsChild, logic->AtNight)) {
connection->childNight = true;
ageTimePropogated = true;
}
if (!connection->adultDay && parent->adultDay && entrance->CheckConditionAtAgeTime(logic->IsAdult, logic->AtDay)) {
connection->adultDay = true;
ageTimePropogated = true;
}
if (!connection->adultNight && parent->adultNight && entrance->CheckConditionAtAgeTime(logic->IsAdult, logic->AtNight)) {
connection->adultNight = true;
ageTimePropogated = true;
}
//special check for temple of time
if (propagateTimeTravel && !AreaTable(RR_ROOT)->Adult() && AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->Child()) { //RANDOTODO: sphere weirdness, other age locations not propagated in this sphere
AreaTable(RR_ROOT)->adultDay = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->childDay;
AreaTable(RR_ROOT)->adultNight = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->childNight;
} else if (propagateTimeTravel && !AreaTable(RR_ROOT)->Child() && AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->Adult()){
AreaTable(RR_ROOT)->childDay = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->adultDay;
AreaTable(RR_ROOT)->childNight = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->adultNight;
}
StopPerformanceTimer(PT_TOD_ACCESS);
return ageTimePropogated;
}
// Check if key locations in the overworld are accessable
static void ValidateOtherEntrance(GetAccessableLocationsStruct& gals) {
auto ctx = Rando::Context::GetInstance();
// Condition for validating Temple of Time Access
if (!gals.foundTempleOfTime && ((ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD && AreaTable(RR_TEMPLE_OF_TIME)->Adult()) ||
(ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_ADULT && AreaTable(RR_TEMPLE_OF_TIME)->Child()))) {
gals.foundTempleOfTime = true;
}
// Condition for validating a valid starting region
if (!gals.validatedStartingRegion) {
bool childAccess = ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD || AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->Child();
bool adultAccess = ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_ADULT || AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->Adult();
Area* kokiri = AreaTable(RR_KOKIRI_FOREST);
Area* kakariko = AreaTable(RR_KAKARIKO_VILLAGE);
if ((childAccess && (kokiri->Child() || kakariko->Child())) ||// RANDOTODO when proper ammo logic is done, this could probably be made optional
(adultAccess && (kokiri->Adult() || kakariko->Adult()))) {
gals.validatedStartingRegion = true;
ApplyStartingInventory(); // RANDOTODO when proper ammo logic is done, this could be moved to the start
}
}
}
// Apply all items that are necessary for checking all location access
static void ApplyAllAdvancmentItems(){
std::vector<RandomizerGet> itemsToPlace =
FilterFromPool(ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).IsAdvancement(); });
for (RandomizerGet unplacedItem : itemsToPlace) {
Rando::StaticData::RetrieveItem(unplacedItem).ApplyEffect();
}
}
// Check if everything in an entrence rando seed that needs to be avalible without items, is,
// and if so allow obtaining items in logic
static void ValidateSphereZero(GetAccessableLocationsStruct& gals){
auto ctx = Rando::Context::GetInstance();
// Condition for verifying everything required for sphere 0, expanding search to all locations
if (logic->CanEmptyBigPoes && gals.validatedStartingRegion && gals.foundTempleOfTime && gals.haveTimeAccess) {
// Apply all items that are necessary for checking all location access
ApplyAllAdvancmentItems();
// Reset access as the non-starting age
if (ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD) {
for (RandomizerRegion regionKey : gals.regionPool) {
AreaTable(regionKey)->adultDay = false;
AreaTable(regionKey)->adultNight = false;
}
} else {
for (RandomizerRegion regionKey : gals.regionPool) {
AreaTable(regionKey)->childDay = false;
AreaTable(regionKey)->childNight = false;
}
}
gals.sphereZeroComplete = true;
}
}
//This function handles each possible exit
void ProcessExit(Entrance& exit, GetAccessableLocationsStruct& gals, bool propagateTimeTravel = true, bool checkPoeCollectorAccess = false, bool checkOtherEntranceAccess = false){
//Update Time of Day Access for the exit
if (UpdateToDAccess(&exit, propagateTimeTravel)) {
gals.ageTimePropogated = true;
if (checkOtherEntranceAccess){
ValidateOtherEntrance(gals);
}
if (!gals.sphereZeroComplete){
ValidateSphereZero(gals);
}
}
//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;
gals.regionPool.push_back(exit.GetConnectedRegionKey());
}
}
//Get the max number of tokens that can possibly be useful
static int GetMaxGSCount() {
auto ctx = Rando::Context::GetInstance();
//If bridge or LACS is set to tokens, get how many are required
int maxBridge = 0;
int maxLACS = 0;
if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_TOKENS)) {
maxBridge = ctx->GetOption(RSK_RAINBOW_BRIDGE_TOKEN_COUNT).Value<uint8_t>();
}
if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_LACS_TOKENS)) {
maxLACS = ctx->GetOption(RSK_LACS_TOKEN_COUNT).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 (ctx->GetItemLocation(RC_KAK_100_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() && ctx->GetItemLocation(RC_KAK_100_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 100;
} else if (ctx->GetItemLocation(RC_KAK_50_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() &&
ctx->GetItemLocation(RC_KAK_50_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 50;
} else if (ctx->GetItemLocation(RC_KAK_40_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() &&
ctx->GetItemLocation(RC_KAK_40_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 40;
} else if (ctx->GetItemLocation(RC_KAK_30_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() &&
ctx->GetItemLocation(RC_KAK_30_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 30;
} else if (ctx->GetItemLocation(RC_KAK_20_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() &&
ctx->GetItemLocation(RC_KAK_20_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 20;
} else if (ctx->GetItemLocation(RC_KAK_10_GOLD_SKULLTULA_REWARD)->GetPlacedItem().IsAdvancement() &&
ctx->GetItemLocation(RC_KAK_10_GOLD_SKULLTULA_REWARD)->GetPlacedItem().GetItemType() != ITEMTYPE_TOKEN) {
maxUseful = 10;
}
//Return max of the two possible reasons tokens could be important, minus the tokens in the starting inventory
return std::max(maxUseful, maxBridge) - ctx->GetOption(RSK_STARTING_SKULLTULA_TOKEN).Value<uint8_t>();
}
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<RandomizerCheck> GetEmptyLocations(std::vector<RandomizerCheck> targetLocations) {
auto ctx = Rando::Context::GetInstance();
return FilterFromPool(targetLocations, [ctx](const auto loc) {
return ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() == RG_NONE;
});
}
std::vector<RandomizerCheck> GetAllEmptyLocations() {
auto ctx = Rando::Context::GetInstance();
return FilterFromPool(ctx->allLocations, [ctx](const auto loc) {
return ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() == RG_NONE;
});
}
bool IsBombchus(RandomizerGet item, bool includeShops = false){
return (item >= RG_BOMBCHU_5 && item <= RG_BOMBCHU_20) || item == RG_PROGRESSIVE_BOMBCHUS ||
(includeShops && (item == RG_BUY_BOMBCHUS_10 || item == RG_BUY_BOMBCHUS_20));
}
bool IsBeatableWithout(RandomizerCheck excludedCheck, bool replaceItem, RandomizerGet ignore = RG_NONE){ //RANDOTODO make excludCheck an ItemLocation
auto ctx = Rando::Context::GetInstance();
RandomizerGet copy = ctx->GetItemLocation(excludedCheck)->GetPlacedRandomizerGet(); //Copy out item
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(RG_NONE); //Write in empty item
ctx->playthroughBeatable = false;
logic->Reset();
CheckBeatable(ignore);
if (replaceItem){
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(copy); //Immediately put item back
}
return ctx->playthroughBeatable;
}
// Reset non-Logic-class logic, and optionally apply the initial inventory
void ResetLogic(std::shared_ptr<Context>& ctx, bool applyInventory = false){
if (applyInventory){
ApplyStartingInventory();
}
Areas::AccessReset();
ctx->LocationReset();
}
//Generate the playthrough, so we want to add advancement items, unless we know to ignore them
void AddToPlaythrough(LocationAccess& locPair, GetAccessableLocationsStruct& gals){
auto ctx = Rando::Context::GetInstance();
RandomizerCheck loc = locPair.GetLocation();
Rando::ItemLocation* location = ctx->GetItemLocation(loc);
RandomizerGet locItem = location->GetPlacedRandomizerGet();
//Item is an advancement item, figure out if it should be added to this sphere
if (!ctx->playthroughBeatable && location->GetPlacedItem().IsAdvancement()) {
ItemType type = location->GetPlacedItem().GetItemType();
//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) 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 && gals.gsCount < gals.maxGsCount) {
gals.gsCount++;
exclude = false;
}
//Handle buy items
//If ammo drops are off, don't do this step, since buyable ammo becomes logically important
// TODO: Reimplement Ammo Drops setting
else if (/*AmmoDrops.IsNot(AMMODROPS_NONE) &&*/ type == ITEMTYPE_SHOP) {
//Only check each buy item once
auto buyItem = location->GetPlacedItem().GetLogicVal();
//Buy item not in list to ignore, add it to list and write to playthrough
if (std::find(gals.buyIgnores.begin(), gals.buyIgnores.end(), buyItem) == gals.buyIgnores.end()) {
exclude = false;
gals.buyIgnores.push_back(buyItem);
}
}
//Add all other advancement items
else if (type != ITEMTYPE_TOKEN && (/*AmmoDrops.Is(AMMODROPS_NONE) ||*/ type != ITEMTYPE_SHOP)) {
exclude = false;
}
//Has not been excluded, add to playthrough
if (!exclude) {
gals.itemSphere.push_back(loc);
}
}
//Triforce has been found, seed is beatable, nothing else in this or future spheres matters
else if (location->GetPlacedRandomizerGet() == RG_TRIFORCE) {
gals.itemSphere.clear();
gals.itemSphere.push_back(loc);
ctx->playthroughBeatable = true;
}
}
// Adds the contents of a location to the current progression and optionally playthrough
bool AddCheckToLogic(LocationAccess& locPair, GetAccessableLocationsStruct& gals, RandomizerGet ignore, bool stopOnBeatable, bool addToPlaythrough=false){
auto ctx = Rando::Context::GetInstance();
StartPerformanceTimer(PT_LOCATION_LOGIC);
RandomizerCheck loc = locPair.GetLocation();
Rando::ItemLocation* location = ctx->GetItemLocation(loc);
RandomizerGet locItem = location->GetPlacedRandomizerGet();
if (!location->IsAddedToPool() && locPair.ConditionsMet()) {
location->AddToPool();
if (locItem == RG_NONE) {
gals.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 != RG_NONE) {
ItemType type = location->GetPlacedItem().GetItemType();
//If we want to ignore tokens, only add if not a token
if (ignore == RG_GOLD_SKULLTULA_TOKEN && type != ITEMTYPE_TOKEN) {
gals.newItemLocations.push_back(location);
}
//If we want to ignore bombchus, only add if bombchu is not in the name
else if (IsBombchus(ignore) && IsBombchus(locItem, true)) {
gals.newItemLocations.push_back(location);
}
//We want to ignore a specific Buy item. Buy items with different RandomizerGets are recognised by a shared GetLogicVal
else if (ignore != RG_GOLD_SKULLTULA_TOKEN && IsBombchus(ignore)) {
if ((type == ITEMTYPE_SHOP && Rando::StaticData::GetItemTable()[ignore].GetLogicVal() != location->GetPlacedItem().GetLogicVal()) || type != ITEMTYPE_SHOP) {
gals.newItemLocations.push_back(location);
}
}
}
//If it doesn't, we can just add the location
else {
gals.newItemLocations.push_back(location); //Add item to cache to be considered in logic next iteration
}
}
if (addToPlaythrough){
AddToPlaythrough(locPair, gals);
}
//All we care about is if the game is beatable, used to pare down playthrough
if (location->GetPlacedRandomizerGet() == RG_TRIFORCE && stopOnBeatable) {
StopPerformanceTimer(PT_LOCATION_LOGIC);
return true; //Return early for efficiency
}
}
StopPerformanceTimer(PT_LOCATION_LOGIC);
return false;
}
// Return any of the targetLocations that are accessible in logic
std::vector<RandomizerCheck> ReachabilitySearch(const std::vector<RandomizerCheck>& targetLocations, RandomizerGet ignore /* = RG_NONE*/) {
auto ctx = Rando::Context::GetInstance();
GetAccessableLocationsStruct gals(0);
ResetLogic(ctx, true);
while (gals.newItemLocations.size() > 0 || gals.updatedEvents || gals.ageTimePropogated || gals.firstIteration) {
gals.InitLoop();
for (size_t i = 0; i < gals.regionPool.size(); i++) {
Area* region = AreaTable(gals.regionPool[i]);
if (region->UpdateEvents()){
gals.updatedEvents = true;
}
for (auto& exit : region->exits) {
ProcessExit(exit, gals);
}
for (size_t k = 0; k < region->locations.size(); k++) {
LocationAccess& locPair = region->locations[k];
AddCheckToLogic(locPair, gals, ignore, false);
}
}
}
erase_if(gals.accessibleLocations, [&targetLocations, ctx](RandomizerCheck loc){
for (RandomizerCheck allowedLocation : targetLocations) {
if (loc == allowedLocation || ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() != RG_NONE) {
return false;
}
}
return true;
});
return gals.accessibleLocations;
}
// Create the playthrough for the seed
void GeneratePlaythrough() {
auto ctx = Rando::Context::GetInstance();
ctx->playthroughBeatable = false;
logic->Reset();
GetAccessableLocationsStruct gals(GetMaxGSCount());
ResetLogic(ctx, true);
while (gals.newItemLocations.size() > 0 || gals.updatedEvents || gals.ageTimePropogated || gals.firstIteration) {
gals.InitLoop();
for (size_t i = 0; i < gals.regionPool.size(); i++) {
Area* region = AreaTable(gals.regionPool[i]);
if (region->UpdateEvents()){
gals.updatedEvents = true;
}
for (auto& exit : region->exits) {
ProcessExit(exit, gals);
// Add shuffled entrances to the entrance playthrough
// Include bluewarps when unshuffled but dungeon or boss shuffle is on
if((exit.IsShuffled() || (exit.GetType() == Rando::EntranceType::BlueWarp &&
(ctx->GetOption(RSK_SHUFFLE_DUNGEON_ENTRANCES) || ctx->GetOption(RSK_SHUFFLE_BOSS_ENTRANCES)))) &&
!exit.IsAddedToPool() && !ctx->GetEntranceShuffler()->HasNoRandomEntrances()) {
gals.entranceSphere.push_back(&exit);
exit.AddToPool();
// Don't list a two-way coupled entrance from both directions
if (exit.GetReverse() != nullptr && exit.GetReplacement()->GetReverse() != nullptr && !exit.IsDecoupled()) {
exit.GetReplacement()->GetReverse()->AddToPool();
}
}
}
for (size_t k = 0; k < region->locations.size(); k++) {
LocationAccess& locPair = region->locations[k];
AddCheckToLogic(locPair, gals, RG_NONE, false, true);
}
}
if (gals.itemSphere.size() > 0) {
ctx->playthroughLocations.push_back(gals.itemSphere);
}
if (gals.entranceSphere.size() > 0 && !ctx->GetEntranceShuffler()->HasNoRandomEntrances()) {
ctx->GetEntranceShuffler()->playthroughEntrances.push_back(gals.entranceSphere);
}
}
}
// return if the seed is currently beatable or not
bool CheckBeatable(RandomizerGet ignore /* = RG_NONE*/) {
auto ctx = Rando::Context::GetInstance();
GetAccessableLocationsStruct gals(0);
ResetLogic(ctx, true);
while (gals.newItemLocations.size() > 0 || gals.updatedEvents || gals.ageTimePropogated || gals.firstIteration) {
gals.InitLoop();
for (size_t i = 0; i < gals.regionPool.size(); i++) {
Area* region = AreaTable(gals.regionPool[i]);
if (region->UpdateEvents()){
gals.updatedEvents = true;
}
for (auto& exit : region->exits) {
ProcessExit(exit, gals);
}
for (size_t k = 0; k < region->locations.size(); k++) {
LocationAccess& locPair = region->locations[k];
if(AddCheckToLogic(locPair, gals, ignore, true)){
ctx->playthroughBeatable = true;
return true;
}
}
}
}
return false;
}
// Check if the currently randomised set of entrences is a valid game map.
void ValidateEntrances(bool checkPoeCollectorAccess, bool checkOtherEntranceAccess) {
auto ctx = Rando::Context::GetInstance();
GetAccessableLocationsStruct gals(0);
ResetLogic(ctx, !checkOtherEntranceAccess);
//Variables for Time Pass access
bool timePassChildDay = false;
bool timePassChildNight = false;
bool timePassAdultDay = false;
bool timePassAdultNight = false;
ctx->allLocationsReachable = false;
if (checkPoeCollectorAccess){
logic->CanEmptyBigPoes = false;
}
if (!checkOtherEntranceAccess){
gals.foundTempleOfTime = true;
gals.validatedStartingRegion = true;
gals.haveTimeAccess = true;
gals.sphereZeroComplete = true;
ApplyAllAdvancmentItems();
//if we assume valid sphere zero, we only want starting age access
if (ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD) {
for (RandomizerRegion regionKey : gals.regionPool) {
AreaTable(RR_ROOT)->childNight = true;
AreaTable(RR_ROOT)->childDay = true;
}
} else {
for (RandomizerRegion regionKey : gals.regionPool) {
AreaTable(RR_ROOT)->adultNight = true;
AreaTable(RR_ROOT)->adultDay = true;
}
}
} else {
AreaTable(RR_ROOT)->childNight = true;
AreaTable(RR_ROOT)->adultNight = true;
AreaTable(RR_ROOT)->childDay = true;
AreaTable(RR_ROOT)->adultDay = true;
}
while (gals.newItemLocations.size() > 0 || gals.updatedEvents || gals.ageTimePropogated || gals.firstIteration) {
gals.InitLoop();
for (size_t i = 0; i < gals.regionPool.size(); i++) {
Area* region = AreaTable(gals.regionPool[i]);
if (region->UpdateEvents(gals.haveTimeAccess)){
gals.updatedEvents = true;
}
// If we're checking for TimePass access do that for each region as it's being updated.
// TimePass Access is satisfied when every AgeTime can reach a region with TimePass
// without the aid of TimePass. During this mode, TimePass won't update ToD access
// in any region.
if (!gals.haveTimeAccess) {
if (region->timePass) {
if (region->childDay) {
timePassChildDay = true;
}
if (region->childNight) {
timePassChildNight = true;
}
if (region->adultDay) {
timePassAdultDay = true;
}
if (region->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) {
gals.haveTimeAccess = true;
}
}
//for each exit in this region
for (auto& exit : region->exits) {
ProcessExit(exit, gals, gals.haveTimeAccess && gals.foundTempleOfTime, checkPoeCollectorAccess, checkOtherEntranceAccess);
}
if (gals.sphereZeroComplete) {
for (size_t k = 0; k < region->locations.size(); k++) {
LocationAccess& locPair = region->locations[k];
AddCheckToLogic(locPair, gals, RG_NONE, false);
}
}
}
}
if (gals.sphereZeroComplete) {
ctx->allLocationsReachable = true;
for (const RandomizerCheck loc : ctx->allLocations) {
if (!ctx->GetItemLocation(loc)->IsAddedToPool()) {
ctx->allLocationsReachable = false;
auto message = "Location " +
Rando::StaticData::GetLocation(ctx->GetItemLocation(loc)->GetRandomizerCheck())->GetName() +
" not reachable\n";
SPDLOG_DEBUG(message);
#ifndef ENABLE_DEBUG
break;
#endif
}
}
}
}
RandomizerArea LookForExternalArea(const Area* const currentRegion, std::vector<RandomizerRegion> &alreadyChecked){
for (const auto& entrance : currentRegion->entrances) {
RandomizerArea otherArea = entrance->GetParentRegion()->GetArea();
const bool isAreaUnchecked = std::find(alreadyChecked.begin(), alreadyChecked.end(), entrance->GetParentRegionKey()) == alreadyChecked.end();
if (otherArea == RA_NONE && isAreaUnchecked) {
//if the region is in RA_NONE and hasn't already been checked, check it
alreadyChecked.push_back(entrance->GetParentRegionKey());
const RandomizerArea passdown = LookForExternalArea(entrance->GetParentRegion(), alreadyChecked);
if(passdown != RA_NONE){
return passdown;
}
} else if (otherArea != RA_LINKS_POCKET){ //if it's links pocket, do not propagate this, Link's Pocket is not a real Area
return otherArea;
}
}
return RA_NONE;
}
void SetAreas(){
auto ctx = Rando::Context::GetInstance();
//RANDOTODO give entrances an enum like RandomizerCheck, the give them all areas here, the use those areas to not need to recursivly find ItemLocation areas
for (int regionType = 0; regionType < RR_MARKER_AREAS_END; regionType++) {
Area region = areaTable[regionType];
RandomizerArea area = region.GetArea();
if (area == RA_NONE) {
std::vector<RandomizerRegion> alreadyChecked = {static_cast<RandomizerRegion>(regionType)};
area = LookForExternalArea(&region, alreadyChecked);
}
for (auto& loc : region.locations){
ctx->GetItemLocation(loc.GetLocation())->SetArea(area);
}
areaTable[regionType].SetArea(area);
}
}
//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() {
auto ctx = Rando::Context::GetInstance();
std::vector<RandomizerCheck> toAddBackItem;
//Start at sphere before Ganon's and count down
for (int i = ctx->playthroughLocations.size() - 2; i >= 0; i--) {
//Check each item location in sphere
std::vector<int> erasableIndices;
std::vector<RandomizerCheck> sphere = ctx->playthroughLocations.at(i);
for (int j = sphere.size() - 1; j >= 0; j--) {
RandomizerCheck loc = sphere.at(j);
RandomizerGet locGet = ctx->GetItemLocation(loc)->GetPlacedRandomizerGet(); //Copy out item
RandomizerGet ignore = RG_NONE;
if (locGet == RG_GOLD_SKULLTULA_TOKEN || IsBombchus(locGet, true)
|| Rando::StaticData::RetrieveItem(locGet).GetItemType() == ITEMTYPE_SHOP) {
ignore = locGet;
}
//Playthrough is still beatable without this item, therefore it can be removed from playthrough section.
if (IsBeatableWithout(loc, false, ignore)) {
ctx->playthroughLocations[i].erase(ctx->playthroughLocations[i].begin() + j);
ctx->GetItemLocation(loc)->SetDelayedItem(locGet); //Game is still beatable, don't add back until later
toAddBackItem.push_back(loc);
}
else {
ctx->GetItemLocation(loc)->SetPlacedItem(locGet); //Immediately put item back so game is beatable again
}
}
}
//Some spheres may now be empty, remove these
for (int i = ctx->playthroughLocations.size() - 2; i >= 0; i--) {
if (ctx->playthroughLocations.at(i).size() == 0) {
ctx->playthroughLocations.erase(ctx->playthroughLocations.begin() + i);
}
}
//Now we can add back items which were removed previously
for (RandomizerCheck loc : toAddBackItem) {
ctx->GetItemLocation(loc)->SaveDelayedItem();
}
}
//Very similar to PareDownPlaythrough except it sets WotH candidacy 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() {
auto ctx = Rando::Context::GetInstance();
//size - 1 so Triforce is not counted
for (size_t i = 0; i < ctx->playthroughLocations.size() - 1; i++) {
for (size_t j = 0; j < ctx->playthroughLocations[i].size(); j++) {
//If removing this item and no other item caused the game to become unbeatable, then it is strictly necessary, so add it
if (ctx->GetItemLocation(ctx->playthroughLocations[i][j])->IsHintable()
&& !(IsBeatableWithout(ctx->playthroughLocations[i][j], true))) {
ctx->GetItemLocation(ctx->playthroughLocations[i][j])->SetWothCandidate();
}
}
}
ctx->playthroughBeatable = true;
logic->Reset();
ReachabilitySearch(ctx->allLocations);
}
//Calculate barren locations and assign Barren Candidacy to all locations inside those areas
static void CalculateBarren() {
auto ctx = Rando::Context::GetInstance();
std::array<bool, RA_MAX> NotBarren = {}; //I would invert this but the "initialise all as true" syntax wasn't working
for (RandomizerCheck loc : ctx->allLocations) {
Rando::ItemLocation* itemLoc = ctx->GetItemLocation(loc);
RandomizerArea locArea = itemLoc->GetArea();
bool test = (itemLoc->GetPlacedItem().IsMajorItem() || itemLoc->IsWothCandidate());
// If a location has a major item or is a way of the hero location, it is not barren
if (NotBarren[locArea] == false && locArea > RA_LINKS_POCKET && (itemLoc->GetPlacedItem().IsMajorItem() || itemLoc->IsWothCandidate())) {
NotBarren[locArea] = true;
}
}
for (RandomizerCheck loc : ctx->allLocations) {
Rando::ItemLocation* itemLoc = ctx->GetItemLocation(loc);
if (!NotBarren[itemLoc->GetArea()]){
itemLoc->SetBarrenCandidate();
}
}
}
//Will place things completely randomly, no logic checks are performed
static void FastFill(std::vector<RandomizerGet> items, std::vector<RandomizerCheck> locations, bool endOnItemsEmpty = false) {
auto ctx = Rando::Context::GetInstance();
//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())) {
if (items.empty() && !endOnItemsEmpty) {
items.push_back(GetJunkItem());
}
RandomizerCheck loc = RandomElement(locations, true);
ctx->GetItemLocation(loc)->SetAsHintable();
ctx->PlaceItemInLocation(loc, RandomElement(items, true));
}
}
/*
| 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<RandomizerGet>& items, const std::vector<RandomizerCheck>& allowedLocations,
bool setLocationsAsHintable = false) {
auto ctx = Rando::Context::GetInstance();
if (items.size() > allowedLocations.size()) {
SPDLOG_ERROR("ERROR: MORE ITEMS THAN LOCATIONS IN GIVEN LISTS");
SPDLOG_DEBUG("Items:\n");
// NOLINTNEXTLINE(clang-diagnostic-unused-variable)
for (const RandomizerGet item : items) {
SPDLOG_DEBUG("\t");
SPDLOG_DEBUG(Rando::StaticData::RetrieveItem(item).GetName().GetEnglish());
SPDLOG_DEBUG("\n");
}
SPDLOG_DEBUG("\nAllowed Locations:\n");
// NOLINTNEXTLINE(clang-diagnostic-unused-variable)
for (const RandomizerCheck loc : allowedLocations) {
SPDLOG_DEBUG("\t");
SPDLOG_DEBUG(Rando::StaticData::GetLocation(loc)->GetName());
SPDLOG_DEBUG("\n");
}
placementFailure = true;
return;
}
if (ctx->GetOption(RSK_LOGIC_RULES).Is(RO_LOGIC_NO_LOGIC)) {
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<RandomizerCheck> attemptedLocations;
do {
retries--;
if (retries <= 0) {
placementFailure = true;
return;
}
unsuccessfulPlacement = false;
std::vector<RandomizerGet> itemsToPlace = items;
// copy all not yet placed advancement items so that we can apply their effects for the fill algorithm
std::vector<RandomizerGet> itemsToNotPlace =
FilterFromPool(ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).IsAdvancement(); });
// shuffle the order of items to place
Shuffle(itemsToPlace);
while (!itemsToPlace.empty()) {
RandomizerGet item = std::move(itemsToPlace.back());
Rando::StaticData::RetrieveItem(item).SetAsPlaythrough();
itemsToPlace.pop_back();
// assume we have all unplaced items
logic->Reset();
for (RandomizerGet unplacedItem : itemsToPlace) {
Rando::StaticData::RetrieveItem(unplacedItem).ApplyEffect();
}
for (RandomizerGet unplacedItem : itemsToNotPlace) {
Rando::StaticData::RetrieveItem(unplacedItem).ApplyEffect();
}
// get all accessible locations that are allowed
const std::vector<RandomizerCheck> accessibleLocations = ReachabilitySearch(allowedLocations);
// retry if there are no more locations to place items
if (accessibleLocations.empty()) {
SPDLOG_DEBUG("\nCANNOT PLACE ");
SPDLOG_DEBUG(Rando::StaticData::RetrieveItem(item).GetName().GetEnglish());
SPDLOG_DEBUG(". TRYING AGAIN...\n");
#ifdef ENABLE_DEBUG
Areas::DumpWorldGraph(Rando::StaticData::RetrieveItem(item).GetName().GetEnglish());
PlacementLog_Write();
#endif
// reset any locations that got an item
for (RandomizerCheck loc : attemptedLocations) {
ctx->GetItemLocation(loc)->SetPlacedItem(RG_NONE);
//itemsPlaced--;
}
attemptedLocations.clear();
unsuccessfulPlacement = true;
break;
}
// place the item within one of the allowed locations
RandomizerCheck selectedLocation = RandomElement(accessibleLocations);
ctx->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) {
ctx->GetItemLocation(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 (!ctx->GetOption(RSK_ALL_LOCATIONS_REACHABLE)) {
ctx->playthroughBeatable = false;
logic->Reset();
CheckBeatable();
if (ctx->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() {
auto ctx = Rando::Context::GetInstance();
std::array<uint32_t, 9> rDungeonRewardOverrides{};
//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 = Rando::StaticData::RetrieveItem(RG_KOKIRI_EMERALD).GetItemID();
//End of Dungeons includes Link's Pocket
if (ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_END_OF_DUNGEON)) {
//get stones and medallions
std::vector<RandomizerGet> rewards = FilterAndEraseFromPool(ItemPool, [](const auto i) {return Rando::StaticData::RetrieveItem(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) {
ctx->PlaceItemInLocation(RC_LINKS_POCKET, RG_GREEN_RUPEE);
}
if (ctx->GetOption(RSK_LOGIC_RULES).Is(RO_LOGIC_VANILLA)) { //Place dungeon rewards in vanilla locations
for (RandomizerCheck loc : Rando::StaticData::dungeonRewardLocations) {
ctx->GetItemLocation(loc)->PlaceVanillaItem();
}
} else { //Randomize dungeon rewards with assumed fill
AssumedFill(rewards, Rando::StaticData::dungeonRewardLocations);
}
for (size_t i = 0; i < Rando::StaticData::dungeonRewardLocations.size(); i++) {
const auto index = ctx->GetItemLocation(Rando::StaticData::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 == Rando::StaticData::dungeonRewardLocations.size()-1) {
// LinksPocketRewardBitMask = bitMaskTable[index];
// }
}
} else if (ctx->GetOption(RSK_LINKS_POCKET).Is(RO_LINKS_POCKET_DUNGEON_REWARD)) {
//get 1 stone/medallion
std::vector<RandomizerGet> rewards = FilterFromPool(
ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).GetItemType() == ITEMTYPE_DUNGEONREWARD; });
// If there are no remaining stones/medallions, then Link's pocket won't get one
if (rewards.empty()) {
ctx->PlaceItemInLocation(RC_LINKS_POCKET, RG_GREEN_RUPEE);
return;
}
RandomizerGet startingReward = RandomElement(rewards, true);
//LinksPocketRewardBitMask = bitMaskTable[Rando::StaticData::RetrieveItem(startingReward).GetItemID() - baseOffset];
ctx->PlaceItemInLocation(RC_LINKS_POCKET, startingReward);
//erase the stone/medallion from the Item Pool
FilterAndEraseFromPool(ItemPool, [startingReward](const RandomizerGet 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() {
auto ctx = Rando::Context::GetInstance();
//Only fill in excluded locations that don't already have something and are forbidden
std::vector<RandomizerCheck> excludedLocations = FilterFromPool(ctx->allLocations, [ctx](const auto loc){
return ctx->GetItemLocation(loc)->IsExcluded();
});
for (RandomizerCheck loc : excludedLocations) {
PlaceJunkInExcludedLocation(loc);
}
}
//Function to handle the Own Dungeon setting
static void RandomizeOwnDungeon(const Rando::DungeonInfo* dungeon) {
auto ctx = Rando::Context::GetInstance();
std::vector<RandomizerGet> dungeonItems;
// Search and filter for locations that match the hint region of the dungeon
// This accounts for boss room shuffle so that own dungeon items can be placed
// in the shuffled boss room
std::vector<RandomizerCheck> dungeonLocations = FilterFromPool(ctx->allLocations, [dungeon, ctx](const auto loc) {
return ctx->GetItemLocation(loc)->GetArea() == dungeon->GetArea();
});
//filter out locations that may be required to have songs placed at them
dungeonLocations = FilterFromPool(dungeonLocations, [ctx](const auto loc){
if (ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_SONG_LOCATIONS)) {
return !(Rando::StaticData::GetLocation(loc)->IsCategory(Category::cSong));
}
if (ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_SONG_LOCATIONS)) {
return !(Rando::StaticData::GetLocation(loc)->IsCategory(Category::cSongDungeonReward));
}
return true;
});
//Add specific items that need be randomized within this dungeon
if (ctx->GetOption(RSK_KEYSANITY).Is(RO_DUNGEON_ITEM_LOC_OWN_DUNGEON) && dungeon->GetSmallKey() != RG_NONE) {
std::vector<RandomizerGet> dungeonSmallKeys = FilterAndEraseFromPool(ItemPool, [dungeon](const RandomizerGet i){ return (i == dungeon->GetSmallKey()) || (i == dungeon->GetKeyRing());});
AddElementsToPool(dungeonItems, dungeonSmallKeys);
}
if ((ctx->GetOption(RSK_BOSS_KEYSANITY).Is(RO_DUNGEON_ITEM_LOC_OWN_DUNGEON) && dungeon->GetBossKey() != RG_GANONS_CASTLE_BOSS_KEY) ||
(ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_OWN_DUNGEON) && dungeon->GetBossKey() == RG_GANONS_CASTLE_BOSS_KEY)) {
auto dungeonBossKey = FilterAndEraseFromPool(ItemPool, [dungeon](const RandomizerGet 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 (ctx->GetOption(RSK_SHUFFLE_MAPANDCOMPASS).Is(RO_DUNGEON_ITEM_LOC_OWN_DUNGEON) && dungeon->GetMap() != RG_NONE && dungeon->GetCompass() != RG_NONE) {
auto dungeonMapAndCompass = FilterAndEraseFromPool(ItemPool, [dungeon](const RandomizerGet 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() {
auto ctx = Rando::Context::GetInstance();
//Get Any Dungeon and Overworld group locations
std::vector<RandomizerCheck> anyDungeonLocations = FilterFromPool(ctx->allLocations, [](const auto loc){return Rando::StaticData::GetLocation(loc)->IsDungeon();});
//overworldLocations defined in item_location.cpp
//Create Any Dungeon and Overworld item pools
std::vector<RandomizerGet> anyDungeonItems;
std::vector<RandomizerGet> overworldItems;
for (auto dungeon : ctx->GetDungeons()->GetDungeonList()) {
if (ctx->GetOption(RSK_KEYSANITY).Is(RO_DUNGEON_ITEM_LOC_ANY_DUNGEON)) {
auto dungeonKeys = FilterAndEraseFromPool(ItemPool, [dungeon](const RandomizerGet i){return (i == dungeon->GetSmallKey()) || (i == dungeon->GetKeyRing());});
AddElementsToPool(anyDungeonItems, dungeonKeys);
} else if (ctx->GetOption(RSK_KEYSANITY).Is(RO_DUNGEON_ITEM_LOC_OVERWORLD)) {
auto dungeonKeys = FilterAndEraseFromPool(ItemPool, [dungeon](const RandomizerGet i){return (i == dungeon->GetSmallKey()) || (i == dungeon->GetKeyRing());});
AddElementsToPool(overworldItems, dungeonKeys);
}
if (ctx->GetOption(RSK_BOSS_KEYSANITY).Is(RO_DUNGEON_ITEM_LOC_ANY_DUNGEON) && dungeon->GetBossKey() != RG_GANONS_CASTLE_BOSS_KEY) {
auto bossKey = FilterAndEraseFromPool(ItemPool, [dungeon](const RandomizerGet i){return i == dungeon->GetBossKey();});
AddElementsToPool(anyDungeonItems, bossKey);
} else if (ctx->GetOption(RSK_BOSS_KEYSANITY).Is(RO_DUNGEON_ITEM_LOC_OVERWORLD) && dungeon->GetBossKey() != RG_GANONS_CASTLE_BOSS_KEY) {
auto bossKey = FilterAndEraseFromPool(ItemPool, [dungeon](const RandomizerGet i){return i == dungeon->GetBossKey();});
AddElementsToPool(overworldItems, bossKey);
}
if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_ANY_DUNGEON)) {
auto ganonBossKey = FilterAndEraseFromPool(ItemPool, [](const auto i){return i == RG_GANONS_CASTLE_BOSS_KEY;});
AddElementsToPool(anyDungeonItems, ganonBossKey);
} else if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_OVERWORLD)) {
auto ganonBossKey = FilterAndEraseFromPool(ItemPool, [](const auto i) { return i == RG_GANONS_CASTLE_BOSS_KEY; });
AddElementsToPool(overworldItems, ganonBossKey);
}
}
if (ctx->GetOption(RSK_GERUDO_KEYS).Is(RO_GERUDO_KEYS_ANY_DUNGEON)) {
auto gerudoKeys = FilterAndEraseFromPool(ItemPool, [](const auto i) { return i == RG_GERUDO_FORTRESS_SMALL_KEY || i == RG_GERUDO_FORTRESS_KEY_RING; });
AddElementsToPool(anyDungeonItems, gerudoKeys);
} else if (ctx->GetOption(RSK_GERUDO_KEYS).Is(RO_GERUDO_KEYS_OVERWORLD)) {
auto gerudoKeys = FilterAndEraseFromPool(ItemPool, [](const auto i) { return i == RG_GERUDO_FORTRESS_SMALL_KEY || i == RG_GERUDO_FORTRESS_KEY_RING; });
AddElementsToPool(overworldItems, gerudoKeys);
}
if (ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_ANY_DUNGEON)) {
auto rewards = FilterAndEraseFromPool(
ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).GetItemType() == ITEMTYPE_DUNGEONREWARD; });
AddElementsToPool(anyDungeonItems, rewards);
} else if (ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_OVERWORLD)) {
auto rewards = FilterAndEraseFromPool(
ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).GetItemType() == ITEMTYPE_DUNGEONREWARD; });
AddElementsToPool(overworldItems, rewards);
}
//Randomize Any Dungeon and Overworld pools
AssumedFill(anyDungeonItems, anyDungeonLocations, true);
AssumedFill(overworldItems, Rando::StaticData::overworldLocations, true);
//Randomize maps and compasses after since they're not advancement items
for (auto dungeon : ctx->GetDungeons()->GetDungeonList()) {
if (ctx->GetOption(RSK_SHUFFLE_MAPANDCOMPASS).Is(RO_DUNGEON_ITEM_LOC_ANY_DUNGEON)) {
auto mapAndCompassItems = FilterAndEraseFromPool(ItemPool, [dungeon](const RandomizerGet i){return i == dungeon->GetMap() || i == dungeon->GetCompass();});
AssumedFill(mapAndCompassItems, anyDungeonLocations, true);
} else if (ctx->GetOption(RSK_SHUFFLE_MAPANDCOMPASS).Is(RO_DUNGEON_ITEM_LOC_OVERWORLD)) {
auto mapAndCompassItems = FilterAndEraseFromPool(ItemPool, [dungeon](const RandomizerGet i){return i == dungeon->GetMap() || i == dungeon->GetCompass();});
AssumedFill(mapAndCompassItems, Rando::StaticData::overworldLocations, true);
}
}
}
static void RandomizeLinksPocket() {
auto ctx = Rando::Context::GetInstance();
if (ctx->GetOption(RSK_LINKS_POCKET).Is(RO_LINKS_POCKET_ADVANCEMENT)) {
//Get all the advancement items don't include tokens
std::vector<RandomizerGet> advancementItems = FilterAndEraseFromPool(ItemPool, [](const auto i) {
return Rando::StaticData::RetrieveItem(i).IsAdvancement() && Rando::StaticData::RetrieveItem(i).GetItemType() != ITEMTYPE_TOKEN;
});
//select a random one
RandomizerGet startingItem = RandomElement(advancementItems, true);
//add the others back
AddElementsToPool(ItemPool, advancementItems);
ctx->PlaceItemInLocation(RC_LINKS_POCKET, startingItem);
} else if (ctx->GetOption(RSK_LINKS_POCKET).Is(RO_LINKS_POCKET_NOTHING)) {
ctx->PlaceItemInLocation(RC_LINKS_POCKET, RG_GREEN_RUPEE);
}
}
void VanillaFill() {
auto ctx = Rando::Context::GetInstance();
//Perform minimum needed initialization
AreaTable_Init();
ctx->GenerateLocationPool();
GenerateItemPool();
GenerateStartingInventory();
//Place vanilla item in each location
RandomizeDungeonRewards();
for (RandomizerCheck loc : ctx->allLocations) {
ctx->GetItemLocation(loc)->PlaceVanillaItem();
}
//If necessary, handle ER stuff
if (ctx->GetOption(RSK_SHUFFLE_ENTRANCES)) {
SPDLOG_INFO("Shuffling Entrances...");
ctx->GetEntranceShuffler()->ShuffleAllEntrances();
SPDLOG_INFO("Shuffling Entrances Done");
}
// Populate the playthrough for entrances so they are placed in the spoiler log
GeneratePlaythrough();
//Finish up
ctx->CreateItemOverrides();
ctx->GetEntranceShuffler()->CreateEntranceOverrides();
CreateWarpSongTexts();
}
void ClearProgress() {
}
int Fill() {
auto ctx = Rando::Context::GetInstance();
int retries = 0;
SPDLOG_INFO("Starting seed generation...");
while(retries < 5) {
SPDLOG_INFO("Attempt {}...", retries + 1);
placementFailure = false;
//showItemProgress = false;
ctx->playthroughLocations.clear();
ctx->GetEntranceShuffler()->playthroughEntrances.clear();
AreaTable_Init(); //Reset the world graph to intialize the proper locations
ctx->ItemReset(); //Reset shops incase of shopsanity random
ctx->GenerateLocationPool();
GenerateItemPool();
GenerateStartingInventory();
RemoveStartingItemsFromPool();
FillExcludedLocations();
//Temporarily add shop items to the ItemPool so that entrance randomization
//can validate the world using deku/hylian shields
StartPerformanceTimer(PT_ENTRANCE_SHUFFLE);
AddElementsToPool(ItemPool, GetMinVanillaShopItems(32)); //assume worst case shopsanity 4
if (ctx->GetOption(RSK_SHUFFLE_ENTRANCES)) {
SPDLOG_INFO("Shuffling Entrances...");
if (ctx->GetEntranceShuffler()->ShuffleAllEntrances() == ENTRANCE_SHUFFLE_FAILURE) {
retries++;
ClearProgress();
continue;
}
SPDLOG_INFO("Shuffling Entrances Done");
}
SetAreas();
//erase temporary shop items
FilterAndEraseFromPool(ItemPool, [](const auto item) { return Rando::StaticData::RetrieveItem(item).GetItemType() == ITEMTYPE_SHOP; });
StopPerformanceTimer(PT_ENTRANCE_SHUFFLE);
//ctx->showItemProgress = true;
//Place shop items first, since a buy shield is needed to place a dungeon reward on Gohma due to access
StartPerformanceTimer(PT_SHOPSANITY);
NonShopItems = {};
if (ctx->GetOption(RSK_SHOPSANITY).Is(RO_SHOPSANITY_OFF)) {
SPDLOG_INFO("Placing Vanilla Shop Items...");
PlaceVanillaShopItems(); //Place vanilla shop items in vanilla location
} else {
SPDLOG_INFO("Shuffling Shop Items");
int total_replaced = 0;
if (ctx->GetOption(RSK_SHOPSANITY).IsNot(RO_SHOPSANITY_ZERO_ITEMS)) { //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 < Rando::StaticData::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
ctx->GetItemLocation(Rando::StaticData::shopLocationLists[i][itemindex - 1])->SetCustomPrice(shopsanityPrice);
}
}
}
//Get all locations and items that don't have a shopsanity price attached
std::vector<RandomizerCheck> 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<RandomizerGet> shopItems = GetMinVanillaShopItems(total_replaced);
for (size_t i = 0; i < Rando::StaticData::shopLocationLists.size(); i++) {
for (size_t j = 0; j < Rando::StaticData::shopLocationLists[i].size(); j++) {
RandomizerCheck loc = Rando::StaticData::shopLocationLists[i][j];
if (!(ctx->GetItemLocation(loc)->HasCustomPrice())) {
shopLocations.push_back(loc);
}
}
}
//Place the shop items which will still be at shop locations
AssumedFill(shopItems, shopLocations);
}
StopPerformanceTimer(PT_SHOPSANITY);
StartPerformanceTimer(PT_OWN_DUNGEON);
//Place dungeon rewards
SPDLOG_INFO("Shuffling and Placing Dungeon Items...");
RandomizeDungeonRewards();
//Place dungeon items restricted to their Own Dungeon
for (auto dungeon : ctx->GetDungeons()->GetDungeonList()) {
RandomizeOwnDungeon(dungeon);
}
StopPerformanceTimer(PT_OWN_DUNGEON);
StartPerformanceTimer(PT_LIMITED_CHECKS);
//Then Place songs if song shuffle is set to specific locations
if (ctx->GetOption(RSK_SHUFFLE_SONGS).IsNot(RO_SONG_SHUFFLE_ANYWHERE)) {
//Get each song
std::vector<RandomizerGet> songs = FilterAndEraseFromPool(
ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).GetItemType() == ITEMTYPE_SONG; });
//Get each song location
std::vector<RandomizerCheck> songLocations;
if (ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_SONG_LOCATIONS)) {
songLocations = FilterFromPool(
ctx->allLocations, [](const auto loc) { return Rando::StaticData::GetLocation(loc)->IsCategory(Category::cSong); });
} else if (ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_DUNGEON_REWARDS)) {
songLocations = FilterFromPool(ctx->allLocations, [](const auto loc) {
return Rando::StaticData::GetLocation(loc)->IsCategory(Category::cSongDungeonReward);
});
}
AssumedFill(songs, songLocations, true);
}
//Then place dungeon items that are assigned to restrictive location pools
RandomizeDungeonItems();
SPDLOG_INFO("Dungeon Items Done");
//Then place Link's Pocket Item if it has to be an advancement item
RandomizeLinksPocket();
StopPerformanceTimer(PT_LIMITED_CHECKS);
StartPerformanceTimer(PT_ADVANCEMENT_ITEMS);
SPDLOG_INFO("Shuffling Advancement Items");
//Then place the rest of the advancement items
std::vector<RandomizerGet> remainingAdvancementItems =
FilterAndEraseFromPool(ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).IsAdvancement(); });
AssumedFill(remainingAdvancementItems, ctx->allLocations, true);
StopPerformanceTimer(PT_ADVANCEMENT_ITEMS);
StartPerformanceTimer(PT_REMAINING_ITEMS);
//Fast fill for the rest of the pool
SPDLOG_INFO("Shuffling Remaining Items");
std::vector<RandomizerGet> remainingPool = FilterAndEraseFromPool(ItemPool, [](const auto i) { return true; });
FastFill(remainingPool, GetAllEmptyLocations(), false);
StopPerformanceTimer(PT_REMAINING_ITEMS);
//Add default prices to scrubs
for (size_t i = 0; i < Rando::StaticData::scrubLocations.size(); i++) {
if (Rando::StaticData::scrubLocations[i] == RC_LW_DEKU_SCRUB_NEAR_BRIDGE || Rando::StaticData::scrubLocations[i] == RC_LW_DEKU_SCRUB_GROTTO_FRONT) {
ctx->GetItemLocation(Rando::StaticData::scrubLocations[i])->SetCustomPrice(40);
} else if (Rando::StaticData::scrubLocations[i] == RC_HF_DEKU_SCRUB_GROTTO) {
ctx->GetItemLocation(Rando::StaticData::scrubLocations[i])->SetCustomPrice(10);
} else {
auto loc = Rando::StaticData::GetLocation(Rando::StaticData::scrubLocations[i]);
auto item = Rando::StaticData::RetrieveItem(loc->GetVanillaItem());
ctx->GetItemLocation(Rando::StaticData::scrubLocations[i])->SetCustomPrice(item.GetPrice());
}
}
if (ctx->GetOption(RSK_SHUFFLE_SCRUBS).Is(RO_SCRUBS_AFFORDABLE)) {
for (size_t i = 0; i < Rando::StaticData::scrubLocations.size(); i++) {
ctx->GetItemLocation(Rando::StaticData::scrubLocations[i])->SetCustomPrice(10);
}
} else if (ctx->GetOption(RSK_SHUFFLE_SCRUBS).Is(RO_SCRUBS_RANDOM)) {
for (size_t i = 0; i < Rando::StaticData::scrubLocations.size(); i++) {
int randomPrice = GetRandomScrubPrice();
ctx->GetItemLocation(Rando::StaticData::scrubLocations[i])->SetCustomPrice(randomPrice);
}
}
StartPerformanceTimer(PT_PLAYTHROUGH_GENERATION);
GeneratePlaythrough();
StopPerformanceTimer(PT_PLAYTHROUGH_GENERATION);
//Successful placement, produced beatable result
if(ctx->playthroughBeatable && !placementFailure) {
SPDLOG_INFO("Calculating Playthrough...");
StartPerformanceTimer(PT_PARE_DOWN_PLAYTHROUGH);
PareDownPlaythrough();
StopPerformanceTimer(PT_PARE_DOWN_PLAYTHROUGH);
StartPerformanceTimer(PT_WOTH);
CalculateWotH();
StopPerformanceTimer(PT_WOTH);
StartPerformanceTimer(PT_FOOLISH);
CalculateBarren();
StopPerformanceTimer(PT_FOOLISH);
SPDLOG_INFO("Calculating Playthrough Done");
StartPerformanceTimer(PT_OVERRIDES);
ctx->CreateItemOverrides();
ctx->GetEntranceShuffler()->CreateEntranceOverrides();
StopPerformanceTimer(PT_OVERRIDES);
StartPerformanceTimer(PT_HINTS);
CreateAllHints();
CreateWarpSongTexts();
StopPerformanceTimer(PT_HINTS);
return 1;
}
//Unsuccessful placement
if(retries < 4) {
SPDLOG_DEBUG("Failed to generate a beatable seed. Retrying...");
Areas::ResetAllLocations();
logic->Reset();
ClearProgress();
}
retries++;
}
//All retries failed
return -1;
}