#include "text.hpp" #include "yaml.hpp" #include #include namespace randomizer { Text::Text(const std::string& str) { for (auto& text : mText) { text = str; } } void Text::Replace(const std::string& oldStr, const Text& 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.mText[i]); } } } } 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::Capitalize() { try { // Determine the platform-specific locale string #if defined(_WIN32) || defined(_WIN64) const char* localeName = "English_United States.1252"; #else const char* localeName = "en_US.iso88591"; #endif static const std::locale latin1Locale(localeName); for (auto& text : mText) { if (!text.empty()) { text[0] = std::toupper(text[0], latin1Locale); } } } catch (const std::runtime_error&) { // Fallback incase the system completely lacks the requested locale definition for (auto& text : mText) { if (!text.empty()) { text[0] = static_cast(std::toupper(static_cast(text[0]))); } } } } void Text::BreakLines(int maxLineWidth /*= MAX_LINE_WIDTH*/) { for (auto& text : mText) { breakLines(text, maxLineWidth); } } bool Text::Empty() const { for (auto& text : mText) { if (!text.empty()) { return false; } } return true; } Text& Text::operator+=(const Text& rhs) { for (size_t i = 0; i < mText.size(); ++i) { mText[i] += rhs.mText[i]; } return *this; } Text& Text::operator+=(const std::string& rhs) { for (auto& text : mText) { text += rhs; } return *this; } Text operator+(Text lhs, const Text& rhs) { lhs += rhs; return lhs; } Text operator+(Text lhs, const std::string& rhs) { for (auto& text : lhs.mText) { text += rhs; } return lhs; } Text operator+(const std::string& lhs, const Text& rhs) { return Text(lhs) + rhs; } Text::Type stringToType(const std::string& str) { std::unordered_map strToType = { {"Standard", Text::Type::STANDARD}, {"Pretty", Text::Type::PRETTY}, {"Cryptic", Text::Type::CRYPTIC}, }; if (strToType.contains(str)) { return strToType.at(str); } throw std::runtime_error("Text type \"" + str + "\" is not recognized."); } Text::Language stringToLanguage(const std::string& str) { std::unordered_map strToLanguage = { {"english", Text::ENGLISH}, {"spanish", Text::SPANISH}, {"french", Text::FRENCH}, {"german", Text::GERMAN}, {"italian", Text::ITALIAN}, {"japanese", Text::JAPANESE} }; if (strToLanguage.contains(str)) { return strToLanguage.at(str); } throw std::runtime_error("Language \"" + str + "\" is not recognized."); } std::string languageToString(Text::Language language) { switch (language) { case Text::ENGLISH: return "english"; case Text::SPANISH: return "spanish"; case Text::FRENCH: return "french"; case Text::GERMAN: return "german"; case Text::ITALIAN: return "italian"; case Text::JAPANESE: return "japanese"; default: return "unknown language enum"; } } Text::Gender stringToGender(const std::string& str) { std::unordered_map strToGender = { {"Masculine", Text::Gender::MASCULINE}, {"Feminine", Text::Gender::FEMININE} }; if (strToGender.contains(str)) { return strToGender.at(str); } return Text::Gender::NEUTRAL; } Text::Plurality stringToPlurality(const std::string& str) { if (str == "Plural") return Text::Plurality::PLURAL; return Text::Plurality::SINGULAR; } std::string UTF8ToLatin1(const std::string& utf8Str) { std::string latin1Str; // The output string will be equal to or shorter than the UTF-8 string latin1Str.reserve(utf8Str.length()); size_t read_pos = 0; size_t len = utf8Str.length(); while (read_pos < len) { unsigned char c = utf8Str[read_pos]; if (c < 0x80) { // Standard ASCII (0x00 - 0x7F) latin1Str.push_back(c); ++read_pos; } else if ((c & 0xE0) == 0xC0 && (read_pos + 1 < len)) { // Two-byte UTF-8 sequence (0xC0 - 0xDF) unsigned char next_byte = utf8Str[read_pos + 1]; // Reconstruct the Latin-1 character value unsigned char latin1_char = ((c & 0x1F) << 6) | (next_byte & 0x3F); latin1Str.push_back(latin1_char); read_pos += 2; } else { // Multi-byte sequences out of Latin-1 range (or malformed bytes) throw std::runtime_error(fmt::format("Invalid bytes when converting to Latin1 with \"{}\"", utf8Str)); } } return latin1Str; } static void LoadTextData(TextDatabase& tb) { struct LanguageEntry { std::string language; std::string languageData; }; auto files = std::to_array({ {"english", GET_EMBED_DATA(RANDO_DATA_PATH "text/languages/english.yaml")}, {"spanish", GET_EMBED_DATA(RANDO_DATA_PATH "text/languages/spanish.yaml")}, {"french", GET_EMBED_DATA(RANDO_DATA_PATH "text/languages/french.yaml")}, {"german", GET_EMBED_DATA(RANDO_DATA_PATH "text/languages/german.yaml")}, {"italian", GET_EMBED_DATA(RANDO_DATA_PATH "text/languages/italian.yaml")}, }); for (const auto& file : files) { auto language = stringToLanguage(file.language); auto textData = LOAD_EMBED_DATA(file.languageData); for (const auto& textNode : textData) { const auto& name = textNode.first.as(); for (const auto& typeNode : textNode.second) { auto type = stringToType(typeNode.first.as()); auto typeData = typeNode.second; const auto& text = typeData["Text"].as(); if (language != Text::JAPANESE) { tb[name][type].mText[language] = UTF8ToLatin1(text); } else { // Probably have to handle Japanese another way at some point tb[name][type].mText[language] = text; } if (typeData["Gender"]) { tb[name][type].mGender[language] = stringToGender(typeData["Gender"].as()); } if (typeData["Plurality"]) { tb[name][type].mPlurality[language] = stringToPlurality(typeData["Plurality"].as()); } } } } } const TextDatabase& getTextDatabase() { static TextDatabase tb{}; // If database is empty, load it up if (tb.empty()) { LoadTextData(tb); } return tb; } bool textObjectExists(const std::string& name) { return getTextDatabase().contains(name); } 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); } 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."); } if (!tb.at(name).at(type).mText.at(language).empty()) { return tb.at(name).at(type).mText.at(language); } // Return english if the other language's string is empty return tb.at(name).at(type).mText.at(language); } Text addColor(const Text& t, Text::Color color, int count /* = 1*/, bool forceAround /* = false*/) { const static std::unordered_map colorStrings = { {Text::WHITE, ""}, {Text::RED, ""}, {Text::GREEN, ""}, {Text::LIGHT_BLUE, ""}, {Text::YELLOW, ""}, {Text::PURPLE, ""}, {Text::ORANGE, ""}, {Text::DARK_GREEN, ""}, {Text::BLUE, ""}, {Text::SILVER, ""}, }; if (color == Text::Color::RAW) { return t; } if (!colorStrings.contains(color)) { throw std::runtime_error("Color enum value \"" + std::to_string(color) + "\" is not recognized."); } Text text = t; if (forceAround) { text = colorStrings.at(color) + text + colorStrings.at(Text::WHITE); } text.Replace("{", colorStrings.at(color), count); text.Replace("}", colorStrings.at(Text::WHITE), count); return text; } using namespace std::string_view_literals; static const std::unordered_map messageCodes = { {"", "\x1A\x05\x00\x00\x01"sv}, {"", "\x1A\x05\x00\x00\x02"sv}, {"", "\x1A\x05\x00\x00\x20"sv}, {"", "\x1A\x05\x06\x00\x02"sv}, {"", "\x1A\x05\x06\x00\x03"sv}, {"", "\x1A\x06\x00\x00\x09\x01"sv}, {"", "\x1A\x06\x00\x00\x09\x02"sv}, {"", "\x1A\x06\x00\x00\x09\x03"sv}, {"", "\x1A\x06\xFF\x00\x00\x00"sv}, {"", "\x1A\x06\xFF\x00\x00\x01"sv}, {"", "\x1A\x06\xFF\x00\x00\x02"sv}, {"", "\x1A\x06\xFF\x00\x00\x03"sv}, {"", "\x1A\x06\xFF\x00\x00\x04"sv}, {"", "\x1A\x06\xFF\x00\x00\x06"sv}, {"", "\x1A\x06\xFF\x00\x00\x08"sv}, // custom colors {"", "\x1A\x06\xFF\x00\x00\x09"sv}, {"", "\x1A\x06\xFF\x00\x00\x0A"sv}, {"", "\x1A\x06\xFF\x00\x00\x0B"sv}, }; void breakLines(std::string& str, int maxLineWidth) { // Randomizer Only shouldn't rely on needing access to the iso #ifndef RANDOMIZER_ONLY // Get game's font auto gameFont = mDoExt_getMesgFont(); #endif int curLineWidth = 0; size_t i = 0; size_t previousSpace = 0; while (i < str.length()) { // Skip over control codes since they don't get displayed std::string code{}; for (const auto& [messageCode, replacement] : messageCodes) { if (str.substr(i, messageCode.length()) == messageCode) { code = messageCode; break; } } if (!code.empty()) { // Assume worst case for player name width. // 8 chars max * max char width if (code == "") { curLineWidth += 8 * 21; } i += code.length(); continue; } // Keep track of the previous space to replace with // a line break when we reach the maximum width if (str[i] == ' ') { previousSpace = i; } // If we encounter an already inserted newline, reset the counter else if (str[i] == '\n') { curLineWidth = 0; ++i; continue; } JUTFont::TWidth width{}; #ifndef RANDOMIZER_ONLY gameFont->getWidthEntry(str[i], &width); #else // Assume worst case with no iso access width.field_0x1 = 21; #endif curLineWidth += /*width.field_0x0 + */width.field_0x1; // If we exceed the maximum line width, replace the // previous space with a newline and start counting // from the newline again if (curLineWidth > maxLineWidth) { str[previousSpace] = '\n'; i = previousSpace; curLineWidth = 0; } ++i; } #ifndef RANDOMIZER_ONLY // Free game's font mDoExt_removeMesgFont(); #endif } void applyMessageCodes(std::string& str) { 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