Fix playthrough paring to not rely on checks past wincon (#7275)

Implement DelayItem to bypass SetPlacedItem setting price,
which when restored would leave behind price of RG_NONE
This commit is contained in:
Philip Dubé
2026-10-03 18:29:17 +00:00
committed by GitHub
parent 9d81cb03cf
commit 31c0d85544
4 changed files with 32 additions and 17 deletions
@@ -304,13 +304,12 @@ bool IsBombchus(RandomizerGet item, bool includeShops = false) {
bool IsBeatableWithout(RandomizerCheck excludedCheck, bool replaceItem,
RandomizerGet ignore = RG_NONE) { // RANDOTODO make excludedCheck an ItemLocation
auto ctx = Rando::Context::GetInstance();
RandomizerGet copy = ctx->GetItemLocation(excludedCheck)->GetPlacedRandomizerGet(); // Copy out item
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(RG_NONE); // Write in empty item
Rando::ItemLocation* location = Rando::Context::GetInstance()->GetItemLocation(excludedCheck);
location->DelayItem();
logic->Reset();
bool result = CheckBeatable(ignore);
if (replaceItem) {
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(copy); // Immediately put item back
location->SaveDelayedItem(); // Immediately put item back
}
return result;
}
@@ -338,6 +337,10 @@ void AddToPlaythrough(LocationAccess& locPair, GetAccessibleLocationsStruct& gal
RandomizerCheck loc = locPair.GetLocation();
Rando::ItemLocation* location = ctx->GetItemLocation(loc);
RandomizerGet locItem = location->GetPlacedRandomizerGet();
// Reached after wincon, so never part of playthrough. Pare first
if (ctx->playthroughBeatable && location->GetPlacedItem().IsAdvancement()) {
gals.lateLocations.push_back(loc);
}
// 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();
@@ -378,6 +381,12 @@ void AddToPlaythrough(LocationAccess& locPair, GetAccessibleLocationsStruct& gal
gals.itemSphere.clear();
gals.itemSphere.push_back(loc);
ctx->playthroughBeatable = true;
// Items found in this sphere aren't collected before wincon
for (Rando::ItemLocation* pending : gals.newItemLocations) {
if (pending != location && pending->GetPlacedItem().IsAdvancement()) {
gals.lateLocations.push_back(pending->GetRandomizerCheck());
}
}
}
}
}
@@ -565,7 +574,7 @@ std::vector<RandomizerCheck> ReachabilitySearch(const std::vector<RandomizerChec
}
// Create the playthrough for the seed
void GeneratePlaythrough() {
std::vector<RandomizerCheck> GeneratePlaythrough() {
auto ctx = Rando::Context::GetInstance();
ctx->playthroughBeatable = false;
logic->Reset();
@@ -589,6 +598,7 @@ void GeneratePlaythrough() {
ctx->GetEntranceShuffler()->playthroughEntrances.push_back(gals.entranceSphere);
}
} while (gals.logicUpdated);
return gals.lateLocations;
}
// return if the seed is currently beatable or not
@@ -710,17 +720,21 @@ void SetAreas() {
// 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() {
static void PareDownPlaythrough(const std::vector<RandomizerCheck>& lateLocations) {
auto ctx = Rando::Context::GetInstance();
std::vector<RandomizerCheck> toAddBackItem;
// Hide items only reachable after wincon. Otherwise they can stand in for a removed playthrough item
for (RandomizerCheck loc : lateLocations) {
ctx->GetItemLocation(loc)->DelayItem();
toAddBackItem.push_back(loc);
}
// Start at sphere before Ganon's and count down
for (int32_t i = static_cast<int32_t>(ctx->playthroughLocations.size()) - 2; i >= 0; i--) {
// Check each item location in sphere
std::vector<int> erasableIndices;
std::vector<RandomizerCheck> sphere = ctx->playthroughLocations.at(i);
for (int32_t j = static_cast<int32_t>(sphere.size()) - 1; j >= 0; j--) {
RandomizerCheck loc = sphere.at(j);
RandomizerGet locGet = ctx->GetItemLocation(loc)->GetPlacedRandomizerGet(); // Copy out item
RandomizerGet locGet = ctx->GetItemLocation(loc)->GetPlacedRandomizerGet();
RandomizerGet ignore = RG_NONE;
if (locGet == RG_GOLD_SKULLTULA_TOKEN || IsBombchus(locGet, true) ||
@@ -731,10 +745,9 @@ static void PareDownPlaythrough() {
// Playthrough is still beatable without this item, therefore it can be removed from playthrough section.
if (IsBeatableWithout(loc, false, ignore)) {
ctx->playthroughLocations[i].erase(ctx->playthroughLocations[i].begin() + j);
ctx->GetItemLocation(loc)->SetDelayedItem(locGet); // Game is still beatable, don't add back until later
toAddBackItem.push_back(loc);
toAddBackItem.push_back(loc); // Game is still beatable, don't add back until later
} else {
ctx->GetItemLocation(loc)->SetPlacedItem(locGet); // Immediately put item back so game is beatable again
ctx->GetItemLocation(loc)->SaveDelayedItem(); // Immediately put item back so game is beatable again
}
}
}
@@ -1459,13 +1472,13 @@ int Fill() {
StopPerformanceTimer(PT_REMAINING_ITEMS);
StartPerformanceTimer(PT_PLAYTHROUGH_GENERATION);
GeneratePlaythrough();
std::vector<RandomizerCheck> lateLocations = GeneratePlaythrough();
StopPerformanceTimer(PT_PLAYTHROUGH_GENERATION);
// Successful placement, produced beatable result
if (ctx->playthroughBeatable && !placementFailure) {
SPDLOG_INFO("Calculating Playthrough...");
StartPerformanceTimer(PT_PARE_DOWN_PLAYTHROUGH);
PareDownPlaythrough();
PareDownPlaythrough(lateLocations);
StopPerformanceTimer(PT_PARE_DOWN_PLAYTHROUGH);
StartPerformanceTimer(PT_WOTH);
@@ -13,6 +13,7 @@ struct GetAccessibleLocationsStruct {
int gsCount;
int maxGsCount;
std::vector<LogicVal> buyIgnores;
std::vector<RandomizerCheck> lateLocations;
// Variables for search
std::vector<Rando::ItemLocation*> newItemLocations;
@@ -68,7 +69,7 @@ std::vector<RandomizerCheck> ReachabilitySearch(const std::vector<RandomizerChec
RandomizerRegion startingRegion = RR_ROOT,
RandoAgeTime startingAgeTime = RAT_NONE);
void GeneratePlaythrough();
std::vector<RandomizerCheck> GeneratePlaythrough();
bool CheckBeatable(RandomizerGet ignore = RG_NONE);
@@ -48,8 +48,9 @@ void ItemLocation::SetPlacedItem(const RandomizerGet item) {
SetPrice(StaticData::RetrieveItem(placedItem).GetPrice());
}
void ItemLocation::SetDelayedItem(const RandomizerGet item) {
delayedItem = item;
void ItemLocation::DelayItem() {
delayedItem = placedItem;
placedItem = RG_NONE;
}
void ItemLocation::SaveDelayedItem() {
@@ -18,7 +18,7 @@ class ItemLocation {
RandomizerGet GetPlacedRandomizerGet() const;
void SetPlacedItem(RandomizerGet item);
RandomizerGet& RefPlacedItem();
void SetDelayedItem(RandomizerGet item);
void DelayItem();
RandomizerRegion GetParentRegionKey() const;
void SetParentRegion(RandomizerRegion region);
std::set<RandomizerArea> GetAreas() const;