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