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(); }