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/GameEngine/Include/GameClient/DisplayString.h b/Core/GameEngine/Include/GameClient/DisplayString.h
index 042ab1e6963..3b5f53a5dab 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;
@@ -126,3 +128,13 @@ 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 );
+
+// 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 a276c7a78bf..1fb976c9565 100644
--- a/Core/GameEngine/Source/GameClient/DisplayString.cpp
+++ b/Core/GameEngine/Source/GameClient/DisplayString.cpp
@@ -64,10 +64,62 @@
// 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;
+}
+
+// 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/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
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/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/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..6d42bb34bef 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,181 @@ 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;
+
+ // 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 (success && 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);
+ success = CurSurface != nullptr;
+ }
+ TextureOffset.I = TEXTURE_OFFSET;
+ TextureStartX = TEXTURE_OFFSET;
+ continue;
+ }
+
+ if (LockedPtr == nullptr) {
+ LockedPtr = (uint16 *)CurSurface->Lock (&LockedStride);
+ if (LockedPtr == nullptr) {
+ success = false;
+ break;
+ }
+ }
+
+ 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 success;
+#else
+ return false;
+#endif
+}
+
+
////////////////////////////////////////////////////////////////////////////////////
//
// FontCharsClass
@@ -1188,6 +1401,9 @@ FontCharsClass::FontCharsClass () :
{
AlternateUnicodeFont = nullptr;
::memset( ASCIICharArray, 0, sizeof (ASCIICharArray) );
+#if defined(_WIN32)
+ Usp10Loader::load();
+#endif
}
@@ -1205,6 +1421,9 @@ FontCharsClass::~FontCharsClass ()
Free_GDI_Font();
Free_Character_Arrays();
+#if defined(_WIN32)
+ Usp10Loader::unload();
+#endif
}
@@ -1316,6 +1535,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/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
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)
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;
}