From adae3f08cb1102d172f4187d9c9e7b9888fd962a Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Fri, 14 Aug 2026 14:24:48 -0600 Subject: [PATCH 1/3] feat(system): Add -cwd option to keep or override the startup working directory Co-authored-by: Cursor --- Core/GameEngine/Include/Common/CommandLine.h | 5 ++ Core/GameEngine/Source/Common/CommandLine.cpp | 53 +++++++++++++++++++ Core/Tools/MapCacheBuilder/Source/WinMain.cpp | 10 +--- Generals/Code/Main/WinMain.cpp | 9 +--- .../Code/Tools/GUIEdit/Source/WinMain.cpp | 10 +--- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 9 +--- GeneralsMD/Code/Main/WinMain.cpp | 9 +--- .../Code/Tools/GUIEdit/Source/WinMain.cpp | 10 +--- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 9 +--- 9 files changed, 70 insertions(+), 54 deletions(-) diff --git a/Core/GameEngine/Include/Common/CommandLine.h b/Core/GameEngine/Include/Common/CommandLine.h index 48e078dc3dd..15f501b17d4 100644 --- a/Core/GameEngine/Include/Common/CommandLine.h +++ b/Core/GameEngine/Include/Common/CommandLine.h @@ -34,4 +34,9 @@ class CommandLine static void parseCommandLineForStartup(); static void parseCommandLineForEngineInit(); + + // TheSuperHackers @feature 14/08/2026 + // Sets the working directory to the executable path, unless -cwd is passed. + // -cwd keeps the OS working directory. -cwd uses the given directory instead. + static void applyStartupWorkingDirectory(); }; diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 772830f0f67..14a0a27bb61 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -463,6 +463,16 @@ Int parseJobs(char *args[], int num) return 1; } +Int parseCwd(char *args[], int num) +{ + // TheSuperHackers @feature 14/08/2026 + // Working directory is applied earlier by CommandLine::applyStartupWorkingDirectory(). + // Consume an optional path argument here so it is not treated as another flag. + if (num > 1 && args[1] != nullptr && args[1][0] != '-') + return 2; + return 1; +} + Int parseXRes(char *args[], int num) { if (num > 1) @@ -1141,6 +1151,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 @@ -1419,6 +1434,44 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) } } +static void setCurrentDirectoryToExecutablePath() +{ + Char buffer[_MAX_PATH]; + GetModuleFileName(nullptr, buffer, sizeof(buffer)); + if (Char *pEnd = strrchr(buffer, '\\')) + { + *pEnd = 0; + } + ::SetCurrentDirectory(buffer); +} + +void CommandLine::applyStartupWorkingDirectory() +{ + std::vector argv; + std::string cmdLine = GetCommandLineA(); + char *token = nextParam(&cmdLine[0], "\" "); + while (token != nullptr) + { + argv.push_back(strtrim(token)); + token = nextParam(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] != '-') + { + ::SetCurrentDirectory(argv[arg + 1]); + } + return; + } + + setCurrentDirectoryToExecutablePath(); +} + void createGlobalData() { if (TheGlobalData == nullptr) diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp index 134f1465980..5644b39ff00 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/CommandLine.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); + CommandLine::applyStartupWorkingDirectory(); /* ** Convert WinMain arguments to simple main argc and argv diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index c8e9bb9961d..bfda08d4161 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -817,14 +817,7 @@ 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::applyStartupWorkingDirectory(); #ifdef RTS_DEBUG diff --git a/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp b/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp index daf1402d74c..11291011bc0 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/CommandLine.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); + CommandLine::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..cc2a2c55d7d 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/CommandLine.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); + CommandLine::applyStartupWorkingDirectory(); TheFileSystem = new FileSystem; diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 0d37cab5933..abf6f7087b5 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -824,14 +824,7 @@ 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::applyStartupWorkingDirectory(); #ifdef RTS_DEBUG diff --git a/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp b/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp index 493d5aecc4e..828fae43789 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/CommandLine.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); + CommandLine::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..7afabb3f500 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/CommandLine.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); + CommandLine::applyStartupWorkingDirectory(); TheFileSystem = new FileSystem; From d92c83ebe3927e2b192f4597b9afbfa5b24d793d Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Fri, 14 Aug 2026 15:00:15 -0600 Subject: [PATCH 2/3] fix(system): Restore WorldBuilder buf and harden -cwd directory changes Co-authored-by: Cursor --- Core/GameEngine/Source/Common/CommandLine.cpp | 27 +++++++++++++++---- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 1 + .../Tools/WorldBuilder/src/WorldBuilder.cpp | 1 + 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 14a0a27bb61..d2e78a1435f 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -1434,15 +1434,28 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) } } -static void setCurrentDirectoryToExecutablePath() +static Bool setCurrentDirectoryToExecutablePath() { Char buffer[_MAX_PATH]; - GetModuleFileName(nullptr, buffer, sizeof(buffer)); + 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; } - ::SetCurrentDirectory(buffer); + + if (::SetCurrentDirectory(buffer) == 0) + { + DEBUG_LOG(("Failed to set working directory to executable path '%s' (error %d)", buffer, GetLastError())); + return FALSE; + } + + return TRUE; } void CommandLine::applyStartupWorkingDirectory() @@ -1462,9 +1475,13 @@ void CommandLine::applyStartupWorkingDirectory() if (stricmp(argv[arg], "-cwd") != 0) continue; - if (arg + 1 < argc && argv[arg + 1] != nullptr && argv[arg + 1][0] != '-') + if (arg + 1 < argc && argv[arg + 1] != nullptr && argv[arg + 1][0] != '-' && argv[arg + 1][0] != '\0') { - ::SetCurrentDirectory(argv[arg + 1]); + if (::SetCurrentDirectory(argv[arg + 1]) == 0) + { + DEBUG_LOG(("Failed to set working directory to '%s' (error %d)", argv[arg + 1], GetLastError())); + setCurrentDirectoryToExecutablePath(); + } } return; } diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index cc2a2c55d7d..b17c1de3c88 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -331,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/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 7afabb3f500..9322c33623e 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -342,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); From 04d6eaa7f92aa97d3cab5abafb384b63c47601d6 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Sat, 15 Aug 2026 16:56:39 -0600 Subject: [PATCH 3/3] refactor(system): Move startup working directory logic out of CommandLine Co-authored-by: Cursor --- Core/GameEngine/CMakeLists.txt | 2 + Core/GameEngine/Include/Common/CommandLine.h | 5 - .../Include/Common/WorkingDirectory.h | 36 +++++ Core/GameEngine/Source/Common/CommandLine.cpp | 67 ++------- .../Source/Common/WorkingDirectory.cpp | 142 ++++++++++++++++++ Core/Tools/MapCacheBuilder/Source/WinMain.cpp | 4 +- .../GameEngine/Include/Common/GlobalData.h | 4 + .../GameEngine/Source/Common/GlobalData.cpp | 1 + Generals/Code/Main/WinMain.cpp | 7 +- .../Code/Tools/GUIEdit/Source/WinMain.cpp | 4 +- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 4 +- .../GameEngine/Include/Common/GlobalData.h | 4 + .../GameEngine/Source/Common/GlobalData.cpp | 1 + GeneralsMD/Code/Main/WinMain.cpp | 6 +- .../Code/Tools/GUIEdit/Source/WinMain.cpp | 4 +- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 4 +- 16 files changed, 217 insertions(+), 78 deletions(-) create mode 100644 Core/GameEngine/Include/Common/WorkingDirectory.h create mode 100644 Core/GameEngine/Source/Common/WorkingDirectory.cpp 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/CommandLine.h b/Core/GameEngine/Include/Common/CommandLine.h index 15f501b17d4..48e078dc3dd 100644 --- a/Core/GameEngine/Include/Common/CommandLine.h +++ b/Core/GameEngine/Include/Common/CommandLine.h @@ -34,9 +34,4 @@ class CommandLine static void parseCommandLineForStartup(); static void parseCommandLineForEngineInit(); - - // TheSuperHackers @feature 14/08/2026 - // Sets the working directory to the executable path, unless -cwd is passed. - // -cwd keeps the OS working directory. -cwd uses the given directory instead. - static void applyStartupWorkingDirectory(); }; 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 d2e78a1435f..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" @@ -466,10 +467,15 @@ Int parseJobs(char *args[], int num) Int parseCwd(char *args[], int num) { // TheSuperHackers @feature 14/08/2026 - // Working directory is applied earlier by CommandLine::applyStartupWorkingDirectory(). - // Consume an optional path argument here so it is not treated as another flag. - if (num > 1 && args[1] != nullptr && args[1][0] != '-') + // -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; } @@ -1434,61 +1440,6 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) } } -static 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; -} - -void CommandLine::applyStartupWorkingDirectory() -{ - std::vector argv; - std::string cmdLine = GetCommandLineA(); - char *token = nextParam(&cmdLine[0], "\" "); - while (token != nullptr) - { - argv.push_back(strtrim(token)); - token = nextParam(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 (::SetCurrentDirectory(argv[arg + 1]) == 0) - { - DEBUG_LOG(("Failed to set working directory to '%s' (error %d)", argv[arg + 1], GetLastError())); - setCurrentDirectoryToExecutablePath(); - } - } - return; - } - - setCurrentDirectoryToExecutablePath(); -} - void createGlobalData() { if (TheGlobalData == nullptr) 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 5644b39ff00..70a8ab09b2a 100644 --- a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp +++ b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp @@ -43,7 +43,7 @@ // USER INCLUDES ////////////////////////////////////////////////////////////// #include "Lib/BaseType.h" -#include "Common/CommandLine.h" +#include "Common/WorkingDirectory.h" #include "Common/Debug.h" #include "Common/GameMemory.h" #include "Common/GlobalData.h" @@ -221,7 +221,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // save application instance ApplicationHInstance = hInstance; - CommandLine::applyStartupWorkingDirectory(); + 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 bfda08d4161..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,7 +818,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // initialize the memory manager early initMemoryManager(); - CommandLine::applyStartupWorkingDirectory(); + CommandLine::parseCommandLineForStartup(); + if (TheGlobalData->m_changeCurrentWorkingDirectoryToExecutablePath) + rts::setCurrentDirectoryToExecutablePath(); #ifdef RTS_DEBUG @@ -838,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 11291011bc0..9160b72e8f1 100644 --- a/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp +++ b/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp @@ -49,7 +49,7 @@ #include // USER INCLUDES ////////////////////////////////////////////////////////////// -#include "Common/CommandLine.h" +#include "Common/WorkingDirectory.h" #include "Common/Debug.h" #include "Common/FramePacer.h" #include "Common/GameMemory.h" @@ -185,7 +185,7 @@ Int APIENTRY WinMain(HINSTANCE hInstance, HACCEL hAccelTable; Bool quit = FALSE; - CommandLine::applyStartupWorkingDirectory(); + 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 b17c1de3c88..ba7db58afae 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -32,7 +32,7 @@ //#include #include "W3DDevice/GameClient/W3DFileSystem.h" -#include "Common/CommandLine.h" +#include "Common/WorkingDirectory.h" #include "Common/FramePacer.h" #include "Common/GlobalData.h" #include "WHeightMapEdit.h" @@ -306,7 +306,7 @@ BOOL CWorldBuilderApp::InitInstance() Enable3dControlsStatic(); // Call this when linking to MFC statically #endif - CommandLine::applyStartupWorkingDirectory(); + rts::applyStartupWorkingDirectory(); TheFileSystem = new FileSystem; 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 abf6f7087b5..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,7 +825,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // initialize the memory manager early initMemoryManager(); - CommandLine::applyStartupWorkingDirectory(); + CommandLine::parseCommandLineForStartup(); + if (TheGlobalData->m_changeCurrentWorkingDirectoryToExecutablePath) + rts::setCurrentDirectoryToExecutablePath(); #ifdef RTS_DEBUG @@ -865,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 828fae43789..84f339c4202 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp +++ b/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp @@ -49,7 +49,7 @@ #include // USER INCLUDES ////////////////////////////////////////////////////////////// -#include "Common/CommandLine.h" +#include "Common/WorkingDirectory.h" #include "Common/Debug.h" #include "Common/FramePacer.h" #include "Common/GameMemory.h" @@ -185,7 +185,7 @@ Int APIENTRY WinMain(HINSTANCE hInstance, HACCEL hAccelTable; Bool quit = FALSE; - CommandLine::applyStartupWorkingDirectory(); + 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 9322c33623e..52a673a6796 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -32,7 +32,7 @@ //#include #include "W3DDevice/GameClient/W3DFileSystem.h" -#include "Common/CommandLine.h" +#include "Common/WorkingDirectory.h" #include "Common/FramePacer.h" #include "Common/GlobalData.h" #include "WHeightMapEdit.h" @@ -316,7 +316,7 @@ BOOL CWorldBuilderApp::InitInstance() Enable3dControlsStatic(); // Call this when linking to MFC statically #endif - CommandLine::applyStartupWorkingDirectory(); + rts::applyStartupWorkingDirectory(); TheFileSystem = new FileSystem;