diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 0f36ff63383..e50ba0d07a9 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -136,6 +136,7 @@ set(GAMEENGINE_SRC Include/Common/version.h # Include/Common/WellKnownKeys.h Include/Common/WorkerProcess.h + Include/Common/WorkingDirectory.h Include/Common/Xfer.h Include/Common/XferCRC.h Include/Common/XferDeepCRC.h @@ -692,6 +693,7 @@ set(GAMEENGINE_SRC Source/Common/UserPreferences.cpp Source/Common/version.cpp Source/Common/WorkerProcess.cpp + Source/Common/WorkingDirectory.cpp Source/GameClient/ClientInstance.cpp Source/GameClient/Color.cpp Source/GameClient/Credits.cpp diff --git a/Core/GameEngine/Include/Common/WorkingDirectory.h b/Core/GameEngine/Include/Common/WorkingDirectory.h new file mode 100644 index 00000000000..013f420cb69 --- /dev/null +++ b/Core/GameEngine/Include/Common/WorkingDirectory.h @@ -0,0 +1,36 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#include "Lib/BaseType.h" + +namespace rts +{ + +// TheSuperHackers @feature 14/08/2026 +// Startup working directory helpers. By default the process working directory is +// the executable directory. -cwd keeps the OS directory. -cwd uses that path. + +Bool setCurrentDirectoryToExecutablePath(); +Bool setCurrentDirectoryToPath(const char *path); + +// For tools that do not parse CommandLine startup flags. +void applyStartupWorkingDirectory(); + +} // namespace rts diff --git a/Core/GameEngine/Source/Common/CommandLine.cpp b/Core/GameEngine/Source/Common/CommandLine.cpp index 772830f0f67..a5383ba9880 100644 --- a/Core/GameEngine/Source/Common/CommandLine.cpp +++ b/Core/GameEngine/Source/Common/CommandLine.cpp @@ -28,6 +28,7 @@ #include "Common/ArchiveFileSystem.h" #include "Common/CommandLine.h" #include "Common/CRCDebug.h" +#include "Common/WorkingDirectory.h" #include "Common/LocalFileSystem.h" #include "Common/Recorder.h" #include "Common/version.h" @@ -463,6 +464,21 @@ Int parseJobs(char *args[], int num) return 1; } +Int parseCwd(char *args[], int num) +{ + // TheSuperHackers @feature 14/08/2026 + // -cwd keeps the OS working directory. -cwd uses that directory instead. + TheWritableGlobalData->m_changeCurrentWorkingDirectoryToExecutablePath = FALSE; + + if (num > 1 && args[1] != nullptr && args[1][0] != '-' && args[1][0] != '\0') + { + if (!rts::setCurrentDirectoryToPath(args[1])) + rts::setCurrentDirectoryToExecutablePath(); + return 2; + } + return 1; +} + Int parseXRes(char *args[], int num) { if (num > 1) @@ -1141,6 +1157,11 @@ static CommandLineParam paramsForStartup[] = // (If you have 4 cores, call it with -jobs 4) // If you do not call this, all replays will be simulated in sequence in the same process. { "-jobs", parseJobs }, + + // TheSuperHackers @feature 14/08/2026 + // Use the current working directory as provided by the OS, or an optional path. + // Without this flag the working directory is forced to the executable directory. + { "-cwd", parseCwd }, }; // These Params are parsed during Engine Init before INI data is loaded diff --git a/Core/GameEngine/Source/Common/WorkingDirectory.cpp b/Core/GameEngine/Source/Common/WorkingDirectory.cpp new file mode 100644 index 00000000000..6852e6a9abe --- /dev/null +++ b/Core/GameEngine/Source/Common/WorkingDirectory.cpp @@ -0,0 +1,142 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine + +#include "Common/WorkingDirectory.h" +#include "WWLib/trim.h" + +namespace rts +{ + +Bool setCurrentDirectoryToExecutablePath() +{ + Char buffer[_MAX_PATH]; + const DWORD len = GetModuleFileName(nullptr, buffer, ARRAY_SIZE(buffer)); + if (len == 0 || len >= ARRAY_SIZE(buffer)) + { + DEBUG_LOG(("Failed to get executable path for working directory (error %d)", GetLastError())); + return FALSE; + } + + if (Char *pEnd = strrchr(buffer, '\\')) + { + *pEnd = 0; + } + + if (::SetCurrentDirectory(buffer) == 0) + { + DEBUG_LOG(("Failed to set working directory to executable path '%s' (error %d)", buffer, GetLastError())); + return FALSE; + } + + return TRUE; +} + +Bool setCurrentDirectoryToPath(const char *path) +{ + if (path == nullptr || path[0] == '\0') + return FALSE; + + if (::SetCurrentDirectory(path) == 0) + { + DEBUG_LOG(("Failed to set working directory to '%s' (error %d)", path, GetLastError())); + return FALSE; + } + + return TRUE; +} + +static char *nextWorkingDirectoryParam(char *newSource, const char *seps) +{ + static char *source = nullptr; + if (newSource) + { + source = newSource; + } + if (!source) + { + return nullptr; + } + + char *first = source; + if (first) + { + char *firstSep = strpbrk(first, seps); + char firstChar[2] = {0,0}; + if (firstSep == first) + { + firstChar[0] = *first; + while (*first == firstChar[0]) first++; + } + + char *end; + if (firstChar[0]) + end = strpbrk(first, firstChar); + else + end = strpbrk(first, seps); + + if (end) + { + source = end+1; + *end = 0; + + if (!*source) + source = nullptr; + } + else + { + source = nullptr; + } + + if (first && !*first) + first = nullptr; + } + + return first; +} + +void applyStartupWorkingDirectory() +{ + std::vector argv; + std::string cmdLine = GetCommandLineA(); + char *token = nextWorkingDirectoryParam(&cmdLine[0], "\" "); + while (token != nullptr) + { + argv.push_back(strtrim(token)); + token = nextWorkingDirectoryParam(nullptr, "\" "); + } + + const int argc = (int)argv.size(); + for (int arg = 1; arg < argc; ++arg) + { + if (stricmp(argv[arg], "-cwd") != 0) + continue; + + if (arg + 1 < argc && argv[arg + 1] != nullptr && argv[arg + 1][0] != '-' && argv[arg + 1][0] != '\0') + { + if (!setCurrentDirectoryToPath(argv[arg + 1])) + setCurrentDirectoryToExecutablePath(); + } + return; + } + + setCurrentDirectoryToExecutablePath(); +} + +} // namespace rts diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp index 134f1465980..70a8ab09b2a 100644 --- a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp +++ b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp @@ -43,6 +43,7 @@ // USER INCLUDES ////////////////////////////////////////////////////////////// #include "Lib/BaseType.h" +#include "Common/WorkingDirectory.h" #include "Common/Debug.h" #include "Common/GameMemory.h" #include "Common/GlobalData.h" @@ -220,14 +221,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // save application instance ApplicationHInstance = hInstance; - - // Set the current directory to the app directory. - char buf[_MAX_PATH]; - GetModuleFileName(nullptr, buf, sizeof(buf)); - if (char *pEnd = strrchr(buf, '\\')) { - *pEnd = 0; - } - ::SetCurrentDirectory(buf); + rts::applyStartupWorkingDirectory(); /* ** Convert WinMain arguments to simple main argc and argv diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index e631654250d..769b02936f3 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -120,6 +120,10 @@ class GlobalData : public SubsystemInterface // Run game without graphics, input or audio. Bool m_headless; + // TheSuperHackers @feature 14/08/2026 + // On startup change the current working directory to the executable's location. + Bool m_changeCurrentWorkingDirectoryToExecutablePath; + Bool m_windowed; Int m_xResolution; Int m_yResolution; diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index f7720c351a2..c2da0341795 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -633,6 +633,7 @@ GlobalData::GlobalData() m_framesPerSecondLimit = 0; m_chipSetType = 0; m_headless = FALSE; + m_changeCurrentWorkingDirectoryToExecutablePath = TRUE; m_windowed = 0; m_xResolution = DEFAULT_DISPLAY_WIDTH; m_yResolution = DEFAULT_DISPLAY_HEIGHT; diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index c8e9bb9961d..bb1532057bb 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -43,6 +43,7 @@ #include "WinMain.h" #include "Lib/BaseType.h" #include "Common/CommandLine.h" +#include "Common/WorkingDirectory.h" #include "Common/CriticalSection.h" #include "Common/GlobalData.h" #include "Common/GameEngine.h" @@ -817,14 +818,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // initialize the memory manager early initMemoryManager(); - /// @todo remove this force set of working directory later - Char buffer[ _MAX_PATH ]; - GetModuleFileName( nullptr, buffer, sizeof( buffer ) ); - if (Char *pEnd = strrchr(buffer, '\\')) - { - *pEnd = 0; - } - ::SetCurrentDirectory(buffer); + CommandLine::parseCommandLineForStartup(); + if (TheGlobalData->m_changeCurrentWorkingDirectoryToExecutablePath) + rts::setCurrentDirectoryToExecutablePath(); #ifdef RTS_DEBUG @@ -845,8 +841,6 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // Force to be loaded from a file, not a resource so same exe can be used in germany and retail. gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, "Install_Final.bmp", IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE); - CommandLine::parseCommandLineForStartup(); - #ifdef RTS_ENABLE_CRASHDUMP // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData()); diff --git a/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp b/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp index daf1402d74c..9160b72e8f1 100644 --- a/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp +++ b/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp @@ -49,6 +49,7 @@ #include // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "Common/WorkingDirectory.h" #include "Common/Debug.h" #include "Common/FramePacer.h" #include "Common/GameMemory.h" @@ -184,14 +185,7 @@ Int APIENTRY WinMain(HINSTANCE hInstance, HACCEL hAccelTable; Bool quit = FALSE; - /// @todo remove this force set of working directory later - Char buffer[ _MAX_PATH ]; - GetModuleFileName( nullptr, buffer, sizeof( buffer ) ); - if (Char *pEnd = strrchr(buffer, '\\')) - { - *pEnd = 0; - } - ::SetCurrentDirectory(buffer); + rts::applyStartupWorkingDirectory(); // initialize the memory manager early initMemoryManager(); diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index c122151259d..ba7db58afae 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -32,6 +32,7 @@ //#include #include "W3DDevice/GameClient/W3DFileSystem.h" +#include "Common/WorkingDirectory.h" #include "Common/FramePacer.h" #include "Common/GlobalData.h" #include "WHeightMapEdit.h" @@ -305,13 +306,7 @@ BOOL CWorldBuilderApp::InitInstance() Enable3dControlsStatic(); // Call this when linking to MFC statically #endif - // Set the current directory to the app directory. - char buf[_MAX_PATH]; - GetModuleFileName(nullptr, buf, sizeof(buf)); - if (char *pEnd = strrchr(buf, '\\')) { - *pEnd = 0; - } - ::SetCurrentDirectory(buf); + rts::applyStartupWorkingDirectory(); TheFileSystem = new FileSystem; @@ -336,6 +331,7 @@ BOOL CWorldBuilderApp::InitInstance() TheWritableGlobalData->m_debugIgnoreAsserts = true; #endif + char buf[_MAX_PATH]; #if 1 // srj sez: put INI into our user data folder, not the ap dir free((void*)m_pszProfileName); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 7f484111672..f1c5ae90048 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -121,6 +121,10 @@ class GlobalData : public SubsystemInterface // Run game without graphics, input or audio. Bool m_headless; + // TheSuperHackers @feature 14/08/2026 + // On startup change the current working directory to the executable's location. + Bool m_changeCurrentWorkingDirectoryToExecutablePath; + Bool m_windowed; Int m_xResolution; Int m_yResolution; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e862cd149d5..1fd15af0374 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -637,6 +637,7 @@ GlobalData::GlobalData() m_framesPerSecondLimit = 0; m_chipSetType = 0; m_headless = FALSE; + m_changeCurrentWorkingDirectoryToExecutablePath = TRUE; m_windowed = 0; m_xResolution = DEFAULT_DISPLAY_WIDTH; m_yResolution = DEFAULT_DISPLAY_HEIGHT; diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 0d37cab5933..3d8e46f2b6e 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -43,6 +43,7 @@ #include "WinMain.h" #include "Lib/BaseType.h" #include "Common/CommandLine.h" +#include "Common/WorkingDirectory.h" #include "Common/CriticalSection.h" #include "Common/GlobalData.h" #include "Common/GameEngine.h" @@ -824,14 +825,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // initialize the memory manager early initMemoryManager(); - /// @todo remove this force set of working directory later - Char buffer[ _MAX_PATH ]; - GetModuleFileName( nullptr, buffer, sizeof( buffer ) ); - if (Char *pEnd = strrchr(buffer, '\\')) - { - *pEnd = 0; - } - ::SetCurrentDirectory(buffer); + CommandLine::parseCommandLineForStartup(); + if (TheGlobalData->m_changeCurrentWorkingDirectoryToExecutablePath) + rts::setCurrentDirectoryToExecutablePath(); #ifdef RTS_DEBUG @@ -872,7 +868,6 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, "Install_Final.bmp", IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE); #endif - CommandLine::parseCommandLineForStartup(); #ifdef RTS_ENABLE_CRASHDUMP // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData()); diff --git a/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp b/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp index 493d5aecc4e..84f339c4202 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp +++ b/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp @@ -49,6 +49,7 @@ #include // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "Common/WorkingDirectory.h" #include "Common/Debug.h" #include "Common/FramePacer.h" #include "Common/GameMemory.h" @@ -184,14 +185,7 @@ Int APIENTRY WinMain(HINSTANCE hInstance, HACCEL hAccelTable; Bool quit = FALSE; - /// @todo remove this force set of working directory later - Char buffer[ _MAX_PATH ]; - GetModuleFileName( nullptr, buffer, sizeof( buffer ) ); - if (Char *pEnd = strrchr(buffer, '\\')) - { - *pEnd = 0; - } - ::SetCurrentDirectory(buffer); + rts::applyStartupWorkingDirectory(); // initialize the memory manager early initMemoryManager(); diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 5f19126638a..52a673a6796 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -32,6 +32,7 @@ //#include #include "W3DDevice/GameClient/W3DFileSystem.h" +#include "Common/WorkingDirectory.h" #include "Common/FramePacer.h" #include "Common/GlobalData.h" #include "WHeightMapEdit.h" @@ -315,13 +316,7 @@ BOOL CWorldBuilderApp::InitInstance() Enable3dControlsStatic(); // Call this when linking to MFC statically #endif - // Set the current directory to the app directory. - char buf[_MAX_PATH]; - GetModuleFileName(nullptr, buf, sizeof(buf)); - if (char *pEnd = strrchr(buf, '\\')) { - *pEnd = 0; - } - ::SetCurrentDirectory(buf); + rts::applyStartupWorkingDirectory(); TheFileSystem = new FileSystem; @@ -347,6 +342,7 @@ BOOL CWorldBuilderApp::InitInstance() #endif DEBUG_LOG(("TheWritableGlobalData %x", TheWritableGlobalData)); + char buf[_MAX_PATH]; #if 1 // srj sez: put INI into our user data folder, not the ap dir free((void*)m_pszProfileName);