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 @@ -136,6 +136,7 @@ set(GAMEENGINE_SRC
Include/Common/version.h
# Include/Common/WellKnownKeys.h
Include/Common/WorkerProcess.h
Include/Common/WorkingDirectory.h
Include/Common/Xfer.h
Include/Common/XferCRC.h
Include/Common/XferDeepCRC.h
Expand Down Expand Up @@ -692,6 +693,7 @@ set(GAMEENGINE_SRC
Source/Common/UserPreferences.cpp
Source/Common/version.cpp
Source/Common/WorkerProcess.cpp
Source/Common/WorkingDirectory.cpp
Source/GameClient/ClientInstance.cpp
Source/GameClient/Color.cpp
Source/GameClient/Credits.cpp
Expand Down
5 changes: 5 additions & 0 deletions Core/GameEngine/Include/Common/CommandLine.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class CommandLine
{
public:

// Parses startup flags and applies the process working directory.
static void parseCommandLineForStartup();
static void parseCommandLineForEngineInit();

// Returns true if command-line parsing consumed the zero-based argument index.
// The index excludes the executable name.
static bool wasCommandLineArgumentParsed(int argIndex);
};
36 changes: 36 additions & 0 deletions Core/GameEngine/Include/Common/WorkingDirectory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2025 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 "Lib/BaseType.h"

namespace rts
{

// TheSuperHackers @feature 14/08/2026
// Process working directory helpers. CommandLine::parseCommandLineForStartup()
// applies these: default is the executable directory, -useCwd keeps the OS
// directory, and -setCwd <path> uses that path.

Bool setCurrentDirectoryToExecutablePath();
Bool setCurrentDirectoryToPath(const char *path);
void keepCurrentDirectory();
void setCurrentDirectoryToExecutablePathIfNotSet();

} // namespace rts
125 changes: 57 additions & 68 deletions Core/GameEngine/Source/Common/CommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@
#include "Common/ArchiveFileSystem.h"
#include "Common/CommandLine.h"
#include "Common/CRCDebug.h"
#include "Common/WorkingDirectory.h"
#include "Common/LocalFileSystem.h"
#include "Common/Recorder.h"
#include "Common/version.h"
#include "GameClient/ClientInstance.h"
#include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition
#include "GameClient/GameText.h"
#include "GameNetwork/NetworkDefs.h"
#include "WWLib/trim.h"



Expand Down Expand Up @@ -463,6 +463,28 @@ Int parseJobs(char *args[], int num)
return 1;
}

Int parseUseCwd(char *[], int)
{
// TheSuperHackers @feature 14/08/2026
// -useCwd keeps the OS working directory.
rts::keepCurrentDirectory();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct.

Picture setting command line -setCwd Path -useCwd, this then uses Path instead of Current Directory.

We would expect the last command line argument to win the race.

return 1;
}

Int parseSetCwd(char *args[], int num)
{
// TheSuperHackers @bugfix CryoTheRenegade 29/08/2026
// -setCwd <path> overrides the working directory.
if (num <= 1 || args[1] == nullptr || args[1][0] == '-' || args[1][0] == '/')
{
rts::setCurrentDirectoryToExecutablePath();
return 1;
}
if (!rts::setCurrentDirectoryToPath(args[1]))
rts::setCurrentDirectoryToExecutablePath();
return 2;
}

Int parseXRes(char *args[], int num)
{
if (num > 1)
Expand Down Expand Up @@ -1141,6 +1163,12 @@ 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 },

// TheSuperHackers @feature 14/08/2026
// Use the current working directory as provided by the OS, or an explicit path.
// Without either flag the working directory is forced to the executable directory.
{ "-setCwd", parseSetCwd },
{ "-useCwd", parseUseCwd },
};

// These Params are parsed during Engine Init before INI data is loaded
Expand Down Expand Up @@ -1309,71 +1337,12 @@ static CommandLineParam paramsForEngineInit[] =

};

char *nextParam(char *newSource, const char *seps)
static void parseCommandLine(const CommandLineParam* params, int numParams, std::vector<Bool> *parsedArguments = nullptr)
{
static char *source = nullptr;
if (newSource)
{
source = newSource;
}
if (!source)
{
return nullptr;
}

// find first separator
char *first = source;//strpbrk(source, seps);
if (first)
{
// go past separator
char *firstSep = strpbrk(first, seps);
char firstChar[2] = {0,0};
if (firstSep == first)
{
firstChar[0] = *first;
while (*first == firstChar[0]) first++;
}

// find end
char *end;
if (firstChar[0])
end = strpbrk(first, firstChar);
else
end = strpbrk(first, seps);

// trim string & save next start pos
if (end)
{
source = end+1;
*end = 0;

if (!*source)
source = nullptr;
}
else
{
source = nullptr;
}

if (first && !*first)
first = nullptr;
}

return first;
}

