diff --git a/Core/GameEngine/Include/Common/ArchiveFileSystem.h b/Core/GameEngine/Include/Common/ArchiveFileSystem.h index 4eb6371f1e3..46bb90168ac 100644 --- a/Core/GameEngine/Include/Common/ArchiveFileSystem.h +++ b/Core/GameEngine/Include/Common/ArchiveFileSystem.h @@ -123,6 +123,13 @@ class ArchivedFileInfo class ArchiveFileSystem : public SubsystemInterface { public: + enum ArchiveFilter + { + ARCHIVE_FILTER_ALL = 0, + ARCHIVE_FILTER_MOD_ONLY, + ARCHIVE_FILTER_NON_MOD_ONLY + }; + ArchiveFileSystem(); virtual ~ArchiveFileSystem() override; @@ -135,16 +142,24 @@ class ArchiveFileSystem : public SubsystemInterface // File operations virtual File* openFile( const Char *filename, Int access = 0, FileInstance instance = 0); ///< Search Archive files for specified file name and open it if found + File* openFile( const Char *filename, Int access, FileInstance instance, ArchiveFilter filter ); virtual void closeAllFiles() = 0; ///< Close all files associated with Archive files virtual Bool doesFileExist(const Char *filename, FileInstance instance = 0) const; ///< return true if that file exists in an archive file somewhere. + Bool doesFileExist(const Char *filename, FileInstance instance, ArchiveFilter filter) const; void getFileListInDirectory(const AsciiString& currentDirectory, const AsciiString& originalDirectory, const AsciiString& searchName, FilenameList &filenameList, Bool searchSubdirectories) const; ///< search the given directory for files matching the searchName (egs. *.ini, *.rep). Possibly search subdirectories. Scans each Archive file. Bool getFileInfo(const AsciiString& filename, FileInfo *fileInfo, FileInstance instance = 0) const; ///< see FileSystem.h + Bool getFileInfo(const AsciiString& filename, FileInfo *fileInfo, FileInstance instance, ArchiveFilter filter) const; virtual Bool loadBigFilesFromDirectory(AsciiString dir, AsciiString fileMask, Bool overwrite = FALSE) = 0; void loadMods(); + // GeneralsX @bugfix felipebraz 02/10/2026 Check if a file is provided by an overriding mod archive (!*.big or script files) + Bool hasModArchiveOverride(const Char *filename) const; + FileInstance getFileCount(const Char *filename, ArchiveFilter filter = ARCHIVE_FILTER_ALL) const; + Bool isModArchive(ArchiveFile *archive, const Char *filename) const; + ArchivedDirectoryInfo* friend_getArchivedDirectoryInfo(const Char* directory); protected: @@ -157,7 +172,7 @@ class ArchiveFileSystem : public SubsystemInterface AsciiString lastToken; ///< Synonymous for file name if the search directory was a file path }; - ArchiveFile* getArchiveFile(const AsciiString& filename, FileInstance instance = 0) const; + ArchiveFile* getArchiveFile(const AsciiString& filename, FileInstance instance = 0, ArchiveFilter filter = ARCHIVE_FILTER_ALL) const; ArchivedDirectoryInfoResult getArchivedDirectoryInfo(const Char* directory); virtual void loadIntoDirectoryTree(ArchiveFile *archiveFile, Bool overwrite = FALSE); ///< load the archive file's header information and apply it to the global archive directory tree. diff --git a/Core/GameEngine/Include/GameLogic/SidesList.h b/Core/GameEngine/Include/GameLogic/SidesList.h index 3713fc1cc35..5cb5815b8ba 100644 --- a/Core/GameEngine/Include/GameLogic/SidesList.h +++ b/Core/GameEngine/Include/GameLogic/SidesList.h @@ -154,6 +154,8 @@ class SidesList : public SubsystemInterface, inline SidesInfo *getSideInfo(Int side); inline SidesInfo *getSkirmishSideInfo(Int side); Int getNumSkirmishSides() { return m_numSkirmishSides; } + // GeneralsX @bugfix fbraz3 05/10/2026 Allow importing missing skirmish player definitions from SkirmishScripts chunk (#335). + void addSkirmishSide(const Dict* d); SidesInfo *findSideInfo(AsciiString name, Int* index = nullptr); SidesInfo *findSkirmishSideInfo(AsciiString name, Int* index = nullptr); @@ -180,7 +182,9 @@ class SidesList : public SubsystemInterface, enum { - MAX_TEAM_DEPTH = 3 + MAX_TEAM_DEPTH = 3, + // GeneralsX @bugfix fbraz3 05/10/2026 Expand skirmish sides capacity to accommodate mod rosters on custom maps (#335). + MAX_SKIRMISH_SIDES = 64 }; protected: @@ -194,7 +198,8 @@ class SidesList : public SubsystemInterface, SidesInfo m_sides[MAX_PLAYER_COUNT]; Int m_numSkirmishSides; - SidesInfo m_skirmishSides[MAX_PLAYER_COUNT]; + // GeneralsX @bugfix fbraz3 05/10/2026 Size skirmish sides array with MAX_SKIRMISH_SIDES to prevent buffer overflow (#335). + SidesInfo m_skirmishSides[MAX_SKIRMISH_SIDES]; TeamsInfoRec m_teamrec; TeamsInfoRec m_skirmishTeamrec; diff --git a/Core/GameEngine/Source/Common/System/ArchiveFileSystem.cpp b/Core/GameEngine/Source/Common/System/ArchiveFileSystem.cpp index 69a91ac21b9..3a50e6cf549 100644 --- a/Core/GameEngine/Source/Common/System/ArchiveFileSystem.cpp +++ b/Core/GameEngine/Source/Common/System/ArchiveFileSystem.cpp @@ -245,14 +245,17 @@ void ArchiveFileSystem::loadMods() Bool ArchiveFileSystem::doesFileExist(const Char *filename, FileInstance instance) const { - ArchivedDirectoryInfoResult result = const_cast(this)->getArchivedDirectoryInfo(filename); + return doesFileExist(filename, instance, ARCHIVE_FILTER_ALL); +} - if (!result.valid()) +Bool ArchiveFileSystem::doesFileExist(const Char *filename, FileInstance instance, ArchiveFilter filter) const +{ + if (filename == nullptr || filename[0] == '\0') + { return false; + } - stl::const_range range = stl::get_range(result.dirInfo->m_files, result.lastToken, instance); - - return range.valid(); + return getArchiveFile(AsciiString(filename), instance, filter) != nullptr; } ArchivedDirectoryInfo* ArchiveFileSystem::friend_getArchivedDirectoryInfo(const Char* directory) @@ -296,7 +299,17 @@ ArchiveFileSystem::ArchivedDirectoryInfoResult ArchiveFileSystem::getArchivedDir File * ArchiveFileSystem::openFile(const Char *filename, Int access, FileInstance instance) { - ArchiveFile* archive = getArchiveFile(filename, instance); + return openFile(filename, access, instance, ARCHIVE_FILTER_ALL); +} + +File * ArchiveFileSystem::openFile(const Char *filename, Int access, FileInstance instance, ArchiveFilter filter) +{ + if (filename == nullptr || filename[0] == '\0') + { + return nullptr; + } + + ArchiveFile* archive = getArchiveFile(AsciiString(filename), instance, filter); if (archive == nullptr) return nullptr; @@ -306,15 +319,16 @@ File * ArchiveFileSystem::openFile(const Char *filename, Int access, FileInstanc Bool ArchiveFileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo, FileInstance instance) const { - if (fileInfo == nullptr) { - return FALSE; - } + return getFileInfo(filename, fileInfo, instance, ARCHIVE_FILTER_ALL); +} - if (filename.isEmpty()) { +Bool ArchiveFileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo, FileInstance instance, ArchiveFilter filter) const +{ + if (fileInfo == nullptr || filename.isEmpty()) { return FALSE; } - ArchiveFile* archive = getArchiveFile(filename, instance); + ArchiveFile* archive = getArchiveFile(filename, instance, filter); if (archive == nullptr) return FALSE; @@ -322,19 +336,37 @@ Bool ArchiveFileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileI return archive->getFileInfo(filename, fileInfo); } -ArchiveFile* ArchiveFileSystem::getArchiveFile(const AsciiString& filename, FileInstance instance) const +ArchiveFile* ArchiveFileSystem::getArchiveFile(const AsciiString& filename, FileInstance instance, ArchiveFilter filter) const { ArchivedDirectoryInfoResult result = const_cast(this)->getArchivedDirectoryInfo(filename.str()); if (!result.valid()) return nullptr; - stl::const_range range = stl::get_range(result.dirInfo->m_files, result.lastToken, instance); + std::pair range = + result.dirInfo->m_files.equal_range(result.lastToken); - if (!range.valid()) - return nullptr; - - return range.get()->second; + FileInstance currentInstance = 0; + for (ArchivedFileLocationMap::const_iterator it = range.first; it != range.second; ++it) + { + ArchiveFile* archive = it->second; + if (filter == ARCHIVE_FILTER_MOD_ONLY && !isModArchive(archive, filename.str())) + { + continue; + } + if (filter == ARCHIVE_FILTER_NON_MOD_ONLY && isModArchive(archive, filename.str())) + { + continue; + } + + if (currentInstance == instance) + { + return archive; + } + ++currentInstance; + } + + return nullptr; } void ArchiveFileSystem::getFileListInDirectory(const AsciiString& currentDirectory, const AsciiString& originalDirectory, const AsciiString& searchName, FilenameList &filenameList, Bool searchSubdirectories) const @@ -345,3 +377,138 @@ void ArchiveFileSystem::getFileListInDirectory(const AsciiString& currentDirecto it++; } } + +// GeneralsX @bugfix felipebraz 02/10/2026 Allow mod archives to take precedence over loose stock files. +// Retail Zero Hour shipped stock loose files (e.g. Data/Scripts/SkirmishScripts.scb) that shadow mod archives +// (such as Shockwave's !Shw_scripts.big) unless the launcher explicitly renamed them on disk. +Bool ArchiveFileSystem::isModArchive(ArchiveFile *archive, const Char *filename) const +{ + if (archive == nullptr) + { + return FALSE; + } + + // 1. Script files: SkirmishScripts.scb, MultiplayerScripts.scb, Scripts.ini. + // Vanilla Zero Hour never packaged these into any .big archive; they only shipped as loose files. + // If any archive in the archive file system contains them, it is guaranteed to be a mod. + if (filename != nullptr) + { + const char* fnRaw = filename; + const char* fnLastSlash = strrchr(fnRaw, '/'); + const char* fnLastBackslash = strrchr(fnRaw, '\\'); + const char* fnSplit = fnLastSlash; + if (fnSplit == nullptr || (fnLastBackslash != nullptr && fnLastBackslash > fnSplit)) + { + fnSplit = fnLastBackslash; + } + const char* fnBase = (fnSplit != nullptr) ? (fnSplit + 1) : fnRaw; + if (stricmp(fnBase, "SkirmishScripts.scb") == 0 || + stricmp(fnBase, "MultiplayerScripts.scb") == 0 || + stricmp(fnBase, "Scripts.ini") == 0) + { + return TRUE; + } + } + + // 2. Mod archives starting with '!' (or '!!', '@', etc.) - the universal SAGE mod convention. + AsciiString archiveName = archive->getName(); + const char* raw = archiveName.str(); + const char* lastSlash = strrchr(raw, '/'); + const char* lastBackslash = strrchr(raw, '\\'); + const char* split = lastSlash; + if (split == nullptr || (lastBackslash != nullptr && lastBackslash > split)) + { + split = lastBackslash; + } + const char* baseName = (split != nullptr) ? (split + 1) : raw; + if (baseName[0] == '!' || baseName[0] == '@') + { + return TRUE; + } + + // 3. Archives loaded explicitly via -mod command line parameter (directory or direct .big). + if (TheGlobalData) + { + if (TheGlobalData->m_modDir.isNotEmpty()) + { + const char* match = strstr(archiveName.str(), TheGlobalData->m_modDir.str()); + if (match != nullptr) + { + char nextChar = match[TheGlobalData->m_modDir.getLength()]; + if (nextChar == '/' || nextChar == '\\' || nextChar == '\0') + { + return TRUE; + } + } + } + if (TheGlobalData->m_modBIG.isNotEmpty()) + { + if (archiveName.compareNoCase(TheGlobalData->m_modBIG) == 0) + { + return TRUE; + } + const char* modBigRaw = TheGlobalData->m_modBIG.str(); + const char* modBigSlash = strrchr(modBigRaw, '/'); + const char* modBigBackslash = strrchr(modBigRaw, '\\'); + const char* modBigSplit = modBigSlash; + if (modBigSplit == nullptr || (modBigBackslash != nullptr && modBigBackslash > modBigSplit)) + { + modBigSplit = modBigBackslash; + } + const char* modBigBase = (modBigSplit != nullptr) ? (modBigSplit + 1) : modBigRaw; + if (stricmp(baseName, modBigBase) == 0) + { + return TRUE; + } + } + } + + return FALSE; +} + +Bool ArchiveFileSystem::hasModArchiveOverride(const Char *filename) const +{ + return getFileCount(filename, ARCHIVE_FILTER_MOD_ONLY) > 0; +} + +FileInstance ArchiveFileSystem::getFileCount(const Char *filename, ArchiveFilter filter) const +{ + if (filename == nullptr || filename[0] == '\0') + { + return 0; + } + + ArchivedDirectoryInfoResult result = const_cast(this)->getArchivedDirectoryInfo(filename); + if (!result.valid()) + { + return 0; + } + + if (filter == ARCHIVE_FILTER_ALL) + { + return static_cast(result.dirInfo->m_files.count(result.lastToken)); + } + + std::pair range = + result.dirInfo->m_files.equal_range(result.lastToken); + + FileInstance count = 0; + for (ArchivedFileLocationMap::const_iterator it = range.first; it != range.second; ++it) + { + ArchiveFile* archive = it->second; + if (filter == ARCHIVE_FILTER_MOD_ONLY && !isModArchive(archive, filename)) + { + continue; + } + if (filter == ARCHIVE_FILTER_NON_MOD_ONLY && isModArchive(archive, filename)) + { + continue; + } + ++count; + } + + return count; +} + + + diff --git a/Core/GameEngine/Source/Common/System/FileSystem.cpp b/Core/GameEngine/Source/Common/System/FileSystem.cpp index 0907722dcf1..73a6296798b 100644 --- a/Core/GameEngine/Source/Common/System/FileSystem.cpp +++ b/Core/GameEngine/Source/Common/System/FileSystem.cpp @@ -177,13 +177,31 @@ File* FileSystem::openFile( const Char *filename, Int access, size_t bufferSize USE_PERF_TIMER(FileSystem) File *file = nullptr; + // GeneralsX @bugfix felipebraz 02/10/2026 Allow mod archives (e.g. !*.big, custom SkirmishScripts) + // to override stock loose files that otherwise shadow mod contents. + const Bool hasModOverride = (TheArchiveFileSystem != nullptr) && + !(access & File::WRITE) && + TheArchiveFileSystem->hasModArchiveOverride(filename); + + FileInstance remainingInstance = instance; + + if (hasModOverride) + { + FileInstance modArchiveCount = TheArchiveFileSystem->getFileCount(filename, ArchiveFileSystem::ARCHIVE_FILTER_MOD_ONLY); + if (remainingInstance < modArchiveCount) + { + return TheArchiveFileSystem->openFile( filename, access, remainingInstance, ArchiveFileSystem::ARCHIVE_FILTER_MOD_ONLY ); + } + remainingInstance -= modArchiveCount; + } + if ( TheLocalFileSystem != nullptr ) { - if (instance != 0) + if (remainingInstance != 0) { if (TheLocalFileSystem->doesFileExist(filename)) { - --instance; + --remainingInstance; } } else @@ -212,8 +230,9 @@ File* FileSystem::openFile( const Char *filename, Int access, size_t bufferSize if ( (TheArchiveFileSystem != nullptr) && (file == nullptr) ) { - // TheSuperHackers @todo Pass 'access' here? - file = TheArchiveFileSystem->openFile( filename, 0, instance ); + // Non-mod (ordinary) archives fallback if mod overrides were active, or all archives if not. + ArchiveFileSystem::ArchiveFilter filter = hasModOverride ? ArchiveFileSystem::ARCHIVE_FILTER_NON_MOD_ONLY : ArchiveFileSystem::ARCHIVE_FILTER_ALL; + file = TheArchiveFileSystem->openFile( filename, 0, remainingInstance, filter ); } return file; @@ -242,29 +261,55 @@ Bool FileSystem::doesFileExist(const Char *filename, FileInstance instance) cons } #endif + const FileInstance requestedInstance = instance; + FileInstance remainingInstance = instance; + + // GeneralsX @bugfix felipebraz 02/10/2026 Prioritize mod archive existence if an override is active. + const Bool hasModOverride = (TheArchiveFileSystem != nullptr) && + TheArchiveFileSystem->hasModArchiveOverride(filename); + + if (hasModOverride) + { + FileInstance modArchiveCount = TheArchiveFileSystem->getFileCount(filename, ArchiveFileSystem::ARCHIVE_FILTER_MOD_ONLY); + if (remainingInstance < modArchiveCount) + { +#if ENABLE_FILESYSTEM_EXISTENCE_CACHE + { + FastCriticalSectionClass::LockClass lock(m_fileExistMutex); + FileExistMap::mapped_type& value = m_fileExist[filename]; + value.instanceExists = max(value.instanceExists, requestedInstance); + } +#endif + return TRUE; + } + remainingInstance -= modArchiveCount; + } + if (TheLocalFileSystem->doesFileExist(filename)) { - if (instance == 0) + if (remainingInstance == 0) { #if ENABLE_FILESYSTEM_EXISTENCE_CACHE { FastCriticalSectionClass::LockClass lock(m_fileExistMutex); - m_fileExist[filename]; + FileExistMap::mapped_type& value = m_fileExist[filename]; + value.instanceExists = max(value.instanceExists, requestedInstance); } #endif return TRUE; } - --instance; + --remainingInstance; } - if (TheArchiveFileSystem->doesFileExist(filename, instance)) + ArchiveFileSystem::ArchiveFilter filter = hasModOverride ? ArchiveFileSystem::ARCHIVE_FILTER_NON_MOD_ONLY : ArchiveFileSystem::ARCHIVE_FILTER_ALL; + if (TheArchiveFileSystem->doesFileExist(filename, remainingInstance, filter)) { #if ENABLE_FILESYSTEM_EXISTENCE_CACHE { FastCriticalSectionClass::LockClass lock(m_fileExistMutex); FileExistMap::mapped_type& value = m_fileExist[filename]; - value.instanceExists = max(value.instanceExists, instance); + value.instanceExists = max(value.instanceExists, requestedInstance); } #endif return TRUE; @@ -274,7 +319,7 @@ Bool FileSystem::doesFileExist(const Char *filename, FileInstance instance) cons { FastCriticalSectionClass::LockClass lock(m_fileExistMutex); FileExistMap::mapped_type& value = m_fileExist[filename]; - value.instanceDoesNotExist = min(value.instanceDoesNotExist, instance); + value.instanceDoesNotExist = min(value.instanceDoesNotExist, requestedInstance); } #endif return FALSE; @@ -304,15 +349,32 @@ Bool FileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo, Fi } memset(fileInfo, 0, sizeof(*fileInfo)); + FileInstance remainingInstance = instance; + + // GeneralsX @bugfix felipebraz 02/10/2026 Prioritize mod archive metadata if an override is active. + const Bool hasModOverride = (TheArchiveFileSystem != nullptr) && + TheArchiveFileSystem->hasModArchiveOverride(filename.str()); + + if (hasModOverride) + { + FileInstance modArchiveCount = TheArchiveFileSystem->getFileCount(filename.str(), ArchiveFileSystem::ARCHIVE_FILTER_MOD_ONLY); + if (remainingInstance < modArchiveCount) + { + return TheArchiveFileSystem->getFileInfo(filename, fileInfo, remainingInstance, ArchiveFileSystem::ARCHIVE_FILTER_MOD_ONLY); + } + remainingInstance -= modArchiveCount; + } + if (TheLocalFileSystem->getFileInfo(filename, fileInfo)) { - if (instance == 0) { + if (remainingInstance == 0) { return TRUE; } - --instance; + --remainingInstance; } - if (TheArchiveFileSystem->getFileInfo(filename, fileInfo, instance)) { + ArchiveFileSystem::ArchiveFilter filter = hasModOverride ? ArchiveFileSystem::ARCHIVE_FILTER_NON_MOD_ONLY : ArchiveFileSystem::ARCHIVE_FILTER_ALL; + if (TheArchiveFileSystem->getFileInfo(filename, fileInfo, remainingInstance, filter)) { return TRUE; } diff --git a/Core/GameEngine/Source/GameLogic/Map/SidesList.cpp b/Core/GameEngine/Source/GameLogic/Map/SidesList.cpp index 6eb2b9ad762..3607bd94769 100644 --- a/Core/GameEngine/Source/GameLogic/Map/SidesList.cpp +++ b/Core/GameEngine/Source/GameLogic/Map/SidesList.cpp @@ -421,7 +421,7 @@ SidesInfo *SidesList::findSkirmishSideInfo(AsciiString name, Int* index /*= null return nullptr; } -static AsciiString static_readPlayerNames[MAX_PLAYER_COUNT]; +static AsciiString static_readPlayerNames[SidesList::MAX_SKIRMISH_SIDES]; /** * ParsePlayersDataChunk - read players names data chunk. @@ -434,6 +434,7 @@ static AsciiString static_readPlayerNames[MAX_PLAYER_COUNT]; static Bool ParsePlayersDataChunk(DataChunkInput &file, DataChunkInfo *info, void *userData) { + SidesList *sides = (SidesList *)userData; Int readDicts = 0; if (info->version >= K_PLAYERS_NAMES_FOR_SCRIPTS_VERSION_2) { readDicts = file.readInt(); @@ -441,10 +442,24 @@ static Bool ParsePlayersDataChunk(DataChunkInput &file, DataChunkInfo *info, voi Int numNames = file.readInt(); Int i; for (i=0; i=MAX_PLAYER_COUNT) break; - static_readPlayerNames[i] = file.readAsciiString(); + AsciiString playerName = file.readAsciiString(); + if (i < SidesList::MAX_SKIRMISH_SIDES) { + static_readPlayerNames[i] = playerName; + } if (readDicts) { Dict sideDict = file.readDict(); + // GeneralsX @bugfix fbraz3/gpherai 05/10/2026 Import missing skirmish player definitions + // from SkirmishScripts chunk to dynamically support mod factions on custom/retail maps (#335). + if (sides && !playerName.isEmpty() && sides->findSkirmishSideInfo(playerName) == nullptr) { + if (sideDict.getAsciiString(TheKey_playerName).isEmpty()) { + sideDict.setAsciiString(TheKey_playerName, playerName); + } + sides->addSkirmishSide(&sideDict); +#ifdef ALLOW_DEBUG_UTILS + fprintf(stderr, "[SKIRMISH_DIAG] Imported missing skirmish side '%s' from SkirmishScripts\n", playerName.str()); + fflush(stderr); +#endif + } } } DEBUG_ASSERTCRASH(file.atEndOfChunk(), ("Unexpected data left over.")); @@ -487,7 +502,7 @@ void SidesList::prepareForMP_or_Skirmish() } m_teamrec.clear(); - for (i = 0; i < MAX_PLAYER_COUNT; i++) { + for (i = 0; i < MAX_SKIRMISH_SIDES; i++) { m_skirmishSides[i].clear(); } m_numSkirmishSides = 0; @@ -573,7 +588,7 @@ void SidesList::prepareForMP_or_Skirmish() getSkirmishSideInfo(curSide)->setScriptList(scripts[i]); scripts[i] = nullptr; } - for (i=0; i loose file -> stock archive) and fixed existence cache corruption caused by instance mutation. + ## 01/10/2026 ### Use shared SELinux labels for PR #344 Docker mounts