diff --git a/Core/GameEngine/Include/GameClient/DisplayString.h b/Core/GameEngine/Include/GameClient/DisplayString.h
index 042ab1e6963..462b1778bb8 100644
--- a/Core/GameEngine/Include/GameClient/DisplayString.h
+++ b/Core/GameEngine/Include/GameClient/DisplayString.h
@@ -91,6 +91,7 @@ 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 void setComplexTextEnabled( Bool enabled ) = 0; ///< enable shaped complex text for this string
virtual void setUseHotkey( Bool useHotkey, Color hotKeyColor ) = 0;
diff --git a/Core/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp b/Core/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp
index 773c22dd94e..a1255937235 100644
--- a/Core/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp
+++ b/Core/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp
@@ -2718,6 +2718,11 @@ GameWindow *GameWindowManager::gogoGadgetTextEntry( GameWindow *parent,
data->text = TheDisplayStringManager->newDisplayString();
data->sText = TheDisplayStringManager->newDisplayString();
data->constructText = TheDisplayStringManager->newDisplayString();
+ // TheSuperHackers @bugfix Omar Aglan 28/08/2026 Keep editable strings on
+ // the legacy path until shaped caret metrics are supported.
+ data->text->setComplexTextEnabled(FALSE);
+ data->sText->setComplexTextEnabled(FALSE);
+ data->constructText->setComplexTextEnabled(FALSE);
// set the max for the text lengths
// data->text->allocateFixed( ENTRY_TEXT_LEN );
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
index 5fe9bf9a01a..31260eff0c6 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
+++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
@@ -40,6 +40,9 @@
#include "WWDebug/wwprofile.h"
#include "WWDebug/wwmemlog.h"
#include "dx8wrapper.h"
+#if defined(_WIN32)
+#include "WWLib/Usp10Loader.h"
+#endif
////////////////////////////////////////////////////////////////////////////////////
@@ -62,6 +65,7 @@ Render2DSentenceClass::Render2DSentenceClass () :
CurSurface (nullptr),
CurrTextureSize (0),
MonoSpaced (false),
+ ComplexTextEnabled (true),
IsClippedEnabled (false),
ClipRect (0, 0, 0, 0),
BaseLocation (0, 0),
@@ -148,7 +152,6 @@ Render2DSentenceClass::Reset ()
Cursor.Set (0, 0);
MonoSpaced = false;
- ParseHotKey = false;
Release_Pending_Surfaces ();
Reset_Sentence_Data ();
@@ -250,6 +253,11 @@ Render2DSentenceClass::Set_Location (const Vector2 &loc)
Vector2
Render2DSentenceClass::Get_Text_Extents (const WCHAR *text)
{
+ Vector2 complex_extent;
+ if (Get_Complex_Text_Extents(text, &complex_extent)) {
+ return complex_extent;
+ }
+
Vector2 extent (0, Font->Get_Char_Height());
while (*text) {
@@ -270,8 +278,20 @@ Render2DSentenceClass::Get_Text_Extents (const WCHAR *text)
//
////////////////////////////////////////////////////////////////////////////////////
Vector2
-Render2DSentenceClass::Get_Formatted_Text_Extents (const WCHAR *text)
+Render2DSentenceClass::Get_Formatted_Text_Extents (const WCHAR *text, bool *used_complex_text)
{
+ if (used_complex_text != nullptr) {
+ *used_complex_text = false;
+ }
+
+ Vector2 complex_extent;
+ if (Get_Complex_Text_Extents(text, &complex_extent)) {
+ if (used_complex_text != nullptr) {
+ *used_complex_text = true;
+ }
+ return complex_extent;
+ }
+
return Build_Sentence_Not_Centered(text, nullptr, nullptr, true);
}
@@ -564,14 +584,16 @@ Render2DSentenceClass::Draw_Sentence (uint32 color)
//
////////////////////////////////////////////////////////////////////////////////////
void
-Render2DSentenceClass::Record_Sentence_Chunk ()
+Render2DSentenceClass::Record_Sentence_Chunk (int char_height)
{
//
// Do we have anything to store?
//
int width = TextureOffset.I - TextureStartX;
if (width > 0) {
- float char_height = Font->Get_Char_Height ();
+ if (char_height <= 0) {
+ char_height = Font->Get_Char_Height ();
+ }
//
// Build a structure that contains enough information
@@ -597,13 +619,140 @@ Render2DSentenceClass::Record_Sentence_Chunk ()
}
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Is_Single_Line_Complex_Text
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+Render2DSentenceClass::Is_Single_Line_Complex_Text (const WCHAR *text) const
+{
+ // TheSuperHackers @feature Omar Aglan 28/08/2026 Shape eligible complex single-line text
+ // as one paragraph to preserve contextual forms and bidirectional order.
+ if (!ComplexTextEnabled || Font == nullptr || text == nullptr || text[0] == 0 || wcschr(text, L'\n') != nullptr ||
+ ParseHotKey || MonoSpaced)
+ {
+ return false;
+ }
+
+ return Font->Is_Complex_Text(text);
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Is_Complex_Text_Size_Supported
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+Render2DSentenceClass::Is_Complex_Text_Size_Supported (int width, int height) const
+{
+ return width > 0 && height > 0 && height < max(TextureSizeHint, 256) &&
+ (WrapWidth <= 0 || width < WrapWidth);
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Get_Complex_Text_Extents
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+Render2DSentenceClass::Get_Complex_Text_Extents (const WCHAR *text, Vector2 *extents)
+{
+ if (extents == nullptr || !Is_Single_Line_Complex_Text(text)) {
+ return false;
+ }
+
+ int width = 0;
+ int height = 0;
+ if (!Font->Get_Complex_Text_Extents(text, &width, &height) ||
+ !Is_Complex_Text_Size_Supported(width, height))
+ {
+ return false;
+ }
+
+ extents->Set((float)width, (float)height);
+ return true;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Build_Complex_Sentence
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+Render2DSentenceClass::Build_Complex_Sentence (const WCHAR *text)
+{
+ // TheSuperHackers @bugfix Omar Aglan 28/08/2026 Build one bounded raster
+ // before splitting it across sentence textures.
+ uint16 *raster = nullptr;
+ int text_width = 0;
+ int text_height = 0;
+ const int maximum_width = WrapWidth > 0 ? (int)WrapWidth : 0;
+ const int maximum_height = max(TextureSizeHint, 256);
+ if (!Font->Rasterize_Complex_Text(text, &raster, &text_width, &text_height,
+ maximum_width, maximum_height))
+ {
+ return false;
+ }
+
+ Reset_Sentence_Data ();
+ Cursor.Set (0, 0);
+
+ if (CurSurface == nullptr) {
+ Allocate_New_Surface (text, false, text_height);
+ }
+
+ int source_x = 0;
+
+ while (source_x < text_width) {
+ if ((TextureOffset.J + text_height) >= CurrTextureSize) {
+ Allocate_New_Surface (text, false, text_height);
+ if (text_height >= CurrTextureSize) {
+ delete [] raster;
+ Reset_Sentence_Data ();
+ return false;
+ }
+ }
+
+ TextureOffset.I = TEXTURE_OFFSET;
+ TextureStartX = TEXTURE_OFFSET;
+ const int available_width = CurrTextureSize - TEXTURE_OFFSET - 1;
+ const int chunk_width = min(text_width - source_x, available_width);
+
+ if (LockedPtr == nullptr) {
+ LockedPtr = (uint16 *)CurSurface->Lock (&LockedStride);
+ WWASSERT (LockedPtr != nullptr);
+ }
+
+ const int dest_inc = LockedStride >> 1;
+ for (int row = 0; row < text_height; ++row) {
+ const uint16 *source = raster + 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 (text_height);
+ Cursor.X += chunk_width;
+ source_x += chunk_width;
+ TextureOffset.J += text_height;
+ }
+
+ delete [] raster;
+ return true;
+}
+
+
////////////////////////////////////////////////////////////////////////////////////
//
// Allocate_New_Surface
//
////////////////////////////////////////////////////////////////////////////////////
void
-Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExtents)
+Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExtents, int min_texture_size)
{
if (!justCalcExtents)
{
@@ -634,6 +783,9 @@ Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExt
for (int pow2 = 6; pow2 <= 8; pow2 ++) {
int size = 1 << pow2;
+ if (size <= min_texture_size) {
+ continue;
+ }
int row_count = (text_width / size) + 1;
int rows_per_texture = size / (char_height + 1);
@@ -704,7 +856,7 @@ float FindStartingXPos( const WCHAR *text )
// Build_Sentence_Centered
//
////////////////////////////////////////////////////////////////////////////////////
-void Render2DSentenceClass::Build_Sentence_Centered (const WCHAR *text, int *hkX, int *hkY)
+Vector2 Render2DSentenceClass::Build_Sentence_Centered (const WCHAR *text, int *hkX, int *hkY)
{
float char_height = Font->Get_Char_Height ();
int wordWidth = 0;
@@ -944,6 +1096,8 @@ void Render2DSentenceClass::Build_Sentence_Centered (const WCHAR *text, int *hkX
*hkX = hotKeyPosX;
if(hkX)
*hkY = hotKeyPosY;
+
+ return extent;
}
////////////////////////////////////////////////////////////////////////////////////
//
@@ -1140,6 +1294,26 @@ Vector2 Render2DSentenceClass::Build_Sentence_Not_Centered (const WCHAR *text, i
void
Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY)
{
+ Build_Sentence(text, hkX, hkY, nullptr, nullptr);
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Build_Sentence
+//
+////////////////////////////////////////////////////////////////////////////////////
+void
+Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY, bool *used_complex_text,
+ Vector2 *legacy_extents)
+{
+ if (used_complex_text != nullptr) {
+ *used_complex_text = false;
+ }
+ if (legacy_extents != nullptr) {
+ legacy_extents->Set(0, 0);
+ }
+
if (text == nullptr) {
return ;
}
@@ -1147,10 +1321,22 @@ Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY)
if (Font == nullptr)
return;
+ if (Is_Single_Line_Complex_Text(text) && Build_Complex_Sentence(text)) {
+ if (used_complex_text != nullptr) {
+ *used_complex_text = true;
+ }
+ return;
+ }
+
+ Vector2 extents;
if(Centered && (WrapWidth > 0 || wcschr(text,L'\n')))
- Build_Sentence_Centered(text, hkX, hkY);
+ extents = Build_Sentence_Centered(text, hkX, hkY);
else
- Build_Sentence_Not_Centered(text, hkX, hkY);
+ extents = Build_Sentence_Not_Centered(text, hkX, hkY);
+
+ if (legacy_extents != nullptr) {
+ *legacy_extents = extents;
+ }
}
@@ -1269,6 +1455,255 @@ FontCharsClass::Get_Char_Spacing (WCHAR ch)
}
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Is_Complex_Text
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+FontCharsClass::Is_Complex_Text (const WCHAR *text)
+{
+#if defined(_WIN32)
+ if (text == nullptr || text[0] == 0) {
+ return false;
+ }
+
+ return Usp10Loader::ScriptIsComplex(text, (int)wcslen(text), Usp10Loader::SIC_COMPLEX) == S_OK;
+#else
+ return false;
+#endif
+}
+
+
+#if defined(_WIN32)
+static bool Is_Complex_Text_Right_To_Left (const WCHAR *text, int text_length)
+{
+ for (int index = 0; index < text_length; ++index) {
+ WORD character_type = C2_NOTAPPLICABLE;
+ if (::GetStringTypeW(CT_CTYPE2, &text[index], 1, &character_type)) {
+ if (character_type == C2_RIGHTTOLEFT) {
+ return true;
+ }
+ if (character_type == C2_LEFTTORIGHT) {
+ return false;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+struct ComplexTextRun
+{
+ ComplexTextRun () :
+ Analysis(nullptr),
+ Font(nullptr),
+ CharacterPosition(0),
+ CharacterCount(0),
+ Width(0),
+ Ascent(0),
+ BidiLevel(0)
+ {
+ }
+
+ Usp10Loader::ScriptStringAnalysis Analysis;
+ HFONT Font;
+ int CharacterPosition;
+ int CharacterCount;
+ int Width;
+ int Ascent;
+ BYTE BidiLevel;
+};
+
+
+struct ComplexTextLayout
+{
+ ComplexTextLayout () : Runs(nullptr), VisualToLogical(nullptr), RunCount(0), Width(0), Height(0), Ascent(0), Descent(0) {}
+ ~ComplexTextLayout () { Clear(); }
+
+ void Clear ()
+ {
+ if (Runs != nullptr) {
+ for (int index = 0; index < RunCount; ++index) {
+ if (Runs[index].Analysis != nullptr) {
+ Usp10Loader::ScriptStringFree(&Runs[index].Analysis);
+ }
+ }
+ }
+
+ delete [] Runs;
+ delete [] VisualToLogical;
+ Runs = nullptr;
+ VisualToLogical = nullptr;
+ RunCount = 0;
+ }
+
+ ComplexTextRun *Runs;
+ int *VisualToLogical;
+ int RunCount;
+ int Width;
+ int Height;
+ int Ascent;
+ int Descent;
+};
+
+
+static bool Uses_Alternate_Unicode_Font (const WCHAR *text, int character_position, HFONT alternate_font)
+{
+ return alternate_font != nullptr && text[character_position] >= 256;
+}
+
+
+static bool Build_Complex_Text_Layout (HDC dc, const WCHAR *text, int text_length,
+ HFONT primary_font, HFONT alternate_font, ComplexTextLayout *layout)
+{
+ Usp10Loader::ScriptState initial_state = { 0 };
+ initial_state.bidi_level = Is_Complex_Text_Right_To_Left(text, text_length) ? 1 : 0;
+
+ Usp10Loader::ScriptItem *items = W3DNEWARRAY Usp10Loader::ScriptItem[text_length + 1];
+ int item_count = 0;
+ if (Usp10Loader::ScriptItemize(text, text_length, text_length + 1, nullptr, &initial_state, items, &item_count) != S_OK ||
+ item_count <= 0)
+ {
+ delete [] items;
+ return false;
+ }
+
+ Usp10Loader::ScriptLogAttr *attributes = W3DNEWARRAY Usp10Loader::ScriptLogAttr[text_length];
+ for (int break_item_index = 0; break_item_index < item_count; ++break_item_index) {
+ const int item_start = items[break_item_index].character_position;
+ const int item_length = items[break_item_index + 1].character_position - item_start;
+ if (Usp10Loader::ScriptBreak(text + item_start, item_length, &items[break_item_index].analysis,
+ attributes + item_start) != S_OK)
+ {
+ delete [] attributes;
+ delete [] items;
+ return false;
+ }
+ }
+
+ layout->Runs = W3DNEWARRAY ComplexTextRun[text_length];
+
+ int run_index = 0;
+ for (int run_item_index = 0; run_item_index < item_count; ++run_item_index) {
+ const int item_end = items[run_item_index + 1].character_position;
+ int run_start = items[run_item_index].character_position;
+ bool uses_alternate_font = Uses_Alternate_Unicode_Font(text, run_start, alternate_font);
+ for (int run_end = run_start + 1; run_end <= item_end; ++run_end) {
+ const bool run_ends = run_end == item_end ||
+ (attributes[run_end].char_stop &&
+ uses_alternate_font != Uses_Alternate_Unicode_Font(text, run_end, alternate_font));
+ if (run_ends) {
+ ComplexTextRun &run = layout->Runs[run_index++];
+ run.Font = uses_alternate_font ? alternate_font : primary_font;
+ run.CharacterPosition = run_start;
+ run.CharacterCount = run_end - run_start;
+ run.BidiLevel = (BYTE)items[run_item_index].analysis.state.bidi_level;
+ run_start = run_end;
+ if (run_end < item_end) {
+ uses_alternate_font = Uses_Alternate_Unicode_Font(text, run_end, alternate_font);
+ }
+ }
+ }
+ }
+ delete [] attributes;
+ delete [] items;
+ layout->RunCount = run_index;
+ layout->VisualToLogical = W3DNEWARRAY int[run_index];
+
+ BYTE *bidi_levels = W3DNEWARRAY BYTE[run_index];
+ HFONT old_font = (HFONT)::GetCurrentObject(dc, OBJ_FONT);
+ bool success = true;
+ for (int index = 0; index < run_index; ++index) {
+ ComplexTextRun &run = layout->Runs[index];
+ ::SelectObject(dc, run.Font);
+
+ const int glyph_count = run.CharacterCount + run.CharacterCount / 2 + 16;
+ DWORD flags = Usp10Loader::SSA_GLYPHS | Usp10Loader::SSA_FALLBACK;
+ if ((run.BidiLevel & 1) != 0) {
+ flags |= Usp10Loader::SSA_RTL;
+ }
+
+ const HRESULT analysis_result = Usp10Loader::ScriptStringAnalyse(dc,
+ text + run.CharacterPosition, run.CharacterCount, glyph_count, -1, flags, 0,
+ nullptr, nullptr, nullptr, nullptr, nullptr, &run.Analysis);
+ const SIZE *run_size = analysis_result == S_OK ? Usp10Loader::ScriptString_pSize(run.Analysis) : nullptr;
+ TEXTMETRIC text_metrics = { 0 };
+ if (run_size == nullptr || run_size->cx < 0 || run_size->cy <= 0 || !::GetTextMetrics(dc, &text_metrics)) {
+ success = false;
+ break;
+ }
+
+ run.Width = run_size->cx;
+ run.Ascent = min((int)text_metrics.tmAscent, (int)run_size->cy);
+ bidi_levels[index] = run.BidiLevel;
+ layout->Width += run.Width;
+ layout->Ascent = max(layout->Ascent, run.Ascent);
+ layout->Descent = max(layout->Descent, (int)run_size->cy - run.Ascent);
+ }
+ layout->Height = layout->Ascent + layout->Descent;
+
+ if (success) {
+ success = Usp10Loader::ScriptLayout(run_index, bidi_levels, layout->VisualToLogical, nullptr) == S_OK &&
+ layout->Width > 0 && layout->Height > 0;
+ }
+
+ ::SelectObject(dc, old_font);
+ delete [] bidi_levels;
+ return success;
+}
+#endif
+
+
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Get_Complex_Text_Extents
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+FontCharsClass::Get_Complex_Text_Extents (const WCHAR *text, int *width, int *height)
+{
+ if (width == nullptr || height == nullptr) {
+ return false;
+ }
+
+ *width = 0;
+ *height = 0;
+
+#if defined(_WIN32)
+ const int text_length = text == nullptr ? 0 : (int)wcslen(text);
+ if (text_length == 0 || MemDC == nullptr) {
+ return false;
+ }
+
+ HDC text_dc = ::CreateCompatibleDC(MemDC);
+ if (text_dc == nullptr) {
+ return false;
+ }
+
+ bool success = false;
+ {
+ // TheSuperHackers @bugfix Omar Aglan 02/09/2026 Preserve the primary font for
+ // Latin runs and use the configured Unicode font for the remaining runs.
+ ComplexTextLayout layout;
+ HFONT alternate_font = AlternateUnicodeFont != nullptr && AlternateUnicodeFont != this ?
+ AlternateUnicodeFont->GDIFont : nullptr;
+ success = Build_Complex_Text_Layout(text_dc, text, text_length, GDIFont, alternate_font, &layout);
+ if (success) {
+ *width = layout.Width;
+ *height = layout.Height;
+ }
+ }
+
+ ::DeleteDC(text_dc);
+ return success;
+#else
+ return false;
+#endif
+}
+
+
////////////////////////////////////////////////////////////////////////////////////
//
// Blit_Char
@@ -1305,6 +1740,125 @@ FontCharsClass::Blit_Char (WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, i
}
+////////////////////////////////////////////////////////////////////////////////////
+//
+// Rasterize_Complex_Text
+//
+////////////////////////////////////////////////////////////////////////////////////
+bool
+FontCharsClass::Rasterize_Complex_Text (const WCHAR *text, uint16 **raster, int *width, int *height,
+ int maximum_width, int maximum_height)
+{
+ if (raster == nullptr || width == nullptr || height == nullptr) {
+ return false;
+ }
+
+ *raster = nullptr;
+ *width = 0;
+ *height = 0;
+
+#if defined(_WIN32)
+ const int text_length = text == nullptr ? 0 : (int)wcslen(text);
+ if (text_length == 0 || MemDC == nullptr) {
+ return false;
+ }
+
+ HDC text_dc = ::CreateCompatibleDC(MemDC);
+ if (text_dc == nullptr) {
+ return false;
+ }
+
+ ::SetBkColor(text_dc, RGB(0, 0, 0));
+ ::SetTextColor(text_dc, RGB(255, 255, 255));
+ ::SetBkMode(text_dc, TRANSPARENT);
+
+ ComplexTextLayout layout;
+ HFONT alternate_font = AlternateUnicodeFont != nullptr && AlternateUnicodeFont != this ?
+ AlternateUnicodeFont->GDIFont : nullptr;
+ if (!Build_Complex_Text_Layout(text_dc, text, text_length, GDIFont, alternate_font, &layout)) {
+ layout.Clear();
+ ::DeleteDC(text_dc);
+ return false;
+ }
+
+ const int text_width = layout.Width;
+ const int text_height = layout.Height;
+ if ((maximum_width > 0 && text_width >= maximum_width) ||
+ (maximum_height > 0 && text_height >= maximum_height))
+ {
+ layout.Clear();
+ ::DeleteDC(text_dc);
+ return false;
+ }
+
+ BITMAPINFO bitmap_info = { 0 };
+ bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
+ bitmap_info.bmiHeader.biWidth = text_width;
+ bitmap_info.bmiHeader.biHeight = -text_height;
+ bitmap_info.bmiHeader.biPlanes = 1;
+ bitmap_info.bmiHeader.biBitCount = 24;
+ bitmap_info.bmiHeader.biCompression = BI_RGB;
+
+ uint8 *bitmap_bits = nullptr;
+ HBITMAP bitmap = ::CreateDIBSection(MemDC, &bitmap_info, DIB_RGB_COLORS,
+ (void **)&bitmap_bits, nullptr, 0L);
+ if (bitmap == nullptr || bitmap_bits == nullptr) {
+ if (bitmap != nullptr) {
+ ::DeleteObject(bitmap);
+ }
+ layout.Clear();
+ ::DeleteDC(text_dc);
+ return false;
+ }
+
+ HBITMAP old_bitmap = (HBITMAP)::SelectObject(text_dc, bitmap);
+
+ const int bitmap_stride = ((text_width * 3) + 3) & ~3;
+ ::memset(bitmap_bits, 0, bitmap_stride * text_height);
+
+ HFONT old_font = (HFONT)::GetCurrentObject(text_dc, OBJ_FONT);
+ int x = 0;
+ bool success = true;
+ for (int visual_index = 0; visual_index < layout.RunCount; ++visual_index) {
+ ComplexTextRun &run = layout.Runs[layout.VisualToLogical[visual_index]];
+ ::SelectObject(text_dc, run.Font);
+ if (Usp10Loader::ScriptStringOut(run.Analysis, x, layout.Ascent - run.Ascent,
+ 0, nullptr, 0, 0, FALSE) != S_OK)
+ {
+ success = false;
+ break;
+ }
+ x += run.Width;
+ }
+ ::SelectObject(text_dc, old_font);
+
+ if (success) {
+ uint16 *pixels = W3DNEWARRAY uint16[text_width * text_height];
+ for (int row = 0; row < text_height; ++row) {
+ const uint8 *source = bitmap_bits + row * bitmap_stride;
+ uint16 *destination = pixels + row * text_width;
+ for (int column = 0; column < text_width; ++column) {
+ const uint8 pixel_value = source[column * 3];
+ const uint16 pixel_color = pixel_value == 0 ? 0 : 0x0FFF;
+ destination[column] = pixel_color | (((pixel_value >> 4) & 0xF) << 12);
+ }
+ }
+ *raster = pixels;
+ *width = text_width;
+ *height = text_height;
+ }
+
+ ::SelectObject(text_dc, old_bitmap);
+ ::DeleteObject(bitmap);
+ layout.Clear();
+ ::DeleteDC(text_dc);
+ return success;
+#else
+ return false;
+#endif
+}
+
+
////////////////////////////////////////////////////////////////////////////////////
//
// Store_GDI_Char
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
index 15426c1e950..8dca7a2270a 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
+++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
@@ -89,6 +89,10 @@ class FontCharsClass : public RefCountClass
int Get_Char_Height() { return CharHeight; }
int Get_Char_Width( WCHAR ch );
int Get_Char_Spacing( WCHAR ch );
+ bool Is_Complex_Text( const WCHAR *text );
+ bool Get_Complex_Text_Extents( const WCHAR *text, int *width, int *height );
+ bool Rasterize_Complex_Text( const WCHAR *text, uint16 **raster, int *width, int *height,
+ int maximum_width, int maximum_height );
int Get_Extra_Overlap() {return PixelOverlap;}
@@ -182,12 +186,15 @@ class Render2DSentenceClass {
// const Vector2 & Get_Cursor() { return Cursor; }
Vector2 Get_Text_Extents( const WCHAR * text );
- Vector2 Get_Formatted_Text_Extents( const WCHAR * text );
+ Vector2 Get_Formatted_Text_Extents( const WCHAR * text, bool *used_complex_text = nullptr );
+ bool Get_Complex_Text_Extents( const WCHAR *text, Vector2 *extents );
//
// Sentence control
//
void Build_Sentence (const WCHAR *text, int *hkX, int *hkY);
+ void Build_Sentence (const WCHAR *text, int *hkX, int *hkY, bool *used_complex_text,
+ Vector2 *legacy_extents);
void Draw_Sentence (uint32 color = 0xFFFFFFFF);
//
@@ -197,6 +204,14 @@ class Render2DSentenceClass {
int Get_Texture_Size_Hint() const { return TextureSizeHint; }
void Set_Mono_Spaced( bool onoff ) { MonoSpaced = onoff; }
+ bool Set_Complex_Text_Enabled( bool enabled ) {
+ if (ComplexTextEnabled == enabled) {
+ return false;
+ }
+
+ ComplexTextEnabled = enabled;
+ return true;
+ }
private:
@@ -233,11 +248,14 @@ class Render2DSentenceClass {
//
void Reset_Sentence_Data ();
void Build_Textures ();
- void Record_Sentence_Chunk ();
- void Allocate_New_Surface (const WCHAR *text, bool justCalcExtents = false);
+ void Record_Sentence_Chunk (int char_height = 0);
+ void Allocate_New_Surface (const WCHAR *text, bool justCalcExtents = false, int min_texture_size = 0);
void Release_Pending_Surfaces ();
- void Build_Sentence_Centered (const WCHAR *text, int *hkX, int *hkY);
+ Vector2 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 Is_Single_Line_Complex_Text (const WCHAR *text) const;
+ bool Is_Complex_Text_Size_Supported (int width, int height) const;
+ bool Build_Complex_Sentence (const WCHAR *text);
//
// Private member data
//
@@ -254,6 +272,7 @@ class Render2DSentenceClass {
int TextureSizeHint;
SurfaceClass * CurSurface;
bool MonoSpaced;
+ bool ComplexTextEnabled;
float WrapWidth;
bool Centered; // Determines whether or not to center each line
RectClass ClipRect;
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/ww3d.cpp b/Core/Libraries/Source/WWVegas/WW3D2/ww3d.cpp
index c43588d279d..0acf0831f86 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/ww3d.cpp
+++ b/Core/Libraries/Source/WWVegas/WW3D2/ww3d.cpp
@@ -115,6 +115,9 @@
#include "sortingrenderer.h"
#include "WWLib/thread.h"
#include "WWLib/cpudetect.h"
+#if defined(_WIN32)
+#include "WWLib/Usp10Loader.h"
+#endif
#include "dx8texman.h"
#include "animatedsoundmgr.h"
#include "static_sort_list.h"
@@ -384,6 +387,9 @@ WW3DErrorType WW3D::Shutdown()
** Release the animation-triggered sound data
*/
AnimatedSoundMgrClass::Shutdown ();
+#if defined(_WIN32)
+ Usp10Loader::unload();
+#endif
IsInitted = false;
return WW3D_ERROR_OK;
diff --git a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
index 77721250c6c..a74cd8baf2c 100644
--- a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
+++ b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
@@ -162,6 +162,8 @@ if(WIN32)
rcfile.h
registry.cpp
registry.h
+ Usp10Loader.cpp
+ Usp10Loader.h
verchk.cpp
verchk.h
WWCOMUtil.cpp
diff --git a/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.cpp b/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.cpp
new file mode 100644
index 00000000000..6f09f861dae
--- /dev/null
+++ b/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.cpp
@@ -0,0 +1,163 @@
+/*
+** 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"
+
+
+CriticalSectionClass Usp10Loader::CriticalSection;
+HMODULE Usp10Loader::Module = HMODULE(nullptr);
+bool Usp10Loader::LoadAttempted = false;
+Usp10Loader::ScriptIsComplex_t Usp10Loader::ScriptIsComplexPtr = nullptr;
+Usp10Loader::ScriptItemize_t Usp10Loader::ScriptItemizePtr = nullptr;
+Usp10Loader::ScriptBreak_t Usp10Loader::ScriptBreakPtr = nullptr;
+Usp10Loader::ScriptLayout_t Usp10Loader::ScriptLayoutPtr = nullptr;
+Usp10Loader::ScriptStringAnalyse_t Usp10Loader::ScriptStringAnalysePtr = nullptr;
+Usp10Loader::ScriptStringFree_t Usp10Loader::ScriptStringFreePtr = nullptr;
+Usp10Loader::ScriptString_pSize_t Usp10Loader::ScriptString_pSizePtr = nullptr;
+Usp10Loader::ScriptStringOut_t Usp10Loader::ScriptStringOutPtr = nullptr;
+
+
+bool Usp10Loader::load()
+{
+ if (LoadAttempted) {
+ return Module != HMODULE(nullptr);
+ }
+ LoadAttempted = true;
+
+ char dll_path[MAX_PATH];
+ const char dll_name[] = "\\usp10.dll";
+ const UINT path_length = ::GetSystemDirectoryA(dll_path, ARRAY_SIZE(dll_path));
+ if (path_length == 0 || path_length + ARRAY_SIZE(dll_name) > ARRAY_SIZE(dll_path)) {
+ return false;
+ }
+ strcpy(dll_path + path_length, dll_name);
+
+ Module = ::LoadLibraryA(dll_path);
+ if (Module == HMODULE(nullptr)) {
+ return false;
+ }
+
+ ScriptIsComplexPtr = reinterpret_cast(::GetProcAddress(Module, "ScriptIsComplex"));
+ ScriptItemizePtr = reinterpret_cast(::GetProcAddress(Module, "ScriptItemize"));
+ ScriptBreakPtr = reinterpret_cast(::GetProcAddress(Module, "ScriptBreak"));
+ ScriptLayoutPtr = reinterpret_cast(::GetProcAddress(Module, "ScriptLayout"));
+ ScriptStringAnalysePtr = reinterpret_cast(::GetProcAddress(Module, "ScriptStringAnalyse"));
+ ScriptStringFreePtr = reinterpret_cast(::GetProcAddress(Module, "ScriptStringFree"));
+ ScriptString_pSizePtr = reinterpret_cast(::GetProcAddress(Module, "ScriptString_pSize"));
+ ScriptStringOutPtr = reinterpret_cast(::GetProcAddress(Module, "ScriptStringOut"));
+
+ if (ScriptIsComplexPtr == nullptr || ScriptItemizePtr == nullptr || ScriptBreakPtr == nullptr ||
+ ScriptLayoutPtr == nullptr || ScriptStringAnalysePtr == nullptr || ScriptStringFreePtr == nullptr ||
+ ScriptString_pSizePtr == nullptr || ScriptStringOutPtr == nullptr)
+ {
+ freeResources();
+ return false;
+ }
+
+ return true;
+}
+
+
+void Usp10Loader::unload()
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+
+ freeResources();
+ LoadAttempted = false;
+}
+
+
+void Usp10Loader::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;
+}
+
+
+HRESULT Usp10Loader::ScriptIsComplex(const WCHAR *text, int text_length, DWORD flags)
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+ return load() ? ScriptIsComplexPtr(text, text_length, flags) : E_FAIL;
+}
+
+
+HRESULT Usp10Loader::ScriptItemize(const WCHAR *text, int text_length, int item_capacity,
+ const ScriptControl *control, const ScriptState *state, ScriptItem *items, int *item_count)
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+ return load() ? ScriptItemizePtr(text, text_length, item_capacity, control, state, items, item_count) : E_FAIL;
+}
+
+
+HRESULT Usp10Loader::ScriptBreak(const WCHAR *text, int text_length, const ScriptAnalysis *analysis,
+ ScriptLogAttr *attributes)
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+ return load() ? ScriptBreakPtr(text, text_length, analysis, attributes) : E_FAIL;
+}
+
+
+HRESULT Usp10Loader::ScriptLayout(int run_count, const BYTE *levels, int *visual_to_logical,
+ int *logical_to_visual)
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+ return load() ? ScriptLayoutPtr(run_count, levels, visual_to_logical, logical_to_visual) : E_FAIL;
+}
+
+
+HRESULT Usp10Loader::ScriptStringAnalyse(HDC dc, const void *text, int text_length, int glyph_count,
+ int charset, DWORD flags, int required_width, ScriptControl *control, ScriptState *state,
+ const int *spacing, ScriptTabDefinition *tabs, const BYTE *character_classes, ScriptStringAnalysis *analysis)
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+ return load() ? ScriptStringAnalysePtr(dc, text, text_length, glyph_count, charset, flags, required_width,
+ control, state, spacing, tabs, character_classes, analysis) : E_FAIL;
+}
+
+
+HRESULT Usp10Loader::ScriptStringFree(ScriptStringAnalysis *analysis)
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+ return load() ? ScriptStringFreePtr(analysis) : E_FAIL;
+}
+
+
+const SIZE *Usp10Loader::ScriptString_pSize(ScriptStringAnalysis analysis)
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+ return load() ? ScriptString_pSizePtr(analysis) : nullptr;
+}
+
+
+HRESULT Usp10Loader::ScriptStringOut(ScriptStringAnalysis analysis, int x, int y, UINT options,
+ const RECT *rect, int minimum_selection, int maximum_selection, BOOL disabled)
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+ return load() ? ScriptStringOutPtr(analysis, x, y, options, rect, minimum_selection, maximum_selection, disabled) : E_FAIL;
+}
diff --git a/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h b/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h
new file mode 100644
index 00000000000..3b35b03b82b
--- /dev/null
+++ b/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h
@@ -0,0 +1,121 @@
+/*
+** 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 "mutex.h"
+#include "win.h"
+
+
+// This static class loads usp10.dll on first use and unloads it during engine shutdown.
+
+class Usp10Loader
+{
+public:
+
+ typedef void *ScriptStringAnalysis;
+ struct ScriptControl;
+ struct ScriptTabDefinition;
+
+ struct ScriptState
+ {
+ WORD bidi_level : 5;
+ WORD reserved : 11;
+ };
+
+ struct ScriptAnalysis
+ {
+ WORD script : 10;
+ WORD right_to_left : 1;
+ WORD layout_right_to_left : 1;
+ WORD link_before : 1;
+ WORD link_after : 1;
+ WORD logical_order : 1;
+ WORD no_glyph_index : 1;
+ ScriptState state;
+ };
+
+ struct ScriptItem
+ {
+ int character_position;
+ ScriptAnalysis analysis;
+ };
+
+ struct ScriptLogAttr
+ {
+ BYTE soft_break : 1;
+ BYTE white_space : 1;
+ BYTE char_stop : 1;
+ BYTE word_stop : 1;
+ BYTE invalid : 1;
+ BYTE reserved : 3;
+ };
+
+ enum
+ {
+ SIC_COMPLEX = 0x00000001,
+ SSA_FALLBACK = 0x00000020,
+ SSA_GLYPHS = 0x00000080,
+ SSA_RTL = 0x00000100,
+ };
+
+ static HRESULT ScriptIsComplex(const WCHAR *text, int text_length, DWORD flags);
+ static HRESULT ScriptItemize(const WCHAR *text, int text_length, int item_capacity,
+ const ScriptControl *control, const ScriptState *state, ScriptItem *items, int *item_count);
+ static HRESULT ScriptBreak(const WCHAR *text, int text_length, const ScriptAnalysis *analysis,
+ ScriptLogAttr *attributes);
+ static HRESULT ScriptLayout(int run_count, const BYTE *levels, int *visual_to_logical,
+ int *logical_to_visual);
+ static HRESULT ScriptStringAnalyse(HDC dc, const void *text, int text_length, int glyph_count,
+ int charset, DWORD flags, int required_width, ScriptControl *control, ScriptState *state,
+ const int *spacing, ScriptTabDefinition *tabs, const BYTE *character_classes,
+ ScriptStringAnalysis *analysis);
+ static HRESULT ScriptStringFree(ScriptStringAnalysis *analysis);
+ static const SIZE *ScriptString_pSize(ScriptStringAnalysis analysis);
+ static HRESULT ScriptStringOut(ScriptStringAnalysis analysis, int x, int y, UINT options,
+ const RECT *rect, int minimum_selection, int maximum_selection, BOOL disabled);
+ static void unload();
+
+private:
+
+ static bool load();
+ static void freeResources();
+
+ typedef HRESULT (WINAPI *ScriptIsComplex_t)(const WCHAR *, int, DWORD);
+ typedef HRESULT (WINAPI *ScriptItemize_t)(const WCHAR *, int, int, const ScriptControl *,
+ const ScriptState *, ScriptItem *, int *);
+ typedef HRESULT (WINAPI *ScriptBreak_t)(const WCHAR *, int, const ScriptAnalysis *, ScriptLogAttr *);
+ typedef HRESULT (WINAPI *ScriptLayout_t)(int, const BYTE *, int *, int *);
+ typedef HRESULT (WINAPI *ScriptStringAnalyse_t)(HDC, const void *, int, int, int, DWORD, int,
+ ScriptControl *, ScriptState *, const int *, ScriptTabDefinition *, const BYTE *, ScriptStringAnalysis *);
+ typedef HRESULT (WINAPI *ScriptStringFree_t)(ScriptStringAnalysis *);
+ typedef const SIZE *(WINAPI *ScriptString_pSize_t)(ScriptStringAnalysis);
+ typedef HRESULT (WINAPI *ScriptStringOut_t)(ScriptStringAnalysis, int, int, UINT, const RECT *, int, int, BOOL);
+
+ static CriticalSectionClass CriticalSection;
+ static HMODULE Module;
+ static bool LoadAttempted;
+ static ScriptIsComplex_t ScriptIsComplexPtr;
+ static ScriptItemize_t ScriptItemizePtr;
+ static ScriptBreak_t ScriptBreakPtr;
+ static ScriptLayout_t ScriptLayoutPtr;
+ static ScriptStringAnalyse_t ScriptStringAnalysePtr;
+ static ScriptStringFree_t ScriptStringFreePtr;
+ static ScriptString_pSize_t ScriptString_pSizePtr;
+ static ScriptStringOut_t ScriptStringOutPtr;
+};
diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
index 80123dd0919..933daa3f823 100644
--- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
+++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
@@ -79,6 +79,7 @@ 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 void setComplexTextEnabled( Bool enabled ) 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
@@ -97,6 +98,8 @@ class W3DDisplayString : public DisplayString
Render2DSentenceClass m_textRendererHotKey; ///< for drawing text
Bool m_textChanged; ///< when contents of string change this is TRUE
Bool m_fontChanged; ///< when font has chagned this is TRUE
+ Bool m_sentenceChanged; ///< when the rendered sentence needs new polygons
+ Bool m_hasComplexTextExtents; ///< cached size uses shaped complex-text metrics
UnicodeString m_hotkey; ///< holds the current hotkey marker.
Bool m_useHotKey;
ICoord2D m_hotKeyPos;
diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
index eafcf5fe6d6..2df3fd13ae4 100644
--- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
+++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
@@ -86,6 +86,8 @@ W3DDisplayString::W3DDisplayString()
m_size.x = 0;
m_size.y = 0;
m_fontChanged = FALSE;
+ m_sentenceChanged = FALSE;
+ m_hasComplexTextExtents = FALSE;
m_clipRegion.lo.x = 0;
m_clipRegion.lo.y = 0;
m_clipRegion.hi.x = 0;
@@ -145,6 +147,38 @@ void W3DDisplayString::notifyTextChanged()
}
+// W3DDisplayString::checkForChangedTextData ==================================
+/** Rebuild the sentence and update its extents when its source data changes */
+//=============================================================================
+void W3DDisplayString::checkForChangedTextData()
+{
+ if( !m_fontChanged && !m_textChanged )
+ return;
+
+ bool usedComplexText = false;
+ Vector2 legacyExtents;
+ if(m_useHotKey)
+ {
+ m_textRenderer.Build_Sentence(
+ getText().str(), &m_hotKeyPos.x, &m_hotKeyPos.y, &usedComplexText, &legacyExtents);
+ m_textRendererHotKey.Build_Sentence(m_hotkey.str(), nullptr, nullptr);
+ }
+ else
+ m_textRenderer.Build_Sentence(
+ getText().str(), nullptr, nullptr, &usedComplexText, &legacyExtents);
+
+ // TheSuperHackers @bugfix Omar Aglan 04/09/2026 Resolve renderer fallback before callers position complex text.
+ if (m_hasComplexTextExtents && !usedComplexText) {
+ m_size.x = legacyExtents.X;
+ m_size.y = legacyExtents.Y;
+ m_hasComplexTextExtents = FALSE;
+ }
+
+ m_fontChanged = FALSE;
+ m_textChanged = FALSE;
+ m_sentenceChanged = TRUE;
+}
+
// W3DDisplayString::Draw =====================================================
/** Draw the text at the specified location in in the specified colors
* in the parameters. Since we keep an instance of the rendered text
@@ -159,35 +193,13 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor )
}
void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDrop, Int yDrop )
{
- Bool needNewPolys = FALSE;
-
// sanity
if( getTextLength() == 0 )
return; // nothing to draw
- // if our font or text has changed we need to build a new sentence
- if( m_fontChanged || m_textChanged )
- {
- if(m_useHotKey)
- {
- m_textRenderer.Set_Hot_Key_Parse(TRUE);
- m_textRenderer.Build_Sentence( getText().str(), &m_hotKeyPos.x, &m_hotKeyPos.y );
- m_hotkey.translate(TheHotKeyManager->searchHotKey(getText()));
- if(!m_hotkey.isEmpty())
- m_textRendererHotKey.Build_Sentence(m_hotkey.str(), nullptr, nullptr);
- else
- {
- m_useHotKey = FALSE;
- m_textRendererHotKey.Reset();
- }
- }
- else
- m_textRenderer.Build_Sentence( getText().str(), nullptr, nullptr );
- m_fontChanged = FALSE;
- m_textChanged = FALSE;
- needNewPolys = TRUE;
-
- }
+ checkForChangedTextData();
+ Bool needNewPolys = m_sentenceChanged;
+ m_sentenceChanged = FALSE;
//
// if our position has changed, or our colors have changed, or our
@@ -247,6 +259,8 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr
//=============================================================================
void W3DDisplayString::getSize( Int *width, Int *height )
{
+ if ( m_hasComplexTextExtents )
+ checkForChangedTextData();
// assign the width and height we have stored to parameters present
if( width )
@@ -262,6 +276,9 @@ void W3DDisplayString::getSize( Int *width, Int *height )
Int W3DDisplayString::getWidth( Int charPos )
{
+ if ( charPos == -1 && m_hasComplexTextExtents )
+ checkForChangedTextData();
+
FontCharsClass * font;
Int width = 0;
Int count = 0;
@@ -270,6 +287,9 @@ Int W3DDisplayString::getWidth( Int charPos )
if ( font )
{
+ if ( charPos == -1 && m_hasComplexTextExtents )
+ return m_size.x;
+
const WideChar *text = m_textString.str();
WideChar ch;
@@ -286,6 +306,17 @@ Int W3DDisplayString::getWidth( Int charPos )
return width;
}
+// W3DDisplayString::setComplexTextEnabled ====================================
+/** Enable shaped complex text for this display string */
+//=============================================================================
+void W3DDisplayString::setComplexTextEnabled( Bool enabled )
+{
+ if (m_textRenderer.Set_Complex_Text_Enabled(enabled)) {
+ m_textRendererHotKey.Set_Complex_Text_Enabled(enabled);
+ notifyTextChanged();
+ }
+}
+
// W3DDisplayString::setFont ==================================================
/** Set the font for this particular display string */
//=============================================================================
@@ -363,14 +394,17 @@ void W3DDisplayString::computeExtents()
m_size.x = 0;
m_size.y = 0;
+ m_hasComplexTextExtents = FALSE;
}
else
{
- Vector2 extents = m_textRenderer.Get_Formatted_Text_Extents(getText().str()); //Get_Text_Extents( getText().str() );
+ bool hasComplexTextExtents = false;
+ Vector2 extents = m_textRenderer.Get_Formatted_Text_Extents(getText().str(), &hasComplexTextExtents);
m_size.x = extents.X;
m_size.y = extents.Y;
+ m_hasComplexTextExtents = hasComplexTextExtents;
}
@@ -388,9 +422,20 @@ void W3DDisplayString::setWordWrap( Int wordWrap )
void W3DDisplayString::setUseHotkey( Bool useHotkey, Color hotKeyColor )
{
- m_useHotKey = useHotkey;
+ UnicodeString hotkey = UnicodeString::TheEmptyString;
+ if (useHotkey && TheHotKeyManager) {
+ hotkey.translate(TheHotKeyManager->searchHotKey(getText()));
+ }
+
+ const Bool hasHotkey = !hotkey.isEmpty();
+ if (m_useHotKey == hasHotkey && m_hotKeyColor == hotKeyColor && m_hotkey == hotkey) {
+ return;
+ }
+
+ m_useHotKey = hasHotkey;
m_hotKeyColor = hotKeyColor;
- m_textRenderer.Set_Hot_Key_Parse(useHotkey);
+ m_hotkey = hotkey;
+ m_textRenderer.Set_Hot_Key_Parse(hasHotkey);
notifyTextChanged();
}
diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
index 0d49e002dab..78de1bde018 100644
--- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
+++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
@@ -79,6 +79,7 @@ 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 void setComplexTextEnabled( Bool enabled ) 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
@@ -97,6 +98,8 @@ class W3DDisplayString : public DisplayString
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_sentenceChanged; ///< when the rendered sentence needs new polygons
+ Bool m_hasComplexTextExtents; ///< cached size uses shaped complex-text metrics
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..e68d88be7cd 100644
--- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
+++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
@@ -86,6 +86,8 @@ W3DDisplayString::W3DDisplayString()
m_size.x = 0;
m_size.y = 0;
m_fontChanged = FALSE;
+ m_sentenceChanged = FALSE;
+ m_hasComplexTextExtents = FALSE;
m_clipRegion.lo.x = 0;
m_clipRegion.lo.y = 0;
m_clipRegion.hi.x = 0;
@@ -145,6 +147,38 @@ void W3DDisplayString::notifyTextChanged()
}
+// W3DDisplayString::checkForChangedTextData ==================================
+/** Rebuild the sentence and update its extents when its source data changes */
+//=============================================================================
+void W3DDisplayString::checkForChangedTextData()
+{
+ if( !m_fontChanged && !m_textChanged )
+ return;
+
+ bool usedComplexText = false;
+ Vector2 legacyExtents;
+ if(m_useHotKey)
+ {
+ m_textRenderer.Build_Sentence(
+ getText().str(), &m_hotKeyPos.x, &m_hotKeyPos.y, &usedComplexText, &legacyExtents);
+ m_textRendererHotKey.Build_Sentence(m_hotkey.str(), nullptr, nullptr);
+ }
+ else
+ m_textRenderer.Build_Sentence(
+ getText().str(), nullptr, nullptr, &usedComplexText, &legacyExtents);
+
+ // TheSuperHackers @bugfix Omar Aglan 04/09/2026 Resolve renderer fallback before callers position complex text.
+ if (m_hasComplexTextExtents && !usedComplexText) {
+ m_size.x = legacyExtents.X;
+ m_size.y = legacyExtents.Y;
+ m_hasComplexTextExtents = FALSE;
+ }
+
+ m_fontChanged = FALSE;
+ m_textChanged = FALSE;
+ m_sentenceChanged = TRUE;
+}
+
// W3DDisplayString::Draw =====================================================
/** Draw the text at the specified location in in the specified colors
* in the parameters. Since we keep an instance of the rendered text
@@ -159,35 +193,13 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor )
}
void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDrop, Int yDrop )
{
- Bool needNewPolys = FALSE;
-
// sanity
if( getTextLength() == 0 )
return; // nothing to draw
- // if our font or text has changed we need to build a new sentence
- if( m_fontChanged || m_textChanged )
- {
- if(m_useHotKey)
- {
- m_textRenderer.Set_Hot_Key_Parse(TRUE);
- m_textRenderer.Build_Sentence( getText().str(), &m_hotKeyPos.x, &m_hotKeyPos.y );
- m_hotkey.translate(TheHotKeyManager->searchHotKey(getText()));
- if(!m_hotkey.isEmpty())
- m_textRendererHotKey.Build_Sentence(m_hotkey.str(), nullptr, nullptr);
- else
- {
- m_useHotKey = FALSE;
- m_textRendererHotKey.Reset();
- }
- }
- else
- m_textRenderer.Build_Sentence( getText().str(), nullptr, nullptr );
- m_fontChanged = FALSE;
- m_textChanged = FALSE;
- needNewPolys = TRUE;
-
- }
+ checkForChangedTextData();
+ Bool needNewPolys = m_sentenceChanged;
+ m_sentenceChanged = FALSE;
//
// if our position has changed, or our colors have changed, or our
@@ -247,6 +259,8 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr
//=============================================================================
void W3DDisplayString::getSize( Int *width, Int *height )
{
+ if ( m_hasComplexTextExtents )
+ checkForChangedTextData();
// assign the width and height we have stored to parameters present
if( width )
@@ -262,6 +276,9 @@ void W3DDisplayString::getSize( Int *width, Int *height )
Int W3DDisplayString::getWidth( Int charPos )
{
+ if ( charPos == -1 && m_hasComplexTextExtents )
+ checkForChangedTextData();
+
FontCharsClass * font;
Int width = 0;
Int count = 0;
@@ -270,6 +287,9 @@ Int W3DDisplayString::getWidth( Int charPos )
if ( font )
{
+ if ( charPos == -1 && m_hasComplexTextExtents )
+ return m_size.x;
+
const WideChar *text = m_textString.str();
WideChar ch;
@@ -286,6 +306,17 @@ Int W3DDisplayString::getWidth( Int charPos )
return width;
}
+// W3DDisplayString::setComplexTextEnabled ====================================
+/** Enable shaped complex text for this display string */
+//=============================================================================
+void W3DDisplayString::setComplexTextEnabled( Bool enabled )
+{
+ if (m_textRenderer.Set_Complex_Text_Enabled(enabled)) {
+ m_textRendererHotKey.Set_Complex_Text_Enabled(enabled);
+ notifyTextChanged();
+ }
+}
+
// W3DDisplayString::setFont ==================================================
/** Set the font for this particular display string */
//=============================================================================
@@ -363,14 +394,17 @@ void W3DDisplayString::computeExtents()
m_size.x = 0;
m_size.y = 0;
+ m_hasComplexTextExtents = FALSE;
}
else
{
- Vector2 extents = m_textRenderer.Get_Formatted_Text_Extents(getText().str()); //Get_Text_Extents( getText().str() );
+ bool hasComplexTextExtents = false;
+ Vector2 extents = m_textRenderer.Get_Formatted_Text_Extents(getText().str(), &hasComplexTextExtents);
m_size.x = extents.X;
m_size.y = extents.Y;
+ m_hasComplexTextExtents = hasComplexTextExtents;
}
@@ -388,9 +422,20 @@ void W3DDisplayString::setWordWrap( Int wordWrap )
void W3DDisplayString::setUseHotkey( Bool useHotkey, Color hotKeyColor )
{
- m_useHotKey = useHotkey;
+ UnicodeString hotkey = UnicodeString::TheEmptyString;
+ if (useHotkey && TheHotKeyManager) {
+ hotkey.translate(TheHotKeyManager->searchHotKey(getText()));
+ }
+
+ const Bool hasHotkey = !hotkey.isEmpty();
+ if (m_useHotKey == hasHotkey && m_hotKeyColor == hotKeyColor && m_hotkey == hotkey) {
+ return;
+ }
+
+ m_useHotKey = hasHotkey;
m_hotKeyColor = hotKeyColor;
- m_textRenderer.Set_Hot_Key_Parse(useHotkey);
+ m_hotkey = hotkey;
+ m_textRenderer.Set_Hot_Key_Parse(hasHotkey);
notifyTextChanged();
}