diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index af39c615d37..c462ee691f2 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 @@ -693,6 +694,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..e76efc40edf --- /dev/null +++ b/Core/GameEngine/Include/Common/WorkingDirectory.h @@ -0,0 +1,48 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 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 CryoTheRenegade 14/08/2026 +// Saves and restores the process working directory. +class WorkingDirectory +{ +public: + static Bool setStartupWorkingDirectory(); + static Bool setExecutableWorkingDirectory(); + // Relative paths are resolved from the current working directory. + static Bool setCustomWorkingDirectory(const char *path); + // Returns true after a setter successfully changes the working directory. + static Bool hasSetWorkingDirectory(); + +private: + friend struct WorkingDirectoryInitializer; + + static Bool saveStartupWorkingDirectory(); + static Bool setWorkingDirectory(const char *path); + + static Bool s_hasSetWorkingDirectory; + static Char s_startupWorkingDirectory[]; +}; + +} // namespace rts diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 96a6f475d30..0daeb20e9ff 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -31,11 +31,11 @@ #include "Common/LocalFileSystem.h" #include "Common/Recorder.h" #include "Common/version.h" +#include "Common/WorkingDirectory.h" #include "GameClient/ClientInstance.h" #include "GameClient/TerrainVisual.h" // for TERRAIN_LOD_MIN definition #include "GameClient/GameText.h" #include "GameNetwork/NetworkDefs.h" -#include "WWLib/trim.h" @@ -463,6 +463,24 @@ Int parseJobs(char *args[], int num) return 1; } +Int parseUseCwd(char *[], int) +{ + // -useCwd restores the startup working directory. + rts::WorkingDirectory::setStartupWorkingDirectory(); + return 1; +} + +Int parseSetCwd(char *args[], int num) +{ + // -setCwd overrides the working directory. + if (num > 1) + { + rts::WorkingDirectory::setCustomWorkingDirectory(args[1]); + return 2; + } + return 1; +} + Int parseXRes(char *args[], int num) { if (num > 1) @@ -1155,6 +1173,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 CryoTheRenegade 14/08/2026 + // Use the current working directory as provided by the OS, or an explicit path. + // The last successful selection wins; otherwise use the executable directory. + { "-setCwd", parseSetCwd }, + { "-useCwd", parseUseCwd }, }; // These Params are parsed during Engine Init before INI data is loaded @@ -1326,116 +1350,63 @@ static CommandLineParam paramsForEngineInit[] = }; -char *nextParam(char *newSource, const char *seps) +static void parseCommandLine(const CommandLineParam* params, int numParams, BoolVector &parsedArguments) { - static char *source = nullptr; - if (newSource) + // Startup parsing can run from static constructors, before WinMain. + int argc = __argc; + char **argv = __argv; + if (argc > 0) { - source = newSource; - } - if (!source) - { - return nullptr; + // Skip the first argument which is the executable file name. + argc -= 1; + argv += 1; } - - // 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(); - - int arg = 1; + // Preserve arguments recorded by the earlier parsing phase. + parsedArguments.resize(argc, FALSE); #ifdef DEBUG_LOGGING DEBUG_LOG(("Command-line args:")); int debugFlags = DebugGetFlags(); DebugSetFlags(debugFlags & ~DEBUG_FLAG_PREPEND_TIME); // turn off timestamps - for (arg=1; argm_commandLineData.m_parsedArguments; + return argIndex >= 0 && argIndex < static_cast(parsedArguments.size()) && parsedArguments[argIndex]; +} + void createGlobalData() { if (TheGlobalData == nullptr) @@ -1452,7 +1423,11 @@ 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); + + if (!rts::WorkingDirectory::hasSetWorkingDirectory()) + rts::WorkingDirectory::setExecutableWorkingDirectory(); } void CommandLine::parseCommandLineForEngineInit() @@ -1465,5 +1440,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..7c927bf9bbe --- /dev/null +++ b/Core/GameEngine/Source/Common/WorkingDirectory.cpp @@ -0,0 +1,117 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 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 +{ + +Bool WorkingDirectory::s_hasSetWorkingDirectory = FALSE; +Char WorkingDirectory::s_startupWorkingDirectory[_MAX_PATH] = ""; + +struct WorkingDirectoryInitializer +{ + WorkingDirectoryInitializer() + { + WorkingDirectory::saveStartupWorkingDirectory(); + } +}; + +// Request capture at startup, even if no setter is called. +static WorkingDirectoryInitializer s_workingDirectoryInitializer; + +Bool WorkingDirectory::saveStartupWorkingDirectory() +{ + // An earlier static constructor may call a setter before our initializer runs. + // Cache the first capture, including failure, without logging or allocating. + static const DWORD len = ::GetCurrentDirectory(ARRAY_SIZE(s_startupWorkingDirectory), s_startupWorkingDirectory); + return len > 0 && len < ARRAY_SIZE(s_startupWorkingDirectory); +} + +Bool WorkingDirectory::setWorkingDirectory(const char *path) +{ + if (path == nullptr || path[0] == '\0') + { + DEBUG_LOG(("Cannot set an empty working directory")); + return FALSE; + } + + if (::SetCurrentDirectory(path) == 0) + { + DEBUG_LOG(("Failed to set working directory to '%s' (error %d)", path, GetLastError())); + return FALSE; + } + + s_hasSetWorkingDirectory = TRUE; + return TRUE; +} + +Bool WorkingDirectory::setStartupWorkingDirectory() +{ + if (!saveStartupWorkingDirectory()) + { + DEBUG_LOG(("Startup working directory is unavailable")); + return FALSE; + } + return setWorkingDirectory(s_startupWorkingDirectory); +} + +Bool WorkingDirectory::setExecutableWorkingDirectory() +{ + saveStartupWorkingDirectory(); + + Char buffer[_MAX_PATH]; + const DWORD len = GetModuleFileName(nullptr, buffer, ARRAY_SIZE(buffer)); + if (len == 0) + { + DEBUG_LOG(("Failed to get executable path for working directory (error %d)", GetLastError())); + return FALSE; + } + if (len >= ARRAY_SIZE(buffer)) + { + DEBUG_LOG(("Executable path exceeds the working directory path buffer")); + return FALSE; + } + + Char *pEnd = strrchr(buffer, '\\'); + if (pEnd == nullptr) + { + DEBUG_LOG(("Executable path has no directory: '%s'", buffer)); + return FALSE; + } + // TheSuperHackers @bugfix For "C:\game.exe", retain "C:\" as the directory. + // Removing the backslash leaves "C:", which refers to that drive's current directory. + pEnd[1] = '\0'; + + return setWorkingDirectory(buffer); +} + +Bool WorkingDirectory::setCustomWorkingDirectory(const char *path) +{ + saveStartupWorkingDirectory(); + return setWorkingDirectory(path); +} + +Bool WorkingDirectory::hasSetWorkingDirectory() +{ + return s_hasSetWorkingDirectory; +} + +} // namespace rts diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp index 1ba344f7c01..4ffad1f5959 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 0c8e8820660..c00862a980b 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 b3287235361..807beebcd80 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 89a5fa08f9d..92761fd3c33 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 d49589ee0c8..b0307ed0261 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