Merge pull request #13212 from IntelOrca/new-format-string

Eradicate all coded format tokens. All internal strings now contain raw tokens such as `{STRINGID}` and `{RED}`. New iterators have been created to iterate the tokens and the UTF-8 codepoints.

Formatting strings has been re-written and a new template version is available.
This commit is contained in:
Ted John 2020-12-04 13:56:54 +00:00 committed by GitHub
commit d58d834925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2535 additions and 2265 deletions

View File

@ -115,6 +115,8 @@
939A35A220C12FFD00630B3F /* InteractiveConsole.h in Headers */ = {isa = PBXBuildFile; fileRef = 939A35A120C12FFD00630B3F /* InteractiveConsole.h */; };
93AE2389252F948A00CD03C3 /* Formatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 93AE2387252F948A00CD03C3 /* Formatter.h */; };
93AE238A252F948A00CD03C3 /* Formatter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93AE2388252F948A00CD03C3 /* Formatter.cpp */; };
93B4DC1525487CDF008D63FF /* Formatting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93B4DC1325487CDE008D63FF /* Formatting.cpp */; };
93B4DC1625487CDF008D63FF /* Formatting.h in Headers */ = {isa = PBXBuildFile; fileRef = 93B4DC1425487CDE008D63FF /* Formatting.h */; };
93CBA4C020A74FF200867D56 /* BitmapReader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93CBA4BF20A74FF200867D56 /* BitmapReader.cpp */; };
93CBA4C320A7502E00867D56 /* Imaging.h in Headers */ = {isa = PBXBuildFile; fileRef = 93CBA4C120A7502D00867D56 /* Imaging.h */; };
93CBA4C420A7502E00867D56 /* Imaging.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 93CBA4C220A7502E00867D56 /* Imaging.cpp */; };
@ -1341,6 +1343,8 @@
939A35A120C12FFD00630B3F /* InteractiveConsole.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InteractiveConsole.h; sourceTree = "<group>"; };
93AE2387252F948A00CD03C3 /* Formatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Formatter.h; sourceTree = "<group>"; };
93AE2388252F948A00CD03C3 /* Formatter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Formatter.cpp; sourceTree = "<group>"; };
93B4DC1325487CDE008D63FF /* Formatting.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Formatting.cpp; sourceTree = "<group>"; };
93B4DC1425487CDE008D63FF /* Formatting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Formatting.h; sourceTree = "<group>"; };
93CBA4BE20A74FF200867D56 /* BitmapReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BitmapReader.h; sourceTree = "<group>"; };
93CBA4BF20A74FF200867D56 /* BitmapReader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BitmapReader.cpp; sourceTree = "<group>"; };
93CBA4C120A7502D00867D56 /* Imaging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Imaging.h; sourceTree = "<group>"; };
@ -2962,6 +2966,8 @@
4C7B53B01FFF935B00A52E21 /* FormatCodes.h */,
93AE2388252F948A00CD03C3 /* Formatter.cpp */,
93AE2387252F948A00CD03C3 /* Formatter.h */,
93B4DC1325487CDE008D63FF /* Formatting.cpp */,
93B4DC1425487CDE008D63FF /* Formatting.h */,
4C7B53B11FFF935B00A52E21 /* Language.cpp */,
4C7B53C91FFF991000A52E21 /* Language.h */,
4C7B53B31FFF935B00A52E21 /* LanguagePack.cpp */,
@ -3707,6 +3713,7 @@
939A359F20C12FDE00630B3F /* Paint.Surface.h in Headers */,
C67B28192002D7F200109C93 /* Window_internal.h in Headers */,
93DFD05024521C1A001FCBAF /* ScPark.hpp in Headers */,
93B4DC1625487CDF008D63FF /* Formatting.h in Headers */,
93FB271F24ED32B7008241C9 /* json.hpp in Headers */,
93DFD02E24521BA0001FCBAF /* FileWatcher.h in Headers */,
2ADE2F28224418B2002598AF /* DataSerialiserTag.h in Headers */,
@ -4292,6 +4299,7 @@
F76C85E71EC4E88300FA49E2 /* String.cpp in Sources */,
C68878DE20289B9B0084B384 /* Supports.cpp in Sources */,
C688791720289B9B0084B384 /* MiniHelicopters.cpp in Sources */,
93B4DC1525487CDF008D63FF /* Formatting.cpp in Sources */,
C688784F202899D00084B384 /* CmdlineSprite.cpp in Sources */,
F76C85EE1EC4E88300FA49E2 /* Zip.cpp in Sources */,
C688793220289B9B0084B384 /* SplashBoats.cpp in Sources */,

View File

@ -3559,7 +3559,7 @@ STR_6304 :Open scenery picker
STR_6305 :Multithreading
STR_6306 :{SMALLFONT}{BLACK}Experimental option to use multiple threads to render, may cause instability.
STR_6307 :Colour scheme: {BLACK}{STRINGID}
STR_6308 :“{STRINGID}{OUTLINE}{TOPAZ}”{NEWLINE}{STRINGID}
STR_6308 :{TOPAZ}“{STRINGID}{OUTLINE}{TOPAZ}”{NEWLINE}{STRINGID}
STR_6309 :Reconnect
STR_6310 :{WINDOW_COLOUR_2}Position: {BLACK}{INT32} {INT32} {INT32}
STR_6311 :{WINDOW_COLOUR_2}Next: {BLACK}{INT32} {INT32} {INT32}

View File

@ -89,7 +89,6 @@ void TextComposition::HandleMessage(const SDL_Event* e)
}
utf8* newText = String::Duplicate(e->text.text);
utf8_remove_formatting(newText, false);
Insert(newText);
Memory::Free(newText);
@ -186,7 +185,6 @@ void TextComposition::HandleMessage(const SDL_Event* e)
if ((modifier & KEYBOARD_PRIMARY_MODIFIER) && SDL_HasClipboardText())
{
utf8* text = SDL_GetClipboardText();
utf8_remove_formatting(text, false);
Insert(text);
SDL_free(text);
window_update_textbox();

View File

@ -140,7 +140,7 @@ void InGameConsole::RefreshCaret(size_t position)
_selectionStart = position;
char tempString[TEXT_INPUT_SIZE] = { 0 };
std::memcpy(tempString, &_consoleCurrentLine, _selectionStart);
_caretScreenPosX = gfx_get_string_width(tempString);
_caretScreenPosX = gfx_get_string_width_no_formatting(tempString);
}
void InGameConsole::Scroll(int32_t linesToScroll)
@ -199,13 +199,13 @@ void InGameConsole::Toggle()
}
}
void InGameConsole::WriteLine(const std::string& input, uint32_t colourFormat)
void InGameConsole::WriteLine(const std::string& input, FormatToken colourFormat)
{
// Include text colour format only for special cases
// The draw function handles the default text colour differently
utf8 colourCodepoint[4]{};
if (colourFormat != FORMAT_WINDOW_COLOUR_2)
utf8_write_codepoint(colourCodepoint, colourFormat);
auto colourCodepoint = "";
if (colourFormat != FormatToken::ColourWindow2)
colourCodepoint = "{WINDOW_COLOUR_2}";
std::string line;
std::size_t splitPos = 0;
@ -254,9 +254,6 @@ void InGameConsole::Update()
}
}
}
// Remove unwanted characters in console input
utf8_remove_format_codes(_consoleCurrentLine, false);
}
// Flash the caret
@ -277,12 +274,11 @@ void InGameConsole::Draw(rct_drawpixelinfo* dpi) const
// This is something of a hack to ensure the text is actually black
// as opposed to a desaturated grey
std::string colourFormatStr;
thread_local std::string colourFormatStr;
colourFormatStr.clear();
if (textColour == COLOUR_BLACK)
{
utf8 extraTextFormatCode[4]{};
utf8_write_codepoint(extraTextFormatCode, FORMAT_BLACK);
colourFormatStr = extraTextFormatCode;
colourFormatStr = "{BLACK}";
}
// TTF looks far better without the outlines
@ -316,7 +312,7 @@ void InGameConsole::Draw(rct_drawpixelinfo* dpi) const
{
const size_t index = i + _consoleScrollPos;
lineBuffer = colourFormatStr + _consoleLines[index];
gfx_draw_string(dpi, lineBuffer.c_str(), textColour, screenCoords);
gfx_draw_string_no_formatting(dpi, lineBuffer.c_str(), textColour, screenCoords);
screenCoords.y += lineHeight;
}
@ -324,7 +320,7 @@ void InGameConsole::Draw(rct_drawpixelinfo* dpi) const
// Draw current line
lineBuffer = colourFormatStr + _consoleCurrentLine;
gfx_draw_string(dpi, lineBuffer.c_str(), TEXT_COLOUR_255, screenCoords);
gfx_draw_string_no_formatting(dpi, lineBuffer.c_str(), TEXT_COLOUR_255, screenCoords);
// Draw caret
if (_consoleCaretTicks < CONSOLE_CARET_FLASH_THRESHOLD)

View File

@ -10,6 +10,7 @@
#pragma once
#include <openrct2/interface/InteractiveConsole.h>
#include <openrct2/localisation/FormatCodes.h>
#include <openrct2/world/Location.hpp>
namespace OpenRCT2::Ui
@ -52,7 +53,7 @@ namespace OpenRCT2::Ui
void Close() override;
void Hide() override;
void Toggle();
void WriteLine(const std::string& s, uint32_t colourFormat) override;
void WriteLine(const std::string& s, FormatToken colourFormat) override;
void Input(ConsoleInput input);
void RefreshCaret(size_t position = 0);

View File

@ -1055,7 +1055,7 @@ static void WidgetTextBoxDraw(rct_drawpixelinfo* dpi, rct_window* w, rct_widgeti
{
safe_strcpy(wrapped_string, w->widgets[widgetIndex].string, 512);
gfx_wrap_string(wrapped_string, bottomRight.x - topLeft.x - 5, &no_lines, &font_height);
gfx_draw_string(dpi, wrapped_string, w->colours[1], { topLeft.x + 2, topLeft.y });
gfx_draw_string_no_formatting(dpi, wrapped_string, w->colours[1], { topLeft.x + 2, topLeft.y });
}
return;
}
@ -1066,14 +1066,14 @@ static void WidgetTextBoxDraw(rct_drawpixelinfo* dpi, rct_window* w, rct_widgeti
// +13 for cursor when max length.
gfx_wrap_string(wrapped_string, bottomRight.x - topLeft.x - 5 - 6, &no_lines, &font_height);
gfx_draw_string(dpi, wrapped_string, w->colours[1], { topLeft.x + 2, topLeft.y });
gfx_draw_string_no_formatting(dpi, wrapped_string, w->colours[1], { topLeft.x + 2, topLeft.y });
size_t string_length = get_string_size(wrapped_string) - 1;
// Make a copy of the string for measuring the width.
char temp_string[TEXT_INPUT_SIZE] = { 0 };
std::memcpy(temp_string, wrapped_string, std::min(string_length, gTextInput->SelectionStart));
int32_t cur_x = topLeft.x + gfx_get_string_width(temp_string) + 3;
int32_t cur_x = topLeft.x + gfx_get_string_width_no_formatting(temp_string) + 3;
int32_t width = 6;
if (static_cast<uint32_t>(gTextInput->SelectionStart) < strlen(gTextBoxInput))
@ -1082,7 +1082,7 @@ static void WidgetTextBoxDraw(rct_drawpixelinfo* dpi, rct_window* w, rct_widgeti
// of the character that the cursor is under.
temp_string[1] = '\0';
temp_string[0] = gTextBoxInput[gTextInput->SelectionStart];
width = std::max(gfx_get_string_width(temp_string) - 2, 4);
width = std::max(gfx_get_string_width_no_formatting(temp_string) - 2, 4);
}
if (gTextBoxFrameNo <= 15)

View File

@ -270,7 +270,7 @@ namespace OpenRCT2::Ui::Windows
result.MaxWidth = GetOptionalInt(desc["maxWidth"]);
result.MinHeight = GetOptionalInt(desc["minHeight"]);
result.MaxHeight = GetOptionalInt(desc["maxHeight"]);
result.Title = language_convert_string(desc["title"].as_string());
result.Title = desc["title"].as_string();
result.Id = GetOptionalInt(desc["id"]);
result.TabIndex = GetOptionalInt(desc["tabIndex"]);
@ -1077,7 +1077,7 @@ namespace OpenRCT2::Ui::Windows
auto customWidgetInfo = customInfo.GetCustomWidgetDesc(w, widgetIndex);
if (customWidgetInfo != nullptr)
{
customWidgetInfo->Text = language_convert_string(value);
customWidgetInfo->Text = value;
w->widgets[widgetIndex].string = customWidgetInfo->Text.data();
widget_invalidate(w, widgetIndex);
}

View File

@ -314,7 +314,7 @@ namespace OpenRCT2::Scripting
auto widget = GetWidget();
if (widget != nullptr && (widget->flags & WIDGET_FLAGS::TEXT_IS_STRING) && widget->string != nullptr)
{
return language_convert_string_to_tokens(widget->string);
return widget->string;
}
}
return "";

View File

@ -243,7 +243,7 @@ namespace OpenRCT2::Scripting
auto w = GetWindow();
if (w != nullptr && w->classification == WC_CUSTOM)
{
return language_convert_string_to_tokens(GetWindowTitle(w));
return GetWindowTitle(w);
}
return {};
}
@ -252,7 +252,7 @@ namespace OpenRCT2::Scripting
auto w = GetWindow();
if (w != nullptr && w->classification == WC_CUSTOM)
{
UpdateWindowTitle(w, language_convert_string(value));
UpdateWindowTitle(w, value);
}
}

View File

@ -236,13 +236,6 @@ static void window_changelog_process_changelog_text(const std::string& text)
while ((pos = text.find("\n", prev)) != std::string::npos)
{
std::string line = text.substr(prev, pos - prev);
for (char* ch = line.data(); *ch != '\0'; ch++)
{
if (utf8_is_format_code(*ch))
{
*ch = FORMAT_OUTLINE_OFF;
}
}
_changelogLines.push_back(line);
prev = pos + 1;
}

View File

