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/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;
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")