From 68a4beb2f090d03cef043b22e7e6ca7ef1165c12 Mon Sep 17 00:00:00 2001 From: Omar Aglan Date: Mon, 31 Aug 2026 22:26:32 +0300 Subject: [PATCH 1/2] feat(system): Load Uniscribe at runtime --- .../Source/WWVegas/WWLib/CMakeLists.txt | 2 + .../Source/WWVegas/WWLib/Usp10Loader.cpp | 111 ++++++++++++++++++ .../Source/WWVegas/WWLib/Usp10Loader.h | 71 +++++++++++ 3 files changed, 184 insertions(+) create mode 100644 Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.cpp create mode 100644 Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h 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; +}; From 74c1e275f6e4bbca72f6644ce9efd39ef0ff0c09 Mon Sep 17 00:00:00 2001 From: Omar Aglan Date: Fri, 28 Aug 2026 22:08:17 +0300 Subject: [PATCH 2/2] feat(text): Shape complex single-line UI text --- .../Include/GameClient/DisplayString.h | 1 + .../Source/WWVegas/WW3D2/render2dsentence.cpp | 358 +++++++++++++++++- .../Source/WWVegas/WW3D2/render2dsentence.h | 20 +- .../GameClient/GUI/GameWindowManager.cpp | 5 + .../W3DDevice/GameClient/W3DDisplayString.h | 1 + .../W3DDevice/GameClient/W3DDisplayString.cpp | 15 + .../GameClient/GUI/GameWindowManager.cpp | 5 + .../W3DDevice/GameClient/W3DDisplayString.h | 1 + .../W3DDevice/GameClient/W3DDisplayString.cpp | 15 + 9 files changed, 416 insertions(+), 5 deletions(-) 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/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 */ //=============================================================================