Files
Shiip-of-Hakinian-Espanol/soh/soh/Enhancements/randomizer/3drando/hints.cpp
Philip Dubé 44d351698b Shuffle Masks (#5536)
Future improvements can build on this,
giving masks abilities,
or adding checks for trading mask to someone
2026-01-17 05:00:03 +00:00

877 lines
40 KiB
C++

#include "hints.hpp"
#include "item_pool.hpp"
#include "random.hpp"
#include "spoiler_log.hpp"
#include "fill.hpp"
#include "../trial.h"
#include "../entrance.h"
#include "z64item.h"
#include <spdlog/spdlog.h>
#include "../randomizerTypes.h"
#include "pool_functions.hpp"
#include "../hint.h"
#include "../static_data.h"
using namespace Rando;
HintDistributionSetting::HintDistributionSetting(std::string _name, HintType _type, uint32_t _weight, uint8_t _fixed,
uint8_t _copies, std::function<bool(RandomizerCheck)> _filter,
uint8_t _dungeonLimit) {
name = _name;
type = _type;
weight = _weight;
fixed = _fixed;
copies = _copies;
filter = _filter;
dungeonLimit = _dungeonLimit;
}
HintText::HintText(CustomMessage clearText_, std::vector<CustomMessage> ambiguousText_,
std::vector<CustomMessage> obscureText_)
: clearText(std::move(clearText_)), ambiguousText(std::move(ambiguousText_)), obscureText(std::move(obscureText_)) {
}
const CustomMessage& HintText::GetClear() const {
return clearText;
}
const CustomMessage& HintText::GetObscure() const {
return obscureText.size() > 0 ? RandomElement(obscureText) : clearText;
}
const CustomMessage& HintText::GetObscure(size_t selection) const {
if (obscureText.size() > selection) {
return obscureText[selection];
} else if (obscureText.size() > 0) {
return obscureText[0];
}
return clearText;
}
const CustomMessage& HintText::GetAmbiguous() const {
return ambiguousText.size() > 0 ? RandomElement(ambiguousText) : clearText;
}
const CustomMessage& HintText::GetAmbiguous(size_t selection) const {
if (ambiguousText.size() > selection) {
return ambiguousText[selection];
} else if (ambiguousText.size() > 0) {
return ambiguousText[0];
}
return clearText;
}
size_t HintText::GetAmbiguousSize() const {
return ambiguousText.size();
}
size_t HintText::GetObscureSize() const {
return obscureText.size();
}
const CustomMessage& HintText::GetHintMessage(size_t selection) const {
auto ctx = Rando::Context::GetInstance();
if (ctx->GetOption(RSK_HINT_CLARITY).Is(RO_HINT_CLARITY_OBSCURE)) {
return GetObscure(selection);
} else if (ctx->GetOption(RSK_HINT_CLARITY).Is(RO_HINT_CLARITY_AMBIGUOUS)) {
return GetAmbiguous(selection);
} else {
return GetClear();
}
}
const CustomMessage HintText::GetMessageCopy() const {
auto ctx = Rando::Context::GetInstance();
if (ctx->GetOption(RSK_HINT_CLARITY).Is(RO_HINT_CLARITY_OBSCURE)) {
return GetObscure();
} else if (ctx->GetOption(RSK_HINT_CLARITY).Is(RO_HINT_CLARITY_AMBIGUOUS)) {
return GetAmbiguous();
} else {
return GetClear();
}
}
bool HintText::operator==(const HintText& right) const {
return obscureText == right.obscureText && ambiguousText == right.ambiguousText && clearText == right.clearText;
}
bool HintText::operator!=(const HintText& right) const {
return !operator==(right);
}
StaticHintInfo::StaticHintInfo(HintType _type, std::vector<RandomizerHintTextKey> _hintKeys,
RandomizerSettingKey _setting, std::variant<bool, uint8_t> _condition,
std::vector<RandomizerCheck> _targetChecks, std::vector<RandomizerGet> _targetItems,
std::vector<RandomizerCheck> _hintChecks, bool _yourPocket, int _num)
: type(_type), hintKeys(_hintKeys), setting(_setting), condition(_condition), targetChecks(_targetChecks),
targetItems(_targetItems), hintChecks(_hintChecks), yourPocket(_yourPocket), num(_num) {
}
RandomizerHintTextKey GetRandomJunkHint() {
// Temp code to handle random junk hints now I work in keys instead of a vector of HintText
// Will be replaced with a better system once more customisable hint pools are added
uint32_t range = RHT_JUNK71 - RHT_JUNK01;
return (RandomizerHintTextKey)(Random(0, range) + RHT_JUNK01);
}
RandomizerHintTextKey GetRandomGanonJoke() {
uint32_t range = RHT_GANON_JOKE11 - RHT_GANON_JOKE01;
return (RandomizerHintTextKey)(Random(0, range) + RHT_GANON_JOKE01);
}
bool FilterWotHLocations(RandomizerCheck loc) {
auto ctx = Rando::Context::GetInstance();
return ctx->GetItemLocation(loc)->IsWothCandidate();
}
bool FilterFoolishLocations(RandomizerCheck loc) {
auto ctx = Rando::Context::GetInstance();
return ctx->GetItemLocation(loc)->IsFoolishCandidate();
}
bool FilterSongLocations(RandomizerCheck loc) {
auto ctx = Rando::Context::GetInstance();
return Rando::StaticData::GetLocation(loc)->GetRCType() == RCTYPE_SONG_LOCATION;
}
bool FilterOverworldLocations(RandomizerCheck loc) {
auto ctx = Rando::Context::GetInstance();
return Rando::StaticData::GetLocation(loc)->IsOverworld();
}
bool FilterDungeonLocations(RandomizerCheck loc) {
auto ctx = Rando::Context::GetInstance();
return Rando::StaticData::GetLocation(loc)->IsDungeon();
}
bool FilterGoodItems(RandomizerCheck loc) {
auto ctx = Rando::Context::GetInstance();
return ctx->GetItemLocation(loc)->GetPlacedItem().IsMajorItem();
}
bool NoFilter(RandomizerCheck loc) {
return true;
}
const std::array<HintSetting, 4> hintSettingTable{{
// Useless hints
{
.alwaysCopies = 0,
.trialCopies = 0,
.junkWeight = 1, //RANDOTODO when the hint pool is not implicitly an itemLocations, handle junk like other hint types
.distTable = {} /*RANDOTODO Instead of loading a function into this,
apply this filter on all possible hintables in advance and then filter by what is acually in the seed at the start of generation.
This allows the distTable to hold the current status in hint generation (reducing potential doubled work) and
will make handling custom hint pools easier later*/
},
// Balanced hints
{
.alwaysCopies = 1,
.trialCopies = 1,
.junkWeight = 6,
.distTable = {
{"WotH", HINT_TYPE_WOTH, 7, 0, 1, FilterWotHLocations, 2},
{"Foolish", HINT_TYPE_FOOLISH, 4, 0, 1, FilterFoolishLocations, 1},
//("Entrance", HINT_TYPE_ENTRANCE, 6, 0, 1), //not yet implemented
{"Song", HINT_TYPE_ITEM, 2, 0, 1, FilterSongLocations},
{"Overworld", HINT_TYPE_ITEM, 4, 0, 1, FilterOverworldLocations},
{"Dungeon", HINT_TYPE_ITEM, 3, 0, 1, FilterDungeonLocations},
{"Named Item", HINT_TYPE_ITEM_AREA, 10, 0, 1, FilterGoodItems},
{"Random" , HINT_TYPE_ITEM_AREA, 12, 0, 1, NoFilter}
}
},
// Strong hints
{
.alwaysCopies = 2,
.trialCopies = 1,
.junkWeight = 0,
.distTable = {
{"WotH", HINT_TYPE_WOTH, 12, 0, 2, FilterWotHLocations, 2},
{"Foolish", HINT_TYPE_FOOLISH, 12, 0, 1, FilterFoolishLocations, 1},
//{"Entrance", HINT_TYPE_ENTRANCE, 4, 0, 1}, //not yet implemented
{"Song", HINT_TYPE_ITEM, 4, 0, 1, FilterSongLocations},
{"Overworld", HINT_TYPE_ITEM, 6, 0, 1, FilterOverworldLocations},
{"Dungeon", HINT_TYPE_ITEM, 6, 0, 1, FilterDungeonLocations},
{"Named Item", HINT_TYPE_ITEM_AREA, 8, 0, 1, FilterGoodItems},
{"Random" , HINT_TYPE_ITEM_AREA, 8, 0, 1, NoFilter},
},
},
// Very strong hints
{
.alwaysCopies = 2,
.trialCopies = 1,
.junkWeight = 0,
.distTable = {
{"WotH", HINT_TYPE_WOTH, 15, 0, 2, FilterWotHLocations},
{"Foolish", HINT_TYPE_FOOLISH, 15, 0, 1, FilterFoolishLocations},
//{"Entrance", HINT_TYPE_ENTRANCE, 10, 0, 1}, //not yet implemented
{"Song", HINT_TYPE_ITEM, 2, 0, 1, FilterSongLocations},
{"Overworld", HINT_TYPE_ITEM, 7, 0, 1, FilterOverworldLocations},
{"Dungeon", HINT_TYPE_ITEM, 7, 0, 1, FilterDungeonLocations},
{"Named Item", HINT_TYPE_ITEM_AREA, 5, 0, 1, FilterGoodItems},
},
},
}};
uint8_t StonesRequiredBySettings() {
auto ctx = Rando::Context::GetInstance();
uint8_t stones = 0;
if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_STONES)) {
stones = ctx->GetOption(RSK_RAINBOW_BRIDGE_STONE_COUNT).Get();
} else if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_DUNGEON_REWARDS)) {
stones = ctx->GetOption(RSK_RAINBOW_BRIDGE_REWARD_COUNT).Get() - 6;
} else if ((ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_DUNGEONS)) &&
(ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_END_OF_DUNGEON))) {
stones = ctx->GetOption(RSK_RAINBOW_BRIDGE_DUNGEON_COUNT).Get() - 6;
}
if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_LACS_STONES)) {
stones = std::max<uint8_t>({ stones, ctx->GetOption(RSK_LACS_STONE_COUNT).Get() });
} else if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_LACS_REWARDS)) {
stones = std::max<uint8_t>({ stones, (uint8_t)(ctx->GetOption(RSK_LACS_REWARD_COUNT).Get() - 6) });
} else if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_LACS_DUNGEONS) &&
ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_END_OF_DUNGEON)) {
stones = std::max<uint8_t>({ stones, (uint8_t)(ctx->GetOption(RSK_LACS_DUNGEON_COUNT).Get() - 6) });
}
return stones;
}
uint8_t MedallionsRequiredBySettings() {
auto ctx = Rando::Context::GetInstance();
uint8_t medallions = 0;
if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_MEDALLIONS)) {
medallions = ctx->GetOption(RSK_RAINBOW_BRIDGE_MEDALLION_COUNT).Get();
} else if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_DUNGEON_REWARDS)) {
medallions = ctx->GetOption(RSK_RAINBOW_BRIDGE_REWARD_COUNT).Get() - 3;
} else if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_DUNGEONS) &&
ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_END_OF_DUNGEON)) {
medallions = ctx->GetOption(RSK_RAINBOW_BRIDGE_DUNGEON_COUNT).Get() - 3;
}
if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_LACS_MEDALLIONS)) {
medallions = std::max(medallions, ctx->GetOption(RSK_LACS_MEDALLION_COUNT).Get());
} else if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_LACS_REWARDS)) {
medallions = std::max(medallions, (uint8_t)(ctx->GetOption(RSK_LACS_REWARD_COUNT).Get() - 3));
} else if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_LACS_DUNGEONS) &&
ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_END_OF_DUNGEON)) {
medallions = std::max(medallions, (uint8_t)(ctx->GetOption(RSK_LACS_DUNGEON_COUNT).Get() - 3));
}
return medallions;
}
uint8_t TokensRequiredBySettings() {
auto ctx = Rando::Context::GetInstance();
uint8_t tokens = 0;
if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_TOKENS)) {
tokens = ctx->GetOption(RSK_RAINBOW_BRIDGE_TOKEN_COUNT).Get();
}
if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_LACS_TOKENS)) {
tokens = std::max<uint8_t>(tokens, ctx->GetOption(RSK_LACS_TOKEN_COUNT).Get());
}
return tokens;
}
std::vector<std::pair<RandomizerCheck, std::function<bool()>>> conditionalAlwaysHints = {
std::make_pair(RC_MARKET_10_BIG_POES,
[]() {
auto ctx = Rando::Context::GetInstance();
return ctx->GetOption(RSK_BIG_POE_COUNT).Get() > 3 && !ctx->GetOption(RSK_BIG_POES_HINT);
}),
std::make_pair(RC_DEKU_THEATER_MASK_OF_TRUTH,
[]() {
auto ctx = Rando::Context::GetInstance();
return !ctx->GetOption(RSK_MASK_SHOP_HINT) && !ctx->GetOption(RSK_MASK_QUEST);
}),
std::make_pair(RC_SONG_FROM_OCARINA_OF_TIME,
[]() {
auto ctx = Rando::Context::GetInstance();
return StonesRequiredBySettings() < 2 && !ctx->GetOption(RSK_OOT_HINT);
}),
std::make_pair(RC_HF_OCARINA_OF_TIME_ITEM, []() { return StonesRequiredBySettings() < 2; }),
std::make_pair(RC_SHEIK_IN_KAKARIKO, []() { return MedallionsRequiredBySettings() < 5; }),
std::make_pair(RC_DMT_TRADE_CLAIM_CHECK,
[]() {
auto ctx = Rando::Context::GetInstance();
return !ctx->GetOption(RSK_BIGGORON_HINT);
}),
std::make_pair(RC_KAK_30_GOLD_SKULLTULA_REWARD,
[]() {
auto ctx = Rando::Context::GetInstance();
return !ctx->GetOption(RSK_KAK_30_SKULLS_HINT) && TokensRequiredBySettings() < 30;
}),
std::make_pair(RC_KAK_40_GOLD_SKULLTULA_REWARD,
[]() {
auto ctx = Rando::Context::GetInstance();
return !ctx->GetOption(RSK_KAK_40_SKULLS_HINT) && TokensRequiredBySettings() < 40;
}),
std::make_pair(RC_KAK_50_GOLD_SKULLTULA_REWARD,
[]() {
auto ctx = Rando::Context::GetInstance();
return !ctx->GetOption(RSK_KAK_50_SKULLS_HINT) && TokensRequiredBySettings() < 50;
}),
std::make_pair(RC_ZR_FROGS_OCARINA_GAME,
[]() {
auto ctx = Rando::Context::GetInstance();
return !ctx->GetOption(RSK_FROGS_HINT);
}),
std::make_pair(RC_KF_LINKS_HOUSE_COW,
[]() {
auto ctx = Rando::Context::GetInstance();
return !ctx->GetOption(RSK_MALON_HINT);
}),
std::make_pair(RC_KAK_100_GOLD_SKULLTULA_REWARD,
[]() {
auto ctx = Rando::Context::GetInstance();
return !ctx->GetOption(RSK_KAK_100_SKULLS_HINT) && TokensRequiredBySettings() < 100;
}),
};
static std::vector<RandomizerCheck> GetEmptyGossipStones() {
auto emptyGossipStones = GetEmptyLocations(Rando::StaticData::GetGossipStoneLocations());
return emptyGossipStones;
}
static std::vector<RandomizerCheck> GetAccessibleGossipStones(const RandomizerCheck hintedLocation = RC_GANON) {
auto ctx = Rando::Context::GetInstance();
// temporarily remove the hinted location's item, and then perform a
// reachability search for gossip stone locations.
RandomizerGet originalItem = ctx->GetItemLocation(hintedLocation)->GetPlacedRandomizerGet();
ctx->GetItemLocation(hintedLocation)->SetPlacedItem(RG_NONE);
ctx->GetLogic()->Reset();
auto accessibleGossipStones = ReachabilitySearch(Rando::StaticData::GetGossipStoneLocations());
// Give the item back to the location
ctx->GetItemLocation(hintedLocation)->SetPlacedItem(originalItem);
return accessibleGossipStones;
}
bool IsReachableWithout(std::vector<RandomizerCheck> locsToCheck, RandomizerCheck excludedCheck,
bool resetAfter = true) {
// temporarily remove the hinted location's item, and then perform a
// reachability search for this check RANDOTODO convert excludedCheck to an ItemLocation
auto ctx = Rando::Context::GetInstance();
RandomizerGet originalItem = ctx->GetItemLocation(excludedCheck)->GetPlacedRandomizerGet();
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(RG_NONE);
ctx->GetLogic()->Reset();
const auto rechableWithout = ReachabilitySearch(locsToCheck);
ctx->GetItemLocation(excludedCheck)->SetPlacedItem(originalItem);
if (resetAfter) {
// if resetAfter is on, reset logic we are done
ctx->GetLogic()->Reset();
}
if (rechableWithout.empty()) {
return false;
}
return true;
}
static void SetAllInAreaAsHintAccesible(RandomizerArea area, std::vector<RandomizerCheck> locations) {
auto ctx = Rando::Context::GetInstance();
std::vector<RandomizerCheck> locsInArea = FilterFromPool(locations, [area, ctx](const RandomizerCheck loc) {
return ctx->GetItemLocation(loc)->GetAreas().contains(area);
});
for (RandomizerCheck loc : locsInArea) {
ctx->GetItemLocation(loc)->SetHintAccesible();
}
}
static void AddGossipStoneHint(const RandomizerCheck gossipStone, const HintType hintType,
const std::string distributionName, const std::vector<RandomizerHintTextKey> hintKeys,
const std::vector<RandomizerCheck> locations, const std::vector<RandomizerArea> areas,
const std::vector<TrialKey> trials) {
auto ctx = Rando::Context::GetInstance();
ctx->AddHint(StaticData::gossipStoneCheckToHint[gossipStone],
Hint(StaticData::gossipStoneCheckToHint[gossipStone], hintType, distributionName, hintKeys, locations,
areas, trials));
ctx->GetItemLocation(gossipStone)
->SetPlacedItem(RG_HINT); // RANDOTODO, better gossip stone to location to hint key system
}
static void AddGossipStoneHintCopies(uint8_t copies, const HintType hintType, const std::string distributionName,
const std::vector<RandomizerHintTextKey> hintKeys = {},
const std::vector<RandomizerCheck> locations = {},
const std::vector<RandomizerArea> areas = {},
const std::vector<TrialKey> trials = {},
RandomizerCheck firstStone = RC_UNKNOWN_CHECK) {
if (firstStone != RC_UNKNOWN_CHECK && copies > 0) {
AddGossipStoneHint(firstStone, hintType, distributionName, hintKeys, locations, areas, trials);
copies -= 1;
}
for (int c = 0; c < copies; c++) {
// get a random gossip stone
auto gossipStones = GetEmptyGossipStones();
if (gossipStones.empty()) {
SPDLOG_DEBUG("\tNO GOSSIP STONES TO PLACE HINT\n\n");
return;
}
auto gossipStone = RandomElement(gossipStones, false);
AddGossipStoneHint(gossipStone, hintType, distributionName, hintKeys, locations, areas, trials);
}
}
static bool CreateHint(RandomizerCheck location, uint8_t copies, HintType type, std::string distribution) {
auto ctx = Rando::Context::GetInstance();
// get a gossip stone accessible without the hinted item
std::vector<RandomizerCheck> gossipStoneLocations = GetAccessibleGossipStones(location);
if (gossipStoneLocations.empty()) {
SPDLOG_DEBUG("\tNO IN LOGIC GOSSIP STONE\n\n");
return false;
}
RandomizerCheck gossipStone = RandomElement(gossipStoneLocations);
RandomizerArea area = ctx->GetItemLocation(location)->GetRandomArea();
// Set that hints are accesible
ctx->GetItemLocation(location)->SetHintAccesible();
if (type == HINT_TYPE_FOOLISH) {
SetAllInAreaAsHintAccesible(area, ctx->allLocations);
}
AddGossipStoneHintCopies(copies, type, distribution, {}, { location }, { area }, {}, gossipStone);
return true;
}
static RandomizerCheck CreateRandomHint(std::vector<RandomizerCheck>& possibleHintLocations, uint8_t copies,
HintType type, std::string distributionName) {
auto ctx = Rando::Context::GetInstance();
// return if there aren't any hintable locations or gossip stones available
if (GetEmptyGossipStones().size() < copies) {
SPDLOG_DEBUG("\tNOT ENOUGH GOSSIP STONES TO PLACE HINTS\n\n");
return RC_UNKNOWN_CHECK;
}
RandomizerCheck hintedLocation;
bool placed = false;
while (!placed) {
if (possibleHintLocations.empty()) {
SPDLOG_DEBUG("\tNO LOCATIONS TO HINT\n\n");
return RC_UNKNOWN_CHECK;
}
hintedLocation =
RandomElement(possibleHintLocations, true); // removing the location to avoid it being hinted again on fail
SPDLOG_DEBUG("\tLocation: ");
SPDLOG_DEBUG(Rando::StaticData::GetLocation(hintedLocation)->GetName());
SPDLOG_DEBUG("\n");
SPDLOG_DEBUG("\tItem: ");
SPDLOG_DEBUG(ctx->GetItemLocation(hintedLocation)->GetPlacedItemName().GetEnglish());
SPDLOG_DEBUG("\n");
placed = CreateHint(hintedLocation, copies, type, distributionName);
}
return hintedLocation;
}
static std::vector<RandomizerCheck> FilterHintability(std::vector<RandomizerCheck>& locations,
std::function<bool(RandomizerCheck)> extraFilter = NoFilter) {
auto ctx = Rando::Context::GetInstance();
return FilterFromPool(locations, [extraFilter, ctx](const RandomizerCheck loc) {
return ctx->GetItemLocation(loc)->IsHintable() && !(ctx->GetItemLocation(loc)->IsAHintAccessible()) &&
extraFilter(loc);
});
}
static void CreateJunkHints(uint8_t numHints) {
for (uint8_t c = 0; c < numHints; c++) {
// duplicate junk hints are possible for now
AddGossipStoneHintCopies(1, HINT_TYPE_HINT_KEY, "Junk", { GetRandomJunkHint() });
}
}
static void CreateTrialHints(uint8_t copies) {
if (copies > 0) {
auto ctx = Rando::Context::GetInstance();
if (ctx->GetOption(RSK_TRIAL_COUNT).Is(6)) { // six trials
AddGossipStoneHintCopies(copies, HINT_TYPE_HINT_KEY, "Trial", { RHT_SIX_TRIALS });
} else if (ctx->GetOption(RSK_TRIAL_COUNT).Is(0)) { // zero trials
AddGossipStoneHintCopies(copies, HINT_TYPE_HINT_KEY, "Trial", { RHT_ZERO_TRIALS });
} else {
std::vector<TrialInfo*> trials =
ctx->GetTrials()->GetTrialList(); // there's probably a way to remove this assignment
if (ctx->GetOption(RSK_TRIAL_COUNT).Get() >= 4) { // 4 or 5 required trials, get skipped trials
trials = FilterFromPool(trials, [](TrialInfo* trial) { return trial->IsSkipped(); });
} else { // 1 to 3 trials, get requried trials
auto requiredTrials = FilterFromPool(trials, [](TrialInfo* trial) { return trial->IsRequired(); });
}
for (auto& trial : trials) { // create a hint for each hinted trial
AddGossipStoneHintCopies(copies, HINT_TYPE_TRIAL, "Trial", {}, {}, {}, { trial->GetTrialKey() });
}
}
}
}
void CreateWarpSongTexts() {
auto ctx = Rando::Context::GetInstance();
if (ctx->GetOption(RSK_WARP_SONG_HINTS)) {
auto warpSongEntrances = GetShuffleableEntrances(EntranceType::WarpSong, false);
for (auto entrance : warpSongEntrances) {
// RANDOTODO make random
RandomizerArea destination = entrance->GetConnectedRegion()->GetFirstArea();
switch (entrance->GetIndex()) {
case 0x0600: // minuet RANDOTODO make into entrance hints when they are added
ctx->AddHint(RH_MINUET_WARP_LOC,
Hint(RH_MINUET_WARP_LOC, HINT_TYPE_AREA, "", { RHT_WARP_SONG }, {}, { destination }));
break;
case 0x04F6: // bolero
ctx->AddHint(RH_BOLERO_WARP_LOC,
Hint(RH_BOLERO_WARP_LOC, HINT_TYPE_AREA, "", { RHT_WARP_SONG }, {}, { destination }));
break;
case 0x0604: // serenade
ctx->AddHint(RH_SERENADE_WARP_LOC, Hint(RH_SERENADE_WARP_LOC, HINT_TYPE_AREA, "", { RHT_WARP_SONG },
{}, { destination }));
break;
case 0x01F1: // requiem
ctx->AddHint(RH_REQUIEM_WARP_LOC,
Hint(RH_REQUIEM_WARP_LOC, HINT_TYPE_AREA, "", { RHT_WARP_SONG }, {}, { destination }));
break;
case 0x0568: // nocturne
ctx->AddHint(RH_NOCTURNE_WARP_LOC, Hint(RH_NOCTURNE_WARP_LOC, HINT_TYPE_AREA, "", { RHT_WARP_SONG },
{}, { destination }));
break;
case 0x05F4: // prelude
ctx->AddHint(RH_PRELUDE_WARP_LOC,
Hint(RH_PRELUDE_WARP_LOC, HINT_TYPE_AREA, "", { RHT_WARP_SONG }, {}, { destination }));
break;
default:
break;
}
}
}
}
int32_t getRandomWeight(int32_t totalWeight) {
if (totalWeight <= 1) {
return 1;
}
return Random(1, totalWeight);
}
static void DistributeHints(std::vector<uint8_t>& selected, size_t stoneCount,
std::vector<HintDistributionSetting> distTable, uint8_t junkWieght, bool addFixed = true) {
int32_t totalWeight = junkWieght; // Start with our Junk Weight, the natural chance of a junk hint
for (size_t c = 0; c < distTable.size();
c++) { // Gather the weights of each distribution and, if it's the first pass, apply fixed hints
totalWeight += distTable[c].weight; // Note that PlaceHints will set weights of distributions to zero if it
// can't place anything from them
if (addFixed) {
selected[c] += distTable[c].fixed;
stoneCount -= distTable[c].fixed * distTable[c].copies;
}
}
int32_t currentWeight = getRandomWeight(totalWeight); // Initialise with the first random weight from 1 to the
// total.
while (stoneCount > 0 &&
totalWeight >
0) { // Loop until we run out of stones or have no TotalWeight. 0 totalWeight means junkWeight is 0
// and that all weights have been 0'd out for another reason, and skips to placing all junk hints
for (size_t distribution = 0; distribution < distTable.size(); distribution++) {
currentWeight -=
distTable[distribution]
.weight; // go over each distribution, subtracting the weight each time. Once we reach zero or less,
if (currentWeight <= 0) { // tell the system to make 1 of that hint, unless not enough stones remain
if (stoneCount >= distTable[distribution].copies && distTable[distribution].copies > 0) {
selected[distribution] += 1; // if we have enough stones, and copies are not zero, assign 1 to this
// hint type, remove the stones, and break
stoneCount -= distTable[distribution].copies;
break; // This leaves the whole for loop
} else { // If we don't have the stones, or copies is 0 despite there being the wieght to trigger a hit,
// temporerally set wieght to zero
totalWeight -=
distTable[distribution]
.weight; // Unlike PlaceHint, distTable is passed by value here, making this temporary
distTable[distribution].weight =
0; // this is so we can still roll this hint type if more stones free up later
break;
}
}
}
// if there's still weight then it's junk, as the leftover weight is junkWeight
if (currentWeight >
0) { // zero TotalWeight breaks the while loop and hits the fallback, so skipping this is fine in that case
selected[selected.size() - 1] += 1;
stoneCount -= 1;
}
currentWeight = getRandomWeight(totalWeight);
}
// if stones are left, assign junk to every remaining stone as a fallback.
if (stoneCount > 0) {
selected[static_cast<uint8_t>(selected.size()) - 1] += static_cast<uint8_t>(stoneCount);
}
}
uint8_t PlaceHints(std::vector<uint8_t>& selectedHints, std::vector<HintDistributionSetting>& distTable) {
auto ctx = Rando::Context::GetInstance();
uint8_t curSlot = 0;
for (HintDistributionSetting distribution : distTable) {
std::vector<RandomizerCheck> hintTypePool = FilterHintability(ctx->allLocations, distribution.filter);
for (uint8_t numHint = 0; numHint < selectedHints[curSlot]; numHint++) {
hintTypePool = FilterHintability(hintTypePool);
SPDLOG_DEBUG("Attempting to make hint of type: " +
StaticData::hintTypeNames[distribution.type].GetEnglish(MF_CLEAN) + "\n");
RandomizerCheck hintedLocation = RC_UNKNOWN_CHECK;
hintedLocation = CreateRandomHint(hintTypePool, distribution.copies, distribution.type, distribution.name);
if (hintedLocation == RC_UNKNOWN_CHECK) { // if hint failed to place, remove all wieght and copies then
// return the number of stones to redistribute
uint8_t hintsToRemove = (selectedHints[curSlot] - numHint) * distribution.copies;
selectedHints[curSlot] = 0; // as distTable is passed by refernce here, these changes stick for the rest
// of this seed generation
distTable[curSlot].copies = 0; // and prevent future distribution from choosing this slot
distTable[curSlot].weight = 0;
return hintsToRemove;
}
if (Rando::StaticData::GetLocation(hintedLocation)->IsDungeon()) {
distribution.dungeonLimit -= 1;
if (distribution.dungeonLimit == 0) {
FilterFromPool(hintTypePool, FilterOverworldLocations);
}
}
}
selectedHints[curSlot] = 0;
curSlot += 1;
}
CreateJunkHints(selectedHints[selectedHints.size() - 1]);
return 0;
}
void CreateStoneHints() {
auto ctx = Rando::Context::GetInstance();
SPDLOG_DEBUG("\nNOW CREATING HINTS\n");
const HintSetting& hintSetting = hintSettingTable[ctx->GetOption(RSK_HINT_DISTRIBUTION).Get()];
std::vector<HintDistributionSetting> distTable = hintSetting.distTable;
// Apply impa's song exclusions when zelda is skipped
if (ctx->GetOption(RSK_SKIP_CHILD_ZELDA)) {
ctx->GetItemLocation(RC_SONG_FROM_IMPA)->SetHintAccesible();
}
if (ctx->GetOption(RSK_SELECTED_STARTING_AGE).Is(RO_AGE_ADULT)) {
ctx->GetItemLocation(RC_TOT_MASTER_SWORD)->SetHintAccesible();
}
// Add 'always' location hints
std::vector<RandomizerCheck> alwaysHintLocations = {};
if (hintSetting.alwaysCopies > 0) {
if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_GREG)) {
// If we have Rainbow Bridge set to Greg and the greg hint isn't useful, add a hint for where Greg is
// Do we really need this with the greg hint?
auto gregLocations = FilterFromPool(ctx->allLocations, [ctx](const RandomizerCheck loc) {
return ((ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() == RG_GREG_RUPEE)) &&
ctx->GetItemLocation(loc)->IsHintable() &&
!(ctx->GetOption(RSK_GREG_HINT) && (IsReachableWithout({ RC_GREG_HINT }, loc, true)));
});
if (gregLocations.size() > 0) {
alwaysHintLocations.push_back(gregLocations[0]);
}
}
for (auto& hint : conditionalAlwaysHints) {
RandomizerCheck loc = hint.first;
if (hint.second() && ctx->GetItemLocation(loc)->IsHintable()) {
alwaysHintLocations.push_back(loc);
}
}
for (RandomizerCheck location : alwaysHintLocations) {
CreateHint(location, hintSetting.alwaysCopies, HINT_TYPE_ITEM, "Always");
}
}
// Add 'trial' location hints
if (ctx->GetOption(RSK_GANONS_TRIALS).IsNot(RO_GANONS_TRIALS_SKIP)) {
CreateTrialHints(hintSetting.trialCopies);
}
size_t totalStones = GetEmptyGossipStones().size();
std::vector<uint8_t> selectedHints = {};
for (size_t c = 0; c < distTable.size(); c++) {
selectedHints.push_back(0);
}
selectedHints.push_back(0);
DistributeHints(selectedHints, totalStones, distTable, hintSetting.junkWeight);
while (totalStones != 0) {
totalStones = PlaceHints(selectedHints, distTable);
if (totalStones != 0) {
DistributeHints(selectedHints, totalStones, distTable, hintSetting.junkWeight, false);
}
}
// Getting gossip stone locations temporarily sets one location to not be reachable.
// Call the function one last time to get rid of false positives on locations not
// being reachable.
ReachabilitySearch({});
}
std::vector<RandomizerCheck> FindItemsAndMarkHinted(std::vector<RandomizerGet> items,
std::vector<RandomizerCheck> hintChecks) {
std::vector<RandomizerCheck> locations = {};
auto ctx = Rando::Context::GetInstance();
for (size_t c = 0; c < items.size(); c++) {
std::vector<RandomizerCheck> found =
FilterFromPool(ctx->allLocations, [items, ctx, c](const RandomizerCheck loc) {
return ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() == items[c];
});
if (found.size() > 0) {
locations.push_back(found[0]);
// RANDOTODO make the called functions of this always return true if empty hintChecks are provided
if (!ctx->GetItemLocation(found[0])->IsAHintAccessible() &&
(hintChecks.size() == 0 || IsReachableWithout(hintChecks, found[0], true))) {
ctx->GetItemLocation(found[0])->SetHintAccesible();
}
} else {
locations.push_back(RC_UNKNOWN_CHECK);
}
}
return locations;
}
void CreateChildAltarHint() {
auto ctx = Rando::Context::GetInstance();
if (!ctx->GetHint(RH_ALTAR_CHILD)->IsEnabled()) {
std::vector<RandomizerCheck> stoneLocs = {};
std::vector<RandomizerArea> stoneAreas = {};
if (ctx->GetOption(RSK_TOT_ALTAR_HINT)) {
// force marking the rewards as hinted if they are at the end of dungeons as they can be inferred
if (ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_END_OF_DUNGEON) ||
ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_VANILLA)) {
stoneLocs = FindItemsAndMarkHinted({ RG_KOKIRI_EMERALD, RG_GORON_RUBY, RG_ZORA_SAPPHIRE }, {});
} else {
stoneLocs = FindItemsAndMarkHinted({ RG_KOKIRI_EMERALD, RG_GORON_RUBY, RG_ZORA_SAPPHIRE },
{ RC_ALTAR_HINT_CHILD });
}
for (auto loc : stoneLocs) {
if (loc != RC_UNKNOWN_CHECK) {
stoneAreas.push_back(ctx->GetItemLocation(loc)->GetRandomArea());
}
}
}
ctx->AddHint(RH_ALTAR_CHILD, Hint(RH_ALTAR_CHILD, HINT_TYPE_ALTAR_CHILD, {}, stoneLocs, stoneAreas));
}
}
void CreateAdultAltarHint() {
auto ctx = Rando::Context::GetInstance();
if (!ctx->GetHint(RH_ALTAR_ADULT)->IsEnabled()) {
std::vector<RandomizerCheck> medallionLocs = {};
std::vector<RandomizerArea> medallionAreas = {};
if (ctx->GetOption(RSK_TOT_ALTAR_HINT)) {
// force marking the rewards as hinted if they are at the end of dungeons as they can be inferred
if (ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_END_OF_DUNGEON) ||
ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_VANILLA)) {
medallionLocs = FindItemsAndMarkHinted({ RG_LIGHT_MEDALLION, RG_FOREST_MEDALLION, RG_FIRE_MEDALLION,
RG_WATER_MEDALLION, RG_SPIRIT_MEDALLION, RG_SHADOW_MEDALLION },
{});
} else {
medallionLocs = FindItemsAndMarkHinted({ RG_LIGHT_MEDALLION, RG_FOREST_MEDALLION, RG_FIRE_MEDALLION,
RG_WATER_MEDALLION, RG_SPIRIT_MEDALLION, RG_SHADOW_MEDALLION },
{ RC_ALTAR_HINT_ADULT });
}
for (auto loc : medallionLocs) {
if (loc != RC_UNKNOWN_CHECK) {
medallionAreas.push_back(ctx->GetItemLocation(loc)->GetRandomArea());
}
}
}
ctx->AddHint(RH_ALTAR_ADULT, Hint(RH_ALTAR_ADULT, HINT_TYPE_ALTAR_ADULT, {}, medallionLocs, medallionAreas));
}
}
void CreateStaticHintFromData(RandomizerHint hint, StaticHintInfo staticData) {
auto ctx = Rando::Context::GetInstance();
if (!ctx->GetHint(hint)->IsEnabled()) {
OptionValue& option = ctx->GetOption(staticData.setting);
if ((std::holds_alternative<bool>(staticData.condition) && option.Is(std::get<bool>(staticData.condition))) ||
(std::holds_alternative<uint8_t>(staticData.condition) &&
option.Is(std::get<uint8_t>(staticData.condition)))) {
std::vector<RandomizerCheck> locations = {};
if (staticData.targetItems.size() > 0) {
locations = FindItemsAndMarkHinted(staticData.targetItems, staticData.hintChecks);
} else {
for (auto check : staticData.targetChecks) {
locations.push_back(check);
}
}
std::vector<RandomizerArea> areas = {};
for (auto loc : locations) {
ctx->GetItemLocation(loc)->SetHintAccesible();
if (ctx->GetItemLocation(loc)->GetAreas().empty()) {
// If we get to here then it means a location got through with no area assignment, which means
// something went wrong elsewhere.
SPDLOG_DEBUG("Attempted to hint location with no areas: ");
SPDLOG_DEBUG(Rando::StaticData::GetLocation(loc)->GetName());
// assert(false);
areas.push_back(RA_NONE);
} else {
areas.push_back(ctx->GetItemLocation(loc)->GetRandomArea());
}
}
// hintKeys are defaulted to in the hint object and do not need to be specified
ctx->AddHint(hint,
Hint(hint, staticData.type, {}, locations, areas, {}, staticData.yourPocket, staticData.num));
}
}
}
void CreateStaticItemHint(RandomizerHint hintKey, std::vector<RandomizerHintTextKey> hintTextKeys,
std::vector<RandomizerGet> items, std::vector<RandomizerCheck> hintChecks,
bool yourPocket = false) {
// RANDOTODO choose area in case there are multiple
auto ctx = Rando::Context::GetInstance();
std::vector<RandomizerCheck> locations = FindItemsAndMarkHinted(items, hintChecks);
std::vector<RandomizerArea> areas = {};
for (auto loc : locations) {
areas.push_back(ctx->GetItemLocation(loc)->GetRandomArea());
}
ctx->AddHint(hintKey, Hint(hintKey, HINT_TYPE_AREA, hintTextKeys, locations, areas, {}, yourPocket));
}
void CreateGanondorfJoke() {
auto ctx = Rando::Context::GetInstance();
if (!ctx->GetHint(RH_GANONDORF_JOKE)->IsEnabled()) {
ctx->AddHint(RH_GANONDORF_JOKE, Hint(RH_GANONDORF_JOKE, HINT_TYPE_HINT_KEY, { GetRandomGanonJoke() }));
}
}
void CreateGanondorfHint() {
auto ctx = Rando::Context::GetInstance();
if (ctx->GetOption(RSK_GANONDORF_HINT) && !ctx->GetHint(RH_GANONDORF_HINT)->IsEnabled()) {
if (ctx->GetOption(RSK_SHUFFLE_MASTER_SWORD) && ctx->GetOption(RSK_STARTING_MASTER_SWORD).Is(RO_GENERIC_OFF)) {
CreateStaticItemHint(
RH_GANONDORF_HINT,
{ RHT_GANONDORF_HINT_LA_ONLY, RHT_GANONDORF_HINT_MS_ONLY, RHT_GANONDORF_HINT_LA_AND_MS },
{ RG_LIGHT_ARROWS, RG_MASTER_SWORD }, { RC_GANONDORF_HINT }, true);
} else {
CreateStaticItemHint(RH_GANONDORF_HINT, { RHT_GANONDORF_HINT_LA_ONLY }, { RG_LIGHT_ARROWS },
{ RC_GANONDORF_HINT }, true);
}
}
}
void CreateStaticHints() {
CreateChildAltarHint();
CreateAdultAltarHint();
CreateGanondorfJoke();
CreateGanondorfHint();
for (auto [hint, staticData] : StaticData::staticHintInfoMap) {
CreateStaticHintFromData(hint, staticData);
}
}
void CreateAllHints() {
auto ctx = Rando::Context::GetInstance();
CreateStaticHints();
if (ctx->GetOption(RSK_GOSSIP_STONE_HINTS).IsNot(RO_GOSSIP_STONES_NONE)) {
SPDLOG_INFO("Creating Hints...");
CreateStoneHints();
SPDLOG_INFO("Creating Hints Done");
}
}