Refactor GetAccessibleLocations (#3871)

* saving for branch change

* V0 doesn't work

* crashing in random places halp

* push to rebase

* commit for branch change

* more branch switching

* First apparent working

* fix entrence validation

* comment cleanups

* post merge fixes

* Fix entrences not validating when spawns/owl drops are on but other entrences are not

* remove bombchusFound from the struct too

* Fix issue causing improper bombchu filtering on the playthrough

* text fixes

* submodules pls

* submodules pls pt 2
This commit is contained in:
Pepper0ni 2024-09-17 18:06:30 +01:00 committed by GitHub
parent 6cd387ddf3
commit cd92e70b84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 423 additions and 291 deletions

View File

@ -22,6 +22,57 @@
using namespace CustomMessages; using namespace CustomMessages;
using namespace Rando; 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 bool placementFailure = false;
static void RemoveStartingItemsFromPool() { static void RemoveStartingItemsFromPool() {
@ -47,12 +98,12 @@ static void RemoveStartingItemsFromPool() {
} }
} }
//This function will propogate Time of Day access through the entrance //This function will propagate Time of Day access through the entrance
static bool UpdateToDAccess(Entrance* entrance, SearchMode mode) { static bool UpdateToDAccess(Entrance* entrance, bool propagateTimeTravel) {
bool ageTimePropogated = false; bool ageTimePropogated = false;
//propogate childDay, childNight, adultDay, and adultNight separately //propagate childDay, childNight, adultDay, and adultNight separately
Area* parent = entrance->GetParentRegion(); Area* parent = entrance->GetParentRegion();
Area* connection = entrance->GetConnectedRegion(); Area* connection = entrance->GetConnectedRegion();
@ -74,11 +125,10 @@ static bool UpdateToDAccess(Entrance* entrance, SearchMode mode) {
} }
//special check for temple of time //special check for temple of time
bool propogateTimeTravel = mode != SearchMode::TimePassAccess && mode != SearchMode::TempleOfTimeAccess; 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
if (!AreaTable(RR_ROOT)->Adult() && AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->Child() && propogateTimeTravel) {
AreaTable(RR_ROOT)->adultDay = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->childDay; AreaTable(RR_ROOT)->adultDay = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->childDay;
AreaTable(RR_ROOT)->adultNight = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->childNight; AreaTable(RR_ROOT)->adultNight = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->childNight;
} else if (!AreaTable(RR_ROOT)->Child() && AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->Adult() && propogateTimeTravel){ } 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)->childDay = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->adultDay;
AreaTable(RR_ROOT)->childNight = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->adultNight; AreaTable(RR_ROOT)->childNight = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->adultNight;
} }
@ -86,55 +136,85 @@ static bool UpdateToDAccess(Entrance* entrance, SearchMode mode) {
return ageTimePropogated; return ageTimePropogated;
} }
// Various checks that need to pass for the world to be validated as completable // Check if key locations in the overworld are accessable
static void ValidateWorldChecks(SearchMode& mode, bool checkPoeCollectorAccess, bool checkOtherEntranceAccess, std::vector<RandomizerRegion>& areaPool) { static void ValidateOtherEntrance(GetAccessableLocationsStruct& gals) {
auto ctx = Rando::Context::GetInstance(); auto ctx = Rando::Context::GetInstance();
// Condition for validating Temple of Time Access // Condition for validating Temple of Time Access
if (mode == SearchMode::TempleOfTimeAccess && ((ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD && AreaTable(RR_TEMPLE_OF_TIME)->Adult()) || (ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_ADULT && AreaTable(RR_TEMPLE_OF_TIME)->Child()) || !checkOtherEntranceAccess)) { if (!gals.foundTempleOfTime && ((ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD && AreaTable(RR_TEMPLE_OF_TIME)->Adult()) ||
mode = SearchMode::ValidStartingRegion; (ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_ADULT && AreaTable(RR_TEMPLE_OF_TIME)->Child()))) {
gals.foundTempleOfTime = true;
} }
// Condition for validating a valid starting region // Condition for validating a valid starting region
if (mode == SearchMode::ValidStartingRegion) { if (!gals.validatedStartingRegion) {
bool childAccess = ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD || AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->Child(); 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(); bool adultAccess = ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_ADULT || AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->Adult();
Area* kokiri = AreaTable(RR_KOKIRI_FOREST); Area* kokiri = AreaTable(RR_KOKIRI_FOREST);
Area* kakariko = AreaTable(RR_KAKARIKO_VILLAGE); Area* kakariko = AreaTable(RR_KAKARIKO_VILLAGE);
if ((childAccess && (kokiri->Child() || kakariko->Child())) || if ((childAccess && (kokiri->Child() || kakariko->Child())) ||// RANDOTODO when proper ammo logic is done, this could probably be made optional
(adultAccess && (kokiri->Adult() || kakariko->Adult())) || (adultAccess && (kokiri->Adult() || kakariko->Adult()))) {
!checkOtherEntranceAccess) { gals.validatedStartingRegion = true;
mode = SearchMode::PoeCollectorAccess; ApplyStartingInventory(); // RANDOTODO when proper ammo logic is done, this could be moved to the start
ApplyStartingInventory();
logic->NoBottles = true;
} }
} }
// Condition for validating Poe Collector Access }
if (mode == SearchMode::PoeCollectorAccess && (AreaTable(RR_MARKET_GUARD_HOUSE)->Adult() || !checkPoeCollectorAccess)) {
// Apply all items that are necessary for checking all location access // Apply all items that are necessary for checking all location access
static void ApplyAllAdvancmentItems(){
std::vector<RandomizerGet> itemsToPlace = std::vector<RandomizerGet> itemsToPlace =
FilterFromPool(ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).IsAdvancement(); }); FilterFromPool(ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).IsAdvancement(); });
for (RandomizerGet unplacedItem : itemsToPlace) { for (RandomizerGet unplacedItem : itemsToPlace) {
Rando::StaticData::RetrieveItem(unplacedItem).ApplyEffect(); 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 // Reset access as the non-starting age
if (ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD) { if (ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD) {
for (RandomizerRegion areaKey : areaPool) { for (RandomizerRegion regionKey : gals.regionPool) {
AreaTable(areaKey)->adultDay = false; AreaTable(regionKey)->adultDay = false;
AreaTable(areaKey)->adultNight = false; AreaTable(regionKey)->adultNight = false;
} }
} else { } else {
for (RandomizerRegion areaKey : areaPool) { for (RandomizerRegion regionKey : gals.regionPool) {
AreaTable(areaKey)->childDay = false; AreaTable(regionKey)->childDay = false;
AreaTable(areaKey)->childNight = false; AreaTable(regionKey)->childNight = false;
} }
} }
mode = SearchMode::AllLocationsReachable; gals.sphereZeroComplete = true;
} else {
logic->NoBottles = false;
} }
} }
//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 //Get the max number of tokens that can possibly be useful
static int GetMaxGSCount() { static int GetMaxGSCount() {
auto ctx = Rando::Context::GetInstance(); auto ctx = Rando::Context::GetInstance();
@ -186,9 +266,9 @@ std::string GetShopItemBaseName(std::string itemName) {
return baseName; return baseName;
} }
std::vector<RandomizerCheck> GetEmptyLocations(std::vector<RandomizerCheck> allowedLocations) { std::vector<RandomizerCheck> GetEmptyLocations(std::vector<RandomizerCheck> targetLocations) {
auto ctx = Rando::Context::GetInstance(); auto ctx = Rando::Context::GetInstance();
return FilterFromPool(allowedLocations, [ctx](const auto loc) { return FilterFromPool(targetLocations, [ctx](const auto loc) {
return ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() == RG_NONE; return ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() == RG_NONE;
}); });
} }
@ -211,175 +291,28 @@ bool IsBeatableWithout(RandomizerCheck excludedCheck, bool replaceItem, Randomiz
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(RG_NONE); //Write in empty item ctx->GetItemLocation(excludedCheck)->SetPlacedItem(RG_NONE); //Write in empty item
ctx->playthroughBeatable = false; ctx->playthroughBeatable = false;
logic->Reset(); logic->Reset();
GetAccessibleLocations(ctx->allLocations, SearchMode::CheckBeatable, ignore); //Check if game is still beatable CheckBeatable(ignore);
if (replaceItem){ if (replaceItem){
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(copy); //Immediately put item back ctx->GetItemLocation(excludedCheck)->SetPlacedItem(copy); //Immediately put item back
} }
return ctx->playthroughBeatable; return ctx->playthroughBeatable;
} }
//This function will return a vector of ItemLocations that are accessible with // Reset non-Logic-class logic, and optionally apply the initial inventory
//where items have been placed so far within the world. The allowedLocations argument void ResetLogic(std::shared_ptr<Context>& ctx, bool applyInventory = false){
//specifies the pool of locations that we're trying to search for an accessible location in if (applyInventory){
std::vector<RandomizerCheck> GetAccessibleLocations(const std::vector<RandomizerCheck>& allowedLocations, SearchMode mode /* = SearchMode::ReachabilitySearch*/, RandomizerGet ignore /* = RG_NONE*/, bool checkPoeCollectorAccess /*= false*/, bool checkOtherEntranceAccess /*= false*/) {
auto ctx = Rando::Context::GetInstance();
std::vector<RandomizerCheck> accessibleLocations;
// Reset all access to begin a new search
if (mode < SearchMode::ValidateWorld) {
ApplyStartingInventory(); ApplyStartingInventory();
} }
Areas::AccessReset(); Areas::AccessReset();
ctx->LocationReset(); ctx->LocationReset();
std::vector<RandomizerRegion> areaPool = {RR_ROOT};
if (mode == SearchMode::ValidateWorld) {
mode = SearchMode::TimePassAccess;
AreaTable(RR_ROOT)->childNight = true;
AreaTable(RR_ROOT)->adultNight = true;
AreaTable(RR_ROOT)->childDay = true;
AreaTable(RR_ROOT)->adultDay = true;
ctx->allLocationsReachable = false;
} }
//Variables for playthrough //Generate the playthrough, so we want to add advancement items, unless we know to ignore them
int gsCount = 0; void AddToPlaythrough(LocationAccess& locPair, GetAccessableLocationsStruct& gals){
const int maxGsCount = mode == SearchMode::GeneratePlaythrough ? GetMaxGSCount() : 0; //If generating playthrough want the max that's possibly useful, else doesn't matter auto ctx = Rando::Context::GetInstance();
std::vector<LogicVal> buyIgnores;
//Variables for search
std::vector<Rando::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 (Rando::ItemLocation* location : newItemLocations) {
location->ApplyPlacedItemEffect();
}
newItemLocations.clear();
std::vector<RandomizerCheck> 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.GetConnectedRegionKey());
}
// Add shuffled entrances to the entrance playthrough
// Include bluewarps when unshuffled but dungeon or boss shuffle is on
if (mode == SearchMode::GeneratePlaythrough &&
(exit.IsShuffled() ||
(exit.GetType() == Rando::EntranceType::BlueWarp &&
(ctx->GetOption(RSK_SHUFFLE_DUNGEON_ENTRANCES) || ctx->GetOption(RSK_SHUFFLE_BOSS_ENTRANCES)))) &&
!exit.IsAddedToPool() && !ctx->GetEntranceShuffler()->HasNoRandomEntrances()) {
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 each ItemLocation in this area
if (mode < SearchMode::ValidateWorld) {
for (size_t k = 0; k < area->locations.size(); k++) {
LocationAccess& locPair = area->locations[k];
RandomizerCheck loc = locPair.GetLocation(); RandomizerCheck loc = locPair.GetLocation();
Rando::ItemLocation* location = ctx->GetItemLocation(loc); Rando::ItemLocation* location = ctx->GetItemLocation(loc);
RandomizerGet locItem = location->GetPlacedRandomizerGet(); RandomizerGet locItem = location->GetPlacedRandomizerGet();
if (!location->IsAddedToPool() && locPair.ConditionsMet()) {
location->AddToPool();
if (locItem == RG_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 != 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) {
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)) {
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) {
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 //Item is an advancement item, figure out if it should be added to this sphere
if (!ctx->playthroughBeatable && location->GetPlacedItem().IsAdvancement()) { if (!ctx->playthroughBeatable && location->GetPlacedItem().IsAdvancement()) {
ItemType type = location->GetPlacedItem().GetItemType(); ItemType type = location->GetPlacedItem().GetItemType();
@ -391,11 +324,10 @@ std::vector<RandomizerCheck> GetAccessibleLocations(const std::vector<Randomizer
//2) Buy items of the same type, after the first (So only see Buy Deku Nut of any amount once) //2) Buy items of the same type, after the first (So only see Buy Deku Nut of any amount once)
bool exclude = true; bool exclude = true;
//Exclude tokens after the last possibly useful one //Exclude tokens after the last possibly useful one
if (type == ITEMTYPE_TOKEN && gsCount < maxGsCount) { if (type == ITEMTYPE_TOKEN && gals.gsCount < gals.maxGsCount) {
gsCount++; gals.gsCount++;
exclude = false; exclude = false;
} }
//Handle buy items //Handle buy items
//If ammo drops are off, don't do this step, since buyable ammo becomes logically important //If ammo drops are off, don't do this step, since buyable ammo becomes logically important
// TODO: Reimplement Ammo Drops setting // TODO: Reimplement Ammo Drops setting
@ -403,9 +335,9 @@ std::vector<RandomizerCheck> GetAccessibleLocations(const std::vector<Randomizer
//Only check each buy item once //Only check each buy item once
auto buyItem = location->GetPlacedItem().GetLogicVal(); auto buyItem = location->GetPlacedItem().GetLogicVal();
//Buy item not in list to ignore, add it to list and write to playthrough //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()) { if (std::find(gals.buyIgnores.begin(), gals.buyIgnores.end(), buyItem) == gals.buyIgnores.end()) {
exclude = false; exclude = false;
buyIgnores.push_back(buyItem); gals.buyIgnores.push_back(buyItem);
} }
} }
//Add all other advancement items //Add all other advancement items
@ -414,36 +346,258 @@ std::vector<RandomizerCheck> GetAccessibleLocations(const std::vector<Randomizer
} }
//Has not been excluded, add to playthrough //Has not been excluded, add to playthrough
if (!exclude) { if (!exclude) {
itemSphere.push_back(loc); gals.itemSphere.push_back(loc);
} }
} }
//Triforce has been found, seed is beatable, nothing else in this or future spheres matters //Triforce has been found, seed is beatable, nothing else in this or future spheres matters
else if (location->GetPlacedRandomizerGet() == RG_TRIFORCE) { else if (location->GetPlacedRandomizerGet() == RG_TRIFORCE) {
itemSphere.clear(); gals.itemSphere.clear();
itemSphere.push_back(loc); gals.itemSphere.push_back(loc);
ctx->playthroughBeatable = true; 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();
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 //All we care about is if the game is beatable, used to pare down playthrough
else if (location->GetPlacedRandomizerGet() == RG_TRIFORCE && mode == SearchMode::CheckBeatable) { if (location->GetPlacedRandomizerGet() == RG_TRIFORCE && stopOnBeatable) {
return true; //Return early for efficiency
}
}
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; ctx->playthroughBeatable = true;
return {}; //Return early for efficiency 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;
}
} }
if (mode == SearchMode::GeneratePlaythrough && itemSphere.size() > 0) { //for each exit in this region
ctx->playthroughLocations.push_back(itemSphere); for (auto& exit : region->exits) {
ProcessExit(exit, gals, gals.haveTimeAccess && gals.foundTempleOfTime, checkPoeCollectorAccess, checkOtherEntranceAccess);
} }
if (mode == SearchMode::GeneratePlaythrough && entranceSphere.size() > 0 && !ctx->GetEntranceShuffler()->HasNoRandomEntrances()) { if (gals.sphereZeroComplete) {
ctx->GetEntranceShuffler()->playthroughEntrances.push_back(entranceSphere); for (size_t k = 0; k < region->locations.size(); k++) {
LocationAccess& locPair = region->locations[k];
AddCheckToLogic(locPair, gals, RG_NONE, false);
} }
} }
}
//Check to see if all locations were reached }
if (mode == SearchMode::AllLocationsReachable) { if (gals.sphereZeroComplete) {
ctx->allLocationsReachable = true; ctx->allLocationsReachable = true;
for (const RandomizerCheck loc : ctx->allLocations) { for (const RandomizerCheck loc : ctx->allLocations) {
if (!ctx->GetItemLocation(loc)->IsAddedToPool()) { if (!ctx->GetItemLocation(loc)->IsAddedToPool()) {
@ -457,25 +611,7 @@ std::vector<RandomizerCheck> GetAccessibleLocations(const std::vector<Randomizer
#endif #endif
} }
} }
return {};
} }
erase_if(accessibleLocations, [&allowedLocations, ctx](RandomizerCheck loc){
for (RandomizerCheck allowedLocation : allowedLocations) {
if (loc == allowedLocation || ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() != RG_NONE) {
return false;
}
}
return true;
});
return accessibleLocations;
}
static void GeneratePlaythrough() {
auto ctx = Rando::Context::GetInstance();
ctx->playthroughBeatable = false;
logic->Reset();
GetAccessibleLocations(ctx->allLocations, SearchMode::GeneratePlaythrough);
} }
RandomizerArea LookForExternalArea(const Area* const currentRegion, std::vector<RandomizerRegion> &alreadyChecked){ RandomizerArea LookForExternalArea(const Area* const currentRegion, std::vector<RandomizerRegion> &alreadyChecked){
@ -489,7 +625,7 @@ RandomizerArea LookForExternalArea(const Area* const currentRegion, std::vector<
if(passdown != RA_NONE){ if(passdown != RA_NONE){
return passdown; return passdown;
} }
} else if (otherArea != RA_LINKS_POCKET){ //if it's links pocket, do not propogate this, Link's Pocket is not a real Area } 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 otherArea;
} }
} }
@ -576,7 +712,7 @@ static void CalculateWotH() {
} }
ctx->playthroughBeatable = true; ctx->playthroughBeatable = true;
logic->Reset(); logic->Reset();
GetAccessibleLocations(ctx->allLocations); ReachabilitySearch(ctx->allLocations);
} }
//Calculate barren locations and assign Barren Candidacy to all locations inside those areas //Calculate barren locations and assign Barren Candidacy to all locations inside those areas
@ -687,7 +823,7 @@ static void AssumedFill(const std::vector<RandomizerGet>& items, const std::vect
} }
// get all accessible locations that are allowed // get all accessible locations that are allowed
const std::vector<RandomizerCheck> accessibleLocations = GetAccessibleLocations(allowedLocations); const std::vector<RandomizerCheck> accessibleLocations = ReachabilitySearch(allowedLocations);
// retry if there are no more locations to place items // retry if there are no more locations to place items
if (accessibleLocations.empty()) { if (accessibleLocations.empty()) {
@ -729,7 +865,7 @@ static void AssumedFill(const std::vector<RandomizerGet>& items, const std::vect
if (!ctx->GetOption(RSK_ALL_LOCATIONS_REACHABLE)) { if (!ctx->GetOption(RSK_ALL_LOCATIONS_REACHABLE)) {
ctx->playthroughBeatable = false; ctx->playthroughBeatable = false;
logic->Reset(); logic->Reset();
GetAccessibleLocations(ctx->allLocations, SearchMode::CheckBeatable); CheckBeatable();
if (ctx->playthroughBeatable) { if (ctx->playthroughBeatable) {
SPDLOG_DEBUG("Game beatable, now placing items randomly. " + std::to_string(itemsToPlace.size()) + SPDLOG_DEBUG("Game beatable, now placing items randomly. " + std::to_string(itemsToPlace.size()) +
" major items remaining.\n\n"); " major items remaining.\n\n");

View File

@ -5,25 +5,16 @@
#include <vector> #include <vector>
#include <string> #include <string>
enum class SearchMode {
ReachabilitySearch,
GeneratePlaythrough,
CheckBeatable,
AllLocationsReachable,
ValidateWorld,
TimePassAccess,
TempleOfTimeAccess,
ValidStartingRegion,
PoeCollectorAccess,
};
void ClearProgress(); void ClearProgress();
void VanillaFill(); void VanillaFill();
int Fill(); int Fill();
std::vector<RandomizerCheck> GetEmptyLocations(std::vector<RandomizerCheck> allowedLocations); std::vector<RandomizerCheck> GetEmptyLocations(std::vector<RandomizerCheck> allowedLocations);
std::vector<RandomizerCheck> GetAccessibleLocations(const std::vector<RandomizerCheck>& allowedLocations, std::vector<RandomizerCheck> ReachabilitySearch(const std::vector<RandomizerCheck>& allowedLocations, RandomizerGet ignore=RG_NONE);
SearchMode mode = SearchMode::ReachabilitySearch, RandomizerGet ignore = RG_NONE,
bool checkPoeCollectorAccess = false, void GeneratePlaythrough();
bool checkOtherEntranceAccess = false);
bool CheckBeatable(RandomizerGet ignore=RG_NONE);
void ValidateEntrances(bool checkPoeCollectorAccess, bool checkOtherEntranceAccess);

View File

@ -328,7 +328,7 @@ static std::vector<RandomizerCheck> GetAccessibleGossipStones(const RandomizerCh
ctx->GetItemLocation(hintedLocation)->SetPlacedItem(RG_NONE); ctx->GetItemLocation(hintedLocation)->SetPlacedItem(RG_NONE);
ctx->GetLogic()->Reset(); ctx->GetLogic()->Reset();
auto accessibleGossipStones = GetAccessibleLocations(Rando::StaticData::gossipStoneLocations); auto accessibleGossipStones = ReachabilitySearch(Rando::StaticData::gossipStoneLocations);
//Give the item back to the location //Give the item back to the location
ctx->GetItemLocation(hintedLocation)->SetPlacedItem(originalItem); ctx->GetItemLocation(hintedLocation)->SetPlacedItem(originalItem);
@ -342,7 +342,7 @@ bool IsReachableWithout(std::vector<RandomizerCheck> locsToCheck, RandomizerChec
RandomizerGet originalItem = ctx->GetItemLocation(excludedCheck)->GetPlacedRandomizerGet(); RandomizerGet originalItem = ctx->GetItemLocation(excludedCheck)->GetPlacedRandomizerGet();
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(RG_NONE); ctx->GetItemLocation(excludedCheck)->SetPlacedItem(RG_NONE);
ctx->GetLogic()->Reset(); ctx->GetLogic()->Reset();
const auto rechableWithout = GetAccessibleLocations(locsToCheck); const auto rechableWithout = ReachabilitySearch(locsToCheck);
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(originalItem); ctx->GetItemLocation(excludedCheck)->SetPlacedItem(originalItem);
if (resetAfter){ if (resetAfter){
//if resetAfter is on, reset logic we are done //if resetAfter is on, reset logic we are done
@ -670,7 +670,7 @@ void CreateStoneHints() {
//Getting gossip stone locations temporarily sets one location to not be reachable. //Getting gossip stone locations temporarily sets one location to not be reachable.
//Call the function one last time to get rid of false positives on locations not //Call the function one last time to get rid of false positives on locations not
//being reachable. //being reachable.
GetAccessibleLocations({}); ReachabilitySearch({});
} }
std::vector<RandomizerCheck> FindItemsAndMarkHinted(std::vector<RandomizerGet> items, std::vector<RandomizerCheck> hintChecks){ std::vector<RandomizerCheck> FindItemsAndMarkHinted(std::vector<RandomizerGet> items, std::vector<RandomizerCheck> hintChecks){

View File

@ -89,9 +89,8 @@ Area::Area(std::string regionName_, std::string scene_, RandomizerArea area,
Area::~Area() = default; Area::~Area() = default;
bool Area::UpdateEvents(SearchMode mode) { bool Area::UpdateEvents(bool haveTimeAccess) {
if (timePass && haveTimeAccess) {
if (timePass && mode != SearchMode::TimePassAccess) {
if (Child()) { if (Child()) {
childDay = true; childDay = true;
childNight = true; childNight = true;
@ -241,7 +240,7 @@ std::shared_ptr<Rando::Logic> logic;
void AreaTable_Init() { void AreaTable_Init() {
using namespace Rando; using namespace Rando;
randoCtx = Context::GetInstance().get(); randoCtx = Context::GetInstance().get();
logic = randoCtx->GetLogic(); logic = randoCtx->GetLogic(); //RANDOTODO do not hardcode, instead allow accepting a Logic class somehow
grottoEvents = { grottoEvents = {
EventAccess(&logic->GossipStoneFairy, { [] { return logic->GossipStoneFairy || logic->CanSummonGossipFairy; } }), EventAccess(&logic->GossipStoneFairy, { [] { return logic->GossipStoneFairy || logic->CanSummonGossipFairy; } }),
EventAccess(&logic->ButterflyFairy, { [] { return logic->ButterflyFairy || (logic->CanUse(RG_STICKS)); } }), EventAccess(&logic->ButterflyFairy, { [] { return logic->ButterflyFairy || (logic->CanUse(RG_STICKS)); } }),

View File

@ -164,9 +164,9 @@ public:
bool childNight = false; bool childNight = false;
bool adultDay = false; bool adultDay = false;
bool adultNight = false; bool adultNight = false;
bool addedToPool = false; bool addedToPool = false;;
bool UpdateEvents(SearchMode mode); bool UpdateEvents(bool haveTimeAccess = true);
void AddExit(RandomizerRegion parentKey, RandomizerRegion newExitKey, ConditionFn condition); void AddExit(RandomizerRegion parentKey, RandomizerRegion newExitKey, ConditionFn condition);

View File

@ -165,7 +165,10 @@ void AreaTable_Init_CastleTown() {
Entrance(RR_GANONS_CASTLE_ENTRYWAY, {[]{return logic->IsAdult;}}), Entrance(RR_GANONS_CASTLE_ENTRYWAY, {[]{return logic->IsAdult;}}),
}); });
areaTable[RR_MARKET_GUARD_HOUSE] = Area("Market Guard House", "Market Guard House", RA_NONE, NO_DAY_NIGHT_CYCLE, {}, { areaTable[RR_MARKET_GUARD_HOUSE] = Area("Market Guard House", "Market Guard House", RA_NONE, NO_DAY_NIGHT_CYCLE, {
//Events
EventAccess(&logic->CanEmptyBigPoes, {[]{return logic->IsAdult;}}),
}, {
//Locations //Locations
LOCATION(RC_MARKET_10_BIG_POES, logic->IsAdult && logic->BigPoeKill), LOCATION(RC_MARKET_10_BIG_POES, logic->IsAdult && logic->BigPoeKill),
LOCATION(RC_MARKET_GS_GUARD_HOUSE, logic->IsChild), LOCATION(RC_MARKET_GS_GUARD_HOUSE, logic->IsChild),

View File

@ -462,7 +462,7 @@ static bool ValidateWorld(Entrance* entrancePlaced) {
// Conditions will be checked during the search and any that fail will be figured out // Conditions will be checked during the search and any that fail will be figured out
// afterwards // afterwards
ctx->GetLogic()->Reset(); ctx->GetLogic()->Reset();
GetAccessibleLocations({}, SearchMode::ValidateWorld, RG_NONE, checkPoeCollectorAccess, checkOtherEntranceAccess); ValidateEntrances(checkPoeCollectorAccess, checkOtherEntranceAccess);
if (!ctx->GetOption(RSK_DECOUPLED_ENTRANCES)) { if (!ctx->GetOption(RSK_DECOUPLED_ENTRANCES)) {
// Unless entrances are decoupled, we don't want the player to end up through certain entrances as the wrong age // Unless entrances are decoupled, we don't want the player to end up through certain entrances as the wrong age
@ -765,7 +765,7 @@ static std::array<std::vector<Entrance*>, 2> SplitEntrancesByRequirements(std::v
Rando::StaticData::RetrieveItem(unplacedItem).ApplyEffect(); Rando::StaticData::RetrieveItem(unplacedItem).ApplyEffect();
} }
// run a search to see what's accessible // run a search to see what's accessible
GetAccessibleLocations({}); ReachabilitySearch({});
for (Entrance* entrance : entrancesToSplit) { for (Entrance* entrance : entrancesToSplit) {
// if an entrance is accessible at all times of day by both ages, it's a soft entrance with no restrictions // if an entrance is accessible at all times of day by both ages, it's a soft entrance with no restrictions

View File

@ -441,9 +441,12 @@ namespace Rando {
uint8_t Logic::BottleCount() { uint8_t Logic::BottleCount() {
uint8_t count = 0; uint8_t count = 0;
if (!CanEmptyBigPoes){
return 0;
}
for (int i = SLOT_BOTTLE_1; i <= SLOT_BOTTLE_4; i++) { for (int i = SLOT_BOTTLE_1; i <= SLOT_BOTTLE_4; i++) {
uint8_t item = ctx->GetSaveContext()->inventory.items[i]; uint8_t item = ctx->GetSaveContext()->inventory.items[i];
if (item != ITEM_NONE && (item != ITEM_LETTER_RUTO || (item == ITEM_LETTER_RUTO && DeliverLetter)) && item != ITEM_BIG_POE) { if (item != ITEM_NONE && (item != ITEM_LETTER_RUTO || (item == ITEM_LETTER_RUTO && DeliverLetter))) {
count++; count++;
} }
} }
@ -492,7 +495,7 @@ namespace Rando {
KokiriSword = CanUse(RG_KOKIRI_SWORD); KokiriSword = CanUse(RG_KOKIRI_SWORD);
MasterSword = CanUse(RG_MASTER_SWORD); MasterSword = CanUse(RG_MASTER_SWORD);
BiggoronSword = CanUse(RG_BIGGORON_SWORD); BiggoronSword = CanUse(RG_BIGGORON_SWORD);
NumBottles = ((NoBottles) ? 0 : BottleCount()); NumBottles = BottleCount();
HasBottle = NumBottles >= 1; HasBottle = NumBottles >= 1;
Slingshot = CanUse(RG_FAIRY_SLINGSHOT) && (BuySeed || AmmoCanDrop); Slingshot = CanUse(RG_FAIRY_SLINGSHOT) && (BuySeed || AmmoCanDrop);
Ocarina = HasItem(RG_FAIRY_OCARINA); Ocarina = HasItem(RG_FAIRY_OCARINA);
@ -921,7 +924,7 @@ namespace Rando {
//Bottle Count //Bottle Count
Bottles = 0; Bottles = 0;
NumBottles = 0; NumBottles = 0;
NoBottles = false; CanEmptyBigPoes = true;
//Triforce Pieces //Triforce Pieces
TriforcePieces = 0; TriforcePieces = 0;

View File

@ -165,7 +165,7 @@ class Logic {
// Bottle Count // Bottle Count
uint8_t Bottles = 0; uint8_t Bottles = 0;
uint8_t NumBottles = 0; uint8_t NumBottles = 0;
bool NoBottles = false; bool CanEmptyBigPoes = true;
// Drops and Bottle Contents Access // Drops and Bottle Contents Access
bool NutPot = false; bool NutPot = false;