diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp index 6551e88892b..ffe68d5dd7c 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp @@ -109,6 +109,32 @@ FontCharsClass *LoadUnicodeFallbackFont(Int size, Bool bold, const char *base_na return nullptr; } + +// GeneralsX @feature felipebraz 26/09/2026 Load Font Awesome Brands as dedicated brand icon fallback font +FontCharsClass *LoadBrandFallbackFont(Int size, Bool bold, const char *base_name) +{ + static const char *kBrandFonts[] = { + "fa-brands-400", + "Font Awesome 6 Brands", + nullptr + }; + + for (int i = 0; kBrandFonts[i]; ++i) { + if (base_name != nullptr && strcmp(kBrandFonts[i], base_name) == 0) + continue; + + FontCharsClass *font = WW3DAssetManager::Get_Instance()->Get_FontChars(kBrandFonts[i], size, bold); + if (font != nullptr) { + // GeneralsX @bugfix felipebraz 27/09/2026 Ensure font has the required OS brand glyphs (Apple, Windows, Linux) + if (font->Has_Glyph(0xF179) && font->Has_Glyph(0xF17A) && font->Has_Glyph(0xF17C)) { + return font; + } + font->Release_Ref(); + } + } + + return nullptr; +} } // DEFINES //////////////////////////////////////////////////////////////////// @@ -163,6 +189,8 @@ Bool W3DFontLibrary::loadFontData( GameFont *font ) "Arial Unicode MS", "Arial Unicode", "DejaVu Sans", + "fa-brands-400", + "Font Awesome 6 Brands", nullptr }; for (int i = 0; kFullCoverageFonts[i]; i++) { @@ -174,6 +202,12 @@ Bool W3DFontLibrary::loadFontData( GameFont *font ) if (!skipFallback) { fontChar->AlternateUnicodeFont = LoadUnicodeFallbackFont(size, bold, name); } + + // GeneralsX @feature felipebraz 26/09/2026 Provide brand glyph fallback for UI fonts + bool isBrandFont = (name != nullptr && (strcmp(name, "fa-brands-400") == 0 || strcmp(name, "Font Awesome 6 Brands") == 0)); + if (!isBrandFont) { + fontChar->AlternateBrandFont = LoadBrandFallbackFont(size, bold, name); + } } return TRUE; @@ -189,6 +223,8 @@ void W3DFontLibrary::releaseFontData( GameFont *font ) // the W3D asset manager which is all taken for of us if (font && font->fontData) { + if(((FontCharsClass *)(font->fontData))->AlternateBrandFont) + ((FontCharsClass *)(font->fontData))->AlternateBrandFont->Release_Ref(); if(((FontCharsClass *)(font->fontData))->AlternateUnicodeFont) ((FontCharsClass *)(font->fontData))->AlternateUnicodeFont->Release_Ref(); ((FontCharsClass *)(font->fontData))->Release_Ref(); diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp index c396aa1ce44..ab401f04772 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp @@ -1182,6 +1182,7 @@ FontCharsClass::FontCharsClass () : IsBold (false) { AlternateUnicodeFont = nullptr; + AlternateBrandFont = nullptr; ::memset( ASCIICharArray, 0, sizeof (ASCIICharArray) ); } @@ -1225,6 +1226,11 @@ FontCharsClass::Get_Char_Data (WCHAR ch) { retval = ASCIICharArray[normalized_char]; } + // GeneralsX @feature felipebraz 26/09/2026 Route Private Use Area (Font Awesome Brand icons) to brand font + else if ( normalized_char >= 0xE000 && normalized_char <= 0xF8FF && AlternateBrandFont && this != AlternateBrandFont ) + { + return AlternateBrandFont->Get_Char_Data( glyph ); + } else if ( AlternateUnicodeFont && this != AlternateUnicodeFont ) { return AlternateUnicodeFont->Get_Char_Data( glyph ); @@ -1260,6 +1266,16 @@ FontCharsClass::Get_Char_Data (WCHAR ch) int FontCharsClass::Get_Char_Width (WCHAR ch) { + const uint16 normalized_char = static_cast(ch); + const WCHAR glyph = static_cast(normalized_char); + + if ( normalized_char >= 0xE000 && normalized_char <= 0xF8FF && AlternateBrandFont && this != AlternateBrandFont ) { + return AlternateBrandFont->Get_Char_Width( glyph ); + } + if ( normalized_char >= 256 && AlternateUnicodeFont && this != AlternateUnicodeFont ) { + return AlternateUnicodeFont->Get_Char_Width( glyph ); + } + const FontCharsClassCharDataStruct * data = Get_Char_Data( ch ); if ( data != nullptr ) { return data->Width; @@ -1277,6 +1293,16 @@ FontCharsClass::Get_Char_Width (WCHAR ch) int FontCharsClass::Get_Char_Spacing (WCHAR ch) { + const uint16 normalized_char = static_cast(ch); + const WCHAR glyph = static_cast(normalized_char); + + if ( normalized_char >= 0xE000 && normalized_char <= 0xF8FF && AlternateBrandFont && this != AlternateBrandFont ) { + return AlternateBrandFont->Get_Char_Spacing( glyph ); + } + if ( normalized_char >= 256 && AlternateUnicodeFont && this != AlternateUnicodeFont ) { + return AlternateUnicodeFont->Get_Char_Spacing( glyph ); + } + const FontCharsClassCharDataStruct * data = Get_Char_Data( ch ); if ( data != nullptr ) { if ( data->Width != 0 ) { @@ -1296,6 +1322,18 @@ FontCharsClass::Get_Char_Spacing (WCHAR ch) void FontCharsClass::Blit_Char (WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, int y) { + const uint16 normalized_char = static_cast(ch); + const WCHAR glyph = static_cast(normalized_char); + + if ( normalized_char >= 0xE000 && normalized_char <= 0xF8FF && AlternateBrandFont && this != AlternateBrandFont ) { + AlternateBrandFont->Blit_Char( glyph, dest_ptr, dest_stride, x, y ); + return; + } + if ( normalized_char >= 256 && AlternateUnicodeFont && this != AlternateUnicodeFont ) { + AlternateUnicodeFont->Blit_Char( glyph, dest_ptr, dest_stride, x, y ); + return; + } + const FontCharsClassCharDataStruct * data = Get_Char_Data( ch ); if ( data != nullptr && data->Width != 0 ) { @@ -1324,6 +1362,35 @@ FontCharsClass::Blit_Char (WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, i } +//////////////////////////////////////////////////////////////////////////////////// +// +// Has_Glyph +// +// GeneralsX @feature felipebraz 27/09/2026 Test if font contains a valid glyph mapping +//////////////////////////////////////////////////////////////////////////////////// +bool +FontCharsClass::Has_Glyph (WCHAR ch) const +{ +#if defined(_WIN32) + if ( MemDC == nullptr ) { + return false; + } + WORD glyph_index = 0; + if ( ::GetGlyphIndicesW( MemDC, &ch, 1, &glyph_index, GGI_MARK_NONEXISTING_GLYPHS ) == GDI_ERROR ) { + return false; + } + return glyph_index != 0xFFFF; +#elif defined(SAGE_USE_FREETYPE) + if ( FTFace == nullptr ) { + return false; + } + return FT_Get_Char_Index( FTFace, ch ) != 0; +#else + return false; +#endif +} + + //////////////////////////////////////////////////////////////////////////////////// // // Update_Current_Buffer @@ -1719,6 +1786,13 @@ FontCharsClass::Locate_Font_FontConfig (const char *font_name) strcmp( normalized, "liberationmono" ) == 0 || strcmp( normalized, "liberationmonobold" ) == 0); + // GeneralsX @feature felipebraz 26/09/2026 Font Awesome 6 Brands font resolution + bool is_fa_brands = (strcmp( normalized, "fontawesome6brands" ) == 0 || + strcmp( normalized, "fontawesomebrands" ) == 0 || + strcmp( normalized, "fabrands400" ) == 0 || + strcmp( normalized, "fabrands" ) == 0 || + strcmp( normalized, "fontawesome" ) == 0); + if ( is_arial ) { if ( is_bold ) { candidates[candidate_count++] = "LiberationSans-Bold"; @@ -1743,6 +1817,11 @@ FontCharsClass::Locate_Font_FontConfig (const char *font_name) candidates[candidate_count++] = "LiberationMono-Regular"; candidates[candidate_count++] = "liberationmono-regular"; candidates[candidate_count++] = "LiberationMono"; + } else if ( is_fa_brands ) { + candidates[candidate_count++] = "fa-brands-400"; + candidates[candidate_count++] = "fa-brands-400.ttf"; + candidates[candidate_count++] = "FontAwesome6Brands-Regular"; + candidates[candidate_count++] = "Font Awesome 6 Brands"; } // @@ -1769,14 +1848,31 @@ FontCharsClass::Locate_Font_FontConfig (const char *font_name) FcResult result = FcResultNoMatch; FcPattern *font = FcFontMatch( config, pattern, &result ); if ( font != nullptr && result == FcResultMatch ) { - FcChar8 *file_path = nullptr; - if ( FcPatternGetString( font, FC_FILE, 0, &file_path ) == FcResultMatch && file_path != nullptr ) { - if ( Platform::IsFileReadable( (const char*)file_path ) ) { - FreetypeFontPath = (const char*)file_path; - FcPatternDestroy( font ); - FcPatternDestroy( pattern ); - FcConfigDestroy( config ); - return FreetypeFontPath; + // GeneralsX @bugfix felipebraz 27/09/2026 Guard brand queries against generic sans fallbacks from FcFontMatch + bool match_valid = true; + if ( is_fa_brands ) { + FcChar8 *family = nullptr; + if ( FcPatternGetString( font, FC_FAMILY, 0, &family ) == FcResultMatch && family != nullptr ) { + if ( strstr( (const char*)family, "Font Awesome" ) == nullptr && + strstr( (const char*)family, "FontAwesome" ) == nullptr && + strstr( (const char*)family, "Brands" ) == nullptr ) { + match_valid = false; + } + } else { + match_valid = false; + } + } + + if ( match_valid ) { + FcChar8 *file_path = nullptr; + if ( FcPatternGetString( font, FC_FILE, 0, &file_path ) == FcResultMatch && file_path != nullptr ) { + if ( Platform::IsFileReadable( (const char*)file_path ) ) { + FreetypeFontPath = (const char*)file_path; + FcPatternDestroy( font ); + FcPatternDestroy( pattern ); + FcConfigDestroy( config ); + return FreetypeFontPath; + } } } FcPatternDestroy( font ); @@ -2003,6 +2099,10 @@ FontCharsClass::Store_Freetype_Char (WCHAR ch) const int skip_cols = (glyph->bitmap_left < 0) ? -glyph->bitmap_left : 0; const int max_cols = char_width - x_offset; for ( unsigned int row = 0; row < glyph->bitmap.rows; row++ ) { + // GeneralsX @bugfix felipebraz 26/09/2026 Guard row write bounds against CharHeight + if ( (y_offset + static_cast(row)) >= CharHeight ) { + break; + } int src_index = row * glyph->bitmap.pitch; int dst_index = (y_offset + row) * char_width; diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h index a122acd5704..c588d9a04fb 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h @@ -99,6 +99,8 @@ class FontCharsClass : public RefCountClass // TR: Hack for unicode font support FontCharsClass *AlternateUnicodeFont; + // GeneralsX @feature felipebraz 26/09/2026 Fallback font for brand/symbolic glyphs (Font Awesome) + FontCharsClass *AlternateBrandFont; bool Initialize_GDI_Font( const char *font_name, int point_size, bool is_bold ); @@ -112,6 +114,8 @@ class FontCharsClass : public RefCountClass int Get_Extra_Overlap() {return PixelOverlap;} void Blit_Char( WCHAR ch, uint16 *dest_ptr, int dest_stride, int x, int y ); + // GeneralsX @feature felipebraz 27/09/2026 Test if font contains a valid glyph mapping + bool Has_Glyph( WCHAR ch ) const; private: diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index 05ef0610a72..606b99572c6 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -223,6 +223,8 @@ void SetLobbyAttemptHostJoin(Bool start) // Tooltips ------------------------------------------------------------------------------- +static UnicodeString FormatPlayerNameWithOSIcon(const AsciiString& rawName); + static void playerTooltip(GameWindow *window, WinInstanceData *instData, UnsignedInt mouse) @@ -240,11 +242,57 @@ static void playerTooltip(GameWindow *window, } UnicodeString uName = GadgetListBoxGetText(window, row, 2); - AsciiString aName; - aName.translate(uName); - PlayerInfoMap::iterator it = TheGameSpyInfo->getPlayerInfoMap()->find(aName); - PlayerInfo *info = &(it->second); + // GeneralsX @bugfix felipebraz 27/09/2026 Resolve player tooltip by profile ID first, then fallback to name + PlayerInfoMap *playerMap = TheGameSpyInfo ? TheGameSpyInfo->getPlayerInfoMap() : nullptr; + if (!playerMap) + { + TheMouse->setCursorTooltip(UnicodeString::TheEmptyString); + return; + } + + PlayerInfo *info = nullptr; + Int profileID = static_cast(reinterpret_cast(GadgetListBoxGetItemData(window, row, 0))); + if (profileID != 0) + { + for (PlayerInfoMap::iterator pIt = playerMap->begin(); pIt != playerMap->end(); ++pIt) + { + if (pIt->second.m_profileID == profileID) + { + info = &(pIt->second); + break; + } + } + } + if (info == nullptr) + { + // GeneralsX @bugfix felipebraz 27/09/2026 Match against formatted name for profile-ID-zero players with OS tags + for (PlayerInfoMap::iterator pIt = playerMap->begin(); pIt != playerMap->end(); ++pIt) + { + if (FormatPlayerNameWithOSIcon(pIt->second.m_name).compare(uName) == 0) + { + info = &(pIt->second); + break; + } + } + } + if (info == nullptr) + { + AsciiString aName; + aName.translate(uName); + PlayerInfoMap::iterator it = playerMap->find(aName); + if (it != playerMap->end()) + { + info = &(it->second); + } + } + + if (info == nullptr) + { + TheMouse->setCursorTooltip(UnicodeString::TheEmptyString); + return; + } + Bool isLocalPlayer = (TheGameSpyInfo->getLocalName().compareNoCase(info->m_name) == 0); if (col == 0) @@ -422,10 +470,61 @@ const Image* LookupSmallRankImage(Int side, Int rankPoints) return img; } +// GeneralsX @feature felipebraz 26/09/2026 Format player display name with Font Awesome OS brand icon +static UnicodeString FormatPlayerNameWithOSIcon(const AsciiString& rawName) +{ + UnicodeString uName; + AsciiString cleanName = rawName; + WideChar osIcon = 0; + + if (cleanName.endsWithNoCase(" [MAC]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 6); + osIcon = 0xF179; // fa-apple + } + else if (cleanName.endsWithNoCase(" [WIN]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 6); + osIcon = 0xF17A; // fa-windows + } + else if (cleanName.endsWithNoCase(" [LNX]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 6); + osIcon = 0xF17C; // fa-linux + } + else if (cleanName.endsWithNoCase("[MAC]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 5); + osIcon = 0xF179; + } + else if (cleanName.endsWithNoCase("[WIN]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 5); + osIcon = 0xF17A; + } + else if (cleanName.endsWithNoCase("[LNX]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 5); + osIcon = 0xF17C; + } + + uName.translate(cleanName); + + if (osIcon != 0) + { + WideChar iconBuf[3]; + iconBuf[0] = ' '; + iconBuf[1] = osIcon; + iconBuf[2] = 0; + uName.concat(iconBuf); + } + + return uName; +} + static Int insertPlayerInListbox(const PlayerInfo& info, Color color) { - UnicodeString uStr; - uStr.translate(info.m_name); + UnicodeString uStr = FormatPlayerNameWithOSIcon(info.m_name); Int currentRank = info.m_rankPoints; Int currentSide = info.m_side; @@ -470,6 +569,7 @@ static Int insertPlayerInListbox(const PlayerInfo& info, Color color) Int index = GadgetListBoxAddEntryImage(listboxLobbyPlayers, preorderImg, -1, 0, w, h); GadgetListBoxAddEntryImage(listboxLobbyPlayers, rankImg, index, 1, w, h); GadgetListBoxAddEntryText(listboxLobbyPlayers, uStr, color, index, 2); + GadgetListBoxSetItemData(listboxLobbyPlayers, (void*)(intptr_t)info.m_profileID, index); return index; } @@ -490,6 +590,7 @@ void PopulateLobbyPlayerListbox() Int maxSelectedItems = GadgetListBoxGetNumEntries(listboxLobbyPlayers); Int *selectedIndices; GadgetListBoxGetSelected(listboxLobbyPlayers, (Int *)(&selectedIndices)); + std::set selectedProfileIDs; std::set selectedNames; std::set::const_iterator selIt; std::set indicesToSelect; @@ -503,6 +604,11 @@ void PopulateLobbyPlayerListbox() break; } ++numSelected; + Int profileID = static_cast(reinterpret_cast(GadgetListBoxGetItemData(listboxLobbyPlayers, selectedIndices[i], 0))); + if (profileID != 0) + { + selectedProfileIDs.insert(profileID); + } AsciiString selectedName; uStr = GadgetListBoxGetText(listboxLobbyPlayers, selectedIndices[i], 2); selectedName.translate(uStr); @@ -515,6 +621,16 @@ void PopulateLobbyPlayerListbox() GadgetListBoxReset(listboxLobbyPlayers); + auto shouldSelect = [&](const PlayerInfo& pInfo) -> bool { + if (pInfo.m_profileID != 0 && selectedProfileIDs.find(pInfo.m_profileID) != selectedProfileIDs.end()) + return true; + if (selectedNames.find(pInfo.m_name) != selectedNames.end()) + return true; + AsciiString formattedName; + formattedName.translate(FormatPlayerNameWithOSIcon(pInfo.m_name)); + return selectedNames.find(formattedName) != selectedNames.end(); + }; + // Ops for (it = players->begin(); it != players->end(); ++it) { @@ -523,8 +639,7 @@ void PopulateLobbyPlayerListbox() { Int index = insertPlayerInListbox(info, info.isIgnored()?GameSpyColor[GSCOLOR_PLAYER_IGNORED]:GameSpyColor[GSCOLOR_PLAYER_OWNER]); - selIt = selectedNames.find(info.m_name); - if (selIt != selectedNames.end()) + if (shouldSelect(info)) { DEBUG_LOG(("Marking index %d (%s) to re-select", index, info.m_name.str())); indicesToSelect.insert(index); @@ -541,8 +656,7 @@ void PopulateLobbyPlayerListbox() { Int index = insertPlayerInListbox(info, info.isIgnored()?GameSpyColor[GSCOLOR_PLAYER_IGNORED]:GameSpyColor[GSCOLOR_PLAYER_BUDDY]); - selIt = selectedNames.find(info.m_name); - if (selIt != selectedNames.end()) + if (shouldSelect(info)) { DEBUG_LOG(("Marking index %d (%s) to re-select", index, info.m_name.str())); indicesToSelect.insert(index); @@ -559,8 +673,7 @@ void PopulateLobbyPlayerListbox() { Int index = insertPlayerInListbox(info, info.isIgnored()?GameSpyColor[GSCOLOR_PLAYER_IGNORED]:GameSpyColor[GSCOLOR_PLAYER_NORMAL]); - selIt = selectedNames.find(info.m_name); - if (selIt != selectedNames.end()) + if (shouldSelect(info)) { DEBUG_LOG(("Marking index %d (%s) to re-select", index, info.m_name.str())); indicesToSelect.insert(index); @@ -1723,12 +1836,46 @@ WindowMsgHandledType WOLLobbyMenuSystem( GameWindow *window, UnsignedInt msg, break; } - GPProfile profileID = 0; + // GeneralsX @bugfix felipebraz 27/09/2026 Resolve profile ID and raw player name consistently in right click menu + GPProfile profileID = static_cast(reinterpret_cast(GadgetListBoxGetItemData(control, rc->pos, 0))); + UnicodeString uRowText = GadgetListBoxGetText(control, rc->pos, COLUMN_PLAYERNAME); AsciiString aName; - aName.translate(GadgetListBoxGetText(control, rc->pos, COLUMN_PLAYERNAME)); - PlayerInfoMap::iterator it = TheGameSpyInfo->getPlayerInfoMap()->find(aName); - if (it != TheGameSpyInfo->getPlayerInfoMap()->end()) - profileID = it->second.m_profileID; + PlayerInfoMap *rcPlayerMap = TheGameSpyInfo ? TheGameSpyInfo->getPlayerInfoMap() : nullptr; + PlayerInfo *rcInfo = nullptr; + if (rcPlayerMap != nullptr) + { + if (profileID != 0) + { + for (PlayerInfoMap::iterator pIt = rcPlayerMap->begin(); pIt != rcPlayerMap->end(); ++pIt) + { + if (pIt->second.m_profileID == profileID) + { + rcInfo = &(pIt->second); + break; + } + } + } + if (rcInfo == nullptr) + { + for (PlayerInfoMap::iterator pIt = rcPlayerMap->begin(); pIt != rcPlayerMap->end(); ++pIt) + { + if (FormatPlayerNameWithOSIcon(pIt->second.m_name).compare(uRowText) == 0) + { + rcInfo = &(pIt->second); + profileID = rcInfo->m_profileID; + break; + } + } + } + } + if (rcInfo != nullptr) + { + aName = rcInfo->m_name; + } + else + { + aName.translate(uRowText); + } Bool isBuddy = FALSE; if (profileID <= 0) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index e36614c1558..1b57c339ab2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -319,6 +319,8 @@ void SetLobbyAttemptHostJoin(Bool start) // Tooltips ------------------------------------------------------------------------------- +static UnicodeString FormatPlayerNameWithOSIcon(const AsciiString& rawName); + static void playerTooltip(GameWindow *window, WinInstanceData *instData, UnsignedInt mouse) @@ -474,16 +476,56 @@ static void playerTooltip(GameWindow *window, } #endif - AsciiString aName; - aName.translate(uName); + // GeneralsX @bugfix felipebraz 27/09/2026 Resolve non-NGMP player tooltip by profile ID first, then fallback to name + PlayerInfoMap *playerMap = TheGameSpyInfo ? TheGameSpyInfo->getPlayerInfoMap() : nullptr; + if (!playerMap) + { + TheMouse->setCursorTooltip(UnicodeString::TheEmptyString); + return; + } - PlayerInfoMap::iterator it = TheGameSpyInfo->getPlayerInfoMap()->find(aName); - if (it == TheGameSpyInfo->getPlayerInfoMap()->end()) + PlayerInfo *info = nullptr; + Int profileID = static_cast(reinterpret_cast(GadgetListBoxGetItemData(window, row, 0))); + if (profileID != 0) + { + for (PlayerInfoMap::iterator pIt = playerMap->begin(); pIt != playerMap->end(); ++pIt) + { + if (pIt->second.m_profileID == profileID) + { + info = &(pIt->second); + break; + } + } + } + if (info == nullptr) + { + // GeneralsX @bugfix felipebraz 27/09/2026 Match against formatted name for profile-ID-zero players with OS tags + for (PlayerInfoMap::iterator pIt = playerMap->begin(); pIt != playerMap->end(); ++pIt) + { + if (FormatPlayerNameWithOSIcon(pIt->second.m_name).compare(uName) == 0) + { + info = &(pIt->second); + break; + } + } + } + if (info == nullptr) + { + AsciiString aName; + aName.translate(uName); + PlayerInfoMap::iterator it = playerMap->find(aName); + if (it != playerMap->end()) + { + info = &(it->second); + } + } + + if (info == nullptr) { TheMouse->setCursorTooltip(UnicodeString::TheEmptyString); return; } - PlayerInfo *info = &(it->second); + Bool isLocalPlayer = (TheGameSpyInfo->getLocalName().compareNoCase(info->m_name) == 0); if (col == 0) @@ -694,10 +736,61 @@ const Image* LookupSmallRankImage(Int side, Int rankPoints) return img; } +// GeneralsX @feature felipebraz 26/09/2026 Format player display name with Font Awesome OS brand icon +static UnicodeString FormatPlayerNameWithOSIcon(const AsciiString& rawName) +{ + UnicodeString uName; + AsciiString cleanName = rawName; + WideChar osIcon = 0; + + if (cleanName.endsWithNoCase(" [MAC]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 6); + osIcon = 0xF179; // fa-apple + } + else if (cleanName.endsWithNoCase(" [WIN]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 6); + osIcon = 0xF17A; // fa-windows + } + else if (cleanName.endsWithNoCase(" [LNX]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 6); + osIcon = 0xF17C; // fa-linux + } + else if (cleanName.endsWithNoCase("[MAC]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 5); + osIcon = 0xF179; + } + else if (cleanName.endsWithNoCase("[WIN]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 5); + osIcon = 0xF17A; + } + else if (cleanName.endsWithNoCase("[LNX]")) + { + cleanName = AsciiString(cleanName.str(), cleanName.getLength() - 5); + osIcon = 0xF17C; + } + + uName.translate(cleanName); + + if (osIcon != 0) + { + WideChar iconBuf[3]; + iconBuf[0] = ' '; + iconBuf[1] = osIcon; + iconBuf[2] = 0; + uName.concat(iconBuf); + } + + return uName; +} + static Int insertPlayerInListbox(const PlayerInfo& info, Color color) { - UnicodeString uStr; - uStr.translate(info.m_name); + UnicodeString uStr = FormatPlayerNameWithOSIcon(info.m_name); Int currentRank = info.m_rankPoints; Int currentSide = info.m_side; @@ -730,6 +823,7 @@ void PopulateLobbyPlayerListbox() Int maxSelectedItems = GadgetListBoxGetNumEntries(listboxLobbyPlayers); Int *selectedIndices = nullptr; GadgetListBoxGetSelected(listboxLobbyPlayers, (Int *)(&selectedIndices)); + std::set selectedProfileIDs; std::set selectedNames; UnicodeString uStr; Int numSelected = 0; @@ -738,6 +832,11 @@ void PopulateLobbyPlayerListbox() if (!selectedIndices || selectedIndices[i] < 0) break; ++numSelected; + Int profileID = static_cast(reinterpret_cast(GadgetListBoxGetItemData(listboxLobbyPlayers, selectedIndices[i], 0))); + if (profileID != 0) + { + selectedProfileIDs.insert(profileID); + } AsciiString selectedName; uStr = GadgetListBoxGetText(listboxLobbyPlayers, selectedIndices[i], COLUMN_PLAYERNAME); selectedName.translate(uStr); @@ -771,7 +870,12 @@ void PopulateLobbyPlayerListbox() Color color = p.isAdmin ? GameSpyColor[GSCOLOR_PLAYER_OWNER] : GameSpyColor[GSCOLOR_PLAYER_NORMAL]; Int index = insertPlayerInListbox(info, color); - if (selectedNames.find(info.m_name) != selectedNames.end()) + AsciiString formattedName; + formattedName.translate(FormatPlayerNameWithOSIcon(info.m_name)); + + if ((info.m_profileID != 0 && selectedProfileIDs.find(info.m_profileID) != selectedProfileIDs.end()) || + selectedNames.find(info.m_name) != selectedNames.end() || + selectedNames.find(formattedName) != selectedNames.end()) { indicesToSelect.insert(index); } @@ -804,8 +908,8 @@ void PopulateLobbyPlayerListbox() Int maxSelectedItems = GadgetListBoxGetNumEntries(listboxLobbyPlayers); Int *selectedIndices; GadgetListBoxGetSelected(listboxLobbyPlayers, (Int *)(&selectedIndices)); + std::set selectedProfileIDs; std::set selectedNames; - std::set::const_iterator selIt; std::set indicesToSelect; UnicodeString uStr; Int numSelected = 0; @@ -817,6 +921,11 @@ void PopulateLobbyPlayerListbox() break; } ++numSelected; + Int profileID = static_cast(reinterpret_cast(GadgetListBoxGetItemData(listboxLobbyPlayers, selectedIndices[i], 0))); + if (profileID != 0) + { + selectedProfileIDs.insert(profileID); + } AsciiString selectedName; uStr = GadgetListBoxGetText(listboxLobbyPlayers, selectedIndices[i], COLUMN_PLAYERNAME); selectedName.translate(uStr); @@ -829,6 +938,16 @@ void PopulateLobbyPlayerListbox() GadgetListBoxReset(listboxLobbyPlayers); + auto shouldSelect = [&](const PlayerInfo& pInfo) -> bool { + if (pInfo.m_profileID != 0 && selectedProfileIDs.find(pInfo.m_profileID) != selectedProfileIDs.end()) + return true; + if (selectedNames.find(pInfo.m_name) != selectedNames.end()) + return true; + AsciiString formattedName; + formattedName.translate(FormatPlayerNameWithOSIcon(pInfo.m_name)); + return selectedNames.find(formattedName) != selectedNames.end(); + }; + // Ops for (it = players->begin(); it != players->end(); ++it) { @@ -837,8 +956,7 @@ void PopulateLobbyPlayerListbox() { Int index = insertPlayerInListbox(info, info.isIgnored()?GameSpyColor[GSCOLOR_PLAYER_IGNORED]:GameSpyColor[GSCOLOR_PLAYER_OWNER]); - selIt = selectedNames.find(info.m_name); - if (selIt != selectedNames.end()) + if (shouldSelect(info)) { DEBUG_LOG(("Marking index %d (%s) to re-select", index, info.m_name.str())); indicesToSelect.insert(index); @@ -855,8 +973,7 @@ void PopulateLobbyPlayerListbox() { Int index = insertPlayerInListbox(info, info.isIgnored()?GameSpyColor[GSCOLOR_PLAYER_IGNORED]:GameSpyColor[GSCOLOR_PLAYER_BUDDY]); - selIt = selectedNames.find(info.m_name); - if (selIt != selectedNames.end()) + if (shouldSelect(info)) { DEBUG_LOG(("Marking index %d (%s) to re-select", index, info.m_name.str())); indicesToSelect.insert(index); @@ -873,8 +990,7 @@ void PopulateLobbyPlayerListbox() { Int index = insertPlayerInListbox(info, info.isIgnored()?GameSpyColor[GSCOLOR_PLAYER_IGNORED]:GameSpyColor[GSCOLOR_PLAYER_NORMAL]); - selIt = selectedNames.find(info.m_name); - if (selIt != selectedNames.end()) + if (shouldSelect(info)) { DEBUG_LOG(("Marking index %d (%s) to re-select", index, info.m_name.str())); indicesToSelect.insert(index); @@ -2322,12 +2438,46 @@ WindowMsgHandledType WOLLobbyMenuSystem( GameWindow *window, UnsignedInt msg, } #else // Legacy GameSpy right-click menu - GPProfile profileID = 0; + // GeneralsX @bugfix felipebraz 27/09/2026 Resolve profile ID and raw player name consistently in right click menu + GPProfile profileID = static_cast(reinterpret_cast(GadgetListBoxGetItemData(control, rc->pos, 0))); + UnicodeString uRowText = GadgetListBoxGetText(control, rc->pos, COLUMN_PLAYERNAME); AsciiString aName; - aName.translate(GadgetListBoxGetText(control, rc->pos, COLUMN_PLAYERNAME)); - PlayerInfoMap::iterator it = TheGameSpyInfo->getPlayerInfoMap()->find(aName); - if (it != TheGameSpyInfo->getPlayerInfoMap()->end()) - profileID = it->second.m_profileID; + PlayerInfoMap *rcPlayerMap = TheGameSpyInfo ? TheGameSpyInfo->getPlayerInfoMap() : nullptr; + PlayerInfo *rcInfo = nullptr; + if (rcPlayerMap != nullptr) + { + if (profileID != 0) + { + for (PlayerInfoMap::iterator pIt = rcPlayerMap->begin(); pIt != rcPlayerMap->end(); ++pIt) + { + if (pIt->second.m_profileID == profileID) + { + rcInfo = &(pIt->second); + break; + } + } + } + if (rcInfo == nullptr) + { + for (PlayerInfoMap::iterator pIt = rcPlayerMap->begin(); pIt != rcPlayerMap->end(); ++pIt) + { + if (FormatPlayerNameWithOSIcon(pIt->second.m_name).compare(uRowText) == 0) + { + rcInfo = &(pIt->second); + profileID = rcInfo->m_profileID; + break; + } + } + } + } + if (rcInfo != nullptr) + { + aName = rcInfo->m_name; + } + else + { + aName.translate(uRowText); + } Bool isBuddy = FALSE; if (profileID <= 0) diff --git a/docs/WORKLOG/2026-09-DIARY.md b/docs/WORKLOG/2026-09-DIARY.md index 7a72c962fbf..3f3bde6c7c5 100644 --- a/docs/WORKLOG/2026-09-DIARY.md +++ b/docs/WORKLOG/2026-09-DIARY.md @@ -3,6 +3,21 @@ > [!NOTE] > **AI-Generated Content Disclosure**: This worklog is automatically generated and maintained by AI coding agents to document daily progress, debugging sessions, and technical decisions. +## 27/09/2026 +### Render OS Brand Glyphs in Lobby & Resolve CodeRabbit Review Findings (#338) +- **Context**: Players in online multiplayer lobbies display operating system tags (`[WIN]`, `[MAC]`, `[LNX]`). To improve visual polish in the lobby roster, player names are formatted to replace text OS tags with Font Awesome 6 Brands glyphs (`0xF179` Apple, `0xF17A` Windows, `0xF17C` Linux). A dedicated brand fallback mechanism was introduced via `AlternateBrandFont` in `FontCharsClass`. +- **CodeRabbit & Security Architecture Review Triage**: + - **Comment `4113848125` & `4114054595` (Fontconfig Brand Fallback Rejection & Glyph Verification across FreeType and GDI)**: + - In `render2dsentence.cpp`, when local font files are absent, Fontconfig's `FcFontMatch` can match a generic non-brand font (e.g. DejaVu Sans), resulting in missing-glyph boxes (glyph index 0). Added a family check in Tier 2 to reject Fontconfig matches if `FC_FAMILY` does not contain `"Font Awesome"`, `"FontAwesome"`, or `"Brands"`. + - Added `bool FontCharsClass::Has_Glyph(WCHAR ch) const` querying `FT_Get_Char_Index` on non-Win32 FreeType and `::GetGlyphIndicesW(MemDC, ...)` with `GGI_MARK_NONEXISTING_GLYPHS` on Windows GDI, ensuring Windows platform builds accurately validate brand glyphs. + - In `W3DGameFont.cpp`, updated `LoadBrandFallbackFont()` to verify that candidate fonts contain all three required OS brand glyphs (`0xF179`, `0xF17A`, `0xF17C`) before assigning `AlternateBrandFont`, releasing the reference and skipping candidates that fail. + - **Comments `4113848130`, `4114054596`, `4114063825` & Security Architecture Review (Roster Selection & Unprofiled Player Tooltip / Right-Click Lookups)**: + - In `playerTooltip` (`GeneralsMD` and `Generals`), updated the lookup to check `profileID` item data first, then iterate `PlayerInfoMap` comparing `FormatPlayerNameWithOSIcon(pIt->second.m_name)` against the displayed `uName`, and finally fallback to raw name lookup with a null guard against end-iterator dereference. This ensures unprofiled players (`profileID == 0`) with OS tags correctly display their tooltips. + - In `WOLLobbyMenuSystem` right-click handler (`GeneralsMD` and `Generals`), resolved `profileID` from item data first, matched formatted names for unprofiled players, and preserved the true raw player name (`rcInfo->m_name`) in `rcData->m_nick` / `aName` instead of the icon-translated string. + - In `PopulateLobbyPlayerListbox` (`GeneralsMD` and `Generals`), selection restoration previously compared raw `info.m_name` against `selectedNames` (which stored the icon-formatted text), causing selected players with OS tags to lose selection upon roster refresh. Introduced `shouldSelect` checking `selectedProfileIDs`, raw `info.m_name`, and icon-formatted `formattedName` across both Zero Hour and Generals base game. +- **Validation**: + - Built Zero Hour (`z_generals`) and Generals base game (`g_generals`) targets cleanly with Clang on macOS ARM64. + ## 26/09/2026 ### Universal Font Resolution & Bundled Liberation Fonts - **Context**: The game engine previously relied exclusively on system `Fontconfig` on non-Win32 platforms (Linux and macOS) to locate fonts. When system fonts (such as Microsoft's proprietary `Arial`) were missing or Fontconfig configuration was absent (e.g. clean macOS installations, stripped Linux/Flatpak environments, or iOS), font resolution failed and resulted in missing in-game text or startup crashes. @@ -1075,3 +1090,25 @@ - **Validation**: - Verified `git status` in both `../references/GameClient` and `../references/GameServer` are clean standalone repositories. - Re-configured and built Zero Hour with CMake (`macos-vulkan`) target `GeneralsXZH` with 0 errors. + +## 26/09/2026 +### Render OS Brand Glyphs in Multiplayer Lobby Using Font Awesome Brands +- **Context**: Replaced raw text platform tags (`[WIN]`, `[MAC]`, `[LNX]`) in the multiplayer lobby player listbox (`WOLCustomLobby.wnd` / `WOLLobbyMenu.cpp`) with authentic Font Awesome 6 Brands glyphs (Apple `U+F179`, Windows `U+F17A`, Linux `U+F17C`) from `assets/fonts/fa-brands-400.ttf`. +- **Implementation & Architecture**: + 1. **Font Awesome Font Resolution (`Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp`)**: + - Added `is_fa_brands` candidate probing in `Locate_Font_FontConfig()` matching `fa-brands-400`, `fa-brands-400.ttf`, `FontAwesome6Brands-Regular`, and `Font Awesome 6 Brands` against bundled directories via `Platform::FindLocalFontFile`. + - Added buffer row height guard in `Store_Freetype_Char()` (`(y_offset + row) < CharHeight`) to prevent memory overruns on tall glyphs (such as Tux `U+F17C`). + 2. **Dedicated Brand Fallback Chain (`Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h`, `render2dsentence.cpp`)**: + - Added `AlternateBrandFont` to `FontCharsClass`, initialized to `nullptr` in constructor. + - Delegated Private Use Area codepoints (`0xE000` - `0xF8FF`) directly to `AlternateBrandFont` across `Get_Char_Data()`, `Get_Char_Width()`, `Get_Char_Spacing()`, and `Blit_Char()`. This ensures standard UI text retains full Latin/accent/Unicode coverage from `Liberation Sans` / `Arial`, while brand icons are rendered from `fa-brands-400.ttf`. + 3. **Engine Font Library Wiring (`Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp`)**: + - Implemented `LoadBrandFallbackFont()` to resolve `fa-brands-400` from `WW3DAssetManager`. + - Wired `AlternateBrandFont` initialization in `W3DFontLibrary::loadFontData()` and release in `releaseFontData()`. + 4. **Lobby UI Formatting & Selection Resilience (`GeneralsMD/.../WOLLobbyMenu.cpp`, `Generals/.../WOLLobbyMenu.cpp`)**: + - Implemented `FormatPlayerNameWithOSIcon()` converting `[MAC]`, `[WIN]`, and `[LNX]` suffixes to corresponding Font Awesome Unicode codepoints (`0xF179`, `0xF17A`, `0xF17C`). + - Updated `insertPlayerInListbox()` to format incoming player names with visual OS icons. + - Enhanced `PopulateLobbyPlayerListbox()` to track selected players by `profileID` in addition to names, preserving selection state across lobby roster refreshes. + - Backported all lobby changes to the Generals base game (`Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp`). +- **Validation**: + - Successfully compiled both `z_generals` (Zero Hour) and `g_generals` (Generals base game) with CMake on macOS with 0 errors. + - Deployed binaries and fonts cleanly using `deploy-macos-zh.sh` and `deploy-macos-generals.sh`.