From 19f0d8dfe969a80eb0df81e6a4a944429c9df501 Mon Sep 17 00:00:00 2001 From: Ted John Date: Wed, 20 Jan 2021 11:35:11 +0000 Subject: [PATCH] Fix #13842: News is imported incorrectly --- src/openrct2-ui/windows/GameBottomToolbar.cpp | 2 +- src/openrct2-ui/windows/News.cpp | 2 +- src/openrct2/Game.cpp | 25 --- src/openrct2/Game.h | 1 - src/openrct2/localisation/FormatCodes.cpp | 2 +- src/openrct2/localisation/FormatCodes.h | 1 + src/openrct2/management/NewsItem.cpp | 2 +- src/openrct2/management/NewsItem.h | 3 +- src/openrct2/rct1/S4Importer.cpp | 3 +- src/openrct2/rct12/RCT12.cpp | 188 ++++++++++++++++++ src/openrct2/rct12/RCT12.h | 3 + src/openrct2/rct2/S6Exporter.cpp | 37 +--- src/openrct2/rct2/S6Importer.cpp | 2 +- src/openrct2/scripting/ScPark.hpp | 6 +- 14 files changed, 206 insertions(+), 71 deletions(-) diff --git a/src/openrct2-ui/windows/GameBottomToolbar.cpp b/src/openrct2-ui/windows/GameBottomToolbar.cpp index 478ab3149f..e789ccd7bc 100644 --- a/src/openrct2-ui/windows/GameBottomToolbar.cpp +++ b/src/openrct2-ui/windows/GameBottomToolbar.cpp @@ -562,7 +562,7 @@ static void window_game_bottom_toolbar_draw_news_item(rct_drawpixelinfo* dpi, rc INSET_RECT_F_30); // Text - utf8* newsItemText = newsItem->Text; + const auto* newsItemText = newsItem->Text.c_str(); auto screenCoords = w->windowPos + ScreenCoordsXY{ middleOutsetWidget->midX(), middleOutsetWidget->top + 11 }; width = middleOutsetWidget->width() - 62; gfx_draw_string_centred_wrapped_partial( diff --git a/src/openrct2-ui/windows/News.cpp b/src/openrct2-ui/windows/News.cpp index fb603cfcac..b5136ddff0 100644 --- a/src/openrct2-ui/windows/News.cpp +++ b/src/openrct2-ui/windows/News.cpp @@ -248,7 +248,7 @@ static void window_news_scrollpaint(rct_window* w, rct_drawpixelinfo* dpi, int32 // Item text { auto ft = Formatter(); - ft.Add(newsItem.Text); + ft.Add(newsItem.Text.c_str()); gfx_draw_string_left_wrapped( dpi, ft.Data(), { 2, y + lineHeight }, 325, STR_BOTTOM_TOOLBAR_NEWS_TEXT, COLOUR_BRIGHT_GREEN); } diff --git a/src/openrct2/Game.cpp b/src/openrct2/Game.cpp index 361d54a6d5..4718adad9c 100644 --- a/src/openrct2/Game.cpp +++ b/src/openrct2/Game.cpp @@ -380,22 +380,6 @@ void game_convert_strings_to_utf8() gScenarioCompletedBy = rct2_to_utf8(gScenarioCompletedBy, RCT2LanguageId::EnglishUK); gScenarioName = rct2_to_utf8(gScenarioName, RCT2LanguageId::EnglishUK); gScenarioDetails = rct2_to_utf8(gScenarioDetails, RCT2LanguageId::EnglishUK); - - // News items - game_convert_news_items_to_utf8(); -} - -void game_convert_news_items_to_utf8() -{ - for (int32_t i = 0; i < News::MaxItems; i++) - { - News::Item* newsItem = News::GetItem(i); - - if (!str_is_null_or_empty(newsItem->Text)) - { - rct2_to_utf8_self(newsItem->Text, sizeof(newsItem->Text)); - } - } } /** @@ -416,15 +400,6 @@ void game_convert_strings_to_rct2(rct_s6_data* s6) utf8_to_rct2_self(userString, RCT12_USER_STRING_MAX_LENGTH); } } - - // News items - for (auto& newsItem : s6->news_items) - { - if (!str_is_null_or_empty(newsItem.Text)) - { - utf8_to_rct2_self(newsItem.Text, sizeof(newsItem.Text)); - } - } } // OpenRCT2 workaround to recalculate some values which are saved redundantly in the save to fix corrupted files. diff --git a/src/openrct2/Game.h b/src/openrct2/Game.h index 94bdbc44aa..897f6eb518 100644 --- a/src/openrct2/Game.h +++ b/src/openrct2/Game.h @@ -173,7 +173,6 @@ void save_game_cmd(const utf8* name = nullptr); void save_game_with_name(const utf8* name); void game_autosave(); void game_convert_strings_to_utf8(); -void game_convert_news_items_to_utf8(); void game_convert_strings_to_rct2(rct_s6_data* s6); void utf8_to_rct2_self(char* buffer, size_t length); void rct2_to_utf8_self(char* buffer, size_t length); diff --git a/src/openrct2/localisation/FormatCodes.cpp b/src/openrct2/localisation/FormatCodes.cpp index f30950982f..f869bb94df 100644 --- a/src/openrct2/localisation/FormatCodes.cpp +++ b/src/openrct2/localisation/FormatCodes.cpp @@ -65,7 +65,7 @@ static const std::unordered_map FormatTokenMap = }; // clang-format on -static std::string_view GetFormatTokenStringWithBraces(FormatToken token) +std::string_view GetFormatTokenStringWithBraces(FormatToken token) { // Ensure cache is thread safe static std::mutex mutex; diff --git a/src/openrct2/localisation/FormatCodes.h b/src/openrct2/localisation/FormatCodes.h index c805f85854..6baf334b35 100644 --- a/src/openrct2/localisation/FormatCodes.h +++ b/src/openrct2/localisation/FormatCodes.h @@ -76,6 +76,7 @@ enum class FormatToken OutlineDisable, }; +std::string_view GetFormatTokenStringWithBraces(FormatToken token); FormatToken FormatTokenFromString(std::string_view token); std::string_view FormatTokenToString(FormatToken token, bool withBraces = false); bool FormatTokenTakesArgument(FormatToken token); diff --git a/src/openrct2/management/NewsItem.cpp b/src/openrct2/management/NewsItem.cpp index 92016fb62e..e96b46cf8e 100644 --- a/src/openrct2/management/NewsItem.cpp +++ b/src/openrct2/management/NewsItem.cpp @@ -319,7 +319,7 @@ News::Item* News::AddItemToQueue(News::ItemType type, const utf8* text, uint32_t newsItem->Ticks = 0; newsItem->MonthYear = static_cast(gDateMonthsElapsed); newsItem->Day = ((days_in_month[date_get_month(newsItem->MonthYear)] * gDateMonthTicks) >> 16) + 1; - safe_strcpy(newsItem->Text, text, sizeof(newsItem->Text)); + newsItem->Text = text; return newsItem; } diff --git a/src/openrct2/management/NewsItem.h b/src/openrct2/management/NewsItem.h index 9bfbfee416..f4712773ee 100644 --- a/src/openrct2/management/NewsItem.h +++ b/src/openrct2/management/NewsItem.h @@ -15,6 +15,7 @@ #include #include #include +#include struct CoordsXYZ; class Formatter; @@ -60,7 +61,7 @@ namespace News uint16_t Ticks; uint16_t MonthYear; uint8_t Day; - utf8 Text[256]; + std::string Text; constexpr bool IsEmpty() const noexcept { diff --git a/src/openrct2/rct1/S4Importer.cpp b/src/openrct2/rct1/S4Importer.cpp index 3de5df9462..77723e782c 100644 --- a/src/openrct2/rct1/S4Importer.cpp +++ b/src/openrct2/rct1/S4Importer.cpp @@ -208,7 +208,6 @@ public: SetDefaultNames(); determine_ride_entrance_and_exit_locations(); - game_convert_news_items_to_utf8(); map_count_remaining_land_rights(); research_determine_first_of_type(); } @@ -2613,7 +2612,7 @@ private: dst->Ticks = src->Ticks; dst->MonthYear = src->MonthYear; dst->Day = src->Day; - std::copy(std::begin(src->Text), std::end(src->Text), dst->Text); + dst->Text = ConvertFormattedStringToOpenRCT2(std::string_view(src->Text, sizeof(src->Text))); if (dst->Type == News::ItemType::Research) { diff --git a/src/openrct2/rct12/RCT12.cpp b/src/openrct2/rct12/RCT12.cpp index 5cfe317d38..2fc36f1dd2 100644 --- a/src/openrct2/rct12/RCT12.cpp +++ b/src/openrct2/rct12/RCT12.cpp @@ -10,6 +10,7 @@ #include "RCT12.h" #include "../core/String.hpp" +#include "../localisation/Formatting.h" #include "../localisation/Localisation.h" #include "../ride/Track.h" #include "../world/Banner.h" @@ -20,6 +21,8 @@ #include "../world/TileElement.h" #include "../world/Wall.h" +using namespace OpenRCT2; + uint8_t RCT12TileElementBase::GetType() const { return this->type & TILE_ELEMENT_TYPE_MASK; @@ -1101,3 +1104,188 @@ std::string RCT12RemoveFormattingUTF8(std::string_view s) result.shrink_to_fit(); return result; } + +namespace RCT12FormatCode +{ + constexpr codepoint_t Newline = 5; + constexpr codepoint_t NewlineSmall = 6; + constexpr codepoint_t ColourBlack = 142; + constexpr codepoint_t ColourGrey = 143; + constexpr codepoint_t ColourWhite = 144; + constexpr codepoint_t ColourRed = 145; + constexpr codepoint_t ColourGreen = 146; + constexpr codepoint_t ColourYellow = 147; + constexpr codepoint_t ColourTopaz = 148; + constexpr codepoint_t ColourCeladon = 149; + constexpr codepoint_t ColourBabyBlue = 150; + constexpr codepoint_t ColourPaleLavender = 151; + constexpr codepoint_t ColourPaleGold = 152; + constexpr codepoint_t ColourLightPink = 153; + constexpr codepoint_t ColourPearlAqua = 154; + constexpr codepoint_t ColourPaleSilver = 155; +} // namespace RCT12FormatCode + +static FormatToken GetFormatTokenFromRCT12Code(codepoint_t codepoint) +{ + switch (codepoint) + { + case RCT12FormatCode::Newline: + return FormatToken::Newline; + case RCT12FormatCode::NewlineSmall: + return FormatToken::NewlineSmall; + case RCT12FormatCode::ColourBlack: + return FormatToken::ColourBlack; + case RCT12FormatCode::ColourGrey: + return FormatToken::ColourGrey; + case RCT12FormatCode::ColourWhite: + return FormatToken::ColourWhite; + case RCT12FormatCode::ColourRed: + return FormatToken::ColourRed; + case RCT12FormatCode::ColourGreen: + return FormatToken::ColourGreen; + case RCT12FormatCode::ColourYellow: + return FormatToken::ColourYellow; + case RCT12FormatCode::ColourTopaz: + return FormatToken::ColourTopaz; + case RCT12FormatCode::ColourCeladon: + return FormatToken::ColourCeladon; + case RCT12FormatCode::ColourBabyBlue: + return FormatToken::ColourBabyBlue; + case RCT12FormatCode::ColourPaleLavender: + return FormatToken::ColourPaleLavender; + case RCT12FormatCode::ColourPaleGold: + return FormatToken::ColourPaleGold; + case RCT12FormatCode::ColourLightPink: + return FormatToken::ColourLightPink; + case RCT12FormatCode::ColourPearlAqua: + return FormatToken::ColourPearlAqua; + case RCT12FormatCode::ColourPaleSilver: + return FormatToken::ColourPaleSilver; + default: + return FormatToken::Unknown; + } +} + +static codepoint_t GetRCT12CodeFromFormatToken(FormatToken token) +{ + switch (token) + { + case FormatToken::Newline: + return RCT12FormatCode::Newline; + case FormatToken::NewlineSmall: + return RCT12FormatCode::NewlineSmall; + case FormatToken::ColourBlack: + return RCT12FormatCode::ColourBlack; + case FormatToken::ColourGrey: + return RCT12FormatCode::ColourGrey; + case FormatToken::ColourWhite: + return RCT12FormatCode::ColourWhite; + case FormatToken::ColourRed: + return RCT12FormatCode::ColourRed; + case FormatToken::ColourGreen: + return RCT12FormatCode::ColourGreen; + case FormatToken::ColourYellow: + return RCT12FormatCode::ColourYellow; + case FormatToken::ColourTopaz: + return RCT12FormatCode::ColourTopaz; + case FormatToken::ColourCeladon: + return RCT12FormatCode::ColourCeladon; + case FormatToken::ColourBabyBlue: + return RCT12FormatCode::ColourBabyBlue; + case FormatToken::ColourPaleLavender: + return RCT12FormatCode::ColourPaleLavender; + case FormatToken::ColourPaleGold: + return RCT12FormatCode::ColourPaleGold; + case FormatToken::ColourLightPink: + return RCT12FormatCode::ColourLightPink; + case FormatToken::ColourPearlAqua: + return RCT12FormatCode::ColourPearlAqua; + case FormatToken::ColourPaleSilver: + return RCT12FormatCode::ColourPaleSilver; + default: + return 0; + } +} + +std::string ConvertFormattedStringToOpenRCT2(std::string_view buffer) +{ + auto nullTerminator = buffer.find('\0'); + if (nullTerminator != std::string::npos) + { + buffer = buffer.substr(0, nullTerminator); + } + auto asUtf8 = rct2_to_utf8(buffer, RCT2LanguageId::EnglishUK); + + std::string result; + CodepointView codepoints(asUtf8); + for (auto codepoint : codepoints) + { + auto token = GetFormatTokenFromRCT12Code(codepoint); + if (token != FormatToken::Unknown) + { + result += GetFormatTokenStringWithBraces(token); + } + else + { + String::AppendCodepoint(result, codepoint); + } + } + return result; +} + +std::string ConvertFormattedStringToRCT2(std::string_view buffer, size_t maxLength) +{ + std::string result; + FmtString fmt(buffer); + for (const auto& token : fmt) + { + if (token.IsLiteral()) + { + result += token.text; + } + else + { + auto codepoint = GetRCT12CodeFromFormatToken(token.kind); + if (codepoint == 0) + { + result += token.text; + } + else + { + String::AppendCodepoint(result, codepoint); + } + } + } + return GetTruncatedRCT2String(result, maxLength); +} + +std::string GetTruncatedRCT2String(std::string_view src, size_t maxLength) +{ + auto rct2encoded = utf8_to_rct2(src); + if (rct2encoded.size() > maxLength - 1) + { + log_warning( + "The user string '%s' is too long for the S6 file format and has therefore been truncated.", + std::string(src).c_str()); + + rct2encoded.resize(maxLength - 1); + for (size_t i = 0; i < rct2encoded.size(); i++) + { + if (rct2encoded[i] == static_cast(static_cast(0xFF))) + { + if (i > maxLength - 4) + { + // This codepoint was truncated, remove codepoint altogether + rct2encoded.resize(i); + break; + } + else + { + // Skip the next two bytes which represent the unicode character + i += 2; + } + } + } + } + return rct2encoded; +} diff --git a/src/openrct2/rct12/RCT12.h b/src/openrct2/rct12/RCT12.h index 22c556acf1..be39fe9052 100644 --- a/src/openrct2/rct12/RCT12.h +++ b/src/openrct2/rct12/RCT12.h @@ -876,3 +876,6 @@ ride_id_t RCT12RideIdToOpenRCT2RideId(const RCT12RideId rideId); RCT12RideId OpenRCT2RideIdToRCT12RideId(const ride_id_t rideId); bool IsLikelyUTF8(std::string_view s); std::string RCT12RemoveFormattingUTF8(std::string_view s); +std::string ConvertFormattedStringToOpenRCT2(std::string_view buffer); +std::string ConvertFormattedStringToRCT2(std::string_view buffer, size_t maxLength); +std::string GetTruncatedRCT2String(std::string_view src, size_t maxLength); diff --git a/src/openrct2/rct2/S6Exporter.cpp b/src/openrct2/rct2/S6Exporter.cpp index 89bf503232..0b0e1152d8 100644 --- a/src/openrct2/rct2/S6Exporter.cpp +++ b/src/openrct2/rct2/S6Exporter.cpp @@ -412,7 +412,9 @@ void S6Exporter::Export() dst->Ticks = src->Ticks; dst->MonthYear = src->MonthYear; dst->Day = src->Day; - std::memcpy(dst->Text, src->Text, sizeof(dst->Text)); + + auto rct2text = ConvertFormattedStringToRCT2(src->Text, sizeof(dst->Text)); + std::memcpy(dst->Text, rct2text.c_str(), std::min(sizeof(dst->Text), rct2text.size())); } // pad_13CE730 @@ -1646,37 +1648,6 @@ std::optional S6Exporter::AllocateUserString(std::string_view value) return std::nullopt; } -static std::string GetTruncatedRCT2String(std::string_view src) -{ - auto rct2encoded = utf8_to_rct2(src); - if (rct2encoded.size() > RCT12_USER_STRING_MAX_LENGTH - 1) - { - log_warning( - "The user string '%s' is too long for the S6 file format and has therefore been truncated.", - std::string(src).c_str()); - - rct2encoded.resize(RCT12_USER_STRING_MAX_LENGTH - 1); - for (size_t i = 0; i < rct2encoded.size(); i++) - { - if (rct2encoded[i] == static_cast(static_cast(0xFF))) - { - if (i > RCT12_USER_STRING_MAX_LENGTH - 4) - { - // This codepoint was truncated, remove codepoint altogether - rct2encoded.resize(i); - break; - } - else - { - // Skip the next two bytes which represent the unicode character - i += 2; - } - } - } - } - return rct2encoded; -} - void S6Exporter::ExportUserStrings() { auto numUserStrings = std::min(_userStrings.size(), RCT12_MAX_USER_STRINGS); @@ -1684,7 +1655,7 @@ void S6Exporter::ExportUserStrings() { auto dst = _s6.custom_strings[i]; const auto& src = _userStrings[i]; - auto encodedSrc = GetTruncatedRCT2String(src); + auto encodedSrc = GetTruncatedRCT2String(src, RCT12_USER_STRING_MAX_LENGTH); auto stringLen = std::min(encodedSrc.size(), RCT12_USER_STRING_MAX_LENGTH - 1); std::memcpy(dst, encodedSrc.data(), stringLen); } diff --git a/src/openrct2/rct2/S6Importer.cpp b/src/openrct2/rct2/S6Importer.cpp index 69a15e7856..6e0bbc6763 100644 --- a/src/openrct2/rct2/S6Importer.cpp +++ b/src/openrct2/rct2/S6Importer.cpp @@ -433,7 +433,7 @@ public: dst->Ticks = src->Ticks; dst->MonthYear = src->MonthYear; dst->Day = src->Day; - std::memcpy(dst->Text, src->Text, sizeof(src->Text)); + dst->Text = ConvertFormattedStringToOpenRCT2(std::string_view(src->Text, sizeof(src->Text))); } else { diff --git a/src/openrct2/scripting/ScPark.hpp b/src/openrct2/scripting/ScPark.hpp index acbc0b0fd7..8a7b894e49 100644 --- a/src/openrct2/scripting/ScPark.hpp +++ b/src/openrct2/scripting/ScPark.hpp @@ -63,9 +63,7 @@ namespace OpenRCT2::Scripting result.Ticks = value["tickCount"].as_int(); result.MonthYear = value["month"].as_int(); result.Day = value["day"].as_int(); - - auto text = value["text"].as_string(); - String::Set(result.Text, sizeof(result.Text), text.c_str()); + result.Text = value["text"].as_string(); return result; } @@ -219,7 +217,7 @@ namespace OpenRCT2::Scripting auto msg = GetMessage(); if (msg != nullptr) { - String::Set(msg->Text, sizeof(msg->Text), value.c_str()); + msg->Text = value; } }