static void parseCommandLine(const CommandLineParam* params, int numParams)
{
std::vector<char*> argv;

std::string cmdLine = GetCommandLineA();
char *token = nextParam(&cmdLine[0], "\" ");
while (token != nullptr)
{
argv.push_back(strtrim(token));
token = nextParam(nullptr, "\" ");
}
int argc = argv.size();
const int argc = __argc;
char **argv = __argv;
if (parsedArguments != nullptr && parsedArguments->size() < static_cast<size_t>(argc > 0 ? argc - 1 : 0))
parsedArguments->resize(argc - 1, FALSE);

int arg = 1;

Expand Down Expand Up @@ -1407,7 +1376,14 @@ static void parseCommandLine(const CommandLineParam* params, int numParams)
continue;
if (strnicmp(argv[arg], params[param].name, len) == 0)
{
arg += params[param].func(&argv[0]+arg, argc-arg);
const int parsedArg = arg;
const int parsedArgCount = params[param].func(&argv[0]+arg, argc-arg);
if (parsedArguments != nullptr)
{
for (int i = 0; i < parsedArgCount && parsedArg + i < argc; ++i)
(*parsedArguments)[parsedArg + i - 1] = TRUE;
}
arg += parsedArgCount;
found = true;
break;
}
Expand All @@ -1419,6 +1395,15 @@ static void parseCommandLine(const CommandLineParam* params, int numParams)
}
}

bool CommandLine::wasCommandLineArgumentParsed(int argIndex)
{
if (TheGlobalData == nullptr)
return false;

const BoolVector &parsedArguments = TheGlobalData->m_commandLineData.m_parsedArguments;
return argIndex >= 0 && argIndex < static_cast<int>(parsedArguments.size()) && parsedArguments[argIndex];
}

void createGlobalData()
{
if (TheGlobalData == nullptr)
Expand All @@ -1435,7 +1420,10 @@ void CommandLine::parseCommandLineForStartup()
return;
TheWritableGlobalData->m_commandLineData.m_hasParsedCommandLineForStartup = true;

parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup));
parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup),
&TheWritableGlobalData->m_commandLineData.m_parsedArguments);

rts::setCurrentDirectoryToExecutablePathIfNotSet();
}

void CommandLine::parseCommandLineForEngineInit()
Expand All @@ -1448,5 +1436,6 @@ void CommandLine::parseCommandLineForEngineInit()
("parseCommandLineForEngineInit is expected to be called once only\n"));
TheWritableGlobalData->m_commandLineData.m_hasParsedCommandLineForEngineInit = true;

parseCommandLine(paramsForEngineInit, ARRAY_SIZE(paramsForEngineInit));
parseCommandLine(paramsForEngineInit, ARRAY_SIZE(paramsForEngineInit),
&TheWritableGlobalData->m_commandLineData.m_parsedArguments);
}
79 changes: 79 additions & 0 deletions Core/GameEngine/Source/Common/WorkingDirectory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2025 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/>.
*/

#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine

#include "Common/WorkingDirectory.h"

namespace rts
{

static Bool s_workingDirectorySet = FALSE;

Bool setCurrentDirectoryToExecutablePath()
{
Char buffer[_MAX_PATH];
const DWORD len = GetModuleFileName(nullptr, buffer, ARRAY_SIZE(buffer));
if (len == 0 || len >= ARRAY_SIZE(buffer))
{
DEBUG_LOG(("Failed to get executable path for working directory (error %d)", GetLastError()));
return FALSE;
}

if (Char *pEnd = strrchr(buffer, '\\'))
{
*pEnd = 0;
}

if (::SetCurrentDirectory(buffer) == 0)
{
DEBUG_LOG(("Failed to set working directory to executable path '%s' (error %d)", buffer, GetLastError()));
return FALSE;
}

s_workingDirectorySet = TRUE;
return TRUE;
}

Bool setCurrentDirectoryToPath(const char *path)
{
if (path == nullptr || path[0] == '\0')
return FALSE;

if (::SetCurrentDirectory(path) == 0)
{
DEBUG_LOG(("Failed to set working directory to '%s' (error %d)", path, GetLastError()));
return FALSE;
}

s_workingDirectorySet = TRUE;
return TRUE;
}

void keepCurrentDirectory()
{
s_workingDirectorySet = TRUE;
}

void setCurrentDirectoryToExecutablePathIfNotSet()
{
if (!s_workingDirectorySet)
setCurrentDirectoryToExecutablePath();
}

} // namespace rts
Loading
Loading