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
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
// Startup working directory helpers. By default the process working directory is
// the executable directory. -cwd keeps the OS directory. -cwd <path> uses that path.

Bool setCurrentDirectoryToExecutablePath();
Bool setCurrentDirectoryToPath(const char *path);

// For tools that do not parse CommandLine startup flags.
void applyStartupWorkingDirectory();

} // namespace rts
21 changes: 21 additions & 0 deletions Core/GameEngine/Source/Common/CommandLine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#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"
Expand Down Expand Up @@ -463,6 +464,21 @@ Int parseJobs(char *args[], int num)
return 1;
}

Int parseCwd(char *args[], int num)
{
// TheSuperHackers @feature 14/08/2026
// -cwd keeps the OS working directory. -cwd <path> uses that directory instead.
TheWritableGlobalData->m_changeCurrentWorkingDirectoryToExecutablePath = FALSE;

if (num > 1 && args[1] != nullptr && args[1][0] != '-' && args[1][0] != '\0')
{
if (!rts::setCurrentDirectoryToPath(args[1]))
rts::setCurrentDirectoryToExecutablePath();
return 2;
}
return 1;
}

Int parseXRes(char *args[], int num)
{
if (num > 1)
Expand Down Expand Up @@ -1141,6 +1157,11 @@ 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 optional path.
// Without this flag the working directory is forced to the executable directory.
{ "-cwd", parseCwd },
};

// These Params are parsed during Engine Init before INI data is loaded
Expand Down
142 changes: 142 additions & 0 deletions Core/GameEngine/Source/Common/WorkingDirectory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
** 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"
#include "WWLib/trim.h"

namespace rts
{

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;
}

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;
}

return TRUE;
}

static char *nextWorkingDirectoryParam(char *newSource, const char *seps)
{
static char *source = nullptr;
if (newSource)
{
source = newSource;
}
if (!source)
{
return nullptr;
}

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

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

if (end)
{
source = end+1;
*end = 0;

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

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

return first;
}

void applyStartupWorkingDirectory()
{
std::vector<char*> argv;
std::string cmdLine = GetCommandLineA();
char *token = nextWorkingDirectoryParam(&cmdLine[0], "\" ");
while (token != nullptr)
{
argv.push_back(strtrim(token));
token = nextWorkingDirectoryParam(nullptr, "\" ");
}

const int argc = (int)argv.size();
for (int arg = 1; arg < argc; ++arg)
{
if (stricmp(argv[arg], "-cwd") != 0)
continue;

if (arg + 1 < argc && argv[arg + 1] != nullptr && argv[arg + 1][0] != '-' && argv[arg + 1][0] != '\0')
{
if (!setCurrentDirectoryToPath(argv[arg + 1]))
setCurrentDirectoryToExecutablePath();
}
return;
}

setCurrentDirectoryToExecutablePath();
}

} // namespace rts
10 changes: 2 additions & 8 deletions Core/Tools/MapCacheBuilder/Source/WinMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

// USER INCLUDES //////////////////////////////////////////////////////////////
#include "Lib/BaseType.h"
#include "Common/WorkingDirectory.h"
#include "Common/Debug.h"
#include "Common/GameMemory.h"
#include "Common/GlobalData.h"
Expand Down Expand Up @@ -220,14 +221,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
// save application instance
ApplicationHInstance = hInstance;


// Set the current directory to the app directory.
char buf[_MAX_PATH];
GetModuleFileName(nullptr, buf, sizeof(buf));
if (char *pEnd = strrchr(buf, '\\')) {
*pEnd = 0;
}
::SetCurrentDirectory(buf);
rts::applyStartupWorkingDirectory();

