mirror of https://github.com/OpenRCT2/OpenRCT2.git
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:
commit
d58d834925
|
@ -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 */,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 "";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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, ...)
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ¤t;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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'>' });
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue