diff --git a/CMakeLists.txt b/CMakeLists.txt index 39062e3fbe6..2c9d23e11b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,11 @@ if (IS_VS6_BUILD) add_subdirectory(Dependencies/MaxSDK) endif() add_subdirectory(Dependencies/Utility) +if (WIN32) + add_subdirectory(Dependencies/Usp10) +else() + add_library(deps_usp10 INTERFACE) +endif() # To be removed when abandoning VC6 if (IS_VS6_BUILD) diff --git a/Core/GameEngine/Include/GameClient/DisplayString.h b/Core/GameEngine/Include/GameClient/DisplayString.h index 042ab1e6963..3b5f53a5dab 100644 --- a/Core/GameEngine/Include/GameClient/DisplayString.h +++ b/Core/GameEngine/Include/GameClient/DisplayString.h @@ -91,6 +91,8 @@ 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 Int getCaretOffset() = 0; ///< get the horizontal offset of a caret after the last character + virtual Bool isRightToLeft() = 0; ///< TRUE if the text starts with a right-to-left paragraph virtual void setUseHotkey( Bool useHotkey, Color hotKeyColor ) = 0; @@ -126,3 +128,13 @@ inline void DisplayString::notifyTextChanged() {} inline DisplayString *DisplayString::next() { return m_next; } // EXTERNALS ////////////////////////////////////////////////////////////////// + +// TheSuperHackers @feature Wraps text, such as a player name or a chat message, in a Unicode +// first strong isolate if it contains right-to-left characters, so that it keeps its own reading +// order when it is shown next to other text, as in "[name] message". +UnicodeString IsolateBidiText( const UnicodeString &text ); + +// TheSuperHackers @feature Applies IsolateBidiText to the names in the leading brackets of a chat line +// that was put together elsewhere, such as "[name] message" or "[tag] [name] message", and to the +// message after them. +UnicodeString IsolateBidiChatLine( const UnicodeString &line ); diff --git a/Core/GameEngine/Source/GameClient/DisplayString.cpp b/Core/GameEngine/Source/GameClient/DisplayString.cpp index a276c7a78bf..1fb976c9565 100644 --- a/Core/GameEngine/Source/GameClient/DisplayString.cpp +++ b/Core/GameEngine/Source/GameClient/DisplayString.cpp @@ -64,10 +64,62 @@ // PRIVATE FUNCTIONS ////////////////////////////////////////////////////////// +// Hebrew, Arabic and the other right-to-left scripts of the Basic Multilingual Plane +static Bool isRightToLeftChar( WideChar ch ) +{ + return (ch >= 0x0590 && ch <= 0x08FF) || (ch >= 0xFB1D && ch <= 0xFDFF) || (ch >= 0xFE70 && ch <= 0xFEFC); +} + /////////////////////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS /////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// +// IsolateBidiText ============================================================ +/** Text without right-to-left characters is returned as it is, so that it is + * still drawn by the glyph cache of the font. */ +//============================================================================= +UnicodeString IsolateBidiText( const UnicodeString &text ) +{ + const WideChar *str = text.str(); + for( Int i = 0; str[i] != 0; ++i ) + { + if( isRightToLeftChar( str[i] ) ) + { + UnicodeString isolated; + isolated.concat( (WideChar)0x2068 ); // first strong isolate + isolated.concat( text ); + isolated.concat( (WideChar)0x2069 ); // pop directional isolate + return isolated; + } + } + return text; +} + +// IsolateBidiChatLine ======================================================== +/** A leading group ends at the first closing bracket that a space follows */ +//============================================================================= +UnicodeString IsolateBidiChatLine( const UnicodeString &line ) +{ + UnicodeString result; + const WideChar *str = line.str(); + while( str[0] == L'[' ) + { + const WideChar *close = wcschr( str, L']' ); + if( close == nullptr || close[1] != L' ' ) + break; + + UnicodeString name; + name.set( str + 1, (Int)(close - str - 1) ); + result.concat( L'[' ); + result.concat( IsolateBidiText( name ) ); + result.concat( L"] " ); + str = close + 2; + } + + result.concat( IsolateBidiText( UnicodeString( str ) ) ); + return result; +} + // DisplayString::DisplayString =============================================== /** */ //============================================================================= diff --git a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp index f347db5a83f..f36df4bd47b 100644 --- a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -40,6 +40,7 @@ #include "Common/Recorder.h" #include "GameClient/Diplomacy.h" +#include "GameClient/DisplayString.h" #include "GameClient/GameText.h" #include "GameClient/MessageBox.h" #include "GameNetwork/ConnectionManager.h" @@ -752,7 +753,7 @@ void ConnectionManager::processDisconnectChat(NetDisconnectChatCommandMsg *msg) } else if (isPlayerConnected(playerID)) { name = m_connections[playerID]->getUser()->GetName(); } - unitext.format(L"[%ls] %ls", name.str(), msg->getText().str()); + unitext.format(L"[%ls] %ls", IsolateBidiText(name).str(), IsolateBidiText(msg->getText()).str()); // DEBUG_LOG(("ConnectionManager::processDisconnectChat - got message from player %d, message is %ls", playerID, unitext.str())); TheDisconnectMenu->showChat(unitext); // <-- need to implement this } @@ -773,7 +774,7 @@ void ConnectionManager::processChat(NetChatCommandMsg *msg) name = m_connections[playerID]->getUser()->GetName(); //DEBUG_LOG(("connection is non-null, using %ls", name.str())); } - unitext.format(L"[%ls] %ls", name.str(), msg->getText().str()); + unitext.format(L"[%ls] %ls", IsolateBidiText(name).str(), IsolateBidiText(msg->getText()).str()); // DEBUG_LOG(("ConnectionManager::processChat - got message from player %d (mask %8.8X), message is %ls", playerID, msg->getPlayerMask(), unitext.str())); AsciiString playerName; diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/Chat.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/Chat.cpp index c9928461183..8e6dbe3aeb6 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/Chat.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/Chat.cpp @@ -30,6 +30,7 @@ #include "Common/AudioEventRTS.h" #include "Common/INI.h" +#include "GameClient/DisplayString.h" #include "GameClient/GameText.h" #include "GameClient/GadgetListBox.h" #include "GameClient/LanguageFilter.h" @@ -301,11 +302,11 @@ void GameSpyInfo::addChat( PlayerInfo p, UnicodeString msg, Bool isPublic, Bool UnicodeString fullMsg; if (isAction) { - fullMsg.format( L"%ls %ls", name.str(), msg.str() ); + fullMsg.format( L"%ls %ls", IsolateBidiText(name).str(), IsolateBidiText(msg).str() ); } else { - fullMsg.format( L"[%ls] %ls", name.str(), msg.str() ); + fullMsg.format( L"[%ls] %ls", IsolateBidiText(name).str(), IsolateBidiText(msg).str() ); } Int index = addText(fullMsg, GameSpyColor[style], win); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp index 701ef3e987e..75d05011f64 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp @@ -38,6 +38,7 @@ #include "Common/QuotedPrintable.h" #include "Common/RandomValue.h" #include "Common/UserPreferences.h" +#include "GameClient/DisplayString.h" #include "GameClient/GameText.h" #include "GameClient/LanguageFilter.h" #include "GameClient/MapUtil.h" @@ -690,9 +691,9 @@ void LANAPI::OnChat( UnicodeString player, UnsignedInt ip, UnicodeString message index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatSystemColor, -1, -1); break; case LANAPIInterface::LANCHAT_EMOTE: - unicodeChat = player; + unicodeChat = IsolateBidiText(player); unicodeChat.concat(L' '); - unicodeChat.concat(message); + unicodeChat.concat(IsolateBidiText(message)); if (ip == m_localIP) index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatLocalActionColor, -1, -1); else @@ -721,9 +722,9 @@ void LANAPI::OnChat( UnicodeString player, UnsignedInt ip, UnicodeString message } unicodeChat = L"["; - unicodeChat.concat(player); + unicodeChat.concat(IsolateBidiText(player)); unicodeChat.concat(L"] "); - unicodeChat.concat(message); + unicodeChat.concat(IsolateBidiText(message)); if (ip == m_localIP) index =GadgetListBoxAddEntryText(chatWindow, unicodeChat, chatColor, -1, -1); else diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DTextEntry.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DTextEntry.cpp index cf11ddebf9a..c250509d7ed 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DTextEntry.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/Gadget/W3DTextEntry.cpp @@ -133,6 +133,11 @@ static void drawTextEntryText( GameWindow *window, WinInstanceData *instData, // get the size of our text, and construct text Int textWidth = text->getWidth(); + // TheSuperHackers @feature Right-to-left text ends on its left side, so the caret + // follows the last character instead of the right edge of the text. + Int caretOffset = text->getCaretOffset(); + Bool rightToLeft = text->isRightToLeft(); + if (!e->drawTextFromStart) { // clip the text to the edit window size @@ -153,14 +158,17 @@ static void drawTextEntryText( GameWindow *window, WinInstanceData *instData, // draw the text if(textWidth < width) { - text->draw( x, y, textColor, textDropColor ); - cursorPos = textWidth + x; + // right-to-left text starts at the right edge of the entry + Int textX = rightToLeft ? x + width - 4 - textWidth : x; + text->draw( textX, y, textColor, textDropColor ); + cursorPos = caretOffset + textX; } else { - Int div = textWidth / (width / 2) - 1; + // scroll by half the entry width until the caret is visible + Int div = max( caretOffset / (width / 2) - 1, 0 ); text->draw(x - (div * (width/2)), y, textColor, textDropColor); - cursorPos = textWidth - (div * (width/2)) + x; + cursorPos = caretOffset - (div * (width/2)) + x; } //cursorPos = x + textWidth; @@ -183,9 +191,13 @@ static void drawTextEntryText( GameWindow *window, WinInstanceData *instData, x+= 5; // draw the text text->draw( x, y, textColor, textDropColor ); - cursorPos = textWidth + x; + cursorPos = caretOffset + x; } + // the next right-to-left character goes left of the caret, so draw the caret left of its position + if (rightToLeft) + cursorPos -= 2; + if (e->constructText->getTextLength() > 0 ) { e->constructText->draw( x + textWidth, y, compositeColor, compositeDropColor ); diff --git a/Core/Libraries/Source/WWVegas/CMakeLists.txt b/Core/Libraries/Source/WWVegas/CMakeLists.txt index e8a6beeb0f2..cd8bbe4d68f 100644 --- a/Core/Libraries/Source/WWVegas/CMakeLists.txt +++ b/Core/Libraries/Source/WWVegas/CMakeLists.txt @@ -8,6 +8,7 @@ target_compile_definitions(core_wwcommon INTERFACE target_link_libraries(core_wwcommon INTERFACE d3d8lib + deps_usp10 milesstub stlport ) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt index ecebd564696..4c96292b88f 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt +++ b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt @@ -233,6 +233,13 @@ set(WW3D2_SRC ww3dtrig.h ) +if (WIN32) + list(APPEND WW3D2_SRC + shapedtext.cpp + shapedtext.h + ) +endif() + add_library(corei_ww3d2 INTERFACE) target_sources(corei_ww3d2 INTERFACE ${WW3D2_SRC}) diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp index 64001c0bc07..6d42bb34bef 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp @@ -40,6 +40,10 @@ #include "WWDebug/wwprofile.h" #include "WWDebug/wwmemlog.h" #include "dx8wrapper.h" +#if defined(_WIN32) +#include "shapedtext.h" +#include "Usp10Loader.h" +#endif //////////////////////////////////////////////////////////////////////////////////// @@ -48,6 +52,17 @@ #define no_TEST_PLACEMENT 1 // Shows alignment markers for text. #define TEXTURE_OFFSET 2 + +//////////////////////////////////////////////////////////////////////////////////// +// Local functions +//////////////////////////////////////////////////////////////////////////////////// + +// Returns whether the character at text is a hot key marker that Build_Sentence_Not_Centered skips. +static inline bool Is_Hot_Key_Marker (const WCHAR *text) +{ + return text[0] == L'&' && text[1] != 0 && text[1] > L' ' && text[1] != L'\n'; +} + //////////////////////////////////////////////////////////////////////////////////// // // Render2DSentenceClass @@ -72,7 +87,8 @@ Render2DSentenceClass::Render2DSentenceClass () : Centered (false), DrawExtents (0, 0, 0, 0), ParseHotKey( false ), - useHardWordWrap( false) + useHardWordWrap( false), + IsShaped( false ) { Shader = Render2DClass::Get_Default_Shader (); } @@ -251,6 +267,9 @@ Vector2 Render2DSentenceClass::Get_Text_Extents (const WCHAR *text) { Vector2 extent (0, Font->Get_Char_Height()); + if (Get_Shaped_Text_Extents (text, 0, &extent)) { + return extent; + } while (*text) { WCHAR ch = *text++; @@ -272,6 +291,11 @@ Render2DSentenceClass::Get_Text_Extents (const WCHAR *text) Vector2 Render2DSentenceClass::Get_Formatted_Text_Extents (const WCHAR *text) { + Vector2 extent; + if (Get_Shaped_Text_Extents (text, (int)WrapWidth, &extent)) { + return extent; + } + return Build_Sentence_Not_Centered(text, nullptr, nullptr, true); } @@ -604,6 +628,21 @@ Render2DSentenceClass::Record_Sentence_Chunk () //////////////////////////////////////////////////////////////////////////////////// void Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExtents) +{ + // + // Calculate the width of the text + // + int text_width = 0; + for (int index = 0; text[index] != 0; index ++) { + text_width += Font->Get_Char_Spacing (text[index]); + } + + Allocate_New_Surface (text_width, justCalcExtents); +} + + +void +Render2DSentenceClass::Allocate_New_Surface (int text_width, bool justCalcExtents) { if (!justCalcExtents) { @@ -616,14 +655,6 @@ Render2DSentenceClass::Allocate_New_Surface (const WCHAR *text, bool justCalcExt } } - // - // Calculate the width of the text - // - int text_width = 0; - for (int index = 0; text[index] != 0; index ++) { - text_width += Font->Get_Char_Spacing (text[index]); - } - int char_height = Font->Get_Char_Height (); // @@ -1151,6 +1182,8 @@ Vector2 Render2DSentenceClass::Build_Sentence_Not_Centered (const WCHAR *text, i void Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY) { + IsShaped = false; + if (text == nullptr) { return ; } @@ -1158,6 +1191,11 @@ Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY) if (Font == nullptr) return; + if (Build_Shaped_Sentence (text)) { + IsShaped = true; + return; + } + if(Centered && (WrapWidth > 0 || wcschr(text,L'\n'))) Build_Sentence_Centered(text, hkX, hkY); else @@ -1166,6 +1204,181 @@ Render2DSentenceClass::Build_Sentence (const WCHAR *text, int *hkX, int *hkY) } +//////////////////////////////////////////////////////////////////////////////////// +// +// Layout_Shaped_Text +// +// TheSuperHackers @feature Lays out text that the glyph cache cannot draw, such as +// joined Arabic letters, right-to-left lines and emoji. Returns false for all other +// text, which keeps its original appearance. +// +//////////////////////////////////////////////////////////////////////////////////// +bool +Render2DSentenceClass::Layout_Shaped_Text (ShapedTextClass &shaped_text, const WCHAR *text, int wrap_width) +{ + if (Font == nullptr || text == nullptr || MonoSpaced || !Font->Needs_Shaping (text)) { + return false; + } + + if (!ParseHotKey) { + return Font->Layout_Shaped_Text (shaped_text, text, wrap_width); + } + + // Shaped text shows no hot key, so only its markers are removed. + const int length = (int)wcslen (text); + WCHAR *stripped_text = W3DNEWARRAY WCHAR[length + 1]; + int stripped_length = 0; + for (int index = 0; index < length; ++index) { + if (!Is_Hot_Key_Marker (text + index)) { + stripped_text[stripped_length++] = text[index]; + } + } + stripped_text[stripped_length] = 0; + + const bool success = Font->Layout_Shaped_Text (shaped_text, stripped_text, wrap_width); + delete [] stripped_text; + return success; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Get_Shaped_Text_Extents +// +//////////////////////////////////////////////////////////////////////////////////// +bool +Render2DSentenceClass::Get_Shaped_Text_Extents (const WCHAR *text, int wrap_width, Vector2 *extents) +{ +#if defined(_WIN32) + ShapedTextClass shaped_text; + if (!Layout_Shaped_Text (shaped_text, text, wrap_width)) { + return false; + } + + extents->Set ((float)(shaped_text.Get_Width () + Font->Get_Extra_Overlap ()), (float)shaped_text.Get_Height ()); + return true; +#else + return false; +#endif +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Get_Shaped_Text_Metrics +// +//////////////////////////////////////////////////////////////////////////////////// +bool +Render2DSentenceClass::Get_Shaped_Text_Metrics (const WCHAR *text, int *width, int *caret_x, bool *right_to_left) +{ +#if defined(_WIN32) + ShapedTextClass shaped_text; + if (!Layout_Shaped_Text (shaped_text, text, (int)WrapWidth)) { + return false; + } + + *width = shaped_text.Get_Width (); + *caret_x = shaped_text.Get_End_Caret_X (Centered); + *right_to_left = shaped_text.Is_Right_To_Left (); + return true; +#else + return false; +#endif +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Build_Shaped_Sentence +// +// Copies the shaped text into the sentence textures line by line. Every line is +// as tall as a line of the glyph cache, so chunks are recorded the same way. +// +//////////////////////////////////////////////////////////////////////////////////// +bool +Render2DSentenceClass::Build_Shaped_Sentence (const WCHAR *text) +{ +#if defined(_WIN32) + const int line_height = Font->Get_Char_Height (); + if (line_height + 1 >= max (TextureSizeHint, 256)) { + return false; + } + + ShapedTextClass shaped_text; + if (!Layout_Shaped_Text (shaped_text, text, (int)WrapWidth)) { + return false; + } + + int text_width = 0; + uint16 *raster = Font->Rasterize_Shaped_Text (shaped_text, Centered, &text_width); + if (raster == nullptr) { + return false; + } + + const int line_count = shaped_text.Get_Height () / line_height; + + Reset_Sentence_Data (); + Cursor.Set (0, 0); + + if (CurSurface == nullptr) { + Allocate_New_Surface (text_width * line_count); + } + TextureOffset.Set (TEXTURE_OFFSET, 0); + TextureStartX = TEXTURE_OFFSET; + + // TheSuperHackers @bugfix Stop when a sentence surface cannot be created or locked. + bool success = CurSurface != nullptr; + for (int line = 0; success && line < line_count; ++line) { + Cursor.Set (0, (float)(line * line_height)); + + int source_x = 0; + while (success && source_x < text_width) { + const int chunk_width = min (text_width - source_x, CurrTextureSize - 1 - TextureOffset.I); + if (chunk_width <= 0) { + // + // Continue on the next row of the texture, or on a new texture + // + TextureOffset.J += line_height; + if ((TextureOffset.J + line_height) >= CurrTextureSize) { + Allocate_New_Surface (text_width * (line_count - line) - source_x); + success = CurSurface != nullptr; + } + TextureOffset.I = TEXTURE_OFFSET; + TextureStartX = TEXTURE_OFFSET; + continue; + } + + if (LockedPtr == nullptr) { + LockedPtr = (uint16 *)CurSurface->Lock (&LockedStride); + if (LockedPtr == nullptr) { + success = false; + break; + } + } + + const int dest_inc = LockedStride >> 1; + for (int row = 0; row < line_height; ++row) { + const uint16 *source = raster + ((line * line_height + 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 (); + Cursor.X += chunk_width; + TextureStartX = TextureOffset.I; + source_x += chunk_width; + } + } + + delete [] raster; + return success; +#else + return false; +#endif +} + + //////////////////////////////////////////////////////////////////////////////////// // // FontCharsClass @@ -1188,6 +1401,9 @@ FontCharsClass::FontCharsClass () : { AlternateUnicodeFont = nullptr; ::memset( ASCIICharArray, 0, sizeof (ASCIICharArray) ); +#if defined(_WIN32) + Usp10Loader::load(); +#endif } @@ -1205,6 +1421,9 @@ FontCharsClass::~FontCharsClass () Free_GDI_Font(); Free_Character_Arrays(); +#if defined(_WIN32) + Usp10Loader::unload(); +#endif } @@ -1316,6 +1535,111 @@ FontCharsClass::Blit_Char (WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, i } +//////////////////////////////////////////////////////////////////////////////////// +// +// Needs_Shaping +// +//////////////////////////////////////////////////////////////////////////////////// +bool +FontCharsClass::Needs_Shaping (const WCHAR *text) +{ +#if defined(_WIN32) + // The glyph cache takes characters beyond Latin-1 from the alternate Unicode font. + FontCharsClass *unicode_font = AlternateUnicodeFont != nullptr ? AlternateUnicodeFont : this; + return unicode_font->MemDC != nullptr && ShapedTextClass::Needs_Shaping (unicode_font->MemDC, text); +#else + return false; +#endif +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Layout_Shaped_Text +// +//////////////////////////////////////////////////////////////////////////////////// +bool +FontCharsClass::Layout_Shaped_Text (ShapedTextClass &shaped_text, const WCHAR *text, int wrap_width) +{ +#if defined(_WIN32) + return MemDC != nullptr && shaped_text.Layout (MemDC, text, wrap_width, CharHeight); +#else + return false; +#endif +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Rasterize_Shaped_Text +// +//////////////////////////////////////////////////////////////////////////////////// +uint16 * +FontCharsClass::Rasterize_Shaped_Text (const ShapedTextClass &shaped_text, bool centered, int *width) +{ +#if defined(_WIN32) + // + // Leave room for glyphs that reach beyond their advance, like the glyph cache does + // + const int text_width = shaped_text.Get_Width () + PixelOverlap; + const int text_height = shaped_text.Get_Height (); + if (text_width <= 0 || text_height <= 0 || text_width > 0x4000 || text_height > 0x4000) { + return nullptr; + } + + BITMAPINFOHEADER bitmap_info = { 0 }; + bitmap_info.biSize = sizeof (BITMAPINFOHEADER); + bitmap_info.biWidth = text_width; + bitmap_info.biHeight = -text_height; + bitmap_info.biPlanes = 1; + bitmap_info.biBitCount = 24; + bitmap_info.biCompression = BI_RGB; + + uint8 *bitmap_bits = nullptr; + HBITMAP bitmap = ::CreateDIBSection (MemDC, (const BITMAPINFO *)&bitmap_info, DIB_RGB_COLORS, + (void **)&bitmap_bits, nullptr, 0L); + if (bitmap == nullptr || bitmap_bits == nullptr) { + if (bitmap != nullptr) { + ::DeleteObject (bitmap); + } + return nullptr; + } + + // + // The layout draws into the device context of this font, so the bitmap of the + // glyph cache is swapped out while it draws + // + const int stride = ((text_width * 3) + 3) & ~3; + ::memset (bitmap_bits, 0, stride * text_height); + HBITMAP old_bitmap = (HBITMAP)::SelectObject (MemDC, bitmap); + const bool success = shaped_text.Draw (centered); + ::SelectObject (MemDC, old_bitmap); + + uint16 *pixels = nullptr; + if (success) { + pixels = W3DNEWARRAY uint16[text_width * text_height]; + for (int row = 0; row < text_height; ++row) { + const uint8 *source = bitmap_bits + (row * stride); + uint16 *destination = pixels + (row * text_width); + for (int col = 0; col < text_width; ++col, source += 3) { + // + // Store the intensity as 4 bit alpha, like Store_GDI_Char does + // + const uint8 value = max (source[0], max (source[1], source[2])); + destination[col] = (value != 0 ? 0x0FFF : 0) | ((value >> 4) << 12); + } + } + *width = text_width; + } + + ::DeleteObject (bitmap); + return pixels; +#else + return nullptr; +#endif +} + + //////////////////////////////////////////////////////////////////////////////////// // // Store_GDI_Char diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h index 15426c1e950..dbad91e7676 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h @@ -47,6 +47,7 @@ ** FontCharsClass */ class SurfaceClass; +class ShapedTextClass; // // Private data structures @@ -94,6 +95,14 @@ class FontCharsClass : public RefCountClass void Blit_Char( WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, int y ); + // + // Text that the glyph cache cannot draw is shaped as a whole, see ShapedTextClass + // + bool Needs_Shaping( const WCHAR *text ); + bool Layout_Shaped_Text( ShapedTextClass &shaped_text, const WCHAR *text, int wrap_width ); + // Returns a buffer of width * shaped_text.Get_Height() pixels that the caller deletes. + uint16 * Rasterize_Shaped_Text( const ShapedTextClass &shaped_text, bool centered, int *width ); + private: // @@ -190,6 +199,15 @@ class Render2DSentenceClass { void Build_Sentence (const WCHAR *text, int *hkX, int *hkY); void Draw_Sentence (uint32 color = 0xFFFFFFFF); + // Returns whether the last built sentence was shaped as a whole. Shaped sentences have no + // hot key position. + bool Is_Shaped_Sentence () const { return IsShaped; } + + // Gets the width of the widest line of shaped text, the distance from its left edge to a caret + // after the last character, and whether the text reads right-to-left. Fails for text that is + // not shaped. + bool Get_Shaped_Text_Metrics (const WCHAR *text, int *width, int *caret_x, bool *right_to_left); + // // Texture hint // @@ -235,9 +253,13 @@ class Render2DSentenceClass { void Build_Textures (); void Record_Sentence_Chunk (); void Allocate_New_Surface (const WCHAR *text, bool justCalcExtents = false); + void Allocate_New_Surface (int text_width, bool justCalcExtents = false); void Release_Pending_Surfaces (); void Build_Sentence_Centered (const WCHAR *text, int *hkX, int *hkY); Vector2 Build_Sentence_Not_Centered (const WCHAR *text, int *hkX, int *hkY,bool justCalcExtents = false ); + bool Layout_Shaped_Text (ShapedTextClass &shaped_text, const WCHAR *text, int wrap_width); + bool Get_Shaped_Text_Extents (const WCHAR *text, int wrap_width, Vector2 *extents); + bool Build_Shaped_Sentence (const WCHAR *text); // // Private member data // @@ -261,6 +283,7 @@ class Render2DSentenceClass { bool IsClippedEnabled; bool ParseHotKey; bool useHardWordWrap; + bool IsShaped; uint16 * LockedPtr; int LockedStride; diff --git a/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.cpp b/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.cpp new file mode 100644 index 00000000000..d8a8cef5e1f --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.cpp @@ -0,0 +1,587 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "shapedtext.h" +#include "Usp10Loader.h" + + +//////////////////////////////////////////////////////////////////////////////////// +// Local functions +//////////////////////////////////////////////////////////////////////////////////// + +static inline bool Is_Surrogate (WCHAR ch) +{ + return ch >= 0xD800 && ch <= 0xDFFF; +} + + +// GetGlyphIndicesW is resolved at runtime, because the headers of VC6 do not declare it. +typedef DWORD (WINAPI *Get_Glyph_Indices_Function)(HDC dc, const WCHAR *text, int length, WORD *glyphs, DWORD flags); +enum { MARK_NONEXISTING_GLYPHS = 0x0001 }; + +static Get_Glyph_Indices_Function Get_Glyph_Indices () +{ + static const Get_Glyph_Indices_Function function = + (Get_Glyph_Indices_Function)::GetProcAddress (::GetModuleHandleA ("gdi32.dll"), "GetGlyphIndicesW"); + return function; +} + + +// Returns the bidirectional type of a character outside of the Basic Multilingual Plane. Emoji and +// other symbols are neutral, historic scripts of the right-to-left blocks are right-to-left. +static int Get_Supplementary_Direction (unsigned codepoint) +{ + if ((codepoint >= 0x10800 && codepoint <= 0x10FFF) || (codepoint >= 0x1E800 && codepoint <= 0x1EFFF)) { + return C2_RIGHTTOLEFT; + } + if (codepoint >= 0x1F000 && codepoint <= 0x1FBFF) { + return C2_OTHERNEUTRAL; + } + return C2_LEFTTORIGHT; +} + + +static inline bool Is_Isolate_Initiator (WCHAR ch) +{ + return ch >= 0x2066 && ch <= 0x2068; +} + + +static inline bool Is_Pop_Directional_Isolate (WCHAR ch) +{ + return ch == 0x2069; +} + + +// Returns the length of the text up to the isolate terminator that matches an isolate that has +// just been opened, or up to the end of the text. +static int Get_Isolate_Length (const WCHAR *text, int length) +{ + int depth = 0; + for (int index = 0; index < length; ++index) { + if (Is_Isolate_Initiator(text[index])) { + ++depth; + } else if (Is_Pop_Directional_Isolate(text[index])) { + if (depth == 0) { + return index; + } + --depth; + } + } + return length; +} + + +// Finds the reading direction of text from the direction of most of its strong characters, and +// from its first strong character when there are as many of each. With the first strong character +// alone, as in rules P2 and P3 of the Unicode Bidirectional Algorithm, a mostly Arabic chat message +// that starts with an English word reads in the wrong order. Characters inside of directional +// isolates count only if there are no strong characters outside of them, so that a line such as +// "[name] message" with an isolated Arabic name and message reads right-to-left as a whole. Text +// without strong characters reads left-to-right. +static bool Is_Right_To_Left_Text (const WCHAR *text, int length) +{ + // Counts of left-to-right and right-to-left characters, outside and inside of isolates + int counts[2][2] = { { 0, 0 }, { 0, 0 } }; + int first_types[2] = { C2_NOTAPPLICABLE, C2_NOTAPPLICABLE }; + int isolate_depth = 0; + for (int index = 0; index < length; ++index) { + const WCHAR ch = text[index]; + if (Is_Isolate_Initiator(ch)) { + ++isolate_depth; + continue; + } + if (Is_Pop_Directional_Isolate(ch)) { + if (isolate_depth > 0) { + --isolate_depth; + } + continue; + } + + int type = C2_NOTAPPLICABLE; + if (ch >= 0xD800 && ch <= 0xDBFF && index + 1 < length && text[index + 1] >= 0xDC00 && text[index + 1] <= 0xDFFF) { + type = Get_Supplementary_Direction(0x10000 + ((ch - 0xD800) << 10) + (text[index + 1] - 0xDC00)); + ++index; + } else if (!Is_Surrogate(ch)) { + WORD char_type = C2_NOTAPPLICABLE; + if (::GetStringTypeW(CT_CTYPE2, &ch, 1, &char_type)) { + type = char_type; + } + } + + if (type != C2_LEFTTORIGHT && type != C2_RIGHTTOLEFT) { + continue; + } + const int inside = isolate_depth > 0 ? 1 : 0; + ++counts[inside][type == C2_RIGHTTOLEFT ? 1 : 0]; + if (first_types[inside] == C2_NOTAPPLICABLE) { + first_types[inside] = type; + } + } + + const int scope = (counts[0][0] + counts[0][1] > 0) ? 0 : 1; + if (counts[scope][0] != counts[scope][1]) { + return counts[scope][1] > counts[scope][0]; + } + return first_types[scope] == C2_RIGHTTOLEFT; +} + + +// Uniscribe does not know the directional isolates of Unicode 6.3 and would draw them as boxes. +// They are replaced by the directional embeddings of the same direction, which order the text in +// the same way for what isolates are used for here, such as a message after a player name. +static void Replace_Directional_Isolates (WCHAR *text, int length) +{ + for (int index = 0; index < length; ++index) { + switch (text[index]) { + case 0x2066: // Left-to-right isolate + text[index] = 0x202A; // Left-to-right embedding + break; + case 0x2067: // Right-to-left isolate + text[index] = 0x202B; // Right-to-left embedding + break; + case 0x2068: // First strong isolate + { + const int isolate_length = Get_Isolate_Length(text + index + 1, length - index - 1); + text[index] = Is_Right_To_Left_Text(text + index + 1, isolate_length) ? 0x202B : 0x202A; + break; + } + case 0x2069: // Pop directional isolate + text[index] = 0x202C; // Pop directional formatting + break; + } + } +} + + +// Gets the directional embeddings and overrides that are still open at the end of the text. +static int Get_Open_Embeddings (const WCHAR *text, int length, WCHAR *embeddings, int max_count) +{ + int count = 0; + int overflow = 0; + for (int index = 0; index < length; ++index) { + const WCHAR ch = text[index]; + if (ch >= 0x202A && ch <= 0x202E && ch != 0x202C) { + if (count < max_count) { + embeddings[count++] = ch; + } else { + ++overflow; + } + } else if (ch == 0x202C) { + if (overflow > 0) { + --overflow; + } else if (count > 0) { + --count; + } + } + } + return count; +} + + +static SCRIPT_STRING_ANALYSIS Analyse_Text (HDC dc, const WCHAR *text, int length, bool right_to_left, DWORD flags) +{ + SCRIPT_CONTROL control; + SCRIPT_STATE state; + ::memset(&control, 0, sizeof(control)); + ::memset(&state, 0, sizeof(state)); + state.uBidiLevel = right_to_left ? 1 : 0; + + flags |= SSA_GLYPHS | SSA_FALLBACK | SSA_LINK; + if (right_to_left) { + flags |= SSA_RTL; + } + + // The glyph buffer size is the one recommended by the documentation of ScriptStringAnalyse. + SCRIPT_STRING_ANALYSIS analysis = nullptr; + if (::ScriptStringAnalyse(dc, text, length, length * 3 / 2 + 16, -1, flags, 0, &control, &state, + nullptr, nullptr, nullptr, &analysis) != S_OK) + { + return nullptr; + } + return analysis; +} + + +static int Get_Length_Without_Trailing_White_Space (const SCRIPT_LOGATTR *attributes, int start, int end) +{ + while (end > start && attributes[end - 1].fWhiteSpace) { + --end; + } + return end - start; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// ShapedTextClass +// +//////////////////////////////////////////////////////////////////////////////////// +ShapedTextClass::ShapedTextClass () : + DC (nullptr), + Width (0), + LineHeight (0) +{ +} + + +ShapedTextClass::~ShapedTextClass () +{ + Free (); +} + + +void +ShapedTextClass::Free () +{ + for (int index = 0; index < Lines.Count (); ++index) { + if (Lines[index].Analysis != nullptr) { + ::ScriptStringFree (&Lines[index].Analysis); + } + } + Lines.Delete_All (); + + for (int text_index = 0; text_index < Texts.Count (); ++text_index) { + delete [] Texts[text_index]; + } + Texts.Delete_All (); + + DC = nullptr; + Width = 0; + LineHeight = 0; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Allocate_Text +// +// The analyses may refer to the text that they were made from, so it is kept +// until they are freed. +// +//////////////////////////////////////////////////////////////////////////////////// +WCHAR * +ShapedTextClass::Allocate_Text (int length) +{ + WCHAR *text = W3DNEWARRAY WCHAR[length + 1]; + text[length] = 0; + Texts.Add (text); + return text; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Needs_Shaping +// +//////////////////////////////////////////////////////////////////////////////////// +bool +ShapedTextClass::Needs_Shaping (HDC dc, const WCHAR *text) +{ + if (text == nullptr || !Usp10Loader::isLoaded ()) { + return false; + } + + int length = 0; + bool has_unicode = false; + for (; text[length] != 0; ++length) { + if (Is_Surrogate (text[length])) { + return true; + } + if (text[length] >= 0x0100) { + has_unicode = true; + } + } + + // Latin-1 is always drawn by the glyph cache. + if (!has_unicode) { + return false; + } + + if (::ScriptIsComplex (text, length, SIC_COMPLEX) == S_OK) { + return true; + } + + // Characters without a glyph in the font would be drawn as boxes. + const Get_Glyph_Indices_Function get_glyph_indices = Get_Glyph_Indices (); + if (get_glyph_indices == nullptr) { + return false; + } + + WORD *glyphs = W3DNEWARRAY WORD[length]; + bool has_missing_glyph = false; + if (get_glyph_indices (dc, text, length, glyphs, MARK_NONEXISTING_GLYPHS) != GDI_ERROR) { + for (int index = 0; index < length; ++index) { + if (text[index] >= 0x0100 && glyphs[index] == 0xFFFF) { + has_missing_glyph = true; + break; + } + } + } + delete [] glyphs; + return has_missing_glyph; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Layout +// +//////////////////////////////////////////////////////////////////////////////////// +bool +ShapedTextClass::Layout (HDC dc, const WCHAR *text, int wrap_width, int line_height) +{ + Free (); + + if (dc == nullptr || text == nullptr || line_height <= 0) { + return false; + } + + DC = dc; + LineHeight = line_height; + + const WCHAR *paragraph = text; + for (;;) { + const WCHAR *paragraph_end = paragraph; + while (*paragraph_end != 0 && *paragraph_end != L'\n') { + ++paragraph_end; + } + + if (!Add_Paragraph (paragraph, (int)(paragraph_end - paragraph), wrap_width)) { + Free (); + return false; + } + + if (*paragraph_end == 0) { + break; + } + paragraph = paragraph_end + 1; + } + + for (int index = 0; index < Lines.Count (); ++index) { + Width = max (Width, Lines[index].Width); + } + + if (Width <= 0) { + Free (); + return false; + } + return true; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Add_Line +// +//////////////////////////////////////////////////////////////////////////////////// +bool +ShapedTextClass::Add_Line (const WCHAR *paragraph, int start, int length, bool right_to_left) +{ + LineStruct line; + line.Analysis = nullptr; + line.Offset = 0; + line.Length = length; + line.Width = 0; + line.RightToLeft = right_to_left; + + if (length > 0) { + // + // Embeddings that are open where a wrapped line starts are opened again on it + // + enum { MAX_OPEN_EMBEDDINGS = 16 }; + WCHAR open_embeddings[MAX_OPEN_EMBEDDINGS]; + line.Offset = Get_Open_Embeddings (paragraph, start, open_embeddings, MAX_OPEN_EMBEDDINGS); + if (line.Offset == 0) { + line.Analysis = Analyse_Text (DC, paragraph + start, length, right_to_left, 0); + } else { + WCHAR *text = Allocate_Text (line.Offset + length); + ::memcpy (text, open_embeddings, line.Offset * sizeof (WCHAR)); + ::memcpy (text + line.Offset, paragraph + start, length * sizeof (WCHAR)); + line.Analysis = Analyse_Text (DC, text, line.Offset + length, right_to_left, 0); + } + + const SIZE *size = line.Analysis != nullptr ? ::ScriptString_pSize (line.Analysis) : nullptr; + if (size == nullptr) { + if (line.Analysis != nullptr) { + ::ScriptStringFree (&line.Analysis); + } + return false; + } + line.Width = size->cx; + } + + Lines.Add (line); + return true; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Add_Paragraph +// +// Breaks the paragraph at soft break opportunities so that no line reaches the wrap +// width. A word that is too wide for a line of its own is broken between clusters. +// White space at the end of a wrapped line is left out, so that it does not shift +// the line away from the edge where its paragraph starts. +// +//////////////////////////////////////////////////////////////////////////////////// +bool +ShapedTextClass::Add_Paragraph (const WCHAR *paragraph_text, int length, int wrap_width) +{ + const bool right_to_left = Is_Right_To_Left_Text (paragraph_text, length); + + WCHAR *text = Allocate_Text (length); + ::memcpy (text, paragraph_text, length * sizeof (WCHAR)); + Replace_Directional_Isolates (text, length); + + if (wrap_width <= 0 || length == 0) { + return Add_Line (text, 0, length, right_to_left); + } + + SCRIPT_STRING_ANALYSIS paragraph = Analyse_Text (DC, text, length, right_to_left, SSA_BREAK); + const SIZE *size = paragraph != nullptr ? ::ScriptString_pSize (paragraph) : nullptr; + const SCRIPT_LOGATTR *paragraph_attributes = paragraph != nullptr ? ::ScriptString_pLogAttr (paragraph) : nullptr; + if (size == nullptr || paragraph_attributes == nullptr) { + if (paragraph != nullptr) { + ::ScriptStringFree (¶graph); + } + return false; + } + + if (size->cx < wrap_width) { + ::ScriptStringFree (¶graph); + return Add_Line (text, 0, length, right_to_left); + } + + // Copy what is needed, so that the analysis of the paragraph can be released before the + // lines are shaped. + int *widths = W3DNEWARRAY int[length]; + SCRIPT_LOGATTR *attributes = W3DNEWARRAY SCRIPT_LOGATTR[length]; + ::memcpy (attributes, paragraph_attributes, length * sizeof (SCRIPT_LOGATTR)); + bool success = ::ScriptStringGetLogicalWidths (paragraph, widths) == S_OK; + ::ScriptStringFree (¶graph); + + int line_start = 0; + int line_width = 0; + int break_position = 0; + for (int index = 0; success && index < length; ++index) { + if (index > line_start && attributes[index].fSoftBreak) { + break_position = index; + } + + while (success && index > line_start && !attributes[index].fWhiteSpace && + line_width + widths[index] >= wrap_width) + { + int line_end = break_position > line_start ? break_position : index; + while (line_end > line_start + 1 && !attributes[line_end].fCharStop) { + --line_end; + } + + success = Add_Line (text, line_start, + Get_Length_Without_Trailing_White_Space (attributes, line_start, line_end), right_to_left); + + line_start = line_end; + line_width = 0; + for (int width_index = line_start; width_index < index; ++width_index) { + line_width += widths[width_index]; + } + } + + line_width += widths[index]; + } + + if (success) { + success = Add_Line (text, line_start, + Get_Length_Without_Trailing_White_Space (attributes, line_start, length), right_to_left); + } + + delete [] attributes; + delete [] widths; + return success; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Get_Line_X +// +//////////////////////////////////////////////////////////////////////////////////// +int +ShapedTextClass::Get_Line_X (const LineStruct &line, bool centered) const +{ + const int free_width = Width - line.Width; + if (centered) { + return free_width / 2; + } + return line.RightToLeft ? free_width : 0; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Draw +// +//////////////////////////////////////////////////////////////////////////////////// +bool +ShapedTextClass::Draw (bool centered) const +{ + if (DC == nullptr) { + return false; + } + + const COLORREF old_text_color = ::SetTextColor (DC, RGB (255, 255, 255)); + const int old_background_mode = ::SetBkMode (DC, TRANSPARENT); + + bool success = true; + for (int index = 0; success && index < Lines.Count (); ++index) { + const LineStruct &line = Lines[index]; + if (line.Analysis != nullptr) { + success = ::ScriptStringOut (line.Analysis, Get_Line_X (line, centered), index * LineHeight, + 0, nullptr, 0, 0, FALSE) == S_OK; + } + } + + ::SetBkMode (DC, old_background_mode); + ::SetTextColor (DC, old_text_color); + + // Complete the drawing before the caller reads the bitmap. + return ::GdiFlush () != FALSE && success; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// Get_End_Caret_X +// +//////////////////////////////////////////////////////////////////////////////////// +int +ShapedTextClass::Get_End_Caret_X (bool centered) const +{ + if (Lines.Count () == 0) { + return 0; + } + + const LineStruct &line = Lines[Lines.Count () - 1]; + const int line_x = Get_Line_X (line, centered); + int caret_x = line.RightToLeft ? line.Width : 0; + if (line.Analysis != nullptr) { + ::ScriptStringCPtoX (line.Analysis, line.Offset + line.Length - 1, TRUE, &caret_x); + } + return line_x + caret_x; +} diff --git a/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.h b/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.h new file mode 100644 index 00000000000..daa28aff768 --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WW3D2/shapedtext.h @@ -0,0 +1,91 @@ +/* +** 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/always.h" +#include "WWLib/Vector.h" +#include "WWLib/win.h" +#include "usp10_adapter.h" + +// Lays out text with Uniscribe for strings that the per character glyph cache of FontCharsClass +// cannot draw correctly: scripts that join letters or read right-to-left, such as Arabic and +// Persian, characters outside of the Basic Multilingual Plane, such as emoji, and characters that +// the font does not have. Uniscribe takes missing glyphs from fallback fonts of the system. +// +// The text is split into paragraphs at line feeds. Each paragraph reads in the direction of most of +// its strong characters. Text in directional isolates, such as a chat message after a player name, +// keeps its own direction. Paragraphs are wrapped at soft break opportunities, and each line is then +// shaped and reordered on its own. +class ShapedTextClass +{ +public: + ShapedTextClass(); + ~ShapedTextClass(); + + // Returns whether the text needs shaping to be displayed correctly with the font that is + // selected into the device context. + static bool Needs_Shaping( HDC dc, const WCHAR *text ); + + // Lays out the text with the font that is selected into the device context. The device + // context is used again by Draw, so it must stay valid until this object is destroyed. + // Lines are wrapped before they reach the wrap width if it is positive. + bool Layout( HDC dc, const WCHAR *text, int wrap_width, int line_height ); + + // Draws the lines in white into the bitmap that is selected into the device context of Layout. + // Lines of right-to-left paragraphs are aligned to the right unless the text is centered. + bool Draw( bool centered ) const; + + int Get_Width() const { return Width; } + int Get_Height() const { return Lines.Count() * LineHeight; } + int Get_Line_Height() const { return LineHeight; } + + // Returns the distance from the left edge of the text to a caret after the last character. + int Get_End_Caret_X( bool centered ) const; + + // Returns whether the first paragraph reads right-to-left. + bool Is_Right_To_Left() const { return Lines.Count() > 0 && Lines[0].RightToLeft; } + +private: + struct LineStruct + { + SCRIPT_STRING_ANALYSIS Analysis; // Null for an empty line. + int Offset; // Number of reopened embeddings in front of the text of the line. + int Length; + int Width; + bool RightToLeft; + + bool operator== (const LineStruct &src) const { return false; } + bool operator!= (const LineStruct &src) const { return true; } + }; + + void Free(); + WCHAR *Allocate_Text( int length ); + bool Add_Line( const WCHAR *paragraph, int start, int length, bool right_to_left ); + bool Add_Paragraph( const WCHAR *text, int length, int wrap_width ); + int Get_Line_X( const LineStruct &line, bool centered ) const; + + ShapedTextClass( const ShapedTextClass & ); + ShapedTextClass &operator=( const ShapedTextClass & ); + + DynamicVectorClass Lines; + DynamicVectorClass Texts; + HDC DC; + int Width; + int LineHeight; +}; diff --git a/Dependencies/Usp10/CMakeLists.txt b/Dependencies/Usp10/CMakeLists.txt new file mode 100644 index 00000000000..264582a3502 --- /dev/null +++ b/Dependencies/Usp10/CMakeLists.txt @@ -0,0 +1,24 @@ +set(USP10_SRC + Usp10Loader.cpp + Usp10Loader.h + usp10_adapter.h + usp10_subset.h +) + +add_library(deps_usp10 STATIC) + +target_sources(deps_usp10 PRIVATE ${USP10_SRC}) + +target_include_directories(deps_usp10 PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# For WWLib/stringex.h, which includes bittype.h from the same directory. +target_include_directories(deps_usp10 PRIVATE + ${CMAKE_SOURCE_DIR}/Core/Libraries/Source/WWVegas + ${CMAKE_SOURCE_DIR}/Core/Libraries/Source/WWVegas/WWLib +) + +target_link_libraries(deps_usp10 PRIVATE + core_utility +) diff --git a/Dependencies/Usp10/Usp10Loader.cpp b/Dependencies/Usp10/Usp10Loader.cpp new file mode 100644 index 00000000000..d1d83401dd3 --- /dev/null +++ b/Dependencies/Usp10/Usp10Loader.cpp @@ -0,0 +1,257 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "Usp10Loader.h" +#include "usp10_adapter.h" + +#include "WWLib/stringex.h" + + +namespace +{ + +typedef HRESULT (WINAPI *ScriptIsComplex_t)(const WCHAR *pwcInChars, int cInChars, DWORD dwFlags); +typedef HRESULT (WINAPI *ScriptItemize_t)(const WCHAR *pwcInChars, int cInChars, int cMaxItems, + const SCRIPT_CONTROL *psControl, const SCRIPT_STATE *psState, SCRIPT_ITEM *pItems, int *pcItems); +typedef HRESULT (WINAPI *ScriptBreak_t)(const WCHAR *pwcChars, int cChars, const SCRIPT_ANALYSIS *psa, + SCRIPT_LOGATTR *psla); +typedef HRESULT (WINAPI *ScriptLayout_t)(int cRuns, const BYTE *pbLevel, int *piVisualToLogical, + int *piLogicalToVisual); +typedef HRESULT (WINAPI *ScriptStringAnalyse_t)(HDC hdc, const void *pString, int cString, int cGlyphs, + int iCharset, DWORD dwFlags, int iReqWidth, SCRIPT_CONTROL *psControl, SCRIPT_STATE *psState, + const int *piDx, SCRIPT_TABDEF *pTabdef, const BYTE *pbInClass, SCRIPT_STRING_ANALYSIS *pssa); +typedef HRESULT (WINAPI *ScriptStringFree_t)(SCRIPT_STRING_ANALYSIS *pssa); +typedef const SIZE *(WINAPI *ScriptString_pSize_t)(SCRIPT_STRING_ANALYSIS ssa); +typedef HRESULT (WINAPI *ScriptStringOut_t)(SCRIPT_STRING_ANALYSIS ssa, int iX, int iY, UINT uOptions, + const RECT *prc, int iMinSel, int iMaxSel, BOOL fDisabled); +typedef const SCRIPT_LOGATTR *(WINAPI *ScriptString_pLogAttr_t)(SCRIPT_STRING_ANALYSIS ssa); +typedef HRESULT (WINAPI *ScriptStringGetLogicalWidths_t)(SCRIPT_STRING_ANALYSIS ssa, int *piDx); +typedef HRESULT (WINAPI *ScriptStringCPtoX_t)(SCRIPT_STRING_ANALYSIS ssa, int icp, BOOL fTrailing, int *pX); + +ScriptIsComplex_t ScriptIsComplexPtr = nullptr; +ScriptItemize_t ScriptItemizePtr = nullptr; +ScriptBreak_t ScriptBreakPtr = nullptr; +ScriptLayout_t ScriptLayoutPtr = nullptr; +ScriptStringAnalyse_t ScriptStringAnalysePtr = nullptr; +ScriptStringFree_t ScriptStringFreePtr = nullptr; +ScriptString_pSize_t ScriptString_pSizePtr = nullptr; +ScriptStringOut_t ScriptStringOutPtr = nullptr; +ScriptString_pLogAttr_t ScriptString_pLogAttrPtr = nullptr; +ScriptStringGetLogicalWidths_t ScriptStringGetLogicalWidthsPtr = nullptr; +ScriptStringCPtoX_t ScriptStringCPtoXPtr = nullptr; + +HMODULE Module = HMODULE(nullptr); +int ReferenceCount = 0; +bool Failed = false; +unsigned long LastError = 0; + +#define USP10_RESOLVE(name) \ + name##Ptr = reinterpret_cast(::GetProcAddress(Module, #name)) + +static bool resolveAll() +{ + USP10_RESOLVE(ScriptIsComplex); + USP10_RESOLVE(ScriptItemize); + USP10_RESOLVE(ScriptBreak); + USP10_RESOLVE(ScriptLayout); + USP10_RESOLVE(ScriptStringAnalyse); + USP10_RESOLVE(ScriptStringFree); + USP10_RESOLVE(ScriptString_pSize); + USP10_RESOLVE(ScriptStringOut); + USP10_RESOLVE(ScriptString_pLogAttr); + USP10_RESOLVE(ScriptStringGetLogicalWidths); + USP10_RESOLVE(ScriptStringCPtoX); + + return ScriptIsComplexPtr != nullptr && ScriptItemizePtr != nullptr && ScriptBreakPtr != nullptr && + ScriptLayoutPtr != nullptr && ScriptStringAnalysePtr != nullptr && ScriptStringFreePtr != nullptr && + ScriptString_pSizePtr != nullptr && ScriptStringOutPtr != nullptr && ScriptString_pLogAttrPtr != nullptr && + ScriptStringGetLogicalWidthsPtr != nullptr && ScriptStringCPtoXPtr != nullptr; +} +#undef USP10_RESOLVE + +static void freeResources() +{ + if (Module != HMODULE(nullptr)) { + ::FreeLibrary(Module); + Module = HMODULE(nullptr); + } + + ScriptIsComplexPtr = nullptr; + ScriptItemizePtr = nullptr; + ScriptBreakPtr = nullptr; + ScriptLayoutPtr = nullptr; + ScriptStringAnalysePtr = nullptr; + ScriptStringFreePtr = nullptr; + ScriptString_pSizePtr = nullptr; + ScriptStringOutPtr = nullptr; + ScriptString_pLogAttrPtr = nullptr; + ScriptStringGetLogicalWidthsPtr = nullptr; + ScriptStringCPtoXPtr = nullptr; +} + +} // namespace + + +bool Usp10Loader::isLoaded() +{ + return Module != HMODULE(nullptr); +} + + +bool Usp10Loader::isFailed() +{ + return Failed; +} + + +unsigned long Usp10Loader::getLastError() +{ + return LastError; +} + + +bool Usp10Loader::load() +{ + // Always increment the reference count. + ++ReferenceCount; + + // Optimization: return early if it failed before. + if (Failed) + return false; + + // Return early if someone else already loaded it. + if (ReferenceCount > 1) + return true; + + // Load usp10.dll from the system directory only. + char dll_path[MAX_PATH]; + const UINT path_length = ::GetSystemDirectoryA(dll_path, sizeof(dll_path)); + if (path_length == 0 || path_length >= sizeof(dll_path) || + strlcat(dll_path, "\\usp10.dll", sizeof(dll_path)) >= sizeof(dll_path)) { + LastError = ERROR_BUFFER_OVERFLOW; + Failed = true; + return false; + } + + Module = ::LoadLibraryA(dll_path); + if (Module == HMODULE(nullptr)) { + LastError = ::GetLastError(); + Failed = true; + return false; + } + + if (!resolveAll()) { + freeResources(); + LastError = ERROR_PROC_NOT_FOUND; + Failed = true; + return false; + } + + return true; +} + + +void Usp10Loader::unload() +{ + if (ReferenceCount > 0) + --ReferenceCount; + + if (ReferenceCount > 0) + return; + + freeResources(); + Failed = false; + LastError = 0; +} + + +// The Uniscribe functions below stand in for the imports of usp10.dll. + +HRESULT WINAPI ScriptIsComplex(const WCHAR *pwcInChars, int cInChars, DWORD dwFlags) +{ + return ScriptIsComplexPtr != nullptr ? ScriptIsComplexPtr(pwcInChars, cInChars, dwFlags) : E_FAIL; +} + +HRESULT WINAPI ScriptItemize(const WCHAR *pwcInChars, int cInChars, int cMaxItems, + const SCRIPT_CONTROL *psControl, const SCRIPT_STATE *psState, SCRIPT_ITEM *pItems, int *pcItems) +{ + if (ScriptItemizePtr != nullptr) + return ScriptItemizePtr(pwcInChars, cInChars, cMaxItems, psControl, psState, pItems, pcItems); + + if (pcItems != nullptr) + *pcItems = 0; + return E_FAIL; +} + +HRESULT WINAPI ScriptBreak(const WCHAR *pwcChars, int cChars, const SCRIPT_ANALYSIS *psa, SCRIPT_LOGATTR *psla) +{ + return ScriptBreakPtr != nullptr ? ScriptBreakPtr(pwcChars, cChars, psa, psla) : E_FAIL; +} + +HRESULT WINAPI ScriptLayout(int cRuns, const BYTE *pbLevel, int *piVisualToLogical, int *piLogicalToVisual) +{ + return ScriptLayoutPtr != nullptr + ? ScriptLayoutPtr(cRuns, pbLevel, piVisualToLogical, piLogicalToVisual) + : E_FAIL; +} + +HRESULT WINAPI ScriptStringAnalyse(HDC hdc, const void *pString, int cString, int cGlyphs, int iCharset, + DWORD dwFlags, int iReqWidth, SCRIPT_CONTROL *psControl, SCRIPT_STATE *psState, const int *piDx, + SCRIPT_TABDEF *pTabdef, const BYTE *pbInClass, SCRIPT_STRING_ANALYSIS *pssa) +{ + if (ScriptStringAnalysePtr != nullptr) + return ScriptStringAnalysePtr(hdc, pString, cString, cGlyphs, iCharset, dwFlags, iReqWidth, psControl, + psState, piDx, pTabdef, pbInClass, pssa); + + if (pssa != nullptr) + *pssa = nullptr; + return E_FAIL; +} + +HRESULT WINAPI ScriptStringFree(SCRIPT_STRING_ANALYSIS *pssa) +{ + return ScriptStringFreePtr != nullptr ? ScriptStringFreePtr(pssa) : E_FAIL; +} + +const SIZE *WINAPI ScriptString_pSize(SCRIPT_STRING_ANALYSIS ssa) +{ + return ScriptString_pSizePtr != nullptr ? ScriptString_pSizePtr(ssa) : nullptr; +} + +HRESULT WINAPI ScriptStringOut(SCRIPT_STRING_ANALYSIS ssa, int iX, int iY, UINT uOptions, const RECT *prc, + int iMinSel, int iMaxSel, BOOL fDisabled) +{ + return ScriptStringOutPtr != nullptr + ? ScriptStringOutPtr(ssa, iX, iY, uOptions, prc, iMinSel, iMaxSel, fDisabled) + : E_FAIL; +} + +const SCRIPT_LOGATTR *WINAPI ScriptString_pLogAttr(SCRIPT_STRING_ANALYSIS ssa) +{ + return ScriptString_pLogAttrPtr != nullptr ? ScriptString_pLogAttrPtr(ssa) : nullptr; +} + +HRESULT WINAPI ScriptStringGetLogicalWidths(SCRIPT_STRING_ANALYSIS ssa, int *piDx) +{ + return ScriptStringGetLogicalWidthsPtr != nullptr ? ScriptStringGetLogicalWidthsPtr(ssa, piDx) : E_FAIL; +} + +HRESULT WINAPI ScriptStringCPtoX(SCRIPT_STRING_ANALYSIS ssa, int icp, BOOL fTrailing, int *pX) +{ + return ScriptStringCPtoXPtr != nullptr ? ScriptStringCPtoXPtr(ssa, icp, fTrailing, pX) : E_FAIL; +} diff --git a/Dependencies/Usp10/Usp10Loader.h b/Dependencies/Usp10/Usp10Loader.h new file mode 100644 index 00000000000..f1b384ee398 --- /dev/null +++ b/Dependencies/Usp10/Usp10Loader.h @@ -0,0 +1,46 @@ +/* +** 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 + +// This static class loads and unloads usp10.dll from the system directory at runtime. +// +// The Uniscribe functions declared in usp10_adapter.h are implemented in this class and forward +// to the matching export of the loaded module, so they can be called as if usp10.dll was linked +// statically. When the module is not loaded, every Uniscribe function returns E_FAIL, or a null +// pointer for ScriptString_pSize and ScriptString_pLogAttr. +// +// Is not thread safe. Every call to load needs a paired call to unload, no matter if the load +// was successful, and both are expected to be called while no Uniscribe function is in flight. + +class Usp10Loader +{ +public: + + // Returns whether usp10.dll is loaded + static bool isLoaded(); + + // Returns whether usp10.dll was attempted to be loaded but failed + static bool isFailed(); + + // Returns the system error code of the failed load attempt + static unsigned long getLastError(); + + static bool load(); + static void unload(); +}; diff --git a/Dependencies/Usp10/usp10_adapter.h b/Dependencies/Usp10/usp10_adapter.h new file mode 100644 index 00000000000..417d3fc7610 --- /dev/null +++ b/Dependencies/Usp10/usp10_adapter.h @@ -0,0 +1,38 @@ +/* +** 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 . +*/ + +// This file includes the Uniscribe API of usp10.h. It uses the header of the Windows SDK if the +// compiler can find it, and otherwise a subset of it for compilers that do not have it, such as VC6. +// The functions are implemented by Usp10Loader.cpp. + +#pragma once + +#if defined(__has_include) + #if __has_include() + #define HAVE_USP10_H 1 + #endif +#endif + +#ifdef HAVE_USP10_H + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include +#else + #include "usp10_subset.h" +#endif diff --git a/Dependencies/Usp10/usp10_subset.h b/Dependencies/Usp10/usp10_subset.h new file mode 100644 index 00000000000..e929ea80632 --- /dev/null +++ b/Dependencies/Usp10/usp10_subset.h @@ -0,0 +1,132 @@ +/* +** 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 . +*/ + +// Subset of the Uniscribe API of the Windows SDK usp10.h, for compilers that do not have it, such as VC6. +// Is included by usp10_adapter.h, which should be used instead of this file. + +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define SSA_FALLBACK 0x00000020 +#define SSA_BREAK 0x00000040 +#define SSA_GLYPHS 0x00000080 +#define SSA_RTL 0x00000100 +#define SSA_LINK 0x00001000 + +#define SIC_COMPLEX 1 + +typedef struct tag_SCRIPT_CONTROL { + DWORD uDefaultLanguage :16; + DWORD fContextDigits :1; + DWORD fInvertPreBoundDir :1; + DWORD fInvertPostBoundDir :1; + DWORD fLinkStringBefore :1; + DWORD fLinkStringAfter :1; + DWORD fNeutralOverride :1; + DWORD fNumericOverride :1; + DWORD fLegacyBidiClass :1; + DWORD fMergeNeutralItems :1; + DWORD fUseStandardBidi :1; + DWORD fReserved :6; +} SCRIPT_CONTROL; + +typedef struct tag_SCRIPT_STATE { + WORD uBidiLevel :5; + WORD fOverrideDirection :1; + WORD fInhibitSymSwap :1; + WORD fCharShape :1; + WORD fDigitSubstitute :1; + WORD fInhibitLigate :1; + WORD fDisplayZWG :1; + WORD fArabicNumContext :1; + WORD fGcpClusters :1; + WORD fReserved :1; + WORD fEngineReserved :2; +} SCRIPT_STATE; + +typedef struct tag_SCRIPT_ANALYSIS { + WORD eScript :10; + WORD fRTL :1; + WORD fLayoutRTL :1; + WORD fLinkBefore :1; + WORD fLinkAfter :1; + WORD fLogicalOrder :1; + WORD fNoGlyphIndex :1; + SCRIPT_STATE s; +} SCRIPT_ANALYSIS; + +typedef struct tag_SCRIPT_ITEM { + int iCharPos; + SCRIPT_ANALYSIS a; +} SCRIPT_ITEM; + +typedef struct tag_SCRIPT_LOGATTR { + BYTE fSoftBreak :1; + BYTE fWhiteSpace :1; + BYTE fCharStop :1; + BYTE fWordStop :1; + BYTE fInvalid :1; + BYTE fReserved :3; +} SCRIPT_LOGATTR; + +typedef struct tag_SCRIPT_TABDEF { + int cTabStops; + int iScale; + int *pTabStops; + int iTabOrigin; +} SCRIPT_TABDEF; + +typedef void *SCRIPT_STRING_ANALYSIS; + +HRESULT WINAPI ScriptIsComplex(const WCHAR *pwcInChars, int cInChars, DWORD dwFlags); + +HRESULT WINAPI ScriptItemize(const WCHAR *pwcInChars, int cInChars, int cMaxItems, + const SCRIPT_CONTROL *psControl, const SCRIPT_STATE *psState, SCRIPT_ITEM *pItems, int *pcItems); + +HRESULT WINAPI ScriptBreak(const WCHAR *pwcChars, int cChars, const SCRIPT_ANALYSIS *psa, SCRIPT_LOGATTR *psla); + +HRESULT WINAPI ScriptLayout(int cRuns, const BYTE *pbLevel, int *piVisualToLogical, int *piLogicalToVisual); + +HRESULT WINAPI ScriptStringAnalyse(HDC hdc, const void *pString, int cString, int cGlyphs, int iCharset, + DWORD dwFlags, int iReqWidth, SCRIPT_CONTROL *psControl, SCRIPT_STATE *psState, const int *piDx, + SCRIPT_TABDEF *pTabdef, const BYTE *pbInClass, SCRIPT_STRING_ANALYSIS *pssa); + +HRESULT WINAPI ScriptStringFree(SCRIPT_STRING_ANALYSIS *pssa); + +const SIZE *WINAPI ScriptString_pSize(SCRIPT_STRING_ANALYSIS ssa); + +HRESULT WINAPI ScriptStringOut(SCRIPT_STRING_ANALYSIS ssa, int iX, int iY, UINT uOptions, const RECT *prc, + int iMinSel, int iMaxSel, BOOL fDisabled); + +const SCRIPT_LOGATTR *WINAPI ScriptString_pLogAttr(SCRIPT_STRING_ANALYSIS ssa); + +HRESULT WINAPI ScriptStringGetLogicalWidths(SCRIPT_STRING_ANALYSIS ssa, int *piDx); + +HRESULT WINAPI ScriptStringCPtoX(SCRIPT_STRING_ANALYSIS ssa, int icp, BOOL fTrailing, int *pX); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp index 07e13867223..f670097012d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp @@ -8,6 +8,7 @@ #include "../OnlineServices_Init.h" #include "../HTTP/HTTPManager.h" #include "GameNetwork/GameSpy/PeerDefs.h" +#include "GameClient/DisplayString.h" // one clock for every websocket timestamp; utc_clock also loads the time zone database on MSVC static int64_t NowMs() @@ -1023,8 +1024,14 @@ void WebSocket::Tick() SYSTEMTIME systemTime; GetLocalTime(&systemTime); + // TheSuperHackers @feature Keep the reading order of the name and the message + // when either contains right-to-left text. Actions have no brackets to split at. + UnicodeString message(from_utf8(chatData.message).c_str()); + if (!chatData.action) + message = IsolateBidiChatLine(message); + UnicodeString unicodeStr; - unicodeStr.format(L"[%2.2d:%2.2d] %s", systemTime.wHour, systemTime.wMinute, from_utf8(chatData.message).c_str()); + unicodeStr.format(L"[%2.2d:%2.2d] %s", systemTime.wHour, systemTime.wMinute, message.str()); Color color = DetermineColorForChatMessage(EChatMessageType::CHAT_MESSAGE_TYPE_NETWORK_ROOM, true, chatData.action, chatData.admin, chatData.name_change); @@ -1427,6 +1434,10 @@ void WebSocket::Tick() if (bParsed) { UnicodeString unicodeStr(from_utf8(chatData.message).c_str()); + // TheSuperHackers @feature Keep the reading order of the name and the message + // when either contains right-to-left text. + if (!chatData.action && !chatData.announcement) + unicodeStr = IsolateBidiChatLine(unicodeStr); NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); if (pLobbyInterface != nullptr) diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h index 0d49e002dab..af576fb9da9 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplayString.h @@ -79,6 +79,8 @@ 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 Int getCaretOffset() override; + virtual Bool isRightToLeft() 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 @@ -92,11 +94,17 @@ class W3DDisplayString : public DisplayString void usingResources( UnsignedInt frame ); /**< call this whenever display resources are in use */ void computeExtents(); ///< compupte text width and height + void computeShapedMetrics(); ///< measure text that is shaped as a whole 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_shapedMetricsChanged; ///< when the shaped metrics need to be measured again + Bool m_isShaped; ///< TRUE if the text is shaped as a whole + Bool m_isRightToLeft; ///< TRUE if shaped text starts with a right-to-left paragraph + Int m_shapedWidth; ///< width of the widest line of shaped text + Int m_shapedCaretOffset; ///< offset of a caret after the last character of shaped text UnicodeString m_hotkey; ///< holds the current hotkey marker. Bool m_useHotKey; ICoord2D m_hotKeyPos; diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp index c8a4b78ea59..a595be0bb90 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplayString.cpp @@ -86,6 +86,11 @@ W3DDisplayString::W3DDisplayString() m_size.x = 0; m_size.y = 0; m_fontChanged = FALSE; + m_shapedMetricsChanged = FALSE; + m_isShaped = FALSE; + m_isRightToLeft = FALSE; + m_shapedWidth = 0; + m_shapedCaretOffset = 0; m_clipRegion.lo.x = 0; m_clipRegion.lo.y = 0; m_clipRegion.hi.x = 0; @@ -138,6 +143,7 @@ void W3DDisplayString::notifyTextChanged() // we know we must first build the sentence // m_textChanged = TRUE; + m_shapedMetricsChanged = TRUE; // reset data for our text renderer m_textRenderer.Reset(); @@ -189,6 +195,9 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr } + // TheSuperHackers @feature Shaped text has no hot key position, so its hot key is not highlighted. + const Bool drawHotKey = m_useHotKey && !m_textRenderer.Is_Shaped_Sentence(); + // // if our position has changed, or our colors have changed, or our // text data has changed, we need to redo the texture quads @@ -217,7 +226,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 (drawHotKey) { m_textRendererHotKey.Reset_Polys(); m_textRendererHotKey.Set_Location( Vector2( m_textPos.x + m_hotKeyPos.x , m_textPos.y +m_hotKeyPos.y) ); @@ -228,7 +237,7 @@ void W3DDisplayString::draw( Int x, Int y, Color color, Color dropColor, Int xDr TheDisplay->flush(); - if (m_useHotKey) + if (drawHotKey) { m_textRendererHotKey.Render(); } @@ -268,6 +277,13 @@ Int W3DDisplayString::getWidth( Int charPos ) font = m_textRenderer.Peek_Font(); + if ( font && charPos == -1 ) + { + computeShapedMetrics(); + if ( m_isShaped ) + return m_shapedWidth; + } + if ( font ) { const WideChar *text = m_textString.str(); @@ -286,6 +302,57 @@ Int W3DDisplayString::getWidth( Int charPos ) return width; } +// W3DDisplayString::getCaretOffset =========================================== +/** Get the horizontal offset of a caret after the last character. Shaped + * text can end on its left side when it reads right-to-left. */ +//============================================================================= +Int W3DDisplayString::getCaretOffset() +{ + computeShapedMetrics(); + if ( m_isShaped ) + return m_shapedCaretOffset; + + return getWidth(); +} + +// W3DDisplayString::isRightToLeft ============================================ +/** TRUE if the text starts with a right-to-left paragraph */ +//============================================================================= +Bool W3DDisplayString::isRightToLeft() +{ + computeShapedMetrics(); + return m_isShaped && m_isRightToLeft; +} + +// W3DDisplayString::computeShapedMetrics ===================================== +/** Measure text that is shaped as a whole, once for every change of it */ +//============================================================================= +void W3DDisplayString::computeShapedMetrics() +{ + if ( !m_shapedMetricsChanged ) + return; + + m_shapedMetricsChanged = FALSE; + m_isShaped = FALSE; + m_isRightToLeft = FALSE; + m_shapedWidth = 0; + m_shapedCaretOffset = 0; + + if ( getTextLength() == 0 || m_font == nullptr ) + return; + + int width = 0; + int caretOffset = 0; + bool rightToLeft = false; + if ( m_textRenderer.Get_Shaped_Text_Metrics( getText().str(), &width, &caretOffset, &rightToLeft ) ) + { + m_isShaped = TRUE; + m_isRightToLeft = rightToLeft; + m_shapedWidth = width; + m_shapedCaretOffset = caretOffset; + } +} + // W3DDisplayString::setFont ================================================== /** Set the font for this particular display string */ //============================================================================= @@ -315,6 +382,7 @@ void W3DDisplayString::setFont( GameFont *font ) // set flag telling us the font has changed since last render m_fontChanged = TRUE; + m_shapedMetricsChanged = TRUE; }