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 48e078dc3dd..e941c0130ba 100644 --- a/Core/GameEngine/Include/Common/CommandLine.h +++ b/Core/GameEngine/Include/Common/CommandLine.h @@ -32,6 +32,11 @@ class CommandLine { public: + // Parses startup flags and applies the process working directory. static void parseCommandLineForStartup(); static void parseCommandLineForEngineInit(); + + // Returns true if command-line parsing consumed the zero-based argument index. + // The index excludes the executable name. + static bool wasCommandLineArgumentParsed(int argIndex); }; diff --git a/Core/GameEngine/Include/Common/WorkingDirectory.h b/Core/GameEngine/Include/Common/WorkingDirectory.h new file mode 100644 index 00000000000..0df55f0d84a --- /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 +// Process working directory helpers. CommandLine::parseCommandLineForStartup() +// applies these: default is the executable directory, -useCwd keeps the OS +// directory, and -setCwd uses that path. + +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 772830f0f67..3b0d9946fb6 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" @@ -35,7 +36,6 @@ #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" -#include "WWLib/trim.h" @@ -463,6 +463,28 @@ Int parseJobs(char *args[], int num) return 1; } +Int parseUseCwd(char *[], int) +{ + // TheSuperHackers @feature 14/08/2026 + // -useCwd keeps the OS working directory. + rts::keepCurrentDirectory(); + return 1; +} + +Int parseSetCwd(char *args[], int num) +{ + // TheSuperHackers @bugfix CryoTheRenegade 29/08/2026 + // -setCwd overrides the working directory. + if (num <= 1 || args[1] == nullptr || args[1][0] == '-' || args[1][0] == '/') + { + rts::setCurrentDirectoryToExecutablePath(); + return 1; + } + if (!rts::setCurrentDirectoryToPath(args[1])) + rts::setCurrentDirectoryToExecutablePath(); + return 2; +} + Int parseXRes(char *args[], int num) { if (num > 1) @@ -1141,6 +1163,12 @@ static CommandLineParam paramsForStartup[] = // (If you have 4 cores, call it with -jobs 4) // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, + + // TheSuperHackers @feature 14/08/2026 + // Use the current working directory as provided by the OS, or an explicit path. + // Without either flag the working directory is forced to the executable directory. + { "-setCwd", parseSetCwd }, + { "-useCwd", parseUseCwd }, }; // These Params are parsed during Engine Init before INI data is loaded @@ -1309,71 +1337,12 @@ static CommandLineParam paramsForEngineInit[] = }; -char *nextParam(char *newSource, const char *seps) +static void parseCommandLine(const CommandLineParam* params, int numParams, std::vector *parsedArguments = nullptr) { - 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 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->size() < static_cast(argc > 0 ? argc - 1 : 0)) + parsedArguments->resize(argc - 1, FALSE); int arg = 1; @@ -1407,7 +1376,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; } @@ -1419,6 +1395,15 @@ static void parseCommandLine(const CommandLineParam* params, int numParams) } } +bool CommandLine::wasCommandLineArgumentParsed(int argIndex) +{ + if (TheGlobalData == nullptr) + return false; + + const BoolVector &parsedArguments = TheGlobalData->m_commandLineData.m_parsedArguments; + return argIndex >= 0 && argIndex < static_cast(parsedArguments.size()) && parsedArguments[argIndex]; +} + void createGlobalData() { if (TheGlobalData == nullptr) @@ -1435,7 +1420,10 @@ void CommandLine::parseCommandLineForStartup() return; TheWritableGlobalData->m_commandLineData.m_hasParsedCommandLineForStartup = true; - parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup)); + parseCommandLine(paramsForStartup, ARRAY_SIZE(paramsForStartup), + &TheWritableGlobalData->m_commandLineData.m_parsedArguments); + + rts::setCurrentDirectoryToExecutablePathIfNotSet(); } void CommandLine::parseCommandLineForEngineInit() @@ -1448,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 new file mode 100644 index 00000000000..703b4d02086 --- /dev/null +++ b/Core/GameEngine/Source/Common/WorkingDirectory.cpp @@ -0,0 +1,79 @@ +/* +** 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" + +namespace rts +{ + +static Bool s_workingDirectorySet = FALSE; + +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; + } + + s_workingDirectorySet = TRUE; + 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; + } + + 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 134f1465980..ac5aeb2e9b0 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" @@ -102,7 +103,6 @@ #include "Win32Device/GameClient/Win32Mouse.h" #include "Win32Device/Common/Win32LocalFileSystem.h" #include "Win32Device/Common/Win32BIGFileSystem.h" -#include "WWLib/trim.h" // DEFINES //////////////////////////////////////////////////////////////////// @@ -141,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 /////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// @@ -220,26 +161,17 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // save application instance ApplicationHInstance = hInstance; + CommandLine::parseCommandLineForStartup(); - // 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); - - /* - ** Convert WinMain arguments to simple main argc and argv - */ + // Collect CRT arguments not handled during startup parsing. std::list argvSet; - char *token; - token = nextParam(lpCmdLine, "\" "); - while (token != nullptr) { - char * str = strtrim(token); - argvSet.push_back(str); - DEBUG_LOG(("Adding '%s'", str)); - token = nextParam(nullptr, "\" "); + for (int arg = 1; arg < __argc; ++arg) + { + if (!CommandLine::wasCommandLineArgumentParsed(arg - 1)) + { + argvSet.push_back(__argv[arg]); + DEBUG_LOG(("Adding '%s'", __argv[arg])); + } } // not part of the subsystem list, because it should normally never be reset! @@ -251,7 +183,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 e631654250d..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,6 +68,7 @@ class CommandLineData Bool m_hasParsedCommandLineForStartup; Bool m_hasParsedCommandLineForEngineInit; + BoolVector m_parsedArguments; }; //------------------------------------------------------------------------------------------------- diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index c8e9bb9961d..e5ee8b9f254 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::parseCommandLineForStartup(); #ifdef RTS_DEBUG @@ -845,8 +838,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/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 daf1402d74c..1b6f25bdf2a 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,18 +185,11 @@ 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); - // 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 c122151259d..066c697c274 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" @@ -151,8 +152,31 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) return pFile; } +///////////////////////////////////////////////////////////////////////////// +// 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_argIndex(0) {} + virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override + { + if (CommandLine::wasCommandLineArgumentParsed(m_argIndex++)) + { + // MFC uses bLast to finalize its shell command, even when the final argument is skipped. + ParseLast(bLast); + return; + } + CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); + } + +private: + int m_argIndex; +}; ///////////////////////////////////////////////////////////////////////////// // The one and only CWorldBuilderApp object @@ -277,6 +301,8 @@ BOOL CWorldBuilderApp::InitInstance() // initialize the memory manager early initMemoryManager(); + CommandLine::parseCommandLineForStartup(); + DEBUG_LOG(("starting Worldbuilder.")); #ifdef RTS_DEBUG DEBUG_LOG(("RTS_DEBUG defined.")); @@ -305,14 +331,6 @@ 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); - TheFileSystem = new FileSystem; initSubsystem(TheLocalFileSystem, (LocalFileSystem*)new Win32LocalFileSystem); @@ -324,7 +342,8 @@ BOOL CWorldBuilderApp::InitInstance() INI ini; - initSubsystem(TheWritableGlobalData, new GlobalData(), "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); + DEBUG_ASSERTCRASH(TheWritableGlobalData, ("TheWritableGlobalData expected to be created")); + initSubsystem(TheWritableGlobalData, TheWritableGlobalData, "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); TheFramePacer = new FramePacer(); @@ -336,6 +355,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); @@ -427,7 +447,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 7f484111672..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,6 +68,7 @@ class CommandLineData Bool m_hasParsedCommandLineForStartup; Bool m_hasParsedCommandLineForEngineInit; + BoolVector m_parsedArguments; }; //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 0d37cab5933..9e53037e441 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::parseCommandLineForStartup(); #ifdef RTS_DEBUG @@ -872,7 +865,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/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 493d5aecc4e..84ed8192793 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,18 +185,11 @@ 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); - // 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 5f19126638a..189111925d4 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" @@ -151,8 +152,31 @@ FileClass * WB_W3DFileSystem::Get_File( char const *filename ) return pFile; } +///////////////////////////////////////////////////////////////////////////// +// 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_argIndex(0) {} + virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast) override + { + if (CommandLine::wasCommandLineArgumentParsed(m_argIndex++)) + { + // MFC uses bLast to finalize its shell command, even when the final argument is skipped. + ParseLast(bLast); + return; + } + CCommandLineInfo::ParseParam(pszParam, bFlag, bLast); + } + +private: + int m_argIndex; +}; ///////////////////////////////////////////////////////////////////////////// // The one and only CWorldBuilderApp object @@ -281,6 +305,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); @@ -315,14 +341,6 @@ 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); - TheFileSystem = new FileSystem; initSubsystem(TheLocalFileSystem, (LocalFileSystem*)new Win32LocalFileSystem); @@ -334,7 +352,8 @@ BOOL CWorldBuilderApp::InitInstance() INI ini; - initSubsystem(TheWritableGlobalData, new GlobalData(), "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); + DEBUG_ASSERTCRASH(TheWritableGlobalData, ("TheWritableGlobalData expected to be created")); + initSubsystem(TheWritableGlobalData, TheWritableGlobalData, "Data\\INI\\Default\\GameData", "Data\\INI\\GameData"); TheFramePacer = new FramePacer(); @@ -347,6 +366,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); @@ -444,7 +464,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