mirror of
https://github.com/HarbourMasters/Shipwright
synced 2026-06-27 19:13:00 -04:00
810 lines
35 KiB
C++
810 lines
35 KiB
C++
#include "hints.hpp"
|
|
|
|
#include "random.hpp"
|
|
#include "fill.hpp"
|
|
#include "../trial.h"
|
|
#include "../entrance.h"
|
|
#include <spdlog/spdlog.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},
|
|
},
|
|
},
|
|
}};
|
|
|
|
struct BridgeReqConfig {
|
|
RandomizerSettingKey bridgeDirectKey;
|
|
RandomizerSettingKey lacsDirectKey;
|
|
RandoOptionRainbowBridge bridgeEnum;
|
|
RandoOptionGanonsBossKey lacsEnum;
|
|
uint8_t offset;
|
|
};
|
|
|
|
static constexpr BridgeReqConfig StonesConfig{ RSK_RAINBOW_BRIDGE_STONE_COUNT, RSK_LACS_STONE_COUNT, RO_BRIDGE_STONES,
|
|
RO_GANON_BOSS_KEY_LACS_STONES, 6 };
|
|
static constexpr BridgeReqConfig MedallionsConfig{ RSK_RAINBOW_BRIDGE_MEDALLION_COUNT, RSK_LACS_MEDALLION_COUNT,
|
|
RO_BRIDGE_MEDALLIONS, RO_GANON_BOSS_KEY_LACS_MEDALLIONS, 3 };
|
|
static constexpr BridgeReqConfig TokensConfig{ RSK_RAINBOW_BRIDGE_TOKEN_COUNT, RSK_LACS_TOKEN_COUNT, RO_BRIDGE_TOKENS,
|
|
RO_GANON_BOSS_KEY_LACS_TOKENS, 0 };
|
|
|
|
static uint8_t RequiredBySettings(const BridgeReqConfig& cfg) {
|
|
auto ctx = Rando::Context::GetInstance();
|
|
uint8_t count = 0;
|
|
if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(cfg.bridgeEnum)) {
|
|
count = ctx->GetOption(cfg.bridgeDirectKey).Get();
|
|
} else if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_DUNGEON_REWARDS)) {
|
|
count = ctx->GetOption(RSK_RAINBOW_BRIDGE_REWARD_COUNT).Get() - cfg.offset;
|
|
} else if (ctx->GetOption(RSK_RAINBOW_BRIDGE).Is(RO_BRIDGE_DUNGEONS) &&
|
|
ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_END_OF_DUNGEON)) {
|
|
count = ctx->GetOption(RSK_RAINBOW_BRIDGE_DUNGEON_COUNT).Get() - cfg.offset;
|
|
}
|
|
if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(cfg.lacsEnum)) {
|
|
count = std::max<uint8_t>(count, ctx->GetOption(cfg.lacsDirectKey).Get());
|
|
} else if (ctx->GetOption(RSK_GANONS_BOSS_KEY).Is(RO_GANON_BOSS_KEY_LACS_REWARDS)) {
|
|
count = std::max<uint8_t>(count, (uint8_t)(ctx->GetOption(RSK_LACS_REWARD_COUNT).Get() - cfg.offset));
|
|
} 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)) {
|
|
count = std::max<uint8_t>(count, (uint8_t)(ctx->GetOption(RSK_LACS_DUNGEON_COUNT).Get() - cfg.offset));
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static uint8_t StonesRequiredBySettings() {
|
|
return RequiredBySettings(StonesConfig);
|
|
}
|
|
static uint8_t MedallionsRequiredBySettings() {
|
|
return RequiredBySettings(MedallionsConfig);
|
|
}
|
|
static uint8_t TokensRequiredBySettings() {
|
|
return RequiredBySettings(TokensConfig);
|
|
}
|
|
|
|
// An 'always' hint that only applies under certain settings. Suppressed when the user
|
|
// has enabled `dedicatedHint` (since a dedicated hint renders the gossip-stone hint redundant),
|
|
// or when `extra` is present and returns false. RSK_NONE in `dedicatedHint` means no suppression.
|
|
struct ConditionalAlwaysHint {
|
|
RandomizerCheck loc;
|
|
RandomizerSettingKey dedicatedHint;
|
|
std::function<bool()> extra;
|
|
};
|
|
|
|
std::vector<ConditionalAlwaysHint> conditionalAlwaysHints = {
|
|
// clang-format off
|
|
{ RC_MARKET_10_BIG_POES, RSK_BIG_POES_HINT, []() { return Rando::Context::GetInstance()->GetOption(RSK_BIG_POE_COUNT).Get() > 3; } },
|
|
{ RC_DEKU_THEATER_MASK_OF_TRUTH, RSK_MASK_SHOP_HINT, []() { return !Rando::Context::GetInstance()->GetOption(RSK_MASK_QUEST); } },
|
|
{ RC_SONG_FROM_OCARINA_OF_TIME, RSK_OOT_HINT, []() { return StonesRequiredBySettings() < 2; } },
|
|
{ RC_HF_OCARINA_OF_TIME_ITEM, RSK_OOT_HINT, []() { return StonesRequiredBySettings() < 2; } },
|
|
{ RC_SHEIK_IN_KAKARIKO, RSK_NONE, []() { return MedallionsRequiredBySettings() < 5; } },
|
|
{ RC_DMT_TRADE_CLAIM_CHECK, RSK_BIGGORON_HINT, nullptr },
|
|
{ RC_KAK_30_GOLD_SKULLTULA_REWARD, RSK_KAK_30_SKULLS_HINT, []() { return TokensRequiredBySettings() < 30; } },
|
|
{ RC_KAK_40_GOLD_SKULLTULA_REWARD, RSK_KAK_40_SKULLS_HINT, []() { return TokensRequiredBySettings() < 40; } },
|
|
{ RC_KAK_50_GOLD_SKULLTULA_REWARD, RSK_KAK_50_SKULLS_HINT, []() { return TokensRequiredBySettings() < 50; } },
|
|
{ RC_ZR_FROGS_OCARINA_GAME, RSK_FROGS_HINT, nullptr },
|
|
{ RC_KF_LINKS_HOUSE_COW, RSK_MALON_HINT, nullptr },
|
|
{ RC_KAK_100_GOLD_SKULLTULA_REWARD, RSK_KAK_100_SKULLS_HINT, []() { return TokensRequiredBySettings() < 100; } },
|
|
// clang-format on
|
|
};
|
|
|
|
static bool ConditionalAlwaysHintApplies(const ConditionalAlwaysHint& h) {
|
|
auto ctx = Rando::Context::GetInstance();
|
|
if (h.dedicatedHint != RSK_NONE && ctx->GetOption(h.dedicatedHint)) {
|
|
return false;
|
|
}
|
|
return !h.extra || h.extra();
|
|
}
|
|
|
|
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");
|
|
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");
|
|
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");
|
|
return RC_UNKNOWN_CHECK;
|
|
}
|
|
|
|
RandomizerCheck hintedLocation;
|
|
bool placed = false;
|
|
while (!placed) {
|
|
if (possibleHintLocations.empty()) {
|
|
SPDLOG_DEBUG("\tNO LOCATIONS TO HINT");
|
|
return RC_UNKNOWN_CHECK;
|
|
}
|
|
hintedLocation =
|
|
RandomElement(possibleHintLocations, true); // removing the location to avoid it being hinted again on fail
|
|
|
|
SPDLOG_DEBUG("\tLocation: {}", Rando::StaticData::GetLocation(hintedLocation)->GetName());
|
|
SPDLOG_DEBUG("\tItem: {}", ctx->GetItemLocation(hintedLocation)->GetPlacedItemName().GetEnglish());
|
|
|
|
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 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int32_t getRandomWeight(uint32_t totalWeight) {
|
|
return totalWeight <= 1 ? 1 : Random(1, totalWeight);
|
|
}
|
|
|
|
static void DistributeAndPlaceHints(std::vector<HintDistributionSetting>& distTable, size_t totalStones) {
|
|
auto ctx = Rando::Context::GetInstance();
|
|
const uint8_t junkIdx = static_cast<uint8_t>(distTable.size() - 1);
|
|
|
|
// Apply fixed hints upfront (they don't participate in weighted selection)
|
|
for (size_t i = 0; i < distTable.size(); i++) {
|
|
if (distTable[i].fixed == 0) {
|
|
continue;
|
|
}
|
|
uint8_t placed = 0;
|
|
for (uint8_t c = 0; c < distTable[i].fixed; c++) {
|
|
std::vector<RandomizerCheck> hintPool = FilterHintability(ctx->allLocations, distTable[i].filter);
|
|
SPDLOG_DEBUG("Attempting fixed hint of type: {}",
|
|
StaticData::hintTypeNames[distTable[i].type].GetEnglish(MF_CLEAN));
|
|
RandomizerCheck fixedLoc =
|
|
CreateRandomHint(hintPool, distTable[i].copies, distTable[i].type, distTable[i].name);
|
|
if (fixedLoc == RC_UNKNOWN_CHECK) {
|
|
distTable[i].weight = 0;
|
|
distTable[i].copies = 0;
|
|
break;
|
|
}
|
|
placed++;
|
|
if (Rando::StaticData::GetLocation(fixedLoc)->IsDungeon()) {
|
|
distTable[i].dungeonLimit -= 1;
|
|
if (distTable[i].dungeonLimit == 0) {
|
|
hintPool = FilterFromPool(hintPool, FilterOverworldLocations);
|
|
}
|
|
}
|
|
}
|
|
totalStones -= placed * distTable[i].copies;
|
|
}
|
|
|
|
while (totalStones > 0) {
|
|
// Pick a weighted distribution type (junk included)
|
|
uint32_t totalWeight = 0;
|
|
for (size_t i = 0; i < distTable.size(); i++) {
|
|
totalWeight += distTable[i].weight;
|
|
}
|
|
|
|
// No weighted types left, fill remaining with junk
|
|
if (totalWeight == 0) {
|
|
for (size_t c = 0; c < totalStones; c++) {
|
|
// duplicate junk hints are possible for now
|
|
AddGossipStoneHintCopies(1, HINT_TYPE_HINT_KEY, "Junk", { GetRandomJunkHint() });
|
|
}
|
|
return;
|
|
}
|
|
|
|
uint32_t roll = getRandomWeight(totalWeight);
|
|
uint32_t cursor = 0;
|
|
uint8_t chosenType = junkIdx;
|
|
for (size_t i = 0; i < distTable.size(); i++) {
|
|
cursor += distTable[i].weight;
|
|
if (roll <= cursor) {
|
|
chosenType = static_cast<uint8_t>(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (chosenType == junkIdx) {
|
|
AddGossipStoneHintCopies(1, HINT_TYPE_HINT_KEY, "Junk", { GetRandomJunkHint() });
|
|
totalStones -= 1;
|
|
continue;
|
|
}
|
|
|
|
auto& dist = distTable[chosenType];
|
|
|
|
// Need at least `copies` stones to place one instance of this type
|
|
if (dist.copies == 0 || totalStones < dist.copies) {
|
|
dist.weight = 0;
|
|
dist.copies = 0;
|
|
continue;
|
|
}
|
|
|
|
// Build hint pool and attempt placement
|
|
std::vector<RandomizerCheck> hintPool = FilterHintability(ctx->allLocations, dist.filter);
|
|
SPDLOG_DEBUG("Attempting to make hint of type: {}", StaticData::hintTypeNames[dist.type].GetEnglish(MF_CLEAN));
|
|
|
|
RandomizerCheck hintedLocation = CreateRandomHint(hintPool, dist.copies, dist.type, dist.name);
|
|
if (hintedLocation == RC_UNKNOWN_CHECK) {
|
|
// Placement failed, disable this type entirely
|
|
dist.weight = 0;
|
|
dist.copies = 0;
|
|
continue;
|
|
}
|
|
|
|
// Track dungeon limit
|
|
if (Rando::StaticData::GetLocation(hintedLocation)->IsDungeon()) {
|
|
dist.dungeonLimit -= 1;
|
|
if (dist.dungeonLimit == 0) {
|
|
hintPool = FilterFromPool(hintPool, FilterOverworldLocations);
|
|
}
|
|
}
|
|
|
|
totalStones -= dist.copies;
|
|
}
|
|
}
|
|
|
|
void CreateStoneHints() {
|
|
auto ctx = Rando::Context::GetInstance();
|
|
SPDLOG_DEBUG("NOW CREATING HINTS");
|
|
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->GetOption(RSK_SHUFFLE_MASTER_SWORD)) {
|
|
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 (const auto& hint : conditionalAlwaysHints) {
|
|
if (ConditionalAlwaysHintApplies(hint) && ctx->GetItemLocation(hint.loc)->IsHintable()) {
|
|
alwaysHintLocations.push_back(hint.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();
|
|
distTable.push_back({ "Junk", HINT_TYPE_HINT_KEY, hintSetting.junkWeight, 0, 1, NoFilter });
|
|
DistributeAndPlaceHints(distTable, totalStones);
|
|
|
|
// 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;
|
|
}
|
|
|
|
static void CreateAltarHint(RandomizerHint hintKey, HintType hintType, std::vector<RandomizerGet> rewards,
|
|
RandomizerCheck altarCheck) {
|
|
auto ctx = Rando::Context::GetInstance();
|
|
if (ctx->GetHint(hintKey)->IsEnabled()) {
|
|
return;
|
|
}
|
|
std::vector<RandomizerCheck> locs = {};
|
|
std::vector<RandomizerArea> areas = {};
|
|
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
|
|
const bool rewardsInferrable =
|
|
ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_END_OF_DUNGEON) ||
|
|
ctx->GetOption(RSK_SHUFFLE_DUNGEON_REWARDS).Is(RO_DUNGEON_REWARDS_VANILLA);
|
|
locs = FindItemsAndMarkHinted(rewards, rewardsInferrable ? std::vector<RandomizerCheck>{}
|
|
: std::vector<RandomizerCheck>{ altarCheck });
|
|
for (auto loc : locs) {
|
|
if (loc != RC_UNKNOWN_CHECK) {
|
|
areas.push_back(ctx->GetItemLocation(loc)->GetRandomArea());
|
|
}
|
|
}
|
|
}
|
|
ctx->AddHint(hintKey, Hint(hintKey, hintType, {}, locs, areas));
|
|
}
|
|
|
|
void CreateChildAltarHint() {
|
|
CreateAltarHint(RH_ALTAR_CHILD, HINT_TYPE_ALTAR_CHILD, { RG_KOKIRI_EMERALD, RG_GORON_RUBY, RG_ZORA_SAPPHIRE },
|
|
RC_ALTAR_HINT_CHILD);
|
|
}
|
|
|
|
void CreateAdultAltarHint() {
|
|
CreateAltarHint(RH_ALTAR_ADULT, HINT_TYPE_ALTAR_ADULT,
|
|
{ RG_LIGHT_MEDALLION, RG_FOREST_MEDALLION, RG_FIRE_MEDALLION, RG_WATER_MEDALLION,
|
|
RG_SPIRIT_MEDALLION, RG_SHADOW_MEDALLION },
|
|
RC_ALTAR_HINT_ADULT);
|
|
}
|
|
|
|
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;
|
|
areas.reserve(locations.size());
|
|
for (auto loc : locations) {
|
|
areas.push_back(loc == RC_UNKNOWN_CHECK ? RA_NONE : 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");
|
|
}
|
|
}
|