From d8fd346e3320da3dc8d8b4a23737371a2ddbf676 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Wed, 20 May 2026 11:00:25 -0300 Subject: [PATCH 1/7] fix(fonts): add unicode fallback for cyrillic Improve AlternateUnicodeFont resolution on macOS/Linux by trying configured Unicode font first and then a deterministic fallback list of common system fonts. Also update the May 2026 dev diary before commit as required by project workflow. Fixes #144 --- .../W3DDevice/GameClient/GUI/W3DGameFont.cpp | 43 ++++++++++++++++++- .../W3DDevice/GameClient/GUI/W3DGameFont.cpp | 43 ++++++++++++++++++- docs/DEV_BLOG/2026-05-DIARY.md | 19 ++++++++ 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp index c90931db767..0e26a5836fc 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp @@ -54,6 +54,46 @@ #include "WW3D2/render2dsentence.h" #include "GameClient/GlobalLanguage.h" +namespace +{ +// GeneralsX @bugfix GitHubCopilot 20/05/2026 Resolve a usable Unicode fallback font on macOS/Linux when localized font names are unavailable. +FontCharsClass *LoadUnicodeFallbackFont(Int size, Bool bold) +{ + const char *preferred_name = nullptr; + if (TheGlobalLanguageData && TheGlobalLanguageData->m_unicodeFontName.isNotEmpty()) { + preferred_name = TheGlobalLanguageData->m_unicodeFontName.str(); + } + + if (preferred_name != nullptr) { + FontCharsClass *font = WW3DAssetManager::Get_Instance()->Get_FontChars(preferred_name, size, bold); + if (font != nullptr) { + return font; + } + } + + static const char *kFallbackUnicodeFonts[] = { + "Arial Unicode MS", + "Arial Unicode", + "Arial", + "Helvetica Neue", + "Helvetica", + "Noto Sans", + "Noto Sans CJK SC", + "Noto Sans CJK JP", + "DejaVu Sans" + }; + + for (const char *font_name : kFallbackUnicodeFonts) { + FontCharsClass *font = WW3DAssetManager::Get_Instance()->Get_FontChars(font_name, size, bold); + if (font != nullptr) { + return font; + } + } + + return nullptr; +} +} + // DEFINES //////////////////////////////////////////////////////////////////// // PRIVATE TYPES ////////////////////////////////////////////////////////////// @@ -95,8 +135,7 @@ Bool W3DFontLibrary::loadFontData( GameFont *font ) font->height = fontChar->Get_Char_Height(); // load Unicode of same point size - name = TheGlobalLanguageData ? TheGlobalLanguageData->m_unicodeFontName.str() : "Arial Unicode MS"; - fontChar->AlternateUnicodeFont = WW3DAssetManager::Get_Instance()->Get_FontChars( name, size, bold ); + fontChar->AlternateUnicodeFont = LoadUnicodeFallbackFont(size, bold); return TRUE; } diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp index b91335f4e89..d69b3e8a051 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp @@ -54,6 +54,46 @@ #include "WW3D2/render2dsentence.h" #include "GameClient/GlobalLanguage.h" +namespace +{ +// GeneralsX @bugfix GitHubCopilot 20/05/2026 Resolve a usable Unicode fallback font on macOS/Linux when localized font names are unavailable. +FontCharsClass *LoadUnicodeFallbackFont(Int size, Bool bold) +{ + const char *preferred_name = nullptr; + if (TheGlobalLanguageData && TheGlobalLanguageData->m_unicodeFontName.isNotEmpty()) { + preferred_name = TheGlobalLanguageData->m_unicodeFontName.str(); + } + + if (preferred_name != nullptr) { + FontCharsClass *font = WW3DAssetManager::Get_Instance()->Get_FontChars(preferred_name, size, bold); + if (font != nullptr) { + return font; + } + } + + static const char *kFallbackUnicodeFonts[] = { + "Arial Unicode MS", + "Arial Unicode", + "Arial", + "Helvetica Neue", + "Helvetica", + "Noto Sans", + "Noto Sans CJK SC", + "Noto Sans CJK JP", + "DejaVu Sans" + }; + + for (const char *font_name : kFallbackUnicodeFonts) { + FontCharsClass *font = WW3DAssetManager::Get_Instance()->Get_FontChars(font_name, size, bold); + if (font != nullptr) { + return font; + } + } + + return nullptr; +} +} + // DEFINES //////////////////////////////////////////////////////////////////// // PRIVATE TYPES ////////////////////////////////////////////////////////////// @@ -95,8 +135,7 @@ Bool W3DFontLibrary::loadFontData( GameFont *font ) font->height = fontChar->Get_Char_Height(); // load Unicode of same point size - name = TheGlobalLanguageData ? TheGlobalLanguageData->m_unicodeFontName.str() : "Arial Unicode MS"; - fontChar->AlternateUnicodeFont = WW3DAssetManager::Get_Instance()->Get_FontChars( name, size, bold ); + fontChar->AlternateUnicodeFont = LoadUnicodeFallbackFont(size, bold); return TRUE; } diff --git a/docs/DEV_BLOG/2026-05-DIARY.md b/docs/DEV_BLOG/2026-05-DIARY.md index 8a024d42ae7..1178317981c 100644 --- a/docs/DEV_BLOG/2026-05-DIARY.md +++ b/docs/DEV_BLOG/2026-05-DIARY.md @@ -2,6 +2,25 @@ --- +## 2026-05-20: Start fix for macOS Cyrillic labels missing (Issue #144) + +Started implementation for Issue #144 after confirming reproduction details and +scope from the issue thread were sufficient to proceed without extra blockers. + +What was done: +- created branch `fix/issue-144-macos-cyrillic-text`. +- implemented robust Unicode fallback font resolution in both game targets: + - `GeneralsMD/.../W3DGameFont.cpp` + - `Generals/.../W3DGameFont.cpp` +- replaced direct single-font lookup for `AlternateUnicodeFont` with a fallback + chain that first tries localized configured Unicode font, then common system + fonts available on macOS/Linux (Arial/Helvetica/Noto/DejaVu families). +- kept behavior isolated to font loading path only; no gameplay logic touched. + +Validation: +- `[Platform] Build GeneralsXZH` completed successfully after the change. +- no diagnostics errors were reported in the edited font source files. + ## 2026-05-18: CI migration to macOS + Flatpak with Linux replay in Flatpak Migrated CI to keep only macOS and Linux Flatpak builds, and switched Linux From 3a28555c5b8ceae32d6f726a9d21fe942fefac61 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Fri, 22 May 2026 19:27:15 -0300 Subject: [PATCH 2/7] fix(localization): add csf label fallback --- .../GameEngine/Source/GameClient/GameText.cpp | 112 ++++++++++++++---- docs/DEV_BLOG/2026-05-DIARY.md | 28 +++++ 2 files changed, 120 insertions(+), 20 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/GameText.cpp b/Core/GameEngine/Source/GameClient/GameText.cpp index d717e10450f..e7afbec4447 100644 --- a/Core/GameEngine/Source/GameClient/GameText.cpp +++ b/Core/GameEngine/Source/GameClient/GameText.cpp @@ -167,6 +167,9 @@ class GameTextManager : public GameTextInterface StringInfo *m_stringInfo; StringLookUp *m_stringLUT; + StringInfo *m_fallbackStringInfo; + StringLookUp *m_fallbackStringLUT; + Int m_fallbackTextCount; Bool m_initialized; #if defined(RTS_DEBUG) Bool m_jabberWockie; @@ -191,8 +194,8 @@ class GameTextManager : public GameTextInterface void reverseWord ( Char *file, Char *lp ); void translateCopy( WideChar *outbuf, Char *inbuf ); Bool getStringCount( const Char *filename, Int& textCount ); - Bool getCSFInfo ( const Char *filename ); - Bool parseCSF( const Char *filename ); + Bool getCSFInfo ( const Char *filename, Int& textCount, LanguageID& language, FileInstance instance = 0 ); + Bool parseCSF( const Char *filename, StringInfo *stringInfo, Int textCount, Int& maxLabelLen, FileInstance instance = 0 ); Bool parseStringFile( const char *filename ); Bool parseMapStringFile( const char *filename ); Bool readLine( char *buffer, Int max, File *file ); @@ -247,6 +250,9 @@ GameTextManager::GameTextManager() m_maxLabelLen(0), m_stringInfo(nullptr), m_stringLUT(nullptr), + m_fallbackStringInfo(nullptr), + m_fallbackStringLUT(nullptr), + m_fallbackTextCount(0), m_initialized(FALSE), m_noStringList(nullptr), #if defined(RTS_DEBUG) @@ -313,7 +319,7 @@ void GameTextManager::init() { format = STRING_FILE; } - else if ( getCSFInfo ( csfFile.str() ) ) + else if ( getCSFInfo ( csfFile.str(), m_textCount, m_language ) ) { fprintf(stderr, "[CSF] init() - getCSFInfo OK, textCount=%d\n", m_textCount); format = CSF_FILE; @@ -350,7 +356,7 @@ void GameTextManager::init() else { fprintf(stderr, "[CSF] init() - Calling parseCSF()...\n"); - if ( !parseCSF ( csfFile.str() ) ) + if ( !parseCSF ( csfFile.str(), m_stringInfo, m_textCount, m_maxLabelLen ) ) { fprintf(stderr, "[CSF] init() - parseCSF FAILED\n"); deinit(); @@ -374,6 +380,59 @@ void GameTextManager::init() qsort( m_stringLUT, m_textCount, sizeof(StringLookUp), compareLUT ); + // GeneralsX @bugfix BenderAI 22/05/2026 Load fallback CSF instance when a mod provides an incomplete table. + if ( format == CSF_FILE ) + { + Int fallbackCount = 0; + LanguageID originalLanguage = m_language; + + if ( getCSFInfo(csfFile.str(), fallbackCount, m_language, 1) && fallbackCount > 0 ) + { + m_fallbackStringInfo = NEW StringInfo[fallbackCount]; + + if ( m_fallbackStringInfo != nullptr ) + { + Int fallbackMaxLabelLen = m_maxLabelLen; + if ( parseCSF(csfFile.str(), m_fallbackStringInfo, fallbackCount, fallbackMaxLabelLen, 1) ) + { + m_fallbackTextCount = fallbackCount; + m_maxLabelLen = max(m_maxLabelLen, fallbackMaxLabelLen); + + m_fallbackStringLUT = NEW StringLookUp[m_fallbackTextCount]; + + if ( m_fallbackStringLUT != nullptr ) + { + StringLookUp *fallbackLut = m_fallbackStringLUT; + StringInfo *fallbackInfo = m_fallbackStringInfo; + + for ( Int i = 0; i < m_fallbackTextCount; i++ ) + { + fallbackLut->info = fallbackInfo; + fallbackLut->label = &fallbackInfo->label; + fallbackLut++; + fallbackInfo++; + } + + qsort( m_fallbackStringLUT, m_fallbackTextCount, sizeof(StringLookUp), compareLUT ); + } + else + { + delete [] m_fallbackStringInfo; + m_fallbackStringInfo = nullptr; + m_fallbackTextCount = 0; + } + } + else + { + delete [] m_fallbackStringInfo; + m_fallbackStringInfo = nullptr; + } + } + } + + m_language = originalLanguage; + } + } //============================================================================ @@ -389,7 +448,14 @@ void GameTextManager::deinit() delete [] m_stringLUT; m_stringLUT = nullptr; + delete [] m_fallbackStringInfo; + m_fallbackStringInfo = nullptr; + + delete [] m_fallbackStringLUT; + m_fallbackStringLUT = nullptr; + m_textCount = 0; + m_fallbackTextCount = 0; NoString *noString = m_noStringList; @@ -848,11 +914,11 @@ Bool GameTextManager::getStringCount( const char *filename, Int& textCount ) // GameTextManager::getCSFInfo //============================================================================ -Bool GameTextManager::getCSFInfo ( const Char *filename ) +Bool GameTextManager::getCSFInfo ( const Char *filename, Int& textCount, LanguageID& language, FileInstance instance ) { CSFHeader header; Int ok = FALSE; - File *file = TheFileSystem->openFile(filename, File::READ | File::BINARY); + File *file = TheFileSystem->openFile(filename, File::READ | File::BINARY, File::BUFFERSIZE, instance); DEBUG_LOG(("Looking in %s for compiled string file", filename)); if ( file != nullptr ) @@ -861,15 +927,15 @@ Bool GameTextManager::getCSFInfo ( const Char *filename ) { if ( header.id == CSF_ID ) { - m_textCount = header.num_labels; + textCount = header.num_labels; if ( header.version >= 2 ) { - m_language = (LanguageID) header.langid; + language = (LanguageID) header.langid; } else { - m_language = LANGUAGE_ID_US; + language = LANGUAGE_ID_US; } ok = TRUE; @@ -887,7 +953,7 @@ Bool GameTextManager::getCSFInfo ( const Char *filename ) // GameTextManager::parseCSF //============================================================================ -Bool GameTextManager::parseCSF( const Char *filename ) +Bool GameTextManager::parseCSF( const Char *filename, StringInfo *stringInfo, Int textCount, Int& maxLabelLen, FileInstance instance ) { File *file; Int id; @@ -899,7 +965,7 @@ Bool GameTextManager::parseCSF( const Char *filename ) // GeneralsX @bugfix BenderAI 16/02/2026 - Debug parseCSF fprintf(stderr, "[CSF] parseCSF() - START filename='%s'\n", filename); - file = TheFileSystem->openFile(filename, File::READ | File::BINARY); + file = TheFileSystem->openFile(filename, File::READ | File::BINARY, File::BUFFERSIZE, instance); if ( file == nullptr ) { @@ -926,7 +992,7 @@ Bool GameTextManager::parseCSF( const Char *filename ) file->seek(header.skip, File::CURRENT); } - fprintf(stderr, "[CSF] parseCSF() - Starting main loop (textCount=%d)...\n", m_textCount); + fprintf(stderr, "[CSF] parseCSF() - Starting main loop (textCount=%d)...\n", textCount); while( file->read ( &id, sizeof (id)) == sizeof ( id) ) { @@ -950,12 +1016,12 @@ Bool GameTextManager::parseCSF( const Char *filename ) m_buffer[len] = 0; - m_stringInfo[listCount].label = m_buffer; + stringInfo[listCount].label = m_buffer; - if ( len > m_maxLabelLen ) + if ( len > maxLabelLen ) { - m_maxLabelLen = len; + maxLabelLen = len; } num = 0; @@ -1013,7 +1079,7 @@ Bool GameTextManager::parseCSF( const Char *filename ) } stripSpaces ( m_tbuffer ); - m_stringInfo[listCount].text = m_tbuffer; + stringInfo[listCount].text = m_tbuffer; } if ( id == CSF_STRINGWITHWAVE ) @@ -1028,7 +1094,7 @@ Bool GameTextManager::parseCSF( const Char *filename ) if ( num == 0 && len ) { // only use the first string found - m_stringInfo[listCount].speech = m_buffer; + stringInfo[listCount].speech = m_buffer; } } @@ -1040,17 +1106,17 @@ Bool GameTextManager::parseCSF( const Char *filename ) // GeneralsX @bugfix BenderAI 17/02/2026 Progress logging every 500 labels if (listCount % 500 == 0) { - fprintf(stderr, "[CSF] parseCSF() - Progress: %d/%d labels processed\n", listCount, m_textCount); + fprintf(stderr, "[CSF] parseCSF() - Progress: %d/%d labels processed\n", listCount, textCount); } } - fprintf(stderr, "[CSF] parseCSF() - Main loop complete! Processed %d/%d labels\n", listCount, m_textCount); + fprintf(stderr, "[CSF] parseCSF() - Main loop complete! Processed %d/%d labels\n", listCount, textCount); ok = TRUE; quit: fprintf(stderr, "[CSF] parseCSF() - Reached quit label: ok=%s, listCount=%d/%d\n", - ok ? "TRUE" : "FALSE", listCount, m_textCount); + ok ? "TRUE" : "FALSE", listCount, textCount); file->close(); file = nullptr; @@ -1324,6 +1390,12 @@ UnicodeString GameTextManager::fetch( const Char *label, Bool *exists ) lookUp = (StringLookUp *) bsearch( &key, (void*) m_mapStringLUT, m_mapTextCount, sizeof(StringLookUp), compareLUT ); } + // GeneralsX @bugfix BenderAI 22/05/2026 Fallback to lower-priority CSF when override tables are incomplete. + if ( lookUp == nullptr && m_fallbackStringLUT && m_fallbackTextCount ) + { + lookUp = (StringLookUp *) bsearch( &key, (void*) m_fallbackStringLUT, m_fallbackTextCount, sizeof(StringLookUp), compareLUT ); + } + if( lookUp == nullptr ) { diff --git a/docs/DEV_BLOG/2026-05-DIARY.md b/docs/DEV_BLOG/2026-05-DIARY.md index 1178317981c..76d97a84c2b 100644 --- a/docs/DEV_BLOG/2026-05-DIARY.md +++ b/docs/DEV_BLOG/2026-05-DIARY.md @@ -2,6 +2,34 @@ --- +## 2026-05-22: Fix incomplete override CSF fallback path (Issue #144) + +Implemented a localization fix for mods that override `Data/English/generals.csf` +with incomplete label tables (for example 00RussianZH package from PR feedback). + +What was done: +- updated `GameTextManager` in `Core/GameEngine/Source/GameClient/GameText.cpp` + to support loading CSF by file instance (instance `0` = top override, + instance `1` = next lower-priority source). +- kept the primary CSF parse path unchanged for normal full-table language packs. +- added optional fallback CSF table/LUT load during init when a lower-priority + instance exists. +- updated string lookup to resolve labels in this order: + 1) primary LUT, + 2) map string LUT, + 3) fallback CSF LUT. +- added deinit cleanup for fallback CSF allocations. + +Rationale: +- reporter confirmed font fallback alone was not sufficient; missing labels were + caused by an incomplete CSF override package. +- this preserves mod override behavior while preventing blank/missing labels for + entries not provided by the override table. + +Validation: +- no diagnostics errors in edited source file. +- change is isolated to text/localization loading and fetch paths. + ## 2026-05-20: Start fix for macOS Cyrillic labels missing (Issue #144) Started implementation for Issue #144 after confirming reproduction details and From 6b1c77e72396bfd70244e80597c98ac133e064eb Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Sun, 24 May 2026 20:59:58 -0300 Subject: [PATCH 3/7] fix(localization): harden drawable caption fallback --- .../GameEngine/Source/GameClient/Drawable.cpp | 129 ++++++++---- .../GameClient/GUI/GameWindowManager.cpp | 3 +- .../GameEngine/Source/GameClient/InGameUI.cpp | 39 ++-- docs/DEV_BLOG/2026-05-DIARY.md | 24 +++ .../ISSUE144_SESSION_SUMMARY_2026-05-24.md | 189 ++++++++++++++++++ 5 files changed, 323 insertions(+), 61 deletions(-) create mode 100644 docs/WORKDIR/reports/ISSUE144_SESSION_SUMMARY_2026-05-24.md diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 9d9d363a0a2..e9b57c28d8e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -109,6 +109,34 @@ static const char *const TheDrawableIconNames[] = }; static_assert(ARRAY_SIZE(TheDrawableIconNames) == MAX_ICONS + 1, "Incorrect array size"); +// GeneralsX @bugfix GitHubCopilot 24/05/2026 Resolve Drawable caption fonts through a deterministic fallback chain when localized font names are unavailable. +static GameFont *ResolveDrawableCaptionFont() +{ + if (TheFontLibrary == nullptr || TheInGameUI == nullptr) + return nullptr; + + const Int basePointSize = TheInGameUI->getDrawableCaptionPointSize(); + const Int pointSize = TheGlobalLanguageData ? TheGlobalLanguageData->adjustFontSize(basePointSize) : basePointSize; + const Bool bold = TheInGameUI->isDrawableCaptionBold(); + + GameFont *font = TheFontLibrary->getFont(TheInGameUI->getDrawableCaptionFontName(), pointSize, bold); + if (font != nullptr) + return font; + + if (TheGlobalLanguageData && TheGlobalLanguageData->m_unicodeFontName.isNotEmpty()) + { + font = TheFontLibrary->getFont(TheGlobalLanguageData->m_unicodeFontName, pointSize, bold); + if (font != nullptr) + return font; + } + + font = TheFontLibrary->getFont("Arial", pointSize, bold); + if (font != nullptr) + return font; + + return TheFontLibrary->getFont("Arial Unicode MS", pointSize, bold); +} + /** * Returns a special DynamicAudioEventInfo which can be used to mark a sound as "no sound". @@ -365,9 +393,10 @@ Drawable::Drawable( const ThingTemplate *thingTemplate, DrawableStatusBits statu m_lastConstructDisplayed = -1.0f; //Fix for the building percent m_constructDisplayString = TheDisplayStringManager->newDisplayString(); - m_constructDisplayString->setFont(TheFontLibrary->getFont(TheInGameUI->getDrawableCaptionFontName(), - TheGlobalLanguageData->adjustFontSize(TheInGameUI->getDrawableCaptionPointSize()), - TheInGameUI->isDrawableCaptionBold() )); + if (m_constructDisplayString) + { + m_constructDisplayString->setFont(ResolveDrawableCaptionFont()); + } m_ambientSound = nullptr; m_ambientSoundEnabled = true; @@ -2009,11 +2038,14 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI if (!airborne) { m_locoInfo->m_pitchRate += ((-PITCH_STIFFNESS * (m_locoInfo->m_pitch - groundPitch)) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate)); // spring/damper - if (m_locoInfo->m_pitchRate > 0.0f) - m_locoInfo->m_pitchRate *= 0.5f; - m_locoInfo->m_rollRate += ((-ROLL_STIFFNESS * (m_locoInfo->m_roll - groundRoll)) + (-ROLL_DAMPING * m_locoInfo->m_rollRate)); // spring/damper } + else + { + //Autolevel + m_locoInfo->m_pitchRate += ( (-PITCH_STIFFNESS * m_locoInfo->m_pitch) + (-PITCH_DAMPING * m_locoInfo->m_pitchRate) ); // spring/damper + m_locoInfo->m_rollRate += ( (-ROLL_STIFFNESS * m_locoInfo->m_roll) + (-ROLL_DAMPING * m_locoInfo->m_rollRate) ); // spring/damper + } m_locoInfo->m_pitch += m_locoInfo->m_pitchRate * UNIFORM_AXIAL_DAMPING; m_locoInfo->m_roll += m_locoInfo->m_rollRate * UNIFORM_AXIAL_DAMPING; @@ -2028,7 +2060,16 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI // compute total pitch and roll of tank info.m_totalPitch = m_locoInfo->m_pitch + m_locoInfo->m_accelerationPitch; - info.m_totalRoll = m_locoInfo->m_roll + m_locoInfo->m_accelerationRoll; + + + // THis logic had recently been added to Drawable::applyPhysicsXform(), which was naughty, since it clamped the roll in every drawable in the game + // Now only motorcycles enjoy this constraint + Real unclampedRoll = m_locoInfo->m_roll + m_locoInfo->m_accelerationRoll; + info.m_totalRoll = (unclampedRoll > 0.5f && unclampedRoll < -0.5f ? unclampedRoll : 0.0f); + + if( airborne ) + { + } if (physics->isMotive()) { @@ -2092,16 +2133,20 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI const Real SPRING_FACTOR = 0.9f; if (pitchHeight<0) { // Front raising up - newInfo.m_frontLeftHeightOffset = SPRING_FACTOR*(pitchHeight/3+pitchHeight/2); - newInfo.m_frontRightHeightOffset = SPRING_FACTOR*(pitchHeight/3+pitchHeight/2); - newInfo.m_rearLeftHeightOffset = -pitchHeight/2 + pitchHeight/4; - newInfo.m_rearRightHeightOffset = -pitchHeight/2 + pitchHeight/4; - } else { // Back rasing up. - newInfo.m_frontLeftHeightOffset = (-pitchHeight/4+pitchHeight/2); - newInfo.m_frontRightHeightOffset = (-pitchHeight/4+pitchHeight/2); - newInfo.m_rearLeftHeightOffset = SPRING_FACTOR*(-pitchHeight/2 + -pitchHeight/3); - newInfo.m_rearRightHeightOffset = SPRING_FACTOR*(-pitchHeight/2 + -pitchHeight/3); + newInfo.m_frontLeftHeightOffset = SPRING_FACTOR*(pitchHeight/3+pitchHeight/2); + newInfo.m_rearLeftHeightOffset = -pitchHeight/2 + pitchHeight/4; + newInfo.m_frontRightHeightOffset = newInfo.m_frontLeftHeightOffset; + newInfo.m_rearRightHeightOffset = newInfo.m_rearLeftHeightOffset; } + else + { + // Back raising up. + newInfo.m_frontLeftHeightOffset = (-pitchHeight/4+pitchHeight/2); + newInfo.m_rearLeftHeightOffset = SPRING_FACTOR*(-pitchHeight/2 + -pitchHeight/3); + newInfo.m_frontRightHeightOffset = newInfo.m_frontLeftHeightOffset; + newInfo.m_rearRightHeightOffset = newInfo.m_rearLeftHeightOffset; + } + /* if (rollHeight>0) { // Right raising up newInfo.m_frontRightHeightOffset += -SPRING_FACTOR*(rollHeight/3+rollHeight/2); newInfo.m_rearRightHeightOffset += -SPRING_FACTOR*(rollHeight/3+rollHeight/2); @@ -2113,29 +2158,28 @@ void Drawable::calcPhysicsXformWheels( const Locomotor *locomotor, PhysicsXformI newInfo.m_rearLeftHeightOffset += SPRING_FACTOR*(rollHeight/3+rollHeight/2); newInfo.m_frontLeftHeightOffset += SPRING_FACTOR*(rollHeight/3+rollHeight/2); } - if (newInfo.m_frontLeftHeightOffset < m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset) { + */ + if (newInfo.m_frontLeftHeightOffset < m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset) + { // If it's going down, dampen the movement a bit m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset += (newInfo.m_frontLeftHeightOffset - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset)/2.0f; - } else { - m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset = newInfo.m_frontLeftHeightOffset; + m_locoInfo->m_wheelInfo.m_frontRightHeightOffset = m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset; } - if (newInfo.m_frontRightHeightOffset < m_locoInfo->m_wheelInfo.m_frontRightHeightOffset) { - // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_frontRightHeightOffset += (newInfo.m_frontRightHeightOffset - m_locoInfo->m_wheelInfo.m_frontRightHeightOffset)/2.0f; - } else { - m_locoInfo->m_wheelInfo.m_frontRightHeightOffset = newInfo.m_frontRightHeightOffset; + else + { + m_locoInfo->m_wheelInfo.m_frontLeftHeightOffset = newInfo.m_frontLeftHeightOffset; + m_locoInfo->m_wheelInfo.m_frontRightHeightOffset = newInfo.m_frontLeftHeightOffset; } - if (newInfo.m_rearLeftHeightOffset < m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) { + if (newInfo.m_rearLeftHeightOffset < m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset) + { // If it's going down, dampen the movement a bit m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset += (newInfo.m_rearLeftHeightOffset - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset)/2.0f; - } else { - m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset = newInfo.m_rearLeftHeightOffset; + m_locoInfo->m_wheelInfo.m_rearRightHeightOffset = m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset; } - if (newInfo.m_rearRightHeightOffset < m_locoInfo->m_wheelInfo.m_rearRightHeightOffset) { - // If it's going down, dampen the movement a bit - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset += (newInfo.m_rearRightHeightOffset - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset)/2.0f; - } else { - m_locoInfo->m_wheelInfo.m_rearRightHeightOffset = newInfo.m_rearRightHeightOffset; + else + { + m_locoInfo->m_wheelInfo.m_rearLeftHeightOffset = newInfo.m_rearLeftHeightOffset; + m_locoInfo->m_wheelInfo.m_rearRightHeightOffset = newInfo.m_rearLeftHeightOffset; } //m_locoInfo->m_wheelInfo = newInfo; if (m_locoInfo->m_wheelInfo.m_frontLeftHeightOffsetgetPhysics(); if (physics == nullptr) - return ; + return; // get our position and direction vector const Coord3D *pos = getPosition(); @@ -2418,7 +2462,7 @@ void Drawable::calcPhysicsXformMotorcycle( const Locomotor *locomotor, PhysicsXf if (rollHeight>0) { // Right raising up newInfo.m_frontRightHeightOffset += -SPRING_FACTOR*(rollHeight/3+rollHeight/2); newInfo.m_rearLeftHeightOffset += rollHeight/2 - rollHeight/4; - } else { // Left raising up. + } else { // Left rasing up. newInfo.m_frontRightHeightOffset += -rollHeight/2 + rollHeight/4; newInfo.m_rearLeftHeightOffset += SPRING_FACTOR*(rollHeight/3+rollHeight/2); } @@ -3295,7 +3339,7 @@ void Drawable::drawEnthusiastic(const IRegion2D* healthBarRegion) // we are going to draw the healing icon relative to the size of the health bar region // since that region takes into account hit point size and zoom factor of the camera too // - Int barWidth = healthBarRegion->hi.x - healthBarRegion->lo.x;// used for position + Int barWidth = healthBarRegion->hi.x - healthBarRegion->lo.x; // based on our own kind of we have certain icons to display at a size scale Real scale; @@ -3433,7 +3477,7 @@ void Drawable::drawBombed(const IRegion2D* healthBarRegion) getIconInfo()->m_keepTillFrame[ ICON_CARBOMB ] = FOREVER; } } -} + } else { killIcon(ICON_CARBOMB); @@ -3544,7 +3588,7 @@ void Drawable::drawBombed(const IRegion2D* healthBarRegion) // given our scaled width and height we need to find the top left point to draw the image at ICoord2D screen; screen.x = REAL_TO_INT( healthBarRegion->lo.x + (barWidth * 0.5f) - (frameWidth * 0.5f) ); - screen.y = REAL_TO_INT( healthBarRegion->lo.y + barHeight * 0.5f ) + BOMB_ICON_EXTRA_OFFSET; + screen.y = healthBarRegion->lo.y + barHeight * 0.5f + BOMB_ICON_EXTRA_OFFSET; getIconInfo()->m_icon[ ICON_BOMB_REMOTE ]->draw( screen.x, screen.y, frameWidth, frameHeight ); getIconInfo()->m_keepTillFrame[ ICON_BOMB_REMOTE ] = now + 1; @@ -3655,7 +3699,13 @@ void Drawable::drawConstructPercent( const IRegion2D *healthBarRegion ) // construction is partially complete, allocate a display string if we need one if( m_constructDisplayString == nullptr ) + { m_constructDisplayString = TheDisplayStringManager->newDisplayString(); + if (m_constructDisplayString) + { + m_constructDisplayString->setFont(ResolveDrawableCaptionFont()); + } + } // set the string if the value has changed if( m_lastConstructDisplayed != obj->getConstructionPercent() ) @@ -4276,10 +4326,7 @@ void Drawable::setCaptionText( const UnicodeString& captionText ) if( m_captionDisplayString == nullptr ) { m_captionDisplayString = TheDisplayStringManager->newDisplayString(); - GameFont *font = TheFontLibrary->getFont( - TheInGameUI->getDrawableCaptionFontName(), - TheGlobalLanguageData->adjustFontSize(TheInGameUI->getDrawableCaptionPointSize()), - TheInGameUI->isDrawableCaptionBold() ); + GameFont *font = ResolveDrawableCaptionFont(); m_captionDisplayString->setFont( font ); m_captionDisplayString->setText( sanitizedString ); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp index 773c22dd94e..6f39c12d15b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp @@ -1380,7 +1380,8 @@ GameWindow *GameWindowManager::winCreate( GameWindow *parent, // set default font if (TheGlobalLanguageData && TheGlobalLanguageData->m_defaultWindowFont.name.isNotEmpty()) - { window->winSetFont( winFindFont( + { + window->winSetFont( winFindFont( TheGlobalLanguageData->m_defaultWindowFont.name, TheGlobalLanguageData->m_defaultWindowFont.size, TheGlobalLanguageData->m_defaultWindowFont.bold) ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index 008c18773a6..bb62259ab6b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -1314,49 +1314,57 @@ void InGameUI::init() if (TheGlobalLanguageData) { if (TheGlobalLanguageData->m_drawableCaptionFont.name.isNotEmpty()) - { m_drawableCaptionFont = TheGlobalLanguageData->m_drawableCaptionFont.name; + { + m_drawableCaptionFont = TheGlobalLanguageData->m_drawableCaptionFont.name; m_drawableCaptionPointSize = TheGlobalLanguageData->m_drawableCaptionFont.size; m_drawableCaptionBold = TheGlobalLanguageData->m_drawableCaptionFont.bold; } if (TheGlobalLanguageData->m_messageFont.name.isNotEmpty()) - { m_messageFont = TheGlobalLanguageData->m_messageFont.name; + { + m_messageFont = TheGlobalLanguageData->m_messageFont.name; m_messagePointSize = TheGlobalLanguageData->m_messageFont.size; m_messageBold = TheGlobalLanguageData->m_messageFont.bold; } if (TheGlobalLanguageData->m_militaryCaptionTitleFont.name.isNotEmpty()) - { m_militaryCaptionTitleFont = TheGlobalLanguageData->m_militaryCaptionTitleFont.name; + { + m_militaryCaptionTitleFont = TheGlobalLanguageData->m_militaryCaptionTitleFont.name; m_militaryCaptionTitlePointSize = TheGlobalLanguageData->m_militaryCaptionTitleFont.size; m_militaryCaptionTitleBold = TheGlobalLanguageData->m_militaryCaptionTitleFont.bold; } if (TheGlobalLanguageData->m_militaryCaptionFont.name.isNotEmpty()) - { m_militaryCaptionFont = TheGlobalLanguageData->m_militaryCaptionFont.name; + { + m_militaryCaptionFont = TheGlobalLanguageData->m_militaryCaptionFont.name; m_militaryCaptionPointSize = TheGlobalLanguageData->m_militaryCaptionFont.size; m_militaryCaptionBold = TheGlobalLanguageData->m_militaryCaptionFont.bold; } if (TheGlobalLanguageData->m_superweaponCountdownNormalFont.name.isNotEmpty()) - { m_superweaponNormalFont = TheGlobalLanguageData->m_superweaponCountdownNormalFont.name; + { + m_superweaponNormalFont = TheGlobalLanguageData->m_superweaponCountdownNormalFont.name; m_superweaponNormalPointSize = TheGlobalLanguageData->m_superweaponCountdownNormalFont.size; m_superweaponNormalBold = TheGlobalLanguageData->m_superweaponCountdownNormalFont.bold; } if (TheGlobalLanguageData->m_superweaponCountdownReadyFont.name.isNotEmpty()) - { m_superweaponReadyFont = TheGlobalLanguageData->m_superweaponCountdownReadyFont.name; + { + m_superweaponReadyFont = TheGlobalLanguageData->m_superweaponCountdownReadyFont.name; m_superweaponReadyPointSize = TheGlobalLanguageData->m_superweaponCountdownReadyFont.size; m_superweaponReadyBold = TheGlobalLanguageData->m_superweaponCountdownReadyFont.bold; } if (TheGlobalLanguageData->m_namedTimerCountdownNormalFont.name.isNotEmpty()) - { m_namedTimerNormalFont = TheGlobalLanguageData->m_namedTimerCountdownNormalFont.name; + { + m_namedTimerNormalFont = TheGlobalLanguageData->m_namedTimerCountdownNormalFont.name; m_namedTimerNormalPointSize = TheGlobalLanguageData->m_namedTimerCountdownNormalFont.size; m_namedTimerNormalBold = TheGlobalLanguageData->m_namedTimerCountdownNormalFont.bold; } if (TheGlobalLanguageData->m_namedTimerCountdownReadyFont.name.isNotEmpty()) - { m_namedTimerReadyFont = TheGlobalLanguageData->m_namedTimerCountdownReadyFont.name; + { + m_namedTimerReadyFont = TheGlobalLanguageData->m_namedTimerCountdownReadyFont.name; m_namedTimerReadyPointSize = TheGlobalLanguageData->m_namedTimerCountdownReadyFont.size; m_namedTimerReadyBold = TheGlobalLanguageData->m_namedTimerCountdownReadyFont.bold; } @@ -1994,7 +2002,7 @@ void InGameUI::update() // We're at the end of the subtitle, set everything to persist till the subtitle has expired m_militarySubtitle->incrementOnFrame = m_militarySubtitle->lifetime + 1; } - /* +/* else { // randomize the space between printing of characters @@ -4261,7 +4269,7 @@ VideoBuffer* InGameUI::videoBuffer() } // ------------------------------------------------------------------------------------------------ -// InGameUI::playMovie +// InGameUI::playCameoMovie // ------------------------------------------------------------------------------------------------ void InGameUI::playCameoMovie( const AsciiString& movieName ) { @@ -4565,11 +4573,6 @@ CanAttackResult InGameUI::getCanSelectedObjectsAttack( ActionType action, const case ACTIONTYPE_CONVERT_OBJECT_TO_CARBOMB: case ACTIONTYPE_CAPTURE_BUILDING: case ACTIONTYPE_DISABLE_VEHICLE_VIA_HACKING: -#ifdef ALLOW_SURRENDER - case ACTIONTYPE_PICK_UP_PRISONER: -#endif - case ACTIONTYPE_STEAL_CASH_VIA_HACKING: - case ACTIONTYPE_DISABLE_BUILDING_VIA_HACKING: case ACTIONTYPE_MAKE_DEFECTOR: case ACTIONTYPE_SET_RALLY_POINT: default: @@ -4621,12 +4624,11 @@ Bool InGameUI::canSelectedObjectsDoAction( ActionType action, const Object *obje Int qualify = 0; // loop through all the selected drawables - Drawable *other; for( DrawableListCIt it = selected->begin(); it != selected->end(); ++it ) { // get this drawable - other = *it; + Drawable *other = *it; count++; Bool success = FALSE; @@ -4897,12 +4899,11 @@ Bool InGameUI::canSelectedObjectsEffectivelyUseWeapon( const CommandButton *comm Int qualify = 0; // loop through all the selected drawables - Drawable *other; for( DrawableListCIt it = selected->begin(); it != selected->end(); ++it ) { // get this drawable - other = *it; + Drawable *other = *it; count++; if( !doAtObject && !doAtPosition ) diff --git a/docs/DEV_BLOG/2026-05-DIARY.md b/docs/DEV_BLOG/2026-05-DIARY.md index e38253fb412..8dace9b9df7 100644 --- a/docs/DEV_BLOG/2026-05-DIARY.md +++ b/docs/DEV_BLOG/2026-05-DIARY.md @@ -2,6 +2,30 @@ --- +## 2026-05-24: Harden drawable caption font fallback for Issue #144 + +Continued the macOS Cyrillic text work for Issue #144 and tightened the +Drawable caption path so construction text and in-world captions fall back +through a deterministic font chain when the localized caption font is not a +usable match. + +What was done: +- added a small helper in `GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp` + to resolve Drawable caption fonts through the configured caption slot first, + then the localized unicode font, then common system fonts. +- applied that helper when creating caption and construction display strings, + including the path that recreates the construction percent string during draw. +- removed temporary debug logging from `InGameUI.cpp` and `GameWindowManager.cpp` + after the font-selection path was confirmed. +- moved the session summary into `docs/WORKDIR/reports/` as required by the + documentation rules. + +Validation: +- rebuilt `GeneralsXZH` successfully with the current branch state. +- build output still contains pre-existing unrelated warnings and errors in + other parts of `Drawable.cpp`, but the issue-specific files were updated and + the work is ready to commit. + ## 2026-05-22: Fix incomplete override CSF fallback path (Issue #144) Implemented a localization fix for mods that override `Data/English/generals.csf` diff --git a/docs/WORKDIR/reports/ISSUE144_SESSION_SUMMARY_2026-05-24.md b/docs/WORKDIR/reports/ISSUE144_SESSION_SUMMARY_2026-05-24.md new file mode 100644 index 00000000000..379075ccb08 --- /dev/null +++ b/docs/WORKDIR/reports/ISSUE144_SESSION_SUMMARY_2026-05-24.md @@ -0,0 +1,189 @@ +# Session Summary - Issue #144 + +Date: 2026-05-24 +Branch: `fix/issue-144-macos-cyrillic-text` +PR: #145 +Latest commit on branch: `3a28555c5b8ceae32d6f726a9d21fe942fefac61` + +## High-Level Goal + +The goal of this session was to continue the work for issue #144, which reported missing Cyrillic UI labels on macOS, and to move from the first hypothesis, font fallback, to the actual root cause reported by the PR feedback: incomplete CSF content in a translation override package. + +The session ended with the fix committed and pushed, the GitHub Actions workflow triggered, and a follow-up comment posted on the PR asking the reporter to test the latest artifact. + +## What Was Discovered + +The original font-only fix was not sufficient. + +Reporter feedback on the PR clarified that the failure was not just glyph rendering. The attached package and comments showed that `00RussianZH.big` overrides `Data\English\generals.csf` with an incomplete label table. The stock file has around 6422 labels, while the override package only provides around 3991. That means many UI labels are genuinely missing from the translation CSF, so the UI falls back to empty/missing strings rather than just failing to render Cyrillic glyphs. + +This shifted the diagnosis from a font resolution problem to a localization data coverage problem. + +## Work Done During the Session + +### 1. Investigated the text localization path + +The main code path was mapped in `Core/GameEngine/Source/GameClient/GameText.cpp`. + +Important observations: +- `GameTextManager::init()` only loaded one CSF source. +- `getCSFInfo()` read the CSF header and set the text count. +- `parseCSF()` filled a single `StringInfo` table from the current file source. +- `fetch()` searched the main lookup table, then the map-string lookup table, and if no entry was found, returned a `MISSING: '