From a1e21594beb0533c31d27875cd904ad234e2f0d5 Mon Sep 17 00:00:00 2001 From: Omar Aglan Date: Sun, 13 Sep 2026 12:55:52 +0300 Subject: [PATCH 1/2] chore(lib): Define Uniscribe itemization control --- Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h b/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h index 3b35b03b82b..2ac7646b75d 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h +++ b/Core/Libraries/Source/WWVegas/WWLib/Usp10Loader.h @@ -29,9 +29,16 @@ class Usp10Loader public: typedef void *ScriptStringAnalysis; - struct ScriptControl; struct ScriptTabDefinition; + // TheSuperHackers @info Omar Aglan 13/09/2026 ScriptItemize needs a non-null control + // structure for bidirectional analysis. Keep the unused control flags zero. + struct ScriptControl + { + DWORD default_language : 16; + DWORD reserved : 16; + }; + struct ScriptState { WORD bidi_level : 5; From c30ed4ae60d1dd9713518e8e3789e4b947c15f1d Mon Sep 17 00:00:00 2001 From: Omar Aglan Date: Fri, 28 Aug 2026 22:08:17 +0300 Subject: [PATCH 2/2] feat(text): Shape complex single-line UI text --- .../Include/GameClient/DisplayString.h | 1 + .../GameClient/GUI/GameWindowManager.cpp | 5 + .../Source/WWVegas/WW3D2/CMakeLists.txt | 3 + .../Source/WWVegas/WW3D2/complextext.h | 228 ++++++++++++ .../Source/WWVegas/WW3D2/render2dsentence.cpp | 347 ++++++++++++++++-- .../Source/WWVegas/WW3D2/render2dsentence.h | 27 +- .../WWVegas/WW3D2/supplementarybidi.inl | 265 +++++++++++++ .../Source/WWVegas/WW3D2/unicode-license.txt | 39 ++ .../Source/WWVegas/WW3D2/unicodebidi.h | 84 +++++ Core/Libraries/Source/WWVegas/WW3D2/ww3d.cpp | 6 + .../W3DDevice/GameClient/W3DDisplayString.h | 9 +- .../W3DDevice/GameClient/W3DDisplayString.cpp | 112 ++++-- .../GameClient/W3DDisplayStringManager.cpp | 3 +- .../W3DDevice/GameClient/W3DDisplayString.h | 9 +- .../W3DDevice/GameClient/W3DDisplayString.cpp | 112 ++++-- .../GameClient/W3DDisplayStringManager.cpp | 3 +- scripts/generate_supplementary_bidi.py | 54 +++ 17 files changed, 1201 insertions(+), 106 deletions(-) create mode 100644 Core/Libraries/Source/WWVegas/WW3D2/complextext.h create mode 100644 Core/Libraries/Source/WWVegas/WW3D2/supplementarybidi.inl create mode 100644 Core/Libraries/Source/WWVegas/WW3D2/unicode-license.txt create mode 100644 Core/Libraries/Source/WWVegas/WW3D2/unicodebidi.h create mode 100644 scripts/generate_supplementary_bidi.py 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/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt index 68f59638745..6e8d52b0fb5 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt +++ b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt @@ -163,6 +163,9 @@ set(WW3D2_SRC #render2d.h render2dsentence.cpp render2dsentence.h + complextext.h + unicodebidi.h + supplementarybidi.inl renderobjectrecycler.cpp renderobjectrecycler.h rendobj.cpp diff --git a/Core/Libraries/Source/WWVegas/WW3D2/complextext.h b/Core/Libraries/Source/WWVegas/WW3D2/complextext.h new file mode 100644 index 00000000000..0cb3583eef5 --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WW3D2/complextext.h @@ -0,0 +1,228 @@ +/* +** 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 "WWLib/Usp10Loader.h" +#include "unicodebidi.h" +#include +#include +#include +#include + +struct ComplexTextRun +{ + ComplexTextRun () : + Analysis(nullptr), + Font(nullptr), + CharacterPosition(0), + CharacterCount(0), + Width(0), + Ascent(0) + { + ::memset(&State, 0, sizeof(State)); + } + + Usp10Loader::ScriptStringAnalysis Analysis; + HFONT Font; + int CharacterPosition; + int CharacterCount; + int Width; + int Ascent; + Usp10Loader::ScriptState State; +}; + + +struct ComplexTextLayout +{ + ComplexTextLayout (HDC source_dc) : + DC(::CreateCompatibleDC(source_dc)), + Width(0), Height(0), Ascent(0), Descent(0) + { + } + + ~ComplexTextLayout () + { + for (size_t index = 0; index < Runs.size(); ++index) { + if (Runs[index].Analysis != nullptr) { + Usp10Loader::ScriptStringFree(&Runs[index].Analysis); + } + } + + if (DC != nullptr) { + ::DeleteDC(DC); + } + } + + HDC DC; + std::vector Runs; + std::vector VisualToLogical; + 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) +{ + if (dc == nullptr || text == nullptr || text_length <= 0 || + text_length > (INT_MAX - 16) / 3 || primary_font == nullptr) + { + return false; + } + + if (alternate_font == primary_font) { + alternate_font = nullptr; + } + + // TheSuperHackers @bugfix Omar Aglan 13/09/2026 Preserve paragraph order for mixed RTL text. + // ScriptItemize needs both control and state for full bidirectional analysis. + Usp10Loader::ScriptControl control = { 0 }; + Usp10Loader::ScriptState initial_state = { 0 }; + initial_state.bidi_level = UnicodeBidi::Get_Paragraph_Level(text, text_length); + + std::vector items(text_length + 2); + int item_count = 0; + if (Usp10Loader::ScriptItemize(text, text_length, text_length + 1, &control, &initial_state, &items[0], &item_count) != S_OK || + item_count <= 0) + { + return false; + } + + std::vector attributes(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) + { + return false; + } + } + + layout->Runs.reserve(text_length); + 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) { + layout->Runs.push_back(ComplexTextRun()); + ComplexTextRun &run = layout->Runs.back(); + run.Font = uses_alternate_font ? alternate_font : primary_font; + run.CharacterPosition = run_start; + run.CharacterCount = run_end - run_start; + run.State = items[run_item_index].analysis.state; + run_start = run_end; + if (run_end < item_end) { + uses_alternate_font = Uses_Alternate_Unicode_Font(text, run_end, alternate_font); + } + } + } + } + const int run_count = (int)layout->Runs.size(); + layout->VisualToLogical.resize(run_count); + + std::vector bidi_levels(run_count); + HFONT old_font = (HFONT)::GetCurrentObject(dc, OBJ_FONT); + bool success = true; + for (int index = 0; index < run_count; ++index) { + ComplexTextRun &run = layout->Runs[index]; + if (::SelectObject(dc, run.Font) == nullptr) { + success = false; + break; + } + + bidi_levels[index] = (BYTE)run.State.bidi_level; + const int glyph_count = run.CharacterCount + run.CharacterCount / 2 + 16; + DWORD flags = Usp10Loader::SSA_GLYPHS | Usp10Loader::SSA_FALLBACK; + if ((run.State.bidi_level & 1) != 0) { + flags |= Usp10Loader::SSA_RTL; + } + + // TheSuperHackers @bugfix Omar Aglan 13/09/2026 Keep directional overrides inside font runs. + const HRESULT analysis_result = Usp10Loader::ScriptStringAnalyse(dc, + text + run.CharacterPosition, run.CharacterCount, glyph_count, -1, flags, 0, + &control, &run.State, 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->cx > INT_MAX - layout->Width || + run_size->cy <= 0 || !::GetTextMetrics(dc, &text_metrics)) + { + success = false; + break; + } + + run.Width = run_size->cx; + run.Ascent = (std::min)((int)text_metrics.tmAscent, (int)run_size->cy); + layout->Width += run.Width; + layout->Ascent = (std::max)(layout->Ascent, run.Ascent); + layout->Descent = (std::max)(layout->Descent, (int)run_size->cy - run.Ascent); + } + if (layout->Ascent > INT_MAX - layout->Descent) { + success = false; + } else { + layout->Height = layout->Ascent + layout->Descent; + } + + if (success) { + success = Usp10Loader::ScriptLayout(run_count, &bidi_levels[0], &layout->VisualToLogical[0], nullptr) == S_OK && + layout->Width > 0 && layout->Height > 0; + } + + ::SelectObject(dc, old_font); + return success; +} + + +static bool Draw_Complex_Text_Layout (ComplexTextLayout &layout) +{ + HFONT old_font = (HFONT)::GetCurrentObject(layout.DC, OBJ_FONT); + int x = 0; + bool success = true; + for (size_t visual_index = 0; visual_index < layout.Runs.size(); ++visual_index) { + ComplexTextRun &run = layout.Runs[layout.VisualToLogical[visual_index]]; + if (::SelectObject(layout.DC, run.Font) == nullptr || + Usp10Loader::ScriptStringOut(run.Analysis, x, layout.Ascent - run.Ascent, + 0, nullptr, 0, 0, FALSE) != S_OK) + { + success = false; + break; + } + x += run.Width; + } + ::SelectObject(layout.DC, old_font); + + // TheSuperHackers @bugfix Omar Aglan 13/09/2026 Complete GDI drawing before the bitmap is read or freed. + return ::GdiFlush() != FALSE && success; +} diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp index 5fe9bf9a01a..397a43ce723 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 "complextext.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 @@ -599,23 +628,126 @@ Render2DSentenceClass::Record_Sentence_Chunk () //////////////////////////////////////////////////////////////////////////////////// // -// Allocate_New_Surface +// Is_Single_Line_Complex_Text // //////////////////////////////////////////////////////////////////////////////////// -void -Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExtents) +bool +Render2DSentenceClass::Is_Single_Line_Complex_Text (const WCHAR *text) const { - if (!justCalcExtents) + // 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 || + wcspbrk(text, L"\r\n\v\f\x0085\x2028\x2029") != nullptr || + ParseHotKey || MonoSpaced) { - // - // Unlock the last surface (if necessary) - // - if (LockedPtr != nullptr) { - CurSurface->Unlock (); - LockedPtr = nullptr; + 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); + + // TheSuperHackers @performance Omar Aglan 13/09/2026 Reuse the shaped dimensions + // when allocating sentence textures to avoid measuring legacy glyphs. + if (CurSurface == nullptr) { + Allocate_New_Surface (text_width, text_height); + } + + int source_x = 0; + + while (source_x < text_width) { + if ((TextureOffset.J + text_height) >= CurrTextureSize) { + Allocate_New_Surface (text_width - source_x, 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) +{ // // Calculate the width of the text // @@ -624,7 +756,17 @@ Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExt text_width += Font->Get_Char_Spacing (text[index]); } - int char_height = Font->Get_Char_Height (); + Allocate_New_Surface (text_width, Font->Get_Char_Height (), justCalcExtents); +} + + +void +Render2DSentenceClass::Allocate_New_Surface (int text_width, int char_height, bool justCalcExtents) +{ + if (!justCalcExtents && LockedPtr != nullptr) { + CurSurface->Unlock (); + LockedPtr = nullptr; + } // // Find the best texture size for the remaining text @@ -704,7 +846,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 +1086,8 @@ void Render2DSentenceClass::Build_Sentence_Centered (const WCHAR *text, int *hkX *hkX = hotKeyPosX; if(hkX) *hkY = hotKeyPosY; + + return extent; } //////////////////////////////////////////////////////////////////////////////////// // @@ -1138,8 +1282,16 @@ Vector2 Render2DSentenceClass::Build_Sentence_Not_Centered (const WCHAR *text, i // //////////////////////////////////////////////////////////////////////////////////// void -Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY) +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 +1299,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 +1433,26 @@ 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 +} + + //////////////////////////////////////////////////////////////////////////////////// // // Blit_Char @@ -1305,6 +1489,122 @@ 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 size_t text_length = text == nullptr ? 0 : wcslen(text); + if (text_length == 0 || text_length > (INT_MAX - 16) / 3 || 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, (int)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; + } + + if (text_width > (INT_MAX - 3) / 3) { + return false; + } + const int bitmap_stride = ((text_width * 3) + 3) & ~3; + if (text_height > INT_MAX / bitmap_stride) { + 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); + } + return false; + } + + HBITMAP old_bitmap = (HBITMAP)::SelectObject(text_dc, bitmap); + if (old_bitmap == nullptr) { + ::DeleteObject(bitmap); + return false; + } + + ::memset(bitmap_bits, 0, bitmap_stride * text_height); + + const bool success = Draw_Complex_Text_Layout(layout); + + 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 +1690,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 +1855,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..01167cfba50 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,14 @@ 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 = nullptr, + Vector2 *legacy_extents = nullptr); void Draw_Sentence (uint32 color = 0xFFFFFFFF); // @@ -197,6 +202,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 +246,14 @@ class Render2DSentenceClass { // void Reset_Sentence_Data (); void Build_Textures (); - void Record_Sentence_Chunk (); + void Record_Sentence_Chunk (int char_height = 0); void Allocate_New_Surface (const WCHAR *text, bool justCalcExtents = false); + void Allocate_New_Surface (int text_width, int char_height, bool justCalcExtents = false); 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/supplementarybidi.inl b/Core/Libraries/Source/WWVegas/WW3D2/supplementarybidi.inl new file mode 100644 index 00000000000..317207d8226 --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WW3D2/supplementarybidi.inl @@ -0,0 +1,265 @@ +// Generated by scripts/generate_supplementary_bidi.py; do not edit. +// Unicode 17.0.0 DerivedBidiClass.txt, including its @missing defaults. +// Copyright 2025 Unicode, Inc. See unicode-license.txt. +// Omitted ranges have direction LeftToRight. Neutral means neither L, R nor AL. + { 0x10101, 0x10101, Neutral }, + { 0x10140, 0x1018C, Neutral }, + { 0x10190, 0x1019C, Neutral }, + { 0x101A0, 0x101A0, Neutral }, + { 0x101FD, 0x101FD, Neutral }, + { 0x102E0, 0x102FB, Neutral }, + { 0x10376, 0x1037A, Neutral }, + { 0x10800, 0x1091E, RightToLeft }, + { 0x1091F, 0x1091F, Neutral }, + { 0x10920, 0x10A00, RightToLeft }, + { 0x10A01, 0x10A03, Neutral }, + { 0x10A04, 0x10A04, RightToLeft }, + { 0x10A05, 0x10A06, Neutral }, + { 0x10A07, 0x10A0B, RightToLeft }, + { 0x10A0C, 0x10A0F, Neutral }, + { 0x10A10, 0x10A37, RightToLeft }, + { 0x10A38, 0x10A3A, Neutral }, + { 0x10A3B, 0x10A3E, RightToLeft }, + { 0x10A3F, 0x10A3F, Neutral }, + { 0x10A40, 0x10AE4, RightToLeft }, + { 0x10AE5, 0x10AE6, Neutral }, + { 0x10AE7, 0x10B38, RightToLeft }, + { 0x10B39, 0x10B3F, Neutral }, + { 0x10B40, 0x10D23, RightToLeft }, + { 0x10D24, 0x10D27, Neutral }, + { 0x10D28, 0x10D2F, RightToLeft }, + { 0x10D30, 0x10D39, Neutral }, + { 0x10D3A, 0x10D3F, RightToLeft }, + { 0x10D40, 0x10D49, Neutral }, + { 0x10D4A, 0x10D68, RightToLeft }, + { 0x10D69, 0x10D6E, Neutral }, + { 0x10D6F, 0x10E5F, RightToLeft }, + { 0x10E60, 0x10E7E, Neutral }, + { 0x10E7F, 0x10EAA, RightToLeft }, + { 0x10EAB, 0x10EAC, Neutral }, + { 0x10EAD, 0x10ECF, RightToLeft }, + { 0x10ED0, 0x10ED8, Neutral }, + { 0x10ED9, 0x10EF9, RightToLeft }, + { 0x10EFA, 0x10EFF, Neutral }, + { 0x10F00, 0x10F45, RightToLeft }, + { 0x10F46, 0x10F50, Neutral }, + { 0x10F51, 0x10F81, RightToLeft }, + { 0x10F82, 0x10F85, Neutral }, + { 0x10F86, 0x10FFF, RightToLeft }, + { 0x11001, 0x11001, Neutral }, + { 0x11038, 0x11046, Neutral }, + { 0x11052, 0x11065, Neutral }, + { 0x11070, 0x11070, Neutral }, + { 0x11073, 0x11074, Neutral }, + { 0x1107F, 0x11081, Neutral }, + { 0x110B3, 0x110B6, Neutral }, + { 0x110B9, 0x110BA, Neutral }, + { 0x110C2, 0x110C2, Neutral }, + { 0x11100, 0x11102, Neutral }, + { 0x11127, 0x1112B, Neutral }, + { 0x1112D, 0x11134, Neutral }, + { 0x11173, 0x11173, Neutral }, + { 0x11180, 0x11181, Neutral }, + { 0x111B6, 0x111BE, Neutral }, + { 0x111C9, 0x111CC, Neutral }, + { 0x111CF, 0x111CF, Neutral }, + { 0x1122F, 0x11231, Neutral }, + { 0x11234, 0x11234, Neutral }, + { 0x11236, 0x11237, Neutral }, + { 0x1123E, 0x1123E, Neutral }, + { 0x11241, 0x11241, Neutral }, + { 0x112DF, 0x112DF, Neutral }, + { 0x112E3, 0x112EA, Neutral }, + { 0x11300, 0x11301, Neutral }, + { 0x1133B, 0x1133C, Neutral }, + { 0x11340, 0x11340, Neutral }, + { 0x11366, 0x1136C, Neutral }, + { 0x11370, 0x11374, Neutral }, + { 0x113BB, 0x113C0, Neutral }, + { 0x113CE, 0x113CE, Neutral }, + { 0x113D0, 0x113D0, Neutral }, + { 0x113D2, 0x113D2, Neutral }, + { 0x113E1, 0x113E2, Neutral }, + { 0x11438, 0x1143F, Neutral }, + { 0x11442, 0x11444, Neutral }, + { 0x11446, 0x11446, Neutral }, + { 0x1145E, 0x1145E, Neutral }, + { 0x114B3, 0x114B8, Neutral }, + { 0x114BA, 0x114BA, Neutral }, + { 0x114BF, 0x114C0, Neutral }, + { 0x114C2, 0x114C3, Neutral }, + { 0x115B2, 0x115B5, Neutral }, + { 0x115BC, 0x115BD, Neutral }, + { 0x115BF, 0x115C0, Neutral }, + { 0x115DC, 0x115DD, Neutral }, + { 0x11633, 0x1163A, Neutral }, + { 0x1163D, 0x1163D, Neutral }, + { 0x1163F, 0x11640, Neutral }, + { 0x11660, 0x1166C, Neutral }, + { 0x116AB, 0x116AB, Neutral }, + { 0x116AD, 0x116AD, Neutral }, + { 0x116B0, 0x116B5, Neutral }, + { 0x116B7, 0x116B7, Neutral }, + { 0x1171D, 0x1171D, Neutral }, + { 0x1171F, 0x1171F, Neutral }, + { 0x11722, 0x11725, Neutral }, + { 0x11727, 0x1172B, Neutral }, + { 0x1182F, 0x11837, Neutral }, + { 0x11839, 0x1183A, Neutral }, + { 0x1193B, 0x1193C, Neutral }, + { 0x1193E, 0x1193E, Neutral }, + { 0x11943, 0x11943, Neutral }, + { 0x119D4, 0x119D7, Neutral }, + { 0x119DA, 0x119DB, Neutral }, + { 0x119E0, 0x119E0, Neutral }, + { 0x11A01, 0x11A06, Neutral }, + { 0x11A09, 0x11A0A, Neutral }, + { 0x11A33, 0x11A38, Neutral }, + { 0x11A3B, 0x11A3E, Neutral }, + { 0x11A47, 0x11A47, Neutral }, + { 0x11A51, 0x11A56, Neutral }, + { 0x11A59, 0x11A5B, Neutral }, + { 0x11A8A, 0x11A96, Neutral }, + { 0x11A98, 0x11A99, Neutral }, + { 0x11B60, 0x11B60, Neutral }, + { 0x11B62, 0x11B64, Neutral }, + { 0x11B66, 0x11B66, Neutral }, + { 0x11C30, 0x11C36, Neutral }, + { 0x11C38, 0x11C3D, Neutral }, + { 0x11C92, 0x11CA7, Neutral }, + { 0x11CAA, 0x11CB0, Neutral }, + { 0x11CB2, 0x11CB3, Neutral }, + { 0x11CB5, 0x11CB6, Neutral }, + { 0x11D31, 0x11D36, Neutral }, + { 0x11D3A, 0x11D3A, Neutral }, + { 0x11D3C, 0x11D3D, Neutral }, + { 0x11D3F, 0x11D45, Neutral }, + { 0x11D47, 0x11D47, Neutral }, + { 0x11D90, 0x11D91, Neutral }, + { 0x11D95, 0x11D95, Neutral }, + { 0x11D97, 0x11D97, Neutral }, + { 0x11EF3, 0x11EF4, Neutral }, + { 0x11F00, 0x11F01, Neutral }, + { 0x11F36, 0x11F3A, Neutral }, + { 0x11F40, 0x11F40, Neutral }, + { 0x11F42, 0x11F42, Neutral }, + { 0x11F5A, 0x11F5A, Neutral }, + { 0x11FD5, 0x11FF1, Neutral }, + { 0x13440, 0x13440, Neutral }, + { 0x13447, 0x13455, Neutral }, + { 0x1611E, 0x16129, Neutral }, + { 0x1612D, 0x1612F, Neutral }, + { 0x16AF0, 0x16AF4, Neutral }, + { 0x16B30, 0x16B36, Neutral }, + { 0x16F4F, 0x16F4F, Neutral }, + { 0x16F8F, 0x16F92, Neutral }, + { 0x16FE2, 0x16FE2, Neutral }, + { 0x16FE4, 0x16FE4, Neutral }, + { 0x1BC9D, 0x1BC9E, Neutral }, + { 0x1BCA0, 0x1BCA3, Neutral }, + { 0x1CC00, 0x1CCD5, Neutral }, + { 0x1CCF0, 0x1CCFC, Neutral }, + { 0x1CD00, 0x1CEB3, Neutral }, + { 0x1CEBA, 0x1CED0, Neutral }, + { 0x1CEE0, 0x1CEF0, Neutral }, + { 0x1CF00, 0x1CF2D, Neutral }, + { 0x1CF30, 0x1CF46, Neutral }, + { 0x1D167, 0x1D169, Neutral }, + { 0x1D173, 0x1D182, Neutral }, + { 0x1D185, 0x1D18B, Neutral }, + { 0x1D1AA, 0x1D1AD, Neutral }, + { 0x1D1E9, 0x1D1EA, Neutral }, + { 0x1D200, 0x1D245, Neutral }, + { 0x1D300, 0x1D356, Neutral }, + { 0x1D6C1, 0x1D6C1, Neutral }, + { 0x1D6DB, 0x1D6DB, Neutral }, + { 0x1D6FB, 0x1D6FB, Neutral }, + { 0x1D715, 0x1D715, Neutral }, + { 0x1D735, 0x1D735, Neutral }, + { 0x1D74F, 0x1D74F, Neutral }, + { 0x1D76F, 0x1D76F, Neutral }, + { 0x1D789, 0x1D789, Neutral }, + { 0x1D7A9, 0x1D7A9, Neutral }, + { 0x1D7C3, 0x1D7C3, Neutral }, + { 0x1D7CE, 0x1D7FF, Neutral }, + { 0x1DA00, 0x1DA36, Neutral }, + { 0x1DA3B, 0x1DA6C, Neutral }, + { 0x1DA75, 0x1DA75, Neutral }, + { 0x1DA84, 0x1DA84, Neutral }, + { 0x1DA9B, 0x1DA9F, Neutral }, + { 0x1DAA1, 0x1DAAF, Neutral }, + { 0x1E000, 0x1E006, Neutral }, + { 0x1E008, 0x1E018, Neutral }, + { 0x1E01B, 0x1E021, Neutral }, + { 0x1E023, 0x1E024, Neutral }, + { 0x1E026, 0x1E02A, Neutral }, + { 0x1E08F, 0x1E08F, Neutral }, + { 0x1E130, 0x1E136, Neutral }, + { 0x1E2AE, 0x1E2AE, Neutral }, + { 0x1E2EC, 0x1E2EF, Neutral }, + { 0x1E2FF, 0x1E2FF, Neutral }, + { 0x1E4EC, 0x1E4EF, Neutral }, + { 0x1E5EE, 0x1E5EF, Neutral }, + { 0x1E6E3, 0x1E6E3, Neutral }, + { 0x1E6E6, 0x1E6E6, Neutral }, + { 0x1E6EE, 0x1E6EF, Neutral }, + { 0x1E6F5, 0x1E6F5, Neutral }, + { 0x1E800, 0x1E8CF, RightToLeft }, + { 0x1E8D0, 0x1E8D6, Neutral }, + { 0x1E8D7, 0x1E943, RightToLeft }, + { 0x1E944, 0x1E94A, Neutral }, + { 0x1E94B, 0x1EEEF, RightToLeft }, + { 0x1EEF0, 0x1EEF1, Neutral }, + { 0x1EEF2, 0x1EFFF, RightToLeft }, + { 0x1F000, 0x1F02B, Neutral }, + { 0x1F030, 0x1F093, Neutral }, + { 0x1F0A0, 0x1F0AE, Neutral }, + { 0x1F0B1, 0x1F0BF, Neutral }, + { 0x1F0C1, 0x1F0CF, Neutral }, + { 0x1F0D1, 0x1F0F5, Neutral }, + { 0x1F100, 0x1F10F, Neutral }, + { 0x1F12F, 0x1F12F, Neutral }, + { 0x1F16A, 0x1F16F, Neutral }, + { 0x1F1AD, 0x1F1AD, Neutral }, + { 0x1F260, 0x1F265, Neutral }, + { 0x1F300, 0x1F6D8, Neutral }, + { 0x1F6DC, 0x1F6EC, Neutral }, + { 0x1F6F0, 0x1F6FC, Neutral }, + { 0x1F700, 0x1F7D9, Neutral }, + { 0x1F7E0, 0x1F7EB, Neutral }, + { 0x1F7F0, 0x1F7F0, Neutral }, + { 0x1F800, 0x1F80B, Neutral }, + { 0x1F810, 0x1F847, Neutral }, + { 0x1F850, 0x1F859, Neutral }, + { 0x1F860, 0x1F887, Neutral }, + { 0x1F890, 0x1F8AD, Neutral }, + { 0x1F8B0, 0x1F8BB, Neutral }, + { 0x1F8C0, 0x1F8C1, Neutral }, + { 0x1F8D0, 0x1F8D8, Neutral }, + { 0x1F900, 0x1FA57, Neutral }, + { 0x1FA60, 0x1FA6D, Neutral }, + { 0x1FA70, 0x1FA7C, Neutral }, + { 0x1FA80, 0x1FA8A, Neutral }, + { 0x1FA8E, 0x1FAC6, Neutral }, + { 0x1FAC8, 0x1FAC8, Neutral }, + { 0x1FACD, 0x1FADC, Neutral }, + { 0x1FADF, 0x1FAEA, Neutral }, + { 0x1FAEF, 0x1FAF8, Neutral }, + { 0x1FB00, 0x1FB92, Neutral }, + { 0x1FB94, 0x1FBFA, Neutral }, + { 0x1FFFE, 0x1FFFF, Neutral }, + { 0x2FFFE, 0x2FFFF, Neutral }, + { 0x3FFFE, 0x3FFFF, Neutral }, + { 0x4FFFE, 0x4FFFF, Neutral }, + { 0x5FFFE, 0x5FFFF, Neutral }, + { 0x6FFFE, 0x6FFFF, Neutral }, + { 0x7FFFE, 0x7FFFF, Neutral }, + { 0x8FFFE, 0x8FFFF, Neutral }, + { 0x9FFFE, 0x9FFFF, Neutral }, + { 0xAFFFE, 0xAFFFF, Neutral }, + { 0xBFFFE, 0xBFFFF, Neutral }, + { 0xCFFFE, 0xCFFFF, Neutral }, + { 0xDFFFE, 0xE0FFF, Neutral }, + { 0xEFFFE, 0xEFFFF, Neutral }, + { 0xFFFFE, 0xFFFFF, Neutral }, + { 0x10FFFE, 0x10FFFF, Neutral }, diff --git a/Core/Libraries/Source/WWVegas/WW3D2/unicode-license.txt b/Core/Libraries/Source/WWVegas/WW3D2/unicode-license.txt new file mode 100644 index 00000000000..56da5891280 --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WW3D2/unicode-license.txt @@ -0,0 +1,39 @@ +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2026 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. diff --git a/Core/Libraries/Source/WWVegas/WW3D2/unicodebidi.h b/Core/Libraries/Source/WWVegas/WW3D2/unicodebidi.h new file mode 100644 index 00000000000..b9d1dfcd306 --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WW3D2/unicodebidi.h @@ -0,0 +1,84 @@ +/* +** 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 "WWLib/win.h" + +namespace UnicodeBidi +{ + +enum Direction { Neutral, LeftToRight, RightToLeft }; + +inline Direction Get_Supplementary_Direction(unsigned codepoint) +{ + struct Range { unsigned First, Last; Direction Value; }; + static const Range ranges[] = { +#include "supplementarybidi.inl" + }; + unsigned first = 0, last = sizeof(ranges) / sizeof(ranges[0]); + while (first < last) { + const unsigned middle = first + (last - first) / 2; + if (codepoint < ranges[middle].First) { + last = middle; + } else if (codepoint > ranges[middle].Last) { + first = middle + 1; + } else { + return ranges[middle].Value; + } + } + return LeftToRight; +} + +// TheSuperHackers @bugfix Omar Aglan 13/09/2026 Determine paragraph direction from complete UTF-16 +// characters. GetStringTypeW does not classify supplementary characters reliably. +inline WORD Get_Paragraph_Level(const WCHAR *text, int length) +{ + int isolate_depth = 0; + for (int index = 0; index < length; ++index) { + const WCHAR ch = text[index]; + if (ch >= 0x2066 && ch <= 0x2068) { + ++isolate_depth; + continue; + } + if (ch == 0x2069) { + if (isolate_depth > 0) --isolate_depth; + continue; + } + // UAX #9 P2 excludes the contents of directional isolates. + if (isolate_depth > 0) continue; + + Direction direction = Neutral; + if (ch >= 0xD800 && ch <= 0xDBFF) { + if (index + 1 < length && text[index + 1] >= 0xDC00 && text[index + 1] <= 0xDFFF) { + const unsigned codepoint = 0x10000 + ((ch - 0xD800) << 10) + (text[++index] - 0xDC00); + direction = Get_Supplementary_Direction(codepoint); + } + } else if (ch < 0xDC00 || ch > 0xDFFF) { + WORD type = C2_NOTAPPLICABLE; + if (::GetStringTypeW(CT_CTYPE2, &ch, 1, &type)) { + if (type == C2_LEFTTORIGHT) direction = LeftToRight; + if (type == C2_RIGHTTOLEFT) direction = RightToLeft; + } + } + if (direction != Neutral) return direction == RightToLeft ? 1 : 0; + } + return 0; +} + +} 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..c11dda1ddfa 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 @@ -95,8 +96,9 @@ class W3DDisplayString : public DisplayString Render2DSentenceClass m_textRenderer; ///< for drawing text 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_textChanged; ///< when text or font changes 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; @@ -113,6 +115,7 @@ class W3DDisplayString : public DisplayString /////////////////////////////////////////////////////////////////////////////// // INLINING /////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// -inline void W3DDisplayString::usingResources( UnsignedInt frame ) { m_lastResourceFrame = frame; } +// TheSuperHackers @bugfix Omar Aglan 10/09/2026 Reserve zero for strings without rendering resources. +inline void W3DDisplayString::usingResources( UnsignedInt frame ) { m_lastResourceFrame = max(frame, 1U); } // EXTERNALS ////////////////////////////////////////////////////////////////// diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp index eafcf5fe6d6..03a71fd555a 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" @@ -85,7 +84,8 @@ W3DDisplayString::W3DDisplayString() m_currDropColor = 0; 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 +130,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 +157,37 @@ void W3DDisplayString::notifyTextChanged() } +// W3DDisplayString::checkForChangedTextData ================================== +/** Rebuild the sentence and update its extents when its source data changes */ +//============================================================================= +void W3DDisplayString::checkForChangedTextData() +{ + if( !m_textChanged ) + return; + + bool usedComplexText = false; + Vector2 legacyExtents; + m_textRenderer.Build_Sentence(getText().str(), + m_hotkey.isEmpty() ? nullptr : &m_hotKeyPos.x, + m_hotkey.isEmpty() ? nullptr : &m_hotKeyPos.y, + &usedComplexText, &legacyExtents); + if(!m_hotkey.isEmpty()) + m_textRendererHotKey.Build_Sentence(m_hotkey.str(), nullptr, nullptr); + + // 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_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 +202,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 +238,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 +249,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 +268,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 +285,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 +296,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 +315,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 */ //============================================================================= @@ -313,8 +353,8 @@ void W3DDisplayString::setFont( GameFont *font ) // recompute extents for text with new font computeExtents(); - // set flag telling us the font has changed since last render - m_fontChanged = TRUE; + // rebuild the sentence with the new font + m_textChanged = TRUE; } @@ -363,14 +403,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 +431,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/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp index d67d6282d30..eff80c77c0e 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp @@ -176,7 +176,8 @@ void W3DDisplayStringManager::update() string = static_cast(m_currentCheckpoint); } - UnsignedInt currFrame = TheGameClient->getFrame(); + // TheSuperHackers @bugfix Omar Aglan 10/09/2026 Match the nonzero timestamp used for frame-zero resources. + UnsignedInt currFrame = max(TheGameClient->getFrame(), 1U); const UnsignedInt w3dCleanupTime = 60; /** any string not rendered after this many frames will have its render resources freed */ diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h index 0d49e002dab..8283cb9534e 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 @@ -95,8 +96,9 @@ class W3DDisplayString : public DisplayString Render2DSentenceClass m_textRenderer; ///< for drawing text 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_textChanged; ///< when text or font changes 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; @@ -113,6 +115,7 @@ class W3DDisplayString : public DisplayString /////////////////////////////////////////////////////////////////////////////// // INLINING /////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// -inline void W3DDisplayString::usingResources( UnsignedInt frame ) { m_lastResourceFrame = frame; } +// TheSuperHackers @bugfix Omar Aglan 10/09/2026 Reserve zero for strings without rendering resources. +inline void W3DDisplayString::usingResources( UnsignedInt frame ) { m_lastResourceFrame = max(frame, 1U); } // EXTERNALS ////////////////////////////////////////////////////////////////// diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp index c8a4b78ea59..233cc39b4e6 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" @@ -85,7 +84,8 @@ W3DDisplayString::W3DDisplayString() m_currDropColor = 0; 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 +130,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 +157,37 @@ void W3DDisplayString::notifyTextChanged() } +// W3DDisplayString::checkForChangedTextData ================================== +/** Rebuild the sentence and update its extents when its source data changes */ +//============================================================================= +void W3DDisplayString::checkForChangedTextData() +{ + if( !m_textChanged ) + return; + + bool usedComplexText = false; + Vector2 legacyExtents; + m_textRenderer.Build_Sentence(getText().str(), + m_hotkey.isEmpty() ? nullptr : &m_hotKeyPos.x, + m_hotkey.isEmpty() ? nullptr : &m_hotKeyPos.y, + &usedComplexText, &legacyExtents); + if(!m_hotkey.isEmpty()) + m_textRendererHotKey.Build_Sentence(m_hotkey.str(), nullptr, nullptr); + + // 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_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 +202,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 +238,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 +249,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 +268,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 +285,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 +296,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 +315,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 */ //============================================================================= @@ -313,8 +353,8 @@ void W3DDisplayString::setFont( GameFont *font ) // recompute extents for text with new font computeExtents(); - // set flag telling us the font has changed since last render - m_fontChanged = TRUE; + // rebuild the sentence with the new font + m_textChanged = TRUE; } @@ -363,14 +403,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 +431,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/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp index 34d1c0e0046..c64a1fea22b 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayStringManager.cpp @@ -176,7 +176,8 @@ void W3DDisplayStringManager::update() string = static_cast(m_currentCheckpoint); } - UnsignedInt currFrame = TheGameClient->getFrame(); + // TheSuperHackers @bugfix Omar Aglan 10/09/2026 Match the nonzero timestamp used for frame-zero resources. + UnsignedInt currFrame = max(TheGameClient->getFrame(), 1U); const UnsignedInt w3dCleanupTime = 60; /** any string not rendered after this many frames will have its render resources freed */ diff --git a/scripts/generate_supplementary_bidi.py b/scripts/generate_supplementary_bidi.py new file mode 100644 index 00000000000..e5e779c64b6 --- /dev/null +++ b/scripts/generate_supplementary_bidi.py @@ -0,0 +1,54 @@ +"""Generate the supplementary first-strong-direction table from Unicode 17.0.0. + +Input: https://www.unicode.org/Public/17.0.0/ucd/extracted/DerivedBidiClass.txt +Usage: python scripts/generate_supplementary_bidi.py DerivedBidiClass.txt [--check] +""" + +import argparse +import hashlib +from pathlib import Path +import re + + +def generate(source): + if not source.startswith("# DerivedBidiClass-17.0.0.txt"): + raise ValueError("Expected Unicode 17.0.0 DerivedBidiClass.txt") + if hashlib.sha256(source.encode("utf-8")).hexdigest() != "4867b4b7f0731ed1bfcd34cc6251211ff1542541fce0734b6fbda139ee80b3a4": + raise ValueError("Unexpected Unicode data checksum") + classes = ["L"] * 0x110000 + aliases = {"Left_To_Right": "L", "Right_To_Left": "R", "Arabic_Letter": "AL"} + missing = re.findall(r"@missing: ([0-9A-F]+)\.\.([0-9A-F]+); (\w+)", source) + explicit = re.findall(r"^([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s*;\s*(\w+)", source, re.M) + for lo, hi, value in missing + explicit: + lo, hi = int(lo, 16), int(hi or lo, 16) + classes[lo:hi + 1] = [aliases.get(value, value)] * (hi - lo + 1) + directions = ["LeftToRight" if c == "L" else "RightToLeft" if c in ("R", "AL") + else "Neutral" for c in classes] + lines = [ + "// Generated by scripts/generate_supplementary_bidi.py; do not edit.", + "// Unicode 17.0.0 DerivedBidiClass.txt, including its @missing defaults.", + "// Copyright 2025 Unicode, Inc. See unicode-license.txt.", + "// Omitted ranges have direction LeftToRight. Neutral means neither L, R nor AL.", + ] + start = 0x10000 + for end in range(start + 1, len(directions) + 1): + if end == len(directions) or directions[end] != directions[start]: + if directions[start] != "LeftToRight": + lines.append("\t{ 0x%05X, 0x%05X, %s }," % (start, end - 1, directions[start])) + start = end + return "\n".join(lines) + "\n" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source", type=Path) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + output = Path(__file__).resolve().parents[1] / "Core/Libraries/Source/WWVegas/WW3D2/supplementarybidi.inl" + generated = generate(args.source.read_text(encoding="utf-8")) + if args.check: + if output.read_text(encoding="utf-8") != generated: + raise SystemExit("Supplementary bidi table is out of date") + print("Unicode 17.0.0 supplementary bidi table matches") + else: + output.write_text(generated, encoding="utf-8")