From adae3f08cb1102d172f4187d9c9e7b9888fd962a Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Fri, 14 Aug 2026 14:24:48 -0600 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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; From 1dbd69110bdbad2ba174229b577a1ecf0274f4a9 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Wed, 26 Aug 2026 12:15:38 -0600 Subject: [PATCH 04/11] refactor(system): Parse -cwd once in CommandLine startup Have the game and tools share parseCommandLineForStartup so the working directory is applied in one place, without a GlobalData flag or a second tokenizer. Co-authored-by: Cursor --- Core/GameEngine/Include/Common/CommandLine.h | 1 + .../Include/Common/WorkingDirectory.h | 8 +- Core/GameEngine/Source/Common/CommandLine.cpp | 8 +- .../Source/Common/WorkingDirectory.cpp | 78 ------------------- Core/Tools/MapCacheBuilder/Source/WinMain.cpp | 17 +++- .../GameEngine/Include/Common/GlobalData.h | 4 - .../GameEngine/Source/Common/GlobalData.cpp | 1 - Generals/Code/Main/WinMain.cpp | 3 - .../Code/Tools/GUIEdit/Source/GUIEdit.cpp | 4 +- .../Code/Tools/GUIEdit/Source/WinMain.cpp | 6 +- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 35 +++++++-- .../GameEngine/Include/Common/GlobalData.h | 4 - .../GameEngine/Source/Common/GlobalData.cpp | 1 - GeneralsMD/Code/Main/WinMain.cpp | 3 - .../Code/Tools/GUIEdit/Source/GUIEdit.cpp | 4 +- .../Code/Tools/GUIEdit/Source/WinMain.cpp | 6 +- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 35 +++++++-- 17 files changed, 95 insertions(+), 123 deletions(-) diff --git a/Core/GameEngine/Include/Common/CommandLine.h b/Core/GameEngine/Include/Common/CommandLine.h index 48e078dc3dd..bb65d3cd80b 100644 --- a/Core/GameEngine/Include/Common/CommandLine.h +++ b/Core/GameEngine/Include/Common/CommandLine.h @@ -32,6 +32,7 @@ class CommandLine { public: + // Parses startup flags and applies the process working directory (-cwd). static void parseCommandLineForStartup(); static void parseCommandLineForEngineInit(); }; diff --git a/Core/GameEngine/Include/Common/WorkingDirectory.h b/Core/GameEngine/Include/Common/WorkingDirectory.h index 013f420cb69..d9391ca3808 100644 --- a/Core/GameEngine/Include/Common/WorkingDirectory.h +++ b/Core/GameEngine/Include/Common/WorkingDirectory.h @@ -24,13 +24,11 @@ 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. +// Process working directory helpers. CommandLine::parseCommandLineForStartup() +// applies these: default is the executable directory, -cwd keeps the OS +// directory, and -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 a5383ba9880..ca3e0be8eb8 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -464,11 +464,14 @@ Int parseJobs(char *args[], int num) return 1; } +// Set when -cwd is present so parseCommandLineForStartup does not force the executable directory. +static Bool s_cwdOptionSpecified = FALSE; + 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; + s_cwdOptionSpecified = TRUE; if (num > 1 && args[1] != nullptr && args[1][0] != '-' && args[1][0] != '\0') { @@ -1457,6 +1460,9 @@ void CommandLine::parseCommandLineForStartup() TheWritableGlobalData->m_commandLineData.m_hasParsedCommandLineForStartup = true; parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup)); + + if (!s_cwdOptionSpecified) + rts::setCurrentDirectoryToExecutablePath(); } void CommandLine::parseCommandLineForEngineInit() diff --git a/Core/GameEngine/Source/Common/WorkingDirectory.cpp b/Core/GameEngine/Source/Common/WorkingDirectory.cpp index 6852e6a9abe..95490cd2657 100644 --- a/Core/GameEngine/Source/Common/WorkingDirectory.cpp +++ b/Core/GameEngine/Source/Common/WorkingDirectory.cpp @@ -19,7 +19,6 @@ #include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine #include "Common/WorkingDirectory.h" -#include "WWLib/trim.h" namespace rts { @@ -62,81 +61,4 @@ Bool setCurrentDirectoryToPath(const char *path) 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 70a8ab09b2a..862831f266c 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/WorkingDirectory.h" +#include "Common/CommandLine.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; - rts::applyStartupWorkingDirectory(); + CommandLine::parseCommandLineForStartup(); /* ** Convert WinMain arguments to simple main argc and argv @@ -231,6 +231,17 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, token = nextParam(lpCmdLine, "\" "); while (token != nullptr) { char * str = strtrim(token); + if (stricmp(str, "-cwd") == 0) + { + token = nextParam(nullptr, "\" "); + if (token != nullptr) + { + char *cwdArg = strtrim(token); + if (cwdArg != nullptr && cwdArg[0] != '-' && cwdArg[0] != '\0') + token = nextParam(nullptr, "\" "); + } + continue; + } argvSet.push_back(str); DEBUG_LOG(("Adding '%s'", str)); token = nextParam(nullptr, "\" "); @@ -245,7 +256,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, initSubsystem(TheLocalFileSystem, (LocalFileSystem*)new Win32LocalFileSystem); initSubsystem(TheArchiveFileSystem, (ArchiveFileSystem*)new Win32BIGFileSystem); INI ini; - initSubsystem(TheWritableGlobalData, new GlobalData(), "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); + initSubsystem(TheWritableGlobalData, TheWritableGlobalData, "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); initSubsystem(TheGameText, CreateGameTextInterface()); initSubsystem(TheScienceStore, new ScienceStore(), "Data\\INI\\Default\\Science", "Data\\INI\\Science"); initSubsystem(TheMultiplayerSettings, new MultiplayerSettings(), "Data\\INI\\Default\\Multiplayer", "Data\\INI\\Multiplayer"); diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 769b02936f3..e631654250d 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -120,10 +120,6 @@ 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 c2da0341795..f7720c351a2 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -633,7 +633,6 @@ 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 bb1532057bb..e5ee8b9f254 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -43,7 +43,6 @@ #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" @@ -819,8 +818,6 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, initMemoryManager(); CommandLine::parseCommandLineForStartup(); - if (TheGlobalData->m_changeCurrentWorkingDirectoryToExecutablePath) - rts::setCurrentDirectoryToExecutablePath(); #ifdef RTS_DEBUG diff --git a/Generals/Code/Tools/GUIEdit/Source/GUIEdit.cpp b/Generals/Code/Tools/GUIEdit/Source/GUIEdit.cpp index 0963b8a53c7..0109ea9bc60 100644 --- a/Generals/Code/Tools/GUIEdit/Source/GUIEdit.cpp +++ b/Generals/Code/Tools/GUIEdit/Source/GUIEdit.cpp @@ -508,8 +508,8 @@ void GUIEdit::init() // Game engine specific initializations ------------------------------------- //--------------------------------------------------------------------------- - // create the global data - TheWritableGlobalData = new GlobalData; + // GlobalData is created by CommandLine::parseCommandLineForStartup(). + DEBUG_ASSERTCRASH(TheWritableGlobalData, ("TheWritableGlobalData expected to be created")); TheWritableGlobalData->init(); // TheSuperHackers @info global language relies on global data being initialized diff --git a/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp b/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp index 9160b72e8f1..1b6f25bdf2a 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/WorkingDirectory.h" +#include "Common/CommandLine.h" #include "Common/Debug.h" #include "Common/FramePacer.h" #include "Common/GameMemory.h" @@ -185,11 +185,11 @@ Int APIENTRY WinMain(HINSTANCE hInstance, HACCEL hAccelTable; Bool quit = FALSE; - rts::applyStartupWorkingDirectory(); - // initialize the memory manager early initMemoryManager(); + CommandLine::parseCommandLineForStartup(); + // register a class for our window with the OS registerClass( hInstance ); diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index ba7db58afae..504bf80b0b8 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/WorkingDirectory.h" +#include "Common/CommandLine.h" #include "Common/FramePacer.h" #include "Common/GlobalData.h" #include "WHeightMapEdit.h" @@ -152,8 +152,33 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) return pFile; } +///////////////////////////////////////////////////////////////////////////// +// Skip -cwd [path] so MFC does not treat the path as a map to open. +class WBCommandLineInfo : public CCommandLineInfo +{ +public: + virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override + { + if (m_expectingCwdPath) + { + m_expectingCwdPath = FALSE; + if (!bFlag && pszParam != nullptr && pszParam[0] != 0) + return; + } + if (bFlag && lstrcmpi(pszParam, _T("cwd")) == 0) + { + m_expectingCwdPath = TRUE; + return; + } + + CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); + } + +private: + BOOL m_expectingCwdPath = FALSE; +}; ///////////////////////////////////////////////////////////////////////////// // The one and only CWorldBuilderApp object @@ -278,6 +303,8 @@ BOOL CWorldBuilderApp::InitInstance() // initialize the memory manager early initMemoryManager(); + CommandLine::parseCommandLineForStartup(); + DEBUG_LOG(("starting Worldbuilder.")); #ifdef RTS_DEBUG DEBUG_LOG(("RTS_DEBUG defined.")); @@ -306,8 +333,6 @@ BOOL CWorldBuilderApp::InitInstance() Enable3dControlsStatic(); // Call this when linking to MFC statically #endif - rts::applyStartupWorkingDirectory(); - TheFileSystem = new FileSystem; initSubsystem(TheLocalFileSystem, (LocalFileSystem*)new Win32LocalFileSystem); @@ -319,7 +344,7 @@ BOOL CWorldBuilderApp::InitInstance() INI ini; - initSubsystem(TheWritableGlobalData, new GlobalData(), "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); + initSubsystem(TheWritableGlobalData, TheWritableGlobalData, "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); TheFramePacer = new FramePacer(); @@ -423,7 +448,7 @@ BOOL CWorldBuilderApp::InitInstance() #endif // Parse command line for standard shell commands, DDE, file open - CCommandLineInfo cmdInfo; + WBCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); // Dispatch commands specified on the command line diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index f1c5ae90048..7f484111672 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -121,10 +121,6 @@ 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 1fd15af0374..e862cd149d5 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -637,7 +637,6 @@ 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 3d8e46f2b6e..9e53037e441 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -43,7 +43,6 @@ #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" @@ -826,8 +825,6 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, initMemoryManager(); CommandLine::parseCommandLineForStartup(); - if (TheGlobalData->m_changeCurrentWorkingDirectoryToExecutablePath) - rts::setCurrentDirectoryToExecutablePath(); #ifdef RTS_DEBUG diff --git a/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp b/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp index 23b315cc5f0..0647552baae 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp +++ b/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp @@ -508,8 +508,8 @@ void GUIEdit::init() // Game engine specific initializations ------------------------------------- //--------------------------------------------------------------------------- - // create the global data - TheWritableGlobalData = new GlobalData; + // GlobalData is created by CommandLine::parseCommandLineForStartup(). + DEBUG_ASSERTCRASH(TheWritableGlobalData, ("TheWritableGlobalData expected to be created")); TheWritableGlobalData->init(); // TheSuperHackers @info global language relies on global data being initialized diff --git a/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp b/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp index 84f339c4202..84ed8192793 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/WorkingDirectory.h" +#include "Common/CommandLine.h" #include "Common/Debug.h" #include "Common/FramePacer.h" #include "Common/GameMemory.h" @@ -185,11 +185,11 @@ Int APIENTRY WinMain(HINSTANCE hInstance, HACCEL hAccelTable; Bool quit = FALSE; - rts::applyStartupWorkingDirectory(); - // initialize the memory manager early initMemoryManager(); + CommandLine::parseCommandLineForStartup(); + // register a class for our window with the OS registerClass( hInstance ); diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 52a673a6796..647a6d469ed 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/WorkingDirectory.h" +#include "Common/CommandLine.h" #include "Common/FramePacer.h" #include "Common/GlobalData.h" #include "WHeightMapEdit.h" @@ -152,8 +152,33 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) return pFile; } +///////////////////////////////////////////////////////////////////////////// +// Skip -cwd [path] so MFC does not treat the path as a map to open. +class WBCommandLineInfo : public CCommandLineInfo +{ +public: + virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override + { + if (m_expectingCwdPath) + { + m_expectingCwdPath = FALSE; + if (!bFlag && pszParam != nullptr && pszParam[0] != 0) + return; + } + if (bFlag && lstrcmpi(pszParam, _T("cwd")) == 0) + { + m_expectingCwdPath = TRUE; + return; + } + + CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); + } + +private: + BOOL m_expectingCwdPath = FALSE; +}; ///////////////////////////////////////////////////////////////////////////// // The one and only CWorldBuilderApp object @@ -282,6 +307,8 @@ BOOL CWorldBuilderApp::InitInstance() // initialize the memory manager early initMemoryManager(); + CommandLine::parseCommandLineForStartup(); + #ifdef DEBUG_LOGGING // Turn on console output jba [3/20/2003] DebugSetFlags(DebugGetFlags() | DEBUG_FLAG_LOG_TO_CONSOLE); @@ -316,8 +343,6 @@ BOOL CWorldBuilderApp::InitInstance() Enable3dControlsStatic(); // Call this when linking to MFC statically #endif - rts::applyStartupWorkingDirectory(); - TheFileSystem = new FileSystem; initSubsystem(TheLocalFileSystem, (LocalFileSystem*)new Win32LocalFileSystem); @@ -329,7 +354,7 @@ BOOL CWorldBuilderApp::InitInstance() INI ini; - initSubsystem(TheWritableGlobalData, new GlobalData(), "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); + initSubsystem(TheWritableGlobalData, TheWritableGlobalData, "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); TheFramePacer = new FramePacer(); @@ -440,7 +465,7 @@ BOOL CWorldBuilderApp::InitInstance() #endif // Parse command line for standard shell commands, DDE, file open - CCommandLineInfo cmdInfo; + WBCommandLineInfo cmdInfo; ParseCommandLine(cmdInfo); // Dispatch commands specified on the command line From a45c8dd98b5853f17bf96a957eed7b7ad7f09750 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Wed, 26 Aug 2026 12:27:07 -0600 Subject: [PATCH 05/11] fix(system): Initialize WorldBuilder -cwd skip flag in a constructor VC6 does not support in-class member initializers, which broke the WorldBuilder command-line parser on CI. Co-authored-by: Cursor --- Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp | 4 +++- GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 504bf80b0b8..09af32d3f64 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -158,6 +158,8 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) class WBCommandLineInfo : public CCommandLineInfo { public: + WBCommandLineInfo() : m_expectingCwdPath(FALSE) {} + virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { if (m_expectingCwdPath) @@ -177,7 +179,7 @@ class WBCommandLineInfo : public CCommandLineInfo } private: - BOOL m_expectingCwdPath = FALSE; + BOOL m_expectingCwdPath; }; ///////////////////////////////////////////////////////////////////////////// diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 647a6d469ed..3619a351ae3 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -158,6 +158,8 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) class WBCommandLineInfo : public CCommandLineInfo { public: + WBCommandLineInfo() : m_expectingCwdPath(FALSE) {} + virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { if (m_expectingCwdPath) @@ -177,7 +179,7 @@ class WBCommandLineInfo : public CCommandLineInfo } private: - BOOL m_expectingCwdPath = FALSE; + BOOL m_expectingCwdPath; }; ///////////////////////////////////////////////////////////////////////////// From cadc529360b54d78b3ce02f5d4385dea94d54006 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Sat, 29 Aug 2026 16:13:46 -0600 Subject: [PATCH 06/11] fix(system): Make cwd overrides explicit --- Core/GameEngine/Include/Common/CommandLine.h | 3 ++ .../Include/Common/WorkingDirectory.h | 2 +- Core/GameEngine/Source/Common/CommandLine.cpp | 38 ++++++++++++++----- Core/Tools/MapCacheBuilder/Source/WinMain.cpp | 8 +--- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 19 +--------- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 19 +--------- 6 files changed, 37 insertions(+), 52 deletions(-) diff --git a/Core/GameEngine/Include/Common/CommandLine.h b/Core/GameEngine/Include/Common/CommandLine.h index bb65d3cd80b..84d7c3a9c6f 100644 --- a/Core/GameEngine/Include/Common/CommandLine.h +++ b/Core/GameEngine/Include/Common/CommandLine.h @@ -35,4 +35,7 @@ class CommandLine // Parses startup flags and applies the process working directory (-cwd). static void parseCommandLineForStartup(); static void parseCommandLineForEngineInit(); + + // Returns true for the single-token -cwd and -cwd= startup options. + static bool isStartupWorkingDirectoryOption(const char *arg); }; diff --git a/Core/GameEngine/Include/Common/WorkingDirectory.h b/Core/GameEngine/Include/Common/WorkingDirectory.h index d9391ca3808..0787cf2e558 100644 --- a/Core/GameEngine/Include/Common/WorkingDirectory.h +++ b/Core/GameEngine/Include/Common/WorkingDirectory.h @@ -26,7 +26,7 @@ namespace rts // TheSuperHackers @feature 14/08/2026 // Process working directory helpers. CommandLine::parseCommandLineForStartup() // applies these: default is the executable directory, -cwd keeps the OS -// directory, and -cwd uses that path. +// directory, and -cwd= uses that path. Bool setCurrentDirectoryToExecutablePath(); Bool setCurrentDirectoryToPath(const char *path); diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index ca3e0be8eb8..6400adc71da 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -467,18 +467,23 @@ Int parseJobs(char *args[], int num) // Set when -cwd is present so parseCommandLineForStartup does not force the executable directory. static Bool s_cwdOptionSpecified = FALSE; -Int parseCwd(char *args[], int num) +Int parseCwd(char *[], int) { // TheSuperHackers @feature 14/08/2026 - // -cwd keeps the OS working directory. -cwd uses that directory instead. + // -cwd keeps the OS working directory. s_cwdOptionSpecified = TRUE; + return 1; +} - if (num > 1 && args[1] != nullptr && args[1][0] != '-' && args[1][0] != '\0') - { - if (!rts::setCurrentDirectoryToPath(args[1])) - rts::setCurrentDirectoryToExecutablePath(); - return 2; - } +Int parseCwdOverride(char *args[], int) +{ + // TheSuperHackers @bugfix CryoTheRenegade 29/08/2026 + // -cwd= overrides the working directory. Keeping the path in the same + // token prevents -cwd from consuming a positional file argument. + s_cwdOptionSpecified = TRUE; + const char *path = args[0] + strlen("-cwd="); + if (!rts::setCurrentDirectoryToPath(path)) + rts::setCurrentDirectoryToExecutablePath(); return 1; } @@ -1162,8 +1167,9 @@ static CommandLineParam paramsForStartup[] = { "-jobs", parseJobs }, // TheSuperHackers @feature 14/08/2026 - // Use the current working directory as provided by the OS, or an optional path. + // Use the current working directory as provided by the OS, or an explicit path. // Without this flag the working directory is forced to the executable directory. + { "-cwd=", parseCwdOverride }, { "-cwd", parseCwd }, }; @@ -1427,7 +1433,8 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) { int len = strlen(params[param].name); int len2 = strlen(argv[arg]); - if (len2 != len) + const Bool matchPrefix = params[param].name[len - 1] == '='; + if ((!matchPrefix && len2 != len) || (matchPrefix && len2 < len)) continue; if (strnicmp(argv[arg], params[param].name, len) == 0) { @@ -1443,6 +1450,17 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) } } +bool CommandLine::isStartupWorkingDirectoryOption(const char *arg) +{ + if (arg == nullptr) + return false; + + if (arg[0] == '-' || arg[0] == '/') + ++arg; + + return stricmp(arg, "cwd") == 0 || strnicmp(arg, "cwd=", 4) == 0; +} + void createGlobalData() { if (TheGlobalData == nullptr) diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp index 862831f266c..cefc78bb332 100644 --- a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp +++ b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp @@ -231,15 +231,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, token = nextParam(lpCmdLine, "\" "); while (token != nullptr) { char * str = strtrim(token); - if (stricmp(str, "-cwd") == 0) + if (CommandLine::isStartupWorkingDirectoryOption(str)) { token = nextParam(nullptr, "\" "); - if (token != nullptr) - { - char *cwdArg = strtrim(token); - if (cwdArg != nullptr && cwdArg[0] != '-' && cwdArg[0] != '\0') - token = nextParam(nullptr, "\" "); - } continue; } argvSet.push_back(str); diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 09af32d3f64..0ea83a4e02c 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -153,33 +153,18 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) } ///////////////////////////////////////////////////////////////////////////// -// Skip -cwd [path] so MFC does not treat the path as a map to open. +// Skip the single-token -cwd and -cwd= options during MFC shell processing. class WBCommandLineInfo : public CCommandLineInfo { public: - WBCommandLineInfo() : m_expectingCwdPath(FALSE) {} - virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { - if (m_expectingCwdPath) - { - m_expectingCwdPath = FALSE; - if (!bFlag && pszParam != nullptr && pszParam[0] != 0) - return; - } - - if (bFlag && lstrcmpi(pszParam, _T("cwd")) == 0) - { - m_expectingCwdPath = TRUE; + if (bFlag && CommandLine::isStartupWorkingDirectoryOption(pszParam)) return; - } CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); } - -private: - BOOL m_expectingCwdPath; }; ///////////////////////////////////////////////////////////////////////////// diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 3619a351ae3..395f8bfd07e 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -153,33 +153,18 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) } ///////////////////////////////////////////////////////////////////////////// -// Skip -cwd [path] so MFC does not treat the path as a map to open. +// Skip the single-token -cwd and -cwd= options during MFC shell processing. class WBCommandLineInfo : public CCommandLineInfo { public: - WBCommandLineInfo() : m_expectingCwdPath(FALSE) {} - virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { - if (m_expectingCwdPath) - { - m_expectingCwdPath = FALSE; - if (!bFlag && pszParam != nullptr && pszParam[0] != 0) - return; - } - - if (bFlag && lstrcmpi(pszParam, _T("cwd")) == 0) - { - m_expectingCwdPath = TRUE; + if (bFlag && CommandLine::isStartupWorkingDirectoryOption(pszParam)) return; - } CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); } - -private: - BOOL m_expectingCwdPath; }; ///////////////////////////////////////////////////////////////////////////// From 13925e7d62b1b2220a172090fdb02b9e66bcc409 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Wed, 2 Sep 2026 10:23:03 -0600 Subject: [PATCH 07/11] refactor(system): Split working directory options --- Core/GameEngine/Include/Common/CommandLine.h | 6 +-- .../Include/Common/WorkingDirectory.h | 4 +- Core/GameEngine/Source/Common/CommandLine.cpp | 42 +++++++++++-------- Core/Tools/MapCacheBuilder/Source/WinMain.cpp | 6 ++- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 21 +++++++++- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 21 +++++++++- 6 files changed, 72 insertions(+), 28 deletions(-) diff --git a/Core/GameEngine/Include/Common/CommandLine.h b/Core/GameEngine/Include/Common/CommandLine.h index 84d7c3a9c6f..649144de807 100644 --- a/Core/GameEngine/Include/Common/CommandLine.h +++ b/Core/GameEngine/Include/Common/CommandLine.h @@ -32,10 +32,10 @@ class CommandLine { public: - // Parses startup flags and applies the process working directory (-cwd). + // Parses startup flags and applies the process working directory. static void parseCommandLineForStartup(); static void parseCommandLineForEngineInit(); - // Returns true for the single-token -cwd and -cwd= startup options. - static bool isStartupWorkingDirectoryOption(const char *arg); + // Returns the number of tokens consumed by -useCwd or -setCwd , or 0. + static int getStartupWorkingDirectoryOptionTokenCount(const char *arg); }; diff --git a/Core/GameEngine/Include/Common/WorkingDirectory.h b/Core/GameEngine/Include/Common/WorkingDirectory.h index 0787cf2e558..2cb66dc4b29 100644 --- a/Core/GameEngine/Include/Common/WorkingDirectory.h +++ b/Core/GameEngine/Include/Common/WorkingDirectory.h @@ -25,8 +25,8 @@ namespace rts // TheSuperHackers @feature 14/08/2026 // Process working directory helpers. CommandLine::parseCommandLineForStartup() -// applies these: default is the executable directory, -cwd keeps the OS -// directory, and -cwd= uses that path. +// applies these: default is the executable directory, -useCwd keeps the OS +// directory, and -setCwd uses that path. Bool setCurrentDirectoryToExecutablePath(); Bool setCurrentDirectoryToPath(const char *path); diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 6400adc71da..399ca2c0b10 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -464,27 +464,32 @@ Int parseJobs(char *args[], int num) return 1; } -// Set when -cwd is present so parseCommandLineForStartup does not force the executable directory. +// Set when a working-directory option is present so parseCommandLineForStartup +// does not force the executable directory after parsing. static Bool s_cwdOptionSpecified = FALSE; -Int parseCwd(char *[], int) +Int parseUseCwd(char *[], int) { // TheSuperHackers @feature 14/08/2026 - // -cwd keeps the OS working directory. + // -useCwd keeps the OS working directory. s_cwdOptionSpecified = TRUE; return 1; } -Int parseCwdOverride(char *args[], int) +Int parseSetCwd(char *args[], int num) { // TheSuperHackers @bugfix CryoTheRenegade 29/08/2026 - // -cwd= overrides the working directory. Keeping the path in the same - // token prevents -cwd from consuming a positional file argument. + // -setCwd overrides the working directory. The separate -useCwd + // option keeps the no-argument behavior unambiguous. s_cwdOptionSpecified = TRUE; - const char *path = args[0] + strlen("-cwd="); - if (!rts::setCurrentDirectoryToPath(path)) + if (num <= 1 || args[1] == nullptr) + { rts::setCurrentDirectoryToExecutablePath(); - return 1; + return 1; + } + if (!rts::setCurrentDirectoryToPath(args[1])) + rts::setCurrentDirectoryToExecutablePath(); + return 2; } Int parseXRes(char *args[], int num) @@ -1168,9 +1173,9 @@ static CommandLineParam paramsForStartup[] = // TheSuperHackers @feature 14/08/2026 // Use the current working directory as provided by the OS, or an explicit path. - // Without this flag the working directory is forced to the executable directory. - { "-cwd=", parseCwdOverride }, - { "-cwd", parseCwd }, + // 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 @@ -1433,8 +1438,7 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) { int len = strlen(params[param].name); int len2 = strlen(argv[arg]); - const Bool matchPrefix = params[param].name[len - 1] == '='; - if ((!matchPrefix && len2 != len) || (matchPrefix && len2 < len)) + if (len2 != len) continue; if (strnicmp(argv[arg], params[param].name, len) == 0) { @@ -1450,15 +1454,19 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) } } -bool CommandLine::isStartupWorkingDirectoryOption(const char *arg) +int CommandLine::getStartupWorkingDirectoryOptionTokenCount(const char *arg) { if (arg == nullptr) - return false; + return 0; if (arg[0] == '-' || arg[0] == '/') ++arg; - return stricmp(arg, "cwd") == 0 || strnicmp(arg, "cwd=", 4) == 0; + if (stricmp(arg, "useCwd") == 0) + return 1; + if (stricmp(arg, "setCwd") == 0) + return 2; + return 0; } void createGlobalData() diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp index cefc78bb332..6bc0e424348 100644 --- a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp +++ b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp @@ -231,9 +231,11 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, token = nextParam(lpCmdLine, "\" "); while (token != nullptr) { char * str = strtrim(token); - if (CommandLine::isStartupWorkingDirectoryOption(str)) + const int cwdTokenCount = CommandLine::getStartupWorkingDirectoryOptionTokenCount(str); + if (cwdTokenCount > 0) { - token = nextParam(nullptr, "\" "); + for (int i = 0; i < cwdTokenCount; ++i) + token = nextParam(nullptr, "\" "); continue; } argvSet.push_back(str); diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 0ea83a4e02c..a152f3ef5cf 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -153,18 +153,34 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) } ///////////////////////////////////////////////////////////////////////////// -// Skip the single-token -cwd and -cwd= options during MFC shell processing. +// MFC parses the command line again to select a document to open. Remove the +// startup-only options so it does not mistake the -setCwd path for a map. class WBCommandLineInfo : public CCommandLineInfo { public: + WBCommandLineInfo() : m_skipCwdPath(FALSE) {} + virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { - if (bFlag && CommandLine::isStartupWorkingDirectoryOption(pszParam)) + if (m_skipCwdPath) + { + m_skipCwdPath = FALSE; + return; + } + + const int cwdTokenCount = bFlag ? CommandLine::getStartupWorkingDirectoryOptionTokenCount(pszParam) : 0; + if (cwdTokenCount > 0) + { + m_skipCwdPath = cwdTokenCount > 1; return; + } CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); } + +private: + BOOL m_skipCwdPath; }; ///////////////////////////////////////////////////////////////////////////// @@ -331,6 +347,7 @@ BOOL CWorldBuilderApp::InitInstance() INI ini; + DEBUG_ASSERTCRASH(TheWritableGlobalData, ("TheWritableGlobalData expected to be created")); initSubsystem(TheWritableGlobalData, TheWritableGlobalData, "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); TheFramePacer = new FramePacer(); diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 395f8bfd07e..c0595674cc9 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -153,18 +153,34 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) } ///////////////////////////////////////////////////////////////////////////// -// Skip the single-token -cwd and -cwd= options during MFC shell processing. +// MFC parses the command line again to select a document to open. Remove the +// startup-only options so it does not mistake the -setCwd path for a map. class WBCommandLineInfo : public CCommandLineInfo { public: + WBCommandLineInfo() : m_skipCwdPath(FALSE) {} + virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { - if (bFlag && CommandLine::isStartupWorkingDirectoryOption(pszParam)) + if (m_skipCwdPath) + { + m_skipCwdPath = FALSE; + return; + } + + const int cwdTokenCount = bFlag ? CommandLine::getStartupWorkingDirectoryOptionTokenCount(pszParam) : 0; + if (cwdTokenCount > 0) + { + m_skipCwdPath = cwdTokenCount > 1; return; + } CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); } + +private: + BOOL m_skipCwdPath; }; ///////////////////////////////////////////////////////////////////////////// @@ -341,6 +357,7 @@ BOOL CWorldBuilderApp::InitInstance() INI ini; + DEBUG_ASSERTCRASH(TheWritableGlobalData, ("TheWritableGlobalData expected to be created")); initSubsystem(TheWritableGlobalData, TheWritableGlobalData, "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); TheFramePacer = new FramePacer(); From 5ff2f5c5fd216581716014875881e0af4201d703 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Wed, 2 Sep 2026 12:04:13 -0600 Subject: [PATCH 08/11] refactor(system): Record parsed startup arguments --- Core/GameEngine/Include/Common/CommandLine.h | 5 +-- Core/GameEngine/Source/Common/CommandLine.cpp | 36 ++++++++++--------- Core/Tools/MapCacheBuilder/Source/WinMain.cpp | 12 +++---- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 21 ++++------- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 21 ++++------- 5 files changed, 39 insertions(+), 56 deletions(-) diff --git a/Core/GameEngine/Include/Common/CommandLine.h b/Core/GameEngine/Include/Common/CommandLine.h index 649144de807..9bfecdd0f28 100644 --- a/Core/GameEngine/Include/Common/CommandLine.h +++ b/Core/GameEngine/Include/Common/CommandLine.h @@ -36,6 +36,7 @@ class CommandLine static void parseCommandLineForStartup(); static void parseCommandLineForEngineInit(); - // Returns the number of tokens consumed by -useCwd or -setCwd , or 0. - static int getStartupWorkingDirectoryOptionTokenCount(const char *arg); + // Returns true if startup parsing consumed the zero-based argument index. + // The index excludes the executable name. + static bool isCommandLineArgumentParsedForStartup(int argIndex); }; diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 399ca2c0b10..7fc0de95996 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -479,8 +479,7 @@ Int parseUseCwd(char *[], int) Int parseSetCwd(char *args[], int num) { // TheSuperHackers @bugfix CryoTheRenegade 29/08/2026 - // -setCwd overrides the working directory. The separate -useCwd - // option keeps the no-argument behavior unambiguous. + // -setCwd overrides the working directory. s_cwdOptionSpecified = TRUE; if (num <= 1 || args[1] == nullptr) { @@ -1397,7 +1396,10 @@ char *nextParam(char *newSource, const char *seps) return first; } -static void parseCommandLine(const CommandLineParam* params, int numParams) +static std::vector s_startupParsedArguments; + +static void parseCommandLine( + const CommandLineParam* params, int numParams, std::vector *parsedArguments = nullptr) { std::vector argv; @@ -1409,6 +1411,8 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) token = nextParam(nullptr, "\" "); } int argc = argv.size(); + if (parsedArguments != nullptr) + parsedArguments->assign(argc > 0 ? argc - 1 : 0, FALSE); int arg = 1; @@ -1442,7 +1446,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; } @@ -1454,19 +1465,10 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) } } -int CommandLine::getStartupWorkingDirectoryOptionTokenCount(const char *arg) +bool CommandLine::isCommandLineArgumentParsedForStartup(int argIndex) { - if (arg == nullptr) - return 0; - - if (arg[0] == '-' || arg[0] == '/') - ++arg; - - if (stricmp(arg, "useCwd") == 0) - return 1; - if (stricmp(arg, "setCwd") == 0) - return 2; - return 0; + return argIndex >= 0 && argIndex < static_cast(s_startupParsedArguments.size()) + && s_startupParsedArguments[argIndex]; } void createGlobalData() @@ -1485,7 +1487,7 @@ void CommandLine::parseCommandLineForStartup() return; TheWritableGlobalData->m_commandLineData.m_hasParsedCommandLineForStartup = true; - parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup)); + parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup), &s_startupParsedArguments); if (!s_cwdOptionSpecified) rts::setCurrentDirectoryToExecutablePath(); diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp index 6bc0e424348..4c813c52dda 100644 --- a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp +++ b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp @@ -228,19 +228,17 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, */ std::list argvSet; char *token; + int argIndex = 0; token = nextParam(lpCmdLine, "\" "); while (token != nullptr) { char * str = strtrim(token); - const int cwdTokenCount = CommandLine::getStartupWorkingDirectoryOptionTokenCount(str); - if (cwdTokenCount > 0) + if (!CommandLine::isCommandLineArgumentParsedForStartup(argIndex)) { - for (int i = 0; i < cwdTokenCount; ++i) - token = nextParam(nullptr, "\" "); - continue; + argvSet.push_back(str); + DEBUG_LOG(("Adding '%s'", str)); } - argvSet.push_back(str); - DEBUG_LOG(("Adding '%s'", str)); token = nextParam(nullptr, "\" "); + ++argIndex; } // not part of the subsystem list, because it should normally never be reset! diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index a152f3ef5cf..47405f42280 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -153,34 +153,25 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) } ///////////////////////////////////////////////////////////////////////////// -// MFC parses the command line again to select a document to open. Remove the -// startup-only options so it does not mistake the -setCwd path for a map. +// MFC parses the command line again to select a document to open. Skip the +// arguments already handled by the startup parser so option values are not +// mistaken for map filenames. class WBCommandLineInfo : public CCommandLineInfo { public: - WBCommandLineInfo() : m_skipCwdPath(FALSE) {} + WBCommandLineInfo() : m_argIndex(0) {} virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { - if (m_skipCwdPath) - { - m_skipCwdPath = FALSE; + if (CommandLine::isCommandLineArgumentParsedForStartup(m_argIndex++)) return; - } - - const int cwdTokenCount = bFlag ? CommandLine::getStartupWorkingDirectoryOptionTokenCount(pszParam) : 0; - if (cwdTokenCount > 0) - { - m_skipCwdPath = cwdTokenCount > 1; - return; - } CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); } private: - BOOL m_skipCwdPath; + int m_argIndex; }; ///////////////////////////////////////////////////////////////////////////// diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index c0595674cc9..5d0586c3f20 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -153,34 +153,25 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) } ///////////////////////////////////////////////////////////////////////////// -// MFC parses the command line again to select a document to open. Remove the -// startup-only options so it does not mistake the -setCwd path for a map. +// MFC parses the command line again to select a document to open. Skip the +// arguments already handled by the startup parser so option values are not +// mistaken for map filenames. class WBCommandLineInfo : public CCommandLineInfo { public: - WBCommandLineInfo() : m_skipCwdPath(FALSE) {} + WBCommandLineInfo() : m_argIndex(0) {} virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { - if (m_skipCwdPath) - { - m_skipCwdPath = FALSE; + if (CommandLine::isCommandLineArgumentParsedForStartup(m_argIndex++)) return; - } - - const int cwdTokenCount = bFlag ? CommandLine::getStartupWorkingDirectoryOptionTokenCount(pszParam) : 0; - if (cwdTokenCount > 0) - { - m_skipCwdPath = cwdTokenCount > 1; - return; - } CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); } private: - BOOL m_skipCwdPath; + int m_argIndex; }; ///////////////////////////////////////////////////////////////////////////// From 377032fb708379047b64625cf7dac5298f0f422a Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Wed, 2 Sep 2026 17:28:44 -0600 Subject: [PATCH 09/11] fix(system): Preserve startup argument parsing state --- Core/GameEngine/Source/Common/CommandLine.cpp | 12 +++++++----- Generals/Code/GameEngine/Include/Common/GlobalData.h | 1 + .../Code/Tools/WorldBuilder/src/WorldBuilder.cpp | 3 +++ .../Code/GameEngine/Include/Common/GlobalData.h | 1 + .../Code/Tools/WorldBuilder/src/WorldBuilder.cpp | 3 +++ 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 7fc0de95996..0c878444d0b 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -1396,8 +1396,6 @@ char *nextParam(char *newSource, const char *seps) return first; } -static std::vector s_startupParsedArguments; - static void parseCommandLine( const CommandLineParam* params, int numParams, std::vector *parsedArguments = nullptr) { @@ -1467,8 +1465,11 @@ static void parseCommandLine( bool CommandLine::isCommandLineArgumentParsedForStartup(int argIndex) { - return argIndex >= 0 && argIndex < static_cast(s_startupParsedArguments.size()) - && s_startupParsedArguments[argIndex]; + if (TheGlobalData == nullptr) + return false; + + const BoolVector &parsedArguments = TheGlobalData->m_commandLineData.m_startupParsedArguments; + return argIndex >= 0 && argIndex < static_cast(parsedArguments.size()) && parsedArguments[argIndex]; } void createGlobalData() @@ -1487,7 +1488,8 @@ void CommandLine::parseCommandLineForStartup() return; TheWritableGlobalData->m_commandLineData.m_hasParsedCommandLineForStartup = true; - parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup), &s_startupParsedArguments); + parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup), + &TheWritableGlobalData->m_commandLineData.m_startupParsedArguments); if (!s_cwdOptionSpecified) rts::setCurrentDirectoryToExecutablePath(); diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index e631654250d..de13e714cdd 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -66,6 +66,7 @@ class CommandLineData Bool m_hasParsedCommandLineForStartup; Bool m_hasParsedCommandLineForEngineInit; + BoolVector m_startupParsedArguments; }; //------------------------------------------------------------------------------------------------- diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 47405f42280..24dd52434a1 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -165,7 +165,10 @@ class WBCommandLineInfo : public CCommandLineInfo virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { if (CommandLine::isCommandLineArgumentParsedForStartup(m_argIndex++)) + { + ParseLast(bLast); return; + } CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 7f484111672..1c978ace2c4 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -66,6 +66,7 @@ class CommandLineData Bool m_hasParsedCommandLineForStartup; Bool m_hasParsedCommandLineForEngineInit; + BoolVector m_startupParsedArguments; }; //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 5d0586c3f20..4ede695e400 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -165,7 +165,10 @@ class WBCommandLineInfo : public CCommandLineInfo virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { if (CommandLine::isCommandLineArgumentParsedForStartup(m_argIndex++)) + { + ParseLast(bLast); return; + } CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); } From 1602ff042d35a410e3afc459b522aeaf12e2a320 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Wed, 2 Sep 2026 18:46:34 -0600 Subject: [PATCH 10/11] fix(system): Use CRT command-line arguments --- Core/GameEngine/Source/Common/CommandLine.cpp | 68 +--------------- Core/Tools/MapCacheBuilder/Source/WinMain.cpp | 79 ++----------------- 2 files changed, 9 insertions(+), 138 deletions(-) diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 0c878444d0b..7e5cc72e91d 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -36,7 +36,6 @@ #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" -#include "WWLib/trim.h" @@ -481,7 +480,7 @@ Int parseSetCwd(char *args[], int num) // TheSuperHackers @bugfix CryoTheRenegade 29/08/2026 // -setCwd overrides the working directory. s_cwdOptionSpecified = TRUE; - if (num <= 1 || args[1] == nullptr) + if (num <= 1 || args[1] == nullptr || args[1][0] == '-' || args[1][0] == '/') { rts::setCurrentDirectoryToExecutablePath(); return 1; @@ -1343,72 +1342,11 @@ 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, std::vector *parsedArguments = nullptr) { - std::vector 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->assign(argc > 0 ? argc - 1 : 0, FALSE); diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp index 4c813c52dda..26765860ee3 100644 --- a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp +++ b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp @@ -103,7 +103,6 @@ #include "Win32Device/GameClient/Win32Mouse.h" #include "Win32Device/Common/Win32LocalFileSystem.h" #include "Win32Device/Common/Win32BIGFileSystem.h" -#include "WWLib/trim.h" // DEFINES //////////////////////////////////////////////////////////////////// @@ -142,65 +141,6 @@ const Char *g_csfFile = "data\\%s\\Generals.csf"; // PRIVATE FUNCTIONS ////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// -static 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 initial spaces - char *firstNonSpace = first; - while (*firstNonSpace == ' ') - ++firstNonSpace; - first = firstNonSpace; - - // 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; -} - /////////////////////////////////////////////////////////////////////////////// // PUBLIC FUNCTIONS /////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// @@ -223,22 +163,15 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, CommandLine::parseCommandLineForStartup(); - /* - ** Convert WinMain arguments to simple main argc and argv - */ + // Collect CRT arguments not handled during startup parsing. std::list argvSet; - char *token; - int argIndex = 0; - token = nextParam(lpCmdLine, "\" "); - while (token != nullptr) { - char * str = strtrim(token); - if (!CommandLine::isCommandLineArgumentParsedForStartup(argIndex)) + for (int arg = 1; arg < __argc; ++arg) + { + if (!CommandLine::isCommandLineArgumentParsedForStartup(arg - 1)) { - argvSet.push_back(str); - DEBUG_LOG(("Adding '%s'", str)); + argvSet.push_back(__argv[arg]); + DEBUG_LOG(("Adding '%s'", __argv[arg])); } - token = nextParam(nullptr, "\" "); - ++argIndex; } // not part of the subsystem list, because it should normally never be reset! From 56d5044d775ef287ee83dd46428526cd7dcb5bb6 Mon Sep 17 00:00:00 2001 From: Jacob Ledbetter Date: Thu, 3 Sep 2026 19:50:17 -0600 Subject: [PATCH 11/11] refactor(system): Clean up command-line state --- Core/GameEngine/Include/Common/CommandLine.h | 4 +-- .../Include/Common/WorkingDirectory.h | 2 ++ Core/GameEngine/Source/Common/CommandLine.cpp | 26 +++++++------------ .../Source/Common/WorkingDirectory.cpp | 15 +++++++++++ Core/Tools/MapCacheBuilder/Source/WinMain.cpp | 2 +- .../GameEngine/Include/Common/GlobalData.h | 4 ++- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 3 ++- .../GameEngine/Include/Common/GlobalData.h | 4 ++- .../Tools/WorldBuilder/src/WorldBuilder.cpp | 3 ++- 9 files changed, 40 insertions(+), 23 deletions(-) diff --git a/Core/GameEngine/Include/Common/CommandLine.h b/Core/GameEngine/Include/Common/CommandLine.h index 9bfecdd0f28..e941c0130ba 100644 --- a/Core/GameEngine/Include/Common/CommandLine.h +++ b/Core/GameEngine/Include/Common/CommandLine.h @@ -36,7 +36,7 @@ class CommandLine static void parseCommandLineForStartup(); static void parseCommandLineForEngineInit(); - // Returns true if startup parsing consumed the zero-based argument index. + // Returns true if command-line parsing consumed the zero-based argument index. // The index excludes the executable name. - static bool isCommandLineArgumentParsedForStartup(int argIndex); + static bool wasCommandLineArgumentParsed(int argIndex); }; diff --git a/Core/GameEngine/Include/Common/WorkingDirectory.h b/Core/GameEngine/Include/Common/WorkingDirectory.h index 2cb66dc4b29..0df55f0d84a 100644 --- a/Core/GameEngine/Include/Common/WorkingDirectory.h +++ b/Core/GameEngine/Include/Common/WorkingDirectory.h @@ -30,5 +30,7 @@ namespace rts Bool setCurrentDirectoryToExecutablePath(); Bool setCurrentDirectoryToPath(const char *path); +void keepCurrentDirectory(); +void setCurrentDirectoryToExecutablePathIfNotSet(); } // namespace rts diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 7e5cc72e91d..3b0d9946fb6 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -463,15 +463,11 @@ Int parseJobs(char *args[], int num) return 1; } -// Set when a working-directory option is present so parseCommandLineForStartup -// does not force the executable directory after parsing. -static Bool s_cwdOptionSpecified = FALSE; - Int parseUseCwd(char *[], int) { // TheSuperHackers @feature 14/08/2026 // -useCwd keeps the OS working directory. - s_cwdOptionSpecified = TRUE; + rts::keepCurrentDirectory(); return 1; } @@ -479,7 +475,6 @@ Int parseSetCwd(char *args[], int num) { // TheSuperHackers @bugfix CryoTheRenegade 29/08/2026 // -setCwd overrides the working directory. - s_cwdOptionSpecified = TRUE; if (num <= 1 || args[1] == nullptr || args[1][0] == '-' || args[1][0] == '/') { rts::setCurrentDirectoryToExecutablePath(); @@ -1342,13 +1337,12 @@ static CommandLineParam paramsForEngineInit[] = }; -static void parseCommandLine( - const CommandLineParam* params, int numParams, std::vector *parsedArguments = nullptr) +static void parseCommandLine(const CommandLineParam* params, int numParams, std::vector *parsedArguments = nullptr) { const int argc = __argc; char **argv = __argv; - if (parsedArguments != nullptr) - parsedArguments->assign(argc > 0 ? argc - 1 : 0, FALSE); + if (parsedArguments != nullptr && parsedArguments->size() < static_cast(argc > 0 ? argc - 1 : 0)) + parsedArguments->resize(argc - 1, FALSE); int arg = 1; @@ -1401,12 +1395,12 @@ static void parseCommandLine( } } -bool CommandLine::isCommandLineArgumentParsedForStartup(int argIndex) +bool CommandLine::wasCommandLineArgumentParsed(int argIndex) { if (TheGlobalData == nullptr) return false; - const BoolVector &parsedArguments = TheGlobalData->m_commandLineData.m_startupParsedArguments; + const BoolVector &parsedArguments = TheGlobalData->m_commandLineData.m_parsedArguments; return argIndex >= 0 && argIndex < static_cast(parsedArguments.size()) && parsedArguments[argIndex]; } @@ -1427,10 +1421,9 @@ void CommandLine::parseCommandLineForStartup() TheWritableGlobalData->m_commandLineData.m_hasParsedCommandLineForStartup = true; parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup), - &TheWritableGlobalData->m_commandLineData.m_startupParsedArguments); + &TheWritableGlobalData->m_commandLineData.m_parsedArguments); - if (!s_cwdOptionSpecified) - rts::setCurrentDirectoryToExecutablePath(); + rts::setCurrentDirectoryToExecutablePathIfNotSet(); } void CommandLine::parseCommandLineForEngineInit() @@ -1443,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); } diff --git a/Core/GameEngine/Source/Common/WorkingDirectory.cpp b/Core/GameEngine/Source/Common/WorkingDirectory.cpp index 95490cd2657..703b4d02086 100644 --- a/Core/GameEngine/Source/Common/WorkingDirectory.cpp +++ b/Core/GameEngine/Source/Common/WorkingDirectory.cpp @@ -23,6 +23,8 @@ namespace rts { +static Bool s_workingDirectorySet = FALSE; + Bool setCurrentDirectoryToExecutablePath() { Char buffer[_MAX_PATH]; @@ -44,6 +46,7 @@ Bool setCurrentDirectoryToExecutablePath() return FALSE; } + s_workingDirectorySet = TRUE; return TRUE; } @@ -58,7 +61,19 @@ Bool setCurrentDirectoryToPath(const char *path) return FALSE; } + s_workingDirectorySet = TRUE; return TRUE; } +void keepCurrentDirectory() +{ + s_workingDirectorySet = TRUE; +} + +void setCurrentDirectoryToExecutablePathIfNotSet() +{ + if (!s_workingDirectorySet) + setCurrentDirectoryToExecutablePath(); +} + } // namespace rts diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp index 26765860ee3..ac5aeb2e9b0 100644 --- a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp +++ b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp @@ -167,7 +167,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, std::list argvSet; for (int arg = 1; arg < __argc; ++arg) { - if (!CommandLine::isCommandLineArgumentParsedForStartup(arg - 1)) + if (!CommandLine::wasCommandLineArgumentParsed(arg - 1)) { argvSet.push_back(__argv[arg]); DEBUG_LOG(("Adding '%s'", __argv[arg])); diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index de13e714cdd..f20a9c2f35f 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -54,6 +54,8 @@ constexpr const Int MAX_GLOBAL_LIGHTS = 3; constexpr const Int SIMULATE_REPLAYS_SEQUENTIAL = -1; //------------------------------------------------------------------------------------------------- +// Command-line parsing state is stored here instead of in CommandLine because +// the parsing result belongs to the GlobalData instance created during startup. class CommandLineData { friend class CommandLine; @@ -66,7 +68,7 @@ class CommandLineData Bool m_hasParsedCommandLineForStartup; Bool m_hasParsedCommandLineForEngineInit; - BoolVector m_startupParsedArguments; + BoolVector m_parsedArguments; }; //------------------------------------------------------------------------------------------------- diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 24dd52434a1..066c697c274 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -164,8 +164,9 @@ class WBCommandLineInfo : public CCommandLineInfo virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { - if (CommandLine::isCommandLineArgumentParsedForStartup(m_argIndex++)) + if (CommandLine::wasCommandLineArgumentParsed(m_argIndex++)) { + // MFC uses bLast to finalize its shell command, even when the final argument is skipped. ParseLast(bLast); return; } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 1c978ace2c4..fbf8f97f1ae 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -54,6 +54,8 @@ constexpr const Int MAX_GLOBAL_LIGHTS = 3; constexpr const Int SIMULATE_REPLAYS_SEQUENTIAL = -1; //------------------------------------------------------------------------------------------------- +// Command-line parsing state is stored here instead of in CommandLine because +// the parsing result belongs to the GlobalData instance created during startup. class CommandLineData { friend class CommandLine; @@ -66,7 +68,7 @@ class CommandLineData Bool m_hasParsedCommandLineForStartup; Bool m_hasParsedCommandLineForEngineInit; - BoolVector m_startupParsedArguments; + BoolVector m_parsedArguments; }; //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 4ede695e400..189111925d4 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -164,8 +164,9 @@ class WBCommandLineInfo : public CCommandLineInfo virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override { - if (CommandLine::isCommandLineArgumentParsedForStartup(m_argIndex++)) + if (CommandLine::wasCommandLineArgumentParsed(m_argIndex++)) { + // MFC uses bLast to finalize its shell command, even when the final argument is skipped. ParseLast(bLast); return; }