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 7c48c2fb257..0f60541eb64 100644 --- a/Core/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp +++ b/Core/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp @@ -2725,6 +2725,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..4e3299bd48f 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 //////////////////////////////////////////////////////////////////////////////////// @@ -48,6 +51,13 @@ #define no_TEST_PLACEMENT 1 // Shows alignment markers for text. #define TEXTURE_OFFSET 2 + +static inline uint16 Convert_Font_Pixel (uint8 intensity) +{ + const uint16 color = intensity == 0 ? 0 : 0x0FFF; + return color | ((intensity >> 4) << 12); +} + //////////////////////////////////////////////////////////////////////////////////// // // Render2DSentenceClass @@ -62,6 +72,7 @@ Render2DSentenceClass::Render2DSentenceClass () : CurSurface (nullptr), CurrTextureSize (0), MonoSpaced (false), + ComplexTextEnabled (true), IsClippedEnabled (false), ClipRect (0, 0, 0, 0), BaseLocation (0, 0), @@ -148,7 +159,6 @@ Render2DSentenceClass::Reset () Cursor.Set (0, 0); MonoSpaced = false; - ParseHotKey = false; Release_Pending_Surfaces (); Reset_Sentence_Data (); @@ -250,6 +260,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 +285,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 +591,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 +626,124 @@ 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); +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// 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->Build_Complex_Text(text, &width, &height, WrapWidth, max(TextureSizeHint, 256))) + { + 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->Build_Complex_Text(text, &text_width, &text_height, + WrapWidth, max(TextureSizeHint, 256), &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 +774,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 +847,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 +1087,8 @@ void Render2DSentenceClass::Build_Sentence_Centered (const WCHAR *text, int *hkX *hkX = hotKeyPosX; if(hkX) *hkY = hotKeyPosY; + + return extent; } //////////////////////////////////////////////////////////////////////////////////// // @@ -1140,6 +1285,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 +1312,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 +1446,215 @@ 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 (HDC source_dc) : + DC(::CreateCompatibleDC(source_dc)), + Runs(nullptr), VisualToLogical(nullptr), RunCount(0), Width(0), Height(0), Ascent(0), Descent(0) + { + } + + ~ComplexTextLayout () + { + if (Runs != nullptr) { + for (int index = 0; index < RunCount; ++index) { + if (Runs[index].Analysis != nullptr) { + Usp10Loader::ScriptStringFree(&Runs[index].Analysis); + } + } + } + + delete [] Runs; + delete [] VisualToLogical; + if (DC != nullptr) { + ::DeleteDC(DC); + } + } + + HDC DC; + ComplexTextRun *Runs; + int *VisualToLogical; + int RunCount; + int Width; + int Height; + int Ascent; + int Descent; + +private: + ComplexTextLayout (const ComplexTextLayout &); + ComplexTextLayout &operator= (const ComplexTextLayout &); +}; + + +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 + 2]; + 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 + + //////////////////////////////////////////////////////////////////////////////////// // // Blit_Char @@ -1305,6 +1691,125 @@ FontCharsClass::Blit_Char (WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, i } +//////////////////////////////////////////////////////////////////////////////////// +// +// Build_Complex_Text +// +//////////////////////////////////////////////////////////////////////////////////// +bool +FontCharsClass::Build_Complex_Text (const WCHAR *text, int *width, int *height, + float maximum_width, int maximum_height, uint16 **raster) +{ + if (width == nullptr || height == nullptr) { + return false; + } + + if (raster != nullptr) { + *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; + } + + ComplexTextLayout layout(MemDC); + HDC text_dc = layout.DC; + if (text_dc == nullptr) { + return false; + } + + ::SetBkColor(text_dc, RGB(0, 0, 0)); + ::SetTextColor(text_dc, RGB(255, 255, 255)); + ::SetBkMode(text_dc, TRANSPARENT); + + // TheSuperHackers @bugfix Omar Aglan 02/09/2026 Preserve the primary font for + // Latin runs and use the configured Unicode font for the remaining runs. + HFONT alternate_font = AlternateUnicodeFont != nullptr && AlternateUnicodeFont != this ? + AlternateUnicodeFont->GDIFont : nullptr; + if (!Build_Complex_Text_Layout(text_dc, text, text_length, GDIFont, alternate_font, &layout)) { + 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)) + { + return false; + } + + if (raster == nullptr) { + *width = text_width; + *height = text_height; + return true; + } + + 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); + } + 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) { + destination[column] = Convert_Font_Pixel(source[column * 3]); + } + } + *raster = pixels; + *width = text_width; + *height = text_height; + } + + ::SelectObject(text_dc, old_bitmap); + ::DeleteObject(bitmap); + return success; +#else + return false; +#endif +} + + //////////////////////////////////////////////////////////////////////////////////// // // Store_GDI_Char @@ -1390,17 +1895,11 @@ FontCharsClass::Store_GDI_Char (WCHAR ch) } #endif - uint16 pixel_color = 0; - if (pixel_value != 0) { - pixel_color = 0x0FFF; - } - // // Convert the pixel intensity from 8bit to 4bit and // store it in our buffer // - uint8 alpha_value = ((pixel_value >> 4) & 0xF); - *curr_buffer_p++ = pixel_color | (alpha_value << 12); + *curr_buffer_p++ = Convert_Font_Pixel(pixel_value); } } @@ -1561,7 +2060,6 @@ FontCharsClass::Create_GDI_Font (const char *font_name) TEXTMETRIC text_metric = { 0 }; ::GetTextMetrics (MemDC, &text_metric); CharHeight = text_metric.tmHeight; - CharAscent = text_metric.tmAscent; CharOverhang = text_metric.tmOverhang; if (doingGenerals) { CharOverhang = 0; diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h index 15426c1e950..dba4ecc9583 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 ); + // A null raster requests measurement only; both paths apply the same size limits. + bool Build_Complex_Text( const WCHAR *text, int *width, int *height, + float maximum_width, int maximum_height, uint16 **raster = nullptr ); int Get_Extra_Overlap() {return PixelOverlap;} @@ -115,7 +119,6 @@ class FontCharsClass : public RefCountClass DynamicVectorClass BufferList; int CurrPixelOffset; int CharHeight; - int CharAscent; int CharOverhang; int PixelOverlap; int PointSize; @@ -182,12 +185,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 +203,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 +247,13 @@ 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 Build_Complex_Sentence (const WCHAR *text); // // Private member data // @@ -254,6 +270,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/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..f159d66b86e 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp @@ -52,7 +52,6 @@ #include "GameClient/Display.h" #include "GameClient/GameClient.h" #include "W3DDevice/GameClient/W3DDisplayString.h" -#include "GameClient/HotKey.h" #include "GameClient/GameFont.h" #include "GameClient/GlobalLanguage.h" @@ -86,6 +85,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; @@ -130,6 +131,18 @@ void W3DDisplayString::notifyTextChanged() } } + // TheSuperHackers @bugfix Omar Aglan 09/09/2026 Refresh the accelerator before measuring changed text. + m_hotkey.clear(); + if (m_useHotKey) { + for (const WideChar *marker = getText().str(); *marker; ++marker) { + if (*marker == L'&' && marker[1] > L' ') { + m_hotkey.concat(marker[1]); + break; + } + } + } + m_textRenderer.Set_Hot_Key_Parse(!m_hotkey.isEmpty()); + // get our new text extents computeExtents(); @@ -145,6 +158,41 @@ 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_hotkey.isEmpty()) + { + 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; + // TheSuperHackers @bugfix Omar Aglan 06/09/2026 Track resources created by size queries before the first draw. + if( TheGameClient ) + usingResources( TheGameClient->getFrame() ); +} + // 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 +207,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 @@ -217,7 +243,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr m_textRenderer.Set_Location( Vector2( m_textPos.x, m_textPos.y ) ); m_textRenderer.Draw_Sentence( m_currTextColor ); - if (m_useHotKey) + if (!m_hotkey.isEmpty()) { m_textRendererHotKey.Reset_Polys(); m_textRendererHotKey.Set_Location( Vector2( m_textPos.x + m_hotKeyPos.x , m_textPos.y +m_hotKeyPos.y) ); @@ -228,7 +254,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr TheDisplay->flush(); - if (m_useHotKey) + if (!m_hotkey.isEmpty()) { m_textRendererHotKey.Render(); } @@ -247,6 +273,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 +290,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 +301,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 +320,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 +408,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 +436,12 @@ void W3DDisplayString::setWordWrap( Int wordWrap ) void W3DDisplayString::setUseHotkey( Bool useHotkey, Color hotKeyColor ) { + if (m_useHotKey == useHotkey && m_hotKeyColor == hotKeyColor) { + return; + } + m_useHotKey = useHotkey; m_hotKeyColor = hotKeyColor; - m_textRenderer.Set_Hot_Key_Parse(useHotkey); 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..c30371125d0 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp @@ -52,7 +52,6 @@ #include "GameClient/Display.h" #include "GameClient/GameClient.h" #include "W3DDevice/GameClient/W3DDisplayString.h" -#include "GameClient/HotKey.h" #include "GameClient/GameFont.h" #include "GameClient/GlobalLanguage.h" @@ -86,6 +85,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; @@ -130,6 +131,18 @@ void W3DDisplayString::notifyTextChanged() } } + // TheSuperHackers @bugfix Omar Aglan 09/09/2026 Refresh the accelerator before measuring changed text. + m_hotkey.clear(); + if (m_useHotKey) { + for (const WideChar *marker = getText().str(); *marker; ++marker) { + if (*marker == L'&' && marker[1] > L' ') { + m_hotkey.concat(marker[1]); + break; + } + } + } + m_textRenderer.Set_Hot_Key_Parse(!m_hotkey.isEmpty()); + // get our new text extents computeExtents(); @@ -145,6 +158,41 @@ 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_hotkey.isEmpty()) + { + 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; + // TheSuperHackers @bugfix Omar Aglan 06/09/2026 Track resources created by size queries before the first draw. + if( TheGameClient ) + usingResources( TheGameClient->getFrame() ); +} + // 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 +207,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 @@ -217,7 +243,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr m_textRenderer.Set_Location( Vector2( m_textPos.x, m_textPos.y ) ); m_textRenderer.Draw_Sentence( m_currTextColor ); - if (m_useHotKey) + if (!m_hotkey.isEmpty()) { m_textRendererHotKey.Reset_Polys(); m_textRendererHotKey.Set_Location( Vector2( m_textPos.x + m_hotKeyPos.x , m_textPos.y +m_hotKeyPos.y) ); @@ -228,7 +254,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr TheDisplay->flush(); - if (m_useHotKey) + if (!m_hotkey.isEmpty()) { m_textRendererHotKey.Render(); } @@ -247,6 +273,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 +290,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 +301,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 +320,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 +408,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 +436,12 @@ void W3DDisplayString::setWordWrap( Int wordWrap ) void W3DDisplayString::setUseHotkey( Bool useHotkey, Color hotKeyColor ) { + if (m_useHotKey == useHotkey && m_hotKeyColor == hotKeyColor) { + return; + } + m_useHotKey = useHotkey; m_hotKeyColor = hotKeyColor; - m_textRenderer.Set_Hot_Key_Parse(useHotkey); notifyTextChanged(); }