first pass at hint system

This commit is contained in:
gymnast86
2026-08-01 09:31:29 -07:00
parent 687307b4c1
commit ce2d37df68
41 changed files with 2399 additions and 165 deletions
+1
View File
@@ -1663,6 +1663,7 @@ set(DUSK_FILES
src/dusk/randomizer/generator/logic/flatten/flatten.hpp
src/dusk/randomizer/generator/logic/flatten/simplify_algebraic.cpp
src/dusk/randomizer/generator/logic/flatten/simplify_algebraic.hpp
src/dusk/randomizer/generator/logic/hint_types.hpp
src/dusk/randomizer/generator/logic/hints.cpp
src/dusk/randomizer/generator/logic/hints.hpp
src/dusk/randomizer/generator/logic/item.cpp
-4
View File
@@ -10,10 +10,6 @@
#include "JSystem/JHostIO/JORReflexible.h"
#include "helpers/endian.h"
#if TARGET_PC
#include <unordered_map>
#endif
static const int DEFAULT_SELECT_ITEM_INDEX = 0;
static const int MAX_SELECT_ITEM = 4;
static const int SELECT_ITEM_NUM = 2;
+1
View File
@@ -8,6 +8,7 @@
#include "dusk/data.hpp"
#include "dusk/map_loader_definitions.h"
#include "dusk/ui/rando_config.hpp"
#include "dusk/randomizer/generator/randomizer.hpp"
#include "dusk/randomizer/generator/logic/search.hpp"
#include "dusk/randomizer/generator/utility/string.hpp"
#include "dusk/randomizer/game/randomizer_context.hpp"
+2 -1
View File
@@ -2,6 +2,7 @@
#include "JSystem/JMessage/control.h"
#include "d/d_msg_class.h"
#include "d/d_save.h"
#include "randomizer_context.hpp"
#include "custom_flow_ids.hpp"
@@ -44,7 +45,7 @@ char* GetFormatedTextOverride(u32 key, std::string& text) {
}
u8 getLanguageForOverride() {
u8 language = randomizer::Text::ENGLISH;
u8 language = dSv_player_config_c::LANGUAGE_ENGLISH;
if (dusk::version::isRegionPal()) {
language = dComIfGs_getPalLanguage();
}/* else if (dusk::version::isRegionJpn()) {
+102 -11
View File
@@ -1090,6 +1090,21 @@ void parseObjPatchData(stage_tgsc_data_class& object, const YAML::Node& patchNod
}
}
static std::array<u8, 20> CreateAttributeData(const YAML::Node& node, const std::string& name) {
auto attributesStr = node.as<std::string>();
auto attributesVec = HexToBytes(attributesStr);
if (attributesVec.size() != 16) {
throw std::runtime_error(fmt::format("Attributes for Text Override {} "
"are the wrong length. (Expected: 16, Actual: {}", name, attributesVec.size()));
}
std::array<u8, 20> attributes{};
for (size_t i = 0; i < attributesVec.size(); ++i) {
attributes[i + 4] = attributesVec[i];
}
return attributes;
}
RandomizerContext WriteSeedData(randomizer::logic::world::World* world) {
RandomizerContext randoData{};
@@ -1499,6 +1514,92 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) {
message->type = 1;
message->msg_index = handleCustomMessageID(flowNode["inf index"]);
message->next_node_idx = handleCustomFlowID(flowNode["next flow index"]);
// If a custom message is too long, split it up among additional flow/message nodes
if (message->msg_index >= BASE_CUSTOM_MSG_AND_FLOW_ID) {
auto textName = flowNode["inf index"].as<std::string>();
if (world->GetTextDatabase().contains(textName)) {
auto& text = world->GetTextObject(textName);
if (text.IsTooLong()) {
// Get the attributes for the text at this ID. Pretty inefficient since we
// have to loop through every element unfortunately
std::optional<std::array<u8, 20>> customAttributes{};
auto textOverrides = LOAD_EMBED_YAML(RANDO_DATA_PATH "text/text_overrides.yaml");
for (const auto& overrideNode : textOverrides) {
const auto& overrideName = overrideNode["Name"].as<std::string>();
if (overrideName == textName && overrideNode["Attributes"]) {
customAttributes = CreateAttributeData(overrideNode["Attributes"], textName);
break;
}
}
// Add each split text as a new custom message entry. The original entry
// still exists but has been sliced down to fit properly.
auto extraText = text.SplitToFitTextLimits();
std::vector<mesg_flow_node> newMsgFlows{};
for (size_t i = 0; i < extraText.size(); i++) {
// Add this custom text to the world
auto extraTextName = textName + std::to_string(i + 1);
world->AddNewText(extraTextName) = extraText[i];
// Kinda silly, but means we don't need to create another function
// to handle direct string names.
YAML::Node node{};
node["name"] = extraTextName;
// Create new Flow and Message Ids for the split text object
auto newCustomFlowIndex = handleCustomFlowID(node["name"]);
auto newCustomMessageIndex = handleCustomMessageID(node["name"]);
// Create the new flow node. We're storing its own flow index with
// itself for now, but we'll shift it back to the previous node later
mesg_flow_node newMsgFlow{};
newMsgFlow.type = 1;
newMsgFlow.msg_index = newCustomMessageIndex;
newMsgFlow.next_node_idx = newCustomFlowIndex;
newMsgFlows.push_back(newMsgFlow);
//Add the custom text to the rando data
u32 key = (CUSTOM_BMG_GROUP << 16) | newCustomMessageIndex;
for (auto language : randomizer::supportedLanguages) {
std::string newText = extraText[i].mText[language];
randomizer::applyMessageCodes(newText);
randoData.mTextOverrides[language][key] = newText;
}
// Add custom attribute data as well if it exists
if (customAttributes.has_value()) {
auto attributes = customAttributes.value();
// Set the message id in the attribute data
attributes[4] = newCustomMessageIndex >> 8;
attributes[5] = newCustomMessageIndex & 0xFF;
randoData.mAttributeOverrides[key] = attributes;
}
}
// Shift all the next_node_idx fields back a node and set the original
// next node idx as the next node idx for the final of the new flows
auto finalNodeIdx = message->next_node_idx;
message->next_node_idx = newMsgFlows[0].next_node_idx;
for (size_t i = 0; i < newMsgFlows.size(); i++) {
auto& curFlow = newMsgFlows[i];
auto curFlowIdx = curFlow.next_node_idx;
if (i == newMsgFlows.size() - 1) {
curFlow.next_node_idx = finalNodeIdx;
} else {
curFlow.next_node_idx = newMsgFlows[i + 1].next_node_idx;
}
// Also Add the new custom flows to our rando data
u32 key = (CUSTOM_BMG_GROUP << 16) | curFlowIdx;
randoData.mFlowPatches[key] = std::bit_cast<u64>(curFlow);
}
}
}
}
}
for (auto index : indices) {
u32 key = (groupNo << 16) | index;
@@ -1540,17 +1641,7 @@ RandomizerContext WriteSeedData(randomizer::logic::world::World* world) {
// If we have custom attributes
if (overrideNode["Attributes"]) {
auto attributesStr = overrideNode["Attributes"].as<std::string>();
auto attributesVec = HexToBytes(attributesStr);
if (attributesVec.size() != 16) {
throw std::runtime_error(fmt::format("Attributes for Text Override {} "
"are the wrong length. (Expected: 16, Actual: {}", name, attributesVec.size()));
}
std::array<u8, 20> attributes{};
for (size_t i = 0; i < attributesVec.size(); ++i) {
attributes[i + 4] = attributesVec[i];
}
auto attributes = CreateAttributeData(overrideNode["Attributes"], name);
// Set the message id in the attribute data
attributes[4] = messageId >> 8;
@@ -4,15 +4,13 @@
#include <dolphin/types.h>
#include <array>
#include <list>
#include <filesystem>
#include <iomanip>
#include <list>
#include <optional>
#include <string>
#include <sstream>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "dusk/randomizer/generator/randomizer.hpp"
/*
* Class holding all the information necessary for playing
@@ -1469,6 +1469,7 @@
- Npc
- Kakariko Village
- ARC
- Remote Location
Metadata:
Name Lookup:
- Talo Sharpshooting
@@ -1492,6 +1493,7 @@
- Overworld
- Npc
- Kakariko Village
- Remote Location
Metadata:
Name Lookup:
- Ilia Memory Reward
@@ -1883,6 +1885,7 @@
- Npc
- Hidden Village
- ARC
- Remote Location
Metadata:
Name Lookup:
- Skybook From Impaz
@@ -1894,6 +1897,7 @@
- Overworld
- Npc
- Hidden Village
- Remote Location
Metadata:
Freestanding Item:
- Stage: 63
@@ -2215,6 +2219,7 @@
- Overworld
- Hyrule Field - Lanayru Province
- ARC
- Remote Location
Metadata:
Chest:
- Stage: 30
@@ -2478,6 +2483,7 @@
- Npc
- Castle Town
- ARC
- Remote Location
Metadata:
Name Lookup:
- Jovani 60 Poe Soul Reward
@@ -3556,6 +3562,7 @@
- Npc
- Lake Hylia
- ARC
- Remote Location
Metadata:
Name Lookup:
- Plumm Fruit Balloon Minigame
@@ -3644,6 +3651,7 @@
- Npc
- Upper Zoras River
- ARC
- Remote Location
Metadata:
Name Lookup:
- Iza Helping Hand
@@ -3656,6 +3664,7 @@
- Npc
- Upper Zoras River
- ARC
- Remote Location
Metadata:
Name Lookup:
- Iza Raging Rapids Minigame
@@ -4066,6 +4075,7 @@
- Npc
- Snowpeak Province
- ARC
- Remote Location
Metadata:
Name Lookup:
- Snowboard Racing Prize
@@ -4712,6 +4722,7 @@
- Npc
- Cave of Ordeals
- ARC
- Remote Location
Metadata:
Name Lookup:
- Cave of Ordeals Great Fairy Reward
@@ -4937,7 +4948,7 @@
- Dungeon Reward
- REL
- ARC
Goal Location: True
Goal Name: Diababa
Metadata:
FLW Message:
- Group: 5
@@ -5206,7 +5217,7 @@
- Dungeon Reward
- REL
- ARC
Goal Location: True
Goal Name: Fyrus
Metadata:
FLW Message:
- Group: 5
@@ -5531,7 +5542,7 @@
- Dungeon Reward
- REL
- ARC
Goal Location: True
Goal Name: Morpheel
Metadata:
FLW Message:
- Group: 5
@@ -5719,6 +5730,7 @@
Categories:
- Dungeon
- Arbiters Grounds
- Remote Location
Metadata:
Chest:
- Stage: 26
@@ -5804,7 +5816,7 @@
- Dungeon
- Arbiters Grounds
- Dungeon Reward
Goal Location: True
Goal Name: Stallord
Metadata:
Name Lookup:
- Arbiters Grounds Dungeon Reward
@@ -6094,7 +6106,7 @@
- Dungeon Reward
- REL
- ARC
Goal Location: True
Goal Name: Blizzeta
Metadata:
FLW Message:
- Group: 5
@@ -6329,7 +6341,7 @@
- Dungeon Reward
- REL
- ARC
Goal Location: True
Goal Name: Armogohma
Metadata:
FLW Message:
- Group: 5
@@ -6619,6 +6631,7 @@
- Dungeon
- City in the Sky
- ARC
- Remote Location
Metadata:
Chest:
- Stage: 14
@@ -6655,7 +6668,7 @@
- Dungeon Reward
- REL
- ARC
Goal Location: True
Goal Name: Argorok
Metadata:
FLW Message:
- Group: 5
@@ -6810,6 +6823,7 @@
- Cutscene
- Dungeon
- Palace of Twilight
- Remote Location
Metadata:
Freestanding Item:
- Stage: 15
@@ -6866,7 +6880,7 @@
- Palace of Twilight
- Heart Container
- Dungeon Reward
Goal Location: True
Goal Name: Zant
Metadata:
Freestanding Item:
- Stage: 16
@@ -7200,6 +7214,7 @@
Original Item: Game Beatable
Categories:
- Placeholder
Goal Name: Ganondorf
Metadata:
- None
@@ -480,6 +480,97 @@
- Master Sword: "The door to the past will require striking at least the Master Sword into the Master Sword pedestal."
- Light Sword: "The door to the past will require striking the Light Sword into the Master Sword pedestal."
#############################
## Hints ##
#############################
- Name: Number of Path Hints
Default Option: 3
Options:
- 0-7: "Select the number of path hints you would like to generate."
- Name: Path Hints on Midna
Default Option: "Off"
Options:
- "Off": "Path Hints will not be part of Midna's hint text."
- "On": "Path hints will be part of Midna's hint text. If path hints are also placed on hint signs, then the hints will be split evenly between the two options."
- Name: Path Hints on Hint Signs
Default Option: "Off"
Options:
- "Off": "Path Hints will not be placed on hint signs."
- "Overworld": "Path hints will be placed only on overworld hint signs. If path hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- "Dungeon": "Path hints will be placed only on dungeon hint signs. If path hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- "Any": "Path hints can be placed at any hint sign. If path hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- Name: Number of Barren Hints
Default Option: 3
Options:
- 0-7: "Select the number of barren hints you would like to generate."
- Name: Barren Hints on Midna
Default Option: "On"
Options:
- "Off": "Barren Hints will not be part of Midna's hint text."
- "On": "Barren hints will be part of Midna's hint text. If barren hints are also placed on hint signs, then the hints will be split evenly between the two options."
#- "On + Region Hint Signs": "Barren hints will be part of Midna's hint text. The hint signs within Midna's hinted barren regions will also declare the region as barren. If barren hints are also placed on hint signs, then the hints will be split evenly between the two options."
- Name: Barren Hints on Hint Signs
Default Option: "Off"
Options:
- "Off": "Barren Hints will not be placed on hint signs."
- "Overworld": "Barren hints will be placed only on overworld hint signs. If barren hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- "Dungeon": "Barren hints will be placed only on dungeon hint signs. If barren hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- "Any": "Barren hints can be placed at any hint sign. If barren hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- Name: Number of Item Hints
Default Option: 1
Options:
- 0-7: "Select the number of item hints you would like to generate."
- Name: Item Hints on Midna
Default Option: "Off"
Options:
- "Off": "Item Hints will not be part of Midna's hint text."
- "On": "Item hints will be part of Midna's hint text. If item hints are also placed on hint signs, then the hints will be split evenly between the two options."
- Name: Item Hints on Hint Signs
Default Option: "Overworld"
Options:
- "Off": "Item Hints will not be placed on hint signs."
- "Overworld": "Item hints will be placed only on overworld hint signs. If item hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- "Dungeon": "Item hints will be placed only on dungeon hint signs. If item hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- "Any": "Item hints can be placed at any hint sign. If item hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- Name: Number of Location Hints
Default Option: 5
Options:
- 0-7: "Select the number of location hints you would like to generate."
- Name: Location Hints on Midna
Default Option: "Off"
Options:
- "Off": "Location Hints will not be part of Midna's hint text."
- "On": "Location hints will be part of Midna's hint text. If location hints are also placed on hint signs, then the hints will be split evenly between the two options."
- Name: Location Hints on Hint Signs
Default Option: "Overworld"
Options:
- "Off": "Location Hints will not be placed on hint signs."
- "Overworld": "Location hints will be placed only on overworld hint signs. If location hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- "Dungeon": "Location hints will be placed only on dungeon hint signs. If location hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- "Any": "Location hints can be placed at any hint sign. If location hints are also part of Midna's hint text, then the hints will be split evenly between the two options."
- Name: Prioritize Remote Location Hints
Default Option: "On"
Options:
- "Off": "All locations will be treated equally for location hints."
- "On": "Certain locations which are time-consuming to check will be given priority for location hints."
#############################
## Entrance Randomizer ##
#############################
- Name: Randomize Starting Spawn
Default Option: "Off"
Options:
@@ -0,0 +1,13 @@
Number of Path Hints: 4
Path Hints on Midna: Off
Path Hints on Hint Signs: Dungeon
Number of Barren Hints: 4
Barren Hints on Midna: Off
Barren Hints on Hint Signs: Dungeon
Number of Item Hints: 4
Item Hints on Midna: Off
Item Hints on Hint Signs: Dungeon
Number of Location Hints: 4
Location Hints on Midna: Off
Location Hints on Hint Signs: Dungeon
Prioritize Remote Location Hints: On
@@ -0,0 +1,14 @@
Seed: TESTTESTTEST
Number of Path Hints: 0
Path Hints on Midna: Off
Path Hints on Hint Signs: Off
Number of Barren Hints: 0
Barren Hints on Midna: Off
Barren Hints on Hint Signs: Off
Number of Item Hints: 0
Item Hints on Midna: Off
Item Hints on Hint Signs: Off
Number of Location Hints: 0
Location Hints on Midna: Off
Location Hints on Hint Signs: Off
Prioritize Remote Location Hints: Off
@@ -0,0 +1,14 @@
Seed: TESTTESTTEST
Number of Path Hints: 7
Path Hints on Midna: On
Path Hints on Hint Signs: Any
Number of Barren Hints: 7
Barren Hints on Midna: On
Barren Hints on Hint Signs: Any
Number of Item Hints: 7
Item Hints on Midna: On
Item Hints on Hint Signs: Any
Number of Location Hints: 7
Location Hints on Midna: On
Location Hints on Hint Signs: Any
Prioritize Remote Location Hints: On
@@ -0,0 +1,13 @@
Number of Path Hints: 4
Path Hints on Midna: Off
Path Hints on Hint Signs: Overworld
Number of Barren Hints: 4
Barren Hints on Midna: Off
Barren Hints on Hint Signs: Overworld
Number of Item Hints: 4
Item Hints on Midna: Off
Item Hints on Hint Signs: Overworld
Number of Location Hints: 4
Location Hints on Midna: Off
Location Hints on Hint Signs: Overworld
Prioritize Remote Location Hints: On
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
Cryptic:
Text: the {northern desert}
Bublin Camp:
Bulblin Camp:
Standard:
Text: Bublin Camp
Text: Bulblin Camp
Pretty:
Text: "{Bublin Camp}"
Text: "{Bulblin Camp}"
Cryptic:
Text: a {camp of enemies}
@@ -1526,6 +1526,14 @@ Mirror Chamber:
Cryptic:
Text: a {chamber of chains}
Snowpeak Mountain:
Standard:
Text: Snowpeak Mountain
Pretty:
Text: "{Snowpeak Mountain}"
Cryptic:
Text: a {snowy mountain}
Forest Temple:
Standard:
Text: Forest Temple
@@ -1538,7 +1546,7 @@ Goron Mines:
Standard:
Text: Goron Mines
Pretty:
Text: the {Goron Mines}
Text: "{Goron Mines}"
Cryptic:
Text: a {volcanic mine}
@@ -1546,7 +1554,7 @@ Lakebed Temple:
Standard:
Text: Lakebed Temple
Pretty:
Text: the {Lakebed Temple}
Text: "{Lakebed Temple}"
Cryptic:
Text: an {underground lake}
@@ -1562,7 +1570,7 @@ Snowpeak Ruins:
Standard:
Text: Snowpeak Ruins
Pretty:
Text: the {Snowpeak Ruins}
Text: "{Snowpeak Ruins}"
Cryptic:
Text: a {snowy mansion}
@@ -1578,7 +1586,7 @@ City in the Sky:
Standard:
Text: City in the Sky
Pretty:
Text: the {City in the Sky}
Text: "{City in the Sky}"
Cryptic:
Text: the {skies above}
@@ -1594,10 +1602,64 @@ Hyrule Castle:
Standard:
Text: Hyrule Castle
Pretty:
Text: the {Hyrule Castle}
Text: "{Hyrule Castle}"
Cryptic:
Text: the {kingdom's castle}
# BOSS/GOAL NAMES
Diababa:
Standard:
Text: Diababa
Fyrus:
Standard:
Text: Fyrus
Morpheel:
Standard:
Text: Morpheel
Stallord:
Standard:
Text: Stallord
Blizzeta:
Standard:
Text: Blizzeta
Armogohma:
Standard:
Text: Armogohma
Argorok:
Standard:
Text: Argorok
Zant:
Standard:
Text: Zant
Ganondorf:
Standard:
Text: Ganondorf
# HINT TEMPLATES
Path Hint:
Standard:
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
Barren Hint:
Standard:
Text: <fast>They say that visiting <region> is a foolish choice.
Item Hint:
Standard:
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
Location Hint:
Standard:
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
# NO REQUIRED DUNGEON TEXT
No Required Dungeons Text:
Standard:
@@ -2212,6 +2274,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
Text: |-
There are <red><required dungeon count><white> required dungeons:
Midna Hints Path Hints Intro:
Standard:
Text: |-
The following <red>paths<white> lie before you:
Midna Hints Barren Hints Intro:
Standard:
Text: |-
There is <purple>nothing<white> to be found in these regions:
Midna Hints Item Hints Intro:
Standard:
Text: |-
These <green>items<white> may be of use to you:
Midna Hints Location Hints Intro:
Standard:
Text: |-
These <red>locations<white> are known to me:
Ordon Hint Sign Text:
Standard:
Text: |-
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
Cryptic:
Text: the {northern desert}
Bublin Camp:
Bulblin Camp:
Standard:
Text: Bublin Camp
Text: Bulblin Camp
Pretty:
Text: "{Bublin Camp}"
Text: "{Bulblin Camp}"
Cryptic:
Text: a {camp of enemies}
@@ -1538,7 +1538,7 @@ Goron Mines:
Standard:
Text: Goron Mines
Pretty:
Text: the {Goron Mines}
Text: "{Goron Mines}"
Cryptic:
Text: a {volcanic mine}
@@ -1546,7 +1546,7 @@ Lakebed Temple:
Standard:
Text: Lakebed Temple
Pretty:
Text: the {Lakebed Temple}
Text: "{Lakebed Temple}"
Cryptic:
Text: an {underground lake}
@@ -1562,7 +1562,7 @@ Snowpeak Ruins:
Standard:
Text: Snowpeak Ruins
Pretty:
Text: the {Snowpeak Ruins}
Text: "{Snowpeak Ruins}"
Cryptic:
Text: a {snowy mansion}
@@ -1578,7 +1578,7 @@ City in the Sky:
Standard:
Text: City in the Sky
Pretty:
Text: the {City in the Sky}
Text: "{City in the Sky}"
Cryptic:
Text: the {skies above}
@@ -1594,10 +1594,64 @@ Hyrule Castle:
Standard:
Text: Hyrule Castle
Pretty:
Text: the {Hyrule Castle}
Text: "{Hyrule Castle}"
Cryptic:
Text: the {kingdom's castle}
# BOSS/GOAL NAMES
Diababa:
Standard:
Text: Diababa
Fyrus:
Standard:
Text: Fyrus
Morpheel:
Standard:
Text: Morpheel
Stallord:
Standard:
Text: Stallord
Blizzeta:
Standard:
Text: Blizzeta
Armogohma:
Standard:
Text: Armogohma
Argorok:
Standard:
Text: Argorok
Zant:
Standard:
Text: Zant
Ganondorf:
Standard:
Text: Ganondorf
# HINT TEMPLATES
Path Hint:
Standard:
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
Barren Hint:
Standard:
Text: <fast>They say that visiting <region> is a foolish choice.
Item Hint:
Standard:
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
Location Hint:
Standard:
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
# NO REQUIRED DUNGEON TEXT
No Required Dungeons Text:
Standard:
@@ -2212,6 +2266,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
Text: |-
There are <red><required dungeon count><white> required dungeons:
Midna Hints Path Hints Intro:
Standard:
Text: |-
The following <red>paths<white> lie before you:
Midna Hints Barren Hints Intro:
Standard:
Text: |-
There is <purple>nothing<white> to be found in these regions:
Midna Hints Item Hints Intro:
Standard:
Text: |-
These <green>items<white> may be of use to you:
Midna Hints Location Hints Intro:
Standard:
Text: |-
These <red>locations<white> are known to me:
Ordon Hint Sign Text:
Standard:
Text: |-
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
Cryptic:
Text: the {northern desert}
Bublin Camp:
Bulblin Camp:
Standard:
Text: Bublin Camp
Text: Bulblin Camp
Pretty:
Text: "{Bublin Camp}"
Text: "{Bulblin Camp}"
Cryptic:
Text: a {camp of enemies}
@@ -1538,7 +1538,7 @@ Goron Mines:
Standard:
Text: Goron Mines
Pretty:
Text: the {Goron Mines}
Text: "{Goron Mines}"
Cryptic:
Text: a {volcanic mine}
@@ -1546,7 +1546,7 @@ Lakebed Temple:
Standard:
Text: Lakebed Temple
Pretty:
Text: the {Lakebed Temple}
Text: "{Lakebed Temple}"
Cryptic:
Text: an {underground lake}
@@ -1562,7 +1562,7 @@ Snowpeak Ruins:
Standard:
Text: Snowpeak Ruins
Pretty:
Text: the {Snowpeak Ruins}
Text: "{Snowpeak Ruins}"
Cryptic:
Text: a {snowy mansion}
@@ -1578,7 +1578,7 @@ City in the Sky:
Standard:
Text: City in the Sky
Pretty:
Text: the {City in the Sky}
Text: "{City in the Sky}"
Cryptic:
Text: the {skies above}
@@ -1594,10 +1594,64 @@ Hyrule Castle:
Standard:
Text: Hyrule Castle
Pretty:
Text: the {Hyrule Castle}
Text: "{Hyrule Castle}"
Cryptic:
Text: the {kingdom's castle}
# BOSS/GOAL NAMES
Diababa:
Standard:
Text: Diababa
Fyrus:
Standard:
Text: Fyrus
Morpheel:
Standard:
Text: Morpheel
Stallord:
Standard:
Text: Stallord
Blizzeta:
Standard:
Text: Blizzeta
Armogohma:
Standard:
Text: Armogohma
Argorok:
Standard:
Text: Argorok
Zant:
Standard:
Text: Zant
Ganondorf:
Standard:
Text: Ganondorf
# HINT TEMPLATES
Path Hint:
Standard:
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
Barren Hint:
Standard:
Text: <fast>They say that visiting <region> is a foolish choice.
Item Hint:
Standard:
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
Location Hint:
Standard:
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
# NO REQUIRED DUNGEON TEXT
No Required Dungeons Text:
Standard:
@@ -2212,6 +2266,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
Text: |-
There are <red><required dungeon count><white> required dungeons:
Midna Hints Path Hints Intro:
Standard:
Text: |-
The following <red>paths<white> lie before you:
Midna Hints Barren Hints Intro:
Standard:
Text: |-
There is <purple>nothing<white> to be found in these regions:
Midna Hints Item Hints Intro:
Standard:
Text: |-
These <green>items<white> may be of use to you:
Midna Hints Location Hints Intro:
Standard:
Text: |-
These <red>locations<white> are known to me:
Ordon Hint Sign Text:
Standard:
Text: |-
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
Cryptic:
Text: the {northern desert}
Bublin Camp:
Bulblin Camp:
Standard:
Text: Bublin Camp
Text: Bulblin Camp
Pretty:
Text: "{Bublin Camp}"
Text: "{Bulblin Camp}"
Cryptic:
Text: a {camp of enemies}
@@ -1538,7 +1538,7 @@ Goron Mines:
Standard:
Text: Goron Mines
Pretty:
Text: the {Goron Mines}
Text: "{Goron Mines}"
Cryptic:
Text: a {volcanic mine}
@@ -1546,7 +1546,7 @@ Lakebed Temple:
Standard:
Text: Lakebed Temple
Pretty:
Text: the {Lakebed Temple}
Text: "{Lakebed Temple}"
Cryptic:
Text: an {underground lake}
@@ -1562,7 +1562,7 @@ Snowpeak Ruins:
Standard:
Text: Snowpeak Ruins
Pretty:
Text: the {Snowpeak Ruins}
Text: "{Snowpeak Ruins}"
Cryptic:
Text: a {snowy mansion}
@@ -1578,7 +1578,7 @@ City in the Sky:
Standard:
Text: City in the Sky
Pretty:
Text: the {City in the Sky}
Text: "{City in the Sky}"
Cryptic:
Text: the {skies above}
@@ -1594,10 +1594,64 @@ Hyrule Castle:
Standard:
Text: Hyrule Castle
Pretty:
Text: the {Hyrule Castle}
Text: "{Hyrule Castle}"
Cryptic:
Text: the {kingdom's castle}
# BOSS/GOAL NAMES
Diababa:
Standard:
Text: Diababa
Fyrus:
Standard:
Text: Fyrus
Morpheel:
Standard:
Text: Morpheel
Stallord:
Standard:
Text: Stallord
Blizzeta:
Standard:
Text: Blizzeta
Armogohma:
Standard:
Text: Armogohma
Argorok:
Standard:
Text: Argorok
Zant:
Standard:
Text: Zant
Ganondorf:
Standard:
Text: Ganondorf
# HINT TEMPLATES
Path Hint:
Standard:
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
Barren Hint:
Standard:
Text: <fast>They say that visiting <region> is a foolish choice.
Item Hint:
Standard:
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
Location Hint:
Standard:
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
# NO REQUIRED DUNGEON TEXT
No Required Dungeons Text:
Standard:
@@ -2212,6 +2266,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
Text: |-
There are <red><required dungeon count><white> required dungeons:
Midna Hints Path Hints Intro:
Standard:
Text: |-
The following <red>paths<white> lie before you:
Midna Hints Barren Hints Intro:
Standard:
Text: |-
There is <purple>nothing<white> to be found in these regions:
Midna Hints Item Hints Intro:
Standard:
Text: |-
These <green>items<white> may be of use to you:
Midna Hints Location Hints Intro:
Standard:
Text: |-
These <red>locations<white> are known to me:
Ordon Hint Sign Text:
Standard:
Text: |-
@@ -1510,11 +1510,11 @@ North Gerudo Desert:
Cryptic:
Text: the {northern desert}
Bublin Camp:
Bulblin Camp:
Standard:
Text: Bublin Camp
Text: Bulblin Camp
Pretty:
Text: "{Bublin Camp}"
Text: "{Bulblin Camp}"
Cryptic:
Text: a {camp of enemies}
@@ -1538,7 +1538,7 @@ Goron Mines:
Standard:
Text: Goron Mines
Pretty:
Text: the {Goron Mines}
Text: "{Goron Mines}"
Cryptic:
Text: a {volcanic mine}
@@ -1546,7 +1546,7 @@ Lakebed Temple:
Standard:
Text: Lakebed Temple
Pretty:
Text: the {Lakebed Temple}
Text: "{Lakebed Temple}"
Cryptic:
Text: an {underground lake}
@@ -1562,7 +1562,7 @@ Snowpeak Ruins:
Standard:
Text: Snowpeak Ruins
Pretty:
Text: the {Snowpeak Ruins}
Text: "{Snowpeak Ruins}"
Cryptic:
Text: a {snowy mansion}
@@ -1578,7 +1578,7 @@ City in the Sky:
Standard:
Text: City in the Sky
Pretty:
Text: the {City in the Sky}
Text: "{City in the Sky}"
Cryptic:
Text: the {skies above}
@@ -1594,10 +1594,64 @@ Hyrule Castle:
Standard:
Text: Hyrule Castle
Pretty:
Text: the {Hyrule Castle}
Text: "{Hyrule Castle}"
Cryptic:
Text: the {kingdom's castle}
# BOSS/GOAL NAMES
Diababa:
Standard:
Text: Diababa
Fyrus:
Standard:
Text: Fyrus
Morpheel:
Standard:
Text: Morpheel
Stallord:
Standard:
Text: Stallord
Blizzeta:
Standard:
Text: Blizzeta
Armogohma:
Standard:
Text: Armogohma
Argorok:
Standard:
Text: Argorok
Zant:
Standard:
Text: Zant
Ganondorf:
Standard:
Text: Ganondorf
# HINT TEMPLATES
Path Hint:
Standard:
Text: <fast>They say that <regions> |is/are| on the path to <goal name>.
Barren Hint:
Standard:
Text: <fast>They say that visiting <region> is a foolish choice.
Item Hint:
Standard:
Text: <fast>They say that <Item Pretty or Cryptic Name> can be found at <regions>.
Location Hint:
Standard:
Text: <fast>They say that the reward for <Location Name> is <Item Pretty or Cryptic Name>.
# NO REQUIRED DUNGEON TEXT
No Required Dungeons Text:
Standard:
@@ -2212,6 +2266,26 @@ Midna Hints Required Dungeons Intro At Least One Dungeon:
Text: |-
There are <red><required dungeon count><white> required dungeons:
Midna Hints Path Hints Intro:
Standard:
Text: |-
The following <red>paths<white> lie before you:
Midna Hints Barren Hints Intro:
Standard:
Text: |-
There is <purple>nothing<white> to be found in these regions:
Midna Hints Item Hints Intro:
Standard:
Text: |-
These <green>items<white> may be of use to you:
Midna Hints Location Hints Intro:
Standard:
Text: |-
These <red>locations<white> are known to me:
Ordon Hint Sign Text:
Standard:
Text: |-
@@ -377,7 +377,7 @@
# Completely Custom Text IDs
- Name: Custom Midna Call Need Something Text
Attributes: 0x07F60000150D0000FF00000000000400
Attributes: 0x07D30000150D0100FF000D001F1F0400
- Name: Custom Midna Call 3 Choice Text
Attributes: 0x07F8000015000000FF00000000000400
@@ -386,10 +386,10 @@
Attributes: 0x07F8000015000000FF00000000000400
- Name: Custom Midna Call Hints Text
Attributes: 0x07F60000150D0000FF00000000000400
Attributes: 0x07D30000150D0100FF000D001F1F0400
- Name: Return to Spawn Dungeon Intro Text
Attributes: 0x07F60000150D0000FF00000000000400
Attributes: 0x07D30000150D0100FF000D001F1F0400
- Name: Return to Spawn Dungeon Choice Text
Attributes: 0x07F60000150D0000FF00000000000400
+35 -15
View File
@@ -5,8 +5,9 @@
#include "../utility/random.hpp"
#include "../utility/string.hpp"
#include <iostream>
#include <algorithm>
#include <iostream>
#include <ranges>
namespace randomizer::logic::fill
{
@@ -64,16 +65,18 @@ namespace randomizer::logic::fill
}
}
void AssumedFill(world::WorldPool& worlds,
item_pool::ItemPool& itemsToPlacePool,
const item_pool::ItemPool& itemsNotYetPlaced,
location::LocationPool allowedLocations,
const int& worldToFill /* = -1 */)
location::LocationPool AssumedFill(
world::WorldPool& worlds,
item_pool::ItemPool& itemsToPlacePool,
const item_pool::ItemPool& itemsNotYetPlaced,
location::LocationPool allowedLocations,
const int& worldToFill /* = -1 */)
{
// Assumed Fill may sometimes place items in such a way that accidentally locks out being able to place specific items
// later on. Allow the algorithm to retry a reasonable amount of times before returning an error.
int retries = 10;
bool unsuccessfulPlacement = true;
location::LocationPool rollbacks = {};
while (unsuccessfulPlacement)
{
if (retries <= 0)
@@ -100,7 +103,7 @@ namespace randomizer::logic::fill
utility::random::ShufflePool(itemsToPlacePool);
auto itemsToPlace = itemsToPlacePool;
location::LocationPool rollbacks = {};
rollbacks.clear();
while (!itemsToPlace.empty())
{
@@ -182,6 +185,8 @@ namespace randomizer::logic::fill
rollbacks.push_back(spotToFill);
}
}
return rollbacks;
}
void FastFill(item_pool::ItemPool& itemsToPlace, location::LocationPool allowedLocations)
@@ -277,7 +282,12 @@ namespace randomizer::logic::fill
// Place goal items at goal locations
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, goalItems, completeItemPool, goalLocations);
auto filledLocations = AssumedFill(worlds, goalItems, completeItemPool, goalLocations);
// Goal items placed at goal locations are expected items
for (auto location: filledLocations) {
location->SetExpectedItem(true);
}
// Determine required dungeons now that we placed goal location items
world->DetermineRequiredDungeons();
@@ -317,7 +327,11 @@ namespace randomizer::logic::fill
(item->GetName() == "Ordon Pumpkin" || item->GetName() == "Ordon Cheese"));
});
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, smallKeys, completeItemPool, dungeonLocations);
auto filledLocations = AssumedFill(worlds, smallKeys, completeItemPool, dungeonLocations);
// Own Dungeon small keys are expected items
for (auto location: filledLocations) {
location->SetExpectedItem(true);
}
}
// Big Keys
@@ -327,7 +341,11 @@ namespace randomizer::logic::fill
[&](const auto& item)
{ return item == dungeon_->GetBigKey(); });
auto completeItemPool = item_pool::GetCompleteItemPool(worlds);
AssumedFill(worlds, bigKeys, completeItemPool, dungeonLocations);
auto filledLocations = AssumedFill(worlds, bigKeys, completeItemPool, dungeonLocations);
// Own Dungeon big keys are expected items
for (auto location: filledLocations) {
location->SetExpectedItem(true);
}
}
// Place maps and compasses last with fast fill since they're junk items
@@ -525,7 +543,7 @@ namespace randomizer::logic::fill
LOG_TO_DEBUG("Caching timeforms for world " + std::to_string(world->GetID()));
auto& exitTimeFormCache = world->GetExitTimeFormCache();
exitTimeFormCache.clear();
for (const auto& [areaName, area] : world->GetAreaTable())
for (const auto& area : world->GetAreaTable() | std::views::values)
{
const auto& areaFormTimes = searchWithItems._areaFormTime[area.get()];
for (const auto& exit : area->GetExits())
@@ -535,10 +553,12 @@ namespace randomizer::logic::fill
for (const auto& formTime : requirement::FormTime::ALL_FORM_TIMES)
{
if (formTime & areaFormTimes &&
requirement::EvaluateRequirementAtFormTime(req,
&searchWithItems,
formTime,
world.get()))
requirement::EvaluateRequirementAtFormTime(
req,
&searchWithItems,
formTime,
world.get()
))
{
exitTimeFormCache[exit] |= formTime;
}
+9 -5
View File
@@ -22,12 +22,16 @@ namespace randomizer::logic::fill
* This is important for the assumed fill algorithm since we need to assume we have these items.
* @param allowedLocations Locations where items in itemsToPlacePool are allowed to be filled.
* @param worldToFill A specific world to fill. If -1 (default), then all worlds are considered
*
* @return The locations which had items placed at them during this fill call
*/
void AssumedFill(world::WorldPool& worlds,
item_pool::ItemPool& itemsToPlacePool,
const item_pool::ItemPool& itemsNotYetPlaced,
location::LocationPool allowedLocations,
const int& worldToFill = -1);
location::LocationPool AssumedFill(
world::WorldPool& worlds,
item_pool::ItemPool& itemsToPlacePool,
const item_pool::ItemPool& itemsNotYetPlaced,
location::LocationPool allowedLocations,
const int& worldToFill = -1
);
/**
* @brief Places items in locations completely randomly without any logic checks.
@@ -0,0 +1,42 @@
#pragma once
#include "../utility/text.hpp"
#include <string>
#include <variant>
namespace randomizer::logic::location {
class Location;
}
namespace randomizer::logic::hints {
struct PathHint {
location::Location* goalLocation;
location::Location* hintedLocation;
bool operator==(const PathHint&) const = default;
};
struct BarrenHint {
std::string region;
bool operator==(const BarrenHint&) const = default;
};
struct ItemHint {
location::Location* location;
bool operator==(const ItemHint&) const = default;
};
struct LocationHint {
location::Location* location;
bool operator==(const LocationHint&) const = default;
};
using HintData = std::variant<std::monostate, PathHint, BarrenHint, ItemHint, LocationHint>;
struct Hint {
Text text{};
HintData data;
bool operator==(const Hint&) const = default;
};
}
+925 -13
View File
@@ -1,7 +1,12 @@
#include "hints.hpp"
#include "dusk/randomizer/generator/utility/text.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>
@@ -16,15 +21,854 @@ namespace randomizer::logic::hints {
{"Temple of Time", "<dark green>"},
{"City in the Sky", "<yellow>"},
{"Palace of Twilight", "<purple>"},
// {"Hyrule Castle", "<silver>"}
{"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";
@@ -42,7 +886,7 @@ namespace randomizer::logic::hints {
const std::list<std::string>& textNames,
Text::Color color) {
auto itemName = world->GetLocation(locationName)->GetCurrentItem()->GetName();
auto itemStandardName = addColor(getTextObject(itemName), color, 1, true);
auto itemStandardName = addColor(getTextObject(itemName), color);
auto itemPrettyName = addColor(getTextObject(itemName, Text::PRETTY), color);
for (const auto& textName : textNames) {
auto& text = world->AddNewText(textName);
@@ -103,32 +947,100 @@ namespace randomizer::logic::hints {
if (numRequiredDungeons > 0) {
midnaHintText += getTextObject("Midna Hints Required Dungeons Intro At Least One Dungeon");
midnaHintText.Replace("<required dungeon count>", std::to_string(numRequiredDungeons));
// Add newlines to begin listing dungeons on the next textbox
midnaHintText += "\n\n\n\n";
midnaHintText.PadToNextBox();
// Then loop through again to add the dungeon names.
// Use dungeonColors to loop through in base game dungeon order
int displayedRequiredDungeons = 0;
for (const auto& [dungeonName, color] : dungeonColors) {
// Hyrule Castle is implicitly required
if (dungeonName == "Hyrule Castle") {
continue;
}
auto dungeon = world->GetDungeon(dungeonName);
if (dungeon->IsRequired()) {
++displayedRequiredDungeons;
midnaHintText += color + getTextObject(dungeonName) + "<white>";
// Add newline after every dungeon except the last one
if (displayedRequiredDungeons < numRequiredDungeons) {
midnaHintText += "\n";
}
midnaHintText += color + getTextObject(dungeonName) + "<white>\n";
}
}
} else {
midnaHintText += getTextObject("Midna Hints Required Dungeons Intro Zero Dungeons");
}
midnaHintText.PadToNextBox();
midnaHintText.BreakLines();
// 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);
+23 -1
View File
@@ -1,5 +1,7 @@
#pragma once
#include "hint_types.hpp"
#include <memory>
namespace randomizer::logic::world {
@@ -9,6 +11,26 @@ using WorldPool = std::vector<std::unique_ptr<World>>;
namespace randomizer::logic::hints {
void GenerateAllHints(world::WorldPool& worldPool);
class HintGenerator {
public:
HintGenerator(world::World* world);
void GeneratePathHints();
void GenerateBarrenHints();
void GenerateItemHints();
void GenerateRemoteLocationHints();
void GenerateLocationHints();
void DistributeHints();
void FinalizeHintSignText();
private:
world::World* _world{};
std::vector<Hint> _pathHints{};
std::vector<Hint> _barrenHints{};
std::vector<Hint> _itemHints{};
std::vector<Hint> _locationHints{};
};
void GenerateAllHints(world::WorldPool& worldPool);
}
+28 -1
View File
@@ -105,7 +105,11 @@ namespace randomizer::logic::item
return this->_gameWinningItem;
}
std::list<Location*> Item::GetChainLocations() const
void Item::AddChainLocation(location::Location* location) {
this->_chainLocations.push_back(location);
}
std::list<location::Location*> Item::GetChainLocations() const
{
return this->_chainLocations;
}
@@ -150,6 +154,29 @@ namespace randomizer::logic::item
return this->_stamp;
}
bool Item::CanBeInBarrenRegion() const {
return !this->IsMajor() ||
(this->IsDungeonSmallKey() && this->_world->Setting("Small Keys").IsAnyOf("Vanilla", "Own Dungeon")) ||
(this->IsBigKey() && this->_world->Setting("Big Keys").IsAnyOf("Vanilla", "Own Dungeon"));
}
bool Item::IsSameOrSimilarItem(Item* otherItem) const {
if (this == otherItem) {
return true;
}
// Heart Piece Items
std::set<const Item*> heartPieceItems = {
this->_world->GetItem("Heart Container"),
this->_world->GetItem("Piece of Heart"),
};
if (heartPieceItems.contains(this) && heartPieceItems.contains(otherItem)) {
return true;
}
return false;
}
bool Item::operator==(const Item& rhs) const
{
return this->_id == rhs._id && this->_world->GetID() == rhs._world->GetID();
+8 -3
View File
@@ -8,7 +8,9 @@ namespace randomizer::logic::world
{
class World;
};
class Location;
namespace randomizer::logic::location {
class Location;
}
namespace randomizer::logic::item
{
enum Importance
@@ -43,7 +45,8 @@ namespace randomizer::logic::item
bool IsMinor() const;
bool isJunk() const;
bool IsGameWinningItem() const;
std::list<Location*> GetChainLocations() const;
void AddChainLocation(location::Location* location);
std::list<location::Location*> GetChainLocations() const;
bool IsDungeonSmallKey() const;
bool IsBigKey() const;
bool IsDungeonMap() const;
@@ -52,6 +55,8 @@ namespace randomizer::logic::item
bool IsShadowCrystal() const;
bool IsBottle() const;
bool IsStamp() const;
bool CanBeInBarrenRegion() const;
bool IsSameOrSimilarItem(Item* otherItem) const;
bool operator==(const Item& rhs) const;
bool operator<(const Item& rhs) const;
@@ -62,7 +67,7 @@ namespace randomizer::logic::item
world::World* _world = nullptr;
Importance _importance = INVALID;
bool _gameWinningItem = false;
std::list<Location*> _chainLocations;
std::list<location::Location*> _chainLocations;
bool _dungeonSmallKey = false;
bool _bigKey = false;
@@ -10,7 +10,8 @@ namespace randomizer::logic::location
const std::unordered_set<std::string>& categories,
world::World* world,
item::Item* originalItem,
const bool& goalLocation,
bool goalLocation,
const std::string& goalName,
const std::string& hintPriority,
const YAML::Node& metadata):
_id(id),
@@ -19,6 +20,7 @@ namespace randomizer::logic::location
_world(world),
_originalItem(originalItem),
_goalLocation(goalLocation),
_goalName(goalName),
_hintPriority(hintPriority),
_metadata(metadata)
{
@@ -45,6 +47,10 @@ namespace randomizer::logic::location
return this->_goalLocation;
}
const std::string& Location::GetGoalName() const {
return this->_goalName;
}
void Location::SetCurrentItem(item::Item* item)
{
LOG_TO_DEBUG("Placed " + item->GetName() + " at " + this->GetName());
@@ -77,7 +83,7 @@ namespace randomizer::logic::location
return this->_trackedItem;
}
void Location::SetKnownVanillaItem(const bool& hasKnownVanillaItem)
void Location::SetKnownVanillaItem(bool hasKnownVanillaItem)
{
this->_hasKnownVanillaItem = hasKnownVanillaItem;
}
@@ -87,6 +93,15 @@ namespace randomizer::logic::location
return this->_hasKnownVanillaItem;
}
void Location::SetExpectedItem(bool hasExpectedItem) {
this->_hasExpectedItem = hasExpectedItem;
}
bool Location::HasExpectedItem() const {
return this->_hasExpectedItem || this->_hasKnownVanillaItem;
}
void Location::SetProgression(const bool& progression)
{
this->_progression = progression;
@@ -149,6 +164,22 @@ namespace randomizer::logic::location
this->_registeredLocationCategories = registeredLocationCategories;
}
void Location::AddPathLocation(Location* location) {
this->_pathLocations.push_back(location);
}
std::vector<Location*>& Location::GetPathLocations() {
return this->_pathLocations;
}
void Location::SetImportance(Importance importance) {
this->_importance = importance;
}
Importance Location::GetImportance() const {
return this->_importance;
}
const std::set<std::string>& GetAllRandomizerLocationNames() {
static std::set<std::string> locationNames{};
@@ -20,6 +20,13 @@ namespace randomizer::logic::area
namespace randomizer::logic::location
{
enum class Importance {
UNKNOWN,
NOT_REQUIRED,
POSSIBLY_REQUIRED,
REQUIRED,
};
class Location
{
public:
@@ -28,7 +35,8 @@ namespace randomizer::logic::location
const std::unordered_set<std::string>& categories,
world::World* world,
item::Item* originalItem,
const bool& goalLocation,
bool goalLocation,
const std::string& goalName,
const std::string& hintPriority,
const YAML::Node& metadata);
@@ -36,14 +44,17 @@ namespace randomizer::logic::location
std::string GetName() const;
world::World* GetWorld() const;
bool IsGoalLocation() const;
const std::string& GetGoalName() const;
void SetCurrentItem(item::Item* currentItem);
item::Item* GetCurrentItem() const;
void RemoveCurrentItem();
bool IsEmpty() const;
item::Item* GetOriginalItem() const;
item::Item* GetTrackedItem() const;
void SetKnownVanillaItem(const bool& hasKnownVanillaItem);
void SetKnownVanillaItem(bool hasKnownVanillaItem);
bool HasKnownVanillaItem() const;
void SetExpectedItem(bool hasExpectedItem);
bool HasExpectedItem() const;
void SetProgression(const bool& progression);
bool IsProgression() const;
void SetHinted(const bool& hinted);
@@ -56,6 +67,10 @@ namespace randomizer::logic::location
void SetComputedRequirement(const requirement::Requirement& computedRequirement);
requirement::Requirement GetComputedRequirement();
void SetRegisteredLocationCategories(std::unordered_set<std::string>* registeredLocationCategories);
void AddPathLocation(Location* location);
std::vector<Location*>& GetPathLocations();
void SetImportance(Importance importance);
Importance GetImportance() const;
/**
* @brief Checks to see if the location has all the passed in categories. If a passed in category was never registered,
@@ -84,13 +99,15 @@ namespace randomizer::logic::location
private:
int _id = -1;
std::string _name = "";
std::string _name{};
std::unordered_set<std::string> _categories = {};
world::World* _world;
item::Item* _originalItem = item::Nothing.get();
bool _goalLocation = false;
std::string _goalName{};
item::Item* _currentItem = item::Nothing.get();
bool _hasKnownVanillaItem = false;
bool _hasExpectedItem = false;
std::list<area::LocationAccess*> _locationAccessList = {};
bool _progression = true; // Set as false later if applicable
bool _hinted = false;
@@ -105,6 +122,10 @@ namespace randomizer::logic::location
*/
std::unordered_set<std::string>* _registeredLocationCategories = nullptr;
// Hint related things
std::vector<Location*> _pathLocations{};
Importance _importance = Importance::UNKNOWN;
// Potential tracker stuff
item::Item* _trackedItem = item::Nothing.get();
};
@@ -149,6 +149,82 @@ namespace randomizer::logic::requirement
return reqStr;
}
// Return a set of all items mentioned in this requirement
std::unordered_set<item::Item*> Requirement::getItems(world::World* world) const {
std::unordered_set<item::Item*> items = {};
switch (this->_type) {
case Type::AND:
case Type::OR:
for (const auto& arg : this->_args) {
items.merge(std::get<Requirement>(arg).getItems(world));
}
break;
case Type::ITEM:
items.insert(std::get<item::Item*>(this->_args[0]));
break;
case Type::COUNT:
items.insert(std::get<item::Item*>(this->_args[1]));
break;
case Type::HEARTS:
items.insert(world->GetItem("Piece of Heart"));
items.insert(world->GetItem("Heart Container"));
break;
default:
break;
}
return items;
}
// Return the heart count necessary for this requirement
int Requirement::getHeartCount(int curCount /*= 0*/) const {
switch (this->_type) {
case Type::AND:
case Type::OR:
for (const auto& arg : this->_args) {
curCount = std::get<Requirement>(arg).getHeartCount(curCount);
}
break;
case Type::HEARTS:
if (std::get<int>(this->_args[0]) > curCount) {
curCount = std::get<int>(this->_args[0]);
}
break;
default:
break;
}
return curCount;
}
bool Requirement::operator==(const Requirement& other) const {
// Types and argument counts must match
if (_type != other._type || _args.size() != other._args.size()) {
return false;
}
// Check matches to account for different ordering
std::vector<bool> matched(other._args.size(), false);
for (const auto& arg : _args) {
bool foundMatch = false;
for (size_t i = 0; i < other._args.size(); ++i) {
if (!matched[i] && arg == other._args[i]) {
matched[i] = true;
foundMatch = true;
break;
}
}
// If an argument in _args has no unassigned match in other._args, they are not equal
if (!foundMatch) {
return false;
}
}
return true;
}
Requirement ParseRequirementString(const std::string& reqStr,
world::World* world,
const bool& forceLogic /* = false */)
@@ -555,6 +631,8 @@ namespace randomizer::logic::requirement
int count;
int eventIndex;
int macroIndex;
bool changedToOnlyItemsAtStart = false;
bool evaluation = false;
switch (req._type)
{
case Type::NOTHING:
@@ -564,11 +642,27 @@ namespace randomizer::logic::requirement
return false;
case Type::OR:
return std::ranges::any_of(
if (search->_searchMode == search::SearchMode::LOCATION_IMPORTANCE &&
std::ranges::any_of(req._args, [&](const auto& arg) {
return std::get<Requirement>(arg) == search->_assumedFalseReq;
}) &&
!search->_onlySearchWithItemsAtStart)
{
changedToOnlyItemsAtStart = true;
search->_onlySearchWithItemsAtStart = true;
}
evaluation = std::ranges::any_of(
req._args,
[&](const auto& arg)
{ return EvaluateRequirementAtFormTime(std::get<Requirement>(arg), search, formTime, world); });
if (changedToOnlyItemsAtStart) {
search->_onlySearchWithItemsAtStart = false;
}
return evaluation;
case Type::AND:
return std::ranges::all_of(
req._args,
@@ -577,11 +671,17 @@ namespace randomizer::logic::requirement
case Type::ITEM:
item = std::get<item::Item*>(req._args[0]);
if (search->_onlySearchWithItemsAtStart) {
return search->_itemsAtStart.contains(item);
}
return search->_ownedItems.contains(item);
case Type::COUNT:
count = std::get<int>(req._args[0]);
item = std::get<item::Item*>(req._args[1]);
if (search->_onlySearchWithItemsAtStart) {
return search->_itemsAtStart.count(item) >= count;
}
return search->_ownedItems.count(item) >= count;
case Type::EVENT:
@@ -614,6 +714,10 @@ namespace randomizer::logic::requirement
case Type::HEARTS:
count = std::get<int>(req._args[0]);
if (search->_searchMode == search::SearchMode::LOCATION_IMPORTANCE &&
search->_assumedHeartCount >= count) {
return true;
}
heartPiece = world->GetItem("Piece of Heart");
heartContainer = world->GetItem("Heart Container");
return search->_ownedItems.count(heartPiece) +
@@ -1,6 +1,7 @@
#pragma once
#include <string>
#include <unordered_set>
#include <variant>
#include <vector>
@@ -96,6 +97,10 @@ namespace randomizer::logic::requirement
std::vector<Argument> _args;
std::string to_string() const;
std::unordered_set<item::Item*> getItems(world::World* world) const;
int getHeartCount(int curCount = 0) const;
bool operator==(const Requirement& other) const;
};
Requirement ParseRequirementString(const std::string& reqStr,
+60 -5
View File
@@ -18,8 +18,10 @@ namespace randomizer::logic::search
world::WorldPool* worlds,
const item_pool::ItemPool& items /* = {} */,
const int& worldToSearch /* = -1 */,
bool startingInventory /*= true */):
_searchMode(searchMode), _worlds(worlds), _startingInventory(startingInventory)
bool startingInventory /*= true */,
location::Location* importanceLocation /*= nullptr*/):
_searchMode(searchMode), _worlds(worlds), _startingInventory(startingInventory),
_importanceLocation(importanceLocation)
{
// Set the items we should already own
this->_ownedItems.insert(items.begin(), items.end());
@@ -34,6 +36,7 @@ namespace randomizer::logic::search
this->_ownedItems.insert(startingInventory.begin(), startingInventory.end());
}
}
this->_itemsAtStart = _ownedItems;
}
// Set search starting properties and add each world's root exits to _exitsToTry
@@ -53,6 +56,33 @@ namespace randomizer::logic::search
}
}
}
// If we're doing an importance search, then we have to set up a few extra things
if (this->_searchMode == SearchMode::LOCATION_IMPORTANCE && importanceLocation != nullptr) {
auto importanceItem = importanceLocation->GetCurrentItem();
// Generate the logic requirement for what it would "mean" to obtain the item at this importance location.
// We're going to assume that this logic requirement is always false when searching.
// This ensures that we only evaluate to true the chain locations that this item absolutely does not help unlock
auto count = this->_ownedItems.count(importanceItem) + 1;
if (count == 1) {
this->_assumedFalseReq = requirement::Requirement{requirement::Type::ITEM, {importanceItem}};
} else {
this->_assumedFalseReq = requirement::Requirement{requirement::Type::COUNT, {count, importanceItem}};
}
// If this location is either a heart container or piece of heart, we'll pass along any assumed heart
// count from this location or any of its path locations. This will properly take into account any
// heart requirements that were necessary to get here and adjust hint importance accordingly.
// (I.e. If Accessing Hyrule Castle requires 10 hearts, and this location has a heart piece, but its
// locked by something in Hyrule Castle, then it'll always be not required.)
if (importanceItem->IsSameOrSimilarItem(importanceItem->GetWorld()->GetItem("Heart Container"))) {
this->_assumedHeartCount = this->_importanceLocation->GetComputedRequirement().getHeartCount();
for (auto location : this->_importanceLocation->GetPathLocations()) {
this->_assumedHeartCount = std::max(this->_assumedHeartCount, location->GetComputedRequirement().getHeartCount());
}
}
}
}
void Search::SearchWorlds()
@@ -224,8 +254,11 @@ namespace randomizer::logic::search
void Search::ProcessLocation(location::Location* location)
{
// Don't return if we aren't collecting items
if (!this->_collectItems)
// Don't return if we aren't collecting items, or if we're doing an importance search
// and the item at this location is a similar item to our importance location
if (!this->_collectItems ||
(this->_searchMode == SearchMode::LOCATION_IMPORTANCE &&
this->_importanceLocation->GetCurrentItem()->IsSameOrSimilarItem(location->GetCurrentItem())))
{
return;
}
@@ -661,4 +694,26 @@ namespace randomizer::logic::search
return search._isBeatable;
}
} // namespace randomizer::logic::search
location::LocationPool GetPossibleHintSigns(location::Location* location) {
// Remove the item at the location
const auto itemAtLocation = location->GetCurrentItem();
location->RemoveCurrentItem();
// Run an accessible search
auto search = Search::Accessible(&location->GetWorld()->GetRandomizer()->GetWorlds());
search.SearchWorlds();
// Give the item back
location->SetCurrentItem(itemAtLocation);
// Return all the hint signs that the search found
location::LocationPool hintSigns{};
for (auto loc : search._visitedLocations) {
if (loc->HasCategories("Hint Sign")) {
hintSigns.push_back(loc);
}
}
return hintSigns;
}
} // namespace randomizer::logic::search
+28 -7
View File
@@ -1,6 +1,8 @@
#pragma once
#include "item_pool.hpp"
#include "requirement.hpp"
#include "location.hpp"
#include "../utility/log.hpp"
#include <unordered_set>
@@ -27,11 +29,6 @@ namespace randomizer::logic::item
class Item;
}
namespace randomizer::logic::location
{
class Location;
}
namespace randomizer::logic::area
{
class EventAccess;
@@ -54,7 +51,8 @@ namespace randomizer::logic::search
ALL_LOCATIONS_REACHABLE,
GENERATE_PLAYTHROUGH,
SPHERE_ZERO,
TRACKER_SPHERES
TRACKER_SPHERES,
LOCATION_IMPORTANCE,
};
class Search
@@ -65,7 +63,8 @@ namespace randomizer::logic::search
world::WorldPool* worlds,
const item_pool::ItemPool& items = {},
const int& worldToSearch = -1,
bool startingInventory = true);
bool startingInventory = true,
location::Location* importanceLocation = nullptr);
static auto Accessible(world::WorldPool* worlds,
const item_pool::ItemPool& items = {},
@@ -109,6 +108,14 @@ namespace randomizer::logic::search
return Search(SearchMode::SPHERE_ZERO, worlds, items, worldToSearch);
}
static auto LocationImportance(world::WorldPool* worlds,
location::Location* importanceLocation,
const item_pool::ItemPool& items = {},
const int& worldToSearch = -1)
{
return Search(SearchMode::LOCATION_IMPORTANCE, worlds, items, worldToSearch, false, importanceLocation);
}
void SearchWorlds();
/**
@@ -159,6 +166,13 @@ namespace randomizer::logic::search
std::list<std::list<entrance::Entrance*>> _entranceSpheres;
std::unordered_map<area::Area*, int> _areaFormTime;
// Variables used for Location Importance searches
std::unordered_multiset<item::Item*> _itemsAtStart{};
bool _onlySearchWithItemsAtStart = false;
int _assumedHeartCount = 0;
requirement::Requirement _assumedFalseReq = requirement::IMPOSSIBLE_REQUIREMENT;
location::Location* _importanceLocation = nullptr;
};
/**
@@ -173,4 +187,11 @@ namespace randomizer::logic::search
const item_pool::ItemPool& items = {});
void GeneratePlaythrough(randomizer::Randomizer* randomizer);
bool GameBeatable(world::WorldPool* worlds, const item_pool::ItemPool& items = {});
/**
* @brief Returns the pool of hint signs that the passed in location can be hinted at.
* We don't want a hint sign to hint a location which is logically required to access
* itself, as that is logically useless information.
*/
location::LocationPool GetPossibleHintSigns(location::Location* location);
} // namespace randomizer::logic::search
@@ -4,6 +4,7 @@
#include "../randomizer.hpp"
#include "../utility/file.hpp"
#include "../utility/platform.hpp"
#include "../utility/string.hpp"
#include "../utility/yaml.hpp"
#include <version.h>
@@ -245,7 +246,45 @@ namespace randomizer::logic::spoiler_log
}
}
// TODO: Hints
// Print Hints
spoilerLog << std::endl << "Hints:" << std::endl;
for (const auto& world : worlds) {
// Midna hints first
spoilerLog << " World " << world->GetID() << ":" << std::endl;
spoilerLog << " Midna:" << std::endl;
auto midnaText = world->GetText("Custom Midna Call Hints Text");
midnaText = utility::str::Replace(midnaText, "\n", "\n ");
spoilerLog << " " << midnaText << std::endl;
spoilerLog << std::endl;
// Put sign hints into a structure that will automatically sort them
const auto& hintSignHints = world->GetHintSignHints();
std::map<std::string, std::list<std::string>> hints{};
for (const auto& [hintSign, hintsOnSign] : hintSignHints) {
for (const auto& hint : hintsOnSign) {
// Format the hints so they appear nice in the spoiler log
auto hintText = hint.text.mText[Text::ENGLISH];
hintText = utility::str::Replace(hintText, "\n", "\n ");
// If this is a path hint, also include the location and item it's referring to
if (auto pathHint = std::get_if<hints::PathHint>(&hint.data)) {
hintText += " (" + pathHint->hintedLocation->GetName() + ": " + pathHint->hintedLocation->GetCurrentItem()->GetName() + ")";
}
hints[hintSign->GetName()].push_back(hintText);
}
}
// Then print them
if (!hints.empty()) {
spoilerLog << " Hint Signs:" << std::endl;
for (const auto& [signName, hintsOnSign] : hints) {
spoilerLog << " " << signName << ":" << std::endl;
for (const auto& hint : hintsOnSign) {
spoilerLog << " " << hint << std::endl;
}
}
spoilerLog << std::endl;
}
}
// Log Settings
LogSettings(spoilerLog, randomizer);
+24 -5
View File
@@ -181,7 +181,12 @@ namespace randomizer::logic::world
}
auto originalItem = this->GetItem(originalItemName);
auto goalLocation = locationNode["Goal Location"].as<bool>(false);
bool goalLocation = false;
std::string goalName = name;
if (locationNode["Goal Name"]) {
goalLocation = true;
goalName = locationNode["Goal Name"].as<std::string>();
}
auto hintPriority = locationNode["Hint Priority"].as<std::string>("Never");
auto metadata = locationNode["Metadata"];
@@ -200,6 +205,7 @@ namespace randomizer::logic::world
this,
originalItem,
goalLocation,
goalName,
hintPriority,
metadata);
@@ -662,7 +668,7 @@ namespace randomizer::logic::world
// Some locations not being randomized can conflict with other settings. When
// the appropriate location and setting conflict, these locations should have their item
// removed and be set to nonprogress.
for (auto& [locationName, location] : this->_locationTable)
for (auto& location : this->_locationTable | std::views::values)
{
auto originalItem = location->GetOriginalItem();
auto originalItemName = originalItem->GetName();
@@ -753,12 +759,12 @@ namespace randomizer::logic::world
void World::AssignGoalLocations()
{
std::unordered_map<std::string, location::LocationPool> dungeonGoalLocations = {};
for (const auto& [dungeonName, dungeon] : this->_dungeons)
for (const auto& dungeonName : this->_dungeons | std::views::keys)
{
dungeonGoalLocations[dungeonName] = {};
}
// Collect all the possible goal locations for each dungeon
for (auto& [areaName, area] : this->_areaTable)
for (auto& area : this->_areaTable | std::views::values)
{
for (const auto& locAcc : area->GetLocations())
{
@@ -1190,7 +1196,7 @@ namespace randomizer::logic::world
location::LocationPool World::GetAllLocations(const bool& includeNonItemLocations /* = false */)
{
location::LocationPool locationPool = {};
for (const auto& [locationName, location] : this->_locationTable)
for (const auto& location : this->_locationTable | std::views::values)
{
if (includeNonItemLocations || !location->HasCategories("Non-Item Location"))
{
@@ -1200,6 +1206,19 @@ namespace randomizer::logic::world
return locationPool;
}
location::LocationPool World::GetHintSignLocations()
{
location::LocationPool locationPool = {};
for (const auto& location : this->_locationTable | std::views::values)
{
if (location->HasCategories("Hint Sign"))
{
locationPool.emplace_back(location.get());
}
}
return locationPool;
}
area::Area* World::GetArea(const std::string& name, const bool& createIfNotFound /* = false */)
{
if (!this->_areaTable.contains(name))
@@ -6,6 +6,7 @@
#include "item_pool.hpp"
#include "location.hpp"
#include "requirement.hpp"
#include "hint_types.hpp"
#include "../seedgen/settings.hpp"
#include "../utility/log.hpp"
@@ -133,6 +134,7 @@ namespace randomizer::logic::world
item_pool::ItemPool& GetStartingItemPool();
location::Location* GetLocation(const std::string& name);
location::LocationPool GetAllLocations(const bool& includeNonItemLocations = false);
location::LocationPool GetHintSignLocations();
area::Area* GetArea(const std::string& name, const bool& createIfNotFound = false);
area::Area* GetRootArea() const;
const std::map<std::string, std::unique_ptr<area::Area>>& GetAreaTable() const;
@@ -161,11 +163,28 @@ namespace randomizer::logic::world
return this->_textDatabase.at(name).at(type).mText.at(Text::ENGLISH);
}
Text& GetTextObject(const std::string& name, Text::Type type = Text::STANDARD) {
if (!this->_textDatabase.contains(name)) {
throw std::runtime_error(name + " is not a text object in world " + std::to_string(this->_id));
}
return this->_textDatabase[name][type];
}
// Make a new custom text entry for this world specifically and return a reference to it
Text& AddNewText(const std::string& name, Text::Type type = Text::STANDARD) {
return this->_textDatabase[name][type];
}
void AddGoalLocation(location::Location* location) {
this->_goalLocations.push_back(location);
}
const auto& GetGoalLocations() {return this->_goalLocations;}
auto& GetBarrenRegions() { return this->_barrenRegions;}
auto& GetMidnaHints() { return this->_midnaHints;}
auto& GetHintSignHints() { return this->_hintSignHints;}
private:
int _id = -1;
int _entranceIdCounter = 0;
@@ -192,6 +211,12 @@ namespace randomizer::logic::world
std::unordered_map<location::Location*, item::Item*> _plandomizerLocations = {};
std::unordered_map<entrance::Entrance*, entrance::Entrance*> _plandomizerEntrances = {};
// Hint stuff
std::list<location::Location*> _goalLocations = {};
std::map<std::string, std::list<location::Location*>> _barrenRegions = {};
std::vector<hints::Hint> _midnaHints{};
std::unordered_map<location::Location*, std::vector<hints::Hint>> _hintSignHints = {};
Randomizer* _randomizer = nullptr;
};
} // namespace randomizer::logic::world
+11 -4
View File
@@ -136,14 +136,21 @@ namespace randomizer
}
logic::fill::CacheExitTimeForms(this->_worlds);
// Flattening isn't used for anything yet, but flattens down the requirements for
// each location and entrance into a single statement. This will be useful for hints and could potentially
// be used to speed up the fill algorithm (but the fill algorithm is already pretty fast, so we'd only gain maybe like
// 0.2 seconds back or something)
// Flattens down the requirements for each location and entrance into a single statement.
// This is used for calculating hint importance.
utility::platform::Log("Flattening...");
FlattenSearch search = FlattenSearch(this->_worlds.at(0).get());
search.doSearch();
// Set chain locations once the flatten search is done
for (auto& world : this->_worlds) {
for (auto& location : world->GetAllLocations()) {
for (auto& item : location->GetComputedRequirement().getItems(world.get())) {
item->AddChainLocation(location);
}
}
}
utility::platform::Log("Filling Worlds...");
logic::fill::FillWorlds(this->_worlds);
@@ -1,10 +1,11 @@
#pragma once
#include <string>
#include "settings.hpp"
#include "../utility/path.hpp"
#include <optional>
#include <string>
// forward declaration
namespace YAML
{
@@ -89,4 +89,20 @@ namespace randomizer::utility::str {
return std::nullopt;
}
std::string Replace(const std::string& originalStr,
const std::string& oldStr,
const std::string& replacementStr,
uint32_t count) {
std::string retStr = originalStr;
auto startPos = retStr.find(oldStr);
for (int i = 0; i < count; ++i) {
if (startPos == std::string::npos) {
return retStr;
}
retStr.replace(startPos, oldStr.length(), replacementStr);
startPos = retStr.find(oldStr, startPos + replacementStr.length());
}
return retStr;
}
}
@@ -9,6 +9,8 @@
#include <iomanip>
#include <algorithm>
#include <cstring>
#include <limits>
#include <optional>
namespace randomizer::utility::str {
std::string toUTF8(const std::u16string& str);
@@ -115,4 +117,8 @@ namespace randomizer::utility::str {
}
std::optional<int> toInt(std::string_view str);
std::string Replace(const std::string& originalStr,
const std::string& oldStr,
const std::string& replacementStr,
uint32_t count = std::numeric_limits<uint32_t>::max());
}
+157 -20
View File
@@ -1,9 +1,11 @@
#include "text.hpp"
#include "string.hpp"
#include "yaml.hpp"
#include <fmt/format.h>
#include <ranges>
#include <unordered_map>
@@ -15,25 +17,16 @@ namespace randomizer {
}
}
void Text::Replace(const std::string& oldStr, const Text& replacementText, int count/* = 1*/) {
void Text::Replace(const std::string& oldText, const Text& replacementText, uint32_t count/* = max*/) {
for (size_t i = 0; i < mText.size(); ++i) {
auto& curString = mText[i];
for (int i = 0; i < count; ++i) {
if (auto startPos = curString.find(oldStr); startPos != std::string::npos) {
curString.replace(startPos, oldStr.length(), replacementText.mText[i]);
}
}
curString = utility::str::Replace(curString, oldText, replacementText.mText[i], count);
}
}
void Text::Replace(const std::string& oldStr, const std::string& replacementText, int count/* = 1*/) {
for (size_t i = 0; i < mText.size(); ++i) {
auto& curString = mText[i];
for (int i = 0; i < count; ++i) {
if (auto startPos = curString.find(oldStr); startPos != std::string::npos) {
curString.replace(startPos, oldStr.length(), replacementText);
}
}
void Text::Replace(const std::string& oldText, const std::string& replacementText, uint32_t count/* = max*/) {
for (auto& text : mText) {
text = utility::str::Replace(text, oldText, replacementText, count);
}
}
@@ -69,6 +62,17 @@ namespace randomizer {
}
}
void Text::PadToNextBox() {
BreakLines();
for (auto& text : mText) {
size_t numNewLines = std::ranges::count_if(text, [](char c){return c == '\n';});
while (numNewLines == 0 || text.back() != '\n' || numNewLines % LINES_PER_BOX != 0) {
text += '\n';
++numNewLines;
}
}
}
bool Text::Empty() const {
for (auto& text : mText) {
if (!text.empty()) {
@@ -78,6 +82,80 @@ namespace randomizer {
return true;
}
bool Text::IsTooLong() const {
for (auto& text : mText) {
auto numNewLines = std::ranges::count_if(text, [](char c){return c == '\n';});
if (numNewLines > MAX_NEWLINES_PER_MESSAGE) {
return true;
}
}
return false;
}
// Will split this text object into multiple objects short enough to fit into individual
// text ids
std::vector<Text> Text::SplitToFitTextLimits() {
if (this->IsTooLong()) {
// Figure out how many new text objects we need to fit all the text
size_t numTextObjects{1};
for (auto& text : mText) {
double numNewLines = std::ranges::count_if(text, [](char c){return c == '\n';});
auto curTextSplitAmount = static_cast<size_t>(std::ceil(numNewLines / MAX_NEWLINES_PER_MESSAGE));
numTextObjects = std::max(curTextSplitAmount, numTextObjects);
}
std::vector<Text> splitText{numTextObjects};
// Split each string into the appropriate number of objects
for (size_t textIdx = 0; textIdx < mText.size(); ++textIdx) {
auto& textStr = mText[textIdx];
// Calculate how many newlines we're allowing in this string per message
// Different languages may have different amounts of newlines
double numNewLines = std::ranges::count_if(textStr, [](char c){return c == '\n';});
auto newLinesPerMessage = static_cast<size_t>(std::ceil(numNewLines / numTextObjects));
// Keep the number of lines as a multiple of how many lines are in a box so we don't split in the middle of a textbox
while (newLinesPerMessage % LINES_PER_BOX != 0) {
++newLinesPerMessage;
}
size_t splitIdx = 0;
do {
size_t pos = 0;
for (int i = 0; i < newLinesPerMessage; ++i) {
pos = textStr.find('\n', pos);
if (pos == std::string::npos) {
break;
}
++pos;
}
// Get the current split of the string
auto curSplit = textStr.substr(0, pos);
// Pop off the last newline since it's unnecessary
if (curSplit.back() == '\n') {
curSplit.pop_back();
}
splitText.at(splitIdx).mText[textIdx] = curSplit;
++splitIdx;
if (pos == std::string::npos) {
textStr.clear();
} else {
textStr = textStr.substr(pos);
}
} while (!textStr.empty());
}
// Recopy the front element to this object
*this = splitText[0];
// Reassign the vector everything except the first element
splitText.assign(splitText.begin() + 1, splitText.end());
return splitText;
}
return {};
}
Text& Text::operator+=(const Text& rhs) {
for (size_t i = 0; i < mText.size(); ++i) {
mText[i] += rhs.mText[i];
@@ -297,7 +375,7 @@ namespace randomizer {
return tb.at(name).at(type).mText.at(language);
}
Text addColor(const Text& t, Text::Color color, int count /* = 1*/, bool forceAround /* = false*/) {
Text addColor(const Text& t, Text::Color color, int count /* = 1*/) {
const static std::unordered_map<Text::Color, std::string> colorStrings = {
{Text::WHITE, "<white>"},
{Text::RED, "<red>"},
@@ -320,11 +398,16 @@ namespace randomizer {
}
Text text = t;
if (forceAround) {
text = colorStrings.at(color) + text + colorStrings.at(Text::WHITE);
for (auto& langText : text.mText) {
// If we don't have brackets indicating color, then surround the entire text
if (langText.find('{') == std::string::npos && langText.find('}') == std::string::npos) {
langText = colorStrings.at(color) + langText + colorStrings.at(Text::WHITE);
} else {
langText = utility::str::Replace(langText, "{", colorStrings.at(color), count);
langText = utility::str::Replace(langText, "}", colorStrings.at(Text::WHITE), count);
}
}
text.Replace("{", colorStrings.at(color), count);
text.Replace("}", colorStrings.at(Text::WHITE), count);
return text;
}
@@ -368,7 +451,7 @@ namespace randomizer {
// Skip over control codes since they don't get displayed
std::string code{};
for (const auto& [messageCode, replacement] : messageCodes) {
for (const auto& messageCode : messageCodes | std::views::keys) {
if (str.substr(i, messageCode.length()) == messageCode) {
code = messageCode;
break;
@@ -432,4 +515,58 @@ namespace randomizer {
}
}
}
Text makeTextListing(std::vector<Text> texts) {
if (texts.empty()) {
return Text{};
}
if (texts.size() == 1) {
return texts.front();
}
// TODO: Other language listing rules
std::string english{};
std::string french{};
std::string german{};
std::string italian{};
std::string spanish{};
for (int i = 0; i < texts.size(); ++i) {
auto& text = texts[i];
// English rules. Move other languages out of english when we have their rules
if (i == 0) {
english += text.mText[Text::ENGLISH];
french += text.mText[Text::FRENCH];
german += text.mText[Text::GERMAN];
italian += text.mText[Text::ITALIAN];
spanish += text.mText[Text::SPANISH];
} else if (i == texts.size() - 1 && texts.size() == 2) {
english += " and " + text.mText[Text::ENGLISH];
french += " and " + text.mText[Text::FRENCH];
german += " and " + text.mText[Text::GERMAN];
italian += " and " + text.mText[Text::ITALIAN];
spanish += " and " + text.mText[Text::SPANISH];
} else if (i == texts.size() - 1) {
english += ", and " + text.mText[Text::ENGLISH];
french += ", and " + text.mText[Text::FRENCH];
german += ", and " + text.mText[Text::GERMAN];
italian += ", and " + text.mText[Text::ITALIAN];
spanish += ", and " + text.mText[Text::SPANISH];
} else {
english += ", " + text.mText[Text::ENGLISH];
french += ", " + text.mText[Text::FRENCH];
german += ", " + text.mText[Text::GERMAN];
italian += ", " + text.mText[Text::ITALIAN];
spanish += ", " + text.mText[Text::SPANISH];
}
}
Text listingText{};
listingText.mText[Text::ENGLISH] = english;
listingText.mText[Text::FRENCH] = french;
listingText.mText[Text::GERMAN] = german;
listingText.mText[Text::ITALIAN] = italian;
listingText.mText[Text::SPANISH] = spanish;
return listingText;
}
}; // namespace Text
+15 -5
View File
@@ -3,6 +3,7 @@
#include <string>
#include <array>
#include <unordered_map>
#include <limits>
namespace randomizer {
class Text {
@@ -59,6 +60,8 @@ namespace randomizer {
static constexpr size_t MAX_LINE_WIDTH_ITEM_TEXTBOX = 441;
static constexpr size_t MAX_LINE_WIDTH_NORMAL_TEXTBOX = 750;
static constexpr size_t MAX_NEWLINES_PER_MESSAGE = 40;
static constexpr size_t LINES_PER_BOX = 4;
Text() = default;
explicit Text(const std::string& str);
@@ -69,15 +72,21 @@ namespace randomizer {
/**
*
* @param oldStr the string to replace
* @param oldText the string to replace
* @param replacementText the Text object to replace the old string
* @param count the number of occurrences to replace
*/
void Replace(const std::string& oldStr, const Text& replacementText, int count = 1);
void Replace(const std::string& oldStr, const std::string& replacementText, int count = 1);
void Replace(const std::string& oldText, const Text& replacementText, uint32_t count = std::numeric_limits<uint32_t>::max());
void Replace(const std::string& oldText, const std::string& replacementText, uint32_t count = std::numeric_limits<uint32_t>::max());
void BreakLines(int maxLineWidth = MAX_LINE_WIDTH_NORMAL_TEXTBOX);
// Inserts newlines to pad the text to the next box
void PadToNextBox();
void Capitalize();
bool Empty() const;
bool IsTooLong() const;
std::vector<Text> SplitToFitTextLimits();
bool operator==(const Text& t) const = default;
Text& operator+=(const Text& rhs);
Text& operator+=(const std::string& rhs);
friend Text operator+(Text lhs, Text& rhs);
@@ -112,12 +121,13 @@ namespace randomizer {
const Text& getTextObject(const std::string& name, Text::Type type = Text::STANDARD);
const std::string& getTextStr(const std::string& name, Text::Type type = Text::STANDARD, Text::Language language = Text::ENGLISH);
Text addColor(const Text& text, Text::Color color, int count = 1, bool forceAround = false);
Text addColor(const Text& t, Text::Color color, int count = 1);
// Adds newlines in appropriate places to properly break the text string for textboxes
void breakLines(std::string& str, int maxLineWidth);
// Replaces the message codes in the string with the ingame hex equivalents
void applyMessageCodes(std::string&);
Text makeTextListing(std::vector<Text> texts);
}; // namespace Text
+79
View File
@@ -174,6 +174,59 @@ SelectButton& rando_config_button(
return button;
}
SelectButton& rando_config_number_button(
Pane& leftPane, Pane& rightPane, std::string settingKey) {
auto setting = FindSetting(settingKey);
// Helper function to call when we want to update the right pane
auto updateRightPane = [setting, &rightPane] {
rightPane.clear();
auto info = setting->GetInfo();
// Show all options/descriptions
rightPane.add_text(info->GetDescriptions()[0]);
};
// Helper function for changing the setting index based on button presses
auto changeOptionIndex = [setting, updateRightPane](int change) {
auto newIndex = setting->GetCurrentOptionIndex() + change;
if (newIndex < 0) {
newIndex = setting->GetInfo()->GetOptions().size() - 1;
} else if (newIndex >= setting->GetInfo()->GetOptions().size()) {
newIndex = 0;
}
setting->SetCurrentOption(newIndex);
SaveRandomizerConfig();
updateRightPane();
};
auto& button = leftPane.add_select_button(ControlledSelectButton::Props{
.key = settingKey,
.getValue = [setting] { return setting->GetCurrentOption(); }
})
// Cycle through the options forward when the button is pressed
.on_pressed([changeOptionIndex] {
changeOptionIndex(1);
});
// Get button component
auto& comp = leftPane.register_control(button, rightPane, [updateRightPane](Pane&) {
updateRightPane();
});
// Listen for left/right nav commands to cycle between the available options
comp.listen(comp.root(), Rml::EventId::Keydown, [changeOptionIndex](Rml::Event& event) {
auto cmd = map_nav_event(event);
if (cmd == NavCommand::Left) {
changeOptionIndex(-1);
event.StopPropagation();
} else if (cmd == NavCommand::Right) {
changeOptionIndex(1);
event.StopPropagation();
}
});
return button;
}
NumberButton* rando_add_optional_setting(std::string optionValue, std::string optionsKeyPrefix,
Pane& pane) {
std::string fullOptionalKey = fmt::format("{} {}", optionsKeyPrefix, optionValue);
@@ -1012,6 +1065,32 @@ RandomizerWindow::RandomizerWindow(dFile_select_c* fileSelect /*= nullptr*/) : m
rando_config_button(leftPane, rightPane, "Ball and Chain Webs");
});
add_tab("Hints", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
leftPane.add_section("Path Hints");
rando_config_number_button(leftPane, rightPane, "Number of Path Hints");
rando_config_button(leftPane, rightPane, "Path Hints on Midna");
rando_config_button(leftPane, rightPane, "Path Hints on Hint Signs");
leftPane.add_section("Barren Hints");
rando_config_number_button(leftPane, rightPane, "Number of Barren Hints");
rando_config_button(leftPane, rightPane, "Barren Hints on Midna");
rando_config_button(leftPane, rightPane, "Barren Hints on Hint Signs");
leftPane.add_section("Item Hints");
rando_config_number_button(leftPane, rightPane, "Number of Item Hints");
rando_config_button(leftPane, rightPane, "Item Hints on Midna");
rando_config_button(leftPane, rightPane, "Item Hints on Hint Signs");
leftPane.add_section("Location Hints");
rando_config_number_button(leftPane, rightPane, "Number of Location Hints");
rando_config_button(leftPane, rightPane, "Location Hints on Midna");
rando_config_button(leftPane, rightPane, "Location Hints on Hint Signs");
rando_config_button(leftPane, rightPane, "Prioritize Remote Location Hints");
});
add_tab("Starting Inventory", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);