@ -1146,20 +1146,18 @@ static void window_editor_object_selection_scrollpaint(rct_window* w, rct_drawpi
gfx_fill_rect_inset(dpi, 2, screenCoords.y, 11, screenCoords.y + 10, w->colours[1], INSET_RECT_F_E0);
// Highlight background
colour = COLOUR_BRIGHT_GREEN | COLOUR_FLAG_TRANSLUCENT;
if (listItem.entry == w->object_entry && !(*listItem.flags & OBJECT_SELECTION_FLAG_6))
auto highlighted = listItem.entry == w->object_entry && !(*listItem.flags & OBJECT_SELECTION_FLAG_6);
if (highlighted)
{
auto bottom = screenCoords.y + (SCROLLABLE_ROW_HEIGHT - 1);
gfx_filter_rect(dpi, 0, screenCoords.y, w->width, bottom, PALETTE_DARKEN_1);
colour = COLOUR_BRIGHT_GREEN;
}
// Draw checkmark
if (!(gScreenFlags & SCREEN_FLAGS_TRACK_MANAGER) && (*listItem.flags & OBJECT_SELECTION_FLAG_SELECTED))
{
screenCoords.x = 2;
gCurrentFontSpriteBase = colour == COLOUR_BRIGHT_GREEN ? FONT_SPRITE_BASE_MEDIUM_EXTRA_DARK
: FONT_SPRITE_BASE_MEDIUM_DARK;
gCurrentFontSpriteBase = highlighted ? FONT_SPRITE_BASE_MEDIUM_EXTRA_DARK : FONT_SPRITE_BASE_MEDIUM_DARK;
colour2 = NOT_TRANSLUCENT(w->colours[1]);
if (*listItem.flags & (OBJECT_SELECTION_FLAG_IN_USE | OBJECT_SELECTION_FLAG_ALWAYS_REQUIRED))
colour2 |= COLOUR_FLAG_INSET;
@ -1169,8 +1167,9 @@ static void window_editor_object_selection_scrollpaint(rct_window* w, rct_drawpi
screenCoords.x = gScreenFlags & SCREEN_FLAGS_TRACK_MANAGER ? 0 : 15;
char* bufferWithColour = gCommonStringFormatBuffer;
char* buffer = utf8_write_codepoint(bufferWithColour, colour);
auto bufferWithColour = strcpy(gCommonStringFormatBuffer, highlighted ? "{WINDOW_COLOUR_2}" : "{BLACK}");
auto buffer = strchr(bufferWithColour, '\0');
if (*listItem.flags & OBJECT_SELECTION_FLAG_6)
{
colour = w->colours[1] & 0x7F;

View File

@ -61,22 +61,13 @@ rct_window* window_error_open(const std::string_view& title, const std::string_v
window_close_by_class(WC_ERROR);
auto& buffer = _window_error_text;
buffer.clear();
// Format the title
{
char temp[8]{};
utf8_write_codepoint(temp, FORMAT_BLACK);
buffer.append(temp);
}
buffer.assign("{BLACK}");
buffer.append(title);
// Format the message
if (!message.empty())
{
char temp[8]{};
utf8_write_codepoint(temp, FORMAT_NEWLINE);
buffer.append(temp);
buffer.push_back('\n');
buffer.append(message);
}

View File

@ -698,17 +698,14 @@ static void window_loadsave_paint(rct_window* w, rct_drawpixelinfo* dpi)
shorten_path(_shortenedDirectory, sizeof(_shortenedDirectory), _directory, w->width - 8);
}
utf8 buffer[256];
// Format text
utf8* ch = buffer;
ch = utf8_write_codepoint(ch, FORMAT_MEDIUMFONT);
ch = utf8_write_codepoint(ch, FORMAT_BLACK);
safe_strcpy(ch, _shortenedDirectory, sizeof(buffer) - (ch - buffer));
thread_local std::string buffer;
buffer.assign("{MEDIUMFONT}{BLACK}");
buffer += _shortenedDirectory;
// Draw path text
auto ft = Formatter();
ft.Add<utf8*>(Platform::StrDecompToPrecomp(buffer));
ft.Add<const char*>(Platform::StrDecompToPrecomp(buffer.data()));
DrawTextEllipsised(dpi, { w->windowPos.x + 4, w->windowPos.y + 20 }, w->width - 8, STR_STRING, ft, COLOUR_BLACK);
// Name button text

View File

@ -576,43 +576,44 @@ static void window_multiplayer_players_scrollpaint(rct_window* w, rct_drawpixeli
if (screenCoords.y + SCROLLABLE_ROW_HEIGHT + 1 >= dpi->y)
{
char buffer[300];
thread_local std::string buffer;
buffer.reserve(512);
buffer.clear();
// Draw player name
char* lineCh = buffer;
int32_t colour = COLOUR_BLACK;
if (i == w->selected_list_item)
{
gfx_filter_rect(dpi, 0, screenCoords.y, 800, screenCoords.y + SCROLLABLE_ROW_HEIGHT - 1, PALETTE_DARKEN_1);
safe_strcpy(buffer, network_get_player_name(i), sizeof(buffer));
buffer += network_get_player_name(i);
colour = w->colours[2];
}
else
{
if (network_get_player_flags(i) & NETWORK_PLAYER_FLAG_ISSERVER)
{
lineCh = utf8_write_codepoint(lineCh, FORMAT_BABYBLUE);
buffer += "{BABYBLUE}";
}
else
{
lineCh = utf8_write_codepoint(lineCh, FORMAT_BLACK);
buffer += "{BLACK}";
}
safe_strcpy(lineCh, network_get_player_name(i), sizeof(buffer) - (lineCh - buffer));
buffer += network_get_player_name(i);
}
screenCoords.x = 0;
gfx_clip_string(buffer, 230);
gfx_draw_string(dpi, buffer, colour, screenCoords);
gfx_clip_string(buffer.data(), 230);
gfx_draw_string(dpi, buffer.c_str(), colour, screenCoords);
// Draw group name
lineCh = buffer;
buffer.resize(0);
int32_t group = network_get_group_index(network_get_player_group(i));
if (group != -1)
{
lineCh = utf8_write_codepoint(lineCh, FORMAT_BLACK);
buffer += "{BLACK}";
screenCoords.x = 173;
safe_strcpy(lineCh, network_get_group_name(group), sizeof(buffer) - (lineCh - buffer));
gfx_clip_string(buffer, 80);
gfx_draw_string(dpi, buffer, colour, screenCoords);
buffer += network_get_group_name(group);
gfx_clip_string(buffer.data(), 80);
gfx_draw_string(dpi, buffer.c_str(), colour, screenCoords);
}
// Draw last action
@ -629,23 +630,27 @@ static void window_multiplayer_players_scrollpaint(rct_window* w, rct_drawpixeli
DrawTextEllipsised(dpi, { 256, screenCoords.y }, 100, STR_BLACK_STRING, ft, COLOUR_BLACK);
// Draw ping
lineCh = buffer;
buffer.resize(0);
int32_t ping = network_get_player_ping(i);
if (ping <= 100)
{
lineCh = utf8_write_codepoint(lineCh, FORMAT_GREEN);
buffer += "{GREEN}";
}
else if (ping <= 250)
{
lineCh = utf8_write_codepoint(lineCh, FORMAT_YELLOW);
buffer += "{YELLOW}";
}
else
{
lineCh = utf8_write_codepoint(lineCh, FORMAT_RED);
buffer += "{RED}";
}
snprintf(lineCh, sizeof(buffer) - (lineCh - buffer), "%d ms", ping);
char pingBuffer[64]{};
snprintf(pingBuffer, sizeof(pingBuffer), "%d ms", ping);
buffer += pingBuffer;
screenCoords.x = 356;
gfx_draw_string(dpi, buffer, colour, screenCoords);
gfx_draw_string(dpi, buffer.c_str(), colour, screenCoords);
}
screenCoords.y += SCROLLABLE_ROW_HEIGHT;
}
@ -823,6 +828,8 @@ static void window_multiplayer_groups_invalidate(rct_window* w)
static void window_multiplayer_groups_paint(rct_window* w, rct_drawpixelinfo* dpi)
{
thread_local std::string buffer;
WindowDrawWidgets(w, dpi);
window_multiplayer_draw_tab_images(w, dpi);
@ -830,13 +837,11 @@ static void window_multiplayer_groups_paint(rct_window* w, rct_drawpixelinfo* dp
int32_t group = network_get_group_index(network_get_default_group());
if (group != -1)
{
char buffer[300];
char* lineCh;
lineCh = buffer;
lineCh = utf8_write_codepoint(lineCh, FORMAT_WINDOW_COLOUR_2);
safe_strcpy(lineCh, network_get_group_name(group), sizeof(buffer) - (lineCh - buffer));
buffer.assign("{WINDOW_COLOUR_2}");
buffer += network_get_group_name(group);
auto ft = Formatter();
ft.Add<const char*>(buffer);
ft.Add<const char*>(buffer.c_str());
DrawTextEllipsised(
dpi, w->windowPos + ScreenCoordsXY{ widget->midX() - 5, widget->top }, widget->width() - 8, STR_STRING, ft,
COLOUR_BLACK, TextAlignment::CENTRE);
@ -857,13 +862,10 @@ static void window_multiplayer_groups_paint(rct_window* w, rct_drawpixelinfo* dp
group = network_get_group_index(_selectedGroup);
if (group != -1)
{
char buffer[300];
char* lineCh;
lineCh = buffer;
lineCh = utf8_write_codepoint(lineCh, FORMAT_WINDOW_COLOUR_2);
safe_strcpy(lineCh, network_get_group_name(group), sizeof(buffer) - (lineCh - buffer));
buffer.assign("{WINDOW_COLOUR_2}");
buffer += network_get_group_name(group);
auto ft = Formatter();
ft.Add<const char*>(buffer);
ft.Add<const char*>(buffer.c_str());
DrawTextEllipsised(
dpi, w->windowPos + ScreenCoordsXY{ widget->midX() - 5, widget->top }, widget->width() - 8, STR_STRING, ft,
COLOUR_BLACK, TextAlignment::CENTRE);
@ -891,17 +893,13 @@ static void window_multiplayer_groups_scrollpaint(rct_window* w, rct_drawpixelin
if (screenCoords.y + SCROLLABLE_ROW_HEIGHT + 1 >= dpi->y)
{
char buffer[300] = { 0 };
int32_t groupindex = network_get_group_index(_selectedGroup);
if (groupindex != -1)
{
if (network_can_perform_action(groupindex, static_cast<NetworkPermission>(i)))
{
char* lineCh = buffer;
lineCh = utf8_write_codepoint(lineCh, FORMAT_WINDOW_COLOUR_2);
lineCh = utf8_write_codepoint(lineCh, UnicodeChar::tick);
screenCoords.x = 0;
gfx_draw_string(dpi, buffer, COLOUR_BLACK, screenCoords);
gfx_draw_string(dpi, u8"{WINDOW_COLOUR_2}✓", COLOUR_BLACK, screenCoords);
}
}

View File

@ -156,12 +156,12 @@ static void window_network_status_paint(rct_window* w, rct_drawpixelinfo* dpi)
{
WindowDrawWidgets(w, dpi);
gCurrentFontSpriteBase = FONT_SPRITE_BASE_MEDIUM;
char buffer[sizeof(window_network_status_text) + 10];
char* lineCh = buffer;
lineCh = utf8_write_codepoint(lineCh, FORMAT_BLACK);
safe_strcpy(lineCh, window_network_status_text, sizeof(buffer) - (lineCh - buffer));
gfx_clip_string(buffer, w->widgets[WIDX_BACKGROUND].right - 50);
thread_local std::string buffer;
buffer.assign("{BLACK}");
buffer += window_network_status_text;
gfx_clip_string(buffer.data(), w->widgets[WIDX_BACKGROUND].right - 50);
ScreenCoordsXY screenCoords(w->windowPos.x + (w->width / 2), w->windowPos.y + (w->height / 2));
screenCoords.x -= gfx_get_string_width(buffer) / 2;
gfx_draw_string(dpi, buffer, COLOUR_BLACK, screenCoords);
gfx_draw_string(dpi, buffer.c_str(), COLOUR_BLACK, screenCoords);
}

View File

@ -329,13 +329,12 @@ void window_player_overview_paint(rct_window* w, rct_drawpixelinfo* dpi)
if (groupindex != -1)
{
rct_widget* widget = &window_player_overview_widgets[WIDX_GROUP];
char buffer[300];
char* lineCh;
lineCh = buffer;
lineCh = utf8_write_codepoint(lineCh, FORMAT_WINDOW_COLOUR_2);
safe_strcpy(lineCh, network_get_group_name(groupindex), sizeof(buffer) - (lineCh - buffer));
thread_local std::string buffer;
buffer.assign("{WINDOW_COLOUR_2}");
buffer += network_get_group_name(groupindex);
auto ft = Formatter();
ft.Add<const char*>(buffer);
ft.Add<const char*>(buffer.c_str());
DrawTextEllipsised(
dpi, w->windowPos + ScreenCoordsXY{ widget->midX() - 5, widget->top }, widget->width() - 8, STR_STRING, ft,

View File

@ -85,7 +85,6 @@ void window_text_input_open(
if (existing_text != STR_NONE)
format_string(buffer, maxLength, existing_text, &existing_args);
utf8_remove_format_codes(buffer, false);
window_text_input_raw_open(call_w, call_widget, title, description, buffer, maxLength);
}
@ -267,7 +266,7 @@ static void window_text_input_paint(rct_window* w, rct_drawpixelinfo* dpi)
for (int32_t line = 0; line <= no_lines; line++)
{
screenCoords.x = w->windowPos.x + 12;
gfx_draw_string(dpi, wrap_pointer, w->colours[1], screenCoords);
gfx_draw_string_no_formatting(dpi, wrap_pointer, w->colours[1], screenCoords);
size_t string_length = get_string_size(wrap_pointer) - 1;
@ -276,7 +275,7 @@ static void window_text_input_paint(rct_window* w, rct_drawpixelinfo* dpi)
// Make a copy of the string for measuring the width.
char temp_string[TEXT_INPUT_SIZE] = { 0 };
std::memcpy(temp_string, wrap_pointer, gTextInput->SelectionStart - char_count);
cursorX = w->windowPos.x + 13 + gfx_get_string_width(temp_string);
cursorX = w->windowPos.x + 13 + gfx_get_string_width_no_formatting(temp_string);
cursorY = screenCoords.y;
int32_t width = 6;
@ -287,7 +286,7 @@ static void window_text_input_paint(rct_window* w, rct_drawpixelinfo* dpi)
utf8 tmp[5] = { 0 }; // This is easier than setting temp_string[0..5]
uint32_t codepoint = utf8_get_next(text_input + gTextInput->SelectionStart, nullptr);
utf8_write_codepoint(tmp, codepoint);
width = std::max(gfx_get_string_width(tmp) - 2, 4);
width = std::max(gfx_get_string_width_no_formatting(tmp) - 2, 4);
}
if (w->frame_no > 15)

View File

@ -19,6 +19,7 @@
#include <openrct2/OpenRCT2.h>
#include <openrct2/ParkImporter.h>
#include <openrct2/config/Config.h>
#include <openrct2/localisation/Formatting.h>
#include <openrct2/localisation/Localisation.h>
#include <openrct2/object/ObjectManager.h>
#include <openrct2/scenario/Scenario.h>
@ -32,6 +33,8 @@
#include <openrct2/util/Util.h>
#include <openrct2/windows/Intent.h>
using namespace OpenRCT2;
// clang-format off
enum WINDOW_TITLE_EDITOR_TAB {
WINDOW_TITLE_EDITOR_TAB_PRESETS,
@ -871,21 +874,19 @@ static void window_title_editor_scrollpaint_saves(rct_window* w, rct_drawpixelin
gfx_fill_rect(dpi, fillRect, ColourMapA[w->colours[1]].lighter | 0x1000000);
}
char buffer[256];
auto saveName = _editingTitleSequence->Saves[i].c_str();
auto ft = Formatter();
ft.Add<const char*>(_editingTitleSequence->Saves[i].c_str());
if (selected || hover)
{
format_string(buffer, 256, STR_STRING, ft.Data());
ft.Add<rct_string_id>(STR_STRING);
}
else
{
format_string(buffer + 1, 255, STR_STRING, ft.Data());
buffer[0] = static_cast<utf8>(static_cast<uint8_t>(FORMAT_BLACK));
ft.Add<rct_string_id>(STR_BLACK_STRING);
ft.Add<rct_string_id>(STR_STRING);
}
ft = Formatter();
ft.Add<const char*>(&buffer);
gfx_draw_string_left(dpi, STR_STRING, ft.Data(), w->colours[1], screenCoords + ScreenCoordsXY{ 5, 0 });
ft.Add<const char*>(saveName);
gfx_draw_string_left(dpi, STR_STRINGID, ft.Data(), w->colours[1], screenCoords + ScreenCoordsXY{ 5, 0 });
}
}
@ -924,63 +925,102 @@ static void window_title_editor_scrollpaint_commands(rct_window* w, rct_drawpixe
gfx_fill_rect(dpi, fillRect, ColourMapA[w->colours[1]].lighter | 0x1000000);
}
if (command.Type == TITLE_SCRIPT_LOAD && command.SaveIndex == SAVE_INDEX_INVALID)
error = true;
auto ft = Formatter();
rct_string_id commandName = STR_NONE;
if (error)
{
ft.Add<rct_string_id>(selected || hover ? STR_LIGHTPINK_STRINGID : STR_RED_STRINGID);
}
else
{
ft.Add<rct_string_id>(selected || hover ? STR_STRINGID : STR_BLACK_STRING);
}
switch (command.Type)
{
case TITLE_SCRIPT_LOAD:
commandName = STR_TITLE_EDITOR_COMMAND_LOAD_FILE;
{
auto commandName = STR_TITLE_EDITOR_COMMAND_LOAD_FILE;
if (command.SaveIndex == SAVE_INDEX_INVALID)
{
commandName = STR_TITLE_EDITOR_COMMAND_LOAD_NO_SAVE;
error = true;
ft.Add<rct_string_id>(commandName);
}
else
{
ft.Add<rct_string_id>(commandName);
ft.Add<const char*>(_editingTitleSequence->Saves[command.SaveIndex].c_str());
}
break;
}
case TITLE_SCRIPT_LOCATION:
commandName = STR_TITLE_EDITOR_COMMAND_LOCATION;
{
auto commandName = STR_TITLE_EDITOR_COMMAND_LOCATION;
ft.Add<rct_string_id>(commandName);
ft.Add<uint16_t>(command.X);
ft.Add<uint16_t>(command.Y);
break;
}
case TITLE_SCRIPT_ROTATE:
commandName = STR_TITLE_EDITOR_COMMAND_ROTATE;
{
auto commandName = STR_TITLE_EDITOR_COMMAND_ROTATE;
ft.Add<rct_string_id>(commandName);
ft.Add<uint16_t>(command.Rotations);
break;
}
case TITLE_SCRIPT_ZOOM:
commandName = STR_TITLE_EDITOR_COMMAND_ZOOM;
{
auto commandName = STR_TITLE_EDITOR_COMMAND_ZOOM;
ft.Add<rct_string_id>(commandName);
ft.Add<uint16_t>(command.Zoom);
break;
}
case TITLE_SCRIPT_SPEED:
commandName = STR_TITLE_EDITOR_COMMAND_SPEED;
{
auto commandName = STR_TITLE_EDITOR_COMMAND_SPEED;
ft.Add<rct_string_id>(commandName);
ft.Add<rct_string_id>(SpeedNames[command.Speed - 1]);
break;
}
case TITLE_SCRIPT_FOLLOW:
commandName = STR_TITLE_EDITOR_COMMAND_FOLLOW;
{
auto commandName = STR_TITLE_EDITOR_COMMAND_FOLLOW;
if (command.SpriteIndex == SPRITE_INDEX_NULL)
{
commandName = STR_TITLE_EDITOR_COMMAND_FOLLOW_NO_SPRITE;
ft.Add<rct_string_id>(commandName);
}
else
{
ft.Add<rct_string_id>(commandName);
ft.Add<utf8*>(command.SpriteName);
}
break;
}
case TITLE_SCRIPT_WAIT:
commandName = STR_TITLE_EDITOR_COMMAND_WAIT;
{
auto commandName = STR_TITLE_EDITOR_COMMAND_WAIT;
ft.Add<rct_string_id>(commandName);
ft.Add<uint16_t>(command.Milliseconds);
break;
}
case TITLE_SCRIPT_RESTART:
commandName = STR_TITLE_EDITOR_RESTART;
{
auto commandName = STR_TITLE_EDITOR_RESTART;
ft.Add<rct_string_id>(commandName);
break;
}
case TITLE_SCRIPT_END:
commandName = STR_TITLE_EDITOR_END;
{
auto commandName = STR_TITLE_EDITOR_END;
ft.Add<rct_string_id>(commandName);
break;
}
case TITLE_SCRIPT_LOADSC:
{
commandName = STR_TITLE_EDITOR_COMMAND_LOAD_FILE;
auto commandName = STR_TITLE_EDITOR_COMMAND_LOAD_FILE;
const char* name = "";
auto scenario = GetScenarioRepository()->GetByInternalName(command.Scenario);
if (command.Scenario[0] == '\0')
@ -995,26 +1035,17 @@ static void window_title_editor_scrollpaint_commands(rct_window* w, rct_drawpixe
{
commandName = STR_TITLE_EDITOR_COMMAND_LOAD_MISSING_SCENARIO;
}
ft.Add<rct_string_id>(commandName);
ft.Add<const char*>(name);
break;
}
default:
{
ft.Add<rct_string_id>(STR_NONE);
log_warning("Unknown command %d", command.Type);
}
}
char buffer[256];
if ((selected || hover) && !error)
{
format_string(buffer, 256, commandName, ft.Data());
}
else
{
format_string(buffer + 1, 255, commandName, ft.Data());
buffer[0] = static_cast<utf8>(error ? ((selected || hover) ? FORMAT_LIGHTPINK : FORMAT_RED) : FORMAT_BLACK);
}
ft = Formatter();
ft.Add<const char*>(&buffer);
gfx_draw_string_left(dpi, STR_STRING, ft.Data(), w->colours[1], screenCoords + ScreenCoordsXY{ 5, 0 });
gfx_draw_string_left(dpi, STR_STRINGID, ft.Data(), w->colours[1], screenCoords + ScreenCoordsXY{ 5, 0 });
}
}

View File

@ -126,6 +126,23 @@ namespace String
#endif
}
std::string_view ToStringView(const char* ch, size_t maxLen)
{
size_t len{};
for (size_t i = 0; i < maxLen; i++)
{
if (ch[i] == '\0')
{
break;
}
else
{
len++;
}
}
return std::string_view(ch, len);
}
bool IsNullOrEmpty(const utf8* str)
{
return str == nullptr || str[0] == '\0';
@ -154,6 +171,32 @@ namespace String
}
}
bool Equals(const std::string_view a, const std::string_view b, bool ignoreCase)
{
if (ignoreCase)
{
if (a.size() == b.size())
{
for (size_t i = 0; i < a.size(); i++)
{
if (tolower(a[i]) != tolower(b[i]))
{
return false;
}
}
return true;
}
else
{
return false;
}
}
else
{
return a == b;
}
}
bool Equals(const std::string& a, const std::string& b, bool ignoreCase)
{
return Equals(a.c_str(), b.c_str(), ignoreCase);
@ -483,6 +526,13 @@ namespace String
return utf8_write_codepoint(dst, codepoint);
}
void AppendCodepoint(std::string& str, codepoint_t codepoint)
{
char buffer[8]{};
utf8_write_codepoint(buffer, codepoint);
str.append(buffer);
}
bool IsWhiteSpace(codepoint_t codepoint)
{
// 0x3000 is the 'ideographic space', a 'fullwidth' character used in CJK languages.
@ -745,16 +795,9 @@ namespace String
return res;
#endif
}
bool ContainsColourCode(const std::string& string)
{
for (unsigned char c : string)
{
if (c >= FORMAT_COLOUR_CODE_START && c <= FORMAT_COLOUR_CODE_END)
{
return true;
}
}
return false;
}
} // namespace String
char32_t CodepointView::iterator::GetNextCodepoint(const char* ch, const char** next)
{
return utf8_get_next(ch, next);
}

View File

@ -39,9 +39,16 @@ namespace String
std::string ToUtf8(const std::wstring_view& src);
std::wstring ToWideChar(const std::string_view& src);
/**
* Creates a string_view from a char pointer with a length up to either the
* first null terminator or a given maximum length, whatever is smallest.
*/
std::string_view ToStringView(const char* ch, size_t maxLen);
bool IsNullOrEmpty(const utf8* str);
int32_t Compare(const std::string& a, const std::string& b, bool ignoreCase = false);
int32_t Compare(const utf8* a, const utf8* b, bool ignoreCase = false);
bool Equals(const std::string_view a, const std::string_view b, bool ignoreCase = false);
bool Equals(const std::string& a, const std::string& b, bool ignoreCase = false);
bool Equals(const utf8* a, const utf8* b, bool ignoreCase = false);
bool StartsWith(const utf8* str, const utf8* match, bool ignoreCase = false);
@ -93,6 +100,7 @@ namespace String
codepoint_t GetNextCodepoint(utf8* ptr, utf8** nextPtr = nullptr);
codepoint_t GetNextCodepoint(const utf8* ptr, const utf8** nextPtr = nullptr);
utf8* WriteCodepoint(utf8* dst, codepoint_t codepoint);
void AppendCodepoint(std::string& str, codepoint_t codepoint);
bool IsWhiteSpace(codepoint_t codepoint);
utf8* Trim(utf8* str);
@ -110,10 +118,81 @@ namespace String
* Returns an uppercased version of a UTF-8 string.
*/
std::string ToUpper(const std::string_view& src);
/**
* Returns true if the string contains an RCT2 colour code.
*/
bool ContainsColourCode(const std::string& string);
} // namespace String
class CodepointView
{
private:
std::string_view _str;
public:
class iterator
{
private:
std::string_view _str;
size_t _index;
public:
iterator(std::string_view str, size_t index)
: _str(str)
, _index(index)
{
}
bool operator==(const iterator& rhs) const
{
return _index == rhs._index;
}
bool operator!=(const iterator& rhs) const
{
return _index != rhs._index;
}
char32_t operator*() const
{
return GetNextCodepoint(&_str[_index], nullptr);
}
iterator& operator++()
{
if (_index < _str.size())
{
const utf8* nextch;
GetNextCodepoint(&_str[_index], &nextch);
_index = nextch - _str.data();
}
return *this;
}
iterator operator++(int)
{
auto result = *this;
if (_index < _str.size())
{
const utf8* nextch;
GetNextCodepoint(&_str[_index], &nextch);
_index = nextch - _str.data();
}
return result;
}
size_t GetIndex() const
{
return _index;
}
static char32_t GetNextCodepoint(const char* ch, const char** next);
};
CodepointView(std::string_view str)
: _str(str)
{
}
iterator begin() const
{
return iterator(_str, 0);
}
iterator end() const
{
return iterator(_str, _str.size());
}
};

View File

@ -9,8 +9,10 @@
#include "../common.h"
#include "../config/Config.h"
#include "../core/String.hpp"
#include "../drawing/Drawing.h"
#include "../interface/Viewport.h"
#include "../localisation/Formatting.h"
#include "../localisation/Localisation.h"
#include "../localisation/LocalisationService.h"
#include "../platform/platform.h"
@ -20,47 +22,54 @@
#include <algorithm>
using namespace OpenRCT2;
enum : uint32_t
{
TEXT_DRAW_FLAG_INSET = 1 << 0,
TEXT_DRAW_FLAG_OUTLINE = 1 << 1,
TEXT_DRAW_FLAG_DARK = 1 << 2,
TEXT_DRAW_FLAG_EXTRA_DARK = 1 << 3,
TEXT_DRAW_FLAG_NO_FORMATTING = 1 << 28,
TEXT_DRAW_FLAG_Y_OFFSET_EFFECT = 1 << 29,
TEXT_DRAW_FLAG_TTF = 1 << 30,
TEXT_DRAW_FLAG_NO_DRAW = 1u << 31
};
static int32_t ttf_get_string_width(const utf8* text);
static int32_t ttf_get_string_width(std::string_view text, bool noFormatting);
/**
*
* rct2: 0x006C23B1
*/
int32_t gfx_get_string_width_new_lined(utf8* text)
int32_t gfx_get_string_width_new_lined(std::string_view text)
{
utf8* ch = text;
utf8* firstCh = text;
utf8* nextCh;
utf8 backup;
int32_t codepoint;
thread_local std::string buffer;
buffer.clear();
int32_t maxWidth = 0;
while ((codepoint = utf8_get_next(ch, const_cast<const utf8**>(&nextCh))) != 0)
std::optional<int32_t> maxWidth;
FmtString fmt(text);
for (const auto& token : fmt)
{
if (codepoint == FORMAT_NEWLINE || codepoint == FORMAT_NEWLINE_SMALLER)
if (token.kind == FormatToken::Newline || token.kind == FormatToken::NewlineSmall)
{
backup = *nextCh;
*nextCh = 0;
maxWidth = std::max(maxWidth, gfx_get_string_width(firstCh));
*nextCh = backup;
firstCh = nextCh;
auto width = gfx_get_string_width(buffer);
if (!maxWidth || maxWidth > width)
{
maxWidth = width;
}
buffer.clear();
}
else
{
buffer.append(token.text);
}
ch = nextCh;
}
maxWidth = std::max(maxWidth, gfx_get_string_width(firstCh));
return maxWidth;
if (!maxWidth)
{
maxWidth = gfx_get_string_width(buffer);
}
return *maxWidth;
}
/**
@ -69,9 +78,14 @@ int32_t gfx_get_string_width_new_lined(utf8* text)
* rct2: 0x006C2321
* buffer (esi)
*/
int32_t gfx_get_string_width(const utf8* buffer)
int32_t gfx_get_string_width(std::string_view text)
{
return ttf_get_string_width(buffer);
return ttf_get_string_width(text, false);
}
int32_t gfx_get_string_width_no_formatting(std::string_view text)
{
return ttf_get_string_width(text, true);
}
/**
@ -83,65 +97,62 @@ int32_t gfx_get_string_width(const utf8* buffer)
*/
int32_t gfx_clip_string(utf8* text, int32_t width)
{
int32_t clippedWidth;
if (width < 6)
{
*text = 0;
return 0;
}
clippedWidth = gfx_get_string_width(text);
// If width of the full string is less than allowed width then we don't need to clip
auto clippedWidth = gfx_get_string_width(text);
if (clippedWidth <= width)
{
return clippedWidth;
}
utf8 backup[4];
utf8* ch = text;
utf8* nextCh = text;
utf8* clipCh = text;
int32_t codepoint;
while ((codepoint = utf8_get_next(ch, const_cast<const utf8**>(&nextCh))) != 0)
// Append each character 1 by 1 with an ellipsis on the end until width is exceeded
thread_local std::string buffer;
buffer.clear();
size_t bestLength = 0;
int32_t bestWidth = 0;
FmtString fmt(text);
for (const auto& token : fmt)
{
if (utf8_is_format_code(codepoint))
CodepointView codepoints(token.text);
for (auto codepoint : codepoints)
{
ch = nextCh;
ch += utf8_get_format_code_arg_length(codepoint);
continue;
}
// Add the ellipsis before checking the width
buffer.append("...");
for (int32_t i = 0; i < 4; i++)
{
backup[i] = nextCh[i];
};
for (int32_t i = 0; i < 3; i++)
{
nextCh[i] = '.';
}
nextCh[3] = 0;
int32_t queryWidth = gfx_get_string_width(text);
if (queryWidth < width)
{
clipCh = nextCh;
clippedWidth = queryWidth;
}
else
{
for (int32_t i = 0; i < 3; i++)
auto currentWidth = gfx_get_string_width(buffer);
if (currentWidth < width)
{
clipCh[i] = '.';
}
clipCh[3] = 0;
return clippedWidth;
}
bestLength = buffer.size();
bestWidth = currentWidth;
for (int32_t i = 0; i < 4; i++)
{
nextCh[i] = backup[i];
};
ch = nextCh;
// Trim the ellipsis
buffer.resize(bestLength - 3);
}
else
{
// Width exceeded, rollback to best length and put ellipsis back
buffer.resize(bestLength);
for (auto i = static_cast<int32_t>(bestLength) - 1; i >= 0 && i >= static_cast<int32_t>(bestLength) - 3; i--)
{
buffer[i] = '.';
}
// Copy buffer back to input text buffer
std::strcpy(text, buffer.c_str());
return bestWidth;
}
char cb[8]{};
utf8_write_codepoint(cb, codepoint);
buffer.append(cb);
}
}
return gfx_get_string_width(text);
}
@ -161,84 +172,95 @@ int32_t gfx_clip_string(utf8* text, int32_t width)
*/
int32_t gfx_wrap_string(utf8* text, int32_t width, int32_t* outNumLines, int32_t* outFontHeight)
{
int32_t lineWidth = 0;
constexpr size_t NULL_INDEX = std::numeric_limits<size_t>::max();
thread_local std::string buffer;
buffer.resize(0);
size_t currentLineIndex = 0;
size_t splitIndex = NULL_INDEX;
size_t bestSplitIndex = NULL_INDEX;
size_t numLines = 0;
int32_t maxWidth = 0;
*outNumLines = 0;
// Pointer to the start of the current word
utf8* currentWord = nullptr;
// Width of line up to current word
int32_t currentWidth = 0;
utf8* ch = text;
utf8* firstCh = text;
utf8* nextCh;
int32_t codepoint;
int32_t numCharactersOnLine = 0;
while ((codepoint = utf8_get_next(ch, const_cast<const utf8**>(&nextCh))) != 0)
FmtString fmt = text;
for (const auto& token : fmt)
{
if (codepoint == ' ')
if (token.IsLiteral())
{
currentWord = ch;
currentWidth = lineWidth;
numCharactersOnLine++;
}
else if (codepoint == FORMAT_NEWLINE)
{
*ch++ = 0;
maxWidth = std::max(maxWidth, lineWidth);
(*outNumLines)++;
lineWidth = 0;
currentWord = nullptr;
firstCh = ch;
numCharactersOnLine = 0;
continue;
}
else if (utf8_is_format_code(codepoint))
{
ch = nextCh;
ch += utf8_get_format_code_arg_length(codepoint);
continue;
}
CodepointView codepoints(token.text);
for (auto codepoint : codepoints)
{
char cb[8]{};
utf8_write_codepoint(cb, codepoint);
buffer.append(cb);
uint8_t saveCh = *nextCh;
*nextCh = 0;
lineWidth = gfx_get_string_width(firstCh);
*nextCh = saveCh;
auto lineWidth = gfx_get_string_width(&buffer[currentLineIndex]);
if (lineWidth <= width || (splitIndex == NULL_INDEX && bestSplitIndex == NULL_INDEX))
{
if (codepoint == ' ')
{
// Mark line split here
splitIndex = buffer.size() - 1;
}
else if (splitIndex == NULL_INDEX)
{
// Mark line split here (this is after first character of line)
bestSplitIndex = buffer.size();
}
}
else
{
// Insert new line before current word
if (splitIndex == NULL_INDEX)
{
splitIndex = bestSplitIndex;
}
buffer.insert(buffer.begin() + splitIndex, '\0');
if (lineWidth <= width || numCharactersOnLine == 0)
{
ch = nextCh;
numCharactersOnLine++;
// Recalculate the line length after splitting
lineWidth = gfx_get_string_width(&buffer[currentLineIndex]);
maxWidth = std::max(maxWidth, lineWidth);
numLines++;
currentLineIndex = splitIndex + 1;
splitIndex = NULL_INDEX;
bestSplitIndex = NULL_INDEX;
// Trim the beginning of the new line
while (buffer[currentLineIndex] == ' ')
{
buffer.erase(buffer.begin() + currentLineIndex);
}
}
}
}
else if (currentWord == nullptr)
else if (token.kind == FormatToken::Newline)
{
// Single word is longer than line, insert null terminator
ch += utf8_insert_codepoint(ch, 0);
buffer.push_back('\0');
auto lineWidth = gfx_get_string_width(&buffer[currentLineIndex]);
maxWidth = std::max(maxWidth, lineWidth);
(*outNumLines)++;
lineWidth = 0;
currentWord = nullptr;
firstCh = ch;
numCharactersOnLine = 0;
numLines++;
currentLineIndex = buffer.size();
splitIndex = NULL_INDEX;
bestSplitIndex = NULL_INDEX;
}
else
{
ch = currentWord;
*ch++ = 0;
maxWidth = std::max(maxWidth, currentWidth);
(*outNumLines)++;
lineWidth = 0;
currentWord = nullptr;
firstCh = ch;
numCharactersOnLine = 0;
buffer.append(token.text);
}
}
maxWidth = std::max(maxWidth, lineWidth);
{
// Final line width calculation
auto lineWidth = gfx_get_string_width(&buffer[currentLineIndex]);
maxWidth = std::max(maxWidth, lineWidth);
}
std::memcpy(text, buffer.data(), buffer.size() + 1);
*outNumLines = static_cast<int32_t>(numLines);
*outFontHeight = gCurrentFontSpriteBase;
return maxWidth == 0 ? lineWidth : maxWidth;
return maxWidth;
}
/**
@ -345,13 +367,12 @@ int32_t string_get_height_raw(char* buffer)
else if (fontBase == FONT_SPRITE_BASE_TINY)
height += 6;
char* ch = buffer;
while (*ch != 0)
FmtString fmt(buffer);
for (const auto& token : fmt)
{
char c = *ch++;
switch (c)
switch (token.kind)
{
case FORMAT_NEWLINE:
case FormatToken::Newline:
if (fontBase == FONT_SPRITE_BASE_SMALL || fontBase == FONT_SPRITE_BASE_MEDIUM)
{
height += 10;
@ -364,7 +385,7 @@ int32_t string_get_height_raw(char* buffer)
}
height += 18;
break;
case FORMAT_NEWLINE_SMALLER:
case FormatToken::NewlineSmall:
if (fontBase == FONT_SPRITE_BASE_SMALL || fontBase == FONT_SPRITE_BASE_MEDIUM)
{
height += 5;
@ -377,33 +398,19 @@ int32_t string_get_height_raw(char* buffer)
}
height += 9;
break;
case FORMAT_TINYFONT:
case FormatToken::FontTiny:
fontBase = FONT_SPRITE_BASE_TINY;
break;
case FORMAT_MEDIUMFONT:
case FormatToken::FontMedium:
fontBase = FONT_SPRITE_BASE_MEDIUM;
break;
case FORMAT_SMALLFONT:
case FormatToken::FontSmall:
fontBase = FONT_SPRITE_BASE_SMALL;
break;
default:
if (c >= 32)
continue;
if (c <= 4)
{
ch++;
continue;
}
if (c <= 16)
continue;
ch += 2;
if (c <= 22)
continue;
ch += 2;
break;
}
}
return height;
}
@ -445,21 +452,27 @@ void gfx_draw_string_centred_wrapped_partial(
{
int32_t halfWidth = gfx_get_string_width(buffer) / 2;
utf8* ch = buffer;
utf8* nextCh;
int32_t codepoint;
while ((codepoint = utf8_get_next(ch, const_cast<const utf8**>(&nextCh))) != 0)
FmtString fmt(buffer);
for (const auto& token : fmt)
{
if (!utf8_is_format_code(codepoint))
bool doubleBreak = false;
if (token.IsLiteral())
{
numCharactersDrawn++;
if (numCharactersDrawn > numCharactersToDraw)
CodepointView codepoints(token.text);
for (auto it = codepoints.begin(); it != codepoints.end(); it++)
{
*ch = 0;
break;
numCharactersDrawn++;
if (numCharactersDrawn > numCharactersToDraw)
{
auto ch = const_cast<char*>(&token.text[it.GetIndex()]);
*ch = '\0';
doubleBreak = true;
break;
}
}
}
ch = nextCh;
if (doubleBreak)
break;
}
screenCoords = { coords.x - halfWidth, lineY };
@ -509,20 +522,18 @@ static void ttf_draw_character_sprite(rct_drawpixelinfo* dpi, int32_t codepoint,
info->x += characterWidth;
}
static void ttf_draw_string_raw_sprite(rct_drawpixelinfo* dpi, const utf8* text, text_draw_info* info)
static void ttf_draw_string_raw_sprite(rct_drawpixelinfo* dpi, const std::string_view text, text_draw_info* info)
{
const utf8* ch = text;
int32_t codepoint;
while (!utf8_is_format_code(codepoint = utf8_get_next(ch, &ch)))
CodepointView codepoints(text);
for (auto codepoint : codepoints)
{
ttf_draw_character_sprite(dpi, codepoint, info);
};
}
}
#ifndef NO_TTF
static void ttf_draw_string_raw_ttf(rct_drawpixelinfo* dpi, const utf8* text, text_draw_info* info)
static void ttf_draw_string_raw_ttf(rct_drawpixelinfo* dpi, std::string_view text, text_draw_info* info)
{
if (!ttf_initialise())
return;
@ -670,201 +681,191 @@ static void ttf_draw_string_raw_ttf(rct_drawpixelinfo* dpi, const utf8* text, te
#endif // NO_TTF
static void ttf_draw_string_raw(rct_drawpixelinfo* dpi, const utf8* text, text_draw_info* info)
static void ttf_process_format_code(rct_drawpixelinfo* dpi, const FmtString::token& token, text_draw_info* info)
{
#ifndef NO_TTF
if (info->flags & TEXT_DRAW_FLAG_TTF)
switch (token.kind)
{
ttf_draw_string_raw_ttf(dpi, text, info);
}
else
{
#endif // NO_TTF
ttf_draw_string_raw_sprite(dpi, text, info);
#ifndef NO_TTF
}
#endif // NO_TTF
}
static const utf8* ttf_process_format_code(rct_drawpixelinfo* dpi, const utf8* text, text_draw_info* info)
{
const utf8* nextCh;
int32_t codepoint;
codepoint = utf8_get_next(text, &nextCh);
switch (codepoint)
{
case FORMAT_MOVE_X:
info->x = info->startX + static_cast<uint8_t>(*nextCh++);
case FormatToken::Move:
info->x = info->startX + token.parameter;
break;
case FORMAT_ADJUST_PALETTE:
{
auto paletteMapId = static_cast<colour_t>(*nextCh++);
auto paletteMap = GetPaletteMapForColour(paletteMapId);
if (paletteMap)
{
uint32_t c = (*paletteMap)[249] + 256;
if (!(info->flags & TEXT_DRAW_FLAG_OUTLINE))
{
c &= 0xFF;
}
info->palette[1] = c & 0xFF;
info->palette[2] = (c >> 8) & 0xFF;
// Adjust the text palette
info->palette[3] = (*paletteMap)[247];
info->palette[4] = (*paletteMap)[248];
info->palette[5] = (*paletteMap)[250];
info->palette[6] = (*paletteMap)[251];
}
break;
}
case FORMAT_3:
case FORMAT_4:
nextCh++;
break;
case FORMAT_NEWLINE:
case FormatToken::Newline:
info->x = info->startX;
info->y += font_get_line_height(info->font_sprite_base);
break;
case FORMAT_NEWLINE_SMALLER:
case FormatToken::NewlineSmall:
info->x = info->startX;
info->y += font_get_line_height_small(info->font_sprite_base);
break;
case FORMAT_TINYFONT:
case FormatToken::FontTiny:
info->font_sprite_base = FONT_SPRITE_BASE_TINY;
break;
case FORMAT_SMALLFONT:
case FormatToken::FontSmall:
info->font_sprite_base = FONT_SPRITE_BASE_SMALL;
break;
case FORMAT_MEDIUMFONT:
case FormatToken::FontMedium:
info->font_sprite_base = FONT_SPRITE_BASE_MEDIUM;
break;
case FORMAT_OUTLINE:
case FormatToken::OutlineEnable:
info->flags |= TEXT_DRAW_FLAG_OUTLINE;
break;
case FORMAT_OUTLINE_OFF:
case FormatToken::OutlineDisable:
info->flags &= ~TEXT_DRAW_FLAG_OUTLINE;
break;
case FORMAT_WINDOW_COLOUR_1:
case FormatToken::ColourWindow1:
{
uint16_t flags = info->flags;
colour_char_window(gCurrentWindowColours[0], &flags, info->palette);
break;
}
case FORMAT_WINDOW_COLOUR_2:
case FormatToken::ColourWindow2:
{
uint16_t flags = info->flags;
colour_char_window(gCurrentWindowColours[1], &flags, info->palette);
break;
}
case FORMAT_WINDOW_COLOUR_3:
case FormatToken::ColourWindow3:
{
uint16_t flags = info->flags;
colour_char_window(gCurrentWindowColours[2], &flags, info->palette);
break;
}
case FORMAT_16:
break;
case FORMAT_INLINE_SPRITE:
case FormatToken::InlineSprite:
{
uint32_t imageId;
std::memcpy(&imageId, nextCh, sizeof(uint32_t));
const rct_g1_element* g1 = gfx_get_g1_element(imageId & 0x7FFFF);
auto g1 = gfx_get_g1_element(token.parameter & 0x7FFFF);
if (g1 != nullptr)
{
if (!(info->flags & TEXT_DRAW_FLAG_NO_DRAW))
{
gfx_draw_sprite(dpi, imageId, { info->x, info->y }, 0);
gfx_draw_sprite(dpi, token.parameter, { info->x, info->y }, 0);
}
info->x += g1->width;
}
nextCh += 4;
break;
}
default:
if (codepoint >= FORMAT_COLOUR_CODE_START && codepoint <= FORMAT_COLOUR_CODE_END)
if (FormatTokenIsColour(token.kind))
{
uint16_t flags = info->flags;
colour_char(codepoint - FORMAT_COLOUR_CODE_START, &flags, info->palette);
auto colourIndex = FormatTokenGetTextColourIndex(token.kind);
colour_char(static_cast<uint8_t>(colourIndex), &flags, info->palette);
}
else if (codepoint <= 0x16)
{ // case 0x11? FORMAT_NEW_LINE_X_Y
nextCh += 2;
break;
}
}
static bool ShouldUseSpriteForCodepoint(char32_t codepoint)
{
switch (codepoint)
{
case UnicodeChar::up:
case UnicodeChar::down:
case UnicodeChar::leftguillemet:
case UnicodeChar::tick:
case UnicodeChar::cross:
case UnicodeChar::right:
case UnicodeChar::rightguillemet:
case UnicodeChar::small_up:
case UnicodeChar::small_down:
case UnicodeChar::left:
case UnicodeChar::quote_open:
case UnicodeChar::quote_close:
case UnicodeChar::german_quote_open:
case UnicodeChar::plus:
case UnicodeChar::minus:
return true;
default:
return false;
}
}
static void ttf_process_string_literal(rct_drawpixelinfo* dpi, const std::string_view text, text_draw_info* info)
{
#ifndef NO_TTF
bool isTTF = info->flags & TEXT_DRAW_FLAG_TTF;
#else
bool isTTF = false;
#endif // NO_TTF
if (!isTTF)
{
ttf_draw_string_raw_sprite(dpi, text, info);
}
#ifndef NO_TTF
else
{
CodepointView codepoints(text);
std::optional<size_t> ttfRunIndex;
for (auto it = codepoints.begin(); it != codepoints.end(); it++)
{
auto codepoint = *it;
if (ShouldUseSpriteForCodepoint(codepoint))
{
if (ttfRunIndex)
{
// Draw the TTF run
auto len = it.GetIndex() - *ttfRunIndex;
ttf_draw_string_raw_ttf(dpi, text.substr(*ttfRunIndex, len), info);
ttfRunIndex = std::nullopt;
}
// Draw the sprite font glyph
ttf_draw_character_sprite(dpi, codepoint, info);
}
else
{
nextCh += 4; // never happens?
if (!ttfRunIndex)
{
ttfRunIndex = it.GetIndex();
}
}
break;
}
if (ttfRunIndex)
{
// Final TTF run
auto len = text.size() - *ttfRunIndex;
ttf_draw_string_raw_ttf(dpi, text.substr(*ttfRunIndex, len), info);
}
}
return nextCh;
#endif // NO_TTF
}
static const utf8* ttf_process_glyph_run(rct_drawpixelinfo* dpi, const utf8* text, text_draw_info* info)
static void ttf_process_string_codepoint(rct_drawpixelinfo* dpi, codepoint_t codepoint, text_draw_info* info)
{
utf8 buffer[512];
const utf8* ch = text;
const utf8* lastCh;
int32_t codepoint;
char buffer[8]{};
utf8_write_codepoint(buffer, codepoint);
ttf_process_string_literal(dpi, buffer, info);
}
#ifndef NO_TTF
bool isTTF = info->flags & TEXT_DRAW_FLAG_TTF;
#else
bool isTTF = false;
#endif // NO_TTF
while (!utf8_is_format_code(codepoint = utf8_get_next(ch, &lastCh)))
static void ttf_process_string(rct_drawpixelinfo* dpi, std::string_view text, text_draw_info* info)
{
if (info->flags & TEXT_DRAW_FLAG_NO_FORMATTING)
{
if (isTTF && utf8_should_use_sprite_for_codepoint(codepoint))
{
break;
}
ch = lastCh;
}
if (codepoint == 0)
{
ttf_draw_string_raw(dpi, text, info);
return ch;
ttf_process_string_literal(dpi, text, info);
info->maxX = std::max(info->maxX, info->x);
info->maxY = std::max(info->maxY, info->y);
}
else
{
size_t length = static_cast<size_t>(ch - text);
std::memcpy(buffer, text, length);
buffer[length] = 0;
ttf_draw_string_raw(dpi, buffer, info);
return ch;
}
}
static void ttf_process_string(rct_drawpixelinfo* dpi, const utf8* text, text_draw_info* info)
{
const utf8* ch = text;
const utf8* nextCh;
int32_t codepoint;
#ifndef NO_TTF
bool isTTF = info->flags & TEXT_DRAW_FLAG_TTF;
#else
bool isTTF = false;
#endif // NO_TTF
while ((codepoint = utf8_get_next(ch, &nextCh)) != 0)
{
if (utf8_is_format_code(codepoint))
FmtString fmt(text);
for (const auto& token : fmt)
{
ch = ttf_process_format_code(dpi, ch, info);
if (token.IsLiteral())
{
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);
}
info->maxX = std::max(info->maxX, info->x);
info->maxY = std::max(info->maxY, info->y);
}
else if (isTTF && utf8_should_use_sprite_for_codepoint(codepoint))
{
ttf_draw_character_sprite(dpi, codepoint, info);
ch = nextCh;
}
else
{
ch = ttf_process_glyph_run(dpi, ch, info);
}
info->maxX = std::max(info->maxX, info->x);
info->maxY = std::max(info->maxY, info->y);
}
}
@ -933,7 +934,8 @@ static void ttf_process_initial_colour(int32_t colour, text_draw_info* info)
}
}
void ttf_draw_string(rct_drawpixelinfo* dpi, const_utf8string text, int32_t colour, const ScreenCoordsXY& coords)
void ttf_draw_string(
rct_drawpixelinfo* dpi, const_utf8string text, int32_t colour, const ScreenCoordsXY& coords, bool noFormatting)
{
if (text == nullptr)
return;
@ -951,6 +953,11 @@ void ttf_draw_string(rct_drawpixelinfo* dpi, const_utf8string text, int32_t colo
info.flags |= TEXT_DRAW_FLAG_TTF;
}
if (noFormatting)
{
info.flags |= TEXT_DRAW_FLAG_NO_FORMATTING;
}
std::memcpy(info.palette, text_palette, sizeof(info.palette));
ttf_process_initial_colour(colour, &info);
ttf_process_string(dpi, text, &info);
@ -963,7 +970,7 @@ void ttf_draw_string(rct_drawpixelinfo* dpi, const_utf8string text, int32_t colo
gLastDrawStringY = info.y;
}
static int32_t ttf_get_string_width(const utf8* text)
static int32_t ttf_get_string_width(std::string_view text, bool noFormatting)
{
text_draw_info info;
info.font_sprite_base = gCurrentFontSpriteBase;
@ -981,6 +988,11 @@ static int32_t ttf_get_string_width(const utf8* text)
info.flags |= TEXT_DRAW_FLAG_TTF;
}
if (noFormatting)
{
info.flags |= TEXT_DRAW_FLAG_NO_FORMATTING;
}
ttf_process_string(nullptr, text, &info);
return info.maxX;

View File

@ -733,6 +733,8 @@ void FASTCALL gfx_draw_sprite_raw_masked_software(
// string
void gfx_draw_string(rct_drawpixelinfo* dpi, const_utf8string buffer, uint8_t colour, const ScreenCoordsXY& coords);
void gfx_draw_string_no_formatting(
rct_drawpixelinfo* dpi, const_utf8string buffer, uint8_t colour, const ScreenCoordsXY& coords);
/** @deprecated */
void gfx_draw_string_left(
@ -757,12 +759,14 @@ void gfx_draw_string_with_y_offsets(
bool forceSpriteFont);
int32_t gfx_wrap_string(char* buffer, int32_t width, int32_t* num_lines, int32_t* font_height);
int32_t gfx_get_string_width(const utf8* buffer);
int32_t gfx_get_string_width_new_lined(char* buffer);
int32_t gfx_get_string_width(std::string_view text);
int32_t gfx_get_string_width_new_lined(std::string_view text);
int32_t gfx_get_string_width_no_formatting(std::string_view text);
int32_t string_get_height_raw(char* buffer);
int32_t gfx_clip_string(char* buffer, int32_t width);
void shorten_path(utf8* buffer, size_t bufferSize, const utf8* path, int32_t availableWidth);
void ttf_draw_string(rct_drawpixelinfo* dpi, const_utf8string text, int32_t colour, const ScreenCoordsXY& coords);
void ttf_draw_string(
rct_drawpixelinfo* dpi, const_utf8string text, int32_t colour, const ScreenCoordsXY& coords, bool noFormatting);
// scrolling text
void scrolling_text_initialise_bitmaps();

View File

@ -240,10 +240,7 @@ void font_sprite_initialise_characters()
int32_t width = 0;
if (g1 != nullptr)
{
if (glyphIndex < (FORMAT_ARGUMENT_CODE_START - 32) || glyphIndex >= (FORMAT_COLOUR_CODE_END - 32))
{
width = (g1->width + 2 * g1->x_offset) - 1;
}
width = g1->width + (2 * g1->x_offset) - 1;
}
_spriteFontCharacterWidths[fontSize][glyphIndex] = static_cast<uint8_t>(width);

View File

@ -8,7 +8,9 @@
*****************************************************************************/
#include "../config/Config.h"
#include "../core/String.hpp"
#include "../interface/Colour.h"
#include "../localisation/Formatting.h"
#include "../localisation/Localisation.h"
#include "../localisation/LocalisationService.h"
#include "../paint/Paint.h"
@ -19,6 +21,8 @@
#include <algorithm>
#include <mutex>
using namespace OpenRCT2;
struct rct_draw_scroll_text
{
rct_string_id string_id;
@ -38,9 +42,9 @@ static uint32_t _drawSCrollNextIndex = 0;
static std::mutex _scrollingTextMutex;
static void scrolling_text_set_bitmap_for_sprite(
utf8* text, int32_t scroll, uint8_t* bitmap, const int16_t* scrollPositionOffsets, colour_t colour);
std::string_view text, int32_t scroll, uint8_t* bitmap, const int16_t* scrollPositionOffsets, colour_t colour);
static void scrolling_text_set_bitmap_for_ttf(
utf8* text, int32_t scroll, uint8_t* bitmap, const int16_t* scrollPositionOffsets, colour_t colour);
std::string_view text, int32_t scroll, uint8_t* bitmap, const int16_t* scrollPositionOffsets, colour_t colour);
void scrolling_text_initialise_bitmaps()
{
@ -1490,105 +1494,98 @@ int32_t scrolling_text_setup(
}
static void scrolling_text_set_bitmap_for_sprite(
utf8* text, int32_t scroll, uint8_t* bitmap, const int16_t* scrollPositionOffsets, colour_t colour)
std::string_view text, int32_t scroll, uint8_t* bitmap, const int16_t* scrollPositionOffsets, colour_t colour)
{
auto characterColour = colour;
auto fmt = FmtString(text);
utf8* ch = text;
while (true)
// Repeat string a maximum of four times (eliminates possibility of infinite loop)
for (auto i = 0; i < 4; i++)
{
uint32_t codepoint = utf8_get_next(ch, const_cast<const utf8**>(&ch));
// If at the end of the string loop back to the start
if (codepoint == 0)
for (const auto& token : fmt)
{
ch = text;
continue;
}
// Set any change in colour
if (codepoint <= FORMAT_COLOUR_CODE_END && codepoint >= FORMAT_COLOUR_CODE_START)
{
codepoint -= FORMAT_COLOUR_CODE_START;
const rct_g1_element* g1 = gfx_get_g1_element(SPR_TEXT_PALETTE);
if (g1 != nullptr)
if (token.IsLiteral())
{
characterColour = g1->offset[codepoint * 4];
}
continue;
}
// If another type of control character ignore
if (codepoint < 32)
continue;
int32_t characterWidth = font_sprite_get_codepoint_width(FONT_SPRITE_BASE_TINY, codepoint);
uint8_t* characterBitmap = font_sprite_get_codepoint_bitmap(codepoint);
for (; characterWidth != 0; characterWidth--, characterBitmap++)
{
// Skip any non-displayed columns
if (scroll != 0)
{
scroll--;
continue;
}
int16_t scrollPosition = *scrollPositionOffsets;
if (scrollPosition == -1)
return;
if (scrollPosition > -1)
{
uint8_t* dst = &bitmap[scrollPosition];
for (uint8_t char_bitmap = *characterBitmap; char_bitmap != 0; char_bitmap >>= 1)
CodepointView codepoints(token.text);
for (auto codepoint : codepoints)
{
if (char_bitmap & 1)
*dst = characterColour;
auto characterWidth = font_sprite_get_codepoint_width(FONT_SPRITE_BASE_TINY, codepoint);
auto characterBitmap = font_sprite_get_codepoint_bitmap(codepoint);
for (; characterWidth != 0; characterWidth--, characterBitmap++)
{
// Skip any non-displayed columns
if (scroll != 0)
{
scroll--;
continue;
}
// Jump to next row
dst += 64;
int16_t scrollPosition = *scrollPositionOffsets;
if (scrollPosition == -1)
return;
if (scrollPosition > -1)
{
auto dst = &bitmap[scrollPosition];
for (uint8_t char_bitmap = *characterBitmap; char_bitmap != 0; char_bitmap >>= 1)
{
if (char_bitmap & 1)
*dst = characterColour;
// Jump to next row
dst += 64;
}
}
scrollPositionOffsets++;
}
}
}
else if (FormatTokenIsColour(token.kind))
{
auto g1 = gfx_get_g1_element(SPR_TEXT_PALETTE);
if (g1 != nullptr)
{
auto colourIndex = FormatTokenGetTextColourIndex(token.kind);
characterColour = g1->offset[colourIndex * 4];
}
}
scrollPositionOffsets++;
}
}
}
static void scrolling_text_set_bitmap_for_ttf(
utf8* text, int32_t scroll, uint8_t* bitmap, const int16_t* scrollPositionOffsets, colour_t colour)
std::string_view text, int32_t scroll, uint8_t* bitmap, const int16_t* scrollPositionOffsets, colour_t colour)
{
#ifndef NO_TTF
TTFFontDescriptor* fontDesc = ttf_get_font_from_sprite_base(FONT_SPRITE_BASE_TINY);
auto fontDesc = ttf_get_font_from_sprite_base(FONT_SPRITE_BASE_TINY);
if (fontDesc->font == nullptr)
{
scrolling_text_set_bitmap_for_sprite(text, scroll, bitmap, scrollPositionOffsets, colour);
return;
}
utf8* dstCh = text;
utf8* ch = text;
int32_t codepoint;
while ((codepoint = utf8_get_next(ch, const_cast<const utf8**>(&ch))) != 0)
thread_local std::string ttfBuffer;
ttfBuffer.clear();
auto fmt = FmtString(text);
for (const auto& token : fmt)
{
if (utf8_is_format_code(codepoint))
if (token.IsLiteral())
{
if (codepoint >= FORMAT_COLOUR_CODE_START && codepoint <= FORMAT_COLOUR_CODE_END)
ttfBuffer.append(token.text);
}
else if (FormatTokenIsColour(token.kind))
{
auto g1 = gfx_get_g1_element(SPR_TEXT_PALETTE);
if (g1 != nullptr)
{
codepoint -= FORMAT_COLOUR_CODE_START;
auto g1 = gfx_get_g1_element(SPR_TEXT_PALETTE);
if (g1 != nullptr)
{
colour = g1->offset[codepoint * 4];
}
auto colourIndex = FormatTokenGetTextColourIndex(token.kind);
colour = g1->offset[colourIndex * 4];
}
}
else
{
dstCh = utf8_write_codepoint(dstCh, codepoint);
}
}
*dstCh = 0;
TTFSurface* surface = ttf_surface_cache_get_or_add(fontDesc->font, text);
auto surface = ttf_surface_cache_get_or_add(fontDesc->font, ttfBuffer.c_str());
if (surface == nullptr)
{
return;

View File

@ -19,6 +19,7 @@
# include "../OpenRCT2.h"
# include "../config/Config.h"
# include "../core/String.hpp"
# include "../localisation/Localisation.h"
# include "../localisation/LocalisationService.h"
# include "../platform/platform.h"
@ -59,13 +60,12 @@ static std::mutex _mutex;
static TTF_Font* ttf_open_font(const utf8* fontPath, int32_t ptSize);
static void ttf_close_font(TTF_Font* font);
static uint32_t ttf_surface_cache_hash(TTF_Font* font, const utf8* text);
static void ttf_surface_cache_dispose(ttf_cache_entry* entry);
static void ttf_surface_cache_dispose_all();
static void ttf_getwidth_cache_dispose_all();
static bool ttf_get_size(TTF_Font* font, const utf8* text, int32_t* width, int32_t* height);
static bool ttf_get_size(TTF_Font* font, std::string_view text, int32_t* outWidth, int32_t* outHeight);
static void ttf_toggle_hinting(bool);
static TTFSurface* ttf_render(TTF_Font* font, const utf8* text);
static TTFSurface* ttf_render(TTF_Font* font, std::string_view text);
template<typename T> class FontLockHelper
{
@ -181,12 +181,12 @@ static void ttf_close_font(TTF_Font* font)
TTF_CloseFont(font);
}
static uint32_t ttf_surface_cache_hash(TTF_Font* font, const utf8* text)
static uint32_t ttf_surface_cache_hash(TTF_Font* font, const std::string_view text)
{
uint32_t hash = static_cast<uint32_t>(((reinterpret_cast<uintptr_t>(font) * 23) ^ 0xAAAAAAAA) & 0xFFFFFFFF);
for (const utf8* ch = text; *ch != 0; ch++)
for (auto& c : text)
{
hash = ror32(hash, 3) ^ (*ch * 13);
hash = ror32(hash, 3) ^ (c * 13);
}
return hash;
}
@ -219,7 +219,7 @@ void ttf_toggle_hinting()
ttf_toggle_hinting(true);
}
TTFSurface* ttf_surface_cache_get_or_add(TTF_Font* font, const utf8* text)
TTFSurface* ttf_surface_cache_get_or_add(TTF_Font* font, std::string_view text)
{
ttf_cache_entry* entry;
@ -235,7 +235,7 @@ TTFSurface* ttf_surface_cache_get_or_add(TTF_Font* font, const utf8* text)
// Check if entry is a hit
if (entry->surface == nullptr)
break;
if (entry->font == font && strcmp(entry->text, text) == 0)
if (entry->font == font && String::Equals(entry->text, text))
{
_ttfSurfaceCacheHitCount++;
entry->lastUseTick = gCurrentDrawCount;
@ -269,7 +269,7 @@ TTFSurface* ttf_surface_cache_get_or_add(TTF_Font* font, const utf8* text)
_ttfSurfaceCacheCount++;
entry->surface = surface;
entry->font = font;
entry->text = _strdup(text);
entry->text = strndup(text.data(), text.size());
entry->lastUseTick = gCurrentDrawCount;
return entry->surface;
}
@ -295,7 +295,7 @@ static void ttf_getwidth_cache_dispose_all()
}
}
uint32_t ttf_getwidth_cache_get_or_add(TTF_Font* font, const utf8* text)
uint32_t ttf_getwidth_cache_get_or_add(TTF_Font* font, std::string_view text)
{
ttf_getwidth_cache_entry* entry;
@ -311,7 +311,7 @@ uint32_t ttf_getwidth_cache_get_or_add(TTF_Font* font, const utf8* text)
// Check if entry is a hit
if (entry->text == nullptr)
break;
if (entry->font == font && strcmp(entry->text, text) == 0)
if (entry->font == font && String::Equals(entry->text, text))
{
_ttfGetWidthCacheHitCount++;
entry->lastUseTick = gCurrentDrawCount;
@ -341,7 +341,7 @@ uint32_t ttf_getwidth_cache_get_or_add(TTF_Font* font, const utf8* text)
_ttfGetWidthCacheCount++;
entry->width = width;
entry->font = font;
entry->text = _strdup(text);
entry->text = strndup(text.data(), text.size());
entry->lastUseTick = gCurrentDrawCount;
return entry->width;
}
@ -357,20 +357,24 @@ bool ttf_provides_glyph(const TTF_Font* font, codepoint_t codepoint)
return TTF_GlyphIsProvided(font, codepoint);
}
static bool ttf_get_size(TTF_Font* font, const utf8* text, int32_t* outWidth, int32_t* outHeight)
static bool ttf_get_size(TTF_Font* font, std::string_view text, int32_t* outWidth, int32_t* outHeight)
{
return TTF_SizeUTF8(font, text, outWidth, outHeight);
thread_local std::string buffer;
buffer.assign(text);
return TTF_SizeUTF8(font, buffer.c_str(), outWidth, outHeight);
}
static TTFSurface* ttf_render(TTF_Font* font, const utf8* text)
static TTFSurface* ttf_render(TTF_Font* font, std::string_view text)
{
thread_local std::string buffer;
buffer.assign(text);
if (TTF_GetFontHinting(font) != 0)
{
return TTF_RenderUTF8_Shaded(font, text, 0x000000FF, 0x000000FF);
return TTF_RenderUTF8_Shaded(font, buffer.c_str(), 0x000000FF, 0x000000FF);
}
else
{
return TTF_RenderUTF8_Solid(font, text, 0x000000FF);
return TTF_RenderUTF8_Solid(font, buffer.c_str(), 0x000000FF);
}
}

View File

@ -11,6 +11,8 @@
#include "Font.h"
#include <string_view>
bool ttf_initialise();
void ttf_dispose();
@ -26,8 +28,8 @@ struct TTFSurface
TTFFontDescriptor* ttf_get_font_from_sprite_base(uint16_t spriteBase);
void ttf_toggle_hinting();
TTFSurface* ttf_surface_cache_get_or_add(TTF_Font* font, const utf8* text);
uint32_t ttf_getwidth_cache_get_or_add(TTF_Font* font, const utf8* text);
TTFSurface* ttf_surface_cache_get_or_add(TTF_Font* font, std::string_view text);
uint32_t ttf_getwidth_cache_get_or_add(TTF_Font* font, std::string_view text);
bool ttf_provides_glyph(const TTF_Font* font, codepoint_t codepoint);
void ttf_free_surface(TTFSurface* surface);

View File

@ -12,7 +12,9 @@
#include "../localisation/Localisation.h"
#include "Drawing.h"
static void DrawText(rct_drawpixelinfo* dpi, const ScreenCoordsXY& coords, const TextPaint& paint, const_utf8string text);
static void DrawText(
rct_drawpixelinfo* dpi, const ScreenCoordsXY& coords, const TextPaint& paint, const_utf8string text,
bool noFormatting = false);
static void DrawText(
rct_drawpixelinfo* dpi, const ScreenCoordsXY& coords, const TextPaint& paint, rct_string_id format, const void* args);
@ -74,9 +76,10 @@ int32_t StaticLayout::GetLineCount()
return LineCount;
}
static void DrawText(rct_drawpixelinfo* dpi, const ScreenCoordsXY& coords, const TextPaint& paint, const_utf8string text)
static void DrawText(
rct_drawpixelinfo* dpi, const ScreenCoordsXY& coords, const TextPaint& paint, const_utf8string text, bool noFormatting)
{
int32_t width = gfx_get_string_width(text);
int32_t width = noFormatting ? gfx_get_string_width_no_formatting(text) : gfx_get_string_width(text);
auto alignedCoords = coords;
switch (paint.Alignment)
@ -91,7 +94,7 @@ static void DrawText(rct_drawpixelinfo* dpi, const ScreenCoordsXY& coords, const
break;
}
ttf_draw_string(dpi, text, paint.Colour, alignedCoords);
ttf_draw_string(dpi, text, paint.Colour, alignedCoords, noFormatting);
if (paint.UnderlineText)
{
@ -151,6 +154,13 @@ void gfx_draw_string(rct_drawpixelinfo* dpi, const_utf8string buffer, uint8_t co
DrawText(dpi, coords, textPaint, buffer);
}
void gfx_draw_string_no_formatting(
rct_drawpixelinfo* dpi, const_utf8string buffer, uint8_t colour, const ScreenCoordsXY& coords)
{
TextPaint textPaint = { colour, gCurrentFontSpriteBase, false, TextAlignment::LEFT };
DrawText(dpi, coords, textPaint, buffer, true);
}
// Basic
void gfx_draw_string_left(
rct_drawpixelinfo* dpi, rct_string_id format, void* args, uint8_t colour, const ScreenCoordsXY& coords)

View File

@ -83,6 +83,8 @@ void chat_update()
void chat_draw(rct_drawpixelinfo* dpi, uint8_t chatBackgroundColor)
{
thread_local std::string lineBuffer;
if (!chat_available())
{
gChatOpen = false;
@ -95,8 +97,6 @@ void chat_draw(rct_drawpixelinfo* dpi, uint8_t chatBackgroundColor)
_chatBottom = context_get_height() - 45;
_chatTop = _chatBottom - 10;
char lineBuffer[CHAT_INPUT_SIZE + 10];
char* lineCh = lineBuffer;
char* inputLine = _chatCurrentLine;
int32_t inputLineHeight = 10;
@ -113,8 +113,8 @@ void chat_draw(rct_drawpixelinfo* dpi, uint8_t chatBackgroundColor)
continue;
}
safe_strcpy(lineBuffer, chat_history_get(i), sizeof(lineBuffer));
lineBuffer.assign(chat_history_get(i));
auto lineCh = lineBuffer.c_str();
int32_t lineHeight = chat_string_wrapped_get_height(static_cast<void*>(&lineCh), _chatWidth - 10);
_chatTop -= (lineHeight + 5);
}
@ -163,8 +163,8 @@ void chat_draw(rct_drawpixelinfo* dpi, uint8_t chatBackgroundColor)
break;
}
safe_strcpy(lineBuffer, chat_history_get(i), sizeof(lineBuffer));
lineBuffer.assign(chat_history_get(i));
auto lineCh = lineBuffer.c_str();
stringHeight = chat_history_draw_string(dpi, static_cast<void*>(&lineCh), screenCoords, _chatWidth - 10) + 5;
gfx_set_dirty_blocks(
{ { screenCoords - ScreenCoordsXY{ 0, stringHeight } }, { screenCoords + ScreenCoordsXY{ _chatWidth, 20 } } });
@ -178,13 +178,12 @@ void chat_draw(rct_drawpixelinfo* dpi, uint8_t chatBackgroundColor)
// Draw current chat input
if (gChatOpen)
{
lineCh = utf8_write_codepoint(lineCh, FORMAT_OUTLINE);
lineCh = utf8_write_codepoint(lineCh, FORMAT_CELADON);
lineBuffer.assign("{OUTLINE}{CELADON}");
lineBuffer += _chatCurrentLine;
safe_strcpy(lineCh, _chatCurrentLine, sizeof(_chatCurrentLine));
screenCoords.y = _chatBottom - inputLineHeight - 5;
lineCh = lineBuffer;
auto lineCh = lineBuffer.c_str();
inputLineHeight = gfx_draw_string_left_wrapped(
dpi, static_cast<void*>(&lineCh), screenCoords + ScreenCoordsXY{ 0, 3 }, _chatWidth - 10, STR_STRING,
TEXT_COLOUR_255);
@ -193,8 +192,7 @@ void chat_draw(rct_drawpixelinfo* dpi, uint8_t chatBackgroundColor)
// TODO: Show caret if the input text has multiple lines
if (_chatCaretTicks < 15 && gfx_get_string_width(lineBuffer) < (_chatWidth - 10))
{
std::memcpy(lineBuffer, _chatCurrentLine, _chatTextInputSession->SelectionStart);
lineBuffer[_chatTextInputSession->SelectionStart] = 0;
lineBuffer.assign(_chatCurrentLine, _chatTextInputSession->SelectionStart);
int32_t caretX = screenCoords.x + gfx_get_string_width(lineBuffer);
int32_t caretY = screenCoords.y + 14;
@ -205,46 +203,26 @@ void chat_draw(rct_drawpixelinfo* dpi, uint8_t chatBackgroundColor)
void chat_history_add(const char* src)
{
size_t bufferSize = strlen(src) + 64;
utf8* buffer = static_cast<utf8*>(calloc(1, bufferSize));
// Find the start of the text (after format codes)
const char* ch = src;
const char* nextCh;
uint32_t codepoint;
while ((codepoint = utf8_get_next(ch, &nextCh)) != 0)
{
if (!utf8_is_format_code(codepoint))
{
break;
}
ch = nextCh;
}
const char* srcText = ch;
// Copy format codes to buffer
std::memcpy(buffer, src, std::min(bufferSize, static_cast<size_t>(srcText - src)));
// Prepend a timestamp
time_t timer;
// Format a timestamp
time_t timer{};
time(&timer);
struct tm* tmInfo = localtime(&timer);
auto tmInfo = localtime(&timer);
char timeBuffer[64]{};
strcatftime(timeBuffer, sizeof(timeBuffer), "[%H:%M] ", tmInfo);
strcatftime(buffer, bufferSize, "[%H:%M] ", tmInfo);
safe_strcat(buffer, srcText, bufferSize);
std::string buffer = timeBuffer;
buffer += src;
// Add to history list
int32_t index = _chatHistoryIndex % CHAT_HISTORY_SIZE;
std::fill_n(_chatHistory[index], CHAT_INPUT_SIZE, 0x00);
std::memcpy(_chatHistory[index], buffer, std::min<size_t>(strlen(buffer), CHAT_INPUT_SIZE - 1));
std::memcpy(_chatHistory[index], buffer.c_str(), std::min<size_t>(buffer.size(), CHAT_INPUT_SIZE - 1));
_chatHistoryTime[index] = platform_get_ticks();
_chatHistoryIndex++;
// Log to file (src only as logging does its own timestamp)
network_append_chat_log(src);
free(buffer);
Mixer_Play_Effect(OpenRCT2::Audio::SoundId::NewsItem, 0, MIXER_VOLUME_MAX, 0.5f, 1.5f, true);
}

View File

@ -1931,17 +1931,17 @@ void InteractiveConsole::Execute(const std::string& s)
void InteractiveConsole::WriteLine(const std::string& s)
{
WriteLine(s, FORMAT_WINDOW_COLOUR_2);
WriteLine(s, FormatToken::ColourWindow2);
}
void InteractiveConsole::WriteLineError(const std::string& s)
{
WriteLine(s, FORMAT_RED);
WriteLine(s, FormatToken::ColourRed);
}
void InteractiveConsole::WriteLineWarning(const std::string& s)
{
WriteLine(s, FORMAT_YELLOW);
WriteLine(s, FormatToken::ColourYellow);
}
void InteractiveConsole::WriteFormatLine(const char* format, ...)

View File

@ -46,7 +46,7 @@ public:
virtual void Clear() abstract;
virtual void Close() abstract;
virtual void Hide() abstract;
virtual void WriteLine(const std::string& s, uint32_t colourFormat) abstract;
virtual void WriteLine(const std::string& s, FormatToken colourFormat) abstract;
};
class StdInOutConsole final : public InteractiveConsole
@ -68,5 +68,5 @@ public:
{
InteractiveConsole::WriteLine(s);
}
void WriteLine(const std::string& s, uint32_t colourFormat) override;
void WriteLine(const std::string& s, FormatToken colourFormat) override;
};

View File

@ -108,20 +108,19 @@ void StdInOutConsole::Close()
openrct2_finish();
}
void StdInOutConsole::WriteLine(const std::string& s, uint32_t colourFormat)
void StdInOutConsole::WriteLine(const std::string& s, FormatToken colourFormat)
{
std::string formatBegin;
if (colourFormat != FORMAT_WINDOW_COLOUR_2)
switch (colourFormat)
{
switch (colourFormat)
{
case FORMAT_RED:
formatBegin = "\033[31m";
break;
case FORMAT_YELLOW:
formatBegin = "\033[33m";
break;
}
case FormatToken::ColourRed:
formatBegin = "\033[31m";
break;
case FormatToken::ColourYellow:
formatBegin = "\033[33m";
break;
default:
break;
}
if (formatBegin.empty() || !Platform::IsColourTerminalSupported())

View File

@ -220,6 +220,7 @@
<ClInclude Include="localisation\Date.h" />
<ClInclude Include="localisation\FormatCodes.h" />
<ClInclude Include="localisation\Formatter.h" />
<ClInclude Include="localisation\Formatting.h" />
<ClInclude Include="localisation\Language.h" />
<ClInclude Include="localisation\LanguagePack.h" />
<ClInclude Include="localisation\Localisation.h" />
@ -554,6 +555,7 @@
<ClCompile Include="localisation\Currency.cpp" />
<ClCompile Include="localisation\FormatCodes.cpp" />
<ClCompile Include="localisation\Formatter.cpp" />
<ClCompile Include="localisation\Formatting.cpp" />
<ClCompile Include="localisation\Language.cpp" />
<ClCompile Include="localisation\LanguagePack.cpp" />
<ClCompile Include="localisation\Localisation.cpp" />

View File

@ -17,54 +17,54 @@
// clang-format off
const encoding_convert_entry RCT2ToUnicodeTable[] =
{
{ 1, FORMAT_MOVE_X },
{ 2, FORMAT_ADJUST_PALETTE },
{ 5, FORMAT_NEWLINE },
{ 6, FORMAT_NEWLINE_SMALLER },
{ 7, FORMAT_TINYFONT },
{ 8, FORMAT_BIGFONT },
{ 9, FORMAT_MEDIUMFONT },
{ 10, FORMAT_SMALLFONT },
{ 11, FORMAT_OUTLINE },
{ 12, FORMAT_OUTLINE_OFF },
{ 13, FORMAT_WINDOW_COLOUR_1 },
{ 14, FORMAT_WINDOW_COLOUR_2 },
{ 15, FORMAT_WINDOW_COLOUR_3 },
{ 17, FORMAT_NEWLINE_X_Y },
{ 23, FORMAT_INLINE_SPRITE },
{ 123, FORMAT_COMMA32 },
{ 124, FORMAT_INT32 },
{ 125, FORMAT_COMMA2DP32 },
{ 126, FORMAT_COMMA16 },
{ 127, FORMAT_UINT16 },
{ 128, FORMAT_CURRENCY2DP },
{ 129, FORMAT_CURRENCY },
{ 130, FORMAT_STRINGID },
{ 131, FORMAT_STRINGID2 },
{ 132, FORMAT_STRING },
{ 133, FORMAT_MONTHYEAR },
{ 134, FORMAT_MONTH },
{ 135, FORMAT_VELOCITY },
{ 136, FORMAT_POP16 },
{ 137, FORMAT_PUSH16 },
{ 138, FORMAT_DURATION },
{ 139, FORMAT_REALTIME },
{ 140, FORMAT_LENGTH },
{ 141, FORMAT_SPRITE },
{ 142, FORMAT_BLACK },
{ 143, FORMAT_GREY },
{ 144, FORMAT_WHITE },
{ 145, FORMAT_RED },
{ 146, FORMAT_GREEN },
{ 147, FORMAT_YELLOW },
{ 148, FORMAT_TOPAZ },
{ 149, FORMAT_CELADON },
{ 150, FORMAT_BABYBLUE },
{ 151, FORMAT_PALELAVENDER },
{ 152, FORMAT_PALEGOLD },
{ 153, FORMAT_LIGHTPINK },
{ 154, FORMAT_PEARLAQUA },
{ 155, FORMAT_PALESILVER },
// { 1, FORMAT_MOVE_X },
// { 2, FORMAT_ADJUST_PALETTE },
// { 5, FORMAT_NEWLINE },
// { 6, FORMAT_NEWLINE_SMALLER },
// { 7, FORMAT_TINYFONT },
// { 8, FORMAT_BIGFONT },
// { 9, FORMAT_MEDIUMFONT },
// { 10, FORMAT_SMALLFONT },
// { 11, FORMAT_OUTLINE },
// { 12, FORMAT_OUTLINE_OFF },
// { 13, FORMAT_WINDOW_COLOUR_1 },
// { 14, FORMAT_WINDOW_COLOUR_2 },
// { 15, FORMAT_WINDOW_COLOUR_3 },
// { 17, FORMAT_NEWLINE_X_Y },
// { 23, FORMAT_INLINE_SPRITE },
// { 123, FORMAT_COMMA32 },
// { 124, FORMAT_INT32 },
// { 125, FORMAT_COMMA2DP32 },
// { 126, FORMAT_COMMA16 },
// { 127, FORMAT_UINT16 },
// { 128, FORMAT_CURRENCY2DP },
// { 129, FORMAT_CURRENCY },
// { 130, FORMAT_STRINGID },
// { 131, FORMAT_STRINGID2 },
// { 132, FORMAT_STRING },
// { 133, FORMAT_MONTHYEAR },
// { 134, FORMAT_MONTH },
// { 135, FORMAT_VELOCITY },
// { 136, FORMAT_POP16 },
// { 137, FORMAT_PUSH16 },
// { 138, FORMAT_DURATION },
// { 139, FORMAT_REALTIME },
// { 140, FORMAT_LENGTH },
// { 141, FORMAT_SPRITE },
// { 142, FORMAT_BLACK },
// { 143, FORMAT_GREY },
// { 144, FORMAT_WHITE },
// { 145, FORMAT_RED },
// { 146, FORMAT_GREEN },
// { 147, FORMAT_YELLOW },
// { 148, FORMAT_TOPAZ },
// { 149, FORMAT_CELADON },
// { 150, FORMAT_BABYBLUE },
// { 151, FORMAT_PALELAVENDER },
// { 152, FORMAT_PALEGOLD },
// { 153, FORMAT_LIGHTPINK },
// { 154, FORMAT_PEARLAQUA },
// { 155, FORMAT_PALESILVER },
{ CSChar::a_ogonek_uc, UnicodeChar::a_ogonek_uc },
{ CSChar::up, UnicodeChar::up },
{ CSChar::c_acute_uc, UnicodeChar::c_acute_uc },

View File

@ -9,116 +9,204 @@
#include "FormatCodes.h"
#include "../common.h"
#include "Localisation.h"
#include <iterator>
#pragma region Format codes
struct format_code_token
{
uint32_t code;
const char* token;
};
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
// clang-format off
static constexpr const format_code_token format_code_tokens[] = {
{ FORMAT_MOVE_X, "MOVE_X" },
{ FORMAT_ADJUST_PALETTE, "ADJUST_PALETTE" },
{ FORMAT_NEWLINE, "NEWLINE" },
{ FORMAT_NEWLINE_SMALLER, "NEWLINE_SMALLER" },
{ FORMAT_TINYFONT, "TINYFONT" },
{ FORMAT_BIGFONT, "BIGFONT" },
{ FORMAT_MEDIUMFONT, "MEDIUMFONT" },
{ FORMAT_SMALLFONT, "SMALLFONT" },
{ FORMAT_OUTLINE, "OUTLINE" },
{ FORMAT_OUTLINE_OFF, "OUTLINE_OFF" },
{ FORMAT_WINDOW_COLOUR_1, "WINDOW_COLOUR_1" },
{ FORMAT_WINDOW_COLOUR_2, "WINDOW_COLOUR_2" },
{ FORMAT_WINDOW_COLOUR_3, "WINDOW_COLOUR_3" },
{ FORMAT_NEWLINE_X_Y, "NEWLINE_X_Y" },
{ FORMAT_INLINE_SPRITE, "INLINE_SPRITE" },
{ FORMAT_COMMA32, "COMMA32" },
{ FORMAT_INT32, "INT32" },
{ FORMAT_COMMA2DP32, "COMMA2DP32" },
{ FORMAT_COMMA16, "COMMA16" },
{ FORMAT_UINT16, "UINT16" },
{ FORMAT_CURRENCY2DP, "CURRENCY2DP" },
{ FORMAT_CURRENCY, "CURRENCY" },
{ FORMAT_STRINGID, "STRINGID" },
{ FORMAT_STRINGID2, "STRINGID2" },
{ FORMAT_STRING, "STRING" },
{ FORMAT_MONTHYEAR, "MONTHYEAR" },
{ FORMAT_MONTH, "MONTH" },
{ FORMAT_VELOCITY, "VELOCITY" },
{ FORMAT_POP16, "POP16" },
{ FORMAT_PUSH16, "PUSH16" },
{ FORMAT_DURATION, "DURATION" },
{ FORMAT_REALTIME, "REALTIME" },
{ FORMAT_LENGTH, "LENGTH" },
{ FORMAT_SPRITE, "SPRITE" },
{ FORMAT_BLACK, "BLACK" },
{ FORMAT_GREY, "GREY" },
{ FORMAT_WHITE, "WHITE" },
{ FORMAT_RED, "RED" },
{ FORMAT_GREEN, "GREEN" },
{ FORMAT_YELLOW, "YELLOW" },
{ FORMAT_TOPAZ, "TOPAZ" },
{ FORMAT_CELADON, "CELADON" },
{ FORMAT_BABYBLUE, "BABYBLUE" },
{ FORMAT_PALELAVENDER, "PALELAVENDER" },
{ FORMAT_PALEGOLD, "PALEGOLD" },
{ FORMAT_LIGHTPINK, "LIGHTPINK" },
{ FORMAT_PEARLAQUA, "PEARLAQUA" },
{ FORMAT_PALESILVER, "PALESILVER" },
{ FORMAT_COMMA1DP16, "COMMA1DP16" }
static const std::unordered_map<std::string_view, FormatToken> FormatTokenMap = {
{ "MOVE_X", FormatToken::Move, },
{ "NEWLINE", FormatToken::Newline, },
{ "NEWLINE_SMALLER", FormatToken::NewlineSmall, },
{ "TINYFONT", FormatToken::FontTiny, },
{ "BIGFONT", FormatToken::FontBig, },
{ "MEDIUMFONT", FormatToken::FontMedium, },
{ "SMALLFONT", FormatToken::FontSmall, },
{ "OUTLINE", FormatToken::OutlineEnable, },
{ "OUTLINE_OFF", FormatToken::OutlineDisable, },
{ "WINDOW_COLOUR_1", FormatToken::ColourWindow1, },
{ "WINDOW_COLOUR_2", FormatToken::ColourWindow2, },
{ "WINDOW_COLOUR_3", FormatToken::ColourWindow3, },
{ "INLINE_SPRITE", FormatToken::InlineSprite, },
{ "COMMA32", FormatToken::Comma32, },
{ "INT32", FormatToken::Int32, },
{ "COMMA1DP16", FormatToken::Comma1dp16, },
{ "COMMA2DP32", FormatToken::Comma2dp32, },
{ "COMMA16", FormatToken::Comma16, },
{ "UINT16", FormatToken::UInt16, },
{ "CURRENCY2DP", FormatToken::Currency2dp, },
{ "CURRENCY", FormatToken::Currency, },
{ "STRINGID", FormatToken::StringId, },
{ "STRING", FormatToken::String, },
{ "MONTHYEAR", FormatToken::MonthYear, },
{ "MONTH", FormatToken::Month, },
{ "VELOCITY", FormatToken::Velocity, },
{ "POP16", FormatToken::Pop16, },
{ "PUSH16", FormatToken::Push16, },
{ "DURATION", FormatToken::DurationShort, },
{ "REALTIME", FormatToken::DurationLong, },
{ "LENGTH", FormatToken::Length, },
{ "SPRITE", FormatToken::Sprite, },
{ "BLACK", FormatToken::ColourBlack, },
{ "GREY", FormatToken::ColourGrey, },
{ "WHITE", FormatToken::ColourWhite, },
{ "RED", FormatToken::ColourRed, },
{ "GREEN", FormatToken::ColourGreen, },
{ "YELLOW", FormatToken::ColourYellow, },
{ "TOPAZ", FormatToken::ColourTopaz, },
{ "CELADON", FormatToken::ColourCeladon, },
{ "BABYBLUE", FormatToken::ColourBabyBlue, },
{ "PALELAVENDER", FormatToken::ColourPaleLavender, },
{ "PALEGOLD", FormatToken::ColourPaleGold, },
{ "LIGHTPINK", FormatToken::ColourLightPink, },
{ "PEARLAQUA", FormatToken::ColourPearlAqua, },
{ "PALESILVER", FormatToken::ColourPaleSilver, },
};
// clang-format on
uint32_t format_get_code(const char* token)
static std::string_view GetFormatTokenStringWithBraces(FormatToken token)
{
for (uint32_t i = 0; i < std::size(format_code_tokens); i++)
// Ensure cache is thread safe
static std::mutex mutex;
std::lock_guard<std::mutex> guard(mutex);
static std::vector<std::string> cache;
auto index = static_cast<size_t>(token);
if (cache.size() <= index)
{
if (_strcmpi(token, format_code_tokens[i].token) == 0)
return format_code_tokens[i].code;
cache.resize(index + 1);
}
return 0;
if (cache[index].empty())
{
cache[index] = "{" + std::string(FormatTokenToString(token)) + "}";
}
return cache[index];
}
const char* format_get_token(uint32_t code)
FormatToken FormatTokenFromString(std::string_view token)
{
for (uint32_t i = 0; i < std::size(format_code_tokens); i++)
{
if (code == format_code_tokens[i].code)
return format_code_tokens[i].token;
}
return nullptr;
auto result = FormatTokenMap.find(token);
return result != std::end(FormatTokenMap) ? result->second : FormatToken::Unknown;
}
bool utf8_should_use_sprite_for_codepoint(char32_t codepoint)
std::string_view FormatTokenToString(FormatToken token, bool withBraces)
{
switch (codepoint)
if (withBraces)
{
case UnicodeChar::up:
case UnicodeChar::down:
case UnicodeChar::leftguillemet:
case UnicodeChar::tick:
case UnicodeChar::cross:
case UnicodeChar::right:
case UnicodeChar::rightguillemet:
case UnicodeChar::small_up:
case UnicodeChar::small_down:
case UnicodeChar::left:
case UnicodeChar::quote_open:
case UnicodeChar::quote_close:
case UnicodeChar::german_quote_open:
case UnicodeChar::plus:
case UnicodeChar::minus:
return GetFormatTokenStringWithBraces(token);
}
else
{
for (const auto& t : FormatTokenMap)
{
if (t.second == token)
{
return t.first;
}
}
return {};
}
}
bool FormatTokenTakesArgument(FormatToken token)
{
switch (token)
{
case FormatToken::Comma32:
case FormatToken::Int32:
case FormatToken::Comma1dp16:
case FormatToken::Comma2dp32:
case FormatToken::Comma16:
case FormatToken::UInt16:
case FormatToken::Currency2dp:
case FormatToken::Currency:
case FormatToken::StringId:
case FormatToken::String:
case FormatToken::MonthYear:
case FormatToken::Month:
case FormatToken::Velocity:
case FormatToken::DurationShort:
case FormatToken::DurationLong:
case FormatToken::Length:
case FormatToken::Sprite:
return true;
default:
return false;
}
}
#pragma endregion
bool FormatTokenIsColour(FormatToken token)
{
switch (token)
{
case FormatToken::ColourBlack:
case FormatToken::ColourGrey:
case FormatToken::ColourWhite:
case FormatToken::ColourRed:
case FormatToken::ColourGreen:
case FormatToken::ColourYellow:
case FormatToken::ColourTopaz:
case FormatToken::ColourCeladon:
case FormatToken::ColourBabyBlue:
case FormatToken::ColourPaleLavender:
case FormatToken::ColourPaleGold:
case FormatToken::ColourLightPink:
case FormatToken::ColourPearlAqua:
case FormatToken::ColourPaleSilver:
return true;
default:
return false;
}
}
size_t FormatTokenGetTextColourIndex(FormatToken token)
{
switch (token)
{
case FormatToken::ColourBlack:
return 0;
case FormatToken::ColourGrey:
return 1;
case FormatToken::ColourWhite:
return 2;
case FormatToken::ColourRed:
return 3;
case FormatToken::ColourGreen:
return 4;
case FormatToken::ColourYellow:
return 5;
case FormatToken::ColourTopaz:
return 6;
case FormatToken::ColourCeladon:
return 7;
case FormatToken::ColourBabyBlue:
return 8;
case FormatToken::ColourPaleLavender:
return 9;
case FormatToken::ColourPaleGold:
return 10;
case FormatToken::ColourLightPink:
return 11;
case FormatToken::ColourPearlAqua:
return 12;
case FormatToken::ColourPaleSilver:
return 13;
default:
return 0;
}
}
FormatToken FormatTokenFromTextColour(size_t textColour)
{
static constexpr const FormatToken tokens[] = {
FormatToken::ColourBlack, FormatToken::ColourGrey, FormatToken::ColourWhite,
FormatToken::ColourRed, FormatToken::ColourGreen, FormatToken::ColourYellow,
FormatToken::ColourTopaz, FormatToken::ColourCeladon, FormatToken::ColourBabyBlue,
FormatToken::ColourPaleLavender, FormatToken::ColourPaleGold, FormatToken::ColourLightPink,
FormatToken::ColourPearlAqua, FormatToken::ColourPaleSilver,
};
if (textColour > std::size(tokens))
return FormatToken::ColourBlack;
return tokens[textColour];
}

View File

@ -11,92 +11,78 @@
#include "../common.h"
uint32_t format_get_code(const char* token);
const char* format_get_token(uint32_t code);
#include <string_view>
enum
enum class FormatToken
{
// Font format codes
Unknown,
Literal,
Escaped,
// The next byte specifies the X coordinate
FORMAT_MOVE_X = 1,
// The next byte specifies the palette
FORMAT_ADJUST_PALETTE,
Newline,
NewlineSmall,
FORMAT_3,
FORMAT_4,
// With parameters
Move,
InlineSprite,
// Moves to the next line
FORMAT_NEWLINE = 5,
// Moves less than NEWLINE
FORMAT_NEWLINE_SMALLER,
// With arguments
Comma32,
Int32,
Comma1dp16,
Comma2dp32,
Comma16,
UInt16,
Currency2dp,
Currency,
StringId,
String,
MonthYear,
Month,
Velocity,
DurationShort,
DurationLong,
Length,
Sprite,
Pop16,
Push16,
FORMAT_TINYFONT,
FORMAT_BIGFONT,
FORMAT_MEDIUMFONT,
FORMAT_SMALLFONT,
// Colours
ColourWindow1,
ColourWindow2,
ColourWindow3,
ColourBlack,
ColourGrey,
ColourWhite,
ColourRed,
ColourGreen,
ColourYellow,
ColourTopaz,
ColourCeladon,
ColourBabyBlue,
ColourPaleLavender,
ColourPaleGold,
ColourLightPink,
ColourPearlAqua,
ColourPaleSilver,
FORMAT_OUTLINE,
FORMAT_OUTLINE_OFF,
// Fonts
FontTiny,
FontSmall,
FontMedium,
FontBig,
// Changes the colour of the text to a predefined window colour.
FORMAT_WINDOW_COLOUR_1,
FORMAT_WINDOW_COLOUR_2,
FORMAT_WINDOW_COLOUR_3,
FORMAT_16,
// The next 2 bytes specify the X and Y coordinates
FORMAT_NEWLINE_X_Y = 17,
// The next 4 bytes specify the sprite
FORMAT_INLINE_SPRITE = 23,
// Argument format codes
FORMAT_ARGUMENT_CODE_START = 123, // 'z' == 122 or 0x7A
FORMAT_COMMA32 = 123,
FORMAT_INT32,
FORMAT_COMMA2DP32,
FORMAT_COMMA16,
FORMAT_UINT16,
FORMAT_CURRENCY2DP,
FORMAT_CURRENCY,
FORMAT_STRINGID,
FORMAT_STRINGID2,
FORMAT_STRING,
FORMAT_MONTHYEAR,
FORMAT_MONTH,
FORMAT_VELOCITY,
FORMAT_POP16,
FORMAT_PUSH16,
FORMAT_DURATION,
FORMAT_REALTIME,
FORMAT_LENGTH,
FORMAT_SPRITE,
FORMAT_ARGUMENT_CODE_END = FORMAT_SPRITE,
// Colour format codes
FORMAT_COLOUR_CODE_START = 142,
FORMAT_BLACK = 142,
FORMAT_GREY,
FORMAT_WHITE,
FORMAT_RED,
FORMAT_GREEN,
FORMAT_YELLOW,
FORMAT_TOPAZ,
FORMAT_CELADON,
FORMAT_BABYBLUE,
FORMAT_PALELAVENDER,
FORMAT_PALEGOLD,
FORMAT_LIGHTPINK,
FORMAT_PEARLAQUA,
FORMAT_PALESILVER,
FORMAT_COLOUR_CODE_END = FORMAT_PALESILVER,
// Format codes that need suitable Unicode allocations
FORMAT_COMMA1DP16 = 20004
OutlineEnable,
OutlineDisable,
};
FormatToken FormatTokenFromString(std::string_view token);
std::string_view FormatTokenToString(FormatToken token, bool withBraces = false);
bool FormatTokenTakesArgument(FormatToken token);
bool FormatTokenIsColour(FormatToken token);
size_t FormatTokenGetTextColourIndex(FormatToken token);
FormatToken FormatTokenFromTextColour(size_t textColour);
constexpr uint8_t CS_SPRITE_FONT_OFFSET = 32;
namespace CSChar

View File

@ -0,0 +1,800 @@
/*****************************************************************************
* Copyright (c) 2014-2020 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#include "Formatting.h"
#include "../config/Config.h"
#include "../util/Util.h"
#include "Localisation.h"
#include "StringIds.h"
#include <cmath>
#include <cstdint>
namespace OpenRCT2
{
static std::optional<int32_t> ParseNumericToken(std::string_view s)
{
if (s.size() >= 3 && s.size() <= 5 && s[0] == '{' && s[s.size() - 1] == '}')
{
char buffer[8]{};
std::memcpy(buffer, s.data() + 1, s.size() - 2);
return std::atoi(buffer);
}
return std::nullopt;
}
static std::optional<int32_t> ParseNumericToken(std::string_view str, size_t& i)
{
if (i < str.size() && str[i] == '{')
{
auto parameterStart = i;
do
{
i++;
} while (i < str.size() && str[i] != '}');
if (i < str.size() && str[i] == '}')
{
i++;
}
auto paramter = str.substr(parameterStart, i - parameterStart);
return ParseNumericToken(paramter);
}
return std::nullopt;
}
FmtString::token::token(FormatToken k, std::string_view s, uint32_t p)
: kind(k)
, text(s)
, parameter(p)
{
}
bool FmtString::token::IsLiteral() const
{
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)
{
update();
}
void FmtString::iterator::update()
{
auto i = index;
if (i >= str.size())
{
current = token();
return;
}
if (str[i] == '\n' || str[i] == '\r')
{
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
auto startIndex = i;
do
{
i++;
} while (i < str.size() && str[i] != '}');
if (i < str.size() && str[i] == '}')
{
i++;
auto inner = str.substr(startIndex + 1, i - startIndex - 2);
if (inner == "MOVE_X")
{
uint32_t p = 0;
auto p0 = ParseNumericToken(str, i);
if (p0)
{
p = *p0;
}
current = token(FormatToken::Move, str.substr(startIndex, i - startIndex), p);
return;
}
else if (inner == "INLINE_SPRITE")
{
uint32_t p = 0;
auto p0 = ParseNumericToken(str, i);
auto p1 = ParseNumericToken(str, i);
auto p2 = ParseNumericToken(str, i);
auto p3 = ParseNumericToken(str, i);
if (p0 && p1 && p2 && p3)
{
p |= (*p0);
p |= (*p1) << 8;
p |= (*p2) << 16;
p |= (*p3) << 24;
}
current = token(FormatToken::InlineSprite, str.substr(startIndex, i - startIndex), p);
return;
}
}
}
else
{
do
{
i++;
} while (i < str.size() && str[i] != '{' && str[i] != '}' && str[i] != '\n' && str[i] != '\r');
}
current = CreateToken(i - index);
}
bool FmtString::iterator::operator==(iterator& rhs)
{
return index == rhs.index;
}
bool FmtString::iterator::operator!=(iterator& rhs)
{
return index != rhs.index;
}
FmtString::token FmtString::iterator::CreateToken(size_t len)
{
std::string_view sztoken = str.substr(index, len);
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);
}
else if (sztoken == "\n" || sztoken == "\r")
{
return token(FormatToken::Newline, sztoken);
}
return token(FormatToken::Literal, sztoken);
}
const FmtString::token* FmtString::iterator::operator->() const
{
return &current;
}
const FmtString::token& FmtString::iterator::operator*()
{
return current;
}
FmtString::iterator& FmtString::iterator::operator++()
{
if (index < str.size())
{
index += current.text.size();
update();
}
return *this;
}
FmtString::iterator FmtString::iterator::operator++(int)
{
auto result = *this;
if (index < str.size())
{
index += current.text.size();
update();
}
return result;
}
bool FmtString::iterator::eol() const
{
return index >= str.size();
}
FmtString::FmtString(std::string&& s)
{
_strOwned = std::move(s);
_str = _strOwned;
}
FmtString::FmtString(std::string_view s)
: _str(s)
{
}
FmtString::FmtString(const char* s)
: FmtString(s == nullptr ? std::string_view() : std::string_view(s))
{
}
FmtString::iterator FmtString::begin() const
{
return iterator(_str, 0);
}
FmtString::iterator FmtString::end() const
{
return iterator(_str, _str.size());
}
std::string FmtString::WithoutFormatTokens() const
{
std::string result;
result.reserve(_str.size() * 4);
for (const auto& t : *this)
{
if (t.IsLiteral())
{
result += t.text;
}
}
return result;
}
static std::string_view GetDigitSeparator()
{
auto sz = language_get_string(STR_LOCALE_THOUSANDS_SEPARATOR);
return sz != nullptr ? sz : std::string_view();
}
static std::string_view GetDecimalSeparator()
{
auto sz = language_get_string(STR_LOCALE_DECIMAL_POINT);
return sz != nullptr ? sz : std::string_view();
}
void FormatRealName(std::stringstream& ss, rct_string_id id)
{
if (IsRealNameStringId(id))
{
auto realNameIndex = id - REAL_NAME_START;
ss << real_names[realNameIndex % std::size(real_names)];
ss << ' ';
ss << real_name_initials[(realNameIndex >> 10) % std::size(real_name_initials)];
ss << '.';
}
}
template<size_t TSize, typename TIndex> static void AppendSeparator(char (&buffer)[TSize], TIndex& i, std::string_view sep)
{
if (i < TSize)
{
auto remainingLen = TSize - i;
auto cpyLen = std::min(sep.size(), remainingLen);
std::memcpy(&buffer[i], sep.data(), cpyLen);
i += static_cast<TIndex>(cpyLen);
}
}
template<size_t TDecimalPlace, bool TDigitSep, typename T> void FormatNumber(std::stringstream& ss, T value)
{
char buffer[32];
size_t i = 0;
uint64_t num;
if constexpr (std::is_signed<T>::value)
{
if (value < 0)
{
ss << '-';
if (value == std::numeric_limits<int64_t>::min())
{
// Edge case: int64_t can not store this number so manually assign num to (int64_t::max + 1)
num = static_cast<uint64_t>(std::numeric_limits<int64_t>::max()) + 1;
}
else
{
// Cast negative number to int64_t and then reverse sign
num = -static_cast<int64_t>(value);
}
}
else
{
num = value;
}
}
else
{
num = value;
}
// Decimal digits
if constexpr (TDecimalPlace > 0)
{
while (num != 0 && i < sizeof(buffer) && i < TDecimalPlace)
{
buffer[i++] = static_cast<char>('0' + (num % 10));
num /= 10;
}
auto decSep = GetDecimalSeparator();
AppendSeparator(buffer, i, decSep);
}
// Whole digits
[[maybe_unused]] auto digitSep = GetDigitSeparator();
size_t groupLen = 0;
do
{
if constexpr (TDigitSep)
{
if (groupLen >= 3)
{
groupLen = 0;
AppendSeparator(buffer, i, digitSep);
}
}
buffer[i++] = static_cast<char>('0' + (num % 10));
num /= 10;
if constexpr (TDigitSep)
{
groupLen++;
}
} while (num != 0 && i < sizeof(buffer));
// Finally reverse append the string
for (int32_t j = static_cast<int32_t>(i - 1); j >= 0; j--)
{
ss << buffer[j];
}
}
template<size_t TDecimalPlace, bool TDigitSep, typename T> void FormatCurrency(std::stringstream& ss, T rawValue)
{
auto currencyDesc = &CurrencyDescriptors[EnumValue(gConfigGeneral.currency_format)];
auto value = static_cast<int64_t>(rawValue) * currencyDesc->rate;
// Negative sign
if (value < 0)
{
ss << '-';
value = -value;
}
// Round the value away from zero
if constexpr (TDecimalPlace < 2)
{
value = (value + 99) / 100;
}
// Currency symbol
auto symbol = currencyDesc->symbol_unicode;
auto affix = currencyDesc->affix_unicode;
if (!font_supports_string(symbol, FONT_SIZE_MEDIUM))
{
symbol = currencyDesc->symbol_ascii;
affix = currencyDesc->affix_ascii;
}
// Currency symbol prefix
if (affix == CurrencyAffix::Prefix)
{
ss << symbol;
}
// Drop the pennies for "large" currencies
auto dropPennies = false;
if constexpr (TDecimalPlace >= 2)
{
dropPennies = currencyDesc->rate >= 100;
}
if (dropPennies)
{
FormatNumber<0, TDigitSep>(ss, value / 100);
}
else
{
FormatNumber<TDecimalPlace, TDigitSep>(ss, value);
}
// Currency symbol suffix
if (affix == CurrencyAffix::Suffix)
{
ss << symbol;
}
}
template<typename T> static void FormatMinutesSeconds(std::stringstream& ss, T value)
{
static constexpr const rct_string_id Formats[][2] = {
{ STR_DURATION_SEC, STR_DURATION_SECS },
{ STR_DURATION_MIN_SEC, STR_DURATION_MIN_SECS },
{ STR_DURATION_MINS_SEC, STR_DURATION_MINS_SECS },
};
auto minutes = value / 60;
auto seconds = value % 60;
if (minutes == 0)
{
auto fmt = Formats[0][seconds == 1 ? 0 : 1];
FormatStringId(ss, fmt, seconds);
}
else
{
auto fmt = Formats[minutes == 1 ? 1 : 2][seconds == 1 ? 0 : 1];
FormatStringId(ss, fmt, minutes, seconds);
}
}
template<typename T> static void FormatHoursMinutes(std::stringstream& ss, T value)
{
static constexpr const rct_string_id Formats[][2] = {
{ STR_REALTIME_MIN, STR_REALTIME_MINS },
{ STR_REALTIME_HOUR_MIN, STR_REALTIME_HOUR_MINS },
{ STR_REALTIME_HOURS_MIN, STR_REALTIME_HOURS_MINS },
};
auto hours = value / 60;
auto minutes = value % 60;
if (hours == 0)
{
auto fmt = Formats[0][minutes == 1 ? 0 : 1];
FormatStringId(ss, fmt, minutes);
}
else
{
auto fmt = Formats[hours == 1 ? 1 : 2][minutes == 1 ? 0 : 1];
FormatStringId(ss, fmt, hours, minutes);
}
}
template<typename T> void FormatArgument(std::stringstream& ss, FormatToken token, T arg)
{
switch (token)
{
case FormatToken::UInt16:
case FormatToken::Int32:
if constexpr (std::is_integral<T>())
{
FormatNumber<0, false>(ss, arg);
}
break;
case FormatToken::Comma16:
case FormatToken::Comma32:
if constexpr (std::is_integral<T>())
{
FormatNumber<0, true>(ss, arg);
}
break;
case FormatToken::Comma1dp16:
if constexpr (std::is_integral<T>())
{
FormatNumber<1, true>(ss, arg);
}
else if constexpr (std::is_floating_point<T>())
{
FormatNumber<1, true>(ss, std::round(arg * 10));
}
break;
case FormatToken::Comma2dp32:
if constexpr (std::is_integral<T>())
{
FormatNumber<2, true>(ss, arg);
}
else if constexpr (std::is_floating_point<T>())
{
FormatNumber<2, true>(ss, std::round(arg * 100));
}
break;
case FormatToken::Currency2dp:
if constexpr (std::is_integral<T>())
{
FormatCurrency<2, true>(ss, arg);
}
break;
case FormatToken::Currency:
if constexpr (std::is_integral<T>())
{
FormatCurrency<0, true>(ss, arg);
}
break;
case FormatToken::Velocity:
if constexpr (std::is_integral<T>())
{
switch (gConfigGeneral.measurement_format)
{
default:
case MeasurementFormat::Imperial:
FormatStringId(ss, STR_UNIT_SUFFIX_MILES_PER_HOUR, arg);
break;
case MeasurementFormat::Metric:
FormatStringId(ss, STR_UNIT_SUFFIX_KILOMETRES_PER_HOUR, mph_to_kmph(arg));
break;
case MeasurementFormat::SI:
FormatStringId(ss, STR_UNIT_SUFFIX_METRES_PER_SECOND, mph_to_dmps(arg));
break;
}
}
break;
case FormatToken::DurationShort:
if constexpr (std::is_integral<T>())
{
FormatMinutesSeconds(ss, arg);
}
break;
case FormatToken::DurationLong:
if constexpr (std::is_integral<T>())
{
FormatHoursMinutes(ss, arg);
}
break;
case FormatToken::Length:
if constexpr (std::is_integral<T>())
{
switch (gConfigGeneral.measurement_format)
{
default:
case MeasurementFormat::Imperial:
FormatStringId(ss, STR_UNIT_SUFFIX_FEET, metres_to_feet(arg));
break;
case MeasurementFormat::Metric:
case MeasurementFormat::SI:
FormatStringId(ss, STR_UNIT_SUFFIX_METRES, arg);
break;
}
}
break;
case FormatToken::MonthYear:
if constexpr (std::is_integral<T>())
{
auto month = date_get_month(arg);
auto year = date_get_year(arg) + 1;
FormatStringId(ss, STR_DATE_FORMAT_MY, month, year);
}
break;
case FormatToken::Month:
if constexpr (std::is_integral<T>())
{
auto szMonth = language_get_string(DateGameMonthNames[date_get_month(arg)]);
if (szMonth != nullptr)
{
ss << szMonth;
}
}
break;
case FormatToken::String:
if constexpr (std::is_same<T, const char*>())
{
ss << arg;
}
else if constexpr (std::is_same<T, const std::string&>())
{
ss << arg.c_str();
}
break;
case FormatToken::Sprite:
if constexpr (std::is_integral<T>())
{
auto idx = static_cast<uint32_t>(arg);
ss << "{INLINE_SPRITE}";
ss << "{" << ((idx >> 0) & 0xFF) << "}";
ss << "{" << ((idx >> 8) & 0xFF) << "}";
ss << "{" << ((idx >> 16) & 0xFF) << "}";
ss << "{" << ((idx >> 24) & 0xFF) << "}";
}
break;
default:
break;
}
}
template void FormatArgument(std::stringstream&, FormatToken, uint16_t);
template void FormatArgument(std::stringstream&, FormatToken, int16_t);
template void FormatArgument(std::stringstream&, FormatToken, int32_t);
template void FormatArgument(std::stringstream&, FormatToken, int64_t);
template void FormatArgument(std::stringstream&, FormatToken, uint64_t);
template void FormatArgument(std::stringstream&, FormatToken, const char*);
bool IsRealNameStringId(rct_string_id id)
{
return id >= REAL_NAME_START && id <= REAL_NAME_END;
}
FmtString GetFmtStringById(rct_string_id id)
{
auto fmtc = language_get_string(id);
return FmtString(fmtc);
}
std::stringstream& GetThreadFormatStream()
{
thread_local std::stringstream ss;
// Reset the buffer (reported as most efficient way)
std::stringstream().swap(ss);
return ss;
}
size_t CopyStringStreamToBuffer(char* buffer, size_t bufferLen, std::stringstream& ss)
{
auto stringLen = ss.tellp();
auto copyLen = std::min<size_t>(bufferLen - 1, stringLen);
ss.seekg(0, std::ios::beg);
ss.read(buffer, copyLen);
buffer[copyLen] = '\0';
return stringLen;
}
static void FormatArgumentAny(std::stringstream& ss, FormatToken token, const FormatArg_t& value)
{
if (std::holds_alternative<uint16_t>(value))
{
FormatArgument(ss, token, std::get<uint16_t>(value));
}
else if (std::holds_alternative<int32_t>(value))
{
FormatArgument(ss, token, std::get<int32_t>(value));
}
else if (std::holds_alternative<const char*>(value))
{
FormatArgument(ss, token, std::get<const char*>(value));
}
else
{
throw std::runtime_error("No support for format argument type.");
}
}
static void FormatStringAny(
std::stringstream& ss, const FmtString& fmt, const std::vector<FormatArg_t>& args, size_t& argIndex)
{
for (const auto& token : fmt)
{
if (token.kind == FormatToken::StringId)
{
if (argIndex < args.size())
{
const auto& arg = args[argIndex++];
if (auto stringid = std::get_if<uint16_t>(&arg))
{
if (IsRealNameStringId(*stringid))
{
FormatRealName(ss, *stringid);
}
else
{
auto subfmt = GetFmtStringById(*stringid);
FormatStringAny(ss, subfmt, args, argIndex);
}
}
}
else
{
argIndex++;
}
}
else if (FormatTokenTakesArgument(token.kind))
{
if (argIndex < args.size())
{
FormatArgumentAny(ss, token.kind, args[argIndex]);
}
argIndex++;
}
else if (token.kind != FormatToken::Push16 && token.kind != FormatToken::Pop16)
{
ss << token.text;
}
}
}
std::string FormatStringAny(const FmtString& fmt, const std::vector<FormatArg_t>& args)
{
auto& ss = GetThreadFormatStream();
size_t argIndex = 0;
FormatStringAny(ss, fmt, args, argIndex);
return ss.str();
}
size_t FormatStringAny(char* buffer, size_t bufferLen, const FmtString& fmt, const std::vector<FormatArg_t>& args)
{
auto& ss = GetThreadFormatStream();
size_t argIndex = 0;
FormatStringAny(ss, fmt, args, argIndex);
return CopyStringStreamToBuffer(buffer, bufferLen, ss);
}
template<typename T> static T ReadFromArgs(const void*& args)
{
T value;
std::memcpy(&value, args, sizeof(T));
args = reinterpret_cast<const char*>(reinterpret_cast<uintptr_t>(args) + sizeof(T));
return value;
}
static void BuildAnyArgListFromLegacyArgBuffer(const FmtString& fmt, std::vector<FormatArg_t>& anyArgs, const void*& args)
{
for (const auto& t : fmt)
{
switch (t.kind)
{
case FormatToken::Comma32:
case FormatToken::Int32:
case FormatToken::Comma2dp32:
case FormatToken::Currency2dp:
case FormatToken::Currency:
case FormatToken::Sprite:
anyArgs.push_back(ReadFromArgs<int32_t>(args));
break;
case FormatToken::Comma16:
case FormatToken::UInt16:
case FormatToken::MonthYear:
case FormatToken::Month:
case FormatToken::Velocity:
case FormatToken::DurationShort:
case FormatToken::DurationLong:
case FormatToken::Length:
anyArgs.push_back(ReadFromArgs<uint16_t>(args));
break;
case FormatToken::StringId:
{
auto stringId = ReadFromArgs<rct_string_id>(args);
anyArgs.push_back(stringId);
BuildAnyArgListFromLegacyArgBuffer(GetFmtStringById(stringId), anyArgs, args);
break;
}
case FormatToken::String:
{
auto sz = ReadFromArgs<const char*>(args);
anyArgs.push_back(sz);
break;
}
case FormatToken::Pop16:
args = reinterpret_cast<const char*>(reinterpret_cast<uintptr_t>(args) + 2);
break;
case FormatToken::Push16:
args = reinterpret_cast<const char*>(reinterpret_cast<uintptr_t>(args) - 2);
break;
default:
break;
}
}
}
size_t FormatStringLegacy(char* buffer, size_t bufferLen, rct_string_id id, const void* args)
{
std::vector<FormatArg_t> anyArgs;
auto fmt = GetFmtStringById(id);
BuildAnyArgListFromLegacyArgBuffer(fmt, anyArgs, args);
return FormatStringAny(buffer, bufferLen, fmt, anyArgs);
}
} // namespace OpenRCT2
void format_string(utf8* dest, size_t size, rct_string_id format, const void* args)
{
OpenRCT2::FormatStringLegacy(dest, size, format, args);
}

View File

@ -0,0 +1,191 @@
/*****************************************************************************
* Copyright (c) 2014-2020 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#pragma once
#include "../common.h"
#include "FormatCodes.h"
#include "Language.h"
#include <sstream>
#include <stack>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
namespace OpenRCT2
{
using FormatArg_t = std::variant<uint16_t, int32_t, const char*>;
class FmtString
{
private:
std::string_view _str;
std::string _strOwned;
public:
struct token
{
FormatToken kind{};
std::string_view text;
uint32_t parameter{};
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
{
private:
std::string_view str;
size_t index;
token current;
void update();
public:
iterator(std::string_view s, size_t i);
bool operator==(iterator& rhs);
bool operator!=(iterator& rhs);
token CreateToken(size_t len);
const token* operator->() const;
const token& operator*();
iterator& operator++();
iterator operator++(int);
bool eol() const;
};
FmtString() = default;
FmtString(std::string&& s);
FmtString(std::string_view s);
FmtString(const char* s);
iterator begin() const;
iterator end() const;
std::string WithoutFormatTokens() const;
};
template<typename T> void FormatArgument(std::stringstream& ss, FormatToken token, T arg);
bool IsRealNameStringId(rct_string_id id);
void FormatRealName(std::stringstream& ss, rct_string_id id);
FmtString GetFmtStringById(rct_string_id id);
std::stringstream& GetThreadFormatStream();
size_t CopyStringStreamToBuffer(char* buffer, size_t bufferLen, std::stringstream& ss);
inline void FormatString(std::stringstream& ss, std::stack<FmtString::iterator>& stack)
{
while (!stack.empty())
{
auto& it = stack.top();
while (!it.eol())
{
const auto& token = *it;
if (!FormatTokenTakesArgument(token.kind))
{
ss << token.text;
}
it++;
}
stack.pop();
}
}
template<typename TArg0, typename... TArgs>
static void FormatString(std::stringstream& ss, std::stack<FmtString::iterator>& stack, TArg0 arg0, TArgs&&... argN)
{
while (!stack.empty())
{
auto& it = stack.top();
while (!it.eol())
{
auto token = *it++;
if (token.kind == FormatToken::StringId)
{
if constexpr (std::is_integral<TArg0>())
{
auto stringId = static_cast<rct_string_id>(arg0);
if (IsRealNameStringId(stringId))
{
FormatRealName(ss, stringId);
return FormatString(ss, stack, argN...);
}
else
{
auto subfmt = GetFmtStringById(stringId);
auto subit = subfmt.begin();
stack.push(subit);
return FormatString(ss, stack, argN...);
}
}
}
else if (FormatTokenTakesArgument(token.kind))
{
FormatArgument(ss, token.kind, arg0);
return FormatString(ss, stack, argN...);
}
else
{
ss << token.text;
}
}
stack.pop();
}
}
template<typename... TArgs> static void FormatString(std::stringstream& ss, const FmtString& fmt, TArgs&&... argN)
{
std::stack<FmtString::iterator> stack;
stack.push(fmt.begin());
FormatString(ss, stack, argN...);
}
template<typename... TArgs> std::string FormatString(const FmtString& fmt, TArgs&&... argN)
{
auto& ss = GetThreadFormatStream();
FormatString(ss, fmt, argN...);
return ss.str();
}
template<typename... TArgs>
size_t FormatStringToBuffer(char* buffer, size_t bufferLen, const FmtString& fmt, TArgs&&... argN)
{
auto& ss = GetThreadFormatStream();
FormatString(ss, fmt, argN...);
return CopyStringStreamToBuffer(buffer, bufferLen, ss);
}
template<typename... TArgs> static void FormatStringId(std::stringstream& ss, rct_string_id id, TArgs&&... argN)
{
auto fmt = GetFmtStringById(id);
FormatString(ss, fmt, argN...);
}
template<typename... TArgs> std::string FormatStringId(rct_string_id id, TArgs&&... argN)
{
auto fmt = GetFmtStringById(id);
return FormatString(fmt, argN...);
}
template<typename... TArgs> size_t FormatStringId(char* buffer, size_t bufferLen, rct_string_id id, TArgs&&... argN)
{
auto& ss = GetThreadFormatStream();
FormatStringId(ss, id, argN...);
return CopyStringStreamToBuffer(buffer, bufferLen, ss);
}
std::string FormatStringAny(const FmtString& fmt, const std::vector<FormatArg_t>& args);
size_t FormatStringAny(char* buffer, size_t bufferLen, const FmtString& fmt, const std::vector<FormatArg_t>& args);
size_t FormatStringLegacy(char* buffer, size_t bufferLen, rct_string_id id, const void* args);
} // namespace OpenRCT2

View File

@ -51,29 +51,6 @@ const language_descriptor LanguagesDescriptors[LANGUAGE_COUNT] =
};
// clang-format on
// clang-format off
const utf8 BlackUpArrowString[] = { static_cast<utf8>(static_cast<uint8_t>(0xC2)), static_cast<utf8>(static_cast<uint8_t>(0x8E)), static_cast<utf8>(static_cast<uint8_t>(0xE2)), static_cast<utf8>(static_cast<uint8_t>(0x96)), static_cast<utf8>(static_cast<uint8_t>(0xB2)), static_cast<utf8>(static_cast<uint8_t>(0x00)) };
const utf8 BlackDownArrowString[] = { static_cast<utf8>(static_cast<uint8_t>(0xC2)), static_cast<utf8>(static_cast<uint8_t>(0x8E)), static_cast<utf8>(static_cast<uint8_t>(0xE2)), static_cast<utf8>(static_cast<uint8_t>(0x96)), static_cast<utf8>(static_cast<uint8_t>(0xBC)), static_cast<utf8>(static_cast<uint8_t>(0x00)) };
const utf8 BlackLeftArrowString[] = { static_cast<utf8>(static_cast<uint8_t>(0xC2)), static_cast<utf8>(static_cast<uint8_t>(0x8E)), static_cast<utf8>(static_cast<uint8_t>(0xE2)), static_cast<utf8>(static_cast<uint8_t>(0x97)), static_cast<utf8>(static_cast<uint8_t>(0x80)), static_cast<utf8>(static_cast<uint8_t>(0x00)) };
const utf8 BlackRightArrowString[] = { static_cast<utf8>(static_cast<uint8_t>(0xC2)), static_cast<utf8>(static_cast<uint8_t>(0x8E)), static_cast<utf8>(static_cast<uint8_t>(0xE2)), static_cast<utf8>(static_cast<uint8_t>(0x96)), static_cast<utf8>(static_cast<uint8_t>(0xB6)), static_cast<utf8>(static_cast<uint8_t>(0x00)) };
const utf8 CheckBoxMarkString[] = { static_cast<utf8>(static_cast<uint8_t>(0xE2)), static_cast<utf8>(static_cast<uint8_t>(0x9C)), static_cast<utf8>(static_cast<uint8_t>(0x93)), static_cast<utf8>(static_cast<uint8_t>(0x00)) };
// clang-format on
void utf8_remove_format_codes(utf8* text, bool allowcolours)
{
const utf8* ch = text;
utf8* dstCh = text;
int32_t codepoint;
while ((codepoint = String::GetNextCodepoint(ch, &ch)) != 0)
{
if (!utf8_is_format_code(codepoint) || (allowcolours && utf8_is_colour_code(codepoint)))
{
dstCh = String::WriteCodepoint(dstCh, codepoint);
}
}
*dstCh = 0;
}
uint8_t language_get_id_from_locale(const char* locale)
{
uint8_t i = 0;
@ -133,113 +110,3 @@ rct_string_id language_allocate_object_string(const std::string& target)
auto& localisationService = OpenRCT2::GetContext()->GetLocalisationService();
return localisationService.AllocateObjectString(target);
}
std::string language_convert_string_to_tokens(const std::string_view& s)
{
std::string result;
result.reserve(s.size() * 4);
std::string input = std::string(s);
auto readPtr = input.c_str();
while (true)
{
char32_t code = utf8_get_next(readPtr, const_cast<const utf8**>(&readPtr));
if (code == 0)
{
break;
}
else if (code == '\n')
{
result.push_back('\n');
}
else if (utf8_is_format_code(code))
{
auto token = format_get_token(code);
result.push_back('{');
result.append(token);
result.push_back('}');
}
else
{
char buffer[8]{};
utf8_write_codepoint(buffer, code);
result.append(buffer);
}
}
result.shrink_to_fit();
return result;
}
std::string language_convert_string(const std::string_view& s)
{
enum class PARSE_STATE
{
DEFAULT,
CR,
TOKEN,
};
std::string result;
std::string token;
PARSE_STATE state{};
token.reserve(64);
result.reserve(s.size() * 2);
for (char c : s)
{
switch (state)
{
case PARSE_STATE::CR:
result.push_back(FORMAT_NEWLINE);
state = PARSE_STATE::DEFAULT;
[[fallthrough]];
case PARSE_STATE::DEFAULT:
switch (c)
{
case '\r':
state = PARSE_STATE::CR;
break;
case '\n':
result.push_back(FORMAT_NEWLINE);
break;
case '{':
token.clear();
state = PARSE_STATE::TOKEN;
break;
default:
if (static_cast<uint8_t>(c) >= 32)
{
result.push_back(c);
}
break;
}
break;
case PARSE_STATE::TOKEN:
if (c == '}')
{
auto code = format_get_code(token.c_str());
if (code == 0)
{
int32_t number{};
if (sscanf(token.c_str(), "%d", &number) == 1)
{
auto b = static_cast<uint8_t>(std::clamp(number, 0, 255));
token.push_back(b);
}
}
else
{
char buffer[8]{};
utf8_write_codepoint(buffer, code);
result.append(buffer);
}
state = PARSE_STATE::DEFAULT;
}
else
{
token.push_back(c);
}
break;
}
}
result.shrink_to_fit();
return result;
}

View File

@ -85,11 +85,11 @@ struct language_descriptor
extern const language_descriptor LanguagesDescriptors[LANGUAGE_COUNT];
extern const utf8 BlackUpArrowString[];
extern const utf8 BlackDownArrowString[];
extern const utf8 BlackLeftArrowString[];
extern const utf8 BlackRightArrowString[];
extern const utf8 CheckBoxMarkString[];
constexpr const char* BlackUpArrowString = u8"{BLACK}▲";
constexpr const char* BlackDownArrowString = u8"{BLACK}▼";
constexpr const char* BlackLeftArrowString = u8"{BLACK}◀";
constexpr const char* BlackRightArrowString = u8"{BLACK}▶";
constexpr const char* CheckBoxMarkString = u8"";
uint8_t language_get_id_from_locale(const char* locale);
const char* language_get_string(rct_string_id id);
@ -98,7 +98,6 @@ bool language_open(int32_t id);
uint32_t utf8_get_next(const utf8* char_ptr, const utf8** nextchar_ptr);
int32_t utf8_insert_codepoint(utf8* dst, uint32_t codepoint);
bool utf8_is_codepoint_start(const utf8* text);
void utf8_remove_format_codes(utf8* text, bool allowcolours);
int32_t utf8_get_codepoint_length(char32_t codepoint);
int32_t utf8_length(const utf8* text);
@ -107,8 +106,6 @@ std::string utf8_to_rct2(const std::string_view& src);
bool language_get_localised_scenario_strings(const utf8* scenarioFilename, rct_string_id* outStringIds);
void language_free_object_string(rct_string_id stringId);
rct_string_id language_allocate_object_string(const std::string& target);
std::string language_convert_string_to_tokens(const std::string_view& s);
std::string language_convert_string(const std::string_view& s);
constexpr utf8* utf8_write_codepoint(utf8* dst, uint32_t codepoint)
{

View File

@ -539,32 +539,8 @@ private:
sb.Clear();
while (reader->TryPeek(&codepoint) && !IsNewLine(codepoint))
{
if (codepoint == '{')
{
uint32_t token;
bool isByte;
if (ParseToken(reader, &token, &isByte))
{
if (isByte)
{
sb.Append(reinterpret_cast<const utf8*>(&token), 1);
}
else
{
sb.Append(static_cast<int32_t>(token));
}
}
else
{
// Syntax error or unknown token, ignore line entirely
return;
}
}
else
{
reader->Skip();
sb.Append(codepoint);
}
reader->Skip();
sb.Append(codepoint);
}
std::string s;
@ -599,47 +575,6 @@ private:
}
}
}
bool ParseToken(IStringReader* reader, uint32_t* token, bool* isByte)
{
auto sb = StringBuilder();
codepoint_t codepoint;
// Skip open brace
reader->Skip();
while (reader->TryPeek(&codepoint))
{
if (IsNewLine(codepoint))
return false;
if (IsWhitespace(codepoint))
return false;
reader->Skip();
if (codepoint == '}')
break;
sb.Append(codepoint);
}
const utf8* tokenName = sb.GetBuffer();
*token = format_get_code(tokenName);
*isByte = false;
// Handle explicit byte values
if (*token == 0)
{
int32_t number;
if (sscanf(tokenName, "%d", &number) == 1)
{
*token = std::clamp(number, 0, 255);
*isByte = true;
}
}
return true;
}
};
namespace LanguagePackFactory

File diff suppressed because it is too large Load Diff

View File

@ -19,15 +19,8 @@
#include <string>
bool utf8_is_format_code(char32_t codepoint);
bool utf8_is_colour_code(char32_t codepoint);
bool utf8_should_use_sprite_for_codepoint(char32_t codepoint);
int32_t utf8_get_format_code_arg_length(char32_t codepoint);
void utf8_remove_formatting(utf8* string, bool allowColours);
std::string format_string(rct_string_id format, const void* args);
void format_string(char* dest, size_t size, rct_string_id format, const void* args);
void format_string_raw(char* dest, size_t size, const char* src, const void* args);
void format_string_to_upper(char* dest, size_t size, rct_string_id format, const void* args);
void generate_string_file();
@ -43,7 +36,6 @@ void format_readable_speed(char* buf, size_t bufSize, uint64_t sizeBytesPerSec);
utf8* get_string_end(const utf8* text);
size_t get_string_size(const utf8* text);
int32_t get_string_length(const utf8* text);
// The maximum number of characters allowed for string/money conversions (anything above will risk integer overflow issues)
#define MONEY_STRING_MAXLENGTH 14

View File

@ -18,7 +18,7 @@ constexpr const rct_string_id STR_VIEWPORT = 0xFFFE;
enum
{
STR_EMPTY = 0,
// STR_0001 :{STRINGID} {COMMA16}
STR_RIDE_NAME_DEFAULT = 1,
STR_RIDE_NAME_SPIRAL_ROLLER_COASTER = 2,
STR_RIDE_NAME_STAND_UP_ROLLER_COASTER = 3,
STR_RIDE_NAME_SUSPENDED_SWINGING_COASTER = 4,

View File

@ -112,15 +112,7 @@ int32_t utf8_length(const utf8* text)
*/
utf8* get_string_end(const utf8* text)
{
int32_t codepoint;
const utf8* ch = text;
while ((codepoint = utf8_get_next(ch, &ch)) != 0)
{
int32_t argLength = utf8_get_format_code_arg_length(codepoint);
ch += argLength;
}
return const_cast<utf8*>(ch - 1);
return const_cast<char*>(std::strchr(text, 0));
}
/**
@ -130,83 +122,3 @@ size_t get_string_size(const utf8* text)
{
return get_string_end(text) - text + 1;
}
/**
* Return the number of visible characters (excludes format codes) in the given UTF-8 string.
*/
int32_t get_string_length(const utf8* text)
{
char32_t codepoint;
const utf8* ch = text;
int32_t count = 0;
while ((codepoint = utf8_get_next(ch, &ch)) != 0)
{
if (utf8_is_format_code(codepoint))
{
ch += utf8_get_format_code_arg_length(codepoint);
}
else
{
count++;
}
}
return count;
}
int32_t utf8_get_format_code_arg_length(char32_t codepoint)
{
switch (codepoint)
{
case FORMAT_MOVE_X:
case FORMAT_ADJUST_PALETTE:
case FORMAT_3:
case FORMAT_4:
return 1;
case FORMAT_NEWLINE_X_Y:
return 2;
case FORMAT_INLINE_SPRITE:
return 4;
default:
return 0;
}
}
void utf8_remove_formatting(utf8* string, bool allowColours)
{
utf8* readPtr = string;
utf8* writePtr = string;
while (true)
{
char32_t code = utf8_get_next(readPtr, const_cast<const utf8**>(&readPtr));
if (code == 0)
{
*writePtr = 0;
break;
}
else if (!utf8_is_format_code(code) || (allowColours && utf8_is_colour_code(code)))
{
writePtr = utf8_write_codepoint(writePtr, code);
}
}
}
bool utf8_is_format_code(char32_t codepoint)
{
if (codepoint < 32)
return true;
if (codepoint >= FORMAT_ARGUMENT_CODE_START && codepoint <= FORMAT_ARGUMENT_CODE_END)
return true;
if (codepoint >= FORMAT_COLOUR_CODE_START && codepoint <= FORMAT_COLOUR_CODE_END)
return true;
if (codepoint == FORMAT_COMMA1DP16)
return true;
return false;
}
bool utf8_is_colour_code(char32_t codepoint)
{
return codepoint >= FORMAT_COLOUR_CODE_START && codepoint <= FORMAT_COLOUR_CODE_END;
}

View File

@ -695,23 +695,18 @@ NetworkGroup* NetworkBase::GetGroupByID(uint8_t id)
const char* NetworkBase::FormatChat(NetworkPlayer* fromplayer, const char* text)
{
static char formatted[1024];
char* lineCh = formatted;
formatted[0] = 0;
static std::string formatted;
formatted.clear();
formatted += "{OUTLINE}";
if (fromplayer)
{
lineCh = utf8_write_codepoint(lineCh, FORMAT_OUTLINE);
lineCh = utf8_write_codepoint(lineCh, FORMAT_BABYBLUE);
safe_strcpy(lineCh, static_cast<const char*>(fromplayer->Name.c_str()), sizeof(formatted) - (lineCh - formatted));
safe_strcat(lineCh, ": ", sizeof(formatted) - (lineCh - formatted));
lineCh = strchr(lineCh, '\0');
formatted += "{BABYBLUE}";
formatted += fromplayer->Name;
formatted += ": ";
}
lineCh = utf8_write_codepoint(lineCh, FORMAT_OUTLINE);
lineCh = utf8_write_codepoint(lineCh, FORMAT_WHITE);
char* ptrtext = lineCh;
safe_strcpy(lineCh, text, 800);
utf8_remove_format_codes(ptrtext, true);
return formatted;
formatted += "{WHITE}";
formatted += text;
return formatted.c_str();
}
void NetworkBase::SendPacketToClients(const NetworkPacket& packet, bool front, bool gameCmd)
@ -1092,7 +1087,6 @@ void NetworkBase::AppendLog(std::ostream& fs, const std::string& s)
if (strftime(buffer, sizeof(buffer), "[%Y/%m/%d %H:%M:%S] ", tmInfo) != 0)
{
String::Append(buffer, sizeof(buffer), s.c_str());
utf8_remove_formatting(buffer, false);
String::Append(buffer, sizeof(buffer), PLATFORM_NEWLINE);
fs.write(buffer, strlen(buffer));
@ -3467,18 +3461,13 @@ void network_chat_show_connected_message()
// Display server greeting if one exists
void network_chat_show_server_greeting()
{
const char* greeting = network_get_server_greeting();
auto greeting = network_get_server_greeting();
if (!str_is_null_or_empty(greeting))
{
static char greeting_formatted[CHAT_INPUT_SIZE];
char* lineCh = greeting_formatted;
greeting_formatted[0] = 0;
lineCh = utf8_write_codepoint(lineCh, FORMAT_OUTLINE);
lineCh = utf8_write_codepoint(lineCh, FORMAT_GREEN);
char* ptrtext = lineCh;
safe_strcpy(lineCh, greeting, CHAT_INPUT_SIZE - 24); // Limit to 1000 characters so we don't overflow the buffer
utf8_remove_format_codes(ptrtext, true);
chat_history_add(greeting_formatted);
thread_local std::string greeting_formatted;
greeting_formatted.assign("{OUTLINE}{GREEN}");
greeting_formatted += greeting;
chat_history_add(greeting_formatted.c_str());
}
}

View File

@ -19,7 +19,6 @@ void NetworkPlayer::SetName(const std::string& name)
{
// 36 == 31 + strlen(" #255");
Name = name.substr(0, 36);
utf8_remove_format_codes(static_cast<utf8*>(Name.data()), false);
}
void NetworkPlayer::Read(NetworkPacket& packet)

View File

@ -19,6 +19,7 @@
#include "../interface/Chat.h"
#include "../interface/InteractiveConsole.h"
#include "../localisation/FormatCodes.h"
#include "../localisation/Formatting.h"
#include "../localisation/Language.h"
#include "../paint/Paint.h"
#include "../title/TitleScreen.h"
@ -83,16 +84,10 @@ void Painter::PaintReplayNotice(rct_drawpixelinfo* dpi, const char* text)
{
ScreenCoordsXY screenCoords(_uiContext->GetWidth() / 2, _uiContext->GetHeight() - 44);
// Format string
utf8 buffer[64] = { 0 };
utf8* ch = buffer;
ch = utf8_write_codepoint(ch, FORMAT_MEDIUMFONT);
ch = utf8_write_codepoint(ch, FORMAT_OUTLINE);
ch = utf8_write_codepoint(ch, FORMAT_RED);
char buffer[64]{};
FormatStringToBuffer(buffer, sizeof(buffer), "{MEDIUMFONT}{OUTLINE}{RED}{STRING}", text);
snprintf(ch, 64 - (ch - buffer), "%s", text);
int32_t stringWidth = gfx_get_string_width(buffer);
auto stringWidth = gfx_get_string_width(buffer);
screenCoords.x = screenCoords.x - stringWidth;
if (((gCurrentTicks >> 1) & 0xF) > 4)
@ -106,17 +101,10 @@ void Painter::PaintFPS(rct_drawpixelinfo* dpi)
{
ScreenCoordsXY screenCoords(_uiContext->GetWidth() / 2, 2);
// Measure FPS
MeasureFPS();
// Format string
utf8 buffer[64] = { 0 };
utf8* ch = buffer;
ch = utf8_write_codepoint(ch, FORMAT_MEDIUMFONT);
ch = utf8_write_codepoint(ch, FORMAT_OUTLINE);
ch = utf8_write_codepoint(ch, FORMAT_WHITE);
snprintf(ch, 64 - (ch - buffer), "%d", _currentFPS);
char buffer[64]{};
FormatStringToBuffer(buffer, sizeof(buffer), "{MEDIUMFONT}{OUTLINE}{WHITE}{INT32}", _currentFPS);
// Draw Text
int32_t stringWidth = gfx_get_string_width(buffer);

View File

@ -3004,10 +3004,10 @@ private:
std::string GetUserString(rct_string_id stringId)
{
const auto originalString = _s4.string_table[(stringId - USER_STRING_START) % 1024];
std::string_view originalStringView(originalString, USER_STRING_MAX_LENGTH);
auto originalStringView = String::ToStringView(originalString, USER_STRING_MAX_LENGTH);
auto asUtf8 = rct2_to_utf8(originalStringView, RCT2_LANGUAGE_ID_ENGLISH_UK);
utf8_remove_format_codes(asUtf8.data(), /*allow colour*/ false);
return asUtf8.data();
auto justText = RCT12RemoveFormattingUTF8(asUtf8);
return justText.data();
}
void FixLandOwnership()

View File

@ -9,6 +9,7 @@
#include "RCT12.h"
#include "../core/String.hpp"
#include "../localisation/Localisation.h"
#include "../ride/Track.h"
#include "../world/Banner.h"
@ -1040,3 +1041,54 @@ RCT12RideId OpenRCT2RideIdToRCT12RideId(const ride_id_t rideId)
return rideId;
}
static bool RCT12IsFormatChar(codepoint_t c)
{
if (c >= RCT2_STRING_FORMAT_ARG_START && c <= RCT2_STRING_FORMAT_ARG_END)
{
return true;
}
if (c >= RCT2_STRING_FORMAT_COLOUR_START && c <= RCT2_STRING_FORMAT_COLOUR_END)
{
return true;
}
return false;
}
static bool RCT12IsFormatChar(char c)
{
return RCT12IsFormatChar(static_cast<codepoint_t>(c));
}
bool IsLikelyUTF8(std::string_view s)
{
// RCT2 uses CP-1252 so some characters may be >= 128. However we don't expect any
// characters that are reserved for formatting strings, so if those are found, assume
// that the string is UTF-8.
for (auto c : s)
{
if (RCT12IsFormatChar(c))
{
return true;
}
}
return false;
}
std::string RCT12RemoveFormattingUTF8(std::string_view s)
{
std::string result;
result.reserve(s.size() * 2);
CodepointView codepoints(s);
for (auto codepoint : codepoints)
{
if (!RCT12IsFormatChar(codepoint))
{
String::AppendCodepoint(result, codepoint);
}
}
result.shrink_to_fit();
return result;
}

View File

@ -18,6 +18,11 @@
#include <string>
#include <string_view>
constexpr uint8_t RCT2_STRING_FORMAT_ARG_START = 123;
constexpr uint8_t RCT2_STRING_FORMAT_ARG_END = 141;
constexpr uint8_t RCT2_STRING_FORMAT_COLOUR_START = 142;
constexpr uint8_t RCT2_STRING_FORMAT_COLOUR_END = 156;
constexpr const uint8_t RCT12_MAX_RIDES_IN_PARK = 255;
constexpr const uint8_t RCT12_MAX_AWARDS = 4;
constexpr const uint8_t RCT12_MAX_NEWS_ITEMS = 61;
@ -862,3 +867,5 @@ ObjectEntryIndex RCTEntryIndexToOpenRCT2EntryIndex(const RCT12ObjectEntryIndex i
RCT12ObjectEntryIndex OpenRCT2EntryIndexToRCTEntryIndex(const ObjectEntryIndex index);
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);

View File

@ -1367,13 +1367,13 @@ void S6Exporter::ExportBanner(RCT12Banner& dst, const Banner& src)
dst.string_idx = STR_DEFAULT_SIGN;
auto bannerText = src.text;
std::string bannerText;
if (!(src.flags & BANNER_FLAG_IS_WALL) && !(src.flags & BANNER_FLAG_IS_LARGE_SCENERY))
{
char codeBuffer[32]{};
utf8_write_codepoint(codeBuffer, FORMAT_COLOUR_CODE_START + src.text_colour);
bannerText = codeBuffer + bannerText;
auto formatCode = static_cast<codepoint_t>(RCT2_STRING_FORMAT_COLOUR_START + src.text_colour);
String::AppendCodepoint(bannerText, formatCode);
}
bannerText.append(src.text);
auto stringId = AllocateUserString(bannerText);
if (stringId != std::nullopt)

View File

@ -33,6 +33,7 @@
#include "../object/ObjectManager.h"
#include "../object/ObjectRepository.h"
#include "../peep/Staff.h"
#include "../rct12/RCT12.h"
#include "../rct12/SawyerChunkReader.h"
#include "../rct12/SawyerEncoding.h"
#include "../rct2/RCT2.h"
@ -195,15 +196,7 @@ public:
gS6Info = _s6.info;
// Some scenarios have their scenario details in UTF-8, due to earlier bugs in OpenRCT2.
// This is hard to detect. Therefore, consider invalid characters like colour codes as a sign the text is in UTF-8.
bool alreadyInUTF8 = false;
if (String::ContainsColourCode(_s6.info.name) || String::ContainsColourCode(_s6.info.details))
{
alreadyInUTF8 = true;
}
if (!alreadyInUTF8)
if (!IsLikelyUTF8(_s6.info.name) && !IsLikelyUTF8(_s6.info.details))
{
auto temp = rct2_to_utf8(_s6.info.name, RCT2_LANGUAGE_ID_ENGLISH_UK);
safe_strcpy(gS6Info.name, temp.data(), sizeof(gS6Info.name));
@ -1666,10 +1659,10 @@ public:
std::string GetUserString(rct_string_id stringId)
{
const auto originalString = _s6.custom_strings[(stringId - USER_STRING_START) % 1024];
std::string_view originalStringView(originalString, USER_STRING_MAX_LENGTH);
auto originalStringView = String::ToStringView(originalString, USER_STRING_MAX_LENGTH);
auto asUtf8 = rct2_to_utf8(originalStringView, RCT2_LANGUAGE_ID_ENGLISH_UK);
utf8_remove_format_codes(asUtf8.data(), /*allow colour*/ false);
return asUtf8.data();
auto justText = RCT12RemoveFormattingUTF8(asUtf8);
return justText.data();
}
std::vector<rct_object_entry> GetRequiredObjects()

View File

@ -25,6 +25,7 @@
#include "../localisation/Localisation.h"
#include "../localisation/LocalisationService.h"
#include "../platform/Platform2.h"
#include "../rct12/RCT12.h"
#include "../rct12/SawyerChunkReader.h"
#include "Scenario.h"
#include "ScenarioSources.h"
@ -261,7 +262,7 @@ private:
rct_s6_info info = chunkReader.ReadChunkAs<rct_s6_info>();
// If the name or the details contain a colour code, they might be in UTF-8 already.
// This is caused by a bug that was in OpenRCT2 for 3 years.
if (!String::ContainsColourCode(info.name) && !String::ContainsColourCode(info.details))
if (!IsLikelyUTF8(info.name) && !IsLikelyUTF8(info.details))
{
rct2_to_utf8_self(info.name, sizeof(info.name));
rct2_to_utf8_self(info.details, sizeof(info.details));

View File

@ -63,7 +63,7 @@ namespace OpenRCT2::Scripting
result.MonthYear = value["month"].as_int();
result.Day = value["day"].as_int();
auto text = language_convert_string(value["text"].as_string());
auto text = value["text"].as_string();
String::Set(result.Text, sizeof(result.Text), text.c_str());
return result;
}
@ -207,7 +207,7 @@ namespace OpenRCT2::Scripting
auto msg = GetMessage();
if (msg != nullptr)
{
return language_convert_string_to_tokens(msg->Text);
return msg->Text;
}
return 0;
}
@ -218,8 +218,7 @@ namespace OpenRCT2::Scripting
auto msg = GetMessage();
if (msg != nullptr)
{
auto text = language_convert_string(value);
String::Set(msg->Text, sizeof(msg->Text), text.c_str());
String::Set(msg->Text, sizeof(msg->Text), value.c_str());
}
}
@ -395,12 +394,12 @@ namespace OpenRCT2::Scripting
std::string text;
if (message.type() == DukValue::Type::STRING)
{
text = language_convert_string(message.as_string());
text = message.as_string();
}
else
{
type = GetParkMessageType(message["type"].as_string());
text = language_convert_string(message["text"].as_string());
text = message["text"].as_string();
if (type == News::ItemType::Blank)
{
assoc = static_cast<uint32_t>(((COORDS_NULL & 0xFFFF) << 16) | (COORDS_NULL & 0xFFFF));

View File

@ -1267,7 +1267,7 @@ std::string OpenRCT2::Scripting::Stringify(const DukValue& val)
std::string OpenRCT2::Scripting::ProcessString(const DukValue& value)
{
if (value.type() == DukValue::Type::STRING)
return language_convert_string(value.as_string());
return value.as_string();
return {};
}

View File

@ -429,17 +429,13 @@ bool title_is_previewing_sequence()
void DrawOpenRCT2(rct_drawpixelinfo* dpi, const ScreenCoordsXY& screenCoords)
{
utf8 buffer[256];
// Write format codes
utf8* ch = buffer;
ch = utf8_write_codepoint(ch, FORMAT_MEDIUMFONT);
ch = utf8_write_codepoint(ch, FORMAT_OUTLINE);
ch = utf8_write_codepoint(ch, FORMAT_WHITE);
thread_local std::string buffer;
buffer.clear();
buffer.assign("{MEDIUMFONT}{OUTLINE}{WHITE}");
// Write name and version information
openrct2_write_full_version_info(ch, sizeof(buffer) - (ch - buffer));
gfx_draw_string(dpi, buffer, COLOUR_BLACK, screenCoords + ScreenCoordsXY(5, 5 - 13));
buffer += gVersionInfoFull;
gfx_draw_string(dpi, buffer.c_str(), COLOUR_BLACK, screenCoords + ScreenCoordsXY(5, 5 - 13));
// Invalidate screen area
int16_t width = static_cast<int16_t>(gfx_get_string_width(buffer));
@ -447,6 +443,10 @@ void DrawOpenRCT2(rct_drawpixelinfo* dpi, const ScreenCoordsXY& screenCoords)
{ screenCoords, screenCoords + ScreenCoordsXY{ width, 30 } }); // 30 is an arbitrary height to catch both strings
// Write platform information
snprintf(ch, 256 - (ch - buffer), "%s (%s)", OPENRCT2_PLATFORM, OPENRCT2_ARCHITECTURE);
gfx_draw_string(dpi, buffer, COLOUR_BLACK, screenCoords + ScreenCoordsXY(5, 5));
buffer.assign("{MEDIUMFONT}{OUTLINE}{WHITE}");
buffer.append(OPENRCT2_PLATFORM);
buffer.append(" (");
buffer.append(OPENRCT2_ARCHITECTURE);
buffer.append(")");
gfx_draw_string(dpi, buffer.c_str(), COLOUR_BLACK, screenCoords + ScreenCoordsXY(5, 5));
}

View File

@ -33,30 +33,6 @@
static Banner _banners[MAX_BANNERS];
namespace
{
template<uint32_t TFrom, uint32_t TTo> struct CodePointToUtf8
{
constexpr CodePointToUtf8()
{
for (uint32_t i = TFrom; i <= TTo; ++i)
{
utf8_write_codepoint(m_colors[i - TFrom], i);
}
}
constexpr auto operator()(uint8_t colourId) const
{
return m_colors[colourId];
}
using Utf8Colour = utf8[5]; // A 32bit codepoint uses at most 4 bytes in utf8
Utf8Colour m_colors[TTo - TFrom + 1]{};
};
} // namespace
static constexpr CodePointToUtf8<FORMAT_COLOUR_CODE_START, FORMAT_COLOUR_CODE_END> colourToUtf8;
std::string Banner::GetText() const
{
Formatter ft;
@ -68,7 +44,10 @@ void Banner::FormatTextTo(Formatter& ft, bool addColour) const
{
if (addColour)
{
ft.Add<rct_string_id>(STR_STRING_STRINGID).Add<const char*>(colourToUtf8(text_colour));
auto formatToken = FormatTokenFromTextColour(text_colour);
auto tokenText = FormatTokenToString(formatToken, true);
ft.Add<rct_string_id>(STR_STRING_STRINGID);
ft.Add<const char*>(tokenText.data());
}
FormatTextTo(ft);

View File

@ -170,6 +170,14 @@ target_link_libraries(test_string ${GTEST_LIBRARIES} test-common ${LDL} z)
target_link_platform_libraries(test_string)
add_test(NAME string COMMAND test_string)
# Formatting tests
set(STRING_TEST_SOURCES "${CMAKE_CURRENT_LIST_DIR}/FormattingTests.cpp")
add_executable(test_formatting ${STRING_TEST_SOURCES})
SET_CHECK_CXX_FLAGS(test_formatting)
target_link_libraries(test_formatting ${GTEST_LIBRARIES} libopenrct2 ${LDL} z)
target_link_platform_libraries(test_formatting)
add_test(NAME formatting COMMAND test_formatting)
# Localisation test
set(STRING_TEST_SOURCES "${CMAKE_CURRENT_LIST_DIR}/Localisation.cpp")
add_executable(test_localisation ${STRING_TEST_SOURCES})

View File

@ -0,0 +1,338 @@
/*****************************************************************************
* Copyright (c) 2014-2020 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
#include "openrct2/localisation/Formatting.h"
#include <gtest/gtest.h>
#include <openrct2/Context.h>
#include <openrct2/OpenRCT2.h>
#include <openrct2/config/Config.h>
#include <openrct2/core/String.hpp>
#include <openrct2/localisation/Localisation.h>
#include <openrct2/localisation/StringIds.h>
using namespace OpenRCT2;
class FmtStringTests : public testing::Test
{
};
TEST_F(FmtStringTests, string_owned)
{
auto fmt = FmtString(std::string("{BLACK}Guests: {INT32}"));
ASSERT_EQ("Guests: ", fmt.WithoutFormatTokens());
}
TEST_F(FmtStringTests, iteration)
{
std::string actual;
auto fmt = FmtString("{BLACK}Guests: {INT32}");
for (const auto& t : fmt)
{
actual += String::StdFormat("[%d:%s]", t.kind, std::string(t.text).c_str());
}
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)
{
auto fmt = FmtString("{BLACK}Guests: {INT32}");
ASSERT_EQ("Guests: ", fmt.WithoutFormatTokens());
}
class FormattingTests : public testing::Test
{
private:
static std::shared_ptr<IContext> _context;
protected:
static void SetUpTestCase()
{
gOpenRCT2Headless = true;
gOpenRCT2NoGraphics = true;
_context = CreateContext();
ASSERT_TRUE(_context->Initialise());
language_open(LANGUAGE_ENGLISH_UK);
}
static void TearDownTestCase()
{
_context = {};
}
};
std::shared_ptr<IContext> FormattingTests::_context;
TEST_F(FormattingTests, no_args)
{
auto actual = FormatString("test string");
ASSERT_EQ("test string", actual);
}
TEST_F(FormattingTests, missing_arg)
{
auto actual = FormatString("test {STRING} arg");
ASSERT_EQ("test arg", actual);
}
TEST_F(FormattingTests, integer)
{
auto actual = FormatString("Guests: {INT32}", 32);
ASSERT_EQ("Guests: 32", actual);
}
TEST_F(FormattingTests, integer_integer)
{
auto actual = FormatString("Guests: {INT32}, Staff: {INT32}", 32, 10);
ASSERT_EQ("Guests: 32, Staff: 10", actual);
}
TEST_F(FormattingTests, comma)
{
auto actual = FormatString("Guests: {COMMA16}", 12534);
ASSERT_EQ("Guests: 12,534", actual);
}
TEST_F(FormattingTests, comma_0)
{
auto actual = FormatString("Guests: {COMMA16}", 0);
ASSERT_EQ("Guests: 0", actual);
}
TEST_F(FormattingTests, comma_large_negative)
{
auto actual = FormatString("{COMMA16}", std::numeric_limits<int64_t>::min());
ASSERT_EQ("-9,223,372,036,854,775,808", actual);
}
TEST_F(FormattingTests, comma_large)
{
auto actual = FormatString("{COMMA16}", std::numeric_limits<uint64_t>::max());
ASSERT_EQ("18,446,744,073,709,551,615", actual);
}
TEST_F(FormattingTests, currency)
{
gConfigGeneral.currency_format = CurrencyType::Pounds;
ASSERT_EQ(u8"-£251", FormatString("{CURRENCY}", -2510));
ASSERT_EQ(u8"£1", FormatString("{CURRENCY}", 4));
ASSERT_EQ(u8"£1", FormatString("{CURRENCY}", 5));
ASSERT_EQ(u8"£1", FormatString("{CURRENCY}", 10));
ASSERT_EQ(u8"£2", FormatString("{CURRENCY}", 11));
ASSERT_EQ(u8"£112", FormatString("{CURRENCY}", 1111));
}
TEST_F(FormattingTests, currency2dp)
{
gConfigGeneral.currency_format = CurrencyType::Pounds;
ASSERT_EQ(u8"-£251.00", FormatString("{CURRENCY2DP}", -2510));
ASSERT_EQ(u8"£0.40", FormatString("{CURRENCY2DP}", 4));
ASSERT_EQ(u8"£0.50", FormatString("{CURRENCY2DP}", 5));
ASSERT_EQ(u8"£1.00", FormatString("{CURRENCY2DP}", 10));
ASSERT_EQ(u8"£1.10", FormatString("{CURRENCY2DP}", 11));
ASSERT_EQ(u8"£111.10", FormatString("{CURRENCY2DP}", 1111));
}
TEST_F(FormattingTests, currency_yen)
{
gConfigGeneral.currency_format = CurrencyType::Yen;
ASSERT_EQ(u8"-¥25,100", FormatString("{CURRENCY}", -2510));
ASSERT_EQ(u8"¥40", FormatString("{CURRENCY2DP}", 4));
ASSERT_EQ(u8"¥50", FormatString("{CURRENCY2DP}", 5));
ASSERT_EQ(u8"¥100", FormatString("{CURRENCY2DP}", 10));
ASSERT_EQ(u8"¥110", FormatString("{CURRENCY2DP}", 11));
ASSERT_EQ(u8"¥11,110", FormatString("{CURRENCY2DP}", 1111));
}
TEST_F(FormattingTests, currency2dp_yen)
{
gConfigGeneral.currency_format = CurrencyType::Yen;
ASSERT_EQ(u8"-¥25,100", FormatString("{CURRENCY2DP}", -2510));
ASSERT_EQ(u8"¥40", FormatString("{CURRENCY2DP}", 4));
ASSERT_EQ(u8"¥50", FormatString("{CURRENCY2DP}", 5));
ASSERT_EQ(u8"¥100", FormatString("{CURRENCY2DP}", 10));
ASSERT_EQ(u8"¥110", FormatString("{CURRENCY2DP}", 11));
ASSERT_EQ(u8"¥11,110", FormatString("{CURRENCY2DP}", 1111));
}
TEST_F(FormattingTests, currency_pts)
{
gConfigGeneral.currency_format = CurrencyType::Peseta;
ASSERT_EQ("-251Pts", FormatString("{CURRENCY}", -2510));
ASSERT_EQ("112Pts", FormatString("{CURRENCY}", 1111));
}
TEST_F(FormattingTests, currency2dp_pts)
{
gConfigGeneral.currency_format = CurrencyType::Peseta;
ASSERT_EQ("-251.00Pts", FormatString("{CURRENCY2DP}", -2510));
ASSERT_EQ("0.40Pts", FormatString("{CURRENCY2DP}", 4));
ASSERT_EQ("111.10Pts", FormatString("{CURRENCY2DP}", 1111));
}
TEST_F(FormattingTests, string)
{
auto actual = FormatString("{RED}{STRING} has broken down.", "Woodchip");
ASSERT_EQ("{RED}Woodchip has broken down.", actual);
}
TEST_F(FormattingTests, escaped_braces)
{
auto actual = FormatString("--{{ESCAPED}}--", 0);
ASSERT_EQ("--{{ESCAPED}}--", actual);
}
TEST_F(FormattingTests, velocity_mph)
{
gConfigGeneral.measurement_format = MeasurementFormat::Imperial;
auto actual = FormatString("Train is going at {VELOCITY}.", 1024);
ASSERT_EQ("Train is going at 1,024 mph.", actual);
}
TEST_F(FormattingTests, velocity_kph)
{
gConfigGeneral.measurement_format = MeasurementFormat::Metric;
auto actual = FormatString("Train is going at {VELOCITY}.", 1024);
ASSERT_EQ("Train is going at 1,648 km/h.", actual);
}
TEST_F(FormattingTests, velocity_mps)
{
gConfigGeneral.measurement_format = MeasurementFormat::SI;
auto actual = FormatString("Train is going at {VELOCITY}.", 1024);
ASSERT_EQ("Train is going at 457.7 m/s.", actual);
}
TEST_F(FormattingTests, length_imperial)
{
gConfigGeneral.measurement_format = MeasurementFormat::Imperial;
auto actual = FormatString("Height: {LENGTH}", 1024);
ASSERT_EQ("Height: 3,360 ft", actual);
}
TEST_F(FormattingTests, length_metric)
{
gConfigGeneral.measurement_format = MeasurementFormat::Metric;
auto actual = FormatString("Height: {LENGTH}", 1024);
ASSERT_EQ("Height: 1,024 m", actual);
}
TEST_F(FormattingTests, length_si)
{
gConfigGeneral.measurement_format = MeasurementFormat::SI;
auto actual = FormatString("Height: {LENGTH}", 2048);
ASSERT_EQ("Height: 2,048 m", actual);
}
TEST_F(FormattingTests, minssecs)
{
ASSERT_EQ("0secs", FormatString("{DURATION}", 0));
ASSERT_EQ("1sec", FormatString("{DURATION}", 1));
ASSERT_EQ("4secs", FormatString("{DURATION}", 4));
ASSERT_EQ("1min:0secs", FormatString("{DURATION}", 60));
ASSERT_EQ("1min:1sec", FormatString("{DURATION}", 60 + 1));
ASSERT_EQ("1min:59secs", FormatString("{DURATION}", 60 + 59));
ASSERT_EQ("2mins:0secs", FormatString("{DURATION}", 120));
ASSERT_EQ("2mins:1sec", FormatString("{DURATION}", 120 + 1));
ASSERT_EQ("2mins:2secs", FormatString("{DURATION}", 120 + 2));
}
TEST_F(FormattingTests, hoursmins)
{
ASSERT_EQ("0mins", FormatString("{REALTIME}", 0));
ASSERT_EQ("1min", FormatString("{REALTIME}", 1));
ASSERT_EQ("4mins", FormatString("{REALTIME}", 4));
ASSERT_EQ("1hour:0mins", FormatString("{REALTIME}", 60));
ASSERT_EQ("1hour:1min", FormatString("{REALTIME}", 60 + 1));
ASSERT_EQ("1hour:59mins", FormatString("{REALTIME}", 60 + 59));
ASSERT_EQ("2hours:0mins", FormatString("{REALTIME}", 120));
ASSERT_EQ("2hours:1min", FormatString("{REALTIME}", 120 + 1));
ASSERT_EQ("2hours:2mins", FormatString("{REALTIME}", 120 + 2));
}
TEST_F(FormattingTests, month)
{
ASSERT_EQ("The month is March", FormatString("The month is {MONTH}", 0));
ASSERT_EQ("The month is October", FormatString("The month is {MONTH}", 7));
ASSERT_EQ("The month is April", FormatString("The month is {MONTH}", 9));
}
TEST_F(FormattingTests, monthyear)
{
ASSERT_EQ("Date: March, Year 1", FormatString("Date: {MONTHYEAR}", 0));
ASSERT_EQ("Date: October, Year 1", FormatString("Date: {MONTHYEAR}", 7));
ASSERT_EQ("Date: April, Year 2", FormatString("Date: {MONTHYEAR}", 9));
}
TEST_F(FormattingTests, two_level_format)
{
constexpr rct_string_id strDefault = STR_RIDE_NAME_DEFAULT;
constexpr rct_string_id strBoatHire = STR_RIDE_NAME_BOAT_HIRE;
auto actual = FormatString("Queuing for {STRINGID}", strDefault, strBoatHire, 2);
ASSERT_EQ("Queuing for Boat Hire 2", actual);
}
TEST_F(FormattingTests, any_string_int_string)
{
auto actual = FormatStringAny(
"{RED}{STRING} {INT32} has broken down due to '{STRING}'.", { "Twist", 2, "Mechanical failure" });
ASSERT_EQ("{RED}Twist 2 has broken down due to 'Mechanical failure'.", actual);
}
TEST_F(FormattingTests, any_two_level_format)
{
constexpr rct_string_id strDefault = STR_RIDE_NAME_DEFAULT;
constexpr rct_string_id strBoatHire = STR_RIDE_NAME_BOAT_HIRE;
auto actual = FormatStringAny("Queuing for {STRINGID}", { strDefault, strBoatHire, 2 });
ASSERT_EQ("Queuing for Boat Hire 2", actual);
}
TEST_F(FormattingTests, to_fixed_buffer)
{
char buffer[16];
std::memset(buffer, '\xFF', sizeof(buffer));
auto len = FormatStringId(buffer, 8, STR_GUEST_X, 123);
ASSERT_EQ(len, 9U);
ASSERT_STREQ("Guest 1", buffer);
// Ensure rest of the buffer was not overwritten
for (size_t i = 8; i < sizeof(buffer); i++)
{
ASSERT_EQ('\xFF', buffer[i]);
}
}
TEST_F(FormattingTests, using_legacy_buffer_args)
{
auto ft = Formatter();
ft.Add<rct_string_id>(STR_RIDE_NAME_DEFAULT);
ft.Add<rct_string_id>(STR_RIDE_NAME_BOAT_HIRE);
ft.Add<uint16_t>(2);
char buffer[32]{};
auto len = FormatStringLegacy(buffer, sizeof(buffer), STR_QUEUING_FOR, ft.Data());
ASSERT_EQ(len, 23U);
ASSERT_STREQ("Queuing for Boat Hire 2", buffer);
}

View File

@ -177,3 +177,29 @@ TEST_F(StringTest, strlogicalcmp)
EXPECT_LT(strlogicalcmp("!", "A"), 0);
EXPECT_LT(strlogicalcmp("!", "a"), 0);
}
class CodepointViewTest : public testing::Test
{
};
static std::vector<char32_t> ToVector(std::string_view s)
{
std::vector<char32_t> codepoints;
for (auto codepoint : CodepointView(s))
{
codepoints.push_back(codepoint);
}
return codepoints;
}
static void AssertCodepoints(std::string_view s, const std::vector<char32_t>& expected)
{
ASSERT_EQ(ToVector(s), expected);
}
TEST_F(CodepointViewTest, CodepointView_iterate)
{
AssertCodepoints("test", { 't', 'e', 's', 't' });
AssertCodepoints("ゲスト", { U'', U'', U'' });
AssertCodepoints("<🎢>", { U'<', U'🎢', U'>' });
}

View File

@ -59,6 +59,7 @@
<ClCompile Include="CircularBuffer.cpp" />
<ClCompile Include="CryptTests.cpp" />
<ClCompile Include="Endianness.cpp" />
<ClCompile Include="FormattingTests.cpp" />
<ClCompile Include="LanguagePackTest.cpp" />
<ClCompile Include="ImageImporterTests.cpp" />
<ClCompile Include="IniReaderTest.cpp" />