diff --git a/files.cmake b/files.cmake index b182392f68..540fb4e314 100644 --- a/files.cmake +++ b/files.cmake @@ -1521,6 +1521,8 @@ set(DUSK_FILES # Randomizer files src/dusk/randomizer/game/flags.cpp src/dusk/randomizer/game/flags.h + src/dusk/randomizer/game/messages.cpp + src/dusk/randomizer/game/messages.hpp src/dusk/randomizer/game/stages.cpp src/dusk/randomizer/game/stages.h src/dusk/randomizer/game/tools.cpp diff --git a/libs/JSystem/src/JMessage/control.cpp b/libs/JSystem/src/JMessage/control.cpp index acd5387f77..68a5ffd1cd 100644 --- a/libs/JSystem/src/JMessage/control.cpp +++ b/libs/JSystem/src/JMessage/control.cpp @@ -7,6 +7,10 @@ #include "JSystem/JMessage/control.h" +#if TARGET_PC +#include "dusk/randomizer/game/messages.hpp" +#endif + JMessage::TControl::TControl() : pSequenceProcessor_(NULL), pRenderingProcessor_(NULL), @@ -90,6 +94,10 @@ bool JMessage::TControl::setMessageCode_inSequence_(JMessage::TProcessor const* JUT_ASSERT(155, pResourceCache_!=NULL); pMessageText_begin_ = pResourceCache_->getMessageText_messageEntry(pEntry_); +#if TARGET_PC + // Feels kinda hacky to have to hijack this deep into JSystem, but works for now + HandleTextOverrides(this, pProcessor, uMessageGroupID_, uMessageID_); +#endif pMessageText_current_ = pMessageText_begin_; oStack_renderingProcessor_.clear(); return true; diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index f67d6054e6..f90a18951b 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -213,12 +213,18 @@ static bool isOutfontKanjiCode(int iCharacter) { } static u32 getFontCCColorTable(u8 i_colorNo, u8 i_fukiKind) { - static const u32 colorTable[9] = { + static const u32 colorTable[DUSK_IF_ELSE(12, 9)] = { 0xFFFFFFFF, 0xF07878FF, 0xAADC8CFF, 0xA0B4DCFF, 0xDCDC82FF, 0xB4C8E6FF, 0xC8A0DCFF, 0xFFFFFFFF, 0xDCAA78FF, +#if TARGET_PC + // Extra text colors for randomizer + 0x4BBE4BFF, // Dark Green + 0x4B96D7FF, // Blue + 0xBFBFBFFF, // Silver +#endif }; - if (i_colorNo > 8) { + if (i_colorNo > DUSK_IF_ELSE(11, 8)) { return 0xFFFFFFFF; } @@ -247,12 +253,18 @@ static u32 getFontCCColorTable(u8 i_colorNo, u8 i_fukiKind) { } static u32 getFontGCColorTable(u8 i_colorNo, u8 i_fukiKind) { - static const u32 colorTable[9] = { + static const u32 colorTable[DUSK_IF_ELSE(12, 9)] = { 0xFFFFFFFF, 0xF07878FF, 0xAADC8CFF, 0xA0B4DCFF, 0xDCDC82FF, 0xB4C8E6FF, 0xC8A0DCFF, 0xFFFFFFFF, 0xDCAA78FF, +#if TARGET_PC + // Extra text colors for randomizer + 0x4BBE4BFF, // Dark Green + 0x4B96D7FF, // Blue + 0xBFBFBFFF, // Silver +#endif }; - if (i_colorNo > 8) { + if (i_colorNo > DUSK_IF_ELSE(11, 8)) { return 0xFFFFFFFF; } diff --git a/src/d/d_msg_flow.cpp b/src/d/d_msg_flow.cpp index 1b4c8058eb..da0bc4cf8a 100644 --- a/src/d/d_msg_flow.cpp +++ b/src/d/d_msg_flow.cpp @@ -523,7 +523,8 @@ int dMsgFlow_c::setNormalMsg(mesg_flow_node* i_flowNode_p, fopAc_ac_c* i_speaker if (flowItemOverrides.contains(key)) { u8 itemId = verifyProgressiveItem(flowItemOverrides[key]); msg_no = getItemMessageID(itemId); - execItemGet(itemId); + // Store this itemId so that we can give the item when the textbox closes + g_randomizerState.mFlowMessageItemId = itemId; } } #endif diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index 3313534de3..de824dd5b5 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -697,13 +697,14 @@ u32 dMsgObject_c::getMessageIndex(u32 param_0) { u32 dMsgObject_c::getRevoMessageIndex(u32 param_1) { #if TARGET_PC if (randomizer_IsActive()) { - // Special case for Ilia Memory Reward Text + // Special case for Ilia Memory Reward Text (param_1 is msgId) // If we're in the sanctuary cutscene where we get the reward, override the text. - // Otherwise we override the text whenever we get the regular horse call + // Otherwise the regular item text for the horse call would be overriden if we find it if (param_1 == 233 && playerIsInRoomStage(0, "R_SP109") && dComIfGp_getLayerNo() == 9) { u8 itemId = verifyProgressiveItem(randomizer_getItemAtLocation("Ilia Memory Reward")); param_1 = getItemMessageID(itemId); - execItemGet(itemId); + // Store this itemId so that we can give the item when the textbox closes + g_randomizerState.mFlowMessageItemId = itemId; } else { // Else override the text if we have an override u32 key = (dMsgObject_getGroupID() << 16) | param_1; @@ -711,7 +712,8 @@ u32 dMsgObject_c::getRevoMessageIndex(u32 param_1) { if (flowItemOverrides.contains(key)) { u8 itemId = verifyProgressiveItem(flowItemOverrides[key]); param_1 = getItemMessageID(itemId); - execItemGet(itemId); + // Store this itemId so that we can give the item when the textbox closes + g_randomizerState.mFlowMessageItemId = itemId; } } } @@ -814,6 +816,14 @@ void dMsgObject_c::waitProc() { } } } +#if TARGET_PC + // If we have a randomizer item to give because of a flow message override + // then give it if the textbox has been fully closed. + if (randomizer_IsActive() && g_randomizerState.mFlowMessageItemId != 0 && mpScrnDraw == NULL) { + execItemGet(g_randomizerState.mFlowMessageItemId); + g_randomizerState.mFlowMessageItemId = 0; + } +#endif } void dMsgObject_c::openProc() { diff --git a/src/dusk/randomizer/game/messages.cpp b/src/dusk/randomizer/game/messages.cpp new file mode 100644 index 0000000000..db1f84d56e --- /dev/null +++ b/src/dusk/randomizer/game/messages.cpp @@ -0,0 +1,65 @@ +#include "messages.hpp" + +#include "JSystem/JMessage/control.h" +#include "d/d_msg_class.h" +#include "randomizer_context.hpp" + +#include + +// Format certain messages that need to have dynamic info in them +const char* GetFormatedTextOverride(u32 key, const std::string& text) { + // Store formatted message in static buffer so it never goes away. + // This is fine as long as we only ever need to format messages + // for textboxes, but will cause issues if we need to use it for + // other UI elements + static std::array buf; + u32 value{}; + char* outIt; + // For item counts, execItemGet hasn't run yet, so add one to the count + switch (key) { + case (0 << 16) | 325: // Group 0, id 325 + // Poe Soul get item text + value = dComIfGs_getPohSpiritNum() + 1; + outIt = std::vformat_to(buf.data(), text, std::make_format_args(value)); + break; + case (0 << 16) | 335: // Group 0, id 335 + // Sky book characters get item text + value = dComIfGs_getAncientDocumentNum() + 1; + outIt = std::vformat_to(buf.data(), text, std::make_format_args(value)); + break; + default: + // No override, return original text + return text.data(); + } + + // Null-terminate + size_t len = std::distance(buf.data(), outIt); + buf[len] = '\0'; + + // Return overriden text + return buf.data(); +} + +void HandleTextOverrides(JMessage::TControl* control, JMessage::TProcessor const* pProcessor, int groupID, int index) { + if (randomizer_IsActive()) { + // Get the entry for this message + auto entry = static_cast(pProcessor->getMessageEntry_messageCode(groupID, index)); + if (!entry) { + return; + } + + // If the message id is >= 5000 then it's part of the stage file's message group + // Otherwise it's part of group 0 + auto msgId = entry->message_id.host(); + u16 group = 0; + if (msgId >= 5000) { + group = dComIfGp_getStageStagInfo()->mMsgGroup; + } + + u32 key = (group << 16) | msgId; + auto& textOverrides = randomizer_GetContext().mTextOverrides; + if (textOverrides.contains(key)) { + control->pMessageText_begin_ = GetFormatedTextOverride(key, textOverrides[key]); + } + } +} diff --git a/src/dusk/randomizer/game/messages.hpp b/src/dusk/randomizer/game/messages.hpp new file mode 100644 index 0000000000..317f274aad --- /dev/null +++ b/src/dusk/randomizer/game/messages.hpp @@ -0,0 +1,9 @@ +#pragma once + +// Forward declaration +namespace JMessage { +struct TProcessor; +struct TControl; +} + +void HandleTextOverrides(JMessage::TControl* control, JMessage::TProcessor const* pProcessor, int groupID, int index); \ No newline at end of file diff --git a/src/dusk/randomizer/game/randomizer_context.cpp b/src/dusk/randomizer/game/randomizer_context.cpp index 24afdab322..393f4073f9 100644 --- a/src/dusk/randomizer/game/randomizer_context.cpp +++ b/src/dusk/randomizer/game/randomizer_context.cpp @@ -9,6 +9,7 @@ #include "dusk/randomizer/generator/utility/endian.hpp" #include "dusk/randomizer/generator/utility/yaml.hpp" #include "dusk/randomizer/generator/randomizer.hpp" +#include "dusk/randomizer/generator/utility/text.hpp" #include "SDL3/SDL_filesystem.h" #include @@ -93,7 +94,20 @@ std::optional RandomizerContext::WriteToFile() { out["mFlowPatches"] = this->mFlowPatches; + // Dump text overrides as binary to avoid losing intentional null characters + YAML::Emitter textData; + textData << YAML::BeginMap; + textData << YAML::Key << "mTextOverrides"; + textData << YAML::BeginMap; + for (const auto& [key, text] : this->mTextOverrides) { + textData << YAML::Key << key; + textData << YAML::Value << YAML::Binary(reinterpret_cast(text.data()), text.size()); + } + textData << YAML::EndMap; + textData << YAML::EndMap; + seedData << YAML::Dump(out); + seedData << '\n' << textData.c_str(); seedData.close(); return std::nullopt; @@ -227,6 +241,14 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha this->mFlowPatches[key] = value; } + // Text Overrides + for (const auto& textNode: in["mTextOverrides"]) { + auto key = textNode.first.as(); + auto binary = textNode.second.as(); + std::string text(reinterpret_cast(binary.data()), binary.size()); + this->mTextOverrides[key] = std::move(text); + } + DuskLog.debug("Loaded Randomizer Seed {}", this->mHash); return std::nullopt; @@ -1055,6 +1077,20 @@ RandomizerContext WriteSeedData(const std::unique_ptr(); + // TODO: Handle multiple languages + auto language = randomizer::Text::ENGLISH; + auto text = randomizer::getTextStr(name); + u8 group = overrideNode["Group"].as(); + u16 messageId = overrideNode["Message Id"].as(); + u32 key = (group << 16) | messageId; + randomizer::applyMessageCodes(text); + randoData.mTextOverrides[key] = text; + } + return std::move(randoData); } diff --git a/src/dusk/randomizer/game/randomizer_context.hpp b/src/dusk/randomizer/game/randomizer_context.hpp index d82c1f8e9f..ceadf6964d 100644 --- a/src/dusk/randomizer/game/randomizer_context.hpp +++ b/src/dusk/randomizer/game/randomizer_context.hpp @@ -46,6 +46,12 @@ public: std::unordered_map>>> mActorAdditions{}; std::unordered_map mFlowPatches{}; + // struct TextOverride { + // std::array mAttributes{}; + // std::string mText{}; + // }; + std::unordered_map mTextOverrides{}; + std::optional WriteToFile(); std::optional LoadFromHash(const std::string& hash); std::string GetSeedDataPath() const; @@ -133,6 +139,12 @@ public: u8 mTimeChange{}; u8 mEventItemQueue[EVENT_ITEM_QUEUE_SIZE]; bool mRoomReloadingState{false}; + + // Used to store an item id for a flow message override so that we can give the item + // once the textbox is closed instead of when the message appears. This lines up + // more naturally with how the timing of how the game normally gives items and affects + // things like the sound of the rupee counter going up. + u8 mFlowMessageItemId{0}; }; extern RandomizerState g_randomizerState; diff --git a/src/dusk/randomizer/generator/data/items.yaml b/src/dusk/randomizer/generator/data/items.yaml index 5d63eb9832..5b34d4ee79 100644 --- a/src/dusk/randomizer/generator/data/items.yaml +++ b/src/dusk/randomizer/generator/data/items.yaml @@ -106,6 +106,10 @@ Importance: Junk Id: 0x18 +#- Name: Water Bombs 3 +# Importance: Junk +# Id: 0x19 + - Name: Bomblings 5 Importance: Junk Id: 0x1A diff --git a/src/dusk/randomizer/generator/data/text/languages/english.yaml b/src/dusk/randomizer/generator/data/text/languages/english.yaml new file mode 100644 index 0000000000..22928dfc77 --- /dev/null +++ b/src/dusk/randomizer/generator/data/text/languages/english.yaml @@ -0,0 +1,325 @@ +Shadow Crystal Get Item Text: + Standard: + Text: |- + You got the Shadow Crystal! + This is a dark manifestation + of Zant's power that allows + you to transform at will! + +Restored Dominion Rod Text: + Standard: + Text: |- + Power has been restored to + the Dominion Rod! Now it can + be used to imbue statues + with life in the present! + +Forest Temple Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for the + Forest Temple! + +Goron Mines Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for + Goron Mines! + +Lakebed Temple Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for the + Lakebed Temple! + +Arbiters Grounds Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for + Arbiter's Grounds! + +Snowpeak Ruins Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for + Snowpeak Ruins! + +Temple of Time Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for the + Temple of Time! + +City in the Sky Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for the + City in the Sky! + +Palace of Twilight Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for the + Palace of Twilight! + +Hyrule Castle Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for + Hyrule Castle! + +Bulblin Camp Small Key Get Item Text: + Standard: + Text: |- + You got a Small Key for the + Bulblin Camp! + +Forest Temple Big Key Get Item Text: + Standard: + Text: |- + You got the Big Key for the + Forest Temple! + +Lakebed Temple Big Key Get Item Text: + Standard: + Text: |- + You got the Big Key for the + Lakebed Temple! + +Arbiters Grounds Big Key Get Item Text: + Standard: + Text: |- + You got the Big Key for + Arbiter's Grounds! + +Temple of Time Big Key Get Item Text: + Standard: + Text: |- + You got the Big Key for the + Temple of Time! + +City in the Sky Big Key Get Item Text: + Standard: + Text: |- + You got the Big Key for the + City in the Sky! + +Palace of Twilight Big Key Get Item Text: + Standard: + Text: |- + You got the Big Key for the + Palace of Twilight! + +Hyrule Castle Big Key Get Item Text: + Standard: + Text: |- + You got the Big Key for + Hyrule Castle! + +Forest Temple Compass Get Item Text: + Standard: + Text: |- + You got the Compass for the + Forest Temple! + +Goron Mines Compass Get Item Text: + Standard: + Text: |- + You got the Compass for + Goron Mines! + +Lakebed Temple Compass Get Item Text: + Standard: + Text: |- + You got the Compass for the + Lakebed Temple! + +Mirror Shard 2 Get item Text: + Standard: + Text: |- + You got the second shard of + the Mirror of Twilight! It + has a beautiful shine to it + and feels slightly cold... + +Mirror Shard 3 Get item Text: + Standard: + Text: |- + You got the third shard of + the Mirror of Twilight! It + is covered in dirt and + webs... + +Mirror Shard 4 Get item Text: + Standard: + Text: |- + You got the final shard of + the Mirror of Twilight! It + feels lighter than air... + +Arbiters Grounds Compass Get Item Text: + Standard: + Text: |- + You got the Compass for + Arbiter's Grounds! + +Snowpeak Ruins Compass Get Item Text: + Standard: + Text: |- + You got the Compass for + Snowpeak Ruins! + +Temple of Time Compass Get Item Text: + Standard: + Text: |- + You got the Compass for the + Temple of Time! + +City in the Sky Compass Get Item Text: + Standard: + Text: |- + You got the Compass for the + City in the Sky! + +Palace of Twilight Compass Get Item Text: + Standard: + Text: |- + You got the Compass for the + Palace of Twilight! + +Hyrule Castle Compass Get Item Text: + Standard: + Text: |- + You got the Compass for + Hyrule Castle! + +# +Forest Temple Dungeon Map Get Item Text: + Standard: + Text: |- + You got the Dungeon Map for the + Forest Temple! + +Goron Mines Dungeon Map Get Item Text: + Standard: + Text: |- + You got the Dungeon Map for + Goron Mines! + +Lakebed Temple Dungeon Map Get Item Text: + Standard: + Text: |- + You got the Dungeon Map for the + Lakebed Temple! + +Snowpeak Ruins Dungeon Map Get Item Text: + Standard: + Text: |- + You got the Dungeon Map for + Snowpeak Ruins! + +Arbiters Grounds Dungeon Map Get Item Text: + Standard: + Text: |- + You got the Dungeon Map for + Arbiter's Grounds! + +Temple of Time Dungeon Map Get Item Text: + Standard: + Text: |- + You got the Dungeon Map for the + Temple of Time! + +City in the Sky Dungeon Map Get Item Text: + Standard: + Text: |- + You got the Dungeon Map for the + City in the Sky! + +Palace of Twilight Dungeon Map Get Item Text: + Standard: + Text: |- + You got the Dungeon Map for the + Palace of Twilight! + +Hyrule Castle Dungeon Map Get Item Text: + Standard: + Text: |- + You got the Dungeon Map for + Hyrule Castle! + + +Fused Shadow 1 Get Item Text: + Standard: + Text: |- + You got a Fused Shadow! + It seems to have some moss + growing on it... + +Fused Shadow 2 Get Item Text: + Standard: + Text: |- + You got the second Fused + Shadow! It feels warm to + the touch... + +Fused Shadow 3 Get Item Text: + Standard: + Text: |- + You got the final Fused + Shadow! It feels wet and + smells like fish... + +Mirror Shard 1 Get Item Text: + Standard: + Text: |- + You got the first shard of + the Mirror of Twilight! It + is covered in sand... + +Poe Soul Get Item Text: + Standard: + Text: |- + You got a Poe's Soul! + You've collected {} so far. + +Ending Blow Get Item Text: + Standard: + Text: |- + You learned the Ending Blow! + +Shield Attack Get Item Text: + Standard: + Text: |- + You learned the Shield Attack! + +Back Slice Get Item Text: + Standard: + Text: |- + You learned the Back Slice! + +Helm Splitter Get Item Text: + Standard: + Text: |- + You learned the Helm Splitter! + +Mortal Draw Get Item Text: + Standard: + Text: |- + You learned the Mortal Draw! + +Jump Strike Get Item Text: + Standard: + Text: |- + You learned the Jump Strike! + +Great Spin Get Item Text: + Standard: + Text: |- + You learned the Great Spin! + +Partially Filled Sky Book Get Item Text: + Standard: + Text: |- + You got a Sky Character! + You've collected {} so far. diff --git a/src/dusk/randomizer/generator/data/text/text_overrides.yaml b/src/dusk/randomizer/generator/data/text/text_overrides.yaml new file mode 100644 index 0000000000..f3151fc6bd --- /dev/null +++ b/src/dusk/randomizer/generator/data/text/text_overrides.yaml @@ -0,0 +1,223 @@ +#- Name: Foolish Get Item Text +# Group: 0 +# Message Id: 120 +# +#- Name: Ordon Spring Portal Get Item Text +# Group: 0 +# Message Id: 121 +# +#- Name: South Faron Portal Get Item Text +# Group: 0 +# Message Id: 122 + +- Name: Shadow Crystal Get Item Text + Group: 0 + Message Id: 151 + +- Name: Restored Dominion Rod Text + Group: 0 + Message Id: 177 + +- Name: Forest Temple Small Key Get Item Text + Group: 0 + Message Id: 234 + +- Name: Goron Mines Small Key Get Item Text + Group: 0 + Message Id: 235 + +- Name: Lakebed Temple Small Key Get Item Text + Group: 0 + Message Id: 236 + +- Name: Arbiters Grounds Small Key Get Item Text + Group: 0 + Message Id: 237 + +- Name: Snowpeak Ruins Small Key Get Item Text + Group: 0 + Message Id: 238 + +- Name: Temple of Time Small Key Get Item Text + Group: 0 + Message Id: 239 + +- Name: City in the Sky Small Key Get Item Text + Group: 0 + Message Id: 240 + +- Name: Palace of Twilight Small Key Get Item Text + Group: 0 + Message Id: 241 + +- Name: Hyrule Castle Small Key Get Item Text + Group: 0 + Message Id: 242 + +- Name: Bulblin Camp Small Key Get Item Text + Group: 0 + Message Id: 243 + +- Name: Forest Temple Big Key Get Item Text + Group: 0 + Message Id: 247 + +- Name: Lakebed Temple Big Key Get Item Text + Group: 0 + Message Id: 248 + +- Name: Arbiters Grounds Big Key Get Item Text + Group: 0 + Message Id: 249 + +- Name: Temple of Time Big Key Get Item Text + Group: 0 + Message Id: 250 + +- Name: City in the Sky Big Key Get Item Text + Group: 0 + Message Id: 251 + +- Name: Palace of Twilight Big Key Get Item Text + Group: 0 + Message Id: 252 + +- Name: Hyrule Castle Big Key Get Item Text + Group: 0 + Message Id: 253 + +- Name: Forest Temple Compass Get Item Text + Group: 0 + Message Id: 254 + +- Name: Goron Mines Compass Get Item Text + Group: 0 + Message Id: 255 + +- Name: Lakebed Temple Compass Get Item Text + Group: 0 + Message Id: 256 + +- Name: Mirror Shard 2 Get item Text + Group: 0 + Message Id: 266 + +- Name: Mirror Shard 3 Get item Text + Group: 0 + Message Id: 267 + +- Name: Mirror Shard 4 Get item Text + Group: 0 + Message Id: 268 + +- Name: Arbiters Grounds Compass Get Item Text + Group: 0 + Message Id: 269 + +- Name: Snowpeak Ruins Compass Get Item Text + Group: 0 + Message Id: 270 + +- Name: Temple of Time Compass Get Item Text + Group: 0 + Message Id: 271 + +- Name: City in the Sky Compass Get Item Text + Group: 0 + Message Id: 272 + +- Name: Palace of Twilight Compass Get Item Text + Group: 0 + Message Id: 273 + +- Name: Hyrule Castle Compass Get Item Text + Group: 0 + Message Id: 274 + +- Name: Forest Temple Dungeon Map Get Item Text + Group: 0 + Message Id: 283 + +- Name: Goron Mines Dungeon Map Get Item Text + Group: 0 + Message Id: 284 + +- Name: Lakebed Temple Dungeon Map Get Item Text + Group: 0 + Message Id: 285 + +- Name: Arbiters Grounds Dungeon Map Get Item Text + Group: 0 + Message Id: 286 + +- Name: Snowpeak Ruins Dungeon Map Get Item Text + Group: 0 + Message Id: 287 + +- Name: Temple of Time Dungeon Map Get Item Text + Group: 0 + Message Id: 288 + +- Name: City in the Sky Dungeon Map Get Item Text + Group: 0 + Message Id: 289 + +- Name: Palace of Twilight Dungeon Map Get Item Text + Group: 0 + Message Id: 290 + +- Name: Hyrule Castle Dungeon Map Get Item Text + Group: 0 + Message Id: 291 + +- Name: Fused Shadow 1 Get Item Text + Group: 0 + Message Id: 317 + +- Name: Fused Shadow 2 Get Item Text + Group: 0 + Message Id: 318 + +- Name: Fused Shadow 3 Get Item Text + Group: 0 + Message Id: 319 + +- Name: Mirror Shard 1 Get Item Text + Group: 0 + Message Id: 320 + +- Name: Poe Soul Get Item Text + Group: 0 + Message Id: 325 + +- Name: Ending Blow Get Item Text + Group: 0 + Message Id: 326 + +- Name: Shield Attack Get Item Text + Group: 0 + Message Id: 327 + +- Name: Back Slice Get Item Text + Group: 0 + Message Id: 328 + +- Name: Helm Splitter Get Item Text + Group: 0 + Message Id: 329 + +- Name: Mortal Draw Get Item Text + Group: 0 + Message Id: 330 + +- Name: Jump Strike Get Item Text + Group: 0 + Message Id: 331 + +- Name: Great Spin Get Item Text + Group: 0 + Message Id: 332 + +- Name: Partially Filled Sky Book Get Item Text + Group: 0 + Message Id: 335 diff --git a/src/dusk/randomizer/generator/utility/text.cpp b/src/dusk/randomizer/generator/utility/text.cpp index 256e2d592f..959c739846 100644 --- a/src/dusk/randomizer/generator/utility/text.cpp +++ b/src/dusk/randomizer/generator/utility/text.cpp @@ -1,178 +1,291 @@ -// #include "../utility/text.hpp" -// #include "../utility/string.hpp" -// #include +#include +#include +#include -// namespace Text { +#include "yaml.hpp" -// std::array supported_languages = {"English", "Spanish", "French"}; +namespace randomizer { -// static std::unordered_map nameToColor = { -// {Text::Color::NONE, TEXT_COLOR_DEFAULT}, -// {Text::Color::RED, TEXT_COLOR_RED}, -// {Text::Color::GREEN, TEXT_COLOR_GREEN}, -// {Text::Color::BLUE, TEXT_COLOR_BLUE}, -// {Text::Color::YELLOW, TEXT_COLOR_YELLOW}, -// {Text::Color::CYAN, TEXT_COLOR_CYAN}, -// {Text::Color::MAGENTA, TEXT_COLOR_MAGENTA}, -// {Text::Color::GRAY, TEXT_COLOR_GRAY}, -// {Text::Color::ORANGE, TEXT_COLOR_ORANGE}, -// }; + // std::array supported_languages = {"English", "Spanish", "French"}; + // + // static std::unordered_map nameToColor = { + // {Text::Color::NONE, TEXT_COLOR_DEFAULT}, + // {Text::Color::RED, TEXT_COLOR_RED}, + // {Text::Color::GREEN, TEXT_COLOR_GREEN}, + // {Text::Color::BLUE, TEXT_COLOR_BLUE}, + // {Text::Color::YELLOW, TEXT_COLOR_YELLOW}, + // {Text::Color::CYAN, TEXT_COLOR_CYAN}, + // {Text::Color::MAGENTA, TEXT_COLOR_MAGENTA}, + // {Text::Color::GRAY, TEXT_COLOR_GRAY}, + // {Text::Color::ORANGE, TEXT_COLOR_ORANGE}, + // }; + // + // std::u16string apply_name_color(std::u16string str, const Color& color) + // { + // // Return the raw text (bars included) + // if (color == Color::RAW) + // { + // return str; + // } + // // If there are no '|'s then just return with the color surrounding the whole string + // if (str.find('|') == std::string::npos) + // { + // auto textColor = nameToColor[color]; + // return textColor + str + TEXT_COLOR_DEFAULT; + // } + // + // // Alternate between the text color and default incase there are multiple + // // pairs of bars + // auto textColor = nameToColor[color]; + // bool insertColor = false; + // for (size_t pos = 0; pos < str.length(); pos++) + // { + // if (str[pos] == '|') + // { + // insertColor = !insertColor; + // str.erase(pos, 1); + // str.insert(pos, insertColor ? textColor : TEXT_COLOR_DEFAULT); + // } + // } + // + // return str; + // } + // + // std::u16string word_wrap_string(const std::u16string& string, const size_t& max_line_len) { + // size_t index_in_str = 0; + // std::u16string wordwrapped_str; + // std::u16string current_word; + // size_t curr_word_len = 0; + // size_t len_curr_line = 0; + // + // while (index_in_str < string.length()) { //length is weird because its utf-16 + // char16_t character = string[index_in_str]; + // + // if (character == u'\x0E') { //need to parse the commands, only implementing a few necessary ones for now (will break with other commands) + // std::u16string substr; + // size_t code_len = 0; + // if (string[index_in_str + 1] == u'\x00') { + // if (string[index_in_str + 2] == u'\x03') { //color command + // if (string[index_in_str + 4] == u'\xFFFF') { //text color white, weird length + // code_len = 10; + // } + // else { + // code_len = 5; + // } + // } + // } + // else if (string[index_in_str + 1] == u'\x01') { //all implemented commands in this group have length 4 + // code_len = 4; + // } + // else if (string[index_in_str + 1] == u'\x02') { //all implemented commands in this group have length 4 + // code_len = 4; + // } + // else if (string[index_in_str + 1] == u'\x03') { //all implemented commands in this group have length 4 + // code_len = 4; + // } + // else if (string[index_in_str + 1] == u'\x04') { //all implemented commands in this group have length 4. Only used for Ho Ho sound + // code_len = 4; + // } + // + // substr = string.substr(index_in_str, code_len); + // current_word += substr; + // index_in_str += code_len; + // } + // else if (character == u'\n') { + // wordwrapped_str += current_word; + // wordwrapped_str += character; + // len_curr_line = 0; + // current_word = u""; + // curr_word_len = 0; + // index_in_str += 1; + // } + // else if (character == u' ') { + // wordwrapped_str += current_word; + // wordwrapped_str += character; + // len_curr_line += curr_word_len + 1; + // current_word = u""; + // curr_word_len = 0; + // index_in_str += 1; + // } + // else { + // current_word += character; + // curr_word_len += 1; + // index_in_str += 1; + // + // if (len_curr_line + curr_word_len > max_line_len) { + // wordwrapped_str += u'\n'; + // len_curr_line = 0; + // + // if (curr_word_len > max_line_len) { + // wordwrapped_str += current_word + u'\n'; + // current_word = u""; + // } + // } + // } + // } + // wordwrapped_str += current_word; + // + // return wordwrapped_str; + // } + // + // std::string pad_str_4_lines(const std::string& string) + // { + // std::vector lines = randomizer::utility::str::Split(string, '\n'); + // + // unsigned int padding_lines_needed = (4 - lines.size() % 4) % 4; + // for (unsigned int i = 0; i < padding_lines_needed; i++) + // { + // lines.push_back(""); + // } + // + // return randomizer::utility::str::Merge(lines, '\n'); + // } + // + // std::u16string pad_str_4_lines(const std::u16string& string) + // { + // std::vector lines = randomizer::utility::str::Split(string, u'\n'); + // + // unsigned int padding_lines_needed = (4 - lines.size() % 4) % 4; + // for (unsigned int i = 0; i < padding_lines_needed; i++) + // { + // lines.push_back(u""); + // } + // + // return randomizer::utility::str::erge(lines, u'\n'); + // } -// std::u16string apply_name_color(std::u16string str, const Color& color) -// { -// // Return the raw text (bars included) -// if (color == Color::RAW) -// { -// return str; -// } -// // If there are no '|'s then just return with the color surrounding the whole string -// if (str.find('|') == std::string::npos) -// { -// auto textColor = nameToColor[color]; -// return textColor + str + TEXT_COLOR_DEFAULT; -// } -// // Alternate between the text color and default incase there are multiple -// // pairs of bars -// auto textColor = nameToColor[color]; -// bool insertColor = false; -// for (size_t pos = 0; pos < str.length(); pos++) -// { -// if (str[pos] == '|') -// { -// insertColor = !insertColor; -// str.erase(pos, 1); -// str.insert(pos, insertColor ? textColor : TEXT_COLOR_DEFAULT); -// } -// } + Text::Type string_to_type(const std::string& str) { + std::unordered_map strToType = { + {"Standard", Text::Type::STANDARD}, + {"Pretty", Text::Type::PRETTY}, + {"Cryptic", Text::Type::CRYPTIC}, + }; -// return str; -// } + if (strToType.contains(str)) + { + return strToType.at(str); + } -// std::u16string word_wrap_string(const std::u16string& string, const size_t& max_line_len) { -// size_t index_in_str = 0; -// std::u16string wordwrapped_str; -// std::u16string current_word; -// size_t curr_word_len = 0; -// size_t len_curr_line = 0; + throw std::runtime_error("Text type \"" + str + "\" is not recognized."); + } -// while (index_in_str < string.length()) { //length is weird because its utf-16 -// char16_t character = string[index_in_str]; + Text::Language string_to_language(const std::string& str) { + std::unordered_map strToLanguage = { + {"english", Text::Language::ENGLISH}, + }; -// if (character == u'\x0E') { //need to parse the commands, only implementing a few necessary ones for now (will break with other commands) -// std::u16string substr; -// size_t code_len = 0; -// if (string[index_in_str + 1] == u'\x00') { -// if (string[index_in_str + 2] == u'\x03') { //color command -// if (string[index_in_str + 4] == u'\xFFFF') { //text color white, weird length -// code_len = 10; -// } -// else { -// code_len = 5; -// } -// } -// } -// else if (string[index_in_str + 1] == u'\x01') { //all implemented commands in this group have length 4 -// code_len = 4; -// } -// else if (string[index_in_str + 1] == u'\x02') { //all implemented commands in this group have length 4 -// code_len = 4; -// } -// else if (string[index_in_str + 1] == u'\x03') { //all implemented commands in this group have length 4 -// code_len = 4; -// } -// else if (string[index_in_str + 1] == u'\x04') { //all implemented commands in this group have length 4. Only used for Ho Ho sound -// code_len = 4; -// } + if (strToLanguage.contains(str)) + { + return strToLanguage.at(str); + } -// substr = string.substr(index_in_str, code_len); -// current_word += substr; -// index_in_str += code_len; -// } -// else if (character == u'\n') { -// wordwrapped_str += current_word; -// wordwrapped_str += character; -// len_curr_line = 0; -// current_word = u""; -// curr_word_len = 0; -// index_in_str += 1; -// } -// else if (character == u' ') { -// wordwrapped_str += current_word; -// wordwrapped_str += character; -// len_curr_line += curr_word_len + 1; -// current_word = u""; -// curr_word_len = 0; -// index_in_str += 1; -// } -// else { -// current_word += character; -// curr_word_len += 1; -// index_in_str += 1; + throw std::runtime_error("Language \"" + str + "\" is not recognized."); + } -// if (len_curr_line + curr_word_len > max_line_len) { -// wordwrapped_str += u'\n'; -// len_curr_line = 0; + Text::Gender string_to_gender(const std::string& str) + { + std::unordered_map strToGender = { + {"Masculine", Text::Gender::MASCULINE}, + {"Feminine", Text::Gender::FEMININE} + }; -// if (curr_word_len > max_line_len) { -// wordwrapped_str += current_word + u'\n'; -// current_word = u""; -// } -// } -// } -// } -// wordwrapped_str += current_word; + if (strToGender.contains(str)) + { + return strToGender.at(str); + } -// return wordwrapped_str; -// } + return Text::Gender::NUETRAL; + } -// std::string pad_str_4_lines(const std::string& string) -// { -// std::vector lines = randomizer::utility::str::Split(string, '\n'); + Text::Plurality string_to_plurality(const std::string& str) + { + if (str == "Plural") return Text::Plurality::PLURAL; + return Text::Plurality::SINGULAR; + } -// unsigned int padding_lines_needed = (4 - lines.size() % 4) % 4; -// for (unsigned int i = 0; i < padding_lines_needed; i++) -// { -// lines.push_back(""); -// } + static void LoadTextData(TextDatabase& tb) { + std::string dataPath = RANDO_DATA_PATH "text/languages"; + for (const auto& entry : std::filesystem::directory_iterator(dataPath)) { + if (entry.is_regular_file() && entry.path().extension() == ".yaml") { + auto language = string_to_language(entry.path().stem().string()); + auto textData = LoadYAML(entry.path()); + for (const auto& textNode : textData) { + const auto& name = textNode.first.as(); + for (const auto& typeNode : textNode.second) { + auto type = string_to_type(typeNode.first.as()); + auto typeData = typeNode.second; + const auto& text = typeData["Text"].as(); + tb[name][type].mtext[language] = text; + if (typeData["Gender"]) { + tb[name][type].mGender[language] = string_to_gender(typeData["Gender"].as()); + } + if (typeData["Plurality"]) { + tb[name][type].mPlurality[language] = string_to_plurality(typeData["Plurality"].as()); + } + } + } + } + } + } -// return randomizer::utility::str::Merge(lines, '\n'); -// } + const TextDatabase& getTextDatabase() { + static TextDatabase tb{}; -// std::u16string pad_str_4_lines(const std::u16string& string) -// { -// std::vector lines = randomizer::utility::str::Split(string, u'\n'); + // If database is empty, load it up + if (tb.empty()) { + LoadTextData(tb); + } -// unsigned int padding_lines_needed = (4 - lines.size() % 4) % 4; -// for (unsigned int i = 0; i < padding_lines_needed; i++) -// { -// lines.push_back(u""); -// } + return tb; + } -// return randomizer::utility::str::erge(lines, u'\n'); -// } + const Text& getTextObject(const std::string& name, Text::Type type /*= Text::STANDARD*/) + { + const auto& tb = getTextDatabase(); + if (!tb.contains(name)) { + throw std::runtime_error("Text name \"" + name + "\" is not recognized."); + } + return tb.at(name).at(type); + } -// Gender string_to_gender(const std::string& str) -// { -// std::unordered_map strToGender = { -// {"Male", Gender::MALE}, -// {"Female", Gender::FEMALE} -// }; + const std::string& getTextStr(const std::string& name, + Text::Type type /*= Text::STANDARD*/, + Text::Language language /*= Text::ENGLISH*/) + { + const auto& tb = getTextDatabase(); + if (!tb.contains(name)) { + throw std::runtime_error("Text name \"" + name + "\" is not recognized."); + } + return tb.at(name).at(type).mtext.at(language); + } -// if (strToGender.contains(str)) -// { -// return strToGender.at(str); -// } + void applyMessageCodes(std::string& str) { + using namespace std::string_literals; + const static std::unordered_map messageCodes = { + {"", "\x1A\x05\x00\x00\x01"s }, + {"", "\x1A\x05\x00\x00\x02"s }, + {"", "\x1A\x06\xFF\x00\x00\x00"s}, + {"", "\x1A\x06\xFF\x00\x00\x01"s}, + {"", "\x1A\x06\xFF\x00\x00\x02"s}, + {"", "\x1A\x06\xFF\x00\x00\x03"s}, + {"", "\x1A\x06\xFF\x00\x00\x04"s}, + {"", "\x1A\x06\xFF\x00\x00\x06"s}, + {"", "\x1A\x06\xFF\x00\x00\x08"s}, + // custom colors + {"", "\x1A\x06\xFF\x00\x00\x09"s}, + {"", "\x1A\x06\xFF\x00\x00\x0A"s}, + {"", "\x1A\x06\xFF\x00\x00\x0B"s}, + }; -// return Gender::NONE; -// } - -// Plurality string_to_plurality(const std::string& str) -// { -// if (str == "Plural") return Plurality::PLURAL; -// return Plurality::SINGULAR; -// } -// }; // namespace Text + for (const auto& [code, replacement] : messageCodes) { + size_t pos = 0; + while ((pos = str.find(code, pos)) != std::string::npos) { + str.replace(pos, code.length(), replacement); + pos += replacement.length(); + } + } + } +}; // namespace Text diff --git a/src/dusk/randomizer/generator/utility/text.hpp b/src/dusk/randomizer/generator/utility/text.hpp index 3efaa78e51..b987963ca7 100644 --- a/src/dusk/randomizer/generator/utility/text.hpp +++ b/src/dusk/randomizer/generator/utility/text.hpp @@ -2,58 +2,77 @@ #include #include -#include +#include -namespace Text - { - enum struct Type - { - STANDARD = 0, - PRETTY, - CRYPTIC, +namespace randomizer { + class Text { + public: + enum Language { + ENGLISH = 0, + LANGUAGE_MAX + }; + + enum Type + { + STANDARD = 0, + PRETTY, + CRYPTIC, + TYPE_MAX + }; + + enum Color + { + RAW = 0, + NONE, + RED, + GREEN, + BLUE, + YELLOW, + CYAN, + MAGENTA, + GRAY, + ORANGE, + }; + + enum Gender + { + NUETRAL = 0, + MASCULINE, + FEMININE, + GENDER_MAX, + }; + + enum Plurality + { + SINGULAR = 0, + PLURAL, + PLURALITY_MAX, + }; + + std::array mtext{}; + std::array mGender{}; + std::array mPlurality{}; }; - enum struct Color - { - RAW = 0, - NONE, - RED, - GREEN, - BLUE, - YELLOW, - CYAN, - MAGENTA, - GRAY, - ORANGE, + inline constexpr std::array supported_languages = { + Text::ENGLISH, }; - enum struct Gender - { - NONE = 0, - MALE, - FEMALE, - }; + // std::u16string apply_name_color(std::u16string str, const Color& color); + // std::u16string word_wrap_string(const std::u16string& string, const size_t& max_line_len); //IMPROVEMENT: use font data to do this "properly" + // std::string pad_str_4_lines(const std::string& string); + // std::u16string pad_str_4_lines(const std::u16string& string); - enum struct Plurality - { - SINGULAR, - PLURAL, - }; + Text::Gender string_to_gender(const std::string& str); + Text::Plurality string_to_plurality(const std::string& str); - struct Translation - { - std::map types; - Gender gender; - Plurality plurality; - }; + // Retrieval of Text objects keyed by name and type (standard, pretty, criptic) + using TextDatabase = std::unordered_map>; - extern std::array supported_languages; + const TextDatabase& getTextDatabase(); - std::u16string apply_name_color(std::u16string str, const Color& color); - std::u16string word_wrap_string(const std::u16string& string, const size_t& max_line_len); //IMPROVEMENT: use font data to do this "properly" - std::string pad_str_4_lines(const std::string& string); - std::u16string pad_str_4_lines(const std::u16string& string); + const std::string& getTextStr(const std::string& name, Text::Type type = Text::STANDARD, Text::Language language = Text::ENGLISH); - Gender string_to_gender(const std::string& str); - Plurality string_to_plurality(const std::string& str); + // Replaces the message codes in the string with the ingame hex equivalents + void applyMessageCodes(std::string&); }; // namespace Text