From 61309481526d17f6a6864ad010c20b193c321672 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Sat, 10 Oct 2026 03:43:44 +0200 Subject: [PATCH 1/4] fix(filesystem): Only list archive subdirectories when asked --- .../Source/Common/System/ArchiveFile.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/ArchiveFile.cpp b/Core/GameEngine/Source/Common/System/ArchiveFile.cpp index 9e0db3f0ee2..ce82fa0b4a8 100644 --- a/Core/GameEngine/Source/Common/System/ArchiveFile.cpp +++ b/Core/GameEngine/Source/Common/System/ArchiveFile.cpp @@ -150,15 +150,17 @@ void ArchiveFile::getFileListInDirectory(const AsciiString& currentDirectory, co void ArchiveFile::getFileListInDirectory(const DetailedArchivedDirectoryInfo *dirInfo, const AsciiString& currentDirectory, const AsciiString& searchName, FilenameList &filenameList, Bool searchSubdirectories) const { - DetailedArchivedDirectoryInfoMap::const_iterator diriter = dirInfo->m_directories.begin(); - for (; diriter != dirInfo->m_directories.end(); ++diriter) { - const DetailedArchivedDirectoryInfo *tempDirInfo = &(diriter->second); - AsciiString tempDirName = currentDirectory; - if (!tempDirName.isEmpty() && !tempDirName.endsWith("\\")) { - tempDirName.concat('\\'); + if (searchSubdirectories) { + DetailedArchivedDirectoryInfoMap::const_iterator diriter = dirInfo->m_directories.begin(); + for (; diriter != dirInfo->m_directories.end(); ++diriter) { + const DetailedArchivedDirectoryInfo *tempDirInfo = &(diriter->second); + AsciiString tempDirName = currentDirectory; + if (!tempDirName.isEmpty() && !tempDirName.endsWith("\\")) { + tempDirName.concat('\\'); + } + tempDirName.concat(tempDirInfo->m_directoryName); + getFileListInDirectory(tempDirInfo, tempDirName, searchName, filenameList, searchSubdirectories); } - tempDirName.concat(tempDirInfo->m_directoryName); - getFileListInDirectory(tempDirInfo, tempDirName, searchName, filenameList, searchSubdirectories); } ArchivedFileInfoMap::const_iterator fileiter = dirInfo->m_files.begin(); From 0864a3378d09fa62cd23ceec193ac95b02cfe4c4 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:00:32 +0200 Subject: [PATCH 2/4] refactor(text): Parse string files into a vector --- .../GameEngine/Source/GameClient/GameText.cpp | 269 ++++-------------- 1 file changed, 56 insertions(+), 213 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/GameText.cpp b/Core/GameEngine/Source/GameClient/GameText.cpp index 9ada81db32c..e6a1c01a928 100644 --- a/Core/GameEngine/Source/GameClient/GameText.cpp +++ b/Core/GameEngine/Source/GameClient/GameText.cpp @@ -79,8 +79,6 @@ Bool g_useStringFile = TRUE; #define CSF_STRINGWITHWAVE ( ('S'<<24) | ('T'<<16) | ('R'<<8) | ('W') ) #define CSF_VERSION 3 -#define STRING_FILE 0 -#define CSF_FILE 1 #define MAX_UITEXT_LENGTH (10*1024) //---------------------------------------------------------------------------- // Private Types @@ -103,6 +101,8 @@ struct StringLookUp StringInfo *info; }; +typedef std::vector StringInfoVec; + //=============================== // CSFHeader //=============================== @@ -190,11 +190,9 @@ class GameTextManager : public GameTextInterface void readToEndOfQuote( File *file, Char *in, Char *out, Char *wavefile, Int maxBufLen ); 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 parseStringFile( const char *filename ); - Bool parseMapStringFile( const char *filename ); + Bool getCSFInfo( const Char *filename, LanguageID& language ); + Bool parseCSF( const Char *filename, StringInfoVec& out ); + Bool parseStringFile( const char *filename, StringInfoVec& out, Bool filterLanguage ); Bool readLine( char *buffer, Int max, File *file ); Char readChar( File *file ); }; @@ -288,7 +286,6 @@ void GameTextManager::init() { AsciiString csfFile; csfFile.format(g_csfFile, GetRegistryLanguage().str()); - Int format; if ( m_initialized ) { @@ -306,19 +303,20 @@ void GameTextManager::init() } #endif - if ( m_useStringFile && getStringCount( g_strFile, m_textCount ) ) - { - format = STRING_FILE; - } - else if ( getCSFInfo ( csfFile.str() ) ) - { - format = CSF_FILE; - } - else + StringInfoVec merged; + + // Generals.str replaces generals.csf if it loads. + if ( !m_useStringFile || !parseStringFile( g_strFile, merged, FALSE ) ) { - return; + merged.clear(); + if ( !getCSFInfo( csfFile.str(), m_language ) || !parseCSF( csfFile.str(), merged ) ) + { + return; + } } + m_textCount = (Int)merged.size(); + if( m_textCount == 0 ) { return; @@ -334,22 +332,7 @@ void GameTextManager::init() return; } - if ( format == STRING_FILE ) - { - if( parseStringFile( g_strFile ) == FALSE ) - { - deinit(); - return; - } - } - else - { - if ( !parseCSF ( csfFile.str() ) ) - { - deinit(); - return; - } - } + std::copy( merged.begin(), merged.end(), m_stringInfo ); m_stringLUT = NEW StringLookUp[m_textCount]; @@ -792,55 +775,11 @@ void GameTextManager::translateCopy( WideChar *outbuf, Char *inbuf ) *outbuf= 0; } -//============================================================================ -// GameTextManager::getStringCount -//============================================================================ - -Bool GameTextManager::getStringCount( const char *filename, Int& textCount ) -{ - Bool ok = TRUE; - - textCount = 0; - - File *file; - file = TheFileSystem->openFile(filename, File::READ | File::TEXT); - DEBUG_LOG(("Looking in %s for string file", filename)); - - if ( file == nullptr ) - { - return FALSE; - } - - while(ok) - { - if( !readLine( m_buffer, sizeof( m_buffer) -1, file ) ) - break; - removeLeadingAndTrailing ( m_buffer ); - - if( m_buffer[0] == '"' ) - { - Int len = strlen(m_buffer); - m_buffer[ len ] = '\n'; - m_buffer[ len+1] = 0; - readToEndOfQuote( file, &m_buffer[1], m_buffer2, m_buffer3, MAX_UITEXT_LENGTH ); - } - else if( stricmp( m_buffer, "END") == 0 ) - { - textCount++; - } - } - - textCount += 500; - file->close(); - file = nullptr; - return TRUE; -} - //============================================================================ // GameTextManager::getCSFInfo //============================================================================ -Bool GameTextManager::getCSFInfo ( const Char *filename ) +Bool GameTextManager::getCSFInfo( const Char *filename, LanguageID& language ) { CSFHeader header; Bool ok = FALSE; @@ -853,15 +792,13 @@ Bool GameTextManager::getCSFInfo ( const Char *filename ) { if ( header.id == CSF_ID ) { - m_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; @@ -879,7 +816,7 @@ Bool GameTextManager::getCSFInfo ( const Char *filename ) // GameTextManager::parseCSF //============================================================================ -Bool GameTextManager::parseCSF( const Char *filename ) +Bool GameTextManager::parseCSF( const Char *filename, StringInfoVec& out ) { File *file; Int id; @@ -900,17 +837,25 @@ Bool GameTextManager::parseCSF( const Char *filename ) return FALSE; } + if ( header.id != CSF_ID ) + { + goto quit; + } + + out.reserve( header.num_labels ); + while( file->read ( &id, sizeof (id)) == sizeof ( id) ) { Int num; Int num_strings; + StringInfo info; if ( id != CSF_LABEL ) { goto quit; } - file->read ( &num_strings, sizeof ( Int )); + file->read ( &num_strings, sizeof ( Int ) ); file->read ( &len, sizeof ( Int ) ); @@ -921,8 +866,7 @@ Bool GameTextManager::parseCSF( const Char *filename ) m_buffer[len] = 0; - m_stringInfo[listCount].label = m_buffer; - + info.label = m_buffer; if ( len > m_maxLabelLen ) { @@ -933,14 +877,14 @@ Bool GameTextManager::parseCSF( const Char *filename ) while ( num < num_strings ) { - file->read ( &id, sizeof ( Int ) ); + file->read ( &id, sizeof ( Int ) ); if ( id != CSF_STRING && id != CSF_STRINGWITHWAVE ) { goto quit; } - file->read ( &len, sizeof ( Int ) ); + file->read ( &len, sizeof ( Int ) ); if ( len ) { @@ -965,12 +909,12 @@ Bool GameTextManager::parseCSF( const Char *filename ) } stripSpaces ( m_tbuffer ); - m_stringInfo[listCount].text = m_tbuffer; + info.text = m_tbuffer; } if ( id == CSF_STRINGWITHWAVE ) { - file->read ( &len, sizeof ( Int ) ); + file->read ( &len, sizeof ( Int ) ); if ( len ) { file->read ( m_buffer, len ); @@ -980,7 +924,7 @@ Bool GameTextManager::parseCSF( const Char *filename ) if ( num == 0 && len ) { // only use the first string found - m_stringInfo[listCount].speech = m_buffer; + info.speech = m_buffer; } } @@ -988,6 +932,7 @@ Bool GameTextManager::parseCSF( const Char *filename ) num++; } + out.push_back( info ); listCount++; } @@ -1006,9 +951,8 @@ Bool GameTextManager::parseCSF( const Char *filename ) // GameTextManager::parseStringFile //============================================================================ -Bool GameTextManager::parseStringFile( const char *filename ) +Bool GameTextManager::parseStringFile( const char *filename, StringInfoVec& out, Bool filterLanguage ) { - Int listCount = 0; Bool ok = TRUE; File *file = TheFileSystem->openFile(filename, File::READ | File::TEXT); @@ -1021,6 +965,8 @@ Bool GameTextManager::parseStringFile( const char *filename ) while( ok ) { Int len; + StringInfo info; + if( !readLine( m_buffer, MAX_UITEXT_LENGTH, file )) { break; @@ -1033,18 +979,17 @@ Bool GameTextManager::parseStringFile( const char *filename ) // make sure label is unique - for ( Int i = 0; i < listCount; i++ ) + for ( StringInfoVec::const_iterator it = out.begin(); it != out.end(); ++it ) { - if ( stricmp ( m_stringInfo[i].label.str(), m_buffer ) == 0) + if ( stricmp ( it->label.str(), m_buffer ) == 0) { DEBUG_CRASH ( ("String label '%s' multiply defined!", m_buffer )); } } - m_stringInfo[listCount].label = m_buffer; + info.label = m_buffer; len = strlen ( m_buffer ); - if ( len > m_maxLabelLen ) { m_maxLabelLen = len; @@ -1069,11 +1014,10 @@ Bool GameTextManager::parseStringFile( const char *filename ) m_buffer[ len+1] = 0; readToEndOfQuote( file, &m_buffer[1], m_buffer2, m_buffer3, MAX_UITEXT_LENGTH ); - if ( readString ) { - // only one string per label allows - DEBUG_CRASH ( ("String label '%s' has more than one string defined!", m_stringInfo[listCount].label.str())); + // only one string per label allowed + DEBUG_CRASH ( ("String label '%s' has more than one string defined!", info.label.str())); } else { @@ -1081,8 +1025,12 @@ Bool GameTextManager::parseStringFile( const char *filename ) translateCopy( m_tbuffer, m_buffer2 ); stripSpaces ( m_tbuffer ); - m_stringInfo[listCount].text = m_tbuffer ; - m_stringInfo[listCount].speech = m_buffer3; + UnicodeString text = UnicodeString(m_tbuffer); + if ( filterLanguage && TheLanguageFilter ) + TheLanguageFilter->filterLine(text); + + info.text = text; + info.speech = m_buffer3; readString = TRUE; } } @@ -1092,7 +1040,7 @@ Bool GameTextManager::parseStringFile( const char *filename ) } } - listCount++; + out.push_back( info ); } quit: @@ -1109,12 +1057,12 @@ Bool GameTextManager::parseStringFile( const char *filename ) void GameTextManager::initMapStringFile( const AsciiString& filename ) { - m_mapTextCount = 0; - getStringCount( filename.str(), m_mapTextCount ); + StringInfoVec parsed; + parseStringFile( filename.str(), parsed, TRUE ); + m_mapTextCount = (Int)parsed.size(); m_mapStringInfo = NEW StringInfo[m_mapTextCount]; - - parseMapStringFile( filename.str() ); + std::copy( parsed.begin(), parsed.end(), m_mapStringInfo ); m_mapStringLUT = NEW StringLookUp[m_mapTextCount]; @@ -1132,111 +1080,6 @@ void GameTextManager::initMapStringFile( const AsciiString& filename ) qsort( m_mapStringLUT, m_mapTextCount, sizeof(StringLookUp), compareLUT ); } -//============================================================================ -// GameTextManager::parseMapStringFile -//============================================================================ - -Bool GameTextManager::parseMapStringFile( const char *filename ) -{ - Int listCount = 0; - Bool ok = TRUE; - - File *file; - - file = TheFileSystem->openFile(filename, File::READ | File::TEXT); - if ( file == nullptr ) - { - return FALSE; - } - - while( ok ) - { - Int len; - if( !readLine( m_buffer, MAX_UITEXT_LENGTH, file )) - { - break; - } - - removeLeadingAndTrailing ( m_buffer ); - - if( ( *(unsigned short *)m_buffer == 0x2F2F) || !m_buffer[0]) // 0x2F2F is Hex for // - continue; - - // make sure label is unique - - for ( Int i = 0; i < listCount; i++ ) - { - if ( stricmp ( m_mapStringInfo[i].label.str(), m_buffer ) == 0) - { - DEBUG_CRASH ( ("String label '%s' multiply defined!", m_buffer )); - } - } - - m_mapStringInfo[listCount].label = m_buffer; - len = strlen ( m_buffer ); - - - if ( len > m_maxLabelLen ) - { - m_maxLabelLen = len; - } - - Bool readString = FALSE; - while( ok ) - { - if (!readLine ( m_buffer, sizeof(m_buffer)-1, file )) - { - DEBUG_CRASH (("Unexpected end of string file")); - ok = FALSE; - goto quit; - } - - removeLeadingAndTrailing ( m_buffer ); - - if( m_buffer[0] == '"' ) - { - len = strlen(m_buffer); - m_buffer[ len ] = '\n'; - m_buffer[ len+1] = 0; - readToEndOfQuote( file, &m_buffer[1], m_buffer2, m_buffer3, MAX_UITEXT_LENGTH ); - - - if ( readString ) - { - // only one string per label allowed - DEBUG_CRASH ( ("String label '%s' has more than one string defined!", m_stringInfo[listCount].label.str())); - } - else - { - // Copy string into new home - translateCopy( m_tbuffer, m_buffer2 ); - stripSpaces ( m_tbuffer ); - - UnicodeString text = UnicodeString(m_tbuffer); - if (TheLanguageFilter) - TheLanguageFilter->filterLine(text); - - m_mapStringInfo[listCount].text = text; - m_mapStringInfo[listCount].speech = m_buffer3; - readString = TRUE; - } - } - else if ( stricmp ( m_buffer, "END" ) == 0) - { - break; - } - } - - listCount++; - } - -quit: - - file->close(); - file = nullptr; - - return ok; -} //============================================================================ // *GameTextManager::fetch From c62f9936e41613a9c8dcfa04a7268457d2ac8ec9 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:00:32 +0200 Subject: [PATCH 3/4] fix(text): Reject malformed string files --- .../GameEngine/Source/GameClient/GameText.cpp | 137 +++++++++++++++--- 1 file changed, 113 insertions(+), 24 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/GameText.cpp b/Core/GameEngine/Source/GameClient/GameText.cpp index e6a1c01a928..b0d356bdaca 100644 --- a/Core/GameEngine/Source/GameClient/GameText.cpp +++ b/Core/GameEngine/Source/GameClient/GameText.cpp @@ -187,7 +187,7 @@ class GameTextManager : public GameTextInterface void stripSpaces ( WideChar *string ); void removeLeadingAndTrailing ( Char *m_buffer ); - void readToEndOfQuote( File *file, Char *in, Char *out, Char *wavefile, Int maxBufLen ); + Bool readToEndOfQuote( File *file, Char *in, Char *out, Char *wavefile, Int maxBufLen ); void reverseWord ( Char *file, Char *lp ); void translateCopy( WideChar *outbuf, Char *inbuf ); Bool getCSFInfo( const Char *filename, LanguageID& language ); @@ -479,7 +479,7 @@ void GameTextManager::removeLeadingAndTrailing ( Char *buffer ) // GameTextManager::readToEndOfQuote //============================================================================ -void GameTextManager::readToEndOfQuote( File *file, Char *in, Char *out, Char *wavefile, Int maxBufLen ) +Bool GameTextManager::readToEndOfQuote( File *file, Char *in, Char *out, Char *wavefile, Int maxBufLen ) { Int slash = FALSE; Int state = 0; @@ -488,8 +488,11 @@ void GameTextManager::readToEndOfQuote( File *file, Char *in, Char *out, Char *w Int ccount = 0; Int len = 0; Int done = FALSE; + Bool closed = FALSE; + // Room for the 'e' suffix and the terminator. + const Int maxWaveLen = maxBufLen - 2; - while ( maxBufLen ) + while ( maxBufLen > 1 ) { // get next Char @@ -506,9 +509,11 @@ void GameTextManager::readToEndOfQuote( File *file, Char *in, Char *out, Char *w ch = readChar ( file ); } - if ( ch == EOF ) + if ( ch == 0 ) { - return ; + *out = 0; + *wavefile = 0; + return FALSE; } if ( ch == '\n' ) @@ -528,6 +533,7 @@ void GameTextManager::readToEndOfQuote( File *file, Char *in, Char *out, Char *w } else if ( ch == '"' && !slash ) { + closed = TRUE; break; // done } else @@ -546,6 +552,12 @@ void GameTextManager::readToEndOfQuote( File *file, Char *in, Char *out, Char *w *out = 0; + if ( !closed ) + { + *wavefile = 0; + return FALSE; + } + while ( !done ) { // get next Char @@ -563,7 +575,7 @@ void GameTextManager::readToEndOfQuote( File *file, Char *in, Char *out, Char *w ch = readChar ( file ); } - if ( ch == '\n' || ch == EOF ) + if ( ch == '\n' || ch == 0 ) { break; } @@ -582,6 +594,11 @@ void GameTextManager::readToEndOfQuote( File *file, Char *in, Char *out, Char *w case 1: if ( ( ch >= 'a' && ch <= 'z') || ( ch >= 'A' && ch <='Z') || (ch >= '0' && ch <= '9') || ch == '_' ) { + if ( len >= maxWaveLen ) + { + *wavefile = 0; + return FALSE; + } *wavefile++ = ch; len++; break; @@ -604,6 +621,7 @@ void GameTextManager::readToEndOfQuote( File *file, Char *in, Char *out, Char *w } } + return TRUE; } @@ -822,6 +840,7 @@ Bool GameTextManager::parseCSF( const Char *filename, StringInfoVec& out ) Int id; Int len; Int listCount = 0; + Int stringCount = 0; Bool ok = FALSE; CSFHeader header; @@ -834,18 +853,25 @@ Bool GameTextManager::parseCSF( const Char *filename, StringInfoVec& out ) if ( file->read ( &header, sizeof ( CSFHeader)) != sizeof ( CSFHeader) ) { - return FALSE; + goto quit; } - if ( header.id != CSF_ID ) + // A label record is at least 12 bytes. + if ( header.id != CSF_ID || header.num_labels < 0 || header.num_labels > file->size() / 12 || + header.num_strings < 0 ) { goto quit; } out.reserve( header.num_labels ); - while( file->read ( &id, sizeof (id)) == sizeof ( id) ) + while ( TRUE ) { + Int bytesRead = file->read( &id, sizeof(id) ); + if ( bytesRead == 0 ) + break; + if ( bytesRead != sizeof(id) ) + goto quit; Int num; Int num_strings; StringInfo info; @@ -855,13 +881,28 @@ Bool GameTextManager::parseCSF( const Char *filename, StringInfoVec& out ) goto quit; } - file->read ( &num_strings, sizeof ( Int ) ); + if ( file->read ( &num_strings, sizeof ( Int ) ) != (Int)(sizeof ( Int )) || + num_strings < 0 || num_strings > header.num_strings - stringCount ) + { + goto quit; + } + + if ( file->read ( &len, sizeof ( Int ) ) != (Int)(sizeof ( Int )) ) + { + goto quit; + } - file->read ( &len, sizeof ( Int ) ); + if ( len < 0 || len >= MAX_UITEXT_LENGTH ) + { + goto quit; + } if ( len ) { - file->read ( m_buffer, len ); + if ( file->read ( m_buffer, len ) != (Int)(len) ) + { + goto quit; + } } m_buffer[len] = 0; @@ -877,18 +918,32 @@ Bool GameTextManager::parseCSF( const Char *filename, StringInfoVec& out ) while ( num < num_strings ) { - file->read ( &id, sizeof ( Int ) ); + if ( file->read ( &id, sizeof ( Int ) ) != (Int)(sizeof ( Int )) ) + { + goto quit; + } if ( id != CSF_STRING && id != CSF_STRINGWITHWAVE ) { goto quit; } - file->read ( &len, sizeof ( Int ) ); + if ( file->read ( &len, sizeof ( Int ) ) != (Int)(sizeof ( Int )) ) + { + goto quit; + } + + if ( len < 0 || len >= MAX_UITEXT_LENGTH*2 ) + { + goto quit; + } if ( len ) { - file->read ( m_tbuffer, len*sizeof(WideChar) ); + if ( file->read ( m_tbuffer, len*sizeof(WideChar) ) != (Int)(len*sizeof(WideChar)) ) + { + goto quit; + } } if ( num == 0 ) @@ -914,10 +969,20 @@ Bool GameTextManager::parseCSF( const Char *filename, StringInfoVec& out ) if ( id == CSF_STRINGWITHWAVE ) { - file->read ( &len, sizeof ( Int ) ); + if ( file->read ( &len, sizeof ( Int ) ) != (Int)(sizeof ( Int )) ) + { + goto quit; + } + if ( len < 0 || len >= MAX_UITEXT_LENGTH ) + { + goto quit; + } if ( len ) { - file->read ( m_buffer, len ); + if ( file->read ( m_buffer, len ) != (Int)(len) ) + { + goto quit; + } } m_buffer[len] = 0; @@ -934,9 +999,10 @@ Bool GameTextManager::parseCSF( const Char *filename, StringInfoVec& out ) out.push_back( info ); listCount++; + stringCount += num_strings; } - ok = TRUE; + ok = ( listCount == (Int)header.num_labels && stringCount == header.num_strings ); quit: @@ -967,11 +1033,17 @@ Bool GameTextManager::parseStringFile( const char *filename, StringInfoVec& out, Int len; StringInfo info; - if( !readLine( m_buffer, MAX_UITEXT_LENGTH, file )) + if( !readLine( m_buffer, sizeof(m_buffer)-1, file )) { break; } + if ( strlen( m_buffer ) >= sizeof(m_buffer)-1 ) + { + ok = FALSE; + goto quit; + } + removeLeadingAndTrailing ( m_buffer ); if( ( *(unsigned short *)m_buffer == 0x2F2F) || !m_buffer[0]) // 0x2F2F is Hex for // @@ -998,13 +1070,19 @@ Bool GameTextManager::parseStringFile( const char *filename, StringInfoVec& out, Bool readString = FALSE; while( ok ) { - if (!readLine ( m_buffer, sizeof(m_buffer)-1, file )) + if (!readLine ( m_buffer, sizeof(m_buffer)-2, file )) { DEBUG_CRASH (("Unexpected end of string file")); ok = FALSE; goto quit; } + if ( strlen( m_buffer ) >= sizeof(m_buffer)-2 ) + { + ok = FALSE; + goto quit; + } + removeLeadingAndTrailing ( m_buffer ); if( m_buffer[0] == '"' ) @@ -1012,16 +1090,18 @@ Bool GameTextManager::parseStringFile( const char *filename, StringInfoVec& out, len = strlen(m_buffer); m_buffer[ len ] = '\n'; m_buffer[ len+1] = 0; - readToEndOfQuote( file, &m_buffer[1], m_buffer2, m_buffer3, MAX_UITEXT_LENGTH ); + if ( !readToEndOfQuote( file, &m_buffer[1], m_buffer2, m_buffer3, MAX_UITEXT_LENGTH ) ) + { + ok = FALSE; + goto quit; + } if ( readString ) { - // only one string per label allowed DEBUG_CRASH ( ("String label '%s' has more than one string defined!", info.label.str())); } else { - // Copy string into new home translateCopy( m_tbuffer, m_buffer2 ); stripSpaces ( m_tbuffer ); @@ -1040,6 +1120,12 @@ Bool GameTextManager::parseStringFile( const char *filename, StringInfoVec& out, } } + if ( !readString ) + { + ok = FALSE; + goto quit; + } + out.push_back( info ); } @@ -1058,7 +1144,10 @@ Bool GameTextManager::parseStringFile( const char *filename, StringInfoVec& out, void GameTextManager::initMapStringFile( const AsciiString& filename ) { StringInfoVec parsed; - parseStringFile( filename.str(), parsed, TRUE ); + if ( !parseStringFile( filename.str(), parsed, TRUE ) ) + { + parsed.clear(); + } m_mapTextCount = (Int)parsed.size(); m_mapStringInfo = NEW StringInfo[m_mapTextCount]; From 4e268ac3f1e0e0aa07a35d05f529df11bd918469 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:00:33 +0200 Subject: [PATCH 4/4] feat(text): Load extra string files next to the string table --- .../GameEngine/Source/GameClient/GameText.cpp | 97 ++++++++++++++++++- 1 file changed, 94 insertions(+), 3 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/GameText.cpp b/Core/GameEngine/Source/GameClient/GameText.cpp index b0d356bdaca..3ba6b49ba1d 100644 --- a/Core/GameEngine/Source/GameClient/GameText.cpp +++ b/Core/GameEngine/Source/GameClient/GameText.cpp @@ -102,6 +102,37 @@ struct StringLookUp }; typedef std::vector StringInfoVec; +typedef std::set > LabelSet; + +// Name order, .str before .csf of the same name. +struct StringFileOrder +{ + Bool operator()( const AsciiString& a, const AsciiString& b ) const + { + const char *extA = a.reverseFind( '.' ); + const char *extB = b.reverseFind( '.' ); + Int lenA = a.getLength(); + if ( extA ) + lenA = (Int)(extA - a.str()); + Int lenB = b.getLength(); + if ( extB ) + lenB = (Int)(extB - b.str()); + Int compareLength = lenA; + if ( lenB < compareLength ) + compareLength = lenB; + Int cmp = _strnicmp( a.str(), b.str(), compareLength ); + if ( cmp != 0 ) + return cmp < 0; + if ( lenA != lenB ) + return lenA < lenB; + if ( !extA || !extB ) + return FALSE; + Bool isStrA = stricmp( extA, ".str" ) == 0; + Bool isStrB = stricmp( extB, ".str" ) == 0; + return isStrA && !isStrB; + } +}; +typedef std::set StringFileList; //=============================== // CSFHeader @@ -195,6 +226,9 @@ class GameTextManager : public GameTextInterface Bool parseStringFile( const char *filename, StringInfoVec& out, Bool filterLanguage ); Bool readLine( char *buffer, Int max, File *file ); Char readChar( File *file ); + + void collectStringFiles( const AsciiString& directory, StringFileList& files ); + Bool mergeStringFile( const AsciiString& filename, StringInfoVec& merged, LabelSet& labels ); }; static int __cdecl compareLUT ( const void *, const void*); @@ -303,13 +337,32 @@ void GameTextManager::init() } #endif + // TheSuperHackers @feature arcticdolphin 27/09/2026 Load every *.str and *.csf in data\\ and data\ before the string file. The first file to define a label wins. StringInfoVec merged; + LabelSet labels; + + AsciiString languageDir; + languageDir.format( "data\\%s\\", GetRegistryLanguage().str() ); + StringFileList languageFiles, neutralFiles; + collectStringFiles( languageDir, languageFiles ); + collectStringFiles( "data\\", neutralFiles ); + languageFiles.erase( csfFile ); + neutralFiles.erase( AsciiString( g_strFile ) ); + + StringFileList::const_iterator it; + for ( it = languageFiles.begin(); it != languageFiles.end(); ++it ) + { + mergeStringFile( *it, merged, labels ); + } + for ( it = neutralFiles.begin(); it != neutralFiles.end(); ++it ) + { + mergeStringFile( *it, merged, labels ); + } // Generals.str replaces generals.csf if it loads. - if ( !m_useStringFile || !parseStringFile( g_strFile, merged, FALSE ) ) + if ( !m_useStringFile || !mergeStringFile( AsciiString( g_strFile ), merged, labels ) ) { - merged.clear(); - if ( !getCSFInfo( csfFile.str(), m_language ) || !parseCSF( csfFile.str(), merged ) ) + if ( !getCSFInfo( csfFile.str(), m_language ) || !mergeStringFile( csfFile, merged, labels ) ) { return; } @@ -351,6 +404,44 @@ void GameTextManager::init() } +void GameTextManager::collectStringFiles( const AsciiString& directory, StringFileList& files ) +{ + FilenameList found; + TheFileSystem->getFileListInDirectory( directory, "*.str", found, FALSE ); + TheFileSystem->getFileListInDirectory( directory, "*.csf", found, FALSE ); + files.insert( found.begin(), found.end() ); +} + +Bool GameTextManager::mergeStringFile( const AsciiString& filename, StringInfoVec& merged, LabelSet& labels ) +{ + const char *ext = filename.reverseFind('.'); + Bool isStr = ext && stricmp( ext, ".str" ) == 0; + + StringInfoVec parsed; + Bool ok = isStr ? parseStringFile( filename.str(), parsed, FALSE ) + : parseCSF( filename.str(), parsed ); + + if ( !ok ) + { + DEBUG_LOG(("GameText: Failed to parse string file '%s', skipping", filename.str())); + return FALSE; + } + + Int skippedCount = 0; + for ( StringInfoVec::const_iterator it = parsed.begin(); it != parsed.end(); ++it ) + { + if ( !labels.insert( it->label ).second ) + { + skippedCount++; + continue; + } + merged.push_back( *it ); + } + + DEBUG_LOG(("GameText: Loaded string file '%s' (%d entries, %d already defined)", filename.str(), (Int)parsed.size(), skippedCount)); + return TRUE; +} + //============================================================================ // GameTextManager::deinit //============================================================================