diff --git a/CMakeLists.txt b/CMakeLists.txt index 88cf1fcc7..545c40917 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -992,9 +992,8 @@ if(NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC") endif() if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") - target_compile_options(libdevilutionx PUBLIC "/W3") + target_compile_options(libdevilutionx PUBLIC "/W3" "/Zc:__cplusplus" "/utf-8") target_compile_definitions(libdevilutionx PUBLIC _CRT_SECURE_NO_WARNINGS) - target_compile_options(libdevilutionx PUBLIC "/Zc:__cplusplus") endif() if(APPLE) diff --git a/Source/DiabloUI/selconn.cpp b/Source/DiabloUI/selconn.cpp index 2088e2177..c709b5a87 100644 --- a/Source/DiabloUI/selconn.cpp +++ b/Source/DiabloUI/selconn.cpp @@ -112,7 +112,8 @@ void SelconnFocus(int value) } strncpy(selconn_MaxPlayers, fmt::format(_("Players Supported: {:d}"), players).c_str(), sizeof(selconn_MaxPlayers)); - WordWrapString(selconn_Description, DESCRIPTION_WIDTH); + const std::string wrapped = WordWrapString(selconn_Description, DESCRIPTION_WIDTH); + strncpy(selconn_Description, wrapped.data(), sizeof(selconn_Description) - 1); } void SelconnSelect(int value) diff --git a/Source/DiabloUI/selgame.cpp b/Source/DiabloUI/selgame.cpp index b6d1890fb..c85b4811e 100644 --- a/Source/DiabloUI/selgame.cpp +++ b/Source/DiabloUI/selgame.cpp @@ -109,7 +109,8 @@ void selgame_GameSelection_Focus(int value) strncpy(selgame_Description, _("Enter an IP or a hostname and join a game already in progress at that address."), sizeof(selgame_Description) - 1); break; } - WordWrapString(selgame_Description, DESCRIPTION_WIDTH); + const std::string wrapped = WordWrapString(selgame_Description, DESCRIPTION_WIDTH); + strncpy(selgame_Description, wrapped.data(), sizeof(selgame_Description) - 1); } /** @@ -212,7 +213,8 @@ void selgame_Diff_Focus(int value) strncpy(selgame_Description, _("Hell Difficulty\nThe most powerful of the underworld's creatures lurk at the gateway into Hell. Only the most experienced characters should venture in this realm."), sizeof(selgame_Description) - 1); break; } - WordWrapString(selgame_Description, DESCRIPTION_WIDTH); + const std::string wrapped = WordWrapString(selgame_Description, DESCRIPTION_WIDTH); + strncpy(selgame_Description, wrapped.data(), sizeof(selgame_Description) - 1); } bool IsDifficultyAllowed(int value) @@ -339,7 +341,8 @@ void selgame_Speed_Focus(int value) strncpy(selgame_Description, _("Fastest Speed\nThe minions of the underworld will rush to attack without hesitation. Only a true speed demon should enter at this pace."), sizeof(selgame_Description) - 1); break; } - WordWrapString(selgame_Description, DESCRIPTION_WIDTH); + const std::string wrapped = WordWrapString(selgame_Description, DESCRIPTION_WIDTH); + strncpy(selgame_Description, wrapped.data(), sizeof(selgame_Description) - 1); } void selgame_Speed_Esc() diff --git a/Source/DiabloUI/selok.cpp b/Source/DiabloUI/selok.cpp index b3491b584..c877f1782 100644 --- a/Source/DiabloUI/selok.cpp +++ b/Source/DiabloUI/selok.cpp @@ -68,8 +68,8 @@ void UiSelOkDialog(const char *title, const char *body, bool background) vecSelOkDialogItems.push_back(std::make_unique(_("OK"), 0)); vecSelOkDialog.push_back(std::make_unique(vecSelOkDialogItems, PANEL_LEFT + 230, (UI_OFFSET_Y + 390), 180, 35, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); - strncpy(dialogText, body, sizeof(dialogText) - 1); - WordWrapString(dialogText, MESSAGE_WIDTH, GameFont24); + const std::string wrapped = WordWrapString(body, MESSAGE_WIDTH, GameFont24); + strncpy(dialogText, wrapped.data(), sizeof(dialogText) - 1); UiInitList(0, nullptr, selok_Select, selok_Esc, vecSelOkDialog, false, nullptr); diff --git a/Source/DiabloUI/selyesno.cpp b/Source/DiabloUI/selyesno.cpp index c4446ed5a..636ff22c5 100644 --- a/Source/DiabloUI/selyesno.cpp +++ b/Source/DiabloUI/selyesno.cpp @@ -55,8 +55,8 @@ bool UiSelHeroYesNoDialog(const char *title, const char *body) vecSelYesNoDialogItems.push_back(std::make_unique(_("No"), 1)); vecSelYesNoDialog.push_back(std::make_unique(vecSelYesNoDialogItems, PANEL_LEFT + 230, (UI_OFFSET_Y + 390), 180, 35, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); - strncpy(selyesno_confirmationMessage, body, sizeof(selyesno_confirmationMessage) - 1); - WordWrapString(selyesno_confirmationMessage, MESSAGE_WIDTH, GameFont24); + const std::string wrapped = WordWrapString(body, MESSAGE_WIDTH, GameFont24); + strncpy(selyesno_confirmationMessage, wrapped.data(), sizeof(selyesno_confirmationMessage) - 1); UiInitList(vecSelYesNoDialogItems.size(), nullptr, SelyesnoSelect, SelyesnoEsc, vecSelYesNoDialog, true, nullptr); diff --git a/Source/control.cpp b/Source/control.cpp index 5ac5a138d..8ac4166cb 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -1642,12 +1642,12 @@ void DrawGoldSplit(const Surface &out, int amount) tempstr[BufferSize - 1] = '\0'; // Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words - WordWrapString(tempstr, 200); + const std::string wrapped = WordWrapString(tempstr, 200); // The split gold dialog is roughly 4 lines high, but we need at least one line for the player to input an amount. // Using a clipping region 50 units high (approx 3 lines with a lineheight of 17) to ensure there is enough room left // for the text entered by the player. - DrawString(out, tempstr, { GetPanelPosition(UiPanels::Inventory, { dialogX + 31, 75 }), { 200, 50 } }, UiFlags::ColorWhitegold | UiFlags::AlignCenter, 1, 17); + DrawString(out, wrapped, { GetPanelPosition(UiPanels::Inventory, { dialogX + 31, 75 }), { 200, 50 } }, UiFlags::ColorWhitegold | UiFlags::AlignCenter, 1, 17); tempstr[0] = '\0'; if (amount > 0) { diff --git a/Source/engine/render/text_render.cpp b/Source/engine/render/text_render.cpp index 1c5dfb596..abc0104af 100644 --- a/Source/engine/render/text_render.cpp +++ b/Source/engine/render/text_render.cpp @@ -170,14 +170,16 @@ int GetLineWidth(string_view text, GameFontTables size, int spacing, int *charac { int lineWidth = 0; - std::string textBuffer(text); - textBuffer.resize(textBuffer.size() + 4); // Buffer must be padded before calling utf8_decode() + std::string textBuffer; + textBuffer.reserve(textBuffer.size() + 3); // Buffer must be padded before calling utf8_decode() + textBuffer.append(text.data(), text.size()); + textBuffer.resize(textBuffer.size() + 3); const char *textData = textBuffer.data(); size_t i = 0; uint32_t currentUnicodeRow = 0; std::array *kerning = nullptr; - uint32_t next; + char32_t next; int error; for (; *textData != '\0'; i++) { textData = utf8_decode(textData, &next, &error); @@ -217,27 +219,37 @@ int AdjustSpacingToFitHorizontally(int &lineWidth, int maxSpacing, int character return maxSpacing - spacingRedux; } -void WordWrapString(char *text, size_t width, GameFontTables size, int spacing) +std::string WordWrapString(string_view text, size_t width, GameFontTables size, int spacing) { - int lastKnownSpaceAt = -1; - size_t lineWidth = 0; - - std::string textBuffer(text); - textBuffer.resize(textBuffer.size() + 4); // Buffer must be padded before calling utf8_decode() - const char *textData = textBuffer.data(); - + int lastBreakablePos = -1; + int lastBreakableLen; + char32_t lastBreakableCodePoint; + + std::string input; + std::string output; + input.reserve(input.size() + 3); // Buffer must be padded before calling utf8_decode() + input.append(text.data(), text.size()); + input.resize(input.size() + 3); + output.reserve(text.size()); + const char *begin = input.data(); + const char *cur = begin; + + const char *processedEnd = cur; uint32_t currentUnicodeRow = 0; + size_t lineWidth = 0; std::array *kerning = nullptr; - uint32_t next; + char32_t next; int error; - while (*textData != '\0') { - textData = utf8_decode(textData, &next, &error); - if (error) - next = '?'; + while (*cur != '\0') { + cur = utf8_decode(cur, &next, &error); + if (error != 0) + next = U'?'; - if (next == '\n') { // Existing line break, scan next line - lastKnownSpaceAt = -1; + if (next == U'\n') { // Existing line break, scan next line + lastBreakablePos = -1; lineWidth = 0; + output.append(processedEnd, cur); + processedEnd = cur; continue; } @@ -252,8 +264,16 @@ void WordWrapString(char *text, size_t width, GameFontTables size, int spacing) } lineWidth += (*kerning)[frame] + spacing; - if (next == ' ') { - lastKnownSpaceAt = textData - textBuffer.data() - 1; + if (IsAnyOf(next, U' ', U',', U'.', U'?', U'!')) { + lastBreakablePos = static_cast(cur - begin - 1); + lastBreakableLen = 1; + lastBreakableCodePoint = next; + continue; + } + if (IsAnyOf(next, U' ', U'、', U'。', U'?', U'!')) { + lastBreakablePos = static_cast(cur - begin - 3); + lastBreakableLen = 3; + lastBreakableCodePoint = next; continue; } @@ -261,16 +281,24 @@ void WordWrapString(char *text, size_t width, GameFontTables size, int spacing) continue; // String is still within the limit, continue to the next symbol } - if (lastKnownSpaceAt == -1) { // Single word longer than width + if (lastBreakablePos == -1) { // Single word longer than width continue; } // Break line and continue to next line - text[lastKnownSpaceAt] = '\n'; - textData = &textBuffer.data()[lastKnownSpaceAt + 1]; - lastKnownSpaceAt = -1; + const char *end = &input[lastBreakablePos]; + if (!IsAnyOf(lastBreakableCodePoint, U' ', U' ')) { + end += lastBreakableLen; + } + output.append(processedEnd, end); + output += '\n'; + cur = &input[lastBreakablePos + lastBreakableLen]; + processedEnd = cur; + lastBreakablePos = -1; lineWidth = 0; } + output.append(processedEnd, cur); + return output; } /** @@ -317,7 +345,7 @@ uint32_t DrawString(const Surface &out, string_view text, const Rectangle &rect, const char *textData = textBuffer.data(); const char *previousPosition = textData; - uint32_t next; + char32_t next; uint32_t currentUnicodeRow = 0; int error; for (; *textData != '\0'; previousPosition = textData) { diff --git a/Source/engine/render/text_render.hpp b/Source/engine/render/text_render.hpp index 2aabe6afb..a36cfc794 100644 --- a/Source/engine/render/text_render.hpp +++ b/Source/engine/render/text_render.hpp @@ -61,7 +61,7 @@ void UnloadFonts(GameFontTables size, text_color color); * @return Line width in pixels */ int GetLineWidth(string_view text, GameFontTables size = GameFont12, int spacing = 1, int *charactersInLine = nullptr); -void WordWrapString(char *text, size_t width, GameFontTables size = GameFont12, int spacing = 1); +[[nodiscard]] std::string WordWrapString(string_view text, size_t width, GameFontTables size = GameFont12, int spacing = 1); /** * @brief Draws a line of text within a clipping rectangle (positioned relative to the origin of the output buffer). diff --git a/Source/error.cpp b/Source/error.cpp index 5e05f422b..ca952fa28 100644 --- a/Source/error.cpp +++ b/Source/error.cpp @@ -35,8 +35,7 @@ void InitNextLines() char tempstr[1536]; // Longest test is about 768 chars * 2 for unicode strcpy(tempstr, message.data()); - WordWrapString(tempstr, LineWidth, GameFont12, 1); - const string_view paragraphs = tempstr; + const std::string paragraphs = WordWrapString(tempstr, LineWidth, GameFont12, 1); size_t previous = 0; while (true) { diff --git a/Source/help.cpp b/Source/help.cpp index 1283d4fa2..65043a7a3 100644 --- a/Source/help.cpp +++ b/Source/help.cpp @@ -106,8 +106,7 @@ void InitHelp() for (const auto *text : HelpText) { strcpy(tempString, _(text)); - WordWrapString(tempString, 577); - const string_view paragraph = tempString; + const std::string paragraph = WordWrapString(tempString, 577); size_t previous = 0; while (true) { diff --git a/Source/minitext.cpp b/Source/minitext.cpp index 36704fab5..1eae62def 100644 --- a/Source/minitext.cpp +++ b/Source/minitext.cpp @@ -44,8 +44,7 @@ void LoadText(const char *text) char tempstr[1536]; // Longest test is about 768 chars * 2 for unicode strcpy(tempstr, text); - WordWrapString(tempstr, 543, GameFont30); - const string_view paragraphs = tempstr; + const std::string paragraphs = WordWrapString(tempstr, 543, GameFont30); size_t previous = 0; while (true) { diff --git a/Source/panels/charpanel.cpp b/Source/panels/charpanel.cpp index 4b51e750d..5d1f4bb3d 100644 --- a/Source/panels/charpanel.cpp +++ b/Source/panels/charpanel.cpp @@ -199,16 +199,19 @@ void DrawPanelField(const Surface &out, Point pos, int len) void DrawShadowString(const Surface &out, const PanelEntry &entry) { - if (entry.label == "") + if (entry.label.empty()) return; - std::string text_tmp = _(entry.label.c_str()); - char buffer[64]; - int spacing = 0; - strcpy(buffer, text_tmp.c_str()); - if (entry.labelLength > 0) - WordWrapString(buffer, entry.labelLength, GameFont12, spacing); - std::string text(buffer); + constexpr int Spacing = 0; + const std::string &textStr = LanguageTranslate(entry.label.c_str()); + string_view text; + std::string wrapped; + if (entry.labelLength > 0) { + wrapped = WordWrapString(textStr, entry.labelLength, GameFont12, Spacing); + text = wrapped; + } else { + text = textStr; + } UiFlags style = UiFlags::VerticalCenter; @@ -221,8 +224,8 @@ void DrawShadowString(const Surface &out, const PanelEntry &entry) labelPosition += Displacement { -entry.labelLength - 3, 0 }; } - DrawString(out, text, { labelPosition + Displacement { -2, 2 }, { entry.labelLength, 20 } }, style | UiFlags::ColorBlack, spacing, 10); - DrawString(out, text, { labelPosition, { entry.labelLength, 20 } }, style | UiFlags::ColorWhite, spacing, 10); + DrawString(out, text, { labelPosition + Displacement { -2, 2 }, { entry.labelLength, 20 } }, style | UiFlags::ColorBlack, Spacing, 10); + DrawString(out, text, { labelPosition, { entry.labelLength, 20 } }, style | UiFlags::ColorWhite, Spacing, 10); } void DrawStatButtons(const Surface &out) diff --git a/Source/plrmsg.cpp b/Source/plrmsg.cpp index af8878233..97d1a17c6 100644 --- a/Source/plrmsg.cpp +++ b/Source/plrmsg.cpp @@ -31,8 +31,7 @@ void PrintChatMessage(const Surface &out, int x, int y, int width, char *text, U if (text[i] == '\n') text[i] = ' '; } - WordWrapString(text, width); - DrawString(out, text, { { x, y }, { width, 0 } }, style, 1, 10); + DrawString(out, WordWrapString(text, width), { { x, y }, { width, 0 } }, style, 1, 10); } } // namespace diff --git a/Source/utils/utf8.h b/Source/utils/utf8.h index d4e0c5115..2e98153b9 100644 --- a/Source/utils/utf8.h +++ b/Source/utils/utf8.h @@ -24,7 +24,7 @@ * occurs, this pointer will be a guess that depends on the particular * error, but it will always advance at least one byte. */ -inline const char *utf8_decode(const char *buf, uint32_t *c, int *e) +inline const char *utf8_decode(const char *buf, char32_t *c, int *e) { static const char lengths[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, @@ -47,10 +47,10 @@ inline const char *utf8_decode(const char *buf, uint32_t *c, int *e) /* Assume a four-byte character and load four bytes. Unused bits are * shifted out. */ - *c = (uint32_t)(s[0] & masks[len]) << 18; - *c |= (uint32_t)(s[1] & 0x3f) << 12; - *c |= (uint32_t)(s[2] & 0x3f) << 6; - *c |= (uint32_t)(s[3] & 0x3f) << 0; + *c = static_cast((s[0] & masks[len]) << 18); + *c |= static_cast((s[1] & 0x3f) << 12); + *c |= static_cast((s[2] & 0x3f) << 6); + *c |= static_cast((s[3] & 0x3f) << 0); *c >>= shiftc[len]; /* Accumulate the various error conditions. */ @@ -73,7 +73,7 @@ inline int FindLastUtf8Symbols(const char *text) const char *textData = textBuffer.data(); const char *previousPosition = textData; - uint32_t next; + char32_t next; int error; for (; *textData != '\0'; previousPosition = textData) { textData = utf8_decode(textData, &next, &error);