#include "fill.hpp" #include "item_pool.hpp" #include "search.hpp" #include "../utility/random.hpp" #include "../utility/string.hpp" #include "../utility/time.hpp" #include #include namespace randomizer::logic::fill { void FillWorlds(world::WorldPool& worlds) { // Place each world's restricted items first for (auto& world : worlds) { PlaceRestrictedItems(world, worlds); } item_pool::ItemPool itemPool = {}; location::LocationPool locationPool = {}; // Combine all worlds' item pools and location pools for (const auto& world : worlds) { for (const auto& item : world->GetItemPool()) { itemPool.emplace_back(item); } for (const auto& location : world->GetAllLocations()) { locationPool.emplace_back(location); } } // Place remaining major items in progress locations auto majorItems = randomizer::utility::container::FilterAndEraseFromVector(itemPool, [](const auto& item) { return item->IsMajor(); }); auto progressLocations = randomizer::utility::container::FilterFromVector(locationPool, [](const auto& location) { return location->IsProgression(); }); AssumedFill(worlds, majorItems, itemPool, progressLocations); // Place Minor items in progression locations if possible auto minorItems = randomizer::utility::container::FilterAndEraseFromVector(itemPool, [](const auto& item) { return item->IsMinor(); }); FastFill(minorItems, progressLocations); // If there are still minor items left, add them back to the main item pool for (const auto& minorItem : minorItems) { itemPool.push_back(minorItem); } // Then place everything else anywhere FastFill(itemPool, locationPool); // Verify that all logic is satisfied auto verifyLogicError = search::VerifyLogic(&worlds); if (verifyLogicError.has_value()) { throw std::runtime_error("Not all logic satisfied! Reason:\n" + verifyLogicError.value()); } } void AssumedFill(world::WorldPool& worlds, item_pool::ItemPool& itemsToPlacePool, const item_pool::ItemPool& itemsNotYetPlaced, location::LocationPool allowedLocations, const int& worldToFill /* = -1 */) { // Assumed Fill may sometimes place items in such a way that accidentally locks out being able to place specific items // later on. Allow the algorithm to retry a reasonable amount of times before returning an error. int retries = 10; bool unsuccessfulPlacement = true; while (unsuccessfulPlacement) { if (retries <= 0) { std::string errorMsg = "Ran out of retries while attempting to place the following items:\n"; int count = itemsToPlacePool.size() > 5 ? 5 : itemsToPlacePool.size(); for (int i = 0; i < count; i++) { auto& item = itemsToPlacePool[i]; errorMsg += "- " + item->GetName() + "\n"; } if (count < itemsToPlacePool.size()) { errorMsg += "- (" + std::to_string(itemsToPlacePool.size() - count) + " more)"; } throw std::runtime_error(errorMsg); } retries -= 1; unsuccessfulPlacement = false; randomizer::utility::random::ShufflePool(itemsToPlacePool); auto itemsToPlace = itemsToPlacePool; location::LocationPool rollbacks = {}; while (!itemsToPlace.empty()) { // Get a random item to place auto itemToPlace = itemsToPlace.back(); itemsToPlace.pop_back(); randomizer::utility::random::ShufflePool(allowedLocations); location::Location* spotToFill = nullptr; // Assume we have all the items which haven't been played yet, except the one we're about to place auto assumedItems = itemsNotYetPlaced; assumedItems.insert(assumedItems.end(), itemsToPlace.begin(), itemsToPlace.end()); auto search = search::Search::Accessible(&worlds, assumedItems, worldToFill); search.SearchWorlds(); // search.DumpWorldGraph(); // return 1; // Loop through the shuffled locations until we find a valid one. // If a world is only checking for beatable logic, then we can ignore // any access checks and just choose a random location if the world is already beatable auto beatableOnlyLogic = itemToPlace->GetWorld()->Setting("Logic Rules") == "Beatable Only"; bool canChooseAnyLocation = search._ownedItems.contains(itemToPlace->GetWorld()->GetGameWinningItem()) && beatableOnlyLogic; for (const auto& location : allowedLocations) { // Get all reachable LocationAccess spots for this location std::list locAccList; for (const auto& locAcc : location->GetAccessList()) { if (canChooseAnyLocation || search._visitedAreas.contains(locAcc->GetArea())) { locAccList.push_back(locAcc); } } // If this location is not empty, or has no potentially reachable LocationAccess spot, or is forbidden from // having this item, then we can't place the item here if (!location->IsEmpty() || locAccList.empty() || location->GetForbiddenItems().contains(itemToPlace)) { continue; } // If any of the LocationAccess spots evaluate to complete, then we can place an item here if (std::any_of(locAccList.begin(), locAccList.end(), [&](const auto& la) { return canChooseAnyLocation || requirement::EvaluateLocationRequirement(&search, la) == requirement::EvalSuccess::COMPLETE; })) { spotToFill = location; break; } } // If we couldn't find a spot to place this item, undo all item placements within this fill attempt and try // again from the top. if (spotToFill == nullptr) { LOG_TO_DEBUG("No accessible locations to place " + itemToPlace->GetName() + ". Retrying " + std::to_string(retries) + " more times."); for (auto& location : rollbacks) { itemsToPlace.push_back(location->GetCurrentItem()); location->RemoveCurrentItem(); } // Also add back the randomly selected item itemsToPlace.push_back(itemToPlace); rollbacks.clear(); // Break out of the item placement loop and flag an unsuccessful placement attempt to try again unsuccessfulPlacement = true; break; } // Place the item at the location spotToFill->SetCurrentItem(itemToPlace); rollbacks.push_back(spotToFill); } } } void FastFill(item_pool::ItemPool& itemsToPlace, location::LocationPool allowedLocations) { auto emptyLocations = randomizer::utility::container::FilterFromVector(allowedLocations, [](const auto& location) { return location->IsEmpty(); }); if (itemsToPlace.size() > emptyLocations.size()) { std::cout << "WARNING: More items than locations when placing items with fast fill. Items: " << itemsToPlace.size() << " Locations: " << emptyLocations.size() << std::endl; } randomizer::utility::random::ShufflePool(emptyLocations); for (auto& location : emptyLocations) { if (itemsToPlace.empty()) { break; } location->SetCurrentItem(randomizer::utility::random::PopRandomElement(itemsToPlace)); } } void PlaceRestrictedItems(std::unique_ptr& world, world::WorldPool& worlds) { PlaceGoalLocationItems(world, worlds); PlaceOwnDungeonItems(world, worlds); PlacePrologueItems(world, worlds); PlaceAnywhereDungeonRewards(world, worlds); // Determine required dungeons now so that we can place "any dungeon" items appropriately world->DetermineRequiredDungeons(); PlaceAnyDungeonItems(world, worlds); PlaceOverworldItems(world, worlds); } void PlacePrologueItems(std::unique_ptr& world, world::WorldPool& worlds) { if (world->Setting("Skip Prologue") == "Off") { // Filter out the slingshot and progressive swords to place first. The first slingshot and sword have a very limited // pool of locations and have to be found in the intro. We also include the lantern, shadow crystal, and progressive // fishing rod because those items can lock prologue locations also. auto& itemPool = world->GetItemPool(); auto prologueItems = randomizer::utility::container::FilterAndEraseFromVector( itemPool, [](const auto& item) { return item->GetName() == "Slingshot" || item->GetName() == "Progressive Sword" || item->GetName() == "Lantern" || item->GetName() == "Progressive Fishing Rod" || item->IsShadowCrystal(); }); auto completeItemPool = item_pool::GetCompleteItemPool(worlds); AssumedFill(worlds, prologueItems, completeItemPool, world->GetAllLocations()); } } void PlaceGoalLocationItems(std::unique_ptr& world, world::WorldPool& worlds) { // If dungeon rewards can be anywhere, then return early and place them later if (world->Setting("Dungeon Rewards Can Be Anywhere") == "On") { return; } auto allLocations = world->GetAllLocations(); location::LocationPool goalLocations = {}; // Filter out goal locations goalLocations = randomizer::utility::container::FilterFromVector( allLocations, [](const auto& location) { return location->IsGoalLocation() && location->IsEmpty(); }); // Filter out goal items std::set goalItemNames = {"Progressive Mirror Shard", "Progressive Fused Shadow"}; auto goalItems = randomizer::utility::container::FilterAndEraseFromVector( world->GetItemPool(), [&](const auto& item) { return goalItemNames.contains(item->GetName()); }); // Return an error if there aren't enough goal locations if (goalItems.size() > goalLocations.size()) { throw std::runtime_error("Not enough goal locations to place dungeon rewards on goal locations."); } // Place goal items at goal locations auto completeItemPool = item_pool::GetCompleteItemPool(worlds); AssumedFill(worlds, goalItems, completeItemPool, goalLocations); } void PlaceOwnDungeonItems(std::unique_ptr& world, world::WorldPool& worlds) { for (const auto& [dungeonName, dungeon] : world->GetDungeonTable()) { // Filter hint signs out of dungeon locations auto dungeonLocations = dungeon->GetLocations(); randomizer::utility::container::FilterAndEraseFromVector(dungeonLocations, [](const auto& location) { return location->HasCategories("Hint Sign"); }); // Clang doesn't like passing structured binding variables to lambda functions via reference, so we create these // temporary variables to serve the purpose auto& dungeon_ = dungeon; auto& dungeonName_ = dungeonName; // Small Keys if (world->Setting("Small Keys") == "Own Dungeon") { auto smallKeys = randomizer::utility::container::FilterAndEraseFromVector( world->GetItemPool(), [&](const auto& item) { return item == dungeon_->GetSmallKey() || (dungeonName_ == "Snowpeak Ruins" && (item->GetName() == "Ordon Pumpkin" || item->GetName() == "Ordon Cheese")); }); auto completeItemPool = item_pool::GetCompleteItemPool(worlds); AssumedFill(worlds, smallKeys, completeItemPool, dungeonLocations); } // Big Keys if (world->Setting("Big Keys") == "Own Dungeon") { auto bigKeys = randomizer::utility::container::FilterAndEraseFromVector(world->GetItemPool(), [&](const auto& item) { return item == dungeon_->GetBigKey(); }); auto completeItemPool = item_pool::GetCompleteItemPool(worlds); AssumedFill(worlds, bigKeys, completeItemPool, dungeonLocations); } // Place maps and compasses last with fast fill since they're junk items if (world->Setting("Maps and Compasses") == "Own Dungeon") { auto mapsCompasses = randomizer::utility::container::FilterAndEraseFromVector( world->GetItemPool(), [&](const auto& item) { return item == dungeon_->GetCompass() || item == dungeon_->GetDungeonMap(); }); auto completeItemPool = item_pool::GetCompleteItemPool(worlds); FastFill(mapsCompasses, dungeonLocations); } } } void PlaceAnywhereDungeonRewards(std::unique_ptr& world, world::WorldPool& worlds) { // If dungeon rewards can't be anywhere, then return early as we placed them earlier if (world->Setting("Dungeon Rewards Can Be Anywhere") == "Off") { return; } auto allLocations = world->GetAllLocations(); // Filter out goal items std::set goalItemNames = {"Progressive Mirror Shard", "Progressive Fused Shadow"}; auto goalItems = randomizer::utility::container::FilterAndEraseFromVector( world->GetItemPool(), [&](const auto& item) { return goalItemNames.contains(item->GetName()); }); // Place the items auto completeItemPool = item_pool::GetCompleteItemPool(worlds); AssumedFill(worlds, goalItems, completeItemPool, allLocations); } void PlaceAnyDungeonItems(std::unique_ptr& world, world::WorldPool& worlds) { item_pool::ItemPool anyDungeonItems = {}; location::LocationPool anyDungeonLocations = {}; // Split the placement of any dungeon items into two pools. Dungeon items from dungeons which should be barren // will only be distributed among barren dungeons, where as items from nonbarren dungeons will be distributed // among nonbarren dungeons std::list nonBarrenDungeons = {}; std::list barrenDungeons = {}; for (const auto& [dungeonName, dungeon] : world->GetDungeonTable()) { if (dungeon->ShouldBeBarren()) { barrenDungeons.push_back(dungeon.get()); } else { nonBarrenDungeons.push_back(dungeon.get()); } } // Loop through each pool separately for (const auto& dungeons : {nonBarrenDungeons, barrenDungeons}) { anyDungeonItems.clear(); anyDungeonLocations.clear(); // Gather all the appropriate items and locations for the dungeon in this pool for (const auto& dungeon : dungeons) { // Clang doesn't like passing structured binding variables to lambda functions via reference, so we create these // temporary variables to serve the purpose auto& dungeon_ = dungeon; // Add small keys to the pool if small keys are any dungeon if (world->Setting("Small Keys") == "Any Dungeon") { auto smallKeys = randomizer::utility::container::FilterAndEraseFromVector( world->GetItemPool(), [&](const auto& item) { return item == dungeon_->GetSmallKey() || (dungeon_->GetName() == "Snowpeak Ruins" && (item->GetName() == "Ordon Pumpkin" || item->GetName() == "Ordon Cheese")); }); std::copy(smallKeys.begin(), smallKeys.end(), std::back_inserter(anyDungeonItems)); } // Add big keys to the pool if big keys are any dungeon if (world->Setting("Big Keys") == "Any Dungeon") { auto bigKeys = randomizer::utility::container::FilterAndEraseFromVector( world->GetItemPool(), [&](const auto& item) { return item == dungeon_->GetBigKey(); }); std::copy(bigKeys.begin(), bigKeys.end(), std::back_inserter(anyDungeonItems)); } // Add maps and compasses to the pool if maps and compasses are any dungeon if (world->Setting("Maps and Compasses") == "Any Dungeon") { auto mapsCompasses = randomizer::utility::container::FilterAndEraseFromVector( world->GetItemPool(), [&](const auto& item) { return item == dungeon_->GetCompass() || item == dungeon_->GetDungeonMap(); }); std::copy(mapsCompasses.begin(), mapsCompasses.end(), std::back_inserter(anyDungeonItems)); } // Add this dungeon's locations to the anyDungeonLocations pool. If this is a nonbarren dungeon, only include // locations which are still progression. If it's a barren dungeon, include all the locations auto dungeonLocations = dungeon->GetLocations(); std::copy_if(dungeonLocations.begin(), dungeonLocations.end(), std::back_inserter(anyDungeonLocations), [&](const auto& location) { return dungeon->ShouldBeBarren() || location->IsProgression(); }); } // Place the dungeon items in the appropriate dungeon locations auto completeItemPool = item_pool::GetCompleteItemPool(worlds); AssumedFill(worlds, anyDungeonItems, completeItemPool, anyDungeonLocations); } } void PlaceOverworldItems(std::unique_ptr& world, world::WorldPool& worlds) { item_pool::ItemPool overworldItems = {}; location::LocationPool overworldLocations = world->GetAllLocations(); // Filter out any nonprogress locations randomizer::utility::container::FilterAndEraseFromVector(overworldLocations, [](const auto& location) { return !location->IsProgression(); }); for (const auto& [dungeonName, dungeon] : world->GetDungeonTable()) { // Clang doesn't like passing structured binding variables to lambda functions via reference, so we create these // temporary variables to serve the purpose auto& dungeon_ = dungeon; auto& dungeonName_ = dungeonName; // Add small keys to the pool if small keys are overworld if (world->Setting("Small Keys") == "Overworld") { auto smallKeys = randomizer::utility::container::FilterAndEraseFromVector( world->GetItemPool(), [&](const auto& item) { return item == dungeon_->GetSmallKey() || (dungeonName_ == "Snowpeak Ruins" && (item->GetName() == "Ordon Pumpkin" || item->GetName() == "Ordon Cheese")); }); std::copy(smallKeys.begin(), smallKeys.end(), std::back_inserter(overworldItems)); } // Add big keys to the pool if big keys are overworld if (world->Setting("Big Keys") == "Overworld") { auto bigKeys = randomizer::utility::container::FilterAndEraseFromVector(world->GetItemPool(), [&](const auto& item) { return item == dungeon_->GetBigKey(); }); std::copy(bigKeys.begin(), bigKeys.end(), std::back_inserter(overworldItems)); } // Add maps and compasses to the pool if maps and compasses are overworld if (world->Setting("Maps and Compasses") == "Overworld") { auto mapsCompasses = randomizer::utility::container::FilterAndEraseFromVector( world->GetItemPool(), [&](const auto& item) { return item == dungeon_->GetCompass() || item == dungeon_->GetDungeonMap(); }); std::copy(mapsCompasses.begin(), mapsCompasses.end(), std::back_inserter(overworldItems)); } // Remove this dungeon's locations from the overworldLocations pool overworldLocations = randomizer::utility::container::FilterFromVector( overworldLocations, [&](const auto& location) { return !randomizer::utility::container::ElementInContainer(dungeon_->GetLocations(), location); }); } // Place the dungeon items in the overworld locations auto completeItemPool = item_pool::GetCompleteItemPool(worlds); AssumedFill(worlds, overworldItems, completeItemPool, overworldLocations); } void CacheExitTimeForms(world::WorldPool& worlds) { auto completeItemPool = item_pool::GetCompleteItemPool(worlds); auto searchWithItems = search::Search::AllLocationsReachable(&worlds, completeItemPool); searchWithItems.SearchWorlds(); for (auto& world : worlds) { LOG_TO_DEBUG("Caching timeforms for world " + std::to_string(world->GetID())); auto& exitTimeFormCache = world->GetExitTimeFormCache(); exitTimeFormCache.clear(); for (const auto& [areaName, area] : world->GetAreaTable()) { const auto& areaFormTimes = searchWithItems._areaFormTime[area.get()]; for (const auto& exit : area->GetExits()) { auto req = exit->GetRequirement(); exitTimeFormCache[exit] = requirement::FormTime::NONE; for (const auto& formTime : requirement::FormTime::ALL_FORM_TIMES) { if (formTime & areaFormTimes && requirement::EvaluateRequirementAtFormTime(req, &searchWithItems, formTime, world.get())) { exitTimeFormCache[exit] |= formTime; } } } } } } } // namespace randomizer::logic::fill