Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
492f69c
feat(system): Add -cwd option to keep or override the startup working…
CryoTheRenegade Aug 14, 2026
eef3f61
fix(system): Restore WorldBuilder buf and harden -cwd directory changes
CryoTheRenegade Aug 14, 2026
acb2e49
refactor(system): Move startup working directory logic out of Command…
CryoTheRenegade Aug 15, 2026
3dc9021
refactor(system): Parse -cwd once in CommandLine startup
CryoTheRenegade Aug 26, 2026
c733abd
fix(system): Initialize WorldBuilder -cwd skip flag in a constructor
CryoTheRenegade Aug 26, 2026
110ba3d
fix(system): Make cwd overrides explicit
CryoTheRenegade Aug 29, 2026
7ee4931
refactor(system): Split working directory options
CryoTheRenegade Sep 2, 2026
0b91889
refactor(system): Record parsed startup arguments
CryoTheRenegade Sep 2, 2026
c63651c
fix(system): Preserve startup argument parsing state
CryoTheRenegade Sep 2, 2026
4bc1ee3
fix(system): Use CRT command-line arguments
CryoTheRenegade Sep 3, 2026
9b728cf
refactor(system): Clean up command-line state
CryoTheRenegade Sep 4, 2026
474f79e
fix(system): Honor last working directory option
CryoTheRenegade Sep 4, 2026
3597861
refactor(system): Simplify working directory handling
CryoTheRenegade Sep 6, 2026
6f4fce1
refactor(system): Clean up startup working directory handling
CryoTheRenegade Sep 6, 2026
abdddd9
fix(system): Preserve directory fallback when startup capture fails
CryoTheRenegade Sep 6, 2026
7b8edb9
fix(system): Simplify startup capture and accept prefixed paths
CryoTheRenegade Sep 6, 2026
92cada9
refactor(system): Remove startup initialization pragmas
CryoTheRenegade Sep 6, 2026
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 @@ -693,6 +694,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);
};
47 changes: 47 additions & 0 deletions Core/GameEngine/Include/Common/WorkingDirectory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
** 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 "Lib/BaseType.h"

namespace rts
{

// TheSuperHackers @feature 14/08/2026
// Saves and restores the process working directory.
class WorkingDirectory
{
public:
static Bool setStartupWorkingDirectory();
static Bool setExecutableWorkingDirectory();
// Relative paths are resolved from the current working directory.
static Bool setCustomWorkingDirectory(const char *path);
// Returns true after any setter call, including a failed attempt.
static Bool hasSetWorkingDirectory();

private:
static Bool saveStartupWorkingDirectory();
static Bool setWorkingDirectory(const char *path);

static Bool s_hasSetWorkingDirectory;
static Char s_startupWorkingDirectory[];
static const Bool s_startupWorkingDirectoryInitializer;
};

} // namespace rts
156 changes: 67 additions & 89 deletions Core/GameEngine/Source/Common/CommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
#include "Common/LocalFileSystem.h"
#include "Common/Recorder.h"
#include "Common/version.h"
#include "Common/WorkingDirectory.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,33 @@ Int parseJobs(char *args[], int num)
return 1;
}

Int parseUseCwd(char *[], int)
{
// TheSuperHackers @feature 14/08/2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Obsolete comment line

// -useCwd restores the startup working directory.
if (!rts::WorkingDirectory::setStartupWorkingDirectory())
rts::WorkingDirectory::setExecutableWorkingDirectory();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe do not call the fallback here? It is already called later.

return 1;
}

Int parseSetCwd(char *args[], int num)
{
// TheSuperHackers @bugfix CryoTheRenegade 29/08/2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is not a bugfix, obsolete comment line.

// -setCwd <path> overrides the working directory.
if (num <= 1)
{
DEBUG_LOG(("-setCwd requires a directory path"));
rts::WorkingDirectory::setExecutableWorkingDirectory();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe call nothing here?

return 1;
}
if (rts::WorkingDirectory::setCustomWorkingDirectory(args[1]))
return 2;

rts::WorkingDirectory::setExecutableWorkingDirectory();
// Leave a failed option-like value available for subsequent argument parsing.
return args[1][0] == '-' || args[1][0] == '/' ? 1 : 2;
}

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Author missing before date.

// 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 @@ -1326,71 +1359,12 @@ static CommandLineParam paramsForEngineInit[] =

};

char *nextParam(char *newSource, const char *seps)
{
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)
static void parseCommandLine(const CommandLineParam* params, int numParams, BoolVector &parsedArguments)
{
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;
// Preserve arguments recorded by the earlier parsing phase.
parsedArguments.resize(argc > 0 ? argc - 1 : 0, FALSE);

int arg = 1;

Expand All @@ -1407,35 +1381,34 @@ static void parseCommandLine(const CommandLineParam* params, int numParams)
arg = 1;
#endif // DEBUG_LOGGING

// To parse command-line parameters, we loop through a table holding arguments
// and functions to handle them. Comparisons can be case-(in)sensitive, and
// can check the entire string (for testing the presence of a flag) or check
// just the start (for a key=val argument). The handling function can also
// look at the next argument(s), to accommodate multi-arg parameters, e.g. "-p 1234".
while (arg<argc)
// Match complete option names without case sensitivity. Each handler returns
// the number of arguments consumed, including the option itself.
while (arg < argc)
{
// Look at arg #i
Bool found = false;
for (int param=0; !found && param<numParams; ++param)
int parsedArgCount = 1;
for (int param = 0; param < numParams; ++param)
{
int len = strlen(params[param].name);
int len2 = strlen(argv[arg]);
if (len2 != len)
if (stricmp(argv[arg], params[param].name) != 0)
continue;
if (strnicmp(argv[arg], params[param].name, len) == 0)
{
arg += params[param].func(&argv[0]+arg, argc-arg);
found = true;
break;
}
}
if (!found)
{
arg++;

parsedArgCount = params[param].func(argv + arg, argc - arg);
for (int i = 0; i < parsedArgCount && arg + i < argc; ++i)
parsedArguments[arg + i - 1] = TRUE;
break;
}
arg += parsedArgCount;
}
}

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 @@ -1452,7 +1425,11 @@ 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);

if (!rts::WorkingDirectory::hasSetWorkingDirectory())
rts::WorkingDirectory::setExecutableWorkingDirectory();
}

void CommandLine::parseCommandLineForEngineInit()
Expand All @@ -1465,5 +1442,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);
}
Loading
Loading