diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt
index 0f36ff63383..e50ba0d07a9 100644
--- a/Core/GameEngine/CMakeLists.txt
+++ b/Core/GameEngine/CMakeLists.txt
@@ -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
@@ -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
diff --git a/Core/GameEngine/Include/Common/WorkingDirectory.h b/Core/GameEngine/Include/Common/WorkingDirectory.h
new file mode 100644
index 00000000000..013f420cb69
--- /dev/null
+++ b/Core/GameEngine/Include/Common/WorkingDirectory.h
@@ -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 .
+*/
+
+#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 uses that path.
+
+Bool setCurrentDirectoryToExecutablePath();
+Bool setCurrentDirectoryToPath(const char *path);
+
+// For tools that do not parse CommandLine startup flags.
+void applyStartupWorkingDirectory();
+
+} // namespace rts
diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp
index 772830f0f67..a5383ba9880 100644
--- a/Core/GameEngine/Source/Common/CommandLine.cpp
+++ b/Core/GameEngine/Source/Common/CommandLine.cpp
@@ -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"
@@ -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 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)
@@ -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
diff --git a/Core/GameEngine/Source/Common/WorkingDirectory.cpp b/Core/GameEngine/Source/Common/WorkingDirectory.cpp
new file mode 100644
index 00000000000..6852e6a9abe
--- /dev/null
+++ b/Core/GameEngine/Source/Common/WorkingDirectory.cpp
@@ -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 .
+*/
+
+#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 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
diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp
index 134f1465980..70a8ab09b2a 100644
--- a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp
+++ b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp
@@ -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"
@@ -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
diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h
index e631654250d..769b02936f3 100644
--- a/Generals/Code/GameEngine/Include/Common/GlobalData.h
+++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h
@@ -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;
diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
index f7720c351a2..c2da0341795 100644
--- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
+++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp
@@ -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;
diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp
index c8e9bb9961d..bb1532057bb 100644
--- a/Generals/Code/Main/WinMain.cpp
+++ b/Generals/Code/Main/WinMain.cpp
@@ -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"
@@ -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
@@ -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());
diff --git a/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp b/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp
index daf1402d74c..9160b72e8f1 100644
--- a/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp
+++ b/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp
@@ -49,6 +49,7 @@
#include
// USER INCLUDES //////////////////////////////////////////////////////////////
+#include "Common/WorkingDirectory.h"
#include "Common/Debug.h"
#include "Common/FramePacer.h"
#include "Common/GameMemory.h"
@@ -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();
diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp
index c122151259d..ba7db58afae 100644
--- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp
+++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp
@@ -32,6 +32,7 @@
//#include
#include "W3DDevice/GameClient/W3DFileSystem.h"
+#include "Common/WorkingDirectory.h"
#include "Common/FramePacer.h"
#include "Common/GlobalData.h"
#include "WHeightMapEdit.h"
@@ -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;
@@ -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);
diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
index 7f484111672..f1c5ae90048 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h
@@ -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;
diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
index e862cd149d5..1fd15af0374 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp
@@ -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;
diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp
index 0d37cab5933..3d8e46f2b6e 100644
--- a/GeneralsMD/Code/Main/WinMain.cpp
+++ b/GeneralsMD/Code/Main/WinMain.cpp
@@ -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"
@@ -824,14 +825,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
@@ -872,7 +868,6 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, "Install_Final.bmp", IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE);
#endif
- CommandLine::parseCommandLineForStartup();
#ifdef RTS_ENABLE_CRASHDUMP
// Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup
MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData());
diff --git a/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp b/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp
index 493d5aecc4e..84f339c4202 100644
--- a/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp
+++ b/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp
@@ -49,6 +49,7 @@
#include
// USER INCLUDES //////////////////////////////////////////////////////////////
+#include "Common/WorkingDirectory.h"
#include "Common/Debug.h"
#include "Common/FramePacer.h"
#include "Common/GameMemory.h"
@@ -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();
diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp
index 5f19126638a..52a673a6796 100644
--- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp
+++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp
@@ -32,6 +32,7 @@
//#include
#include "W3DDevice/GameClient/W3DFileSystem.h"
+#include "Common/WorkingDirectory.h"
#include "Common/FramePacer.h"
#include "Common/GlobalData.h"
#include "WHeightMapEdit.h"
@@ -315,13 +316,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;
@@ -347,6 +342,7 @@ BOOL CWorldBuilderApp::InitInstance()
#endif
DEBUG_LOG(("TheWritableGlobalData %x", TheWritableGlobalData));
+ char buf[_MAX_PATH];
#if 1
// srj sez: put INI into our user data folder, not the ap dir
free((void*)m_pszProfileName);