From cd92e70b84630284f60169348d4c182a11520a6b Mon Sep 17 00:00:00 2001 From: Pepper0ni <93387759+Pepper0ni@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:06:30 +0100 Subject: [PATCH] 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 --- .../Enhancements/randomizer/3drando/fill.cpp | 654 +++++++++++------- .../Enhancements/randomizer/3drando/fill.hpp | 23 +- .../Enhancements/randomizer/3drando/hints.cpp | 6 +- .../randomizer/3drando/location_access.cpp | 7 +- .../randomizer/3drando/location_access.hpp | 4 +- .../location_access/locacc_castle_town.cpp | 5 +- soh/soh/Enhancements/randomizer/entrance.cpp | 4 +- soh/soh/Enhancements/randomizer/logic.cpp | 9 +- soh/soh/Enhancements/randomizer/logic.h | 2 +- 9 files changed, 423 insertions(+), 291 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/fill.cpp b/soh/soh/Enhancements/randomizer/3drando/fill.cpp index 835055ee4..366fa1ed1 100644 --- a/soh/soh/Enhancements/randomizer/3drando/fill.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/fill.cpp @@ -22,6 +22,57 @@ using namespace CustomMessages; using namespace Rando; + +//RANDOTODO merge into Logic once Logic is a class passed to logic funtions +struct GetAccessableLocationsStruct { + std::vector accessibleLocations; + std::vector regionPool; + //Variables for playthrough + int gsCount; + int maxGsCount; + std::vector buyIgnores; + + //Variables for search + std::vector newItemLocations; + bool updatedEvents; + bool ageTimePropogated; + bool firstIteration; + + //Variables For Validating Entrences + bool haveTimeAccess; + bool foundTempleOfTime; + bool validatedStartingRegion; + bool sphereZeroComplete; + + std::vector itemSphere; + std::list 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() { @@ -47,12 +98,12 @@ static void RemoveStartingItemsFromPool() { } } -//This function will propogate Time of Day access through the entrance -static bool UpdateToDAccess(Entrance* entrance, SearchMode mode) { +//This function will propagate Time of Day access through the entrance +static bool UpdateToDAccess(Entrance* entrance, bool propagateTimeTravel) { bool ageTimePropogated = false; - //propogate childDay, childNight, adultDay, and adultNight separately + //propagate childDay, childNight, adultDay, and adultNight separately Area* parent = entrance->GetParentRegion(); Area* connection = entrance->GetConnectedRegion(); @@ -74,11 +125,10 @@ static bool UpdateToDAccess(Entrance* entrance, SearchMode mode) { } //special check for temple of time - bool propogateTimeTravel = mode != SearchMode::TimePassAccess && mode != SearchMode::TempleOfTimeAccess; - if (!AreaTable(RR_ROOT)->Adult() && AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->Child() && propogateTimeTravel) { + 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 (!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)->childNight = AreaTable(RR_TOT_BEYOND_DOOR_OF_TIME)->adultNight; } @@ -86,55 +136,85 @@ static bool UpdateToDAccess(Entrance* entrance, SearchMode mode) { return ageTimePropogated; } -// Various checks that need to pass for the world to be validated as completable -static void ValidateWorldChecks(SearchMode& mode, bool checkPoeCollectorAccess, bool checkOtherEntranceAccess, std::vector& areaPool) { +// 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 (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)) { - mode = SearchMode::ValidStartingRegion; + 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 (mode == SearchMode::ValidStartingRegion) { + 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())) || - (adultAccess && (kokiri->Adult() || kakariko->Adult())) || - !checkOtherEntranceAccess) { - mode = SearchMode::PoeCollectorAccess; - ApplyStartingInventory(); - logic->NoBottles = true; + 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 } } - // 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 - std::vector itemsToPlace = - FilterFromPool(ItemPool, [](const auto i) { return Rando::StaticData::RetrieveItem(i).IsAdvancement(); }); - for (RandomizerGet unplacedItem : itemsToPlace) { - Rando::StaticData::RetrieveItem(unplacedItem).ApplyEffect(); - } - // Reset access as the non-starting age - if (ctx->GetSettings()->ResolvedStartingAge() == RO_AGE_CHILD) { - for (RandomizerRegion areaKey : areaPool) { - AreaTable(areaKey)->adultDay = false; - AreaTable(areaKey)->adultNight = false; - } - } else { - for (RandomizerRegion areaKey : areaPool) { - AreaTable(areaKey)->childDay = false; - AreaTable(areaKey)->childNight = false; - } - } - mode = SearchMode::AllLocationsReachable; - } else { - logic->NoBottles = false; - } } +// Apply all items that are necessary for checking all location access +static void ApplyAllAdvancmentItems(){ + std::vector 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(); @@ -186,9 +266,9 @@ std::string GetShopItemBaseName(std::string itemName) { return baseName; } -std::vector GetEmptyLocations(std::vector allowedLocations) { +std::vector GetEmptyLocations(std::vector targetLocations) { 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; }); } @@ -211,124 +291,176 @@ bool IsBeatableWithout(RandomizerCheck excludedCheck, bool replaceItem, Randomiz ctx->GetItemLocation(excludedCheck)->SetPlacedItem(RG_NONE); //Write in empty item ctx->playthroughBeatable = false; logic->Reset(); - GetAccessibleLocations(ctx->allLocations, SearchMode::CheckBeatable, ignore); //Check if game is still beatable + CheckBeatable(ignore); if (replaceItem){ ctx->GetItemLocation(excludedCheck)->SetPlacedItem(copy); //Immediately put item back } return ctx->playthroughBeatable; } -//This function will return a vector of ItemLocations that are accessible with -//where items have been placed so far within the world. The allowedLocations argument -//specifies the pool of locations that we're trying to search for an accessible location in -std::vector GetAccessibleLocations(const std::vector& allowedLocations, SearchMode mode /* = SearchMode::ReachabilitySearch*/, RandomizerGet ignore /* = RG_NONE*/, bool checkPoeCollectorAccess /*= false*/, bool checkOtherEntranceAccess /*= false*/) { - auto ctx = Rando::Context::GetInstance(); - std::vector accessibleLocations; - // Reset all access to begin a new search - if (mode < SearchMode::ValidateWorld) { - ApplyStartingInventory(); +// Reset non-Logic-class logic, and optionally apply the initial inventory +void ResetLogic(std::shared_ptr& ctx, bool applyInventory = false){ + if (applyInventory){ + ApplyStartingInventory(); } Areas::AccessReset(); ctx->LocationReset(); - std::vector 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; - } +//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(); - //Variables for playthrough - int gsCount = 0; - const int maxGsCount = mode == SearchMode::GeneratePlaythrough ? GetMaxGSCount() : 0; //If generating playthrough want the max that's possibly useful, else doesn't matter - std::vector buyIgnores; - - //Variables for search - std::vector 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(); + //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; } - newItemLocations.clear(); - - std::vector itemSphere; - std::list entranceSphere; - - for (size_t i = 0; i < areaPool.size(); i++) { - Area* area = AreaTable(areaPool[i]); - - if (area->UpdateEvents(mode)){ - updatedEvents = true; + //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; + } +} - // 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; - } +// 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); } - // 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; + //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) { + return true; //Return early for efficiency + } + } + return false; +} - //for each exit in this area - for (auto& exit : area->exits) { +// Return any of the targetLocations that are accessible in logic +std::vector ReachabilitySearch(const std::vector& 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]); - //Update Time of Day Access for the exit - if (UpdateToDAccess(&exit, mode)) { - ageTimePropogated = true; - ValidateWorldChecks(mode, checkPoeCollectorAccess, checkOtherEntranceAccess, areaPool); - } + 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; +} - //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()); - } +// 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 (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); + 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()) { @@ -336,114 +468,136 @@ std::vector GetAccessibleLocations(const std::vectorlocations.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); + } + } +} - //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(); - Rando::ItemLocation* location = ctx->GetItemLocation(loc); - RandomizerGet locItem = location->GetPlacedRandomizerGet(); +// 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 (!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 - 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 && gsCount < maxGsCount) { - 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(buyIgnores.begin(), buyIgnores.end(), buyItem) == buyIgnores.end()) { - exclude = false; - 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) { - 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) { - itemSphere.clear(); - itemSphere.push_back(loc); - ctx->playthroughBeatable = true; - } - } - //All we care about is if the game is beatable, used to pare down playthrough - else if (location->GetPlacedRandomizerGet() == RG_TRIFORCE && mode == SearchMode::CheckBeatable) { - ctx->playthroughBeatable = true; - return {}; //Return early for efficiency - } - } + 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; +} - if (mode == SearchMode::GeneratePlaythrough && itemSphere.size() > 0) { - ctx->playthroughLocations.push_back(itemSphere); - } - if (mode == SearchMode::GeneratePlaythrough && entranceSphere.size() > 0 && !ctx->GetEntranceShuffler()->HasNoRandomEntrances()) { - ctx->GetEntranceShuffler()->playthroughEntrances.push_back(entranceSphere); +// 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; } - //Check to see if all locations were reached - if (mode == SearchMode::AllLocationsReachable) { + 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()) { @@ -457,25 +611,7 @@ std::vector GetAccessibleLocations(const std::vectorGetItemLocation(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 &alreadyChecked){ @@ -489,7 +625,7 @@ RandomizerArea LookForExternalArea(const Area* const currentRegion, std::vector< if(passdown != RA_NONE){ 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; } } @@ -576,7 +712,7 @@ static void CalculateWotH() { } ctx->playthroughBeatable = true; logic->Reset(); - GetAccessibleLocations(ctx->allLocations); + ReachabilitySearch(ctx->allLocations); } //Calculate barren locations and assign Barren Candidacy to all locations inside those areas @@ -687,7 +823,7 @@ static void AssumedFill(const std::vector& items, const std::vect } // get all accessible locations that are allowed - const std::vector accessibleLocations = GetAccessibleLocations(allowedLocations); + const std::vector accessibleLocations = ReachabilitySearch(allowedLocations); // retry if there are no more locations to place items if (accessibleLocations.empty()) { @@ -729,7 +865,7 @@ static void AssumedFill(const std::vector& items, const std::vect if (!ctx->GetOption(RSK_ALL_LOCATIONS_REACHABLE)) { ctx->playthroughBeatable = false; logic->Reset(); - GetAccessibleLocations(ctx->allLocations, SearchMode::CheckBeatable); + CheckBeatable(); if (ctx->playthroughBeatable) { SPDLOG_DEBUG("Game beatable, now placing items randomly. " + std::to_string(itemsToPlace.size()) + " major items remaining.\n\n"); diff --git a/soh/soh/Enhancements/randomizer/3drando/fill.hpp b/soh/soh/Enhancements/randomizer/3drando/fill.hpp index 2dd6c21c8..60feafc16 100644 --- a/soh/soh/Enhancements/randomizer/3drando/fill.hpp +++ b/soh/soh/Enhancements/randomizer/3drando/fill.hpp @@ -5,25 +5,16 @@ #include #include -enum class SearchMode { - ReachabilitySearch, - GeneratePlaythrough, - CheckBeatable, - AllLocationsReachable, - ValidateWorld, - TimePassAccess, - TempleOfTimeAccess, - ValidStartingRegion, - PoeCollectorAccess, -}; - void ClearProgress(); void VanillaFill(); int Fill(); std::vector GetEmptyLocations(std::vector allowedLocations); -std::vector GetAccessibleLocations(const std::vector& allowedLocations, - SearchMode mode = SearchMode::ReachabilitySearch, RandomizerGet ignore = RG_NONE, - bool checkPoeCollectorAccess = false, - bool checkOtherEntranceAccess = false); \ No newline at end of file +std::vector ReachabilitySearch(const std::vector& allowedLocations, RandomizerGet ignore=RG_NONE); + +void GeneratePlaythrough(); + +bool CheckBeatable(RandomizerGet ignore=RG_NONE); + +void ValidateEntrances(bool checkPoeCollectorAccess, bool checkOtherEntranceAccess); \ No newline at end of file diff --git a/soh/soh/Enhancements/randomizer/3drando/hints.cpp b/soh/soh/Enhancements/randomizer/3drando/hints.cpp index 95f5a1144..9b22706cd 100644 --- a/soh/soh/Enhancements/randomizer/3drando/hints.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/hints.cpp @@ -328,7 +328,7 @@ static std::vector GetAccessibleGossipStones(const RandomizerCh ctx->GetItemLocation(hintedLocation)->SetPlacedItem(RG_NONE); ctx->GetLogic()->Reset(); - auto accessibleGossipStones = GetAccessibleLocations(Rando::StaticData::gossipStoneLocations); + auto accessibleGossipStones = ReachabilitySearch(Rando::StaticData::gossipStoneLocations); //Give the item back to the location ctx->GetItemLocation(hintedLocation)->SetPlacedItem(originalItem); @@ -342,7 +342,7 @@ bool IsReachableWithout(std::vector locsToCheck, RandomizerChec RandomizerGet originalItem = ctx->GetItemLocation(excludedCheck)->GetPlacedRandomizerGet(); ctx->GetItemLocation(excludedCheck)->SetPlacedItem(RG_NONE); ctx->GetLogic()->Reset(); - const auto rechableWithout = GetAccessibleLocations(locsToCheck); + const auto rechableWithout = ReachabilitySearch(locsToCheck); ctx->GetItemLocation(excludedCheck)->SetPlacedItem(originalItem); if (resetAfter){ //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. //Call the function one last time to get rid of false positives on locations not //being reachable. - GetAccessibleLocations({}); + ReachabilitySearch({}); } std::vector FindItemsAndMarkHinted(std::vector items, std::vector hintChecks){ diff --git a/soh/soh/Enhancements/randomizer/3drando/location_access.cpp b/soh/soh/Enhancements/randomizer/3drando/location_access.cpp index 29b3cb217..851c26a48 100644 --- a/soh/soh/Enhancements/randomizer/3drando/location_access.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/location_access.cpp @@ -89,9 +89,8 @@ Area::Area(std::string regionName_, std::string scene_, RandomizerArea area, Area::~Area() = default; -bool Area::UpdateEvents(SearchMode mode) { - - if (timePass && mode != SearchMode::TimePassAccess) { +bool Area::UpdateEvents(bool haveTimeAccess) { + if (timePass && haveTimeAccess) { if (Child()) { childDay = true; childNight = true; @@ -241,7 +240,7 @@ std::shared_ptr logic; void AreaTable_Init() { using namespace Rando; randoCtx = Context::GetInstance().get(); - logic = randoCtx->GetLogic(); + logic = randoCtx->GetLogic(); //RANDOTODO do not hardcode, instead allow accepting a Logic class somehow grottoEvents = { EventAccess(&logic->GossipStoneFairy, { [] { return logic->GossipStoneFairy || logic->CanSummonGossipFairy; } }), EventAccess(&logic->ButterflyFairy, { [] { return logic->ButterflyFairy || (logic->CanUse(RG_STICKS)); } }), diff --git a/soh/soh/Enhancements/randomizer/3drando/location_access.hpp b/soh/soh/Enhancements/randomizer/3drando/location_access.hpp index 286551cab..3dcdbfb26 100644 --- a/soh/soh/Enhancements/randomizer/3drando/location_access.hpp +++ b/soh/soh/Enhancements/randomizer/3drando/location_access.hpp @@ -164,9 +164,9 @@ public: bool childNight = false; bool adultDay = 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); diff --git a/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_castle_town.cpp b/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_castle_town.cpp index e809beb54..9f113218c 100644 --- a/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_castle_town.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_castle_town.cpp @@ -165,7 +165,10 @@ void AreaTable_Init_CastleTown() { 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 LOCATION(RC_MARKET_10_BIG_POES, logic->IsAdult && logic->BigPoeKill), LOCATION(RC_MARKET_GS_GUARD_HOUSE, logic->IsChild), diff --git a/soh/soh/Enhancements/randomizer/entrance.cpp b/soh/soh/Enhancements/randomizer/entrance.cpp index 39eab1398..16f362cb9 100644 --- a/soh/soh/Enhancements/randomizer/entrance.cpp +++ b/soh/soh/Enhancements/randomizer/entrance.cpp @@ -462,7 +462,7 @@ static bool ValidateWorld(Entrance* entrancePlaced) { // Conditions will be checked during the search and any that fail will be figured out // afterwards ctx->GetLogic()->Reset(); - GetAccessibleLocations({}, SearchMode::ValidateWorld, RG_NONE, checkPoeCollectorAccess, checkOtherEntranceAccess); + ValidateEntrances(checkPoeCollectorAccess, checkOtherEntranceAccess); 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 @@ -765,7 +765,7 @@ static std::array, 2> SplitEntrancesByRequirements(std::v Rando::StaticData::RetrieveItem(unplacedItem).ApplyEffect(); } // run a search to see what's accessible - GetAccessibleLocations({}); + ReachabilitySearch({}); 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 diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index edb17df0c..231799baf 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -441,9 +441,12 @@ namespace Rando { uint8_t Logic::BottleCount() { uint8_t count = 0; + if (!CanEmptyBigPoes){ + return 0; + } for (int i = SLOT_BOTTLE_1; i <= SLOT_BOTTLE_4; 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++; } } @@ -492,7 +495,7 @@ namespace Rando { KokiriSword = CanUse(RG_KOKIRI_SWORD); MasterSword = CanUse(RG_MASTER_SWORD); BiggoronSword = CanUse(RG_BIGGORON_SWORD); - NumBottles = ((NoBottles) ? 0 : BottleCount()); + NumBottles = BottleCount(); HasBottle = NumBottles >= 1; Slingshot = CanUse(RG_FAIRY_SLINGSHOT) && (BuySeed || AmmoCanDrop); Ocarina = HasItem(RG_FAIRY_OCARINA); @@ -921,7 +924,7 @@ namespace Rando { //Bottle Count Bottles = 0; NumBottles = 0; - NoBottles = false; + CanEmptyBigPoes = true; //Triforce Pieces TriforcePieces = 0; diff --git a/soh/soh/Enhancements/randomizer/logic.h b/soh/soh/Enhancements/randomizer/logic.h index 6a669d7eb..d5de5f899 100644 --- a/soh/soh/Enhancements/randomizer/logic.h +++ b/soh/soh/Enhancements/randomizer/logic.h @@ -165,7 +165,7 @@ class Logic { // Bottle Count uint8_t Bottles = 0; uint8_t NumBottles = 0; - bool NoBottles = false; + bool CanEmptyBigPoes = true; // Drops and Bottle Contents Access bool NutPot = false;