diff --git a/src/openrct2/drawing/Drawing.String.cpp b/src/openrct2/drawing/Drawing.String.cpp index bba1cfe3f5..34b3331e09 100644 --- a/src/openrct2/drawing/Drawing.String.cpp +++ b/src/openrct2/drawing/Drawing.String.cpp @@ -824,6 +824,13 @@ static void ttf_process_string_literal(rct_drawpixelinfo* dpi, const std::string #endif // NO_TTF } +static void ttf_process_string_codepoint(rct_drawpixelinfo* dpi, codepoint_t codepoint, text_draw_info* info) +{ + char buffer[8]{}; + utf8_write_codepoint(buffer, codepoint); + ttf_process_string_literal(dpi, buffer, info); +} + static void ttf_process_string(rct_drawpixelinfo* dpi, std::string_view text, text_draw_info* info) { FmtString fmt(text); @@ -833,6 +840,11 @@ static void ttf_process_string(rct_drawpixelinfo* dpi, std::string_view text, te { ttf_process_string_literal(dpi, token.text, info); } + else if (token.IsCodepoint()) + { + auto codepoint = token.GetCodepoint(); + ttf_process_string_codepoint(dpi, codepoint, info); + } else { ttf_process_format_code(dpi, token, info); diff --git a/src/openrct2/localisation/FormatCodes.h b/src/openrct2/localisation/FormatCodes.h index 2a8f227668..49eae6cb48 100644 --- a/src/openrct2/localisation/FormatCodes.h +++ b/src/openrct2/localisation/FormatCodes.h @@ -17,6 +17,7 @@ enum class FormatToken { Unknown, Literal, + Escaped, Newline, NewlineSmall, diff --git a/src/openrct2/localisation/Formatting.cpp b/src/openrct2/localisation/Formatting.cpp index e3a7ea586c..e8486e3308 100644 --- a/src/openrct2/localisation/Formatting.cpp +++ b/src/openrct2/localisation/Formatting.cpp @@ -62,6 +62,21 @@ namespace OpenRCT2 return kind == FormatToken::Literal; } + bool FmtString::token::IsCodepoint() const + { + return kind == FormatToken::Escaped; + } + + codepoint_t FmtString::token::GetCodepoint() const + { + if (kind == FormatToken::Escaped) + { + // Assume text is only "{{" or "}}" for now + return text[0]; + } + return 0; + } + FmtString::iterator::iterator(std::string_view s, size_t i) : str(s) , index(i) @@ -82,6 +97,14 @@ namespace OpenRCT2 { i++; } + else if (str[i] == '{' && i + 1 < str.size() && str[i + 1] == '{') + { + i += 2; + } + else if (str[i] == '}' && i + 1 < str.size() && str[i + 1] == '}') + { + i += 2; + } else if (str[i] == '{' && i + 1 < str.size() && str[i + 1] != '{') { // Move to end brace @@ -130,7 +153,7 @@ namespace OpenRCT2 do { i++; - } while (i < str.size() && str[i] != '{' && str[i] != '\n' && str[i] != '\r'); + } while (i < str.size() && str[i] != '{' && str[i] != '}' && str[i] != '\n' && str[i] != '\r'); } current = CreateToken(i - index); } @@ -148,7 +171,12 @@ namespace OpenRCT2 FmtString::token FmtString::iterator::CreateToken(size_t len) { std::string_view sztoken = str.substr(index, len); - if (sztoken.size() >= 2 && sztoken[0] == '{' && sztoken[1] != '{') + + if (sztoken.size() >= 2 && ((sztoken[0] == '{' && sztoken[1] == '{') || (sztoken[0] == '}' && sztoken[1] == '}'))) + { + return token(FormatToken::Escaped, sztoken); + } + else if (sztoken.size() >= 2 && sztoken[0] == '{' && sztoken[1] != '{') { auto kind = FormatTokenFromString(sztoken.substr(1, len - 2)); return token(kind, sztoken); diff --git a/src/openrct2/localisation/Formatting.h b/src/openrct2/localisation/Formatting.h index d6afe65039..0f325a50cd 100644 --- a/src/openrct2/localisation/Formatting.h +++ b/src/openrct2/localisation/Formatting.h @@ -41,6 +41,8 @@ namespace OpenRCT2 token() = default; token(FormatToken k, std::string_view s, uint32_t p = 0); bool IsLiteral() const; + bool IsCodepoint() const; + codepoint_t GetCodepoint() const; }; struct iterator diff --git a/test/tests/FormattingTests.cpp b/test/tests/FormattingTests.cpp index ac35ca3944..125994664a 100644 --- a/test/tests/FormattingTests.cpp +++ b/test/tests/FormattingTests.cpp @@ -39,7 +39,20 @@ TEST_F(FmtStringTests, iteration) actual += String::StdFormat("[%d:%s]", t.kind, std::string(t.text).c_str()); } - ASSERT_EQ("[28:{BLACK}][1:Guests: ][7:{INT32}]", actual); + ASSERT_EQ("[29:{BLACK}][1:Guests: ][8:{INT32}]", actual); +} + +TEST_F(FmtStringTests, iteration_escaped) +{ + std::string actual; + + auto fmt = FmtString("This is an {{ESCAPED}} string."); + for (const auto& t : fmt) + { + actual += String::StdFormat("[%d:%s]", t.kind, std::string(t.text).c_str()); + } + + ASSERT_EQ("[1:This is an ][2:{{][1:ESCAPED][2:}}][1: string.]", actual); } TEST_F(FmtStringTests, without_format_tokens) @@ -60,6 +73,8 @@ protected: gOpenRCT2NoGraphics = true; _context = CreateContext(); ASSERT_TRUE(_context->Initialise()); + + language_open(LANGUAGE_ENGLISH_UK); } static void TearDownTestCase()