From d66accaeb175872f8d3c27b7908641b3e4681bee Mon Sep 17 00:00:00 2001 From: Jaredl-Dev <260281143+Jaredl-Dev@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:18:42 -0700 Subject: [PATCH 1/5] refactor(filesystem): Centralize file existence cache updates (#3091) --- Core/GameEngine/Include/Common/FileSystem.h | 1 + .../Source/Common/System/FileSystem.cpp | 42 ++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/Core/GameEngine/Include/Common/FileSystem.h b/Core/GameEngine/Include/Common/FileSystem.h index 2aaa30a61f1..8fbebc231da 100644 --- a/Core/GameEngine/Include/Common/FileSystem.h +++ b/Core/GameEngine/Include/Common/FileSystem.h @@ -178,6 +178,7 @@ class FileSystem : public SubsystemInterface mutable FileExistMap m_fileExist; mutable FastCriticalSectionClass m_fileExistMutex; + void cacheFileExistence(const Char *filename, FileInstance instance, Bool exists) const; #endif }; diff --git a/Core/GameEngine/Source/Common/System/FileSystem.cpp b/Core/GameEngine/Source/Common/System/FileSystem.cpp index b8e4c4695b6..623cebc5b4a 100644 --- a/Core/GameEngine/Source/Common/System/FileSystem.cpp +++ b/Core/GameEngine/Source/Common/System/FileSystem.cpp @@ -226,6 +226,9 @@ File* FileSystem::openFile( const Char *filename, Int access, size_t bufferSize Bool FileSystem::doesFileExist(const Char *filename, FileInstance instance) const { USE_PERF_TIMER(FileSystem) +#if ENABLE_FILESYSTEM_EXISTENCE_CACHE + const FileInstance requestedInstance = instance; +#endif #if ENABLE_FILESYSTEM_EXISTENCE_CACHE { @@ -234,9 +237,9 @@ Bool FileSystem::doesFileExist(const Char *filename, FileInstance instance) cons if (it != m_fileExist.end()) { // Must test instanceDoesNotExist first! - if (instance >= it->second.instanceDoesNotExist) + if (requestedInstance >= it->second.instanceDoesNotExist) return FALSE; - if (instance <= it->second.instanceExists) + if (requestedInstance <= it->second.instanceExists) return TRUE; } } @@ -247,10 +250,7 @@ Bool FileSystem::doesFileExist(const Char *filename, FileInstance instance) cons if (instance == 0) { #if ENABLE_FILESYSTEM_EXISTENCE_CACHE - { - FastCriticalSectionClass::LockClass lock(m_fileExistMutex); - m_fileExist[filename]; - } + cacheFileExistence(filename, requestedInstance, TRUE); #endif return TRUE; } @@ -261,24 +261,36 @@ Bool FileSystem::doesFileExist(const Char *filename, FileInstance instance) cons if (TheArchiveFileSystem->doesFileExist(filename, instance)) { #if ENABLE_FILESYSTEM_EXISTENCE_CACHE - { - FastCriticalSectionClass::LockClass lock(m_fileExistMutex); - FileExistMap::mapped_type& value = m_fileExist[filename]; - value.instanceExists = max(value.instanceExists, instance); - } + cacheFileExistence(filename, requestedInstance, TRUE); #endif return TRUE; } #if ENABLE_FILESYSTEM_EXISTENCE_CACHE + cacheFileExistence(filename, requestedInstance, FALSE); +#endif + return FALSE; +} + +#if ENABLE_FILESYSTEM_EXISTENCE_CACHE +//============================================================================ +// FileSystem::cacheFileExistence +//============================================================================ + +void FileSystem::cacheFileExistence(const Char *filename, FileInstance instance, Bool exists) const +{ + FastCriticalSectionClass::LockClass lock(m_fileExistMutex); + FileExistMap::mapped_type& value = m_fileExist[filename]; + if (exists) + { + value.instanceExists = max(value.instanceExists, instance); + } + else { - FastCriticalSectionClass::LockClass lock(m_fileExistMutex); - FileExistMap::mapped_type& value = m_fileExist[filename]; value.instanceDoesNotExist = min(value.instanceDoesNotExist, instance); } -#endif - return FALSE; } +#endif //============================================================================ // FileSystem::getFileListInDirectory From 970596780dfc36e080d1de90967a9ea6b8566f26 Mon Sep 17 00:00:00 2001 From: Jaredl-Dev <260281143+Jaredl-Dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:03:38 -0700 Subject: [PATCH 2/5] feat(commandline): Capture ordered mod arguments (#3091) --- Core/GameEngine/Source/Common/CommandLine.cpp | 47 +++---------------- .../GameEngine/Include/Common/GlobalData.h | 6 +++ 2 files changed, 13 insertions(+), 40 deletions(-) diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 772830f0f67..f060a9cb99e 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -1036,50 +1036,17 @@ Int parseUpdateImages(char *args[], int num) return 1; } -Int parseMod(char *args[], Int num) +// TheSuperHackers @feature Jaredl-Dev 08/08/2026 Capture each raw -mod operand in command-line order during startup. +Int parseModForStartup(char *args[], Int num) { if (num > 1) { - AsciiString modPath = args[1]; - if (strchr(modPath.str(), ':') || modPath.startsWith("/") || modPath.startsWith("\\")) - { - // full path passed in. Don't append base path. - } - else - { - modPath.format("%s%s", TheGlobalData->getPath_UserData().str(), args[1]); - } - DEBUG_LOG(("Looking for mod '%s'", modPath.str())); - - if (!TheLocalFileSystem->doesFileExist(modPath.str())) - { - DEBUG_LOG(("Mod does not exist.")); - return 2; // no such file/dir. - } - - // now check for dir-ness - struct _stat statBuf; - if (_stat(modPath.str(), &statBuf) != 0) - { - DEBUG_LOG(("Could not _stat() mod.")); - return 2; // could not stat the file/dir. - } - - if (statBuf.st_mode & _S_IFDIR) - { - if (!modPath.endsWith("\\") && !modPath.endsWith("/")) - modPath.concat('\\'); - DEBUG_LOG(("Mod dir is '%s'.", modPath.str())); - TheWritableGlobalData->m_modDir = modPath; - } - else - { - DEBUG_LOG(("Mod file is '%s'.", modPath.str())); - TheWritableGlobalData->m_modBIG = modPath; - } - + TheWritableGlobalData->m_commandLineData.m_modArguments.push_back(args[1]); return 2; } + + // Preserve the malformed occurrence so startup can fail after the file systems exist. + TheWritableGlobalData->m_commandLineData.m_modArguments.push_back(AsciiString::TheEmptyString); return 1; } @@ -1141,6 +1108,7 @@ static CommandLineParam paramsForStartup[] = // (If you have 4 cores, call it with -jobs 4) // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, + { "-mod", parseModForStartup }, }; // These Params are parsed during Engine Init before INI data is loaded @@ -1155,7 +1123,6 @@ static CommandLineParam paramsForEngineInit[] = { "-particleEdit", parseParticleEdit }, { "-scriptDebug", parseScriptDebug }, { "-playStats", parsePlayStats }, - { "-mod", parseMod }, { "-noshaders", parseNoShaders }, { "-quickstart", parseQuickStart }, { "-useWaveEditor", parseUseWaveEditor }, diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 7f484111672..d1bed5a9832 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -56,8 +56,13 @@ constexpr const Int SIMULATE_REPLAYS_SEQUENTIAL = -1; //------------------------------------------------------------------------------------------------- class CommandLineData { +public: + const std::vector &getModArguments() const { return m_modArguments; } + +private: friend class CommandLine; friend class GlobalData; + friend Int parseModForStartup(char *args[], Int num); CommandLineData() : m_hasParsedCommandLineForStartup(false) @@ -66,6 +71,7 @@ class CommandLineData Bool m_hasParsedCommandLineForStartup; Bool m_hasParsedCommandLineForEngineInit; + std::vector m_modArguments; }; //------------------------------------------------------------------------------------------------- From d71a4d5be1dfd13556dc89c9f450b602db33d78a Mon Sep 17 00:00:00 2001 From: Jaredl-Dev <260281143+Jaredl-Dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:04:02 -0700 Subject: [PATCH 3/5] feat(filesystem): Add ordered mod file overlays (#3091) --- Core/GameEngine/CMakeLists.txt | 2 + Core/GameEngine/Include/Common/FileSystem.h | 25 + .../GameEngine/Include/Common/ModFileSystem.h | 64 ++ .../Source/Common/System/FileSystem.cpp | 97 ++- .../Source/Common/System/ModFileSystem.cpp | 580 ++++++++++++++++++ 5 files changed, 766 insertions(+), 2 deletions(-) create mode 100644 Core/GameEngine/Include/Common/ModFileSystem.h create mode 100644 Core/GameEngine/Source/Common/System/ModFileSystem.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 0f36ff63383..91e762845ec 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -77,6 +77,7 @@ set(GAMEENGINE_SRC Include/Common/MiscAudio.h # Include/Common/MissionStats.h # Include/Common/ModelState.h + Include/Common/ModFileSystem.h # Include/Common/Module.h # Include/Common/ModuleFactory.h # Include/Common/Money.h @@ -664,6 +665,7 @@ set(GAMEENGINE_SRC Source/Common/System/LocalFile.cpp Source/Common/System/LocalFileSystem.cpp Source/Common/System/MiniDumper.cpp + Source/Common/System/ModFileSystem.cpp Source/Common/System/ObjectStatusTypes.cpp # Source/Common/System/QuotedPrintable.cpp Source/Common/System/Radar.cpp diff --git a/Core/GameEngine/Include/Common/FileSystem.h b/Core/GameEngine/Include/Common/FileSystem.h index 8fbebc231da..804426da7a8 100644 --- a/Core/GameEngine/Include/Common/FileSystem.h +++ b/Core/GameEngine/Include/Common/FileSystem.h @@ -55,6 +55,8 @@ #include "WWLib/mutex.h" +#include + //---------------------------------------------------------------------------- // Forward References //---------------------------------------------------------------------------- @@ -67,6 +69,15 @@ typedef std::set/**/> FilenameLi typedef FilenameList::iterator FilenameListIter; typedef UnsignedByte FileInstance; +enum ModFileNativePathResult +{ + MOD_FILE_NOT_FOUND, + MOD_FILE_NATIVE_PATH, + MOD_FILE_NATIVE_PATH_UNAVAILABLE +}; + +class ModFileSystem; + //---------------------------------------------------------------------------- // Type Defines //---------------------------------------------------------------------------- @@ -136,6 +147,8 @@ struct FileInfo { // from multiple threads. // // TheSuperHackers @feature Mauller 24/04/2026 Add extension removal functions +// +// TheSuperHackers @feature Jaredl-Dev 09/08/2026 Add ordered, read-only mod file overlays. //=============================== class FileSystem : public SubsystemInterface { @@ -155,6 +168,15 @@ class FileSystem : public SubsystemInterface void getFileListInDirectory(const AsciiString& directory, const AsciiString& searchName, FilenameList &filenameList, Bool searchSubdirectories) const; ///< search the given directory for files matching the searchName (egs. *.ini, *.rep). Possibly search subdirectories. Bool getFileInfo(const AsciiString& filename, FileInfo *fileInfo, FileInstance instance = 0) const; ///< fills in the FileInfo struct for the file given. returns TRUE if successful. + Bool loadModLayers(const std::vector& modPaths, + AsciiString& failureReason); ///< transactionally replaces the ordered mod overlay. + File *openFirstModFile(const AsciiString *filenames, Int filenameCount, + Bool& fileFound); ///< opens the first candidate from the highest-priority mod layer. + ModFileNativePathResult resolveModFileToNativePath(const AsciiString& filename, + AsciiString& filePath) const; ///< resolves the winning mod file when it has a native OS path. + ModFileNativePathResult resolveFirstModFileToNativePath(const AsciiString *filenames, + Int filenameCount, AsciiString& filePath) const; ///< resolves the first candidate from the highest-priority mod layer. + Bool createDirectory(AsciiString directory); ///< create a directory of the given name. static AsciiString normalizePath(const AsciiString& path); ///< normalizes a file path. The path can refer to a directory. File path must be absolute, but does not need to exist. Returns an empty string on failure. @@ -180,6 +202,9 @@ class FileSystem : public SubsystemInterface mutable FastCriticalSectionClass m_fileExistMutex; void cacheFileExistence(const Char *filename, FileInstance instance, Bool exists) const; #endif + +private: + ModFileSystem *m_modFileSystem; }; extern FileSystem* TheFileSystem; diff --git a/Core/GameEngine/Include/Common/ModFileSystem.h b/Core/GameEngine/Include/Common/ModFileSystem.h new file mode 100644 index 00000000000..f85277e20a3 --- /dev/null +++ b/Core/GameEngine/Include/Common/ModFileSystem.h @@ -0,0 +1,64 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include "Common/FileSystem.h" + +class ModFileSystem +{ + ModFileSystem(const ModFileSystem&); + ModFileSystem& operator=(const ModFileSystem&); + +public: + ModFileSystem(); + ~ModFileSystem(); + + Bool load(const std::vector& modPaths, AsciiString& failureReason); + Bool hasLayers() const; + + File *openFile(const AsciiString& filename, Int access, size_t bufferSize, + FileInstance& instance, Bool& fileFound) const; + File *openFirstFile(const AsciiString *filenames, Int filenameCount, Int access, + size_t bufferSize, Bool& fileFound) const; + Bool doesFileExist(const AsciiString& filename, FileInstance& instance) const; + Bool getFileInfo(const AsciiString& filename, FileInfo *fileInfo, FileInstance& instance) const; + void getFileListInDirectory(const AsciiString& directory, const AsciiString& searchName, + FilenameList& filenameList, Bool searchSubdirectories) const; + ModFileNativePathResult resolveFileToNativePath(const AsciiString& filename, AsciiString& filePath) const; + ModFileNativePathResult resolveFirstFileToNativePath(const AsciiString *filenames, + Int filenameCount, AsciiString& filePath) const; + +private: + struct Layer; + struct FileLocation; + + Bool build(const std::vector& modPaths, AsciiString& failureReason); + Bool findFile(const AsciiString& filename, FileInstance& instance, FileLocation& location) const; + Bool findFirstFile(const AsciiString *filenames, Int filenameCount, FileLocation& location) const; + Bool findFileInLayer(const Layer& layer, const AsciiString& normalizedPath, + FileInstance& instance, FileLocation& location) const; + File *openFileAtLocation(const FileLocation& location, Int access, size_t bufferSize) const; + ModFileNativePathResult resolveFileLocationToNativePath(const FileLocation& location, + AsciiString& filePath) const; + Bool appendArchive(Layer& layer, const AsciiString& archivePath, AsciiString& failureReason); + void appendLooseFileList(const Layer& layer, const AsciiString& directory, const AsciiString& searchName, + FilenameList& filenameList, Bool searchSubdirectories) const; + + std::vector m_layers; +}; diff --git a/Core/GameEngine/Source/Common/System/FileSystem.cpp b/Core/GameEngine/Source/Common/System/FileSystem.cpp index 623cebc5b4a..8248b04c124 100644 --- a/Core/GameEngine/Source/Common/System/FileSystem.cpp +++ b/Core/GameEngine/Source/Common/System/FileSystem.cpp @@ -52,6 +52,7 @@ #include "Common/ArchiveFileSystem.h" #include "Common/GameAudio.h" #include "Common/LocalFileSystem.h" +#include "Common/ModFileSystem.h" #include "Common/PerfTimer.h" #include "Lib/PathUtil.h" @@ -122,7 +123,7 @@ FileSystem *TheFileSystem = nullptr; // FileSystem::FileSystem //============================================================================ -FileSystem::FileSystem() +FileSystem::FileSystem() : m_modFileSystem(new ModFileSystem) { } @@ -133,7 +134,7 @@ FileSystem::FileSystem() FileSystem::~FileSystem() { - + delete m_modFileSystem; } //============================================================================ @@ -177,6 +178,16 @@ File* FileSystem::openFile( const Char *filename, Int access, size_t bufferSize USE_PERF_TIMER(FileSystem) File *file = nullptr; + if (m_modFileSystem->hasLayers() && (access & (File::CREATE | File::WRITE)) == 0) + { + Bool overlayFileFound = FALSE; + file = m_modFileSystem->openFile(AsciiString(filename), access, bufferSize, instance, overlayFileFound); + if (overlayFileFound) + { + return file; + } + } + if ( TheLocalFileSystem != nullptr ) { if (instance != 0) @@ -226,6 +237,7 @@ File* FileSystem::openFile( const Char *filename, Int access, size_t bufferSize Bool FileSystem::doesFileExist(const Char *filename, FileInstance instance) const { USE_PERF_TIMER(FileSystem) + const Bool hasModLayers = m_modFileSystem->hasLayers(); #if ENABLE_FILESYSTEM_EXISTENCE_CACHE const FileInstance requestedInstance = instance; #endif @@ -245,6 +257,14 @@ Bool FileSystem::doesFileExist(const Char *filename, FileInstance instance) cons } #endif + if (hasModLayers && m_modFileSystem->doesFileExist(AsciiString(filename), instance)) + { +#if ENABLE_FILESYSTEM_EXISTENCE_CACHE + cacheFileExistence(filename, requestedInstance, TRUE); +#endif + return TRUE; + } + if (TheLocalFileSystem->doesFileExist(filename)) { if (instance == 0) @@ -298,6 +318,10 @@ void FileSystem::cacheFileExistence(const Char *filename, FileInstance instance, void FileSystem::getFileListInDirectory(const AsciiString& directory, const AsciiString& searchName, FilenameList &filenameList, Bool searchSubdirectories) const { USE_PERF_TIMER(FileSystem) + if (m_modFileSystem->hasLayers()) + { + m_modFileSystem->getFileListInDirectory(directory, searchName, filenameList, searchSubdirectories); + } TheLocalFileSystem->getFileListInDirectory(AsciiString::TheEmptyString, directory, searchName, filenameList, searchSubdirectories); TheArchiveFileSystem->getFileListInDirectory(AsciiString::TheEmptyString, directory, searchName, filenameList, searchSubdirectories); } @@ -316,6 +340,11 @@ Bool FileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo, Fi } memset(fileInfo, 0, sizeof(*fileInfo)); + if (m_modFileSystem->hasLayers() && m_modFileSystem->getFileInfo(filename, fileInfo, instance)) + { + return TRUE; + } + if (TheLocalFileSystem->getFileInfo(filename, fileInfo)) { if (instance == 0) { return TRUE; @@ -331,6 +360,70 @@ Bool FileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo, Fi return FALSE; } +//============================================================================ +// FileSystem::loadModLayers +//============================================================================ + +Bool FileSystem::loadModLayers(const std::vector& modPaths, AsciiString& failureReason) +{ + if (!m_modFileSystem->load(modPaths, failureReason)) + { + return FALSE; + } + +#if ENABLE_FILESYSTEM_EXISTENCE_CACHE + { + FastCriticalSectionClass::LockClass lock(m_fileExistMutex); + m_fileExist.clear(); + } +#endif + return TRUE; +} + +//============================================================================ +// FileSystem::openFirstModFile +//============================================================================ + +File *FileSystem::openFirstModFile(const AsciiString *filenames, Int filenameCount, Bool& fileFound) +{ + fileFound = FALSE; + if (!m_modFileSystem->hasLayers()) + { + return nullptr; + } + return m_modFileSystem->openFirstFile(filenames, filenameCount, File::NONE, File::BUFFERSIZE, fileFound); +} + +//============================================================================ +// FileSystem::resolveModFileToNativePath +//============================================================================ + +ModFileNativePathResult FileSystem::resolveModFileToNativePath(const AsciiString& filename, + AsciiString& filePath) const +{ + if (m_modFileSystem->hasLayers()) + { + return m_modFileSystem->resolveFileToNativePath(filename, filePath); + } + filePath.clear(); + return MOD_FILE_NOT_FOUND; +} + +//============================================================================ +// FileSystem::resolveFirstModFileToNativePath +//============================================================================ + +ModFileNativePathResult FileSystem::resolveFirstModFileToNativePath(const AsciiString *filenames, + Int filenameCount, AsciiString& filePath) const +{ + if (m_modFileSystem->hasLayers()) + { + return m_modFileSystem->resolveFirstFileToNativePath(filenames, filenameCount, filePath); + } + filePath.clear(); + return MOD_FILE_NOT_FOUND; +} + //============================================================================ // FileSystem::createDirectory //============================================================================ diff --git a/Core/GameEngine/Source/Common/System/ModFileSystem.cpp b/Core/GameEngine/Source/Common/System/ModFileSystem.cpp new file mode 100644 index 00000000000..d078128bed3 --- /dev/null +++ b/Core/GameEngine/Source/Common/System/ModFileSystem.cpp @@ -0,0 +1,580 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "PreRTS.h" + +#include "Common/ArchiveFile.h" +#include "Common/ArchiveFileSystem.h" +#include "Common/file.h" +#include "Common/LocalFileSystem.h" +#include "Common/ModFileSystem.h" + +#include +#include + +namespace +{ +void normalizeSeparators(std::string *path) +{ + std::replace(path->begin(), path->end(), '/', '\\'); +} + +AsciiString makeDirectoryPath(const AsciiString& path) +{ + std::string result = path.str(); + normalizeSeparators(&result); + if (!result.empty() && result[result.size() - 1] != '\\') + { + result.push_back('\\'); + } + return AsciiString(result.c_str()); +} + +void setFailureReason(AsciiString& failureReason, const char *format, const AsciiString& path) +{ + failureReason.format(format, path.str()); +} + +bool normalizeVirtualPath(const AsciiString& input, AsciiString& normalizedPath, bool allowEmpty) +{ + normalizedPath.clear(); + if (input.isEmpty()) + { + return allowEmpty; + } + + if (input.startsWith("\\") || input.startsWith("/") || input.find(':') != nullptr) + { + return false; + } + + AsciiString remainingPath = input; + AsciiString token; + while (remainingPath.nextToken(&token, "\\/")) + { + if (token == "..") + { + return false; + } + if (token != ".") + { + if (normalizedPath.isNotEmpty()) + { + normalizedPath.concat('\\'); + } + normalizedPath.concat(token); + } + } + + return allowEmpty || normalizedPath.isNotEmpty(); +} + +AsciiString joinPath(const AsciiString& root, const AsciiString& relativePath) +{ + std::string path = root.str(); + if (!relativePath.isEmpty()) + { + if (!path.empty() && path[path.size() - 1] != '\\' && path[path.size() - 1] != '/') + { + path.push_back('\\'); + } + path.append(relativePath.str()); + } + normalizeSeparators(&path); + return AsciiString(path.c_str()); +} + +enum PhysicalPathType +{ + PHYSICAL_PATH_MISSING, + PHYSICAL_PATH_DIRECTORY, + PHYSICAL_PATH_FILE, + PHYSICAL_PATH_OTHER +}; + +PhysicalPathType getPathType(const AsciiString& path) +{ + struct _stat status; + if (_stat(path.str(), &status) != 0) + { + return PHYSICAL_PATH_MISSING; + } + if ((status.st_mode & _S_IFDIR) != 0) + { + return PHYSICAL_PATH_DIRECTORY; + } + if ((status.st_mode & _S_IFREG) != 0) + { + return PHYSICAL_PATH_FILE; + } + return PHYSICAL_PATH_OTHER; +} + +} // namespace + +struct ModFileSystem::Layer +{ + Layer() : isDirectory(FALSE) {} + ~Layer() + { + for (std::vector::iterator it = archives.begin(); it != archives.end(); ++it) + { + delete *it; + } + } + + AsciiString path; + Bool isDirectory; + std::vector archives; + +private: + Layer(const Layer&); + Layer& operator=(const Layer&); +}; + +struct ModFileSystem::FileLocation +{ + FileLocation() : archive(nullptr) {} + + AsciiString loosePath; + ArchiveFile *archive; + AsciiString virtualPath; + FileInfo fileInfo; +}; + +//============================================================================ +// ModFileSystem::ModFileSystem +//============================================================================ + +ModFileSystem::ModFileSystem() +{ +} + +//============================================================================ +// ModFileSystem::~ModFileSystem +//============================================================================ + +ModFileSystem::~ModFileSystem() +{ + for (std::vector::iterator it = m_layers.begin(); it != m_layers.end(); ++it) + { + delete *it; + } +} + +//============================================================================ +// ModFileSystem::load +//============================================================================ + +Bool ModFileSystem::load(const std::vector& modPaths, AsciiString& failureReason) +{ + failureReason.clear(); + + ModFileSystem candidate; + if (!candidate.build(modPaths, failureReason)) + { + return FALSE; + } + + m_layers.swap(candidate.m_layers); + return TRUE; +} + +//============================================================================ +// ModFileSystem::build +//============================================================================ + +Bool ModFileSystem::build(const std::vector& modPaths, AsciiString& failureReason) +{ + m_layers.reserve(modPaths.size()); + for (std::vector::const_iterator pathIt = modPaths.begin(); pathIt != modPaths.end(); ++pathIt) + { + const AsciiString& modPath = *pathIt; + if (modPath.isEmpty()) + { + failureReason = "Mod layer path is empty."; + return FALSE; + } + + AsciiString layerPath = TheLocalFileSystem != nullptr ? + TheLocalFileSystem->normalizePath(modPath) : AsciiString::TheEmptyString; + if (layerPath.isEmpty()) + { + setFailureReason(failureReason, "Unable to normalize mod layer '%s'.", modPath); + return FALSE; + } + + const PhysicalPathType pathType = getPathType(layerPath); + if (pathType == PHYSICAL_PATH_MISSING) + { + setFailureReason(failureReason, "Mod layer '%s' does not exist or is unreadable.", layerPath); + return FALSE; + } + + Layer *layer = new Layer; + layer->path = layerPath; + m_layers.push_back(layer); + if (pathType == PHYSICAL_PATH_DIRECTORY) + { + layer->isDirectory = TRUE; + FilenameList archivePaths; + TheLocalFileSystem->getFileListInDirectory(makeDirectoryPath(layerPath), + AsciiString::TheEmptyString, "*.big", archivePaths, TRUE); + + // The legacy overwrite loader made later archive names win, so keep them in descending priority order. + for (FilenameList::const_reverse_iterator archiveIt = archivePaths.rbegin(); + archiveIt != archivePaths.rend(); ++archiveIt) + { + if (!appendArchive(*layer, *archiveIt, failureReason)) + { + return FALSE; + } + } + } + else if (pathType == PHYSICAL_PATH_FILE) + { + if (!appendArchive(*layer, layerPath, failureReason)) + { + return FALSE; + } + } + else + { + setFailureReason(failureReason, "Mod layer '%s' is not a regular file or directory.", layerPath); + return FALSE; + } + } + return TRUE; +} + +//============================================================================ +// ModFileSystem::hasLayers +//============================================================================ + +Bool ModFileSystem::hasLayers() const +{ + return !m_layers.empty(); +} + +//============================================================================ +// ModFileSystem::findFile +//============================================================================ + +Bool ModFileSystem::findFile(const AsciiString& filename, FileInstance& instance, FileLocation& location) const +{ + AsciiString normalizedPath; + if (!normalizeVirtualPath(filename, normalizedPath, false)) + { + return FALSE; + } + + for (std::vector::const_reverse_iterator layerIt = m_layers.rbegin(); layerIt != m_layers.rend(); ++layerIt) + { + if (findFileInLayer(**layerIt, normalizedPath, instance, location)) + { + return TRUE; + } + } + return FALSE; +} + +//============================================================================ +// ModFileSystem::findFirstFile +//============================================================================ + +Bool ModFileSystem::findFirstFile(const AsciiString *filenames, Int filenameCount, FileLocation& location) const +{ + std::vector normalizedPaths; + for (Int i = 0; i < filenameCount; ++i) + { + AsciiString normalizedPath; + if (normalizeVirtualPath(filenames[i], normalizedPath, false)) + { + normalizedPaths.push_back(normalizedPath); + } + } + + for (std::vector::const_reverse_iterator layerIt = m_layers.rbegin(); layerIt != m_layers.rend(); ++layerIt) + { + for (std::vector::const_iterator pathIt = normalizedPaths.begin(); pathIt != normalizedPaths.end(); ++pathIt) + { + FileInstance instance = 0; + if (findFileInLayer(**layerIt, *pathIt, instance, location)) + { + return TRUE; + } + } + } + return FALSE; +} + +//============================================================================ +// ModFileSystem::findFileInLayer +//============================================================================ + +Bool ModFileSystem::findFileInLayer(const Layer& layer, const AsciiString& normalizedPath, + FileInstance& instance, FileLocation& location) const +{ + if (layer.isDirectory) + { + const AsciiString loosePath = joinPath(layer.path, normalizedPath); + if (TheLocalFileSystem != nullptr && TheLocalFileSystem->getFileInfo(loosePath, &location.fileInfo)) + { + if (instance == 0) + { + location.loosePath = loosePath; + location.archive = nullptr; + location.virtualPath.clear(); + return TRUE; + } + --instance; + } + } + + // Within a directory layer, loose files win, followed by archives in physical-directory priority order. + for (std::vector::const_iterator archiveIt = layer.archives.begin(); + archiveIt != layer.archives.end(); ++archiveIt) + { + if ((*archiveIt)->getFileInfo(normalizedPath, &location.fileInfo)) + { + if (instance == 0) + { + location.loosePath.clear(); + location.archive = *archiveIt; + location.virtualPath = normalizedPath; + return TRUE; + } + --instance; + } + } + return FALSE; +} + +//============================================================================ +// ModFileSystem::openFile +//============================================================================ + +File *ModFileSystem::openFile(const AsciiString& filename, Int access, size_t bufferSize, + FileInstance& instance, Bool& fileFound) const +{ + FileLocation location; + fileFound = findFile(filename, instance, location); + if (!fileFound) + { + return nullptr; + } + + return openFileAtLocation(location, access, bufferSize); +} + +//============================================================================ +// ModFileSystem::openFirstFile +//============================================================================ + +File *ModFileSystem::openFirstFile(const AsciiString *filenames, Int filenameCount, Int access, + size_t bufferSize, Bool& fileFound) const +{ + FileLocation location; + fileFound = findFirstFile(filenames, filenameCount, location); + return fileFound ? openFileAtLocation(location, access, bufferSize) : nullptr; +} + +//============================================================================ +// ModFileSystem::openFileAtLocation +//============================================================================ + +File *ModFileSystem::openFileAtLocation(const FileLocation& location, Int access, size_t bufferSize) const +{ + if (location.archive != nullptr) + { + return location.archive->openFile(location.virtualPath.str(), access); + } + return TheLocalFileSystem->openFile(location.loosePath.str(), access, bufferSize); +} + +//============================================================================ +// ModFileSystem::doesFileExist +//============================================================================ + +Bool ModFileSystem::doesFileExist(const AsciiString& filename, FileInstance& instance) const +{ + FileLocation location; + return findFile(filename, instance, location); +} + +//============================================================================ +// ModFileSystem::getFileInfo +//============================================================================ + +Bool ModFileSystem::getFileInfo(const AsciiString& filename, FileInfo *fileInfo, FileInstance& instance) const +{ + FileLocation location; + if (!findFile(filename, instance, location)) + { + return FALSE; + } + + *fileInfo = location.fileInfo; + return TRUE; +} + +//============================================================================ +// ModFileSystem::getFileListInDirectory +//============================================================================ + +void ModFileSystem::getFileListInDirectory(const AsciiString& directory, const AsciiString& searchName, + FilenameList& filenameList, Bool searchSubdirectories) const +{ + AsciiString normalizedDirectory; + if (!normalizeVirtualPath(directory, normalizedDirectory, true)) + { + return; + } + + AsciiString normalizedSearchName = searchName; + normalizedSearchName.toLower(); + AsciiString archiveDirectory = normalizedDirectory; + if (archiveDirectory.isNotEmpty() && !archiveDirectory.endsWith("\\")) + { + archiveDirectory.concat('\\'); + } + + for (std::vector::const_reverse_iterator layerIt = m_layers.rbegin(); layerIt != m_layers.rend(); ++layerIt) + { + const Layer *layer = *layerIt; + if (layer->isDirectory) + { + appendLooseFileList(*layer, normalizedDirectory, normalizedSearchName, filenameList, searchSubdirectories); + } + for (std::vector::const_iterator archiveIt = layer->archives.begin(); + archiveIt != layer->archives.end(); ++archiveIt) + { + (*archiveIt)->getFileListInDirectory(AsciiString::TheEmptyString, archiveDirectory, + normalizedSearchName, filenameList, searchSubdirectories); + } + } +} + +//============================================================================ +// ModFileSystem::appendLooseFileList +//============================================================================ + +void ModFileSystem::appendLooseFileList(const Layer& layer, const AsciiString& directory, const AsciiString& searchName, + FilenameList& filenameList, Bool searchSubdirectories) const +{ + const AsciiString layerDirectory = makeDirectoryPath(layer.path); + FilenameList physicalFiles; + TheLocalFileSystem->getFileListInDirectory(makeDirectoryPath(joinPath(layer.path, directory)), + AsciiString::TheEmptyString, searchName, physicalFiles, searchSubdirectories); + + for (FilenameList::const_iterator it = physicalFiles.begin(); it != physicalFiles.end(); ++it) + { + std::string physicalPath = it->str(); + normalizeSeparators(&physicalPath); + const AsciiString normalizedPhysicalPath(physicalPath.c_str()); + if (!normalizedPhysicalPath.startsWithNoCase(layerDirectory)) + { + DEBUG_ASSERTCRASH(FALSE, ("Enumerated path '%s' is outside mod layer '%s'.", + normalizedPhysicalPath.str(), layer.path.str())); + continue; + } + + filenameList.insert(AsciiString(normalizedPhysicalPath.str() + layerDirectory.getLength())); + } +} + +//============================================================================ +// ModFileSystem::resolveFileToNativePath +//============================================================================ + +ModFileNativePathResult ModFileSystem::resolveFileToNativePath(const AsciiString& filename, AsciiString& filePath) const +{ + filePath.clear(); + + FileInstance instance = 0; + FileLocation location; + if (!findFile(filename, instance, location)) + { + return MOD_FILE_NOT_FOUND; + } + return resolveFileLocationToNativePath(location, filePath); +} + +//============================================================================ +// ModFileSystem::resolveFirstFileToNativePath +//============================================================================ + +ModFileNativePathResult ModFileSystem::resolveFirstFileToNativePath(const AsciiString *filenames, + Int filenameCount, AsciiString& filePath) const +{ + filePath.clear(); + + FileLocation location; + if (!findFirstFile(filenames, filenameCount, location)) + { + return MOD_FILE_NOT_FOUND; + } + return resolveFileLocationToNativePath(location, filePath); +} + +//============================================================================ +// ModFileSystem::resolveFileLocationToNativePath +//============================================================================ + +ModFileNativePathResult ModFileSystem::resolveFileLocationToNativePath(const FileLocation& location, + AsciiString& filePath) const +{ + if (location.archive != nullptr || TheLocalFileSystem == nullptr) + { + return MOD_FILE_NATIVE_PATH_UNAVAILABLE; + } + + filePath = TheLocalFileSystem->normalizePath(location.loosePath); + return filePath.isNotEmpty() ? MOD_FILE_NATIVE_PATH : MOD_FILE_NATIVE_PATH_UNAVAILABLE; +} + +//============================================================================ +// ModFileSystem::appendArchive +//============================================================================ + +Bool ModFileSystem::appendArchive(Layer& layer, const AsciiString& archivePath, AsciiString& failureReason) +{ + ArchiveFile *archive = TheArchiveFileSystem != nullptr ? + TheArchiveFileSystem->openArchiveFile(archivePath.str()) : nullptr; + if (archive == nullptr) + { + const Int LEGACY_WINDOWS_PATH_LIMIT = 260; + if (archivePath.getLength() >= LEGACY_WINDOWS_PATH_LIMIT) + { + setFailureReason(failureReason, + "A mod archive could not be opened because its resolved path is too long for the legacy " + "Windows 260-character limit.\n\nMove the mod to a shorter folder and try again.\n\nArchive:\n%s", + archivePath); + } + else + { + setFailureReason(failureReason, + "A mod archive could not be opened. The file may be missing, unreadable, or invalid.\n\nArchive:\n%s", + archivePath); + } + return FALSE; + } + + layer.archives.push_back(archive); + return TRUE; +} From 3e158f90bee61925848975a003dd5730a9815cce Mon Sep 17 00:00:00 2001 From: Jaredl-Dev <260281143+Jaredl-Dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:04:23 -0700 Subject: [PATCH 4/5] feat(filesystem): Resolve native mod resources (#3091) --- .../VideoDevice/Bink/BinkVideoPlayer.cpp | 31 +++++++++++------- .../VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp | 26 +++++++-------- .../Win32Device/GameClient/Win32Mouse.cpp | 32 ++++++++----------- 3 files changed, 45 insertions(+), 44 deletions(-) diff --git a/Core/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp b/Core/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp index ebbc91179c1..f45419ab8b4 100644 --- a/Core/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp +++ b/Core/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp @@ -48,9 +48,9 @@ #include "Lib/BaseType.h" #include "VideoDevice/Bink/BinkVideoPlayer.h" #include "Common/AudioAffect.h" +#include "Common/FileSystem.h" #include "Common/GameAudio.h" #include "Common/GameMemory.h" -#include "Common/GlobalData.h" #include "Common/Registry.h" //---------------------------------------------------------------------------- @@ -226,26 +226,33 @@ VideoStreamInterface* BinkVideoPlayer::open( AsciiString movieTitle ) if (pVideo) { DEBUG_LOG(("BinkVideoPlayer::createStream() - About to open bink file")); - if (TheGlobalData->m_modDir.isNotEmpty()) + char localizedFilePath[ _MAX_PATH ]; + snprintf( localizedFilePath, ARRAY_SIZE(localizedFilePath), VIDEO_LANG_PATH_FORMAT, GetRegistryLanguage().str(), pVideo->m_filename.str(), VIDEO_EXT ); + char filePath[ _MAX_PATH ]; + snprintf( filePath, ARRAY_SIZE(filePath), "%s\\%s.%s", VIDEO_PATH, pVideo->m_filename.str(), VIDEO_EXT ); + + // TheSuperHackers @feature Jaredl-Dev 09/08/2026 Resolve loose movies from explicit mod layers. + const AsciiString modMoviePaths[] = { localizedFilePath, filePath }; + AsciiString nativeMoviePath; + const ModFileNativePathResult modMovieResult = + TheFileSystem->resolveFirstModFileToNativePath(modMoviePaths, ARRAY_SIZE(modMoviePaths), nativeMoviePath); + if (modMovieResult != MOD_FILE_NOT_FOUND) { - char filePath[ _MAX_PATH ]; - snprintf( filePath, ARRAY_SIZE(filePath), "%s%s\\%s.%s", TheGlobalData->m_modDir.str(), VIDEO_PATH, pVideo->m_filename.str(), VIDEO_EXT ); - HBINK handle = BinkOpen(filePath , BINKPRELOADALL ); - DEBUG_ASSERTLOG(!handle, ("opened bink file %s", filePath)); - if (handle) + if (modMovieResult == MOD_FILE_NATIVE_PATH_UNAVAILABLE) { - return createStream( handle ); + DEBUG_LOG(("BinkVideoPlayer::open - mod movie '%s' has no native file path.", pVideo->m_filename.str())); + return nullptr; } + + HBINK modHandle = BinkOpen(nativeMoviePath.str(), BINKPRELOADALL); + DEBUG_ASSERTLOG(!modHandle, ("opened bink file %s", nativeMoviePath.str())); + return modHandle != nullptr ? createStream( modHandle ) : nullptr; } - char localizedFilePath[ _MAX_PATH ]; - snprintf( localizedFilePath, ARRAY_SIZE(localizedFilePath), VIDEO_LANG_PATH_FORMAT, GetRegistryLanguage().str(), pVideo->m_filename.str(), VIDEO_EXT ); HBINK handle = BinkOpen(localizedFilePath , BINKPRELOADALL ); DEBUG_ASSERTLOG(!handle, ("opened localized bink file %s", localizedFilePath)); if (!handle) { - char filePath[ _MAX_PATH ]; - snprintf( filePath, ARRAY_SIZE(filePath), "%s\\%s.%s", VIDEO_PATH, pVideo->m_filename.str(), VIDEO_EXT ); handle = BinkOpen(filePath , BINKPRELOADALL ); DEBUG_ASSERTLOG(!handle, ("opened bink file %s", localizedFilePath)); } diff --git a/Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp b/Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp index a8a1e6ec83e..b6a40b0269e 100644 --- a/Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp +++ b/Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp @@ -35,7 +35,6 @@ #include "Common/AudioAffect.h" #include "Common/GameAudio.h" #include "Common/GameMemory.h" -#include "Common/GlobalData.h" #include "Common/Registry.h" #include "Common/FileSystem.h" @@ -232,26 +231,25 @@ VideoStreamInterface* FFmpegVideoPlayer::open( AsciiString movieTitle ) if (pVideo) { DEBUG_LOG(("FFmpegVideoPlayer::createStream() - About to open bink file")); - if (TheGlobalData->m_modDir.isNotEmpty()) + char localizedFilePath[ _MAX_PATH ]; + snprintf( localizedFilePath, ARRAY_SIZE(localizedFilePath), VIDEO_LANG_PATH_FORMAT, GetRegistryLanguage().str(), pVideo->m_filename.str(), VIDEO_EXT ); + char filePath[ _MAX_PATH ]; + snprintf( filePath, ARRAY_SIZE(filePath), "%s\\%s.%s", VIDEO_PATH, pVideo->m_filename.str(), VIDEO_EXT ); + + // TheSuperHackers @feature Jaredl-Dev 09/08/2026 Resolve movies from explicit mod layers. + const AsciiString modMoviePaths[] = { localizedFilePath, filePath }; + Bool modMovieFound = FALSE; + File *modFile = TheFileSystem->openFirstModFile(modMoviePaths, ARRAY_SIZE(modMoviePaths), modMovieFound); + if (modMovieFound) { - char filePath[ _MAX_PATH ]; - snprintf( filePath, ARRAY_SIZE(filePath), "%s%s\\%s.%s", TheGlobalData->m_modDir.str(), VIDEO_PATH, pVideo->m_filename.str(), VIDEO_EXT ); - File* file = TheFileSystem->openFile(filePath); - DEBUG_ASSERTLOG(!file, ("opened bink file %s", filePath)); - if (file) - { - return createStream( file ); - } + DEBUG_ASSERTLOG(!modFile, ("opened mod bink file %s", pVideo->m_filename.str())); + return modFile != nullptr ? createStream( modFile ) : nullptr; } - char localizedFilePath[ _MAX_PATH ]; - snprintf( localizedFilePath, ARRAY_SIZE(localizedFilePath), VIDEO_LANG_PATH_FORMAT, GetRegistryLanguage().str(), pVideo->m_filename.str(), VIDEO_EXT ); File* file = TheFileSystem->openFile(localizedFilePath); DEBUG_ASSERTLOG(!file, ("opened localized bink file %s", localizedFilePath)); if (!file) { - char filePath[ _MAX_PATH ]; - snprintf( filePath, ARRAY_SIZE(filePath), "%s\\%s.%s", VIDEO_PATH, pVideo->m_filename.str(), VIDEO_EXT ); file = TheFileSystem->openFile(filePath); DEBUG_ASSERTLOG(!file, ("opened bink file %s", filePath)); } diff --git a/Core/GameEngineDevice/Source/Win32Device/GameClient/Win32Mouse.cpp b/Core/GameEngineDevice/Source/Win32Device/GameClient/Win32Mouse.cpp index e63e9234595..069b9288d9d 100644 --- a/Core/GameEngineDevice/Source/Win32Device/GameClient/Win32Mouse.cpp +++ b/Core/GameEngineDevice/Source/Win32Device/GameClient/Win32Mouse.cpp @@ -31,8 +31,7 @@ #include #include "Common/Debug.h" -#include "Common/GlobalData.h" -#include "Common/LocalFileSystem.h" +#include "Common/FileSystem.h" #include "GameClient/GameClient.h" #include "Win32Device/GameClient/Win32Mouse.h" #include "WinMain.h" @@ -379,25 +378,22 @@ void Win32Mouse::initCursorResources() snprintf(resourcePath, ARRAY_SIZE(resourcePath), "data\\cursors\\%s.ANI", m_cursorInfo[cursor].textureName.str()); - // check for a MOD cursor. - Bool loaded = FALSE; - if (TheGlobalData->m_modDir.isNotEmpty()) + // TheSuperHackers @feature Jaredl-Dev 09/08/2026 Resolve loose cursors from explicit mod layers. + AsciiString nativeCursorPath; + const ModFileNativePathResult result = + TheFileSystem->resolveModFileToNativePath(resourcePath, nativeCursorPath); + if (result == MOD_FILE_NATIVE_PATH) { - AsciiString fname; - if (m_cursorInfo[cursor].numDirections > 1) - fname.format("%sdata\\cursors\\%s%d.ANI", TheGlobalData->m_modDir.str(), m_cursorInfo[cursor].textureName.str(), direction); - else - fname.format("%sdata\\cursors\\%s.ANI", TheGlobalData->m_modDir.str(), m_cursorInfo[cursor].textureName.str()); - - if (TheLocalFileSystem->doesFileExist(fname.str())) - { - cursorResources[cursor][direction]=LoadCursorFromFile(fname.str()); - loaded = TRUE; - } + cursorResources[cursor][direction] = LoadCursorFromFile(nativeCursorPath.str()); } - - if (!loaded) + else if (result == MOD_FILE_NOT_FOUND) + { cursorResources[cursor][direction]=LoadCursorFromFile(resourcePath); + } + else + { + DEBUG_LOG(("Win32Mouse::initCursorResources - mod cursor '%s' has no native file path.", resourcePath)); + } DEBUG_ASSERTCRASH(cursorResources[cursor][direction], ("MissingCursor %s",resourcePath)); } } From 78a5f58fcbaa290ac00ac122a501df4123f9a5a0 Mon Sep 17 00:00:00 2001 From: Jaredl-Dev <260281143+Jaredl-Dev@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:04:51 -0700 Subject: [PATCH 5/5] feat(filesystem): Load ordered mods before global data (#3091) --- .../Include/Common/ArchiveFileSystem.h | 2 - .../Common/System/ArchiveFileSystem.cpp | 26 -------- .../GameEngine/Include/Common/GlobalData.h | 2 - .../GameEngine/Source/Common/GameEngine.cpp | 60 ++++++++++++++++++- 4 files changed, 58 insertions(+), 32 deletions(-) diff --git a/Core/GameEngine/Include/Common/ArchiveFileSystem.h b/Core/GameEngine/Include/Common/ArchiveFileSystem.h index af2321d4e25..3d740abf1f6 100644 --- a/Core/GameEngine/Include/Common/ArchiveFileSystem.h +++ b/Core/GameEngine/Include/Common/ArchiveFileSystem.h @@ -146,8 +146,6 @@ class ArchiveFileSystem : public SubsystemInterface // Unprotected this for copy-protection routines ArchiveFile* getArchiveFile(const AsciiString& filename, FileInstance instance = 0) const; - void loadMods(); - ArchivedDirectoryInfo* friend_getArchivedDirectoryInfo(const Char* directory); protected: diff --git a/Core/GameEngine/Source/Common/System/ArchiveFileSystem.cpp b/Core/GameEngine/Source/Common/System/ArchiveFileSystem.cpp index f118b34bad0..c6aa48f2767 100644 --- a/Core/GameEngine/Source/Common/System/ArchiveFileSystem.cpp +++ b/Core/GameEngine/Source/Common/System/ArchiveFileSystem.cpp @@ -210,32 +210,6 @@ void ArchiveFileSystem::loadIntoDirectoryTree(ArchiveFile *archiveFile, Bool ove } } -void ArchiveFileSystem::loadMods() -{ - if (TheGlobalData->m_modBIG.isNotEmpty()) - { - ArchiveFile *archiveFile = openArchiveFile(TheGlobalData->m_modBIG.str()); - - if (archiveFile != nullptr) { - DEBUG_LOG(("ArchiveFileSystem::loadMods - loading %s into the directory tree.", TheGlobalData->m_modBIG.str())); - loadIntoDirectoryTree(archiveFile, TRUE); - m_archiveFileMap[TheGlobalData->m_modBIG] = archiveFile; - DEBUG_LOG(("ArchiveFileSystem::loadMods - %s inserted into the archive file map.", TheGlobalData->m_modBIG.str())); - } - else - { - DEBUG_LOG(("ArchiveFileSystem::loadMods - could not openArchiveFile(%s)", TheGlobalData->m_modBIG.str())); - } - } - - if (TheGlobalData->m_modDir.isNotEmpty()) - { - MAYBE_UNUSED Bool ret = loadBigFilesFromDirectory(TheGlobalData->m_modDir, "*.big", TRUE); - (void)ret; - DEBUG_ASSERTLOG(ret, ("loadBigFilesFromDirectory(%s) returned FALSE!", TheGlobalData->m_modDir.str())); - } -} - Bool ArchiveFileSystem::doesFileExist(const Char *filename, FileInstance instance) const { ArchivedDirectoryInfoResult result = const_cast(this)->getArchivedDirectoryInfo(filename); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index d1bed5a9832..9672e658bfa 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -570,8 +570,6 @@ class GlobalData : public SubsystemInterface Bool m_isBreakableMovie; ///< if we enter a breakable movie, set this flag Bool m_breakTheMovie; ///< The user has hit escape! - AsciiString m_modDir; - AsciiString m_modBIG; //-allAdvice feature //Bool m_allAdvice; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 32b93d3dba7..4bdea761a38 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -174,6 +174,57 @@ extern CComModule _Module; //------------------------------------------------------------------------------------------------- static void updateTGAtoDDS(); +//------------------------------------------------------------------------------------------------- +static Bool isAbsoluteModPath(const AsciiString& path) +{ + const Char *value = path.str(); + return value[0] == '\\' || value[0] == '/' || + (value[0] != 0 && value[1] == ':' && (value[2] == '\\' || value[2] == '/')); +} + +//------------------------------------------------------------------------------------------------- +static Bool loadCommandLineModLayers(AsciiString& failureReason) +{ + const std::vector& modArguments = TheGlobalData->m_commandLineData.getModArguments(); + if (modArguments.empty()) + { + return TRUE; + } + + std::vector resolvedPaths; + resolvedPaths.reserve(modArguments.size()); + for (std::vector::const_iterator it = modArguments.begin(); it != modArguments.end(); ++it) + { + if (it->isEmpty()) + { + failureReason = "Missing path after -mod."; + return FALSE; + } + + AsciiString resolvedPath = *it; + if (!isAbsoluteModPath(resolvedPath)) + { + resolvedPath.format("%s%s", TheGlobalData->getPath_UserData().str(), it->str()); + } + resolvedPaths.push_back(resolvedPath); + } + + return TheFileSystem->loadModLayers(resolvedPaths, failureReason); +} + +//------------------------------------------------------------------------------------------------- +static void failCommandLineModStartup(const AsciiString& failureReason) +{ + DEBUG_LOG(("Unable to load requested mod: %s", failureReason.str())); + if (TheGlobalData->m_headless) + { + RELEASE_CRASH((failureReason.str())); + } + + ::MessageBox(nullptr, failureReason.str(), "Unable to load mod", MB_OK | MB_SYSTEMMODAL | MB_ICONERROR); + _exit(1); +} + //------------------------------------------------------------------------------------------------- static void updateWindowTitle() { @@ -442,6 +493,13 @@ void GameEngine::init() initSubsystem(TheArchiveFileSystem, "TheArchiveFileSystem", createArchiveFileSystem(), nullptr); // this MUST come after TheLocalFileSystem creation + // TheSuperHackers @feature Jaredl-Dev 09/08/2026 Load ordered command-line mod layers before global game data. + AsciiString modFailureReason; + if (!loadCommandLineModLayers(modFailureReason)) + { + failCommandLineModStartup(modFailureReason); + } + #ifdef DUMP_PERF_STATS/////////////////////////////////////////////////////////////////////////// GetPrecisionTimer(&endTime64);////////////////////////////////////////////////////////////////// sprintf(Buf,"----------------------------------------------------------------------------After TheArchiveFileSystem = %f seconds",((double)(endTime64-startTime64)/(double)(freq64))); @@ -472,8 +530,6 @@ void GameEngine::init() // special-case: parse command-line parameters after loading global data CommandLine::parseCommandLineForEngineInit(); - TheArchiveFileSystem->loadMods(); - // doesn't require resets so just create a single instance here. TheGameLODManager = MSGNEW("GameEngineSubsystem") GameLODManager; TheGameLODManager->init();