Files
dusklight/mods/randomizer/generator/logic/hints.cpp
T
2026-08-02 01:25:06 -07:00

1048 lines
49 KiB
C++

#include "hints.hpp"
#include "search.hpp"
#include "world.hpp"
#include "../utility/platform.hpp"
#include "../utility/string.hpp"
#include "../utility/text.hpp"
#include "../utility/random.hpp"
#include "../randomizer.hpp"
#include <ranges>
namespace randomizer::logic::hints {
static const std::list<std::pair<std::string, std::string>> dungeonColors = {
{"Forest Temple", "<green>"},
{"Goron Mines", "<red>"},
{"Lakebed Temple", "<blue>"},
{"Arbiters Grounds", "<orange>"},
{"Snowpeak Ruins", "<light blue>"},
{"Temple of Time", "<dark green>"},
{"City in the Sky", "<yellow>"},
{"Palace of Twilight", "<purple>"},
{"Hyrule Castle", "<silver>"}
};
static const std::map<std::string, std::string> bossColors = {
{"Diababa", "<green>"},
{"Fyrus", "<red>"},
{"Morpheel", "<blue>"},
{"Stallord", "<orange>"},
{"Blizzeta", "<light blue>"},
{"Armogohma", "<dark green>"},
{"Argorok", "<yellow>"},
{"Zant", "<purple>"},
{"Ganondorf", "<silver>"}
};
static location::Location* GetHintableLocation(const location::LocationPool& locations) {
for (const auto location : locations) {
if (!location->IsHinted()) {
return location;
}
}
return nullptr;
}
static void CalculatePossiblePathLocations(world::WorldPool& worlds) {
LOG_TO_DEBUG("Calculating Possible Path Locations");
// First generate the goal location keys and also remove items from non-
// progress locations since they shouldn't be considered when determining
// path locations
std::unordered_map<location::Location*, item::Item*> nonRequiredLocations{};
for (const auto& world : worlds) {
// Defeat Ganondorf is always a goal location
world->AddGoalLocation(world->GetLocation("Defeat Ganondorf"));
// Required dungeons also have goal locations
for (const auto& dungeon : world->GetDungeonTable() | std::views::values) {
if (dungeon->IsRequired()) {
world->AddGoalLocation(dungeon->GetGoalLocation());
}
}
// Collect items at non-progress locations to give back later
for (const auto& location: world->GetAllLocations()) {
if (!location->IsProgression()) {
nonRequiredLocations[location] = location->GetCurrentItem();
location->RemoveCurrentItem();
}
}
}
// Determine path locations for every progression location with a major item by going through
// and seeing if taking away the item at each location can still access the other locations
for (const auto& world : worlds) {
for (const auto& potentialPathLocation : world->GetAllLocations()) {
auto itemAtLocation = potentialPathLocation->GetCurrentItem();
// Skip over locations with removed items
if (itemAtLocation == nullptr) {
continue;
}
// If this is not a progression location with a major item, skip over it
if (!potentialPathLocation->IsProgression() || !itemAtLocation->IsMajor()) {
continue;
}
// Take the item away from the location
potentialPathLocation->RemoveCurrentItem();
// Run a search without the item
auto search = search::Search::Accessible(&worlds);
search.SearchWorlds();
// Check to see if we can reach each progression location. For each progression location that we can't reach,
// add the potential path location as a path location
for (auto& location : world->GetAllLocations()) {
if (!search._visitedLocations.contains(location)) {
location->AddPathLocation(potentialPathLocation);
}
}
// Then give back the location's item
potentialPathLocation->SetCurrentItem(itemAtLocation);
}
}
// Give back non-progress items
for (auto& [location, item] : nonRequiredLocations) {
location->SetCurrentItem(item);
}
}
// Calculates the passed in location's hint importance, as well as recursively calculates the importance of any
// locations that this one may unlock, but haven't had their own importance calculated yet.
// By the time we call this function, we're only trying to determine if a location is
// possibly required or not required.
//
// This function returns any location that blocks the passed in location from being not required
static location::Location* CalculateLocationHintImportance(
location::Location* location,
std::unordered_set<location::Location*>& currentlyChecking)
{
// If we already know this location's importance, we don't need to calculate it
if (location->GetImportance() != location::Importance::UNKNOWN) {
return nullptr;
}
// Get all the progressive chain locations for this location's item.
// Filter out locations that aren't progression, are already in this
// location's path locations, have an item that we can trivially know
// is barren, or have already been deemed as not required.
std::unordered_set<location::Location*> chainLocations{};
std::unordered_set pathLocations(
location->GetPathLocations().begin(),
location->GetPathLocations().end()
);
for (const auto& loc : location->GetCurrentItem()->GetChainLocations()) {
if (loc->IsProgression() &&
loc->GetCurrentItem()->IsMajor() &&
!pathLocations.contains(loc) &&
!loc->GetCurrentItem()->CanBeInBarrenRegion() &&
loc->GetImportance() != location::Importance::NOT_REQUIRED)
{
chainLocations.insert(loc);
}
}
// Remove the current location, as the item at this location obviously can't lead to itself
chainLocations.erase(location);
// If there are no chain locations left, then this item is not required
if (chainLocations.empty()) {
location->SetImportance(location::Importance::NOT_REQUIRED);
return nullptr;
}
// Get all items from this location's logically required path locations
item_pool::ItemPool logicallyRequiredItems{};
for (auto pathLocation : pathLocations) {
logicallyRequiredItems.push_back(pathLocation->GetCurrentItem());
}
// Perform an importance search to see which locations are reachable without using this item in any way
auto worlds = &location->GetWorld()->GetRandomizer()->GetWorlds();
auto importanceSearch = search::Search::LocationImportance(worlds, location, logicallyRequiredItems);
importanceSearch.SearchWorlds();
// Any locations that we found can be subtracted out of our chain locations because the item at this location
// does not help reach them in any way.
std::erase_if(chainLocations, [&](location::Location* loc) {
return importanceSearch._visitedLocations.contains(loc);
});
// If any remaining chain locations are also currently being checked for their hint importance,
// then we subtract them out. This will allow us to only check locations which aren't potentially
// dependent on each other's items. If we go through all the other locations first and don't
// find anything that makes this location possibly required, we'd only be left with locations
// currently being checked that are dependent on one another. If these location's items are only
// potentially dependent on each other, then they're both not required.
std::erase_if(chainLocations, [&](location::Location* loc) {
return currentlyChecking.contains(loc);
});
// Check to see if any remaining chain locations are at least possible required
for (auto chainLocation : chainLocations) {
// If any remaining chain location hasn't had its hint importance calculated, then do so now
if (chainLocation->GetImportance() == location::Importance::UNKNOWN) {
currentlyChecking.insert(chainLocation);
CalculateLocationHintImportance(chainLocation, currentlyChecking);
currentlyChecking.erase(chainLocation);
}
// If any remaining chain location is at least possibly required, then this location is also possibly required
if (chainLocation->GetImportance() > location::Importance::NOT_REQUIRED) {
location->SetImportance(location::Importance::POSSIBLY_REQUIRED);
return chainLocation;
}
}
// If all remaining chain locations were not required, then this location is also not required
location->SetImportance(location::Importance::NOT_REQUIRED);
return nullptr;
}
static void CalculatePossibleBarrenRegions(const world::WorldPool& worlds) {
LOG_TO_DEBUG("Calculating Barren Regions");
for (const auto& world : worlds) {
auto defeatGanondorf = world->GetLocation("Defeat Ganondorf");
std::unordered_set ganondorfPathLocations(
defeatGanondorf->GetPathLocations().begin(),
defeatGanondorf->GetPathLocations().end()
);
for (auto& location: world->GetAllLocations()) {
// If this location is progression, then add its hint regions to
// the set of potentially barren regions
if (location->IsProgression()) {
for (const auto& locAccess : location->GetAccessList()) {
for (const auto& hintRegion : locAccess->GetArea()->GetHintRegions()) {
world->GetBarrenRegions()[hintRegion] = {};
}
}
}
// Set importance for locations that we can easily calculate as required or not
// required right now.
// Locations which contain barren items, or whose chain locations
// all contain barren items are not required
if (!location->GetCurrentItem()->IsMajor() || std::ranges::none_of(
location->GetCurrentItem()->GetChainLocations(), [](location::Location* loc) {
return loc->GetCurrentItem()->IsMajor();
}
)) {
location->SetImportance(location::Importance::NOT_REQUIRED);
// Locations which are on the path to Ganondorf are always required
} else if (ganondorfPathLocations.contains(location) || location == defeatGanondorf) {
location->SetImportance(location::Importance::REQUIRED);
}
}
// Any location which appears in the playthrough, but doesn't have an importance set
// yet, is going to be possibly required
const auto& playthrough = world->GetRandomizer()->GetPlaythroughSpheres();
for (const auto& sphere : playthrough) {
for (auto loc : sphere) {
if (loc->GetImportance() == location::Importance::UNKNOWN) {
loc->SetImportance(location::Importance::POSSIBLY_REQUIRED);
}
}
}
// Any remaining locations which don't have a hint importance set yet are either possibly required or not required.
// These locations' hint importance take much longer to calculate if we have complex hint importance on
for (const auto& location : world->GetAllLocations()) {
if (location->GetImportance() == location::Importance::UNKNOWN) {
std::unordered_set<location::Location*> currentlyChecking{};
auto blockLocation = CalculateLocationHintImportance(location, currentlyChecking);
if (blockLocation) {
LOG_TO_DEBUG(location->GetName() + ": " + location->GetCurrentItem()->GetName() +
" possibly required due to " + blockLocation->GetName() + ": " + blockLocation->GetCurrentItem()->GetName());
} else {
LOG_TO_DEBUG(location->GetName() + ": " + location->GetCurrentItem()->GetName() + " not required.");
}
}
}
// Now loop through all the progression locations again and remove
// any regions from the barren regions which have any possibly required or
// required items (unless they can be in a barren region). Otherwise, add
// the location to the list of locations in the barren region
for (auto location : world->GetAllLocations()) {
if (!location->IsProgression()) {
continue;
}
for (auto& locAccess : location->GetAccessList()) {
for (const auto& hintRegion : locAccess->GetArea()->GetHintRegions()) {
if (world->GetBarrenRegions().contains(hintRegion)) {
if (location->GetImportance() == location::Importance::NOT_REQUIRED ||
location->GetCurrentItem()->CanBeInBarrenRegion())
{
world->GetBarrenRegions()[hintRegion].push_back(location);
} else {
world->GetBarrenRegions().erase(hintRegion);
}
}
}
}
}
LOG_TO_DEBUG("Barren regions for world " + std::to_string(world->GetID()));
for (const auto& region : world->GetBarrenRegions() | std::views::keys) {
LOG_TO_DEBUG("- " + region)
}
}
}
// Chooses the appropriate plurality for the given text
static std::string ProcessHintPlurality(const std::string& text, bool plural) {
auto pluralStartIndex = text.find('|');
auto pluralEndIndex = text.find('|', pluralStartIndex + 1);
auto pluralSubstr = text.substr(pluralStartIndex + 1, pluralEndIndex - pluralStartIndex - 1);
auto pluralSubstrParts = utility::str::Split(pluralSubstr, '/');
std::string& chosenPluralityStr = plural ? pluralSubstrParts[1] : pluralSubstrParts[0];
return text.substr(0, pluralStartIndex) + chosenPluralityStr + text.substr(pluralEndIndex + 1);
}
static Text GeneratePathHintText(location::Location* location, location::Location* goalLocation) {
// Collect all the hint regions this location is in
std::set<std::string> hintRegionNames{};
for (const auto& locAcc : location->GetAccessList()) {
for (const auto& region : locAcc->GetArea()->GetHintRegions()) {
hintRegionNames.insert(region);
}
}
// Get the text object for each region and surround it with the color red
std::vector<Text> hintRegionText{};
std::ranges::transform(hintRegionNames, std::back_inserter(hintRegionText), [&](const std::string& region) {
return addColor(getTextObject(region), Text::RED);
});
// Make the regions into a listing
Text hintRegionListing = makeTextListing(hintRegionText);
// Get the goal name and apply its color
const auto& goalName = goalLocation->GetGoalName();
const auto& goalColor = bossColors.at(goalName);
Text goalNameText = goalColor + getTextObject(goalName) + "<white>";
// Construct the full text
Text fullText = getTextObject("Path Hint");
fullText.Replace("<regions>", hintRegionListing);
fullText.Replace("<goal name>", goalNameText);
// Handle plurality if necessary
for (auto& langText : fullText.mText) {
if (!langText.empty() && utility::str::Contains(langText, '|')) {
langText = ProcessHintPlurality(langText, hintRegionNames.size() > 1);
}
}
return fullText;
}
static Text GenerateBarrenHintText(const std::string& region) {
auto fullText = getTextObject("Barren Hint");
auto regionText = addColor(getTextObject(region), Text::PURPLE);
fullText.Replace("<region>", regionText);
return fullText;
}
static Text GenerateItemHintText(location::Location* location) {
// Collect all the hint regions this location is in
std::set<std::string> hintRegionNames{};
for (const auto& locAcc : location->GetAccessList()) {
for (const auto& region : locAcc->GetArea()->GetHintRegions()) {
hintRegionNames.insert(region);
}
}
// Get the text object for each region and surround it with the color red
std::vector<Text> hintRegionText{};
std::ranges::transform(hintRegionNames, std::back_inserter(hintRegionText), [&](const std::string& region) {
return addColor(getTextObject(region, Text::PRETTY), Text::RED);
});
// Make the regions into a listing
Text hintRegionListing = makeTextListing(hintRegionText);
// Get item text with color added
auto textType = Text::PRETTY;
Text itemText = addColor(getTextObject(location->GetCurrentItem()->GetName(), textType), Text::GREEN);
// TODO: Cryptic Text
Text fullText = getTextObject("Item Hint");
fullText.Replace("<regions>", hintRegionListing);
fullText.Replace("<Item Pretty or Cryptic Name>", itemText);
// Handle plurality if necessary
for (auto& langText : fullText.mText) {
if (!langText.empty() && utility::str::Contains(langText, '|')) {
langText = ProcessHintPlurality(langText, hintRegionNames.size() > 1);
}
}
return fullText;
}
static Text GenerateLocationHintText(location::Location* location) {
// TODO: Cryptic Text
auto textType = Text::PRETTY;
const auto& itemText = addColor(getTextObject(location->GetCurrentItem()->GetName(), textType), Text::GREEN);
Text fullText = getTextObject("Location Hint");
fullText.Replace("<Item Pretty or Cryptic Name>", itemText);
fullText.Replace("<Location Name>", addColor(Text{location->GetName()}, Text::RED));
return fullText;
}
HintGenerator::HintGenerator(world::World* world) : _world(world) {}
void HintGenerator::GeneratePathHints() {
// Shuffle each pool of path locations so that their orders are random
std::vector<location::Location*> goalLocationsList{};
auto defeatGanondorf = this->_world->GetLocation("Defeat Ganondorf");
for (auto& goalLocation : this->_world->GetGoalLocations()) {
utility::random::ShufflePool(goalLocation->GetPathLocations());
// Initially we want to create hints for required dungeon's goal locations
// and then add Ganondorf in once we have at least 1 hint for each required dungeon
if (goalLocation != defeatGanondorf) {
goalLocationsList.push_back(goalLocation);
}
}
bool addedGanondorfPathLocation = false;
auto numPathHints = this->_world->Setting("Number of Path Hints").GetCurrentOptionAsNumber();
for (size_t i = 0; i < numPathHints; ++i) {
// Try to get at least one hint for each required dungeon first
location::Location* goalLocation{};
if (i < goalLocationsList.size()) {
goalLocation = goalLocationsList[i];
} else {
// Once we've pulled from all required dungeons, then add ganondorf
// to the list and choose randomly
if (i == goalLocationsList.size() && !addedGanondorfPathLocation) {
addedGanondorfPathLocation = true;
goalLocationsList.push_back(defeatGanondorf);
}
if (goalLocationsList.empty()) {
LOG_TO_DEBUG("No more possible path hints")
break;
}
goalLocation = utility::random::RandomElement(goalLocationsList);
}
// Collect all valid path locations to hint for this location
std::vector<location::Location*> validPathLocations{};
for (auto location : goalLocation->GetPathLocations()) {
// Don't choose locations which have expected items or that are already hinted
if (location->HasExpectedItem() || location->IsHinted()) {
continue;
}
// This is unlikely, but also don't choose any locations which are logically
// necessary for accessing *every* possible hint sign if we're placing path hints
// on hint signs.
if (this->_world->Setting("Path Hints on Hint Signs") != "Off") {
// Get a list of all hint signs where we can place path hints to use later
auto possiblePathHintSigns = search::GetPossibleHintSigns(location);
// Remove the ones which wouldn't be relevant
std::erase_if(possiblePathHintSigns, [](location::Location* loc) {
const auto& hintSignPlacement = loc->GetWorld()->Setting("Path Hints on Hint Signs");
return (hintSignPlacement == "Overworld" && loc->HasCategories("Dungeon")) ||
(hintSignPlacement == "Dungeon" && loc->HasCategories("Overworld"));
});
// If none are left, then don't choose this location
if (possiblePathHintSigns.empty()) {
continue;
}
}
validPathLocations.push_back(location);
}
auto hintLocation = GetHintableLocation(validPathLocations);
if (hintLocation == nullptr) {
LOG_TO_DEBUG("No more path locations to hint for " + goalLocation->GetName())
utility::container::Erase(goalLocationsList, goalLocation);
--i;
continue;
}
hintLocation->SetHinted(true);
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + " as path hint for " + goalLocation->GetName());
auto hintText = GeneratePathHintText(hintLocation, goalLocation);
this->_pathHints.emplace_back(hintText, PathHint{goalLocation, hintLocation});
}
}
void HintGenerator::GenerateBarrenHints() {
std::vector<std::string> barrenPool = {};
std::vector<double> barrenDistributions = {};
for (auto& [barrenRegion, barrenLocations] : this->_world->GetBarrenRegions()) {
barrenPool.push_back(barrenRegion);
// The probability of a region being chosen for a barren hint is the square root
// of how many locations are in that region
barrenDistributions.push_back(sqrt(barrenLocations.size()));
}
std::discrete_distribution<size_t> barrenDistribution(barrenDistributions.begin(), barrenDistributions.end());
auto numBarrenHints = this->_world->Setting("Number of Barren Hints").GetCurrentOptionAsNumber();
for (size_t i = 0; i < numBarrenHints; ++i) {
if (barrenPool.empty()) {
LOG_TO_DEBUG("No more barren regions to hint at.")
break;
}
auto regionIndex = barrenDistribution(utility::random::GetGenerator());
auto& barrenRegion = barrenPool[regionIndex];
// Set all locations in the selected barren region as hinted at
// so we don't hint at them again
for (auto location : this->_world->GetBarrenRegions()[barrenRegion]) {
location->SetHinted(true);
}
auto hintText = GenerateBarrenHintText(barrenRegion);
this->_barrenHints.emplace_back(hintText, BarrenHint{barrenRegion});
LOG_TO_DEBUG("Chose " + barrenRegion + " as hinted barren region");
// Erase the hinted region from the pool
barrenPool.erase(barrenPool.begin() + regionIndex);
barrenDistributions.erase(barrenDistributions.begin() + regionIndex);
// Reset the distribution
barrenDistribution = std::discrete_distribution<size_t>(barrenDistributions.begin(), barrenDistributions.end());
}
}
void HintGenerator::GenerateItemHints() {
location::LocationPool possibleItemHintLocations{};
for (auto location : this->_world->GetAllLocations()) {
// If the location is progression...
// and has a major item...
// and does not have an expected item...
// and is not already hinted...
// then it can be hinted as an item hint
if (location->IsProgression() &&
location->GetCurrentItem()->IsMajor() &&
!location->HasExpectedItem() &&
!location->IsHinted())
{
possibleItemHintLocations.push_back(location);
}
}
// Choose randomly until we've selected the appropriate number of item hints
utility::random::ShufflePool(possibleItemHintLocations);
auto numItemHints = this->_world->Setting("Number of Item Hints").GetCurrentOptionAsNumber();
for (auto i = 0; i < numItemHints; ++i) {
if (possibleItemHintLocations.empty()) {
LOG_TO_DEBUG("No more possible item hint locations.")
break;
}
auto hintLocation = possibleItemHintLocations.back();
possibleItemHintLocations.pop_back();
hintLocation->SetHinted(true);
auto hintText = GenerateItemHintText(hintLocation);
this->_itemHints.emplace_back(hintText, ItemHint{hintLocation});
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + ": " + hintLocation->GetCurrentItem()->GetName() + " as item hint location")
}
}
void HintGenerator::GenerateRemoteLocationHints() {
// Return early if we're not prioritizing remote locations for location hints
if (this->_world->Setting("Prioritize Remote Location Hints") == "Off") {
return;
}
// Gather all the remote locations that are progression and haven't already been hinted
std::vector<location::Location*> remoteLocations{};
for (auto location : this->_world->GetAllLocations()) {
if (location->IsProgression() &&
location->HasCategories("Remote Location") &&
!location->IsHinted())
{
remoteLocations.push_back(location);
}
}
// Shuffle the pool
utility::random::ShufflePool(remoteLocations);
// Get as many remote locations as we can
auto numLocationHints = this->_world->Setting("Number of Location Hints").GetCurrentOptionAsNumber();
for (int i = 0; i < numLocationHints; ++i) {
if (i >= remoteLocations.size()) {
LOG_TO_DEBUG("All remote locations hinted");
break;
}
auto hintLocation = remoteLocations[i];
hintLocation->SetHinted(true);
auto hintText = GenerateLocationHintText(hintLocation);
this->_locationHints.emplace_back(hintText, LocationHint{hintLocation});
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + " as remote location hint location")
}
}
void HintGenerator::GenerateLocationHints() {
// Collect all possible locations to hint
std::vector<location::Location*> possibleLocationHintLocations{};
for (auto location : this->_world->GetAllLocations()) {
if (location->IsProgression() && !location->IsHinted() && !location->HasExpectedItem()) {
possibleLocationHintLocations.push_back(location);
}
}
// Shuffle the pool
utility::random::ShufflePool(possibleLocationHintLocations);
// Get the necessary number of hints we want. If we failed to generate enough hints for any of
// the other hint types, compensate by adding the difference to the number of location hints
auto numLocationHints = this->_world->Setting("Number of Location Hints").GetCurrentOptionAsNumber();
auto expectedNumHints = this->_world->Setting("Number of Path Hints").GetCurrentOptionAsNumber() +
this->_world->Setting("Number of Barren Hints").GetCurrentOptionAsNumber() +
this->_world->Setting("Number of Item Hints").GetCurrentOptionAsNumber();
auto totalNumHints = this->_pathHints.size() + this->_barrenHints.size() +
this->_itemHints.size() + this->_locationHints.size();
numLocationHints += expectedNumHints - totalNumHints;
size_t curNumLocationHints = this->_locationHints.size();
// Choose location hints until we've filled up the amount we need. Since we previously may
// have already chosen some remote location hints, start with the number we already have
for (auto i = curNumLocationHints; i < numLocationHints; ++i) {
if (possibleLocationHintLocations.empty()) {
LOG_TO_DEBUG("No more possible location hint locations.")
break;
}
auto hintLocation = possibleLocationHintLocations.back();
possibleLocationHintLocations.pop_back();
hintLocation->SetHinted(true);
auto hintText = GenerateLocationHintText(hintLocation);
this->_locationHints.emplace_back(hintText, LocationHint{hintLocation});
LOG_TO_DEBUG("Chose " + hintLocation->GetName() + " as location hint location")
}
}
static void AssignHintSignHints(const location::LocationPool& hintSigns, std::vector<Hint> hints, world::World* world) {
size_t hintsPerSign = std::ceil(static_cast<double>(hints.size()) / static_cast<double>(hintSigns.size()));
auto& worlds = world->GetRandomizer()->GetWorlds();
auto& hintSignHints = world->GetHintSignHints();
auto hintSignHintsOriginal = hintSignHints;
// Keep trying to place hints until all have been logically placed at least once
bool successfullyPlaceHints = false;
int retryCount = 50;
while (!successfullyPlaceHints) {
--retryCount;
if (retryCount < 0) {
throw std::runtime_error("Failed to properly place hint sign hints. "
"Try using a different seed for generation");
}
// Clear any previous attempt at placing hints
successfullyPlaceHints = true;
hintSignHints = hintSignHintsOriginal;
for (auto& hint : hints) {
item::Item* itemAtHintLocation{};
location::Location* hintLocation{};
if (auto pathHint = std::get_if<PathHint>(&hint.data)) {
hintLocation = pathHint->hintedLocation;
} else if (auto itemHint = std::get_if<ItemHint>(&hint.data)) {
hintLocation = itemHint->location;
}
else if (auto locationHint = std::get_if<LocationHint>(&hint.data)) {
hintLocation = locationHint->location;
}
// Remove this item from the world and see which hint signs are available to place hints
if (hintLocation != nullptr) {
itemAtHintLocation = hintLocation->GetCurrentItem();
hintLocation->RemoveCurrentItem();
}
auto search = search::Search::Accessible(&worlds);
search.SearchWorlds();
location::LocationPool availableHintSigns{};
for (const auto& sign : hintSigns) {
if (search._visitedLocations.contains(sign) && hintSignHints[sign].size() < hintsPerSign) {
availableHintSigns.emplace_back(sign);
}
}
if (availableHintSigns.empty()) {
LOG_TO_DEBUG("No available hint signs to place hint " + hint.text.mText[Text::ENGLISH]);
if (hintLocation != nullptr) {
hintLocation->SetCurrentItem(itemAtHintLocation);
}
successfullyPlaceHints = false;
break;
}
// Place the hint at the hint sign
auto hintSign = utility::random::RandomElement(availableHintSigns);
hintSignHints[hintSign].push_back(hint);
if (hintLocation != nullptr) {
hintLocation->SetCurrentItem(itemAtHintLocation);
}
}
}
// Once we've placed every hint at least once, duplicate hints
// and place them randomly until all hint signs have the
// necessary number of hints. Don't check for logic here since
// every hint can already be logically accessed at some sign
auto duplicateHints = hints;
location::LocationPool availableHintSigns{};
for (const auto& sign : hintSigns) {
if (hintSignHints[sign].size() < hintsPerSign) {
availableHintSigns.emplace_back(sign);
}
}
utility::random::ShufflePool(availableHintSigns);
while (!availableHintSigns.empty()) {
// Reset the pool of duplicate hints if we run out.
// This way we'll duplicate every hint at least n times
// before potentially duplicating each hint n + 1 times
if (duplicateHints.empty()) {
duplicateHints = hints;
}
auto hint = duplicateHints.back();
duplicateHints.pop_back();
location::Location* chosenSign{};
for (auto sign : availableHintSigns) {
// Don't give the same hint to a single sign multiple times
if (!utility::container::ElementInContainer(hintSignHints[sign], hint)) {
chosenSign = sign;
break;
}
}
if (chosenSign == nullptr) {
LOG_TO_DEBUG("Could not find any gossip stones to place hint " + hint.text.mText[Text::ENGLISH] + " Trying a different hint.")
continue;
}
hintSignHints[chosenSign].push_back(hint);
if (hintSignHints[chosenSign].size() >= hintsPerSign) {
utility::container::Erase(availableHintSigns, chosenSign);
}
}
}
void HintGenerator::DistributeHints() {
// Pools to distribute hints
std::vector<Hint> overworldSignHints{};
std::vector<Hint> dungeonSignHints{};
std::vector<Hint> anySignHints{};
static constexpr int MIDNA = 0;
static constexpr int HINT_SIGN = 1;
// Helper function for distributing hints to their appropriate places
auto distributeHintTypeToPools = [&](std::vector<Hint>& hints, const std::string& hintType) {
std::vector<int> hintAssignments{};
if (this->_world->Setting(hintType + " on Midna") != "Off") {
hintAssignments.push_back(MIDNA);
}
if (this->_world->Setting(hintType + " on Hint Signs") != "Off") {
hintAssignments.push_back(HINT_SIGN);
}
// If the user didn't choose any place to assign these hints, return early
if (hintAssignments.empty()) {
return;
}
for (size_t i = 0; i < hints.size(); ++i) {
auto& pathHint = hints[i];
auto distributeTo = hintAssignments[i % hintAssignments.size()];
if (distributeTo == MIDNA) {
this->_world->GetMidnaHints().push_back(pathHint);
} else if (distributeTo == HINT_SIGN) {
auto& pathHintSigns = this->_world->Setting(hintType + " on Hint Signs");
if (pathHintSigns == "Overworld") {
overworldSignHints.push_back(pathHint);
} else if (pathHintSigns == "Dungeon") {
dungeonSignHints.push_back(pathHint);
} else if (pathHintSigns == "Any") {
anySignHints.push_back(pathHint);
}
}
}
};
distributeHintTypeToPools(this->_pathHints, "Path Hints");
distributeHintTypeToPools(this->_barrenHints, "Barren Hints");
distributeHintTypeToPools(this->_itemHints, "Item Hints");
distributeHintTypeToPools(this->_locationHints, "Location Hints");
// Get our pools of overworld and dungeon signs
location::LocationPool overworldSignLocations{};
location::LocationPool dungeonSignLocations{};
for (auto hintSign : this->_world->GetHintSignLocations()) {
if (hintSign->HasCategories("Overworld")) {
overworldSignLocations.push_back(hintSign);
}
if (hintSign->HasCategories("Dungeon")) {
dungeonSignLocations.push_back(hintSign);
}
}
utility::random::ShufflePool(anySignHints);
// Distribute our hints which can be on any sign to either overworld or dungeon. Ideally
// we fill up each pool of hints to match the number of signs there are equally. Only do
// this if we're already placing some hints on only dungeon, or only overworld hint signs.
if (!overworldSignHints.empty() || !dungeonSignHints.empty()) {
for (size_t i = 0; i < anySignHints.size(); ++i) {
const auto& anySignHint = anySignHints[i];
size_t hintsPerOverworldSign = overworldSignHints.size() / overworldSignLocations.size();
size_t hintsPerDungeonSign = dungeonSignHints.size() / dungeonSignLocations.size();
if (hintsPerOverworldSign < hintsPerDungeonSign) {
overworldSignHints.push_back(anySignHint);
} else if (hintsPerDungeonSign < hintsPerOverworldSign) {
dungeonSignHints.push_back(anySignHint);
} else {
if (dungeonSignHints.empty() || (dungeonSignHints.size() % dungeonSignLocations.size() != 0 && i % 2 != 0)) {
dungeonSignHints.push_back(anySignHint);
} else {
overworldSignHints.push_back(anySignHint);
}
}
}
anySignHints.clear();
}
// Now distribute hints to our hint signs
// If no hints have to be constrained to either overworld or dungeon signs,
// then distribute all hints over all signs
if (overworldSignHints.empty() && dungeonSignHints.empty()) {
AssignHintSignHints(this->_world->GetHintSignLocations(), anySignHints, this->_world);
}
if (!overworldSignHints.empty()) {
AssignHintSignHints(overworldSignLocations, overworldSignHints, this->_world);
}
if (!dungeonSignHints.empty()) {
AssignHintSignHints(dungeonSignLocations, dungeonSignHints, this->_world);
}
}
void HintGenerator::FinalizeHintSignText() {
for (const auto& [sign, hints] : this->_world->GetHintSignHints()) {
auto key = sign->GetName() + " Text";
auto& signText = this->_world->AddNewText(key);
// Put on all the hints
for (const auto& hint : hints) {
signText += hint.text;
signText.PadToNextBox();
}
// pop off last '\n' so we don't have an extra blank textbox
for (auto& text : signText.mText) {
text.pop_back();
}
}
}
// Tell the player which dungeons are required on the sign in front of Link's House
static void GenerateRequiredDungeonsHint(world::WorldPool& worlds) {
for (const auto& world : worlds) {
auto& requiredDungeonText = world->AddNewText("Links House Sign");
// Use dungeonColors to loop through in base game dungeon order
for (const auto& [dungeonName, color] : dungeonColors) {
// Hyrule Castle is implicitly required
if (dungeonName == "Hyrule Castle") {
continue;
}
auto dungeon = world->GetDungeon(dungeonName);
if (dungeon->IsRequired()) {
requiredDungeonText += color + getTextObject(dungeonName) + "\n";
}
}
if (requiredDungeonText.Empty()) {
requiredDungeonText += getTextObject("No Required Dungeons Text");
}
}
}
static void doItemTextReplacement(const std::unique_ptr<world::World>& world,
const std::string& locationName,
const std::list<std::string>& textNames,
Text::Color color) {
auto itemName = world->GetLocation(locationName)->GetCurrentItem()->GetName();
auto itemStandardName = addColor(getTextObject(itemName), color);
auto itemPrettyName = addColor(getTextObject(itemName, Text::PRETTY), color);
for (const auto& textName : textNames) {
auto& text = world->AddNewText(textName);
text = getTextObject(textName + " Template");
text.Replace("<Item Standard Name>", itemStandardName);
text.Replace("<Item Pretty Name>", itemPrettyName);
text.Capitalize();
text.BreakLines();
}
}
static void GenerateItemTextReplacements(world::WorldPool& worlds) {
for (const auto& world : worlds) {
doItemTextReplacement(world, "Fishing Hole Bottle", {"Fishing Hole Sign Text"}, Text::GREEN);
doItemTextReplacement(world, "Charlo Donation Blessing", {"Charlo Donation Ask Text"}, Text::GREEN);
doItemTextReplacement(world, "Sera Shop Slingshot", {"Slingshot Shop Text",
"Slingshot Shop Too Expensive Text", "Slingshot Shop Purchase Confirmation Text",
"Slingshot Shop After Purchase Text"}, Text::ORANGE);
doItemTextReplacement(world, "Barnes Bomb Bag", {"Barnes Special Offer Text"}, Text::ORANGE);
doItemTextReplacement(world, "Kakariko Village Malo Mart Wooden Shield", {"Kakariko Malo Mart Wooden Shield Purchase Confirmation Text",
"Kakariko Malo Mart Wooden Shield Too Expensive Text", "Kakariko Malo Mart Wooden Shield Text"}, Text::ORANGE);
doItemTextReplacement(world, "Kakariko Village Malo Mart Hylian Shield", {"Kakariko Malo Mart Hylian Shield Purchase Confirmation Text",
"Kakariko Malo Mart Hylian Shield Too Expensive Text", "Kakariko Malo Mart Hylian Shield After Purchase Text",
"Kakariko Malo Mart Hylian Shield Text"}, Text::ORANGE);
doItemTextReplacement(world, "Kakariko Village Malo Mart Red Potion", {"Kakariko Malo Mart Red Potion Too Expensive Text",
"Kakariko Malo Mart Red Potion Purchase Confirmation Text", "Kakariko Malo Mart Red Potion Text"}, Text::ORANGE);
doItemTextReplacement(world, "Kakariko Village Malo Mart Hawkeye", {"Kakariko Malo Mart Hawkeye Purchase Confirmation Text",
"Kakariko Malo Mart Hawkeye Too Expensive Text", "Kakariko Malo Mart Hawkeye After Purchase Text",
"Kakariko Malo Mart Hawkeye Coming Soon Text", "Kakariko Malo Mart Hawkeye Text"}, Text::ORANGE);
doItemTextReplacement(world, "Castle Town Malo Mart Magic Armor", {"Chudleys Shop Magic Armor Text",
"Castle Town Malo Mart Magic Armor After Purchase Text", "Castle Town Malo Mart Magic Armor Text",
"Castle Town Malo Mart Magic Armor Sold Out Text"}, Text::ORANGE);
doItemTextReplacement(world, "Coro Bottle", {"Coro Bottle Offer 1 Text",
"Coro Bottle Offer 2 Text", "Coro Bottle Offer 3 Text", "Coro Bottle Offer 4 Text"}, Text::ORANGE);
}
}
void GenerateMidnaHintsText(world::WorldPool& worlds) {
for (const auto& world : worlds) {
auto& midnaHintText = world->AddNewText("Custom Midna Call Hints Text");
// Put required dungeons on Midna.
// First loop through to get required number
int numRequiredDungeons = 0;
for (const auto& dungeon : world->GetDungeonTable() | std::views::values) {
if (dungeon->IsRequired()) {
++numRequiredDungeons;
}
}
// Set the text for the number of required dungeons
if (numRequiredDungeons > 0) {
midnaHintText += getTextObject("Midna Hints Required Dungeons Intro At Least One Dungeon");
midnaHintText.Replace("<required dungeon count>", std::to_string(numRequiredDungeons));
midnaHintText.PadToNextBox();
// Then loop through again to add the dungeon names.
// Use dungeonColors to loop through in base game dungeon order
for (const auto& [dungeonName, color] : dungeonColors) {
// Hyrule Castle is implicitly required
if (dungeonName == "Hyrule Castle") {
continue;
}
auto dungeon = world->GetDungeon(dungeonName);
if (dungeon->IsRequired()) {
midnaHintText += color + getTextObject(dungeonName) + "<white>\n";
}
}
} else {
midnaHintText += getTextObject("Midna Hints Required Dungeons Intro Zero Dungeons");
}
midnaHintText.PadToNextBox();
// Separate Midna hints based on type
std::vector<Hint> midnaPathHints{};
std::vector<Hint> midnaBarrenHints{};
std::vector<Hint> midnaItemHints{};
std::vector<Hint> midnaLocationHints{};
for (auto& hint : world->GetMidnaHints()) {
if (std::holds_alternative<PathHint>(hint.data)) {
midnaPathHints.push_back(hint);
} else if (std::holds_alternative<BarrenHint>(hint.data)) {
midnaBarrenHints.push_back(hint);
} else if (std::holds_alternative<ItemHint>(hint.data)) {
midnaItemHints.push_back(hint);
} else if (std::holds_alternative<LocationHint>(hint.data)) {
midnaLocationHints.push_back(hint);
}
}
// Helper function for adding hint text based on type
auto addHintsToMidnaText = [&](const std::vector<Hint>& hints, const std::string& type) {
if (!hints.empty()) {
midnaHintText += getTextObject("Midna Hints " + type + " Intro");
midnaHintText.PadToNextBox();
for (auto& [text, data] : hints) {
midnaHintText += text;
midnaHintText.PadToNextBox();
}
}
};
addHintsToMidnaText(midnaPathHints, "Path Hints");
// Barren hints are presented slightly differently
if (!midnaBarrenHints.empty()) {
midnaHintText += getTextObject("Midna Hints Barren Hints Intro");
midnaHintText.PadToNextBox();
for (auto& [text, data] : midnaBarrenHints) {
midnaHintText += addColor(getTextObject(std::get<BarrenHint>(data).region), Text::PURPLE) + "\n";
}
midnaHintText.PadToNextBox();
}
addHintsToMidnaText(midnaItemHints, "Item Hints");
addHintsToMidnaText(midnaLocationHints, "Location Hints");
// pop off the last new line so we don't get an extra blank textbox
for (auto& text : midnaHintText.mText) {
text.pop_back();
}
}
}
void GenerateAllHints(world::WorldPool& worlds) {
utility::platform::Log("Generating Hints...");
CalculatePossiblePathLocations(worlds);
CalculatePossibleBarrenRegions(worlds);
for (const auto& world : worlds) {
auto hintGenerator = HintGenerator{world.get()};
// Select barren hints first, as we don't want hinted barren regions to conflict with
// location hints
hintGenerator.GenerateBarrenHints();
// Select remote location hints next as these are the most specific ones.
hintGenerator.GenerateRemoteLocationHints();
// Then come path and item hints which hint at specific locations that have good items
hintGenerator.GeneratePathHints();
hintGenerator.GenerateItemHints();
// And finally regular location hints can just be any location
hintGenerator.GenerateLocationHints();
hintGenerator.DistributeHints();
hintGenerator.FinalizeHintSignText();
}
GenerateRequiredDungeonsHint(worlds);
GenerateItemTextReplacements(worlds);
GenerateMidnaHintsText(worlds);
}
}