Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Core/GameEngine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions Core/GameEngine/Include/Common/ArchiveFileSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions Core/GameEngine/Include/Common/FileSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@

#include "WWLib/mutex.h"

#include <vector>

//----------------------------------------------------------------------------
// Forward References
//----------------------------------------------------------------------------
Expand All @@ -67,6 +69,15 @@ typedef std::set<AsciiString, rts::less_than_nocase<AsciiString>/**/> 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
//----------------------------------------------------------------------------
Expand Down Expand Up @@ -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
{
Expand All @@ -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<AsciiString>& 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.
Expand All @@ -178,7 +200,11 @@ class FileSystem : public SubsystemInterface

mutable FileExistMap m_fileExist;
mutable FastCriticalSectionClass m_fileExistMutex;
void cacheFileExistence(const Char *filename, FileInstance instance, Bool exists) const;
#endif

private:
ModFileSystem *m_modFileSystem;
};

extern FileSystem* TheFileSystem;
Expand Down
64 changes: 64 additions & 0 deletions Core/GameEngine/Include/Common/ModFileSystem.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "Common/FileSystem.h"

class ModFileSystem
{
ModFileSystem(const ModFileSystem&);
ModFileSystem& operator=(const ModFileSystem&);

public:
ModFileSystem();
~ModFileSystem();

Bool load(const std::vector<AsciiString>& 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<AsciiString>& 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<Layer *> m_layers;
};
47 changes: 7 additions & 40 deletions Core/GameEngine/Source/Common/CommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand All @@ -1155,7 +1123,6 @@ static CommandLineParam paramsForEngineInit[] =
{ "-particleEdit", parseParticleEdit },
{ "-scriptDebug", parseScriptDebug },
{ "-playStats", parsePlayStats },
{ "-mod", parseMod },
{ "-noshaders", parseNoShaders },
{ "-quickstart", parseQuickStart },
{ "-useWaveEditor", parseUseWaveEditor },
Expand Down
26 changes: 0 additions & 26 deletions Core/GameEngine/Source/Common/System/ArchiveFileSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArchiveFileSystem*>(this)->getArchivedDirectoryInfo(filename);
Expand Down
Loading
Loading