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/Core/GameEngine/Source/GameClient/GlobalLanguage.cpp b/Core/GameEngine/Source/GameClient/GlobalLanguage.cpp index a9bc586d494..974f8128e6d 100644 --- a/Core/GameEngine/Source/GameClient/GlobalLanguage.cpp +++ b/Core/GameEngine/Source/GameClient/GlobalLanguage.cpp @@ -52,6 +52,8 @@ //----------------------------------------------------------------------------- #include "PreRTS.h" +#include + #include "Common/AddonCompat.h" #include "Common/INI.h" #include "Common/Registry.h" @@ -148,12 +150,48 @@ GlobalLanguage::~GlobalLanguage() void GlobalLanguage::init() { + // GeneralsX @bugfix GitHubCopilot 27/05/2026 Implement Language.ini fallback chain so stock values fill missing override keys. + char log_buffer[512]; { + AsciiString registryLanguage = GetRegistryLanguage(); AsciiString fname; - fname.format("Data\\%s\\Language", GetRegistryLanguage().str()); + fname.format("Data\\%s\\Language", registryLanguage.str()); + + sprintf(log_buffer, + "[GX-ISSUE144] GlobalLanguage init registryLanguage=%s primaryIni=%s", + registryLanguage.str(), + fname.str()); + fprintf(stderr, "%s\n", log_buffer); INI ini; ini.loadFileDirectory( fname, INI_LOAD_OVERWRITE, nullptr ); + sprintf(log_buffer, + "[GX-ISSUE144] GlobalLanguage init loaded primary unicodeFont=%s", + m_unicodeFontName.isNotEmpty() ? m_unicodeFontName.str() : ""); + fprintf(stderr, "%s\n", log_buffer); + + // Load stock Language.ini as fallback for missing keys (e.g., russifier may override UnicodeFontName=Arial, + // so we load stock English to restore Arial Unicode MS if not redefined) + if (registryLanguage.compare("English") != 0) + { + AsciiString stockFname("Data\\English\\Language"); + sprintf(log_buffer, + "[GX-ISSUE144] GlobalLanguage init loading stock fallback=%s", + stockFname.str()); + fprintf(stderr, "%s\n", log_buffer); + ini.loadFileDirectory( stockFname, INI_LOAD_MULTIFILE, nullptr ); + sprintf(log_buffer, + "[GX-ISSUE144] GlobalLanguage init fallback merged unicodeFont=%s (may have been filled)", + m_unicodeFontName.isNotEmpty() ? m_unicodeFontName.str() : ""); + fprintf(stderr, "%s\n", log_buffer); + } + + sprintf(log_buffer, + "[GX-ISSUE144] GlobalLanguage init final unicodeFont=%s drawableCaption=%s defaultWindow=%s", + m_unicodeFontName.isNotEmpty() ? m_unicodeFontName.str() : "", + m_drawableCaptionFont.name.isNotEmpty() ? m_drawableCaptionFont.name.str() : "", + m_defaultWindowFont.name.isNotEmpty() ? m_defaultWindowFont.name.str() : ""); + fprintf(stderr, "%s\n", log_buffer); } StringList::iterator it = m_localFonts.begin(); @@ -162,10 +200,14 @@ void GlobalLanguage::init() AsciiString font = *it; if(AddFontResource(font.str()) == 0) { + sprintf(log_buffer, "[GX-ISSUE144] GlobalLanguage local font add FAILED file=%s", font.str()); + fprintf(stderr, "%s\n", log_buffer); DEBUG_CRASH(("GlobalLanguage::init Failed to add font %s", font.str())); } else { + sprintf(log_buffer, "[GX-ISSUE144] GlobalLanguage local font add OK file=%s", font.str()); + fprintf(stderr, "%s\n", log_buffer); //SendMessage( HWND_BROADCAST, WM_FONTCHANGE, 0, 0); } ++it; @@ -174,6 +216,12 @@ void GlobalLanguage::init() // override values with user preferences OptionPreferences optionPref; m_userResolutionFontSizeAdjustment = optionPref.getResolutionFontAdjustment(); + sprintf(log_buffer, + "[GX-ISSUE144] GlobalLanguage resolutionAdjustment effective=%.3f user=%.3f base=%.3f", + getResolutionFontSizeAdjustment(), + m_userResolutionFontSizeAdjustment, + m_resolutionFontSizeAdjustment); + fprintf(stderr, "%s\n", log_buffer); } void GlobalLanguage::reset() diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp index c90931db767..e3e11ba4f29 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp @@ -45,6 +45,7 @@ // SYSTEM INCLUDES //////////////////////////////////////////////////////////// #include +#include // USER INCLUDES ////////////////////////////////////////////////////////////// #include "Common/Debug.h" @@ -54,6 +55,47 @@ #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. +// GeneralsX @bugfix GitHubCopilot 29/05/2026 Prevent circular Unicode fallback when the localized unicode family equals the base font family. +FontCharsClass *LoadUnicodeFallbackFont(Int size, Bool bold, const char *base_name) +{ + const char *preferred_name = nullptr; + if (TheGlobalLanguageData && TheGlobalLanguageData->m_unicodeFontName.isNotEmpty()) { + preferred_name = TheGlobalLanguageData->m_unicodeFontName.str(); + } + + if (preferred_name != nullptr && (base_name == nullptr || strcmp(preferred_name, base_name) != 0)) { + 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 +137,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, name); return TRUE; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 9d9d363a0a2..df34e66729e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -30,6 +30,7 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine #include // For _isnan compatibility +#include #include "Common/AudioEventInfo.h" #include "Common/DynamicAudioEventInfo.h" #include "Common/AudioSettings.h" @@ -109,6 +110,72 @@ 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() +{ + // GeneralsX @tweak GitHubCopilot 27/05/2026 Trace Drawable caption font fallback decisions and active source. + char log_buffer[512]; + + if (TheFontLibrary == nullptr || TheInGameUI == nullptr) + { + sprintf(log_buffer, "[GX-ISSUE144] Drawable ResolveCaptionFont missing TheFontLibrary=%p TheInGameUI=%p", TheFontLibrary, TheInGameUI); + fprintf(stderr, "%s\n", log_buffer); + return nullptr; + } + + const Int basePointSize = TheInGameUI->getDrawableCaptionPointSize(); + const Int pointSize = TheGlobalLanguageData ? TheGlobalLanguageData->adjustFontSize(basePointSize) : basePointSize; + const Bool bold = TheInGameUI->isDrawableCaptionBold(); + sprintf(log_buffer, + "[GX-ISSUE144] Drawable ResolveCaptionFont request uiFont=%s baseSize=%d adjustedSize=%d bold=%d unicode=%s", + TheInGameUI->getDrawableCaptionFontName().str(), + basePointSize, + pointSize, + bold, + (TheGlobalLanguageData && TheGlobalLanguageData->m_unicodeFontName.isNotEmpty()) ? TheGlobalLanguageData->m_unicodeFontName.str() : ""); + fprintf(stderr, "%s\n", log_buffer); + + GameFont *font = TheFontLibrary->getFont(TheInGameUI->getDrawableCaptionFontName(), pointSize, bold); + if (font != nullptr) + { + sprintf(log_buffer, "[GX-ISSUE144] Drawable ResolveCaptionFont hit uiFont=%s", TheInGameUI->getDrawableCaptionFontName().str()); + fprintf(stderr, "%s\n", log_buffer); + return font; + } + sprintf(log_buffer, "[GX-ISSUE144] Drawable ResolveCaptionFont miss uiFont=%s", TheInGameUI->getDrawableCaptionFontName().str()); + fprintf(stderr, "%s\n", log_buffer); + + if (TheGlobalLanguageData && TheGlobalLanguageData->m_unicodeFontName.isNotEmpty()) + { + font = TheFontLibrary->getFont(TheGlobalLanguageData->m_unicodeFontName, pointSize, bold); + if (font != nullptr) + { + sprintf(log_buffer, "[GX-ISSUE144] Drawable ResolveCaptionFont hit unicodeFont=%s", TheGlobalLanguageData->m_unicodeFontName.str()); + fprintf(stderr, "%s\n", log_buffer); + return font; + } + sprintf(log_buffer, "[GX-ISSUE144] Drawable ResolveCaptionFont miss unicodeFont=%s", TheGlobalLanguageData->m_unicodeFontName.str()); + fprintf(stderr, "%s\n", log_buffer); + } + + font = TheFontLibrary->getFont("Arial", pointSize, bold); + if (font != nullptr) + { + sprintf(log_buffer, "[GX-ISSUE144] Drawable ResolveCaptionFont hit fallback=Arial"); + fprintf(stderr, "%s\n", log_buffer); + return font; + } + sprintf(log_buffer, "[GX-ISSUE144] Drawable ResolveCaptionFont miss fallback=Arial"); + fprintf(stderr, "%s\n", log_buffer); + + font = TheFontLibrary->getFont("Arial Unicode MS", pointSize, bold); + sprintf(log_buffer, + "[GX-ISSUE144] Drawable ResolveCaptionFont final fallback Arial Unicode MS %s", + font ? "hit" : "miss"); + fprintf(stderr, "%s\n", log_buffer); + return font; +} + /** * Returns a special DynamicAudioEventInfo which can be used to mark a sound as "no sound". @@ -365,9 +432,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 +2077,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 +2099,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 +2172,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 +2197,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 +2501,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 +3378,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 +3516,7 @@ void Drawable::drawBombed(const IRegion2D* healthBarRegion) getIconInfo()->m_keepTillFrame[ ICON_CARBOMB ] = FOREVER; } } -} + } else { killIcon(ICON_CARBOMB); @@ -3544,7 +3627,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; @@ -3630,6 +3713,7 @@ void Drawable::drawDisabled(const IRegion2D* healthBarRegion) //------------------------------------------------------------------------------------------------- void Drawable::drawConstructPercent( const IRegion2D *healthBarRegion ) { + char log_buffer[512]; // this data is in an attached object Object *obj = getObject(); @@ -3655,7 +3739,18 @@ 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()); + sprintf(log_buffer, + "[GX-ISSUE144] Drawable construct string allocated drawable=%p obj=%p", + this, + obj); + fprintf(stderr, "%s\n", log_buffer); + } + } // set the string if the value has changed if( m_lastConstructDisplayed != obj->getConstructionPercent() ) @@ -3668,6 +3763,11 @@ void Drawable::drawConstructPercent( const IRegion2D *healthBarRegion ) // record this percent as our last displayed so we don't un-necessarily rebuild the string m_lastConstructDisplayed = obj->getConstructionPercent(); + sprintf(log_buffer, + "[GX-ISSUE144] Drawable construct text update drawable=%p percent=%d", + this, + m_lastConstructDisplayed); + fprintf(stderr, "%s\n", log_buffer); } @@ -4264,9 +4364,13 @@ const Matrix3D *Drawable::getTransformMatrix() const //------------------------------------------------------------------------------------------------- void Drawable::setCaptionText( const UnicodeString& captionText ) { + char log_buffer[512]; + if (captionText.isEmpty()) { clearCaptionText(); + sprintf(log_buffer, "[GX-ISSUE144] Drawable caption clear-request drawable=%p", this); + fprintf(stderr, "%s\n", log_buffer); return; } @@ -4276,12 +4380,15 @@ 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 ); + sprintf(log_buffer, + "[GX-ISSUE144] Drawable caption new drawable=%p textLength=%d font=%p", + this, + sanitizedString.getLength(), + font); + fprintf(stderr, "%s\n", log_buffer); } else { @@ -4289,6 +4396,11 @@ void Drawable::setCaptionText( const UnicodeString& captionText ) if( m_captionDisplayString->getText().compare(sanitizedString) != 0 ) { m_captionDisplayString->setText( sanitizedString ); + sprintf(log_buffer, + "[GX-ISSUE144] Drawable caption update drawable=%p textLength=%d", + this, + sanitizedString.getLength()); + fprintf(stderr, "%s\n", log_buffer); } } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarUnderConstruction.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarUnderConstruction.cpp index c150ac1baa6..1de36d94606 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarUnderConstruction.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBarUnderConstruction.cpp @@ -30,6 +30,8 @@ // USER INCLUDES ////////////////////////////////////////////////////////////////////////////////// #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine +#include + #include "Common/NameKeyGenerator.h" #include "Common/ThingTemplate.h" @@ -47,6 +49,8 @@ //------------------------------------------------------------------------------------------------- void ControlBar::updateConstructionTextDisplay( Object *obj ) { + // GeneralsX @tweak GitHubCopilot 27/05/2026 Trace under-construction text key usage and formatted output updates. + char log_buffer[512]; UnicodeString text; static UnsignedInt descID = TheNameKeyGenerator->nameToKey( "ControlBar.wnd:UnderConstructionDesc" ); GameWindow *descWindow = TheWindowManager->winGetWindowFromId( nullptr, descID ); @@ -58,6 +62,13 @@ void ControlBar::updateConstructionTextDisplay( Object *obj ) text.format( TheGameText->fetch( "CONTROLBAR:UnderConstructionDesc" ), obj->getConstructionPercent() ); GadgetStaticTextSetText( descWindow, text ); + sprintf(log_buffer, + "[GX-ISSUE144] UnderConstruction text update obj=%p percent=%d textLen=%d descWindow=%p", + obj, + obj->getConstructionPercent(), + text.getLength(), + descWindow); + fprintf(stderr, "%s\n", log_buffer); // record this as the last percentage displayed m_displayedConstructPercent = obj->getConstructionPercent(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarPopupDescription.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarPopupDescription.cpp index 10bfcb18b4d..63a7b3cb969 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarPopupDescription.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarPopupDescription.cpp @@ -46,6 +46,7 @@ //----------------------------------------------------------------------------- // SYSTEM INCLUDES //////////////////////////////////////////////////////////// //----------------------------------------------------------------------------- +#include //----------------------------------------------------------------------------- // USER INCLUDES ////////////////////////////////////////////////////////////// @@ -127,8 +128,22 @@ void ControlBarPopupDescriptionUpdateFunc( WindowLayout *layout, void *param ) // --------------------------------------------------------------------------------------- void ControlBar::showBuildTooltipLayout( GameWindow *cmdButton ) { + // GeneralsX @tweak GitHubCopilot 27/05/2026 Trace command tooltip population and cost-line visibility decisions. + char log_buffer[512]; + sprintf(log_buffer, + "[GX-ISSUE144] Tooltip show request cmdWindow=%p prevWindow=%p hidden=%d", + cmdButton, + prevWindow, + m_buildToolTipLayout ? m_buildToolTipLayout->isHidden() : -1); + fprintf(stderr, "%s\n", log_buffer); + if (TheInGameUI->areTooltipsDisabled() || TheScriptEngine->isGameEnding()) { + sprintf(log_buffer, + "[GX-ISSUE144] Tooltip show blocked tooltipsDisabled=%d gameEnding=%d", + TheInGameUI->areTooltipsDisabled(), + TheScriptEngine->isGameEnding()); + fprintf(stderr, "%s\n", log_buffer); return; } @@ -178,13 +193,21 @@ void ControlBar::showBuildTooltipLayout( GameWindow *cmdButton ) isInitialized = TRUE; if(!cmdButton) + { + sprintf(log_buffer, "[GX-ISSUE144] Tooltip show aborted null cmdButton"); + fprintf(stderr, "%s\n", log_buffer); return; + } if(BitIsSet(cmdButton->winGetStyle(), GWS_PUSH_BUTTON)) { const CommandButton *commandButton = (const CommandButton *)GadgetButtonGetData(cmdButton); if(!commandButton) + { + sprintf(log_buffer, "[GX-ISSUE144] Tooltip show aborted missing CommandButton window=%p", cmdButton); + fprintf(stderr, "%s\n", log_buffer); return; + } // note that, in this branch, ENABLE_SOLO_PLAY is ***NEVER*** defined... // this is so that we have a multiplayer build that cannot possibly be hacked @@ -209,6 +232,13 @@ void ControlBar::showBuildTooltipLayout( GameWindow *cmdButton ) // m_buildToolTipLayout = TheWindowManager->winCreateLayout( "ControlBarPopupDescription.wnd" ); // m_buildToolTipLayout->setUpdate(ControlBarPopupDescriptionUpdateFunc); + sprintf(log_buffer, + "[GX-ISSUE144] Tooltip show command=%s textLabel=%s descriptionLabel=%s", + commandButton->getName().str(), + commandButton->getTextLabel().str(), + commandButton->getDescriptionLabel().str()); + fprintf(stderr, "%s\n", log_buffer); + populateBuildTooltipLayout(commandButton); } else @@ -216,6 +246,8 @@ void ControlBar::showBuildTooltipLayout( GameWindow *cmdButton ) // we're a generic window if(!BitIsSet(cmdButton->winGetStyle(), GWS_USER_WINDOW) && !BitIsSet(cmdButton->winGetStyle(), GWS_STATIC_TEXT)) return; + sprintf(log_buffer, "[GX-ISSUE144] Tooltip show generic window style=0x%x", cmdButton->winGetStyle()); + fprintf(stderr, "%s\n", log_buffer); populateBuildTooltipLayout(nullptr, cmdButton); } m_buildToolTipLayout->hide(FALSE); @@ -233,18 +265,39 @@ void ControlBar::showBuildTooltipLayout( GameWindow *cmdButton ) void ControlBar::repopulateBuildTooltipLayout() { + char log_buffer[512]; if(!prevWindow || !m_buildToolTipLayout) + { + sprintf(log_buffer, + "[GX-ISSUE144] Tooltip repopulate skipped prevWindow=%p layout=%p", + prevWindow, + m_buildToolTipLayout); + fprintf(stderr, "%s\n", log_buffer); return; + } if(!BitIsSet(prevWindow->winGetStyle(), GWS_PUSH_BUTTON)) + { + sprintf(log_buffer, "[GX-ISSUE144] Tooltip repopulate skipped non-push style=0x%x", prevWindow->winGetStyle()); + fprintf(stderr, "%s\n", log_buffer); return; + } const CommandButton *commandButton = (const CommandButton *)GadgetButtonGetData(prevWindow); + sprintf(log_buffer, + "[GX-ISSUE144] Tooltip repopulate command=%s", + commandButton ? commandButton->getName().str() : ""); + fprintf(stderr, "%s\n", log_buffer); populateBuildTooltipLayout(commandButton); } void ControlBar::populateBuildTooltipLayout( const CommandButton *commandButton, GameWindow *tooltipWin) { + char log_buffer[512]; if(!m_buildToolTipLayout) + { + sprintf(log_buffer, "[GX-ISSUE144] Tooltip populate skipped missing layout"); + fprintf(stderr, "%s\n", log_buffer); return; + } Player *player = ThePlayerList->getLocalPlayer(); UnicodeString name, cost, descrip; @@ -256,6 +309,14 @@ void ControlBar::populateBuildTooltipLayout( const CommandButton *commandButton, if(commandButton) { + sprintf(log_buffer, + "[GX-ISSUE144] Tooltip populate command=%s type=%d textLabel=%s descriptionLabel=%s", + commandButton->getName().str(), + commandButton->getCommandType(), + commandButton->getTextLabel().str(), + commandButton->getDescriptionLabel().str()); + fprintf(stderr, "%s\n", log_buffer); + const ThingTemplate *thingTemplate = commandButton->getThingTemplate(); const UpgradeTemplate *upgradeTemplate = commandButton->getUpgradeTemplate(); @@ -604,10 +665,20 @@ void ControlBar::populateBuildTooltipLayout( const CommandButton *commandButton, { win->winHide( FALSE ); GadgetStaticTextSetText(win, cost); + sprintf(log_buffer, + "[GX-ISSUE144] Tooltip cost visible command=%s cost=%u", + commandButton ? commandButton->getName().str() : "", + costToBuild); + fprintf(stderr, "%s\n", log_buffer); } else { win->winHide( TRUE ); + sprintf(log_buffer, + "[GX-ISSUE144] Tooltip cost hidden command=%s cost=%u", + commandButton ? commandButton->getName().str() : "", + costToBuild); + fprintf(stderr, "%s\n", log_buffer); } } @@ -672,6 +743,13 @@ void ControlBar::populateBuildTooltipLayout( const CommandButton *commandButton, win->winSetSize(size.x, size.y + diffSize); GadgetStaticTextSetText(win, descrip); + sprintf(log_buffer, + "[GX-ISSUE144] Tooltip description updated command=%s nameLen=%d descLen=%d cost=%u", + commandButton ? commandButton->getName().str() : "", + name.getLength(), + descrip.getLength(), + costToBuild); + fprintf(stderr, "%s\n", log_buffer); } m_buildToolTipLayout->hide(FALSE); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp index 773c22dd94e..d95dd0d729f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManager.cpp @@ -31,6 +31,8 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine +#include + #include "Common/Debug.h" #include "Common/Language.h" #include "GameClient/Display.h" @@ -1379,14 +1381,30 @@ GameWindow *GameWindowManager::winCreate( GameWindow *parent, window->winSetInstanceData( instData ); // set default font + // GeneralsX @tweak GitHubCopilot 27/05/2026 Trace window default font resolution to diagnose localized font propagation. if (TheGlobalLanguageData && TheGlobalLanguageData->m_defaultWindowFont.name.isNotEmpty()) - { window->winSetFont( winFindFont( + { + char log_buffer[512]; + sprintf(log_buffer, + "[GX-ISSUE144] WinCreate default font localized name=%s size=%d bold=%d window=%p", + TheGlobalLanguageData->m_defaultWindowFont.name.str(), + TheGlobalLanguageData->m_defaultWindowFont.size, + TheGlobalLanguageData->m_defaultWindowFont.bold, + window); + fprintf(stderr, "%s\n", log_buffer); + + window->winSetFont( winFindFont( TheGlobalLanguageData->m_defaultWindowFont.name, TheGlobalLanguageData->m_defaultWindowFont.size, TheGlobalLanguageData->m_defaultWindowFont.bold) ); } else + { + char log_buffer[512]; + sprintf(log_buffer, "[GX-ISSUE144] WinCreate default font fallback Times New Roman size=14 bold=0 window=%p", window); + fprintf(stderr, "%s\n", log_buffer); window->winSetFont( winFindFont( "Times New Roman", 14, FALSE ) ); + } return window; @@ -2848,13 +2866,28 @@ void GameWindowManager::assignDefaultGadgetLook( GameWindow *gadget, else { if (TheGlobalLanguageData && TheGlobalLanguageData->m_defaultWindowFont.name.isNotEmpty()) - { gadget->winSetFont( winFindFont( + { + char log_buffer[512]; + sprintf(log_buffer, + "[GX-ISSUE144] assignDefaultGadgetLook localized font name=%s size=%d bold=%d gadget=%p", + TheGlobalLanguageData->m_defaultWindowFont.name.str(), + TheGlobalLanguageData->m_defaultWindowFont.size, + TheGlobalLanguageData->m_defaultWindowFont.bold, + gadget); + fprintf(stderr, "%s\n", log_buffer); + + gadget->winSetFont( winFindFont( TheGlobalLanguageData->m_defaultWindowFont.name, TheGlobalLanguageData->m_defaultWindowFont.size, TheGlobalLanguageData->m_defaultWindowFont.bold) ); } else + { + char log_buffer[512]; + sprintf(log_buffer, "[GX-ISSUE144] assignDefaultGadgetLook fallback font Times New Roman size=14 bold=0 gadget=%p", gadget); + fprintf(stderr, "%s\n", log_buffer); gadget->winSetFont( winFindFont( "Times New Roman", 14, FALSE ) ); + } } // if we don't want to assign default colors/images get out of here diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index 008c18773a6..6ca814058e3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -29,6 +29,8 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine +#include + #define DEFINE_SHADOW_NAMES #include "Common/ActionManager.h" @@ -1307,59 +1309,88 @@ InGameUI::~InGameUI() //------------------------------------------------------------------------------------------------- void InGameUI::init() { + // GeneralsX @tweak GitHubCopilot 27/05/2026 Trace final in-game UI font slots after language overrides. + char log_buffer[512]; + INI ini; ini.loadFileDirectory( "Data\\INI\\InGameUI", INI_LOAD_OVERWRITE, nullptr ); + sprintf(log_buffer, "[GX-ISSUE144] InGameUI init loaded Data\\INI\\InGameUI"); + fprintf(stderr, "%s\\n", log_buffer); //override INI values with language localized values: 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; } + + sprintf(log_buffer, + "[GX-ISSUE144] InGameUI font override drawableCaption=%s size=%d bold=%d defaultWindow=%s size=%d bold=%d unicode=%s", + m_drawableCaptionFont.str(), + m_drawableCaptionPointSize, + m_drawableCaptionBold, + TheGlobalLanguageData->m_defaultWindowFont.name.str(), + TheGlobalLanguageData->m_defaultWindowFont.size, + TheGlobalLanguageData->m_defaultWindowFont.bold, + TheGlobalLanguageData->m_unicodeFontName.isNotEmpty() ? TheGlobalLanguageData->m_unicodeFontName.str() : ""); + fprintf(stderr, "%s\\n", log_buffer); + } + else + { + sprintf(log_buffer, "[GX-ISSUE144] InGameUI init without TheGlobalLanguageData"); + fprintf(stderr, "%s\\n", log_buffer); } /**@ todo we used to put in the hint spy translator, but it's difficult @@ -1994,7 +2025,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 +4292,7 @@ VideoBuffer* InGameUI::videoBuffer() } // ------------------------------------------------------------------------------------------------ -// InGameUI::playMovie +// InGameUI::playCameoMovie // ------------------------------------------------------------------------------------------------ void InGameUI::playCameoMovie( const AsciiString& movieName ) { @@ -4565,11 +4596,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 +4647,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 +4922,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/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp index b91335f4e89..b2c1d7e4caa 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp @@ -45,6 +45,8 @@ // SYSTEM INCLUDES //////////////////////////////////////////////////////////// #include +#include +#include // USER INCLUDES ////////////////////////////////////////////////////////////// #include "Common/Debug.h" @@ -54,6 +56,75 @@ #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. +// GeneralsX @tweak GitHubCopilot 27/05/2026 Add explicit stderr tracing for Unicode fallback font lookup decisions. +// GeneralsX @bugfix GitHubCopilot 29/05/2026 Prevent circular Unicode fallback when the localized unicode family equals the base font family. +FontCharsClass *LoadUnicodeFallbackFont(Int size, Bool bold, const char *base_name) +{ + const char *preferred_name = nullptr; + char log_buffer[512]; + + if (TheGlobalLanguageData && TheGlobalLanguageData->m_unicodeFontName.isNotEmpty()) { + preferred_name = TheGlobalLanguageData->m_unicodeFontName.str(); + } + + sprintf(log_buffer, + "[GX-ISSUE144] W3DFont fallback start size=%d bold=%d preferred=%s base=%s", + size, + bold, + preferred_name ? preferred_name : "", + base_name ? base_name : ""); + fprintf(stderr, "%s\n", log_buffer); + + if (preferred_name != nullptr && (base_name == nullptr || strcmp(preferred_name, base_name) != 0)) { + FontCharsClass *font = WW3DAssetManager::Get_Instance()->Get_FontChars(preferred_name, size, bold); + if (font != nullptr) { + sprintf(log_buffer, "[GX-ISSUE144] W3DFont fallback hit preferred=%s", preferred_name); + fprintf(stderr, "%s\n", log_buffer); + return font; + } + + sprintf(log_buffer, "[GX-ISSUE144] W3DFont fallback miss preferred=%s", preferred_name); + fprintf(stderr, "%s\n", log_buffer); + } + else if (preferred_name != nullptr) { + sprintf(log_buffer, "[GX-ISSUE144] W3DFont fallback skip preferred=%s reason=same-as-base", preferred_name); + fprintf(stderr, "%s\n", log_buffer); + } + + 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) { + sprintf(log_buffer, "[GX-ISSUE144] W3DFont fallback hit list=%s", font_name); + fprintf(stderr, "%s\n", log_buffer); + return font; + } + + sprintf(log_buffer, "[GX-ISSUE144] W3DFont fallback miss list=%s", font_name); + fprintf(stderr, "%s\n", log_buffer); + } + + sprintf(log_buffer, "[GX-ISSUE144] W3DFont fallback exhausted size=%d bold=%d", size, bold); + fprintf(stderr, "%s\n", log_buffer); + + return nullptr; +} +} + // DEFINES //////////////////////////////////////////////////////////////////// // PRIVATE TYPES ////////////////////////////////////////////////////////////// @@ -73,6 +144,8 @@ //============================================================================= Bool W3DFontLibrary::loadFontData( GameFont *font ) { + char log_buffer[512]; + // sanity if( font == nullptr ) return FALSE; @@ -80,23 +153,34 @@ Bool W3DFontLibrary::loadFontData( GameFont *font ) const char* name = font->nameString.str(); const Int size = font->pointSize; const Bool bold = font->bold; + sprintf(log_buffer, "[GX-ISSUE144] W3DFont load request name=%s size=%d bold=%d", name ? name : "", size, bold); + fprintf(stderr, "%s\n", log_buffer); // get the font data from the asset manager FontCharsClass *fontChar = WW3DAssetManager::Get_Instance()->Get_FontChars( name, size, bold ); if( fontChar == nullptr ) { + sprintf(log_buffer, "[GX-ISSUE144] W3DFont load miss name=%s size=%d bold=%d", name ? name : "", size, bold); + fprintf(stderr, "%s\n", log_buffer); DEBUG_CRASH(( "Unable to find font '%s' in Asset Manager", name )); return FALSE; } + sprintf(log_buffer, "[GX-ISSUE144] W3DFont load hit name=%s size=%d bold=%d", name ? name : "", size, bold); + fprintf(stderr, "%s\n", log_buffer); + // assign font data font->fontData = fontChar; 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, name); + sprintf(log_buffer, + "[GX-ISSUE144] W3DFont alternate unicode %s for base=%s", + fontChar->AlternateUnicodeFont ? "assigned" : "missing", + name ? name : ""); + fprintf(stderr, "%s\n", log_buffer); return TRUE; } diff --git a/docs/DEV_BLOG/2026-05-DIARY.md b/docs/DEV_BLOG/2026-05-DIARY.md index 45f6267a7c6..c93f78e2abc 100644 --- a/docs/DEV_BLOG/2026-05-DIARY.md +++ b/docs/DEV_BLOG/2026-05-DIARY.md @@ -2,6 +2,7 @@ --- +<<<<<<< HEAD ## 2026-05-25: Compress agent instructions for lower context cost Compressed `AGENTS.md` and `.github/instructions/*.md` to cut prompt token @@ -21,6 +22,103 @@ Result: Follow-up: - keep using the compressed versions for future sessions - revisit `generalsx.instructions.md` later if more savings are needed +======= +## 2026-05-27: Implement Language.ini fallback chain for Issue #144 + +Following reporter feedback and [GX-ISSUE144] log analysis, implemented fallback +chain for Language.ini so stock values fill keys missing from override packages +(e.g., russifier's incomplete Language.ini). + +What was done: +- added fallback load step in `GlobalLanguage::init()` using `INI_LOAD_MULTIFILE` +- when `registryLanguage != "English"`, load stock `Data\English\Language` after + the primary override to fill missing keys +- specifically restores `UnicodeFontName="Arial Unicode MS"` when russifier only + defines override as plain `"Arial"` (which lacks Cyrillic on bundled fontconfig) +- updated logging to show primary + fallback states + +Validation: +- This mirrors the CSF fallback strategy from commit 3a28555 +- Expected to resolve circular fallback (Arial → Arial) by restoring Arial Unicode MS +- waiting for reporter test with this fix + +## 2026-05-26: Add runtime stderr instrumentation for Issue #144 diagnosis + +Added explicit runtime instrumentation to trace font resolution and tooltip/cost +rendering paths directly in executable stderr output for the active Issue #144 +investigation. + +What was done: +- added tagged logs with prefix `[GX-ISSUE144]` using `sprintf` + + `fprintf(stderr, ...)` in key paths: + - `W3DGameFont.cpp` font load + unicode fallback attempts + - `Drawable.cpp` caption/construct font resolution and text updates + - `GlobalLanguage.cpp` language ini source + localized font descriptor state + - `InGameUI.cpp` post-language font slot override results + - `GameWindowManager.cpp` default window/gadget font assignments + - `ControlBarPopupDescription.cpp` tooltip build-cost visibility and content + - `ControlBarUnderConstruction.cpp` under-construction text update flow +- kept instrumentation scoped to diagnostics only, without changing gameplay + logic. + +Validation: +- both platform build targets compiled locally during this session. +- no new diagnostics errors were reported in the edited files. +- logs identified root cause: circular fallback due to Language.ini not having + fallback chain (russifier sets UnicodeFontName=Arial, stock has Arial Unicode MS) +>>>>>>> 813cc7d9e (fix(issue-144): add stderr diagnostics traces) + +## 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` +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-22: Temporarily disable deterministic replay jobs in CI @@ -181,6 +279,25 @@ Impact: - instruction-file discovery and enforcement rules are explicit - `.github/copilot-instructions.md` no longer needs maintenance +## 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 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: '