From 258403b2b2495365aa25b2edd7f5053957d1fa41 Mon Sep 17 00:00:00 2001
From: MAWKLY VK <206752684+MAWKLY-VK@users.noreply.github.com>
Date: Wed, 30 Sep 2026 23:09:13 +0300
Subject: [PATCH 1/5] chore(usp10): Add Usp10Loader from TheSuperHackers
Brings the runtime loader of usp10.dll from TheSuperHackers/GeneralsGameCode in
its current form in Dependencies/Usp10 (#3241, #3342), so that the Uniscribe
functions can be called as if usp10.dll was linked statically. It includes
ScriptString_pLogAttr, ScriptStringGetLogicalWidths and ScriptStringCPtoX for
wrapping shaped text and placing a caret in it.
The only difference to the upstream file is the include of stringex.h, which
is still in WWLib here.
---
CMakeLists.txt | 5 +
Core/Libraries/Source/WWVegas/CMakeLists.txt | 1 +
Dependencies/Usp10/CMakeLists.txt | 24 ++
Dependencies/Usp10/Usp10Loader.cpp | 257 +++++++++++++++++++
Dependencies/Usp10/Usp10Loader.h | 46 ++++
Dependencies/Usp10/usp10_adapter.h | 38 +++
Dependencies/Usp10/usp10_subset.h | 132 ++++++++++
7 files changed, 503 insertions(+)
create mode 100644 Dependencies/Usp10/CMakeLists.txt
create mode 100644 Dependencies/Usp10/Usp10Loader.cpp
create mode 100644 Dependencies/Usp10/Usp10Loader.h
create mode 100644 Dependencies/Usp10/usp10_adapter.h
create mode 100644 Dependencies/Usp10/usp10_subset.h
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 39062e3fbe6..2c9d23e11b3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -73,6 +73,11 @@ if (IS_VS6_BUILD)
add_subdirectory(Dependencies/MaxSDK)
endif()
add_subdirectory(Dependencies/Utility)
+if (WIN32)
+ add_subdirectory(Dependencies/Usp10)
+else()
+ add_library(deps_usp10 INTERFACE)
+endif()
# To be removed when abandoning VC6
if (IS_VS6_BUILD)
diff --git a/Core/Libraries/Source/WWVegas/CMakeLists.txt b/Core/Libraries/Source/WWVegas/CMakeLists.txt
index e8a6beeb0f2..cd8bbe4d68f 100644
--- a/Core/Libraries/Source/WWVegas/CMakeLists.txt
+++ b/Core/Libraries/Source/WWVegas/CMakeLists.txt
@@ -8,6 +8,7 @@ target_compile_definitions(core_wwcommon INTERFACE
target_link_libraries(core_wwcommon INTERFACE
d3d8lib
+ deps_usp10
milesstub
stlport
)
diff --git a/Dependencies/Usp10/CMakeLists.txt b/Dependencies/Usp10/CMakeLists.txt
new file mode 100644
index 00000000000..264582a3502
--- /dev/null
+++ b/Dependencies/Usp10/CMakeLists.txt
@@ -0,0 +1,24 @@
+set(USP10_SRC
+ Usp10Loader.cpp
+ Usp10Loader.h
+ usp10_adapter.h
+ usp10_subset.h
+)
+
+add_library(deps_usp10 STATIC)
+
+target_sources(deps_usp10 PRIVATE ${USP10_SRC})
+
+target_include_directories(deps_usp10 PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+# For WWLib/stringex.h, which includes bittype.h from the same directory.
+target_include_directories(deps_usp10 PRIVATE
+ ${CMAKE_SOURCE_DIR}/Core/Libraries/Source/WWVegas
+ ${CMAKE_SOURCE_DIR}/Core/Libraries/Source/WWVegas/WWLib
+)
+
+target_link_libraries(deps_usp10 PRIVATE
+ core_utility
+)
diff --git a/Dependencies/Usp10/Usp10Loader.cpp b/Dependencies/Usp10/Usp10Loader.cpp
new file mode 100644
index 00000000000..d1d83401dd3
--- /dev/null
+++ b/Dependencies/Usp10/Usp10Loader.cpp
@@ -0,0 +1,257 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2026 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#include "Usp10Loader.h"
+#include "usp10_adapter.h"
+
+#include "WWLib/stringex.h"
+
+
+namespace
+{
+
+typedef HRESULT (WINAPI *ScriptIsComplex_t)(const WCHAR *pwcInChars, int cInChars, DWORD dwFlags);
+typedef HRESULT (WINAPI *ScriptItemize_t)(const WCHAR *pwcInChars, int cInChars, int cMaxItems,
+ const SCRIPT_CONTROL *psControl, const SCRIPT_STATE *psState, SCRIPT_ITEM *pItems, int *pcItems);
+typedef HRESULT (WINAPI *ScriptBreak_t)(const WCHAR *pwcChars, int cChars, const SCRIPT_ANALYSIS *psa,
+ SCRIPT_LOGATTR *psla);
+typedef HRESULT (WINAPI *ScriptLayout_t)(int cRuns, const BYTE *pbLevel, int *piVisualToLogical,
+ int *piLogicalToVisual);
+typedef HRESULT (WINAPI *ScriptStringAnalyse_t)(HDC hdc, const void *pString, int cString, int cGlyphs,
+ int iCharset, DWORD dwFlags, int iReqWidth, SCRIPT_CONTROL *psControl, SCRIPT_STATE *psState,
+ const int *piDx, SCRIPT_TABDEF *pTabdef, const BYTE *pbInClass, SCRIPT_STRING_ANALYSIS *pssa);
+typedef HRESULT (WINAPI *ScriptStringFree_t)(SCRIPT_STRING_ANALYSIS *pssa);
+typedef const SIZE *(WINAPI *ScriptString_pSize_t)(SCRIPT_STRING_ANALYSIS ssa);
+typedef HRESULT (WINAPI *ScriptStringOut_t)(SCRIPT_STRING_ANALYSIS ssa, int iX, int iY, UINT uOptions,
+ const RECT *prc, int iMinSel, int iMaxSel, BOOL fDisabled);
+typedef const SCRIPT_LOGATTR *(WINAPI *ScriptString_pLogAttr_t)(SCRIPT_STRING_ANALYSIS ssa);
+typedef HRESULT (WINAPI *ScriptStringGetLogicalWidths_t)(SCRIPT_STRING_ANALYSIS ssa, int *piDx);
+typedef HRESULT (WINAPI *ScriptStringCPtoX_t)(SCRIPT_STRING_ANALYSIS ssa, int icp, BOOL fTrailing, int *pX);
+
+ScriptIsComplex_t ScriptIsComplexPtr = nullptr;
+ScriptItemize_t ScriptItemizePtr = nullptr;
+ScriptBreak_t ScriptBreakPtr = nullptr;
+ScriptLayout_t ScriptLayoutPtr = nullptr;
+ScriptStringAnalyse_t ScriptStringAnalysePtr = nullptr;
+ScriptStringFree_t ScriptStringFreePtr = nullptr;
+ScriptString_pSize_t ScriptString_pSizePtr = nullptr;
+ScriptStringOut_t ScriptStringOutPtr = nullptr;
+ScriptString_pLogAttr_t ScriptString_pLogAttrPtr = nullptr;
+ScriptStringGetLogicalWidths_t ScriptStringGetLogicalWidthsPtr = nullptr;
+ScriptStringCPtoX_t ScriptStringCPtoXPtr = nullptr;
+
+HMODULE Module = HMODULE(nullptr);
+int ReferenceCount = 0;
+bool Failed = false;
+unsigned long LastError = 0;
+
+#define USP10_RESOLVE(name) \
+ name##Ptr = reinterpret_cast(::GetProcAddress(Module, #name))
+
+static bool resolveAll()
+{
+ USP10_RESOLVE(ScriptIsComplex);
+ USP10_RESOLVE(ScriptItemize);
+ USP10_RESOLVE(ScriptBreak);
+ USP10_RESOLVE(ScriptLayout);
+ USP10_RESOLVE(ScriptStringAnalyse);
+ USP10_RESOLVE(ScriptStringFree);
+ USP10_RESOLVE(ScriptString_pSize);
+ USP10_RESOLVE(ScriptStringOut);
+ USP10_RESOLVE(ScriptString_pLogAttr);
+ USP10_RESOLVE(ScriptStringGetLogicalWidths);
+ USP10_RESOLVE(ScriptStringCPtoX);
+
+ return ScriptIsComplexPtr != nullptr && ScriptItemizePtr != nullptr && ScriptBreakPtr != nullptr &&
+ ScriptLayoutPtr != nullptr && ScriptStringAnalysePtr != nullptr && ScriptStringFreePtr != nullptr &&
+ ScriptString_pSizePtr != nullptr && ScriptStringOutPtr != nullptr && ScriptString_pLogAttrPtr != nullptr &&
+ ScriptStringGetLogicalWidthsPtr != nullptr && ScriptStringCPtoXPtr != nullptr;
+}
+#undef USP10_RESOLVE
+
+static void freeResources()
+{
+ if (Module != HMODULE(nullptr)) {
+ ::FreeLibrary(Module);
+ Module = HMODULE(nullptr);
+ }
+
+ ScriptIsComplexPtr = nullptr;
+ ScriptItemizePtr = nullptr;
+ ScriptBreakPtr = nullptr;
+ ScriptLayoutPtr = nullptr;
+ ScriptStringAnalysePtr = nullptr;
+ ScriptStringFreePtr = nullptr;
+ ScriptString_pSizePtr = nullptr;
+ ScriptStringOutPtr = nullptr;
+ ScriptString_pLogAttrPtr = nullptr;
+ ScriptStringGetLogicalWidthsPtr = nullptr;
+ ScriptStringCPtoXPtr = nullptr;
+}
+
+} // namespace
+
+
+bool Usp10Loader::isLoaded()
+{
+ return Module != HMODULE(nullptr);
+}
+
+
+bool Usp10Loader::isFailed()
+{
+ return Failed;
+}
+
+
+unsigned long Usp10Loader::getLastError()
+{
+ return LastError;
+}
+
+
+bool Usp10Loader::load()
+{
+ // Always increment the reference count.
+ ++ReferenceCount;
+
+ // Optimization: return early if it failed before.
+ if (Failed)
+ return false;
+
+ // Return early if someone else already loaded it.
+ if (ReferenceCount > 1)
+ return true;
+
+ // Load usp10.dll from the system directory only.
+ char dll_path[MAX_PATH];
+ const UINT path_length = ::GetSystemDirectoryA(dll_path, sizeof(dll_path));
+ if (path_length == 0 || path_length >= sizeof(dll_path) ||
+ strlcat(dll_path, "\\usp10.dll", sizeof(dll_path)) >= sizeof(dll_path)) {
+ LastError = ERROR_BUFFER_OVERFLOW;
+ Failed = true;
+ return false;
+ }
+
+ Module = ::LoadLibraryA(dll_path);
+ if (Module == HMODULE(nullptr)) {
+ LastError = ::GetLastError();
+ Failed = true;
+ return false;
+ }
+
+ if (!resolveAll()) {
+ freeResources();
+ LastError = ERROR_PROC_NOT_FOUND;
+ Failed = true;
+ return false;
+ }
+
+ return true;
+}
+
+
+void Usp10Loader::unload()
+{
+ if (ReferenceCount > 0)
+ --ReferenceCount;
+
+ if (ReferenceCount > 0)
+ return;
+
+ freeResources();
+ Failed = false;
+ LastError = 0;
+}
+
+
+// The Uniscribe functions below stand in for the imports of usp10.dll.
+
+HRESULT WINAPI ScriptIsComplex(const WCHAR *pwcInChars, int cInChars, DWORD dwFlags)
+{
+ return ScriptIsComplexPtr != nullptr ? ScriptIsComplexPtr(pwcInChars, cInChars, dwFlags) : E_FAIL;
+}
+
+HRESULT WINAPI ScriptItemize(const WCHAR *pwcInChars, int cInChars, int cMaxItems,
+ const SCRIPT_CONTROL *psControl, const SCRIPT_STATE *psState, SCRIPT_ITEM *pItems, int *pcItems)
+{
+ if (ScriptItemizePtr != nullptr)
+ return ScriptItemizePtr(pwcInChars, cInChars, cMaxItems, psControl, psState, pItems, pcItems);
+
+ if (pcItems != nullptr)
+ *pcItems = 0;
+ return E_FAIL;
+}
+
+HRESULT WINAPI ScriptBreak(const WCHAR *pwcChars, int cChars, const SCRIPT_ANALYSIS *psa, SCRIPT_LOGATTR *psla)
+{
+ return ScriptBreakPtr != nullptr ? ScriptBreakPtr(pwcChars, cChars, psa, psla) : E_FAIL;
+}
+
+HRESULT WINAPI ScriptLayout(int cRuns, const BYTE *pbLevel, int *piVisualToLogical, int *piLogicalToVisual)
+{
+ return ScriptLayoutPtr != nullptr
+ ? ScriptLayoutPtr(cRuns, pbLevel, piVisualToLogical, piLogicalToVisual)
+ : E_FAIL;
+}
+
+HRESULT WINAPI ScriptStringAnalyse(HDC hdc, const void *pString, int cString, int cGlyphs, int iCharset,
+ DWORD dwFlags, int iReqWidth, SCRIPT_CONTROL *psControl, SCRIPT_STATE *psState, const int *piDx,
+ SCRIPT_TABDEF *pTabdef, const BYTE *pbInClass, SCRIPT_STRING_ANALYSIS *pssa)
+{
+ if (ScriptStringAnalysePtr != nullptr)
+ return ScriptStringAnalysePtr(hdc, pString, cString, cGlyphs, iCharset, dwFlags, iReqWidth, psControl,
+ psState, piDx, pTabdef, pbInClass, pssa);
+
+ if (pssa != nullptr)
+ *pssa = nullptr;
+ return E_FAIL;
+}
+
+HRESULT WINAPI ScriptStringFree(SCRIPT_STRING_ANALYSIS *pssa)
+{
+ return ScriptStringFreePtr != nullptr ? ScriptStringFreePtr(pssa) : E_FAIL;
+}
+
+const SIZE *WINAPI ScriptString_pSize(SCRIPT_STRING_ANALYSIS ssa)
+{
+ return ScriptString_pSizePtr != nullptr ? ScriptString_pSizePtr(ssa) : nullptr;
+}
+
+HRESULT WINAPI ScriptStringOut(SCRIPT_STRING_ANALYSIS ssa, int iX, int iY, UINT uOptions, const RECT *prc,
+ int iMinSel, int iMaxSel, BOOL fDisabled)
+{
+ return ScriptStringOutPtr != nullptr
+ ? ScriptStringOutPtr(ssa, iX, iY, uOptions, prc, iMinSel, iMaxSel, fDisabled)
+ : E_FAIL;
+}
+
+const SCRIPT_LOGATTR *WINAPI ScriptString_pLogAttr(SCRIPT_STRING_ANALYSIS ssa)
+{
+ return ScriptString_pLogAttrPtr != nullptr ? ScriptString_pLogAttrPtr(ssa) : nullptr;
+}
+
+HRESULT WINAPI ScriptStringGetLogicalWidths(SCRIPT_STRING_ANALYSIS ssa, int *piDx)
+{
+ return ScriptStringGetLogicalWidthsPtr != nullptr ? ScriptStringGetLogicalWidthsPtr(ssa, piDx) : E_FAIL;
+}
+
+HRESULT WINAPI ScriptStringCPtoX(SCRIPT_STRING_ANALYSIS ssa, int icp, BOOL fTrailing, int *pX)
+{
+ return ScriptStringCPtoXPtr != nullptr ? ScriptStringCPtoXPtr(ssa, icp, fTrailing, pX) : E_FAIL;
+}
diff --git a/Dependencies/Usp10/Usp10Loader.h b/Dependencies/Usp10/Usp10Loader.h
new file mode 100644
index 00000000000..f1b384ee398
--- /dev/null
+++ b/Dependencies/Usp10/Usp10Loader.h
@@ -0,0 +1,46 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2026 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#pragma once
+
+// This static class loads and unloads usp10.dll from the system directory at runtime.
+//
+// The Uniscribe functions declared in usp10_adapter.h are implemented in this class and forward
+// to the matching export of the loaded module, so they can be called as if usp10.dll was linked
+// statically. When the module is not loaded, every Uniscribe function returns E_FAIL, or a null
+// pointer for ScriptString_pSize and ScriptString_pLogAttr.
+//
+// Is not thread safe. Every call to load needs a paired call to unload, no matter if the load
+// was successful, and both are expected to be called while no Uniscribe function is in flight.
+
+class Usp10Loader
+{
+public:
+
+ // Returns whether usp10.dll is loaded
+ static bool isLoaded();
+
+ // Returns whether usp10.dll was attempted to be loaded but failed
+ static bool isFailed();
+
+ // Returns the system error code of the failed load attempt
+ static unsigned long getLastError();
+
+ static bool load();
+ static void unload();
+};
diff --git a/Dependencies/Usp10/usp10_adapter.h b/Dependencies/Usp10/usp10_adapter.h
new file mode 100644
index 00000000000..417d3fc7610
--- /dev/null
+++ b/Dependencies/Usp10/usp10_adapter.h
@@ -0,0 +1,38 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2026 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+// This file includes the Uniscribe API of usp10.h. It uses the header of the Windows SDK if the
+// compiler can find it, and otherwise a subset of it for compilers that do not have it, such as VC6.
+// The functions are implemented by Usp10Loader.cpp.
+
+#pragma once
+
+#if defined(__has_include)
+ #if __has_include()
+ #define HAVE_USP10_H 1
+ #endif
+#endif
+
+#ifdef HAVE_USP10_H
+ #ifndef WIN32_LEAN_AND_MEAN
+ #define WIN32_LEAN_AND_MEAN
+ #endif
+ #include
+#else
+ #include "usp10_subset.h"
+#endif
diff --git a/Dependencies/Usp10/usp10_subset.h b/Dependencies/Usp10/usp10_subset.h
new file mode 100644
index 00000000000..e929ea80632
--- /dev/null
+++ b/Dependencies/Usp10/usp10_subset.h
@@ -0,0 +1,132 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2026 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+// Subset of the Uniscribe API of the Windows SDK usp10.h, for compilers that do not have it, such as VC6.
+// Is included by usp10_adapter.h, which should be used instead of this file.
+
+#pragma once
+
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define SSA_FALLBACK 0x00000020
+#define SSA_BREAK 0x00000040
+#define SSA_GLYPHS 0x00000080
+#define SSA_RTL 0x00000100
+#define SSA_LINK 0x00001000
+
+#define SIC_COMPLEX 1
+
+typedef struct tag_SCRIPT_CONTROL {
+ DWORD uDefaultLanguage :16;
+ DWORD fContextDigits :1;
+ DWORD fInvertPreBoundDir :1;
+ DWORD fInvertPostBoundDir :1;
+ DWORD fLinkStringBefore :1;
+ DWORD fLinkStringAfter :1;
+ DWORD fNeutralOverride :1;
+ DWORD fNumericOverride :1;
+ DWORD fLegacyBidiClass :1;
+ DWORD fMergeNeutralItems :1;
+ DWORD fUseStandardBidi :1;
+ DWORD fReserved :6;
+} SCRIPT_CONTROL;
+
+typedef struct tag_SCRIPT_STATE {
+ WORD uBidiLevel :5;
+ WORD fOverrideDirection :1;
+ WORD fInhibitSymSwap :1;
+ WORD fCharShape :1;
+ WORD fDigitSubstitute :1;
+ WORD fInhibitLigate :1;
+ WORD fDisplayZWG :1;
+ WORD fArabicNumContext :1;
+ WORD fGcpClusters :1;
+ WORD fReserved :1;
+ WORD fEngineReserved :2;
+} SCRIPT_STATE;
+
+typedef struct tag_SCRIPT_ANALYSIS {
+ WORD eScript :10;
+ WORD fRTL :1;
+ WORD fLayoutRTL :1;
+ WORD fLinkBefore :1;
+ WORD fLinkAfter :1;
+ WORD fLogicalOrder :1;
+ WORD fNoGlyphIndex :1;
+ SCRIPT_STATE s;
+} SCRIPT_ANALYSIS;
+
+typedef struct tag_SCRIPT_ITEM {
+ int iCharPos;
+ SCRIPT_ANALYSIS a;
+} SCRIPT_ITEM;
+
+typedef struct tag_SCRIPT_LOGATTR {
+ BYTE fSoftBreak :1;
+ BYTE fWhiteSpace :1;
+ BYTE fCharStop :1;
+ BYTE fWordStop :1;
+ BYTE fInvalid :1;
+ BYTE fReserved :3;
+} SCRIPT_LOGATTR;
+
+typedef struct tag_SCRIPT_TABDEF {
+ int cTabStops;
+ int iScale;
+ int *pTabStops;
+ int iTabOrigin;
+} SCRIPT_TABDEF;
+
+typedef void *SCRIPT_STRING_ANALYSIS;
+
+HRESULT WINAPI ScriptIsComplex(const WCHAR *pwcInChars, int cInChars, DWORD dwFlags);
+
+HRESULT WINAPI ScriptItemize(const WCHAR *pwcInChars, int cInChars, int cMaxItems,
+ const SCRIPT_CONTROL *psControl, const SCRIPT_STATE *psState, SCRIPT_ITEM *pItems, int *pcItems);
+
+HRESULT WINAPI ScriptBreak(const WCHAR *pwcChars, int cChars, const SCRIPT_ANALYSIS *psa, SCRIPT_LOGATTR *psla);
+
+HRESULT WINAPI ScriptLayout(int cRuns, const BYTE *pbLevel, int *piVisualToLogical, int *piLogicalToVisual);
+
+HRESULT WINAPI ScriptStringAnalyse(HDC hdc, const void *pString, int cString, int cGlyphs, int iCharset,
+ DWORD dwFlags, int iReqWidth, SCRIPT_CONTROL *psControl, SCRIPT_STATE *psState, const int *piDx,
+ SCRIPT_TABDEF *pTabdef, const BYTE *pbInClass, SCRIPT_STRING_ANALYSIS *pssa);
+
+HRESULT WINAPI ScriptStringFree(SCRIPT_STRING_ANALYSIS *pssa);
+
+const SIZE *WINAPI ScriptString_pSize(SCRIPT_STRING_ANALYSIS ssa);
+
+HRESULT WINAPI ScriptStringOut(SCRIPT_STRING_ANALYSIS ssa, int iX, int iY, UINT uOptions, const RECT *prc,
+ int iMinSel, int iMaxSel, BOOL fDisabled);
+
+const SCRIPT_LOGATTR *WINAPI ScriptString_pLogAttr(SCRIPT_STRING_ANALYSIS ssa);
+
+HRESULT WINAPI ScriptStringGetLogicalWidths(SCRIPT_STRING_ANALYSIS ssa, int *piDx);
+
+HRESULT WINAPI ScriptStringCPtoX(SCRIPT_STRING_ANALYSIS ssa, int icp, BOOL fTrailing, int *pX);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
From d93c38274c35106fa6cdebd181bf5b3fe0813128 Mon Sep 17 00:00:00 2001
From: MAWKLY VK <206752684+MAWKLY-VK@users.noreply.github.com>
Date: Wed, 30 Sep 2026 21:13:05 +0300
Subject: [PATCH 2/5] feat(text): Shape and reorder text that the glyph cache
cannot draw
The glyph cache of FontCharsClass draws every UTF-16 unit on its own from left
to right. Arabic and Persian letters therefore appear unjoined and in reverse
order, emoji and other characters outside of the Basic Multilingual Plane
appear as two boxes, and characters that the font does not have appear as
boxes as well.
Text that needs it is now laid out with the ScriptString functions of
Uniscribe and drawn as a whole into the sentence textures:
* Letters are shaped and each paragraph is ordered with the Unicode
Bidirectional Algorithm. A paragraph reads in the direction of most of its
strong characters, so that a mostly Arabic message that starts with an
English word keeps the Arabic reading order.
* Missing glyphs, including emoji, come from fallback fonts of the system.
Glyphs are drawn in the text color like all other text.
* Word wrapping breaks paragraphs at soft break opportunities before each
line is shaped, so wrapped chat messages stay readable. Lines of
right-to-left paragraphs are aligned to the right.
* Directional isolates are replaced by embeddings, because Uniscribe does
not support them.
Latin-1 text, and other text that the font can draw, such as Cyrillic, keeps
using the glyph cache and looks the same as before.
Text entries place the caret after the last character of right-to-left text
and start right-to-left text at the right edge of the entry.
---
.../Include/GameClient/DisplayString.h | 2 +
.../GameClient/GUI/Gadget/W3DTextEntry.cpp | 22 +-
.../Source/WWVegas/WW3D2/CMakeLists.txt | 7 +
.../Source/WWVegas/WW3D2/render2dsentence.cpp | 336 +++++++++-
.../Source/WWVegas/WW3D2/render2dsentence.h | 23 +
.../Source/WWVegas/WW3D2/shapedtext.cpp | 587 ++++++++++++++++++
.../Source/WWVegas/WW3D2/shapedtext.h | 91 +++
.../W3DDevice/GameClient/W3DDisplayString.h | 8 +
.../W3DDevice/GameClient/W3DDisplayString.cpp | 72 ++-
9 files changed, 1132 insertions(+), 16 deletions(-)
create mode 100644 Core/Libraries/Source/WWVegas/WW3D2/shapedtext.cpp
create mode 100644 Core/Libraries/Source/WWVegas/WW3D2/shapedtext.h
diff --git a/Core/GameEngine/Include/GameClient/DisplayString.h b/Core/GameEngine/Include/GameClient/DisplayString.h
index 042ab1e6963..aa3b400e2a1 100644
--- a/Core/GameEngine/Include/GameClient/DisplayString.h
+++ b/Core/GameEngine/Include/GameClient/DisplayString.h
@@ -91,6 +91,8 @@ class DisplayString : public MemoryPoolObject
virtual void draw( Int x, Int y, Color color, Color dropColor, Int xDrop, Int yDrop ) = 0; ///< render text with the drop shadow being at the offsets passed in
virtual void getSize( Int *width, Int *height ) = 0; ///< get render size
virtual Int getWidth( Int charPos = -1 ) = 0; ///< get text with up to charPos characters, 1- = all characters
+ virtual Int getCaretOffset() = 0; ///< get the horizontal offset of a caret after the last character
+ virtual Bool isRightToLeft() = 0; ///< TRUE if the text starts with a right-to-left paragraph
virtual void setUseHotkey( Bool useHotkey, Color hotKeyColor ) = 0;
diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DTextEntry.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DTextEntry.cpp
index cf11ddebf9a..c250509d7ed 100644
--- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DTextEntry.cpp
+++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DTextEntry.cpp
@@ -133,6 +133,11 @@ static void drawTextEntryText( GameWindow *window, WinInstanceData *instData,
// get the size of our text, and construct text
Int textWidth = text->getWidth();
+ // TheSuperHackers @feature Right-to-left text ends on its left side, so the caret
+ // follows the last character instead of the right edge of the text.
+ Int caretOffset = text->getCaretOffset();
+ Bool rightToLeft = text->isRightToLeft();
+
if (!e->drawTextFromStart)
{
// clip the text to the edit window size
@@ -153,14 +158,17 @@ static void drawTextEntryText( GameWindow *window, WinInstanceData *instData,
// draw the text
if(textWidth < width)
{
- text->draw( x, y, textColor, textDropColor );
- cursorPos = textWidth + x;
+ // right-to-left text starts at the right edge of the entry
+ Int textX = rightToLeft ? x + width - 4 - textWidth : x;
+ text->draw( textX, y, textColor, textDropColor );
+ cursorPos = caretOffset + textX;
}
else
{
- Int div = textWidth / (width / 2) - 1;
+ // scroll by half the entry width until the caret is visible
+ Int div = max( caretOffset / (width / 2) - 1, 0 );
text->draw(x - (div * (width/2)), y, textColor, textDropColor);
- cursorPos = textWidth - (div * (width/2)) + x;
+ cursorPos = caretOffset - (div * (width/2)) + x;
}
//cursorPos = x + textWidth;
@@ -183,9 +191,13 @@ static void drawTextEntryText( GameWindow *window, WinInstanceData *instData,
x+= 5;
// draw the text
text->draw( x, y, textColor, textDropColor );
- cursorPos = textWidth + x;
+ cursorPos = caretOffset + x;
}
+ // the next right-to-left character goes left of the caret, so draw the caret left of its position
+ if (rightToLeft)
+ cursorPos -= 2;
+
if (e->constructText->getTextLength() > 0 )
{
e->constructText->draw( x + textWidth, y, compositeColor, compositeDropColor );
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt
index ecebd564696..4c96292b88f 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt
+++ b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt
@@ -233,6 +233,13 @@ set(WW3D2_SRC
ww3dtrig.h
)
+if (WIN32)
+ list(APPEND WW3D2_SRC
+ shapedtext.cpp
+ shapedtext.h
+ )
+endif()
+
add_library(corei_ww3d2 INTERFACE)
target_sources(corei_ww3d2 INTERFACE ${WW3D2_SRC})
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
index 64001c0bc07..9341a20ddc1 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
+++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
@@ -40,6 +40,10 @@
#include "WWDebug/wwprofile.h"
#include "WWDebug/wwmemlog.h"
#include "dx8wrapper.h"
+#if defined(_WIN32)
+#include "shapedtext.h"
+#include "Usp10Loader.h"
+#endif
////////////////////////////////////////////////////////////////////////////////////
@@ -48,6 +52,17 @@
#define no_TEST_PLACEMENT 1 // Shows alignment markers for text.
#define TEXTURE_OFFSET 2
+
+////////////////////////////////////////////////////////////////////////////////////
+// Local functions
+////////////////////////////////////////////////////////////////////////////////////
+
+// Returns whether the character at text is a hot key marker that Build_Sentence_Not_Centered skips.
+static inline bool Is_Hot_Key_Marker (const WCHAR *text)
+{
+ return text[0] == L'&' && text[1] != 0 && text[1] > L' ' && text[1] != L'\n';
+}
+
////////////////////////////////////////////////////////////////////////////////////
//
// Render2DSentenceClass
@@ -72,7 +87,8 @@ Render2DSentenceClass::Render2DSentenceClass () :
Centered (false),
DrawExtents (0, 0, 0, 0),
ParseHotKey( false ),
- useHardWordWrap( false)
+ useHardWordWrap( false),
+ IsShaped( false )
{
Shader = Render2DClass::Get_Default_Shader ();
}
@@ -251,6 +267,9 @@ Vector2
Render2DSentenceClass::Get_Text_Extents (const WCHAR *text)
{
Vector2 extent (0, Font->Get_Char_Height());
+ if (Get_Shaped_Text_Extents (text, 0, &extent)) {
+ return extent;
+ }
while (*text) {
WCHAR ch = *text++;
@@ -272,6 +291,11 @@ Render2DSentenceClass::Get_Text_Extents (const WCHAR *text)
Vector2
Render2DSentenceClass::Get_Formatted_Text_Extents (const WCHAR *text)
{
+ Vector2 extent;
+ if (Get_Shaped_Text_Extents (text, (int)WrapWidth, &extent)) {
+ return extent;
+ }
+
return Build_Sentence_Not_Centered(text, nullptr, nullptr, true);
}
@@ -604,6 +628,21 @@ Render2DSentenceClass::Record_Sentence_Chunk ()
////////////////////////////////////////////////////////////////////////////////////
void
Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExtents)
+{
+ //
+ // Calculate the width of the text
+ //
+ int text_width = 0;
+ for (int index = 0; text[index] != 0; index ++) {
+ text_width += Font->Get_Char_Spacing (text[index]);
+ }
+
+ Allocate_New_Surface (text_width, justCalcExtents);
+}
+
+
+void
+Render2DSentenceClass::Allocate_New_Surface (int text_width, bool justCalcExtents)
{
if (!justCalcExtents)
{
@@ -616,14 +655,6 @@ Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExt
}
}
- //
- // Calculate the width of the text
- //
- int text_width = 0;
- for (int index = 0; text[index] != 0; index ++) {
- text_width += Font->Get_Char_Spacing (text[index]);
- }
-
int char_height = Font->Get_Char_Height ();
//
@@ -1151,6 +1182,8 @@ Vector2 Render2DSentenceClass::Build_Sentence_Not_Centered (const WCHAR *text, i
void
Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY)
{
+ IsShaped = false;
+
if (text == nullptr) {
return ;
}
@@ -1158,6 +1191,11 @@ Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY)
if (Font == nullptr)
return;
+ if (Build_Shaped_Sentence (text)) {
+ IsShaped = true;
+ return;
+ }
+
if(Centered && (WrapWidth > 0 || wcschr(text,L'\n')))
Build_Sentence_Centered(text, hkX, hkY);
else
@@ -1166,6 +1204,175 @@ Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY)
}
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Layout_Shaped_Text
+//
+// TheSuperHackers @feature Lays out text that the glyph cache cannot draw, such as
+// joined Arabic letters, right-to-left lines and emoji. Returns false for all other
+// text, which keeps its original appearance.
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+Render2DSentenceClass::Layout_Shaped_Text (ShapedTextClass &shaped_text, const WCHAR *text, int wrap_width)
+{
+ if (Font == nullptr || text == nullptr || MonoSpaced || !Font->Needs_Shaping (text)) {
+ return false;
+ }
+
+ if (!ParseHotKey) {
+ return Font->Layout_Shaped_Text (shaped_text, text, wrap_width);
+ }
+
+ // Shaped text shows no hot key, so only its markers are removed.
+ const int length = (int)wcslen (text);
+ WCHAR *stripped_text = W3DNEWARRAY WCHAR[length + 1];
+ int stripped_length = 0;
+ for (int index = 0; index < length; ++index) {
+ if (!Is_Hot_Key_Marker (text + index)) {
+ stripped_text[stripped_length++] = text[index];
+ }
+ }
+ stripped_text[stripped_length] = 0;
+
+ const bool success = Font->Layout_Shaped_Text (shaped_text, stripped_text, wrap_width);
+ delete [] stripped_text;
+ return success;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Get_Shaped_Text_Extents
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+Render2DSentenceClass::Get_Shaped_Text_Extents (const WCHAR *text, int wrap_width, Vector2 *extents)
+{
+#if defined(_WIN32)
+ ShapedTextClass shaped_text;
+ if (!Layout_Shaped_Text (shaped_text, text, wrap_width)) {
+ return false;
+ }
+
+ extents->Set ((float)(shaped_text.Get_Width () + Font->Get_Extra_Overlap ()), (float)shaped_text.Get_Height ());
+ return true;
+#else
+ return false;
+#endif
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Get_Shaped_Text_Metrics
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+Render2DSentenceClass::Get_Shaped_Text_Metrics (const WCHAR *text, int *width, int *caret_x, bool *right_to_left)
+{
+#if defined(_WIN32)
+ ShapedTextClass shaped_text;
+ if (!Layout_Shaped_Text (shaped_text, text, (int)WrapWidth)) {
+ return false;
+ }
+
+ *width = shaped_text.Get_Width ();
+ *caret_x = shaped_text.Get_End_Caret_X (Centered);
+ *right_to_left = shaped_text.Is_Right_To_Left ();
+ return true;
+#else
+ return false;
+#endif
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Build_Shaped_Sentence
+//
+// Copies the shaped text into the sentence textures line by line. Every line is
+// as tall as a line of the glyph cache, so chunks are recorded the same way.
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+Render2DSentenceClass::Build_Shaped_Sentence (const WCHAR *text)
+{
+#if defined(_WIN32)
+ const int line_height = Font->Get_Char_Height ();
+ if (line_height + 1 >= max (TextureSizeHint, 256)) {
+ return false;
+ }
+
+ ShapedTextClass shaped_text;
+ if (!Layout_Shaped_Text (shaped_text, text, (int)WrapWidth)) {
+ return false;
+ }
+
+ int text_width = 0;
+ uint16 *raster = Font->Rasterize_Shaped_Text (shaped_text, Centered, &text_width);
+ if (raster == nullptr) {
+ return false;
+ }
+
+ const int line_count = shaped_text.Get_Height () / line_height;
+
+ Reset_Sentence_Data ();
+ Cursor.Set (0, 0);
+
+ if (CurSurface == nullptr) {
+ Allocate_New_Surface (text_width * line_count);
+ }
+ TextureOffset.Set (TEXTURE_OFFSET, 0);
+ TextureStartX = TEXTURE_OFFSET;
+
+ for (int line = 0; line < line_count; ++line) {
+ Cursor.Set (0, (float)(line * line_height));
+
+ int source_x = 0;
+ while (source_x < text_width) {
+ const int chunk_width = min (text_width - source_x, CurrTextureSize - 1 - TextureOffset.I);
+ if (chunk_width <= 0) {
+ //
+ // Continue on the next row of the texture, or on a new texture
+ //
+ TextureOffset.J += line_height;
+ if ((TextureOffset.J + line_height) >= CurrTextureSize) {
+ Allocate_New_Surface (text_width * (line_count - line) - source_x);
+ }
+ TextureOffset.I = TEXTURE_OFFSET;
+ TextureStartX = TEXTURE_OFFSET;
+ continue;
+ }
+
+ if (LockedPtr == nullptr) {
+ LockedPtr = (uint16 *)CurSurface->Lock (&LockedStride);
+ WWASSERT (LockedPtr != nullptr);
+ }
+
+ const int dest_inc = LockedStride >> 1;
+ for (int row = 0; row < line_height; ++row) {
+ const uint16 *source = raster + ((line * line_height + row) * text_width) + source_x;
+ uint16 *destination = LockedPtr + ((TextureOffset.J + row) * dest_inc) + TextureOffset.I;
+ ::memcpy (destination, source, chunk_width * sizeof (uint16));
+ }
+
+ TextureOffset.I += chunk_width;
+ Record_Sentence_Chunk ();
+ Cursor.X += chunk_width;
+ TextureStartX = TextureOffset.I;
+ source_x += chunk_width;
+ }
+ }
+
+ delete [] raster;
+ return true;
+#else
+ return false;
+#endif
+}
+
+
////////////////////////////////////////////////////////////////////////////////////
//
// FontCharsClass
@@ -1188,6 +1395,9 @@ FontCharsClass::FontCharsClass () :
{
AlternateUnicodeFont = nullptr;
::memset( ASCIICharArray, 0, sizeof (ASCIICharArray) );
+#if defined(_WIN32)
+ Usp10Loader::load();
+#endif
}
@@ -1205,6 +1415,9 @@ FontCharsClass::~FontCharsClass ()
Free_GDI_Font();
Free_Character_Arrays();
+#if defined(_WIN32)
+ Usp10Loader::unload();
+#endif
}
@@ -1316,6 +1529,111 @@ FontCharsClass::Blit_Char (WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, i
}
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Needs_Shaping
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+FontCharsClass::Needs_Shaping (const WCHAR *text)
+{
+#if defined(_WIN32)
+ // The glyph cache takes characters beyond Latin-1 from the alternate Unicode font.
+ FontCharsClass *unicode_font = AlternateUnicodeFont != nullptr ? AlternateUnicodeFont : this;
+ return unicode_font->MemDC != nullptr && ShapedTextClass::Needs_Shaping (unicode_font->MemDC, text);
+#else
+ return false;
+#endif
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Layout_Shaped_Text
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+FontCharsClass::Layout_Shaped_Text (ShapedTextClass &shaped_text, const WCHAR *text, int wrap_width)
+{
+#if defined(_WIN32)
+ return MemDC != nullptr && shaped_text.Layout (MemDC, text, wrap_width, CharHeight);
+#else
+ return false;
+#endif
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Rasterize_Shaped_Text
+//
+////////////////////////////////////////////////////////////////////////////////////
+uint16 *
+FontCharsClass::Rasterize_Shaped_Text (const ShapedTextClass &shaped_text, bool centered, int *width)
+{
+#if defined(_WIN32)
+ //
+ // Leave room for glyphs that reach beyond their advance, like the glyph cache does
+ //
+ const int text_width = shaped_text.Get_Width () + PixelOverlap;
+ const int text_height = shaped_text.Get_Height ();
+ if (text_width <= 0 || text_height <= 0 || text_width > 0x4000 || text_height > 0x4000) {
+ return nullptr;
+ }
+
+ BITMAPINFOHEADER bitmap_info = { 0 };
+ bitmap_info.biSize = sizeof (BITMAPINFOHEADER);
+ bitmap_info.biWidth = text_width;
+ bitmap_info.biHeight = -text_height;
+ bitmap_info.biPlanes = 1;
+ bitmap_info.biBitCount = 24;
+ bitmap_info.biCompression = BI_RGB;
+
+ uint8 *bitmap_bits = nullptr;
+ HBITMAP bitmap = ::CreateDIBSection (MemDC, (const BITMAPINFO *)&bitmap_info, DIB_RGB_COLORS,
+ (void **)&bitmap_bits, nullptr, 0L);
+ if (bitmap == nullptr || bitmap_bits == nullptr) {
+ if (bitmap != nullptr) {
+ ::DeleteObject (bitmap);
+ }
+ return nullptr;
+ }
+
+ //
+ // The layout draws into the device context of this font, so the bitmap of the
+ // glyph cache is swapped out while it draws
+ //
+ const int stride = ((text_width * 3) + 3) & ~3;
+ ::memset (bitmap_bits, 0, stride * text_height);
+ HBITMAP old_bitmap = (HBITMAP)::SelectObject (MemDC, bitmap);
+ const bool success = shaped_text.Draw (centered);
+ ::SelectObject (MemDC, old_bitmap);
+
+ uint16 *pixels = nullptr;
+ if (success) {
+ pixels = W3DNEWARRAY uint16[text_width * text_height];
+ for (int row = 0; row < text_height; ++row) {
+ const uint8 *source = bitmap_bits + (row * stride);
+ uint16 *destination = pixels + (row * text_width);
+ for (int col = 0; col < text_width; ++col, source += 3) {
+ //
+ // Store the intensity as 4 bit alpha, like Store_GDI_Char does
+ //
+ const uint8 value = max (source[0], max (source[1], source[2]));
+ destination[col] = (value != 0 ? 0x0FFF : 0) | ((value >> 4) << 12);
+ }
+ }
+ *width = text_width;
+ }
+
+ ::DeleteObject (bitmap);
+ return pixels;
+#else
+ return nullptr;
+#endif
+}
+
+
////////////////////////////////////////////////////////////////////////////////////
//
// Store_GDI_Char
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
index 15426c1e950..dbad91e7676 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
+++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
@@ -47,6 +47,7 @@
** FontCharsClass
*/
class SurfaceClass;
+class ShapedTextClass;
//
// Private data structures
@@ -94,6 +95,14 @@ class FontCharsClass : public RefCountClass
void Blit_Char( WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, int y );
+ //
+ // Text that the glyph cache cannot draw is shaped as a whole, see ShapedTextClass
+ //
+ bool Needs_Shaping( const WCHAR *text );
+ bool Layout_Shaped_Text( ShapedTextClass &shaped_text, const WCHAR *text, int wrap_width );
+ // Returns a buffer of width * shaped_text.Get_Height() pixels that the caller deletes.
+ uint16 * Rasterize_Shaped_Text( const ShapedTextClass &shaped_text, bool centered, int *width );
+
private:
//
@@ -190,6 +199,15 @@ class Render2DSentenceClass {
void Build_Sentence (const WCHAR *text, int *hkX, int *hkY);
void Draw_Sentence (uint32 color = 0xFFFFFFFF);
+ // Returns whether the last built sentence was shaped as a whole. Shaped sentences have no
+ // hot key position.
+ bool Is_Shaped_Sentence () const { return IsShaped; }
+
+ // Gets the width of the widest line of shaped text, the distance from its left edge to a caret
+ // after the last character, and whether the text reads right-to-left. Fails for text that is
+ // not shaped.
+ bool Get_Shaped_Text_Metrics (const WCHAR *text, int *width, int *caret_x, bool *right_to_left);
+
//
// Texture hint
//
@@ -235,9 +253,13 @@ class Render2DSentenceClass {
void Build_Textures ();
void Record_Sentence_Chunk ();
void Allocate_New_Surface (const WCHAR *text, bool justCalcExtents = false);
+ void Allocate_New_Surface (int text_width, bool justCalcExtents = false);
void Release_Pending_Surfaces ();
void Build_Sentence_Centered (const WCHAR *text, int *hkX, int *hkY);
Vector2 Build_Sentence_Not_Centered (const WCHAR *text, int *hkX, int *hkY,bool justCalcExtents = false );
+ bool Layout_Shaped_Text (ShapedTextClass &shaped_text, const WCHAR *text, int wrap_width);
+ bool Get_Shaped_Text_Extents (const WCHAR *text, int wrap_width, Vector2 *extents);
+ bool Build_Shaped_Sentence (const WCHAR *text);
//
// Private member data
//
@@ -261,6 +283,7 @@ class Render2DSentenceClass {
bool IsClippedEnabled;
bool ParseHotKey;
bool useHardWordWrap;
+ bool IsShaped;
uint16 * LockedPtr;
int LockedStride;
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.cpp b/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.cpp
new file mode 100644
index 00000000000..d8a8cef5e1f
--- /dev/null
+++ b/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.cpp
@@ -0,0 +1,587 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2026 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#include "shapedtext.h"
+#include "Usp10Loader.h"
+
+
+////////////////////////////////////////////////////////////////////////////////////
+// Local functions
+////////////////////////////////////////////////////////////////////////////////////
+
+static inline bool Is_Surrogate (WCHAR ch)
+{
+ return ch >= 0xD800 && ch <= 0xDFFF;
+}
+
+
+// GetGlyphIndicesW is resolved at runtime, because the headers of VC6 do not declare it.
+typedef DWORD (WINAPI *Get_Glyph_Indices_Function)(HDC dc, const WCHAR *text, int length, WORD *glyphs, DWORD flags);
+enum { MARK_NONEXISTING_GLYPHS = 0x0001 };
+
+static Get_Glyph_Indices_Function Get_Glyph_Indices ()
+{
+ static const Get_Glyph_Indices_Function function =
+ (Get_Glyph_Indices_Function)::GetProcAddress (::GetModuleHandleA ("gdi32.dll"), "GetGlyphIndicesW");
+ return function;
+}
+
+
+// Returns the bidirectional type of a character outside of the Basic Multilingual Plane. Emoji and
+// other symbols are neutral, historic scripts of the right-to-left blocks are right-to-left.
+static int Get_Supplementary_Direction (unsigned codepoint)
+{
+ if ((codepoint >= 0x10800 && codepoint <= 0x10FFF) || (codepoint >= 0x1E800 && codepoint <= 0x1EFFF)) {
+ return C2_RIGHTTOLEFT;
+ }
+ if (codepoint >= 0x1F000 && codepoint <= 0x1FBFF) {
+ return C2_OTHERNEUTRAL;
+ }
+ return C2_LEFTTORIGHT;
+}
+
+
+static inline bool Is_Isolate_Initiator (WCHAR ch)
+{
+ return ch >= 0x2066 && ch <= 0x2068;
+}
+
+
+static inline bool Is_Pop_Directional_Isolate (WCHAR ch)
+{
+ return ch == 0x2069;
+}
+
+
+// Returns the length of the text up to the isolate terminator that matches an isolate that has
+// just been opened, or up to the end of the text.
+static int Get_Isolate_Length (const WCHAR *text, int length)
+{
+ int depth = 0;
+ for (int index = 0; index < length; ++index) {
+ if (Is_Isolate_Initiator(text[index])) {
+ ++depth;
+ } else if (Is_Pop_Directional_Isolate(text[index])) {
+ if (depth == 0) {
+ return index;
+ }
+ --depth;
+ }
+ }
+ return length;
+}
+
+
+// Finds the reading direction of text from the direction of most of its strong characters, and
+// from its first strong character when there are as many of each. With the first strong character
+// alone, as in rules P2 and P3 of the Unicode Bidirectional Algorithm, a mostly Arabic chat message
+// that starts with an English word reads in the wrong order. Characters inside of directional
+// isolates count only if there are no strong characters outside of them, so that a line such as
+// "[name] message" with an isolated Arabic name and message reads right-to-left as a whole. Text
+// without strong characters reads left-to-right.
+static bool Is_Right_To_Left_Text (const WCHAR *text, int length)
+{
+ // Counts of left-to-right and right-to-left characters, outside and inside of isolates
+ int counts[2][2] = { { 0, 0 }, { 0, 0 } };
+ int first_types[2] = { C2_NOTAPPLICABLE, C2_NOTAPPLICABLE };
+ int isolate_depth = 0;
+ for (int index = 0; index < length; ++index) {
+ const WCHAR ch = text[index];
+ if (Is_Isolate_Initiator(ch)) {
+ ++isolate_depth;
+ continue;
+ }
+ if (Is_Pop_Directional_Isolate(ch)) {
+ if (isolate_depth > 0) {
+ --isolate_depth;
+ }
+ continue;
+ }
+
+ int type = C2_NOTAPPLICABLE;
+ if (ch >= 0xD800 && ch <= 0xDBFF && index + 1 < length && text[index + 1] >= 0xDC00 && text[index + 1] <= 0xDFFF) {
+ type = Get_Supplementary_Direction(0x10000 + ((ch - 0xD800) << 10) + (text[index + 1] - 0xDC00));
+ ++index;
+ } else if (!Is_Surrogate(ch)) {
+ WORD char_type = C2_NOTAPPLICABLE;
+ if (::GetStringTypeW(CT_CTYPE2, &ch, 1, &char_type)) {
+ type = char_type;
+ }
+ }
+
+ if (type != C2_LEFTTORIGHT && type != C2_RIGHTTOLEFT) {
+ continue;
+ }
+ const int inside = isolate_depth > 0 ? 1 : 0;
+ ++counts[inside][type == C2_RIGHTTOLEFT ? 1 : 0];
+ if (first_types[inside] == C2_NOTAPPLICABLE) {
+ first_types[inside] = type;
+ }
+ }
+
+ const int scope = (counts[0][0] + counts[0][1] > 0) ? 0 : 1;
+ if (counts[scope][0] != counts[scope][1]) {
+ return counts[scope][1] > counts[scope][0];
+ }
+ return first_types[scope] == C2_RIGHTTOLEFT;
+}
+
+
+// Uniscribe does not know the directional isolates of Unicode 6.3 and would draw them as boxes.
+// They are replaced by the directional embeddings of the same direction, which order the text in
+// the same way for what isolates are used for here, such as a message after a player name.
+static void Replace_Directional_Isolates (WCHAR *text, int length)
+{
+ for (int index = 0; index < length; ++index) {
+ switch (text[index]) {
+ case 0x2066: // Left-to-right isolate
+ text[index] = 0x202A; // Left-to-right embedding
+ break;
+ case 0x2067: // Right-to-left isolate
+ text[index] = 0x202B; // Right-to-left embedding
+ break;
+ case 0x2068: // First strong isolate
+ {
+ const int isolate_length = Get_Isolate_Length(text + index + 1, length - index - 1);
+ text[index] = Is_Right_To_Left_Text(text + index + 1, isolate_length) ? 0x202B : 0x202A;
+ break;
+ }
+ case 0x2069: // Pop directional isolate
+ text[index] = 0x202C; // Pop directional formatting
+ break;
+ }
+ }
+}
+
+
+// Gets the directional embeddings and overrides that are still open at the end of the text.
+static int Get_Open_Embeddings (const WCHAR *text, int length, WCHAR *embeddings, int max_count)
+{
+ int count = 0;
+ int overflow = 0;
+ for (int index = 0; index < length; ++index) {
+ const WCHAR ch = text[index];
+ if (ch >= 0x202A && ch <= 0x202E && ch != 0x202C) {
+ if (count < max_count) {
+ embeddings[count++] = ch;
+ } else {
+ ++overflow;
+ }
+ } else if (ch == 0x202C) {
+ if (overflow > 0) {
+ --overflow;
+ } else if (count > 0) {
+ --count;
+ }
+ }
+ }
+ return count;
+}
+
+
+static SCRIPT_STRING_ANALYSIS Analyse_Text (HDC dc, const WCHAR *text, int length, bool right_to_left, DWORD flags)
+{
+ SCRIPT_CONTROL control;
+ SCRIPT_STATE state;
+ ::memset(&control, 0, sizeof(control));
+ ::memset(&state, 0, sizeof(state));
+ state.uBidiLevel = right_to_left ? 1 : 0;
+
+ flags |= SSA_GLYPHS | SSA_FALLBACK | SSA_LINK;
+ if (right_to_left) {
+ flags |= SSA_RTL;
+ }
+
+ // The glyph buffer size is the one recommended by the documentation of ScriptStringAnalyse.
+ SCRIPT_STRING_ANALYSIS analysis = nullptr;
+ if (::ScriptStringAnalyse(dc, text, length, length * 3 / 2 + 16, -1, flags, 0, &control, &state,
+ nullptr, nullptr, nullptr, &analysis) != S_OK)
+ {
+ return nullptr;
+ }
+ return analysis;
+}
+
+
+static int Get_Length_Without_Trailing_White_Space (const SCRIPT_LOGATTR *attributes, int start, int end)
+{
+ while (end > start && attributes[end - 1].fWhiteSpace) {
+ --end;
+ }
+ return end - start;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// ShapedTextClass
+//
+////////////////////////////////////////////////////////////////////////////////////
+ShapedTextClass::ShapedTextClass () :
+ DC (nullptr),
+ Width (0),
+ LineHeight (0)
+{
+}
+
+
+ShapedTextClass::~ShapedTextClass ()
+{
+ Free ();
+}
+
+
+void
+ShapedTextClass::Free ()
+{
+ for (int index = 0; index < Lines.Count (); ++index) {
+ if (Lines[index].Analysis != nullptr) {
+ ::ScriptStringFree (&Lines[index].Analysis);
+ }
+ }
+ Lines.Delete_All ();
+
+ for (int text_index = 0; text_index < Texts.Count (); ++text_index) {
+ delete [] Texts[text_index];
+ }
+ Texts.Delete_All ();
+
+ DC = nullptr;
+ Width = 0;
+ LineHeight = 0;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Allocate_Text
+//
+// The analyses may refer to the text that they were made from, so it is kept
+// until they are freed.
+//
+////////////////////////////////////////////////////////////////////////////////////
+WCHAR *
+ShapedTextClass::Allocate_Text (int length)
+{
+ WCHAR *text = W3DNEWARRAY WCHAR[length + 1];
+ text[length] = 0;
+ Texts.Add (text);
+ return text;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Needs_Shaping
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+ShapedTextClass::Needs_Shaping (HDC dc, const WCHAR *text)
+{
+ if (text == nullptr || !Usp10Loader::isLoaded ()) {
+ return false;
+ }
+
+ int length = 0;
+ bool has_unicode = false;
+ for (; text[length] != 0; ++length) {
+ if (Is_Surrogate (text[length])) {
+ return true;
+ }
+ if (text[length] >= 0x0100) {
+ has_unicode = true;
+ }
+ }
+
+ // Latin-1 is always drawn by the glyph cache.
+ if (!has_unicode) {
+ return false;
+ }
+
+ if (::ScriptIsComplex (text, length, SIC_COMPLEX) == S_OK) {
+ return true;
+ }
+
+ // Characters without a glyph in the font would be drawn as boxes.
+ const Get_Glyph_Indices_Function get_glyph_indices = Get_Glyph_Indices ();
+ if (get_glyph_indices == nullptr) {
+ return false;
+ }
+
+ WORD *glyphs = W3DNEWARRAY WORD[length];
+ bool has_missing_glyph = false;
+ if (get_glyph_indices (dc, text, length, glyphs, MARK_NONEXISTING_GLYPHS) != GDI_ERROR) {
+ for (int index = 0; index < length; ++index) {
+ if (text[index] >= 0x0100 && glyphs[index] == 0xFFFF) {
+ has_missing_glyph = true;
+ break;
+ }
+ }
+ }
+ delete [] glyphs;
+ return has_missing_glyph;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Layout
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+ShapedTextClass::Layout (HDC dc, const WCHAR *text, int wrap_width, int line_height)
+{
+ Free ();
+
+ if (dc == nullptr || text == nullptr || line_height <= 0) {
+ return false;
+ }
+
+ DC = dc;
+ LineHeight = line_height;
+
+ const WCHAR *paragraph = text;
+ for (;;) {
+ const WCHAR *paragraph_end = paragraph;
+ while (*paragraph_end != 0 && *paragraph_end != L'\n') {
+ ++paragraph_end;
+ }
+
+ if (!Add_Paragraph (paragraph, (int)(paragraph_end - paragraph), wrap_width)) {
+ Free ();
+ return false;
+ }
+
+ if (*paragraph_end == 0) {
+ break;
+ }
+ paragraph = paragraph_end + 1;
+ }
+
+ for (int index = 0; index < Lines.Count (); ++index) {
+ Width = max (Width, Lines[index].Width);
+ }
+
+ if (Width <= 0) {
+ Free ();
+ return false;
+ }
+ return true;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Add_Line
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+ShapedTextClass::Add_Line (const WCHAR *paragraph, int start, int length, bool right_to_left)
+{
+ LineStruct line;
+ line.Analysis = nullptr;
+ line.Offset = 0;
+ line.Length = length;
+ line.Width = 0;
+ line.RightToLeft = right_to_left;
+
+ if (length > 0) {
+ //
+ // Embeddings that are open where a wrapped line starts are opened again on it
+ //
+ enum { MAX_OPEN_EMBEDDINGS = 16 };
+ WCHAR open_embeddings[MAX_OPEN_EMBEDDINGS];
+ line.Offset = Get_Open_Embeddings (paragraph, start, open_embeddings, MAX_OPEN_EMBEDDINGS);
+ if (line.Offset == 0) {
+ line.Analysis = Analyse_Text (DC, paragraph + start, length, right_to_left, 0);
+ } else {
+ WCHAR *text = Allocate_Text (line.Offset + length);
+ ::memcpy (text, open_embeddings, line.Offset * sizeof (WCHAR));
+ ::memcpy (text + line.Offset, paragraph + start, length * sizeof (WCHAR));
+ line.Analysis = Analyse_Text (DC, text, line.Offset + length, right_to_left, 0);
+ }
+
+ const SIZE *size = line.Analysis != nullptr ? ::ScriptString_pSize (line.Analysis) : nullptr;
+ if (size == nullptr) {
+ if (line.Analysis != nullptr) {
+ ::ScriptStringFree (&line.Analysis);
+ }
+ return false;
+ }
+ line.Width = size->cx;
+ }
+
+ Lines.Add (line);
+ return true;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Add_Paragraph
+//
+// Breaks the paragraph at soft break opportunities so that no line reaches the wrap
+// width. A word that is too wide for a line of its own is broken between clusters.
+// White space at the end of a wrapped line is left out, so that it does not shift
+// the line away from the edge where its paragraph starts.
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+ShapedTextClass::Add_Paragraph (const WCHAR *paragraph_text, int length, int wrap_width)
+{
+ const bool right_to_left = Is_Right_To_Left_Text (paragraph_text, length);
+
+ WCHAR *text = Allocate_Text (length);
+ ::memcpy (text, paragraph_text, length * sizeof (WCHAR));
+ Replace_Directional_Isolates (text, length);
+
+ if (wrap_width <= 0 || length == 0) {
+ return Add_Line (text, 0, length, right_to_left);
+ }
+
+ SCRIPT_STRING_ANALYSIS paragraph = Analyse_Text (DC, text, length, right_to_left, SSA_BREAK);
+ const SIZE *size = paragraph != nullptr ? ::ScriptString_pSize (paragraph) : nullptr;
+ const SCRIPT_LOGATTR *paragraph_attributes = paragraph != nullptr ? ::ScriptString_pLogAttr (paragraph) : nullptr;
+ if (size == nullptr || paragraph_attributes == nullptr) {
+ if (paragraph != nullptr) {
+ ::ScriptStringFree (¶graph);
+ }
+ return false;
+ }
+
+ if (size->cx < wrap_width) {
+ ::ScriptStringFree (¶graph);
+ return Add_Line (text, 0, length, right_to_left);
+ }
+
+ // Copy what is needed, so that the analysis of the paragraph can be released before the
+ // lines are shaped.
+ int *widths = W3DNEWARRAY int[length];
+ SCRIPT_LOGATTR *attributes = W3DNEWARRAY SCRIPT_LOGATTR[length];
+ ::memcpy (attributes, paragraph_attributes, length * sizeof (SCRIPT_LOGATTR));
+ bool success = ::ScriptStringGetLogicalWidths (paragraph, widths) == S_OK;
+ ::ScriptStringFree (¶graph);
+
+ int line_start = 0;
+ int line_width = 0;
+ int break_position = 0;
+ for (int index = 0; success && index < length; ++index) {
+ if (index > line_start && attributes[index].fSoftBreak) {
+ break_position = index;
+ }
+
+ while (success && index > line_start && !attributes[index].fWhiteSpace &&
+ line_width + widths[index] >= wrap_width)
+ {
+ int line_end = break_position > line_start ? break_position : index;
+ while (line_end > line_start + 1 && !attributes[line_end].fCharStop) {
+ --line_end;
+ }
+
+ success = Add_Line (text, line_start,
+ Get_Length_Without_Trailing_White_Space (attributes, line_start, line_end), right_to_left);
+
+ line_start = line_end;
+ line_width = 0;
+ for (int width_index = line_start; width_index < index; ++width_index) {
+ line_width += widths[width_index];
+ }
+ }
+
+ line_width += widths[index];
+ }
+
+ if (success) {
+ success = Add_Line (text, line_start,
+ Get_Length_Without_Trailing_White_Space (attributes, line_start, length), right_to_left);
+ }
+
+ delete [] attributes;
+ delete [] widths;
+ return success;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Get_Line_X
+//
+////////////////////////////////////////////////////////////////////////////////////
+int
+ShapedTextClass::Get_Line_X (const LineStruct &line, bool centered) const
+{
+ const int free_width = Width - line.Width;
+ if (centered) {
+ return free_width / 2;
+ }
+ return line.RightToLeft ? free_width : 0;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Draw
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+ShapedTextClass::Draw (bool centered) const
+{
+ if (DC == nullptr) {
+ return false;
+ }
+
+ const COLORREF old_text_color = ::SetTextColor (DC, RGB (255, 255, 255));
+ const int old_background_mode = ::SetBkMode (DC, TRANSPARENT);
+
+ bool success = true;
+ for (int index = 0; success && index < Lines.Count (); ++index) {
+ const LineStruct &line = Lines[index];
+ if (line.Analysis != nullptr) {
+ success = ::ScriptStringOut (line.Analysis, Get_Line_X (line, centered), index * LineHeight,
+ 0, nullptr, 0, 0, FALSE) == S_OK;
+ }
+ }
+
+ ::SetBkMode (DC, old_background_mode);
+ ::SetTextColor (DC, old_text_color);
+
+ // Complete the drawing before the caller reads the bitmap.
+ return ::GdiFlush () != FALSE && success;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Get_End_Caret_X
+//
+////////////////////////////////////////////////////////////////////////////////////
+int
+ShapedTextClass::Get_End_Caret_X (bool centered) const
+{
+ if (Lines.Count () == 0) {
+ return 0;
+ }
+
+ const LineStruct &line = Lines[Lines.Count () - 1];
+ const int line_x = Get_Line_X (line, centered);
+ int caret_x = line.RightToLeft ? line.Width : 0;
+ if (line.Analysis != nullptr) {
+ ::ScriptStringCPtoX (line.Analysis, line.Offset + line.Length - 1, TRUE, &caret_x);
+ }
+ return line_x + caret_x;
+}
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.h b/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.h
new file mode 100644
index 00000000000..daa28aff768
--- /dev/null
+++ b/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.h
@@ -0,0 +1,91 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2026 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#pragma once
+
+#include "WWLib/always.h"
+#include "WWLib/Vector.h"
+#include "WWLib/win.h"
+#include "usp10_adapter.h"
+
+// Lays out text with Uniscribe for strings that the per character glyph cache of FontCharsClass
+// cannot draw correctly: scripts that join letters or read right-to-left, such as Arabic and
+// Persian, characters outside of the Basic Multilingual Plane, such as emoji, and characters that
+// the font does not have. Uniscribe takes missing glyphs from fallback fonts of the system.
+//
+// The text is split into paragraphs at line feeds. Each paragraph reads in the direction of most of
+// its strong characters. Text in directional isolates, such as a chat message after a player name,
+// keeps its own direction. Paragraphs are wrapped at soft break opportunities, and each line is then
+// shaped and reordered on its own.
+class ShapedTextClass
+{
+public:
+ ShapedTextClass();
+ ~ShapedTextClass();
+
+ // Returns whether the text needs shaping to be displayed correctly with the font that is
+ // selected into the device context.
+ static bool Needs_Shaping( HDC dc, const WCHAR *text );
+
+ // Lays out the text with the font that is selected into the device context. The device
+ // context is used again by Draw, so it must stay valid until this object is destroyed.
+ // Lines are wrapped before they reach the wrap width if it is positive.
+ bool Layout( HDC dc, const WCHAR *text, int wrap_width, int line_height );
+
+ // Draws the lines in white into the bitmap that is selected into the device context of Layout.
+ // Lines of right-to-left paragraphs are aligned to the right unless the text is centered.
+ bool Draw( bool centered ) const;
+
+ int Get_Width() const { return Width; }
+ int Get_Height() const { return Lines.Count() * LineHeight; }
+ int Get_Line_Height() const { return LineHeight; }
+
+ // Returns the distance from the left edge of the text to a caret after the last character.
+ int Get_End_Caret_X( bool centered ) const;
+
+ // Returns whether the first paragraph reads right-to-left.
+ bool Is_Right_To_Left() const { return Lines.Count() > 0 && Lines[0].RightToLeft; }
+
+private:
+ struct LineStruct
+ {
+ SCRIPT_STRING_ANALYSIS Analysis; // Null for an empty line.
+ int Offset; // Number of reopened embeddings in front of the text of the line.
+ int Length;
+ int Width;
+ bool RightToLeft;
+
+ bool operator== (const LineStruct &src) const { return false; }
+ bool operator!= (const LineStruct &src) const { return true; }
+ };
+
+ void Free();
+ WCHAR *Allocate_Text( int length );
+ bool Add_Line( const WCHAR *paragraph, int start, int length, bool right_to_left );
+ bool Add_Paragraph( const WCHAR *text, int length, int wrap_width );
+ int Get_Line_X( const LineStruct &line, bool centered ) const;
+
+ ShapedTextClass( const ShapedTextClass & );
+ ShapedTextClass &operator=( const ShapedTextClass & );
+
+ DynamicVectorClass Lines;
+ DynamicVectorClass Texts;
+ HDC DC;
+ int Width;
+ int LineHeight;
+};
diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
index 0d49e002dab..af576fb9da9 100644
--- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
+++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
@@ -79,6 +79,8 @@ class W3DDisplayString : public DisplayString
virtual void draw( Int x, Int y, Color color, Color dropColor, Int xDrop, Int yDrop ) override; ///< render text with the drop shadow being at the offsets passed in
virtual void getSize( Int *width, Int *height ) override; ///< get render size
virtual Int getWidth( Int charPos = -1) override;
+ virtual Int getCaretOffset() override;
+ virtual Bool isRightToLeft() override;
virtual void setWordWrap( Int wordWrap ) override; ///< set the word wrap width
virtual void setWordWrapCentered( Bool isCentered ) override; ///< If this is set to true, the text on a new line is centered
virtual void setFont( GameFont *font ) override; ///< set a font for display
@@ -92,11 +94,17 @@ class W3DDisplayString : public DisplayString
void usingResources( UnsignedInt frame ); /**< call this whenever display
resources are in use */
void computeExtents(); ///< compupte text width and height
+ void computeShapedMetrics(); ///< measure text that is shaped as a whole
Render2DSentenceClass m_textRenderer; ///< for drawing text
Render2DSentenceClass m_textRendererHotKey; ///< for drawing text
Bool m_textChanged; ///< when contents of string change this is TRUE
Bool m_fontChanged; ///< when font has changed this is TRUE
+ Bool m_shapedMetricsChanged; ///< when the shaped metrics need to be measured again
+ Bool m_isShaped; ///< TRUE if the text is shaped as a whole
+ Bool m_isRightToLeft; ///< TRUE if shaped text starts with a right-to-left paragraph
+ Int m_shapedWidth; ///< width of the widest line of shaped text
+ Int m_shapedCaretOffset; ///< offset of a caret after the last character of shaped text
UnicodeString m_hotkey; ///< holds the current hotkey marker.
Bool m_useHotKey;
ICoord2D m_hotKeyPos;
diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
index c8a4b78ea59..a595be0bb90 100644
--- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
+++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
@@ -86,6 +86,11 @@ W3DDisplayString::W3DDisplayString()
m_size.x = 0;
m_size.y = 0;
m_fontChanged = FALSE;
+ m_shapedMetricsChanged = FALSE;
+ m_isShaped = FALSE;
+ m_isRightToLeft = FALSE;
+ m_shapedWidth = 0;
+ m_shapedCaretOffset = 0;
m_clipRegion.lo.x = 0;
m_clipRegion.lo.y = 0;
m_clipRegion.hi.x = 0;
@@ -138,6 +143,7 @@ void W3DDisplayString::notifyTextChanged()
// we know we must first build the sentence
//
m_textChanged = TRUE;
+ m_shapedMetricsChanged = TRUE;
// reset data for our text renderer
m_textRenderer.Reset();
@@ -189,6 +195,9 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr
}
+ // TheSuperHackers @feature Shaped text has no hot key position, so its hot key is not highlighted.
+ const Bool drawHotKey = m_useHotKey && !m_textRenderer.Is_Shaped_Sentence();
+
//
// if our position has changed, or our colors have changed, or our
// text data has changed, we need to redo the texture quads
@@ -217,7 +226,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr
m_textRenderer.Set_Location( Vector2( m_textPos.x, m_textPos.y ) );
m_textRenderer.Draw_Sentence( m_currTextColor );
- if (m_useHotKey)
+ if (drawHotKey)
{
m_textRendererHotKey.Reset_Polys();
m_textRendererHotKey.Set_Location( Vector2( m_textPos.x + m_hotKeyPos.x , m_textPos.y +m_hotKeyPos.y) );
@@ -228,7 +237,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr
TheDisplay->flush();
- if (m_useHotKey)
+ if (drawHotKey)
{
m_textRendererHotKey.Render();
}
@@ -268,6 +277,13 @@ Int W3DDisplayString::getWidth( Int charPos )
font = m_textRenderer.Peek_Font();
+ if ( font && charPos == -1 )
+ {
+ computeShapedMetrics();
+ if ( m_isShaped )
+ return m_shapedWidth;
+ }
+
if ( font )
{
const WideChar *text = m_textString.str();
@@ -286,6 +302,57 @@ Int W3DDisplayString::getWidth( Int charPos )
return width;
}
+// W3DDisplayString::getCaretOffset ===========================================
+/** Get the horizontal offset of a caret after the last character. Shaped
+ * text can end on its left side when it reads right-to-left. */
+//=============================================================================
+Int W3DDisplayString::getCaretOffset()
+{
+ computeShapedMetrics();
+ if ( m_isShaped )
+ return m_shapedCaretOffset;
+
+ return getWidth();
+}
+
+// W3DDisplayString::isRightToLeft ============================================
+/** TRUE if the text starts with a right-to-left paragraph */
+//=============================================================================
+Bool W3DDisplayString::isRightToLeft()
+{
+ computeShapedMetrics();
+ return m_isShaped && m_isRightToLeft;
+}
+
+// W3DDisplayString::computeShapedMetrics =====================================
+/** Measure text that is shaped as a whole, once for every change of it */
+//=============================================================================
+void W3DDisplayString::computeShapedMetrics()
+{
+ if ( !m_shapedMetricsChanged )
+ return;
+
+ m_shapedMetricsChanged = FALSE;
+ m_isShaped = FALSE;
+ m_isRightToLeft = FALSE;
+ m_shapedWidth = 0;
+ m_shapedCaretOffset = 0;
+
+ if ( getTextLength() == 0 || m_font == nullptr )
+ return;
+
+ int width = 0;
+ int caretOffset = 0;
+ bool rightToLeft = false;
+ if ( m_textRenderer.Get_Shaped_Text_Metrics( getText().str(), &width, &caretOffset, &rightToLeft ) )
+ {
+ m_isShaped = TRUE;
+ m_isRightToLeft = rightToLeft;
+ m_shapedWidth = width;
+ m_shapedCaretOffset = caretOffset;
+ }
+}
+
// W3DDisplayString::setFont ==================================================
/** Set the font for this particular display string */
//=============================================================================
@@ -315,6 +382,7 @@ void W3DDisplayString::setFont( GameFont *font )
// set flag telling us the font has changed since last render
m_fontChanged = TRUE;
+ m_shapedMetricsChanged = TRUE;
}
From 25e7747762e6452a1bc06b298a2e49290896d996 Mon Sep 17 00:00:00 2001
From: MAWKLY VK <206752684+MAWKLY-VK@users.noreply.github.com>
Date: Wed, 30 Sep 2026 23:11:42 +0300
Subject: [PATCH 3/5] fix(text): Stop drawing shaped text when a sentence
surface is not available
Allocate_New_Surface leaves no surface behind when the D3D surface cannot be
created. Build_Shaped_Sentence now stops in that case, and when a surface
cannot be locked, so that the sentence falls back to the glyph cache instead
of writing through a null pointer.
---
.../Source/WWVegas/WW3D2/render2dsentence.cpp | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
index 9341a20ddc1..6d42bb34bef 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
+++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
@@ -1326,11 +1326,13 @@ Render2DSentenceClass::Build_Shaped_Sentence (const WCHAR *text)
TextureOffset.Set (TEXTURE_OFFSET, 0);
TextureStartX = TEXTURE_OFFSET;
- for (int line = 0; line < line_count; ++line) {
+ // TheSuperHackers @bugfix Stop when a sentence surface cannot be created or locked.
+ bool success = CurSurface != nullptr;
+ for (int line = 0; success && line < line_count; ++line) {
Cursor.Set (0, (float)(line * line_height));
int source_x = 0;
- while (source_x < text_width) {
+ while (success && source_x < text_width) {
const int chunk_width = min (text_width - source_x, CurrTextureSize - 1 - TextureOffset.I);
if (chunk_width <= 0) {
//
@@ -1339,6 +1341,7 @@ Render2DSentenceClass::Build_Shaped_Sentence (const WCHAR *text)
TextureOffset.J += line_height;
if ((TextureOffset.J + line_height) >= CurrTextureSize) {
Allocate_New_Surface (text_width * (line_count - line) - source_x);
+ success = CurSurface != nullptr;
}
TextureOffset.I = TEXTURE_OFFSET;
TextureStartX = TEXTURE_OFFSET;
@@ -1347,7 +1350,10 @@ Render2DSentenceClass::Build_Shaped_Sentence (const WCHAR *text)
if (LockedPtr == nullptr) {
LockedPtr = (uint16 *)CurSurface->Lock (&LockedStride);
- WWASSERT (LockedPtr != nullptr);
+ if (LockedPtr == nullptr) {
+ success = false;
+ break;
+ }
}
const int dest_inc = LockedStride >> 1;
@@ -1366,7 +1372,7 @@ Render2DSentenceClass::Build_Shaped_Sentence (const WCHAR *text)
}
delete [] raster;
- return true;
+ return success;
#else
return false;
#endif
From d344dc7b0ac30024464edbd33d4d0e773580403f Mon Sep 17 00:00:00 2001
From: MAWKLY VK <206752684+MAWKLY-VK@users.noreply.github.com>
Date: Wed, 30 Sep 2026 21:13:05 +0300
Subject: [PATCH 4/5] feat(chat): Keep the reading order of player names and
messages in chat lines
A chat line such as "[name] message" is one paragraph. When the name is English
and the message mixes Arabic and English, the parts of the Arabic message end
up in left-to-right order around the English words and read in the wrong order.
Player names and messages that contain right-to-left characters are now
wrapped in Unicode first strong isolates, so that each keeps its own reading
order within the line. Chat lines without right-to-left characters are not
changed.
---
.../Include/GameClient/DisplayString.h | 5 ++++
.../Source/GameClient/DisplayString.cpp | 27 +++++++++++++++++++
.../Source/GameNetwork/ConnectionManager.cpp | 5 ++--
.../Source/GameNetwork/GameSpy/Chat.cpp | 5 ++--
.../Source/GameNetwork/LANAPICallbacks.cpp | 9 ++++---
5 files changed, 43 insertions(+), 8 deletions(-)
diff --git a/Core/GameEngine/Include/GameClient/DisplayString.h b/Core/GameEngine/Include/GameClient/DisplayString.h
index aa3b400e2a1..549fa31a4bd 100644
--- a/Core/GameEngine/Include/GameClient/DisplayString.h
+++ b/Core/GameEngine/Include/GameClient/DisplayString.h
@@ -128,3 +128,8 @@ inline void DisplayString::notifyTextChanged() {}
inline DisplayString *DisplayString::next() { return m_next; }
// EXTERNALS //////////////////////////////////////////////////////////////////
+
+// TheSuperHackers @feature Wraps text, such as a player name or a chat message, in a Unicode
+// first strong isolate if it contains right-to-left characters, so that it keeps its own reading
+// order when it is shown next to other text, as in "[name] message".
+UnicodeString IsolateBidiText( const UnicodeString &text );
diff --git a/Core/GameEngine/Source/GameClient/DisplayString.cpp b/Core/GameEngine/Source/GameClient/DisplayString.cpp
index a276c7a78bf..efbe4f8dced 100644
--- a/Core/GameEngine/Source/GameClient/DisplayString.cpp
+++ b/Core/GameEngine/Source/GameClient/DisplayString.cpp
@@ -64,10 +64,37 @@
// PRIVATE FUNCTIONS //////////////////////////////////////////////////////////
+// Hebrew, Arabic and the other right-to-left scripts of the Basic Multilingual Plane
+static Bool isRightToLeftChar( WideChar ch )
+{
+ return (ch >= 0x0590 && ch <= 0x08FF) || (ch >= 0xFB1D && ch <= 0xFDFF) || (ch >= 0xFE70 && ch <= 0xFEFC);
+}
+
///////////////////////////////////////////////////////////////////////////////
// PUBLIC FUNCTIONS ///////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
+// IsolateBidiText ============================================================
+/** Text without right-to-left characters is returned as it is, so that it is
+ * still drawn by the glyph cache of the font. */
+//=============================================================================
+UnicodeString IsolateBidiText( const UnicodeString &text )
+{
+ const WideChar *str = text.str();
+ for( Int i = 0; str[i] != 0; ++i )
+ {
+ if( isRightToLeftChar( str[i] ) )
+ {
+ UnicodeString isolated;
+ isolated.concat( (WideChar)0x2068 ); // first strong isolate
+ isolated.concat( text );
+ isolated.concat( (WideChar)0x2069 ); // pop directional isolate
+ return isolated;
+ }
+ }
+ return text;
+}
+
// DisplayString::DisplayString ===============================================
/** */
//=============================================================================
diff --git a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp
index f347db5a83f..f36df4bd47b 100644
--- a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp
+++ b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp
@@ -40,6 +40,7 @@
#include "Common/Recorder.h"
#include "GameClient/Diplomacy.h"
+#include "GameClient/DisplayString.h"
#include "GameClient/GameText.h"
#include "GameClient/MessageBox.h"
#include "GameNetwork/ConnectionManager.h"
@@ -752,7 +753,7 @@ void ConnectionManager::processDisconnectChat(NetDisconnectChatCommandMsg *msg)
} else if (isPlayerConnected(playerID)) {
name = m_connections[playerID]->getUser()->GetName();
}
- unitext.format(L"[%ls] %ls", name.str(), msg->getText().str());
+ unitext.format(L"[%ls] %ls", IsolateBidiText(name).str(), IsolateBidiText(msg->getText()).str());
// DEBUG_LOG(("ConnectionManager::processDisconnectChat - got message from player %d, message is %ls", playerID, unitext.str()));
TheDisconnectMenu->showChat(unitext); // <-- need to implement this
}
@@ -773,7 +774,7 @@ void ConnectionManager::processChat(NetChatCommandMsg *msg)
name = m_connections[playerID]->getUser()->GetName();
//DEBUG_LOG(("connection is non-null, using %ls", name.str()));
}
- unitext.format(L"[%ls] %ls", name.str(), msg->getText().str());
+ unitext.format(L"[%ls] %ls", IsolateBidiText(name).str(), IsolateBidiText(msg->getText()).str());
// DEBUG_LOG(("ConnectionManager::processChat - got message from player %d (mask %8.8X), message is %ls", playerID, msg->getPlayerMask(), unitext.str()));
AsciiString playerName;
diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/Chat.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/Chat.cpp
index c9928461183..8e6dbe3aeb6 100644
--- a/Core/GameEngine/Source/GameNetwork/GameSpy/Chat.cpp
+++ b/Core/GameEngine/Source/GameNetwork/GameSpy/Chat.cpp
@@ -30,6 +30,7 @@
#include "Common/AudioEventRTS.h"
#include "Common/INI.h"
+#include "GameClient/DisplayString.h"
#include "GameClient/GameText.h"
#include "GameClient/GadgetListBox.h"
#include "GameClient/LanguageFilter.h"
@@ -301,11 +302,11 @@ void GameSpyInfo::addChat( PlayerInfo p, UnicodeString msg, Bool isPublic, Bool
UnicodeString fullMsg;
if (isAction)
{
- fullMsg.format( L"%ls %ls", name.str(), msg.str() );
+ fullMsg.format( L"%ls %ls", IsolateBidiText(name).str(), IsolateBidiText(msg).str() );
}
else
{
- fullMsg.format( L"[%ls] %ls", name.str(), msg.str() );
+ fullMsg.format( L"[%ls] %ls", IsolateBidiText(name).str(), IsolateBidiText(msg).str() );
}
Int index = addText(fullMsg, GameSpyColor[style], win);
diff --git a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp
index 701ef3e987e..75d05011f64 100644
--- a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp
+++ b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp
@@ -38,6 +38,7 @@
#include "Common/QuotedPrintable.h"
#include "Common/RandomValue.h"
#include "Common/UserPreferences.h"
+#include "GameClient/DisplayString.h"
#include "GameClient/GameText.h"
#include "GameClient/LanguageFilter.h"
#include "GameClient/MapUtil.h"
@@ -690,9 +691,9 @@ void LANAPI::OnChat( UnicodeString player, UnsignedInt ip, UnicodeString message
index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatSystemColor, -1, -1);
break;
case LANAPIInterface::LANCHAT_EMOTE:
- unicodeChat = player;
+ unicodeChat = IsolateBidiText(player);
unicodeChat.concat(L' ');
- unicodeChat.concat(message);
+ unicodeChat.concat(IsolateBidiText(message));
if (ip == m_localIP)
index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatLocalActionColor, -1, -1);
else
@@ -721,9 +722,9 @@ void LANAPI::OnChat( UnicodeString player, UnsignedInt ip, UnicodeString message
}
unicodeChat = L"[";
- unicodeChat.concat(player);
+ unicodeChat.concat(IsolateBidiText(player));
unicodeChat.concat(L"] ");
- unicodeChat.concat(message);
+ unicodeChat.concat(IsolateBidiText(message));
if (ip == m_localIP)
index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatColor, -1, -1);
else
From 82916c558a14d9a8f41b301c5c9e8f6975183802 Mon Sep 17 00:00:00 2001
From: MAWKLY VK <206752684+MAWKLY-VK@users.noreply.github.com>
Date: Wed, 30 Sep 2026 23:15:19 +0300
Subject: [PATCH 5/5] feat(online): Keep the reading order of names and
messages in room and lobby chat
The services send room and lobby chat as one line, such as "[name] message" or
"[tag] [name] message". When the name is English and the message mixes Arabic
and English, the parts of the Arabic message end up in left-to-right order
around the English words.
IsolateBidiChatLine applies IsolateBidiText to the names in the leading
brackets and to the message after them, so that each keeps its own reading
order. Lines without right-to-left characters are not changed.
---
.../Include/GameClient/DisplayString.h | 5 ++++
.../Source/GameClient/DisplayString.cpp | 25 +++++++++++++++++++
.../OnlineServices_RoomsInterface.cpp | 13 +++++++++-
3 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/Core/GameEngine/Include/GameClient/DisplayString.h b/Core/GameEngine/Include/GameClient/DisplayString.h
index 549fa31a4bd..3b5f53a5dab 100644
--- a/Core/GameEngine/Include/GameClient/DisplayString.h
+++ b/Core/GameEngine/Include/GameClient/DisplayString.h
@@ -133,3 +133,8 @@ inline DisplayString *DisplayString::next() { return m_next; }
// first strong isolate if it contains right-to-left characters, so that it keeps its own reading
// order when it is shown next to other text, as in "[name] message".
UnicodeString IsolateBidiText( const UnicodeString &text );
+
+// TheSuperHackers @feature Applies IsolateBidiText to the names in the leading brackets of a chat line
+// that was put together elsewhere, such as "[name] message" or "[tag] [name] message", and to the
+// message after them.
+UnicodeString IsolateBidiChatLine( const UnicodeString &line );
diff --git a/Core/GameEngine/Source/GameClient/DisplayString.cpp b/Core/GameEngine/Source/GameClient/DisplayString.cpp
index efbe4f8dced..1fb976c9565 100644
--- a/Core/GameEngine/Source/GameClient/DisplayString.cpp
+++ b/Core/GameEngine/Source/GameClient/DisplayString.cpp
@@ -95,6 +95,31 @@ UnicodeString IsolateBidiText( const UnicodeString &text )
return text;
}
+// IsolateBidiChatLine ========================================================
+/** A leading group ends at the first closing bracket that a space follows */
+//=============================================================================
+UnicodeString IsolateBidiChatLine( const UnicodeString &line )
+{
+ UnicodeString result;
+ const WideChar *str = line.str();
+ while( str[0] == L'[' )
+ {
+ const WideChar *close = wcschr( str, L']' );
+ if( close == nullptr || close[1] != L' ' )
+ break;
+
+ UnicodeString name;
+ name.set( str + 1, (Int)(close - str - 1) );
+ result.concat( L'[' );
+ result.concat( IsolateBidiText( name ) );
+ result.concat( L"] " );
+ str = close + 2;
+ }
+
+ result.concat( IsolateBidiText( UnicodeString( str ) ) );
+ return result;
+}
+
// DisplayString::DisplayString ===============================================
/** */
//=============================================================================
diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp
index 07e13867223..f670097012d 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp
@@ -8,6 +8,7 @@
#include "../OnlineServices_Init.h"
#include "../HTTP/HTTPManager.h"
#include "GameNetwork/GameSpy/PeerDefs.h"
+#include "GameClient/DisplayString.h"
// one clock for every websocket timestamp; utc_clock also loads the time zone database on MSVC
static int64_t NowMs()
@@ -1023,8 +1024,14 @@ void WebSocket::Tick()
SYSTEMTIME systemTime;
GetLocalTime(&systemTime);
+ // TheSuperHackers @feature Keep the reading order of the name and the message
+ // when either contains right-to-left text. Actions have no brackets to split at.
+ UnicodeString message(from_utf8(chatData.message).c_str());
+ if (!chatData.action)
+ message = IsolateBidiChatLine(message);
+
UnicodeString unicodeStr;
- unicodeStr.format(L"[%2.2d:%2.2d] %s", systemTime.wHour, systemTime.wMinute, from_utf8(chatData.message).c_str());
+ unicodeStr.format(L"[%2.2d:%2.2d] %s", systemTime.wHour, systemTime.wMinute, message.str());
Color color = DetermineColorForChatMessage(EChatMessageType::CHAT_MESSAGE_TYPE_NETWORK_ROOM, true, chatData.action, chatData.admin, chatData.name_change);
@@ -1427,6 +1434,10 @@ void WebSocket::Tick()
if (bParsed)
{
UnicodeString unicodeStr(from_utf8(chatData.message).c_str());
+ // TheSuperHackers @feature Keep the reading order of the name and the message
+ // when either contains right-to-left text.
+ if (!chatData.action && !chatData.announcement)
+ unicodeStr = IsolateBidiChatLine(unicodeStr);
NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface();
if (pLobbyInterface != nullptr)