/*
** Convert WinMain arguments to simple main argc and argv
Expand Down
4 changes: 4 additions & 0 deletions Generals/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ class GlobalData : public SubsystemInterface
// Run game without graphics, input or audio.
Bool m_headless;

// TheSuperHackers @feature 14/08/2026
// On startup change the current working directory to the executable's location.
Bool m_changeCurrentWorkingDirectoryToExecutablePath;

Bool m_windowed;
Int m_xResolution;
Int m_yResolution;
Expand Down
1 change: 1 addition & 0 deletions Generals/Code/GameEngine/Source/Common/GlobalData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ GlobalData::GlobalData()
m_framesPerSecondLimit = 0;
m_chipSetType = 0;
m_headless = FALSE;
m_changeCurrentWorkingDirectoryToExecutablePath = TRUE;
m_windowed = 0;
m_xResolution = DEFAULT_DISPLAY_WIDTH;
m_yResolution = DEFAULT_DISPLAY_HEIGHT;
Expand Down
14 changes: 4 additions & 10 deletions Generals/Code/Main/WinMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#include "WinMain.h"
#include "Lib/BaseType.h"
#include "Common/CommandLine.h"
#include "Common/WorkingDirectory.h"
#include "Common/CriticalSection.h"
#include "Common/GlobalData.h"
#include "Common/GameEngine.h"
Expand Down Expand Up @@ -817,14 +818,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
// initialize the memory manager early
initMemoryManager();

/// @todo remove this force set of working directory later
Char buffer[ _MAX_PATH ];
GetModuleFileName( nullptr, buffer, sizeof( buffer ) );
if (Char *pEnd = strrchr(buffer, '\\'))
{
*pEnd = 0;
}
::SetCurrentDirectory(buffer);
CommandLine::parseCommandLineForStartup();
if (TheGlobalData->m_changeCurrentWorkingDirectoryToExecutablePath)
rts::setCurrentDirectoryToExecutablePath();


#ifdef RTS_DEBUG
Expand All @@ -845,8 +841,6 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
// Force to be loaded from a file, not a resource so same exe can be used in germany and retail.
gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, "Install_Final.bmp", IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE);

CommandLine::parseCommandLineForStartup();

#ifdef RTS_ENABLE_CRASHDUMP
// Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup
MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData());
Expand Down
10 changes: 2 additions & 8 deletions Generals/Code/Tools/GUIEdit/Source/WinMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include <commctrl.h>

// USER INCLUDES //////////////////////////////////////////////////////////////
#include "Common/WorkingDirectory.h"
#include "Common/Debug.h"
#include "Common/FramePacer.h"
#include "Common/GameMemory.h"
Expand Down Expand Up @@ -184,14 +185,7 @@ Int APIENTRY WinMain(HINSTANCE hInstance,
HACCEL hAccelTable;
Bool quit = FALSE;

/// @todo remove this force set of working directory later
Char buffer[ _MAX_PATH ];
GetModuleFileName( nullptr, buffer, sizeof( buffer ) );
if (Char *pEnd = strrchr(buffer, '\\'))
{
*pEnd = 0;
}
::SetCurrentDirectory(buffer);
rts::applyStartupWorkingDirectory();

// initialize the memory manager early
initMemoryManager();
Expand Down
10 changes: 3 additions & 7 deletions Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

//#include <wsys/StdFileSystem.h>
#include "W3DDevice/GameClient/W3DFileSystem.h"
#include "Common/WorkingDirectory.h"
#include "Common/FramePacer.h"
#include "Common/GlobalData.h"
#include "WHeightMapEdit.h"
Expand Down Expand Up @@ -305,13 +306,7 @@ BOOL CWorldBuilderApp::InitInstance()
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif

// Set the current directory to the app directory.
char buf[_MAX_PATH];
GetModuleFileName(nullptr, buf, sizeof(buf));
if (char *pEnd = strrchr(buf, '\\')) {
*pEnd = 0;
}
::SetCurrentDirectory(buf);
rts::applyStartupWorkingDirectory();

TheFileSystem = new FileSystem;

Expand All @@ -336,6 +331,7 @@ BOOL CWorldBuilderApp::InitInstance()
TheWritableGlobalData->m_debugIgnoreAsserts = true;
#endif

char buf[_MAX_PATH];
#if 1
// srj sez: put INI into our user data folder, not the ap dir
free((void*)m_pszProfileName);
Expand Down
4 changes: 4 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ class GlobalData : public SubsystemInterface
// Run game without graphics, input or audio.
Bool m_headless;

// TheSuperHackers @feature 14/08/2026
// On startup change the current working directory to the executable's location.
Bool m_changeCurrentWorkingDirectoryToExecutablePath;

Bool m_windowed;
Int m_xResolution;
Int m_yResolution;
Expand Down
1 change: 1 addition & 0 deletions GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ GlobalData::GlobalData()
m_framesPerSecondLimit = 0;
m_chipSetType = 0;
m_headless = FALSE;
m_changeCurrentWorkingDirectoryToExecutablePath = TRUE;
m_windowed = 0;
m_xResolution = DEFAULT_DISPLAY_WIDTH;
m_yResolution = DEFAULT_DISPLAY_HEIGHT;
Expand Down
Loading
Loading