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/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
index 5fe9bf9a01a..2c1e590f06c 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),
@@ -250,6 +254,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) {
@@ -272,6 +281,11 @@ Render2DSentenceClass::Get_Text_Extents (const WCHAR *text)
Vector2
Render2DSentenceClass::Get_Formatted_Text_Extents (const WCHAR *text)
{
+ Vector2 complex_extent;
+ if (Get_Complex_Text_Extents(text, &complex_extent)) {
+ return complex_extent;
+ }
+
return Build_Sentence_Not_Centered(text, nullptr, nullptr, true);
}
@@ -564,14 +578,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 +613,139 @@ 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 complex single-line text as one run
+ // 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;
+ if (!Font->Rasterize_Complex_Text(text, &raster, &text_width, &text_height) ||
+ !Is_Complex_Text_Size_Supported(text_width, text_height))
+ {
+ delete [] raster;
+ 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 +776,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);
@@ -1147,6 +1292,10 @@ 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)) {
+ return;
+ }
+
if(Centered && (WrapWidth > 0 || wcschr(text,L'\n')))
Build_Sentence_Centered(text, hkX, hkY);
else
@@ -1269,6 +1418,108 @@ 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 DWORD Get_Complex_Text_Analysis_Flags (const WCHAR *text, int text_length)
+{
+ DWORD flags = Usp10Loader::SSA_GLYPHS | Usp10Loader::SSA_FALLBACK;
+ 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) {
+ flags |= Usp10Loader::SSA_RTL;
+ break;
+ }
+ if (character_type == C2_LEFTTORIGHT) {
+ break;
+ }
+ }
+ }
+
+ return flags;
+}
+
+
+static Usp10Loader::ScriptStringAnalysis Analyse_Complex_Text (HDC dc, const WCHAR *text, int text_length)
+{
+ Usp10Loader::ScriptStringAnalysis analysis = nullptr;
+ const int glyph_count = text_length + text_length / 2 + 16;
+ const HRESULT result = Usp10Loader::ScriptStringAnalyse(dc, text, text_length, glyph_count, -1,
+ Get_Complex_Text_Analysis_Flags(text, text_length), 0, nullptr, nullptr, nullptr, nullptr, nullptr, &analysis);
+ if (result != S_OK) {
+ if (analysis != nullptr) {
+ Usp10Loader::ScriptStringFree(&analysis);
+ }
+ return nullptr;
+ }
+
+ return analysis;
+}
+#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;
+ }
+
+ // TheSuperHackers @bugfix Omar Aglan 28/08/2026 Keep supported characters
+ // in the primary font and use Uniscribe fallback for the rest.
+ Usp10Loader::ScriptStringAnalysis analysis = Analyse_Complex_Text(MemDC, text, text_length);
+ if (analysis == nullptr) {
+ return false;
+ }
+
+ const SIZE *size = Usp10Loader::ScriptString_pSize(analysis);
+ const bool success = size != nullptr && size->cx > 0 && size->cy > 0;
+ if (success) {
+ *width = size->cx;
+ *height = size->cy;
+ }
+
+ Usp10Loader::ScriptStringFree(&analysis);
+ return success;
+#else
+ return false;
+#endif
+}
+
+
////////////////////////////////////////////////////////////////////////////////////
//
// Blit_Char
@@ -1305,6 +1556,107 @@ 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)
+{
+ 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;
+ }
+
+ HFONT old_font = (HFONT)::SelectObject(text_dc, GDIFont);
+ ::SetBkColor(text_dc, RGB(0, 0, 0));
+ ::SetTextColor(text_dc, RGB(255, 255, 255));
+
+ Usp10Loader::ScriptStringAnalysis analysis = Analyse_Complex_Text(text_dc, text, text_length);
+ const SIZE *text_size = analysis != nullptr ? Usp10Loader::ScriptString_pSize(analysis) : nullptr;
+ if (text_size == nullptr || text_size->cx <= 0 || text_size->cy <= 0) {
+ if (analysis != nullptr) {
+ Usp10Loader::ScriptStringFree(&analysis);
+ }
+ ::SelectObject(text_dc, old_font);
+ ::DeleteDC(text_dc);
+ return false;
+ }
+
+ const int text_width = text_size->cx;
+ const int text_height = text_size->cy;
+ 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) {
+ Usp10Loader::ScriptStringFree(&analysis);
+ if (bitmap != nullptr) {
+ ::DeleteObject(bitmap);
+ }
+ ::SelectObject(text_dc, old_font);
+ ::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);
+
+ RECT rect = { 0, 0, text_width, text_height };
+ bool success = Usp10Loader::ScriptStringOut(analysis, 0, 0, ETO_OPAQUE, &rect, 0, 0, FALSE) == S_OK;
+
+ 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;
+ }
+
+ Usp10Loader::ScriptStringFree(&analysis);
+ ::SelectObject(text_dc, old_font);
+ ::SelectObject(text_dc, old_bitmap);
+ ::DeleteObject(bitmap);
+ ::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..54578313b42 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
+++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
@@ -89,6 +89,9 @@ 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 Get_Extra_Overlap() {return PixelOverlap;}
@@ -183,6 +186,7 @@ class Render2DSentenceClass {
Vector2 Get_Text_Extents( const WCHAR * text );
Vector2 Get_Formatted_Text_Extents( const WCHAR * text );
+ bool Get_Complex_Text_Extents( const WCHAR *text, Vector2 *extents );
//
// Sentence control
@@ -197,6 +201,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 +245,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_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 +269,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/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..e5685993aa5
--- /dev/null
+++ b/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.cpp
@@ -0,0 +1,111 @@
+/*
+** 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 "mutex.h"
+
+
+static CriticalSectionClass Usp10LoaderCriticalSection;
+
+HMODULE Usp10Loader::Module = HMODULE(nullptr);
+bool Usp10Loader::LoadAttempted = false;
+Usp10Loader::ScriptIsComplex_t Usp10Loader::ScriptIsComplexPtr = nullptr;
+Usp10Loader::ScriptStringAnalyse_t Usp10Loader::ScriptStringAnalysePtr = nullptr;
+Usp10Loader::ScriptStringFree_t Usp10Loader::ScriptStringFreePtr = nullptr;
+Usp10Loader::ScriptStringSize_t Usp10Loader::ScriptStringSizePtr = nullptr;
+Usp10Loader::ScriptStringOut_t Usp10Loader::ScriptStringOutPtr = nullptr;
+
+
+bool Usp10Loader::load()
+{
+ CriticalSectionClass::LockClass lock(Usp10LoaderCriticalSection);
+
+ 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"));
+ ScriptStringAnalysePtr = reinterpret_cast(::GetProcAddress(Module, "ScriptStringAnalyse"));
+ ScriptStringFreePtr = reinterpret_cast(::GetProcAddress(Module, "ScriptStringFree"));
+ ScriptStringSizePtr = reinterpret_cast(::GetProcAddress(Module, "ScriptString_pSize"));
+ ScriptStringOutPtr = reinterpret_cast(::GetProcAddress(Module, "ScriptStringOut"));
+
+ if (ScriptIsComplexPtr == nullptr || ScriptStringAnalysePtr == nullptr || ScriptStringFreePtr == nullptr ||
+ ScriptStringSizePtr == nullptr || ScriptStringOutPtr == nullptr)
+ {
+ ::FreeLibrary(Module);
+ Module = HMODULE(nullptr);
+ return false;
+ }
+
+ return true;
+}
+
+
+bool Usp10Loader::isLoaded()
+{
+ return load();
+}
+
+
+HRESULT WINAPI Usp10Loader::ScriptIsComplex(const WCHAR *text, int text_length, DWORD flags)
+{
+ return load() ? ScriptIsComplexPtr(text, text_length, flags) : E_FAIL;
+}
+
+
+HRESULT WINAPI 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)
+{
+ return load() ? ScriptStringAnalysePtr(dc, text, text_length, glyph_count, charset, flags, required_width,
+ control, state, spacing, tabs, character_classes, analysis) : E_FAIL;
+}
+
+
+HRESULT WINAPI Usp10Loader::ScriptStringFree(ScriptStringAnalysis *analysis)
+{
+ return load() ? ScriptStringFreePtr(analysis) : E_FAIL;
+}
+
+
+const SIZE *WINAPI Usp10Loader::ScriptString_pSize(ScriptStringAnalysis analysis)
+{
+ return load() ? ScriptStringSizePtr(analysis) : nullptr;
+}
+
+
+HRESULT WINAPI Usp10Loader::ScriptStringOut(ScriptStringAnalysis analysis, int x, int y, UINT options,
+ const RECT *rect, int minimum_selection, int maximum_selection, BOOL disabled)
+{
+ 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..f6dfb8e4fea
--- /dev/null
+++ b/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h
@@ -0,0 +1,71 @@
+/*
+** 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 "win.h"
+
+
+class Usp10Loader
+{
+public:
+
+ typedef void *ScriptStringAnalysis;
+ struct ScriptControl;
+ struct ScriptState;
+ struct ScriptTabDefinition;
+
+ enum
+ {
+ SIC_COMPLEX = 0x00000001,
+ SSA_FALLBACK = 0x00000020,
+ SSA_GLYPHS = 0x00000080,
+ SSA_RTL = 0x00000100,
+ };
+
+ static bool isLoaded();
+
+ static HRESULT WINAPI ScriptIsComplex(const WCHAR *text, int text_length, DWORD flags);
+ static HRESULT WINAPI 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 WINAPI ScriptStringFree(ScriptStringAnalysis *analysis);
+ static const SIZE *WINAPI ScriptString_pSize(ScriptStringAnalysis analysis);
+ static HRESULT WINAPI ScriptStringOut(ScriptStringAnalysis analysis, int x, int y, UINT options,
+ const RECT *rect, int minimum_selection, int maximum_selection, BOOL disabled);
+
+private:
+
+ static bool load();
+
+ typedef HRESULT (WINAPI *ScriptIsComplex_t)(const WCHAR *, int, DWORD);
+ 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 *ScriptStringSize_t)(ScriptStringAnalysis);
+ typedef HRESULT (WINAPI *ScriptStringOut_t)(ScriptStringAnalysis, int, int, UINT, const RECT *, int, int, BOOL);
+
+ static HMODULE Module;
+ static bool LoadAttempted;
+ static ScriptIsComplex_t ScriptIsComplexPtr;
+ static ScriptStringAnalyse_t ScriptStringAnalysePtr;
+ static ScriptStringFree_t ScriptStringFreePtr;
+ static ScriptStringSize_t ScriptStringSizePtr;
+ static ScriptStringOut_t ScriptStringOutPtr;
+};
diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp
index d3d2afadcdc..7e081127147 100644
--- a/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp
+++ b/Generals/Code/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/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
index 80123dd0919..d134dc18f62 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
diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
index eafcf5fe6d6..3fedba4b11c 100644
--- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
+++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
@@ -270,6 +270,10 @@ Int W3DDisplayString::getWidth( Int charPos )
if ( font )
{
+ Vector2 complexExtents;
+ if ( charPos == -1 && m_textRenderer.Get_Complex_Text_Extents( m_textString.str(), &complexExtents ) )
+ return (Int)complexExtents.X;
+
const WideChar *text = m_textString.str();
WideChar ch;
@@ -286,6 +290,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 */
//=============================================================================
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp
index 773c22dd94e..a1255937235 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp
+++ b/GeneralsMD/Code/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/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h
index 0d49e002dab..c86112d3f39 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
diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
index c8a4b78ea59..edd27cedd65 100644
--- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
+++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp
@@ -270,6 +270,10 @@ Int W3DDisplayString::getWidth( Int charPos )
if ( font )
{
+ Vector2 complexExtents;
+ if ( charPos == -1 && m_textRenderer.Get_Complex_Text_Extents( m_textString.str(), &complexExtents ) )
+ return (Int)complexExtents.X;
+
const WideChar *text = m_textString.str();
WideChar ch;
@@ -286,6 +290,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 */
//=============================================================================