From fcc794910eee3a25914d2d3d8af9deb9d50c33ff Mon Sep 17 00:00:00 2001 From: Arklon <384705-Arklon@users.noreply.gitlab.com> Date: Fri, 4 Dec 2020 00:51:12 -0500 Subject: [PATCH 01/16] op2ext changes for 1.4.0 --- srcDLL/DllMain.cpp | 71 ++-- srcDLL/op2extDLL.vcxproj | 310 +++++++-------- srcStatic/ConsoleArgumentParser.cpp | 82 ++-- srcStatic/ConsoleArgumentParser.h | 17 +- srcStatic/FileSystemHelper.cpp | 7 +- srcStatic/FileSystemHelper.h | 1 + srcStatic/GameModules/ConsoleModule.cpp | 2 +- srcStatic/GameModules/DllModule.cpp | 20 +- srcStatic/GameModules/DllModule.h | 6 +- .../EarthworkerProximityTasking.cpp | 11 - .../GameModules/EarthworkerProximityTasking.h | 16 - srcStatic/GameModules/IniModule.cpp | 19 +- srcStatic/GetCommandLineArguments.cpp | 4 +- srcStatic/Log/LoggerFile.cpp | 2 +- srcStatic/ModuleLoader.cpp | 53 +-- srcStatic/ModuleLoader.h | 4 +- srcStatic/ResourceSearchPath.cpp | 65 ++- srcStatic/ResourceSearchPath.h | 5 + srcStatic/op2extStatic.vcxproj | 370 +++++++++--------- srcStatic/op2extStatic.vcxproj.filters | 196 +++++----- test/ConsoleArgumentParser.test.cpp | 71 +++- test/ConsoleModule.test.cpp | 10 +- test/LoggerFile.test.cpp | 2 +- 23 files changed, 727 insertions(+), 617 deletions(-) delete mode 100644 srcStatic/GameModules/EarthworkerProximityTasking.cpp delete mode 100644 srcStatic/GameModules/EarthworkerProximityTasking.h diff --git a/srcDLL/DllMain.cpp b/srcDLL/DllMain.cpp index 18a0ecd..ed089f7 100644 --- a/srcDLL/DllMain.cpp +++ b/srcDLL/DllMain.cpp @@ -6,6 +6,7 @@ #include "OP2Memory.h" #include "FileSystemHelper.h" #include "FsInclude.h" +#include "ConsoleArgumentParser.h" #include "Log.h" #include "Log/LoggerFile.h" #include "Log/LoggerMessageBox.h" @@ -40,34 +41,39 @@ AppEvents appEvents; BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*reserved*/) { // This will be called once the program is unpacked and running - if (dwReason == DLL_PROCESS_ATTACH) { - // Setup logging - SetLoggerError(&loggerDistributor); - SetLoggerMessage(&loggerFile); - SetLoggerDebug(&loggerDebug); - - // Construct global objects - vols = std::make_unique>(); - moduleLoader = std::make_unique(); - - // Set load offset for Outpost2.exe module, used during memory patching - // If this fails, it's because Outpost2.exe is not loaded - // Failure means op2ext.dll was loaded by something else, such as a unit test - // For unit tests, just stay in memory, it's not an error if this fails - if (EnableOp2MemoryPatching()) { - // These hooks are needed to further bootstrap the rest of module loading - if (!InstallTAppEventHooks()) { - LogError("Failed to install initial TApp event hooks. Module loading and patching disabled."); - return FALSE; + if ((dwReason == DLL_PROCESS_ATTACH) && CommandOptionExists("OPU")) { + wchar_t filename[MAX_PATH] = L""; + GetModuleFileNameW(NULL, &filename[0], MAX_PATH); + + if (fs::path(filename).filename() == "Outpost2.exe") { + // Setup logging + SetLoggerError(&loggerDistributor); + SetLoggerMessage(&loggerFile); + SetLoggerDebug(&loggerDebug); + + // Construct global objects + vols = std::make_unique>(); + moduleLoader = std::make_unique(); + + // Set load offset for Outpost2.exe module, used during memory patching + // If this fails, it's because Outpost2.exe is not loaded + // Failure means op2ext.dll was loaded by something else, such as a unit test + // For unit tests, just stay in memory, it's not an error if this fails + if (EnableOp2MemoryPatching()) { + // These hooks are needed to further bootstrap the rest of module loading + if (!InstallTAppEventHooks()) { + LogError("Failed to install initial TApp event hooks. Module loading and patching disabled."); + return FALSE; + } + + // Setup event handlers + appEvents.onInit.Add(&OnInit); + appEvents.onLoadShell.Add(&OnLoadShell); + appEvents.onShutDown.Add(&OnShutdown); + + // Set active events + appEvents.Activate(); } - - // Setup event handlers - appEvents.onInit.Add(&OnInit); - appEvents.onLoadShell.Add(&OnLoadShell); - appEvents.onShutDown.Add(&OnShutdown); - - // Set active events - appEvents.Activate(); } // Disable any more thread attach calls @@ -104,11 +110,22 @@ bool InstallDepPatch() } +// Redirects Outpost2.ini to be accessed from the OPU directory. +void RedirectIniFile() +{ + const auto iniPath = GetOutpost2IniPath(); + Op2MemCopy(0x00547090, iniPath.length() + 1, iniPath.data()); // Overwrite gConfigFile.iniPath +} + + void OnInit() { // Install DEP patch so newer versions of Windows don't terminate the game InstallDepPatch(); + // Set the game to look for Outpost2.ini under the OPU directory + RedirectIniFile(); + // Order of precedence for loading vol files is: // ART_PATH (from console module), Console Module, Ini Modules, Addon directory, Game directory diff --git a/srcDLL/op2extDLL.vcxproj b/srcDLL/op2extDLL.vcxproj index e0fa7d3..0ff19a9 100644 --- a/srcDLL/op2extDLL.vcxproj +++ b/srcDLL/op2extDLL.vcxproj @@ -1,155 +1,157 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - - - - {6DE3610C-C9EE-4FD3-BBD7-66382C16FDE9} - - - - DynamicLibrary - v142 - false - MultiByte - - - DynamicLibrary - v142 - false - MultiByte - - - - - - - - - - - - - - - false - op2ext - - - false - op2ext - - - - MultiThreaded - Default - true - true - MinSpace - true - Level3 - ../srcStatic;%(AdditionalIncludeDirectories) - WIN32;OP2EXT_INTERNAL_BUILD;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - stdcpp17 - - - if defined Outpost2Path (xcopy /y /d "$(TargetPath)" "$(Outpost2Path)\") else (echo Outpost2Path environment variable not defined. Skipping Post Build Copy.) - - - true - NDEBUG;%(PreprocessorDefinitions) - true - Win32 - - - 0x0409 - NDEBUG;%(PreprocessorDefinitions) - - - true - - - true - true - Console - false - false - true - 0x10000000 - false - - - - - MultiThreadedDebug - Default - false - Disabled - true - Level3 - false - ../srcStatic;%(AdditionalIncludeDirectories) - WIN32;OP2EXT_INTERNAL_BUILD;DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - stdcpp17 - - - true - DEBUG;%(PreprocessorDefinitions) - true - Win32 - - - 0x0409 - DEBUG;%(PreprocessorDefinitions) - - - true - - - true - true - true - Console - false - false - true - 0x10000000 - false - - - if defined Outpost2Path (xcopy /y /d "$(TargetPath)" "$(Outpost2Path)\") else (echo Outpost2Path environment variable not defined. Skipping Post Build Copy.) - - - - - - - - - - - - - - - - - - - - {4f82df84-de4a-4ae0-9e05-fc5408ae23ba} - - - - - + + + + + Debug + Win32 + + + Release + Win32 + + + + + + {6DE3610C-C9EE-4FD3-BBD7-66382C16FDE9} + + + + DynamicLibrary + v142 + false + MultiByte + + + DynamicLibrary + v142 + false + MultiByte + + + + + + + + + + + + + + + false + op2ext + + + false + op2ext + + + + MultiThreaded + Default + true + true + MinSpace + true + Level3 + ../srcStatic;%(AdditionalIncludeDirectories) + WIN32;OP2EXT_INTERNAL_BUILD;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdcpp17 + /Zc:threadSafeInit- %(AdditionalOptions) + + + if defined Outpost2Path (xcopy /y /d "$(TargetPath)" "$(Outpost2Path)\") else (echo Outpost2Path environment variable not defined. Skipping Post Build Copy.) + + + true + NDEBUG;%(PreprocessorDefinitions) + true + Win32 + + + 0x0409 + NDEBUG;%(PreprocessorDefinitions) + + + true + + + true + true + Console + false + false + true + 0x10000000 + false + + + + + MultiThreadedDebug + Default + false + Disabled + true + Level3 + false + ../srcStatic;%(AdditionalIncludeDirectories) + WIN32;OP2EXT_INTERNAL_BUILD;DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdcpp17 + /Zc:threadSafeInit- %(AdditionalOptions) + + + true + DEBUG;%(PreprocessorDefinitions) + true + Win32 + + + 0x0409 + DEBUG;%(PreprocessorDefinitions) + + + true + + + true + true + true + Console + false + false + true + 0x10000000 + false + + + if defined Outpost2Path (xcopy /y /d "$(TargetPath)" "$(Outpost2Path)\") else (echo Outpost2Path environment variable not defined. Skipping Post Build Copy.) + + + + + + + + + + + + + + + + + + + + {4f82df84-de4a-4ae0-9e05-fc5408ae23ba} + + + + + \ No newline at end of file diff --git a/srcStatic/ConsoleArgumentParser.cpp b/srcStatic/ConsoleArgumentParser.cpp index f8a5003..0c3738f 100644 --- a/srcStatic/ConsoleArgumentParser.cpp +++ b/srcStatic/ConsoleArgumentParser.cpp @@ -2,66 +2,60 @@ #include "ConsoleArgumentParser.h" #include "StringConversion.h" #include "Log.h" -#include - -std::vector GetCommandLineArguments(); -std::string GetSwitch(std::vector& arguments); -std::string ParseSwitchName(std::string switchName); +static CommandIterator FindCommandSwitch(CommandIterator begin, CommandIterator end, const std::string& option) +{ + return std::find_if(begin, end, [&option](const std::string& argument) + { return ((argument[0] == '/') || (argument[0] == '-')) && (_stricmp(&argument[1], option.data()) == 0); }); +} -std::vector FindModuleDirectories() +// Returns iterator to option's argument, or end if not found +CommandIterator GetCommandOption(CommandIterator begin, CommandIterator end, const std::string& option) { - try { - const auto arguments = GetCommandLineArguments(); - return FindModuleDirectories(arguments); - } catch(const std::exception& e) { - LogError("Error parsing command line arguments: " + std::string(e.what())); - return {}; - } + auto it = FindCommandSwitch(begin, end, option); + return ((it != end) && (++it != end)) ? it : end; } -std::vector FindModuleDirectories(std::vector arguments) +// Returns option's argument, or empty string if not found +std::string GetCommandOption(const std::string& option) { - const std::string switchName = GetSwitch(arguments); - - if (switchName.empty()) { - return {}; - } - - if (switchName == "loadmod") { - if (arguments.empty()) { - throw std::runtime_error("No relative directory argument provided for the switch loadmod"); - } + const auto arguments = GetCommandLineArguments(); + const auto it = GetCommandOption(arguments.cbegin(), arguments.cend(), option); + return (it != arguments.end()) ? *it : ""; +} - return arguments; - } - throw std::runtime_error("Provided switch is not supported: " + switchName); +bool CommandOptionExists(CommandIterator begin, CommandIterator end, const std::string& option) +{ + return (FindCommandSwitch(begin, end, option) != end); } -std::string GetSwitch(std::vector& arguments) +bool CommandOptionExists(const std::string& option) { - if (arguments.size() == 0) { - return std::string(); // Switch is not present - } - - const std::string rawSwitch = arguments[0]; - arguments.erase(arguments.begin()); // Remove rawSwitch from arguments - - return ParseSwitchName(rawSwitch); + const auto arguments = GetCommandLineArguments(); + return CommandOptionExists(arguments.cbegin(), arguments.cend(), option); } -// Returns empty string on failure -std::string ParseSwitchName(std::string switchName) + +std::vector FindModuleDirectories(const std::vector& arguments) { - if (switchName[0] != '/' && switchName[0] != '-') { - const std::string message("A switch was expected but not found. Prefix switch name with '/' or '-'. The following statement was found instead: " + switchName); - throw std::runtime_error(message); + std::vector directories; + + for (auto it = arguments.cbegin(); (it = GetCommandOption(it, arguments.cend(), "loadmod")) != arguments.cend();) { + auto& option = *it; + if ((option[0] == '/') || (option[0] == '-')) { + LogMessage("Ignoring parsing ill-formed command line arguments " + (*(it - 1)) + " " + option); + } + else { + directories.push_back(option); + } } - switchName.erase(switchName.begin(), switchName.begin() + 1); //Removes leading - or / - ToLowerInPlace(switchName); + return directories; +} - return switchName; +std::vector FindModuleDirectories() +{ + return FindModuleDirectories(GetCommandLineArguments()); } diff --git a/srcStatic/ConsoleArgumentParser.h b/srcStatic/ConsoleArgumentParser.h index 161d10f..b32b8c8 100644 --- a/srcStatic/ConsoleArgumentParser.h +++ b/srcStatic/ConsoleArgumentParser.h @@ -3,6 +3,21 @@ #include #include +using CommandIterator = std::vector::const_iterator; + +// Returns the specified option's argument (e.g. "/option argument" or "-option argument"), or empty string if not found +std::string GetCommandOption(const std::string& option); + +// Returns true if option was found in command line arguments. +bool CommandOptionExists(const std::string& option); + +// Returns an iterator to the argument for the specified option, or end if not found. +// Can be used to iterate over repeated arguments. +CommandIterator GetCommandOption(CommandIterator begin, CommandIterator end, const std::string& option); + +// Returns true if option was found in command line arguments. +bool CommandOptionExists(CommandIterator begin, CommandIterator end, const std::string& option); + // Returns an empty vector if no console switch is found or if the switch is ill-formed. // Logs error and posts modal dialog if switch is ill-formed. // Automatically pulls Console arguments using CommandLineToArgvW @@ -10,4 +25,4 @@ std::vector FindModuleDirectories(); // Instead of pulling Console arguments, allows passing in arguments for testability // Passed in values should already be formatted as the return value of CommandLineToArgvW without the executable's name -std::vector FindModuleDirectories(std::vector arguments); +std::vector FindModuleDirectories(const std::vector& arguments); diff --git a/srcStatic/FileSystemHelper.cpp b/srcStatic/FileSystemHelper.cpp index 7f1ac7d..3886451 100644 --- a/srcStatic/FileSystemHelper.cpp +++ b/srcStatic/FileSystemHelper.cpp @@ -15,9 +15,14 @@ std::string GetExeDirectory() return fs::path(moduleFilename).remove_filename().string(); } +std::string GetOpuDirectory() +{ + return (fs::path(GetExeDirectory())/"OPU").string(); +} + std::string GetOutpost2IniPath() { - return fs::path(GetExeDirectory()).append("outpost2.ini").string(); + return fs::path(GetOpuDirectory()).append("outpost2.ini").string(); } std::string GetOutpost2IniSetting(const std::string& sectionName, const std::string& key) diff --git a/srcStatic/FileSystemHelper.h b/srcStatic/FileSystemHelper.h index e863f17..8a9425e 100644 --- a/srcStatic/FileSystemHelper.h +++ b/srcStatic/FileSystemHelper.h @@ -5,6 +5,7 @@ std::string GetExeDirectory(); +std::string GetOpuDirectory(); std::string GetOutpost2IniPath(); std::string GetOutpost2IniSetting(const std::string& sectionName, const std::string& key); diff --git a/srcStatic/GameModules/ConsoleModule.cpp b/srcStatic/GameModules/ConsoleModule.cpp index 3c3e94e..f19e312 100644 --- a/srcStatic/GameModules/ConsoleModule.cpp +++ b/srcStatic/GameModules/ConsoleModule.cpp @@ -8,7 +8,7 @@ ConsoleModule::ConsoleModule(const std::string& moduleName) : DllModule(moduleName), moduleDirectory(moduleName + "\\") { - auto absoluteModuleDirectory = fs::path(GetExeDirectory()) / moduleDirectory; + auto absoluteModuleDirectory = fs::path(GetOpuDirectory()) / moduleDirectory; if (!IsDirectory(absoluteModuleDirectory.string())) { throw std::runtime_error("Unable to access the provided module directory: " + diff --git a/srcStatic/GameModules/DllModule.cpp b/srcStatic/GameModules/DllModule.cpp index dc6ccc8..6475c36 100644 --- a/srcStatic/GameModules/DllModule.cpp +++ b/srcStatic/GameModules/DllModule.cpp @@ -1,5 +1,6 @@ #include "DllModule.h" #include "../Log.h" +#include "../FsInclude.h" #include "../WindowsErrorCode.h" #include @@ -10,7 +11,8 @@ DllModule::DllModule(const std::string& moduleName) void DllModule::LoadModuleDll(const std::string& dllPath) { // Try to load a DLL with the given name (possibly an empty string) - moduleDllHandle.reset(LoadLibraryA(dllPath.c_str())); + moduleDllHandle.reset( + LoadLibraryExA(dllPath.c_str(), NULL, fs::path(dllPath).is_absolute() ? LOAD_WITH_ALTERED_SEARCH_PATH : 0)); if (!moduleDllHandle) { throw std::runtime_error( @@ -29,9 +31,9 @@ void DllModule::DetectExportedModuleFunctions() loadModuleFunctionConsole = GetExportAddress("mod_init"); } - unloadModuleFunctionIni = GetExportAddress("DestroyMod"); - if (!unloadModuleFunctionIni) { - unloadModuleFunctionConsole = GetExportAddress("mod_destroy"); + unloadModuleFunction = GetExportAddress("DestroyMod"); + if (!unloadModuleFunction) { + unloadModuleFunction = GetExportAddress("mod_destroy"); } runModuleFunction = GetExportAddress("RunMod"); @@ -54,14 +56,8 @@ bool DllModule::Unload() { bool success = true; - if (unloadModuleFunctionIni) { - unloadModuleFunctionIni(); - } - else if (unloadModuleFunctionConsole) { - success = unloadModuleFunctionConsole(); - if (!success) { - LogMessage("Module reports error during unload: " + Name()); - } + if (unloadModuleFunction) { + unloadModuleFunction(); } moduleDllHandle.reset(nullptr); diff --git a/srcStatic/GameModules/DllModule.h b/srcStatic/GameModules/DllModule.h index d4414fc..7c3e79e 100644 --- a/srcStatic/GameModules/DllModule.h +++ b/srcStatic/GameModules/DllModule.h @@ -29,8 +29,7 @@ class DllModule : public GameModule // The exported naming schema is different between console and ini functions using LoadModuleFunctionIni = void(*)(const char* iniSectionName); using LoadModuleFunctionConsole = void(*)(); - using UnloadModuleFunctionIni = void(*)(); - using UnloadModuleFunctionConsole = bool(*)(); + using UnloadModuleFunction = void(*)(); using RunModuleFunction = void(*)(); // Search for dll's initialization, run & destroy functions @@ -44,7 +43,6 @@ class DllModule : public GameModule LoadModuleFunctionIni loadModuleFunctionIni = nullptr; LoadModuleFunctionConsole loadModuleFunctionConsole = nullptr; - UnloadModuleFunctionIni unloadModuleFunctionIni = nullptr; - UnloadModuleFunctionConsole unloadModuleFunctionConsole = nullptr; + UnloadModuleFunction unloadModuleFunction = nullptr; RunModuleFunction runModuleFunction = nullptr; }; diff --git a/srcStatic/GameModules/EarthworkerProximityTasking.cpp b/srcStatic/GameModules/EarthworkerProximityTasking.cpp deleted file mode 100644 index 64ad221..0000000 --- a/srcStatic/GameModules/EarthworkerProximityTasking.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "EarthworkerProximityTasking.h" -#include "../OP2Memory.h" - -EarthworkerProximityTasking::EarthworkerProximityTasking() : GameModule("EarthworkerProximityTasking") { } - -void EarthworkerProximityTasking::Load() -{ - constexpr unsigned short earthworkerFix = 0x9090; - - Op2MemCopy(0x438E07, sizeof(earthworkerFix), &earthworkerFix); -} diff --git a/srcStatic/GameModules/EarthworkerProximityTasking.h b/srcStatic/GameModules/EarthworkerProximityTasking.h deleted file mode 100644 index 14d545a..0000000 --- a/srcStatic/GameModules/EarthworkerProximityTasking.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "../GameModule.h" - -// Allows designating walls for construction in a 3x3 area around where -// other earthworkers are already working and next to lava flows. -// Adapted from Caught in the Crossfire 2 mission code - -class EarthworkerProximityTasking : public GameModule -{ -public: - EarthworkerProximityTasking(); - - void Load() override; - bool Unload() override { return true; } -}; diff --git a/srcStatic/GameModules/IniModule.cpp b/srcStatic/GameModules/IniModule.cpp index c10a8a7..e2a4135 100644 --- a/srcStatic/GameModules/IniModule.cpp +++ b/srcStatic/GameModules/IniModule.cpp @@ -1,5 +1,6 @@ #include "IniModule.h" #include "../FsInclude.h" +#include "../FileSystemHelper.h" #include #include @@ -9,7 +10,10 @@ IniModule::IniModule(IniSection iniSection) { try { // Get the DLL name from the corresponding section - LoadModuleDll(DllName()); + const fs::path path = DllName(); + if (path.empty() == false) { + LoadModuleDll((path.is_absolute() ? path : GetOpuDirectory() / path).string()); + } } catch (const std::exception& error) { throw std::runtime_error("Unable to load dll for module " + Name() + ". " + std::string(error.what())); @@ -19,12 +23,17 @@ IniModule::IniModule(IniSection iniSection) std::string IniModule::Directory() { - auto dllSetting = DllName(); - return fs::path(dllSetting).remove_filename().string(); + fs::path iniSetting(iniSection.GetValue("Path")); + + if (iniSetting.empty()) { + iniSetting = DllName(); + iniSetting.remove_filename(); + } + + return iniSetting.string(); } std::string IniModule::DllName() { - auto defaultDllName = (fs::path(Name()) / "op2mod.dll").string(); - return iniSection.GetValue("Dll", defaultDllName); + return iniSection.GetValue("Dll"); } diff --git a/srcStatic/GetCommandLineArguments.cpp b/srcStatic/GetCommandLineArguments.cpp index 5109d16..b012b5f 100644 --- a/srcStatic/GetCommandLineArguments.cpp +++ b/srcStatic/GetCommandLineArguments.cpp @@ -1,7 +1,9 @@ #include "GetCommandLineArguments.h" #include "StringConversion.h" #include "LocalResource.h" -#include // Cannot use WIN32_LEAN_AND_MEAN (it does not contain CommandLineToArgvW) +#define WIN32_LEAN_AND_MEAN +#include +#include #include diff --git a/srcStatic/Log/LoggerFile.cpp b/srcStatic/Log/LoggerFile.cpp index f17b9d5..d778c61 100644 --- a/srcStatic/Log/LoggerFile.cpp +++ b/srcStatic/Log/LoggerFile.cpp @@ -6,7 +6,7 @@ LoggerFile::LoggerFile() : - logFile(GetExeDirectory() + "\\Outpost2Log.txt", std::ios::app | std::ios::out | std::ios::binary) + logFile(GetOpuDirectory() + "\\Outpost2Log.txt", std::ios::app | std::ios::out | std::ios::binary) { if (!logFile.is_open()) { LogError("Unable to create or open Outpost2Log.txt"); diff --git a/srcStatic/ModuleLoader.cpp b/srcStatic/ModuleLoader.cpp index 983371d..a2d8cef 100644 --- a/srcStatic/ModuleLoader.cpp +++ b/srcStatic/ModuleLoader.cpp @@ -1,6 +1,5 @@ #include "ModuleLoader.h" #include "GameModules/IpDropDown.h" -#include "GameModules/EarthworkerProximityTasking.h" #include "GameModules/ConsoleModule.h" #include "GameModules/IniModule.h" #include "StringConversion.h" @@ -29,13 +28,9 @@ void ModuleLoader::RegisterBuiltInModules() { const std::string sectionName("BuiltInModules"); - if (IsModuleRequested(sectionName, "IPDropDown")) { + if (IsModuleRequested(sectionName, "IPDropDown", true)) { RegisterModule(std::make_unique()); } - - if (IsModuleRequested(sectionName, "EarthworkerProximityTasking")) { - RegisterModule(std::make_unique()); - } } void ModuleLoader::RegisterConsoleModules() @@ -55,8 +50,11 @@ void ModuleLoader::RegisterConsoleModules() for (const auto& moduleName : consoleModuleNames) { try { - auto consoleModule = std::make_unique(moduleName); - moduleDirectories.push_back((fs::path(GetExeDirectory()) / consoleModule->Directory()).string()); + auto consoleModule = std::make_unique(moduleName); + const fs::path modulePath = fs::path(GetOpuDirectory()) / consoleModule->Directory(); + + moduleDirectories.push_back(modulePath.string()); + AddOsSearchPaths({ modulePath }); // ** TODO moduleDirectories should be vector, then this can be outside RegisterModule(std::move(consoleModule)); } @@ -75,7 +73,18 @@ void ModuleLoader::RegisterIniModules() if (IsModuleRequested("ExternalModules", moduleName)) { try { - RegisterModule(std::make_unique(iniFile[moduleName])); + auto iniModule = std::make_unique(iniFile[moduleName]); + + fs::path modulePath = iniModule->Directory(); + if (modulePath.is_relative()) { + modulePath = GetOpuDirectory() / modulePath; + } + + if ((modulePath != GetExeDirectory()) && (modulePath != GetOpuDirectory())) { + AddOsSearchPaths({ modulePath }); // ** TODO this function should build a vector of moduleDirectories as well + } + + RegisterModule(std::move(iniModule)); } catch (const std::exception& e) { LogError("Unable to load ini module " + moduleName + ". " + e.what()); @@ -84,16 +93,19 @@ void ModuleLoader::RegisterIniModules() } } -bool ModuleLoader::IsModuleRequested(const std::string& sectionName, const std::string& moduleName) +bool ModuleLoader::IsModuleRequested(const std::string& sectionName, const std::string& moduleName, bool defaultValue) { const auto isModuleRequested = ToLower(iniFile.GetValue(sectionName, moduleName)); if (isModuleRequested == "yes") { return true; } - else if (isModuleRequested == "no" || isModuleRequested == "") { + else if (isModuleRequested == "no") { return false; } + else if (isModuleRequested == "") { + return defaultValue; + } LogError("Module named " + moduleName + " contains an innapropriate setting of " + isModuleRequested + ". It must be set to Yes or No"); return false; @@ -149,22 +161,14 @@ void ModuleLoader::RegisterModule(std::unique_ptr newGameModule) void ModuleLoader::LoadModules() { - RegisterBuiltInModules(); - RegisterIniModules(); - RegisterConsoleModules(); - - // Abort early to avoid hooking file search path if not needed - if (modules.empty()) { - return; - } - // Setup loading of additional resources from module folders ResourceSearchPath::Activate(); + RegisterBuiltInModules(); + RegisterIniModules(); + RegisterConsoleModules(); - - for (auto& gameModule : modules) - { + for (auto& gameModule : modules){ try { gameModule->Load(); } @@ -178,8 +182,7 @@ bool ModuleLoader::UnloadModules() { bool areAllModulesProperlyDestroyed = true; - for (auto& gameModule : modules) - { + for (auto& gameModule : modules) { try { bool isModuleProperlyDestroyed = gameModule->Unload(); diff --git a/srcStatic/ModuleLoader.h b/srcStatic/ModuleLoader.h index 25162d5..8656fd3 100644 --- a/srcStatic/ModuleLoader.h +++ b/srcStatic/ModuleLoader.h @@ -36,8 +36,8 @@ class ModuleLoader std::vector> modules; void RegisterBuiltInModules(); - // Console module names are the relative path from the game folder (no trailing slash) + // Console module names are the relative path from the OPU folder (no trailing slash) void RegisterConsoleModules(); void RegisterIniModules(); - bool IsModuleRequested(const std::string& sectionName, const std::string& moduleName); + bool IsModuleRequested(const std::string& sectionName, const std::string& moduleName, bool defaultValue = false); }; diff --git a/srcStatic/ResourceSearchPath.cpp b/srcStatic/ResourceSearchPath.cpp index 98be1d3..221884f 100644 --- a/srcStatic/ResourceSearchPath.cpp +++ b/srcStatic/ResourceSearchPath.cpp @@ -1,5 +1,7 @@ #include "ResourceSearchPath.h" #include "StringConversion.h" +#include "FSInclude.h" +#include "FileSystemHelper.h" #include "OP2Memory.h" #include "Log.h" #define WIN32_LEAN_AND_MEAN @@ -8,6 +10,29 @@ #include +static std::wstring GetPathEnv() +{ + std::unique_ptr pTmp(new wchar_t[_MAX_ENV]); + pTmp[0] = L'\0'; + GetEnvironmentVariableW(L"PATH", pTmp.get(), _MAX_ENV); + return std::wstring(pTmp.get()); +} + +static void SetPathEnv(const std::wstring_view& value) +{ + SetEnvironmentVariableW(L"PATH", value.data()); +} + +void AddOsSearchPaths(const std::vector& paths) +{ + std::wstring pathEnv = GetPathEnv(); + for (const auto& path : paths) { + pathEnv.insert(0, path.wstring() + (pathEnv.empty() ? L"" : L";")); + } + SetPathEnv(pathEnv); +} + + // For compatibility with Outpost2.exe's built in class class ResManager { public: @@ -25,6 +50,9 @@ void ResourceSearchPath::Set(std::vector paths) void ResourceSearchPath::Activate() { HookFileSearchPath(); + + // Add Outpost2 and OPU directories to PATH so module DLLs loaded from subdirs can locate DLL dependencies there. + AddOsSearchPaths({ GetExeDirectory(), GetOpuDirectory() }); } @@ -75,17 +103,32 @@ bool ResourceSearchPath::CallOriginalGetFilePath(const char* resourceName, /* [o bool ResManager::GetFilePath(const char* resourceName, /* [out] */ char* filePath) const { - // Get access to private static - auto moduleDirectories = ResourceSearchPath::ModuleDirectories(); - - for (const auto& moduleDirectory : moduleDirectories) { - // Search for resource in module folder - const auto path = moduleDirectory + resourceName; - if (INVALID_FILE_ATTRIBUTES != GetFileAttributesA(path.c_str())) { - if (0 == CopyStringViewIntoCharBuffer(path, filePath, MAX_PATH)) { - return true; // Resource found - } else { - LogMessage("MAX_PATH exceeded while trying to return path to resource: " + std::string(resourceName)); + constexpr auto SearchOptions = fs::directory_options::follow_directory_symlink | + fs::directory_options::skip_permission_denied; + + auto CheckResult = [filePath](const fs::path& curPath) { + const std::string str = curPath.string(); + bool result = Exists(str); + if (result && (CopyStringViewIntoCharBuffer(str, filePath, MAX_PATH) != 0)) { + LogMessage("MAX_PATH exceeded while trying to return path to resource: " + curPath.filename().string()); + result = false; + } + return result; + }; + + const std::string exeDirectory = GetExeDirectory(); + const std::string opuDirectory = GetOpuDirectory(); + + for (const auto& moduleDirectory : ResourceSearchPath::ModuleDirectories()) { + if ((moduleDirectory != exeDirectory) && (moduleDirectory != opuDirectory)) { + // Search for resource in module folder + if (CheckResult(fs::path(moduleDirectory) / resourceName)) { + return true; + } + else for (const auto& entry : fs::recursive_directory_iterator(moduleDirectory, SearchOptions)) { + if (IsDirectory(entry.path().string()) && CheckResult(entry / resourceName)) { + return true; + } } } } diff --git a/srcStatic/ResourceSearchPath.h b/srcStatic/ResourceSearchPath.h index 754b09e..b5804d3 100644 --- a/srcStatic/ResourceSearchPath.h +++ b/srcStatic/ResourceSearchPath.h @@ -2,10 +2,15 @@ #include #include +#include "FsInclude.h" class ResManager; +// Adds OS DLL search paths by prepending to PATH environment variable. +void AddOsSearchPaths(const std::vector& paths); + + class ResourceSearchPath { public: static void Set(std::vector paths); diff --git a/srcStatic/op2extStatic.vcxproj b/srcStatic/op2extStatic.vcxproj index b2dbf79..32fed56 100644 --- a/srcStatic/op2extStatic.vcxproj +++ b/srcStatic/op2extStatic.vcxproj @@ -1,186 +1,186 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - - - - {4F82DF84-DE4A-4AE0-9E05-FC5408AE23BA} - - - - StaticLibrary - v142 - false - MultiByte - - - StaticLibrary - v142 - false - MultiByte - - - - - - - - - - - - - - - false - - - false - - - - MultiThreaded - Default - true - true - MinSpace - true - Level3 - WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - stdcpp17 - - - true - NDEBUG;%(PreprocessorDefinitions) - true - Win32 - - - 0x0409 - NDEBUG;%(PreprocessorDefinitions) - - - true - - - true - true - Console - false - %(AdditionalDependencies) - false - true - 0x10000000 - false - - - wsock32.lib - - - - - MultiThreadedDebug - Default - false - Disabled - true - Level3 - false - WIN32;DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - stdcpp17 - - - true - DEBUG;%(PreprocessorDefinitions) - true - Win32 - - - 0x0409 - DEBUG;%(PreprocessorDefinitions) - - - true - - - true - true - true - Console - false - %(AdditionalDependencies) - false - true - 0x10000000 - false - - - wsock32.lib - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + Debug + Win32 + + + Release + Win32 + + + + + + {4F82DF84-DE4A-4AE0-9E05-FC5408AE23BA} + + + + StaticLibrary + v142 + false + MultiByte + + + StaticLibrary + v142 + false + MultiByte + + + + + + + + + + + + + + + false + + + false + + + + MultiThreaded + Default + true + true + MinSpace + true + Level3 + WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdcpp17 + /Zc:threadSafeInit- %(AdditionalOptions) + + + true + NDEBUG;%(PreprocessorDefinitions) + true + Win32 + + + 0x0409 + NDEBUG;%(PreprocessorDefinitions) + + + true + + + true + true + Console + false + %(AdditionalDependencies) + false + true + 0x10000000 + false + + + wsock32.lib + + + + + MultiThreadedDebug + Default + false + Disabled + true + Level3 + false + WIN32;DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + stdcpp17 + /Zc:threadSafeInit- %(AdditionalOptions) + + + true + DEBUG;%(PreprocessorDefinitions) + true + Win32 + + + 0x0409 + DEBUG;%(PreprocessorDefinitions) + + + true + + + true + true + true + Console + false + %(AdditionalDependencies) + false + true + 0x10000000 + false + + + wsock32.lib + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/srcStatic/op2extStatic.vcxproj.filters b/srcStatic/op2extStatic.vcxproj.filters index 7566f91..175dc7a 100644 --- a/srcStatic/op2extStatic.vcxproj.filters +++ b/srcStatic/op2extStatic.vcxproj.filters @@ -1,102 +1,96 @@ - - - - - - - - - - - - - - - - - GameModules - - - GameModules - - - GameModules - - - GameModules - - - Log - - - Log - - - Log - - - Log - - - GameModules - - - - - - - - - - - - - - - - - - - - - GameModules - - - GameModules - - - GameModules - - - GameModules - - - - - Log - - - Log - - - Log - - - Log - - - Log - - - GameModules - - - - - {ec8f43e7-0e90-410c-9166-06a4cdbe9270} - - - {11efe594-ddbc-46f2-9463-db850f3e9526} - - + + + + + + + + + + + + + + + + + GameModules + + + GameModules + + + GameModules + + + GameModules + + + Log + + + Log + + + Log + + + Log + + + + + + + + + + + + + + + + + + + + + GameModules + + + GameModules + + + GameModules + + + GameModules + + + + + Log + + + Log + + + Log + + + Log + + + Log + + + + + {ec8f43e7-0e90-410c-9166-06a4cdbe9270} + + + {11efe594-ddbc-46f2-9463-db850f3e9526} + + \ No newline at end of file diff --git a/test/ConsoleArgumentParser.test.cpp b/test/ConsoleArgumentParser.test.cpp index 427a08a..f0f6408 100644 --- a/test/ConsoleArgumentParser.test.cpp +++ b/test/ConsoleArgumentParser.test.cpp @@ -4,11 +4,60 @@ #include -TEST(ConsoleArgumentParser, NoArgument) { +TEST(ConsoleArgumentParser, GetOptionNoArguments) { + const std::vector arguments; + + EXPECT_EQ(arguments.end(), GetCommandOption(arguments.begin(), arguments.end(), "option")); +} + +TEST(ConsoleArgumentParser, OptionExistsNoArguments) { + const std::vector arguments; + + EXPECT_FALSE(CommandOptionExists(arguments.begin(), arguments.end(), "option")); +} + +TEST(ConsoleArgumentParser, GetOptionWellFormed) { + const std::vector arguments = { "/option1", "a", "-option2", "b c" }; + const auto it = GetCommandOption(arguments.begin(), arguments.end(), "option1"); + + EXPECT_EQ("a", *it); + EXPECT_EQ("b c", *GetCommandOption(it, arguments.end(), "option2")); + EXPECT_EQ(arguments.end(), GetCommandOption(arguments.begin(), arguments.end(), "option3")); +} + +TEST(ConsoleArgumentParser, OptionExistsWellFormed) { + const std::vector arguments = { "/option1", "-option2" }; + + EXPECT_TRUE(CommandOptionExists(arguments.begin(), arguments.end(), "option1")); + EXPECT_TRUE(CommandOptionExists(arguments.begin(), arguments.end(), "option2")); + EXPECT_FALSE(CommandOptionExists(arguments.begin(), arguments.end(), "option3")); +} + +TEST(ConsoleArgumentParser, GetOptionNoSwitchArgument) { + const std::vector arguments = { "/option" }; + + EXPECT_TRUE(CommandOptionExists(arguments.begin(), arguments.end(), "option")); + EXPECT_EQ(arguments.end(), GetCommandOption(arguments.begin(), arguments.end(), "option")); +} + +TEST(ConsoleArgumentParser, GetOptionNoSwitchPrefix) { + const std::vector arguments = { "option", "argument" }; + + EXPECT_EQ(arguments.end(), GetCommandOption(arguments.begin(), arguments.end(), "option")); +} + +TEST(ConsoleArgumentParser, OptionExistsNoSwitchPrefix) { + const std::vector arguments = { "option" }; + + EXPECT_FALSE(CommandOptionExists(arguments.begin(), arguments.end(), "option")); +} + + +TEST(ConsoleArgumentParser, FindDirectoriesNoArgument) { EXPECT_TRUE(FindModuleDirectories({ }).empty()); } -TEST(ConsoleArgumentParser, WellFormedNoSpaces) +TEST(ConsoleArgumentParser, FindDirectoriesWellFormedNoSpaces) { const std::string path("path"); const auto directories = FindModuleDirectories({ "/loadmod", path }); @@ -17,7 +66,7 @@ TEST(ConsoleArgumentParser, WellFormedNoSpaces) EXPECT_EQ(path, directories[0]); } -TEST(ConsoleArgmunetParser, WellFormedSpaces) +TEST(ConsoleArgumentParser, FindDirectoriesWellFormedSpaces) { const std::string path("path with spaces"); const auto directories = FindModuleDirectories({ "/loadmod", path }); @@ -27,19 +76,23 @@ TEST(ConsoleArgmunetParser, WellFormedSpaces) } -TEST(ConsoleArgumentParser, WrongSwitchName) { - EXPECT_THROW(FindModuleDirectories({ "/WrongSwitch" }), std::runtime_error); +TEST(ConsoleArgumentParser, FindDirectoriesWrongSwitchName) { + EXPECT_TRUE(FindModuleDirectories({ "/WrongSwitch" }).empty()); +} + +TEST(ConsoleArgumentParser, FindDirectoriesNoSwitchArgument) { + EXPECT_TRUE(FindModuleDirectories({ "/loadmod" }).empty()); } -TEST(ConsoleArgumentParser, NoSwitchArgument) { - EXPECT_THROW(FindModuleDirectories({ "/loadmod" }), std::runtime_error); +TEST(ConsoleArgumentParser, FindDirectoriesRepeatedNoSwitchArgument) { + EXPECT_TRUE(FindModuleDirectories({ "/loadmod", "/loadmod", "-loadmod", "-loadmod" }).empty()); } -TEST(ConsoleArgumentParser, MultipleArguments) { +TEST(ConsoleArgumentParser, FindDirectoriesMultipleArguments) { const std::string path1("path1"); const std::string path2("path2"); - const auto directories = FindModuleDirectories({ "/loadmod", path1, path2 }); + const auto directories = FindModuleDirectories({ "/loadmod", path1, "/loadmod", path2 }); EXPECT_EQ(2u, directories.size()); EXPECT_EQ(path1, directories[0]); diff --git a/test/ConsoleModule.test.cpp b/test/ConsoleModule.test.cpp index c05d974..faa51c7 100644 --- a/test/ConsoleModule.test.cpp +++ b/test/ConsoleModule.test.cpp @@ -15,7 +15,7 @@ TEST(ConsoleModuleLoader, ModuleWithoutDLL) // Test will need temporary module directory with no DLL present // Ensure module directory ends with a trailing slash - const auto moduleDirectory = fs::path(GetExeDirectory()) / moduleName; + const auto moduleDirectory = fs::path(GetOpuDirectory()) / moduleName; fs::create_directory(moduleDirectory); const std::string iniFileName{ GetExeDirectory() + "TestIniFile.NonExistentData.ini" }; @@ -47,7 +47,7 @@ TEST(ConsoleModuleLoader, ModuleWithEmptyDLL) // Test will need temporary module directory and invalid DLL file // Ensure module directory ends with a trailing slash - const auto moduleDirectory = fs::path(GetExeDirectory()) / moduleName; + const auto moduleDirectory = fs::path(GetOpuDirectory()) / moduleName; const auto dllFile = moduleDirectory / "op2mod.dll"; // Create temporary module directory @@ -86,12 +86,12 @@ TEST(ConsoleModuleLoader, ModuleWithEmptyDLL) } TEST(ConsoleModuleLoader, MultiModule) { - const auto exeDirectory = fs::path(GetExeDirectory()); + const auto opuDirectory = fs::path(GetOpuDirectory()); const std::vector moduleNames{"Module1", "Module2"}; // Create some empty test module directories for (const auto& moduleName : moduleNames) { - fs::create_directory(exeDirectory / moduleName); + fs::create_directory(opuDirectory / moduleName); } const std::string iniFileName{ GetExeDirectory() + "TestIniFile.NonExistentData.ini" }; @@ -115,6 +115,6 @@ TEST(ConsoleModuleLoader, MultiModule) { // Cleanup test module directories for (const auto& moduleName : moduleNames) { // Use Win API directly since fs::remove doesn't work under Mingw - EXPECT_NE(0, RemoveDirectoryW((exeDirectory / moduleName).wstring().c_str())); + EXPECT_NE(0, RemoveDirectoryW((opuDirectory / moduleName).wstring().c_str())); } } diff --git a/test/LoggerFile.test.cpp b/test/LoggerFile.test.cpp index e99a6de..d179b50 100644 --- a/test/LoggerFile.test.cpp +++ b/test/LoggerFile.test.cpp @@ -4,7 +4,7 @@ #include -const auto logPath = fs::path(GetExeDirectory()).append("Outpost2Log.txt").string(); +const auto logPath = fs::path(GetOpuDirectory()).append("Outpost2Log.txt").string(); TEST(LoggerFile, LogFileExists) From 874c4517e756727fb383340ad2671f35927b48cb Mon Sep 17 00:00:00 2001 From: Arklon <384705-Arklon@users.noreply.gitlab.com> Date: Sat, 5 Dec 2020 00:04:32 -0500 Subject: [PATCH 02/16] Add [OPU]/libs to DLL search paths. --- srcDLL/DllMain.cpp | 60 ++++++++++++++------------------ srcStatic/ResourceSearchPath.cpp | 3 +- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/srcDLL/DllMain.cpp b/srcDLL/DllMain.cpp index ed089f7..b86898a 100644 --- a/srcDLL/DllMain.cpp +++ b/srcDLL/DllMain.cpp @@ -42,42 +42,34 @@ BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*reserved*/) { // This will be called once the program is unpacked and running if ((dwReason == DLL_PROCESS_ATTACH) && CommandOptionExists("OPU")) { - wchar_t filename[MAX_PATH] = L""; - GetModuleFileNameW(NULL, &filename[0], MAX_PATH); - - if (fs::path(filename).filename() == "Outpost2.exe") { - // Setup logging - SetLoggerError(&loggerDistributor); - SetLoggerMessage(&loggerFile); - SetLoggerDebug(&loggerDebug); - - // Construct global objects - vols = std::make_unique>(); - moduleLoader = std::make_unique(); - - // Set load offset for Outpost2.exe module, used during memory patching - // If this fails, it's because Outpost2.exe is not loaded - // Failure means op2ext.dll was loaded by something else, such as a unit test - // For unit tests, just stay in memory, it's not an error if this fails - if (EnableOp2MemoryPatching()) { - // These hooks are needed to further bootstrap the rest of module loading - if (!InstallTAppEventHooks()) { - LogError("Failed to install initial TApp event hooks. Module loading and patching disabled."); - return FALSE; - } - - // Setup event handlers - appEvents.onInit.Add(&OnInit); - appEvents.onLoadShell.Add(&OnLoadShell); - appEvents.onShutDown.Add(&OnShutdown); - - // Set active events - appEvents.Activate(); + // Setup logging + SetLoggerError(&loggerDistributor); + SetLoggerMessage(&loggerFile); + SetLoggerDebug(&loggerDebug); + + // Construct global objects + vols = std::make_unique>(); + moduleLoader = std::make_unique(); + + // Set load offset for Outpost2.exe module, used during memory patching + // If this fails, it's because Outpost2.exe is not loaded + // Failure means op2ext.dll was loaded by something else, such as a unit test + // For unit tests, just stay in memory, it's not an error if this fails + if (EnableOp2MemoryPatching()) { + // These hooks are needed to further bootstrap the rest of module loading + if (!InstallTAppEventHooks()) { + LogError("Failed to install initial TApp event hooks. Module loading and patching disabled."); + return FALSE; } - } - // Disable any more thread attach calls - DisableThreadLibraryCalls(hInstance); + // Setup event handlers + appEvents.onInit.Add(&OnInit); + appEvents.onLoadShell.Add(&OnLoadShell); + appEvents.onShutDown.Add(&OnShutdown); + + // Set active events + appEvents.Activate(); + } } return TRUE; diff --git a/srcStatic/ResourceSearchPath.cpp b/srcStatic/ResourceSearchPath.cpp index 221884f..048bab8 100644 --- a/srcStatic/ResourceSearchPath.cpp +++ b/srcStatic/ResourceSearchPath.cpp @@ -52,7 +52,8 @@ void ResourceSearchPath::Activate() HookFileSearchPath(); // Add Outpost2 and OPU directories to PATH so module DLLs loaded from subdirs can locate DLL dependencies there. - AddOsSearchPaths({ GetExeDirectory(), GetOpuDirectory() }); + const fs::path opuPath(GetOpuDirectory()); + AddOsSearchPaths({ GetExeDirectory(), opuPath, opuPath / "libs" }); } From d1404d06d795d7cf9ed26df19bcbffad86285b4e Mon Sep 17 00:00:00 2001 From: Arklon <384705-Arklon@users.noreply.gitlab.com> Date: Mon, 7 Dec 2020 00:38:02 -0500 Subject: [PATCH 03/16] Fixes to DLL-less ini mods. Added GetModuleDirectoryCount, GetModuleDirectory exported APIs. Added OP2 version check. --- srcDLL/DllMain.cpp | 4 ++-- srcDLL/op2ext.cpp | 17 ++++++++++++++-- srcDLL/op2ext.h | 9 +++++++++ srcStatic/GameModules/IniModule.cpp | 5 +++-- srcStatic/ModuleLoader.cpp | 28 +++++++++++++++++--------- srcStatic/ModuleLoader.h | 4 ++-- srcStatic/OP2Memory.cpp | 30 ++++++++++++++++++++++++++-- srcStatic/OP2Memory.h | 11 +++++----- srcStatic/ResourceSearchPath.cpp | 14 ++++++------- srcStatic/ResourceSearchPath.h | 1 + testDll/op2ext.test.cpp | 31 +++++++++++++++++++++++++++-- 11 files changed, 120 insertions(+), 34 deletions(-) diff --git a/srcDLL/DllMain.cpp b/srcDLL/DllMain.cpp index b86898a..08a4da6 100644 --- a/srcDLL/DllMain.cpp +++ b/srcDLL/DllMain.cpp @@ -105,8 +105,8 @@ bool InstallDepPatch() // Redirects Outpost2.ini to be accessed from the OPU directory. void RedirectIniFile() { - const auto iniPath = GetOutpost2IniPath(); - Op2MemCopy(0x00547090, iniPath.length() + 1, iniPath.data()); // Overwrite gConfigFile.iniPath + // Overwrite gConfigFile.iniPath + CopyStringViewIntoCharBuffer(GetOutpost2IniPath(), static_cast(Op2RelocatePointer(0x00547090)), MAX_PATH); } diff --git a/srcDLL/op2ext.cpp b/srcDLL/op2ext.cpp index 7d78c00..77167c8 100644 --- a/srcDLL/op2ext.cpp +++ b/srcDLL/op2ext.cpp @@ -3,6 +3,7 @@ #include "StringConversion.h" #include "OP2Memory.h" #include "FileSystemHelper.h" +#include "ResourceSearchPath.h" #include "Log.h" #include "WindowsModule.h" #define WIN32_LEAN_AND_MEAN @@ -13,8 +14,8 @@ #include -// Dummy export for linking requirements from Outpost2.exe and OP2Shell.dll. -// Outpost2.exe and OP2Shell.dll reference this dummy entry, causing op2ext.dll to load. +// Dummy export for linking requirements from winmm.dll injector shim loaded by Outpost2.exe. +// The winmm.dll shim references this dummy entry, causing op2ext.dll to load. // It is not used in any way, but must exist to prevent Windows loader errors. extern "C" OP2EXT_API int StubExt; int StubExt = 0; @@ -164,3 +165,15 @@ OP2EXT_API size_t GetLoadedModuleName(size_t moduleIndex, char* buffer, size_t b return CopyStringViewIntoCharBuffer(moduleName, buffer, bufferSize); } + +OP2EXT_API size_t GetModuleDirectoryCount() +{ + return ResourceSearchPath::GetSearchPaths().size(); +} + +OP2EXT_API size_t GetModuleDirectory(size_t moduleIndex, char* buffer, size_t bufferSize) +{ + const auto& searchPaths = ResourceSearchPath::GetSearchPaths(); + const std::string moduleName = (moduleIndex < searchPaths.size()) ? searchPaths[moduleIndex] : ""; + return CopyStringViewIntoCharBuffer(moduleName, buffer, bufferSize); +} diff --git a/srcDLL/op2ext.h b/srcDLL/op2ext.h index 9ace024..b06280f 100644 --- a/srcDLL/op2ext.h +++ b/srcDLL/op2ext.h @@ -86,6 +86,15 @@ OP2EXT_API size_t GetLoadedModuleCount(); // Returns 0 on success. Returns the required minimum size of the buffer on failure. OP2EXT_API size_t GetLoadedModuleName(size_t moduleIndex, char* buffer, size_t bufferSize); +// Returns the number of module directories. +OP2EXT_API size_t GetModuleDirectoryCount(); + +// Retrieves the module directory path at the specified index. +// Use function GetModuleDirectoryCount() to determine how many module directories exist. +// If an index is beyond the loaded module count, returns 0 and clears the buffer. +// Returns 0 on success. Returns the required minimum size of the buffer on failure. +OP2EXT_API size_t GetModuleDirectory(size_t moduleIndex, char* buffer, size_t bufferSize); + #ifdef __cplusplus } // extern "C" #endif diff --git a/srcStatic/GameModules/IniModule.cpp b/srcStatic/GameModules/IniModule.cpp index e2a4135..ac719e1 100644 --- a/srcStatic/GameModules/IniModule.cpp +++ b/srcStatic/GameModules/IniModule.cpp @@ -11,7 +11,7 @@ IniModule::IniModule(IniSection iniSection) try { // Get the DLL name from the corresponding section const fs::path path = DllName(); - if (path.empty() == false) { + if (path.has_filename()) { LoadModuleDll((path.is_absolute() ? path : GetOpuDirectory() / path).string()); } } @@ -23,6 +23,7 @@ IniModule::IniModule(IniSection iniSection) std::string IniModule::Directory() { + // For ini modules, either one of Dll= or Path= should be set. fs::path iniSetting(iniSection.GetValue("Path")); if (iniSetting.empty()) { @@ -35,5 +36,5 @@ std::string IniModule::Directory() std::string IniModule::DllName() { - return iniSection.GetValue("Dll"); + return iniSection.GetValue("Dll", iniSection.SectionName() + "\\"); } diff --git a/srcStatic/ModuleLoader.cpp b/srcStatic/ModuleLoader.cpp index a2d8cef..12fbc37 100644 --- a/srcStatic/ModuleLoader.cpp +++ b/srcStatic/ModuleLoader.cpp @@ -33,7 +33,7 @@ void ModuleLoader::RegisterBuiltInModules() } } -void ModuleLoader::RegisterConsoleModules() +void ModuleLoader::RegisterConsoleModules(std::vector& moduleDirectories) { if (consoleModuleNames.empty()) { return; // No console modules to load @@ -44,8 +44,7 @@ void ModuleLoader::RegisterConsoleModules() return; } - std::vector moduleDirectories; - moduleDirectories.reserve(consoleModuleNames.size()); + moduleDirectories.reserve(moduleDirectories.size() + consoleModuleNames.size()); for (const auto& moduleName : consoleModuleNames) { @@ -63,12 +62,14 @@ void ModuleLoader::RegisterConsoleModules() } } - ResourceSearchPath::Set(std::move(moduleDirectories)); } -void ModuleLoader::RegisterIniModules() +void ModuleLoader::RegisterIniModules(std::vector& moduleDirectories) { - for (const auto moduleName : iniFile.GetKeyNames("ExternalModules")) + const auto keyNames = iniFile.GetKeyNames("ExternalModules"); + moduleDirectories.reserve(moduleDirectories.size() + keyNames.size()); + + for (const auto moduleName : keyNames) { if (IsModuleRequested("ExternalModules", moduleName)) { @@ -79,8 +80,12 @@ void ModuleLoader::RegisterIniModules() if (modulePath.is_relative()) { modulePath = GetOpuDirectory() / modulePath; } + modulePath /= ""; - if ((modulePath != GetExeDirectory()) && (modulePath != GetOpuDirectory())) { + if (((modulePath) != (fs::path(GetExeDirectory()) / "")) && + ((modulePath) != (fs::path(GetOpuDirectory()) / ""))) + { + moduleDirectories.push_back(modulePath.string()); AddOsSearchPaths({ modulePath }); // ** TODO this function should build a vector of moduleDirectories as well } @@ -164,9 +169,14 @@ void ModuleLoader::LoadModules() // Setup loading of additional resources from module folders ResourceSearchPath::Activate(); + std::vector moduleDirectories; + moduleDirectories.reserve(consoleModuleNames.size()); + RegisterBuiltInModules(); - RegisterIniModules(); - RegisterConsoleModules(); + RegisterIniModules(moduleDirectories); + RegisterConsoleModules(moduleDirectories); + + ResourceSearchPath::Set(std::move(moduleDirectories)); for (auto& gameModule : modules){ try { diff --git a/srcStatic/ModuleLoader.h b/srcStatic/ModuleLoader.h index 8656fd3..c2db71e 100644 --- a/srcStatic/ModuleLoader.h +++ b/srcStatic/ModuleLoader.h @@ -37,7 +37,7 @@ class ModuleLoader void RegisterBuiltInModules(); // Console module names are the relative path from the OPU folder (no trailing slash) - void RegisterConsoleModules(); - void RegisterIniModules(); + void RegisterConsoleModules(std::vector& moduleDirectories); + void RegisterIniModules(std::vector& moduleDirectories); bool IsModuleRequested(const std::string& sectionName, const std::string& moduleName, bool defaultValue = false); }; diff --git a/srcStatic/OP2Memory.cpp b/srcStatic/OP2Memory.cpp index 3bc2edf..3a6bb3b 100644 --- a/srcStatic/OP2Memory.cpp +++ b/srcStatic/OP2Memory.cpp @@ -11,6 +11,19 @@ std::uintptr_t loadOffset = 0; constexpr std::uintptr_t ExpectedOutpost2Address = 0x00400000; +// Calls Outpost2.exe's gTApp.Version() exported function. +static uint32_t GetOp2Version(HMODULE moduleHandle) +{ + // Use GetProcAddress instead of load-time importing (needs C++ decorated names). + // We avoid importing since it is possible op2ext.dll could get loaded into non-OP2 processes via the loader shim DLL. + auto*const gTApp = reinterpret_cast(GetProcAddress(moduleHandle, "?gTApp@@3VTApp@@A")); + auto*const GetVersion = + reinterpret_cast(GetProcAddress(moduleHandle, "?Version@TApp@@QAEKXZ")); + + return ((gTApp != nullptr) && (GetVersion != nullptr)) ? GetVersion(gTApp) : 0; +} + + // Enabled patching of Outpost2.exe memory // Adjust offsets in case Outpost2.exe module is relocated // Returns true on success, or false on failure @@ -26,6 +39,13 @@ bool EnableOp2MemoryPatching() return false; } + const uint32_t op2Version = GetOp2Version(moduleHandle); + if ((op2Version < 0x01020007) || (op2Version >= 0x20000000)) { + // op2ext requires Outpost2 1.2.7 (English CD version with official updates 1-3, or GOG). + // Unpatched English CD version is 1.2.5, and unpatched translationed versions are {2-5}.2.{7-9}. + return false; + } + // Convert module handle to module base address // Under Win32 these are the same auto moduleBaseAddress = reinterpret_cast(moduleHandle); @@ -37,13 +57,19 @@ bool EnableOp2MemoryPatching() } +void* Op2RelocatePointer(std::uintptr_t destBaseAddress) +{ + return reinterpret_cast(destBaseAddress + loadOffset); +} + + bool Op2UnprotectMemory(std::uintptr_t destBaseAddress, std::size_t size) { if (!memoryPatchingEnabled) { return false; } - void* destAddress = reinterpret_cast(destBaseAddress + loadOffset); + void*const destAddress = Op2RelocatePointer(destBaseAddress); // Try to unprotect memory DWORD oldAttribute; return VirtualProtect(destAddress, size, PAGE_EXECUTE_READWRITE, &oldAttribute); @@ -57,7 +83,7 @@ bool Op2MemEdit(std::uintptr_t destBaseAddress, std::size_t size, Function memor return false; } - void* destAddress = reinterpret_cast(destBaseAddress + loadOffset); + void*const destAddress = Op2RelocatePointer(destBaseAddress); // Try to unprotect memory DWORD oldAttribute; diff --git a/srcStatic/OP2Memory.h b/srcStatic/OP2Memory.h index a90b1ff..7812326 100644 --- a/srcStatic/OP2Memory.h +++ b/srcStatic/OP2Memory.h @@ -9,11 +9,12 @@ bool EnableOp2MemoryPatching(); -bool Op2UnprotectMemory(std::uintptr_t destBaseAddress, std::size_t size); -bool Op2MemSet(std::uintptr_t destBaseAddress, std::size_t size, unsigned char value); -bool Op2MemCopy(std::uintptr_t destBaseAddress, std::size_t size, const void* sourceAddress); -bool Op2WriteAddress(std::uintptr_t destBaseAddress, const void* newAddress); -bool Op2RelinkCall(std::uintptr_t callInstructionAddress, const void* newFunctionAddress); +void* Op2RelocatePointer(std::uintptr_t destBaseAddress); +bool Op2UnprotectMemory(std::uintptr_t destBaseAddress, std::size_t size); +bool Op2MemSet(std::uintptr_t destBaseAddress, std::size_t size, unsigned char value); +bool Op2MemCopy(std::uintptr_t destBaseAddress, std::size_t size, const void* sourceAddress); +bool Op2WriteAddress(std::uintptr_t destBaseAddress, const void* newAddress); +bool Op2RelinkCall(std::uintptr_t callInstructionAddress, const void* newFunctionAddress); template diff --git a/srcStatic/ResourceSearchPath.cpp b/srcStatic/ResourceSearchPath.cpp index 048bab8..16ab45b 100644 --- a/srcStatic/ResourceSearchPath.cpp +++ b/srcStatic/ResourceSearchPath.cpp @@ -121,16 +121,14 @@ bool ResManager::GetFilePath(const char* resourceName, /* [out] */ char* filePat const std::string opuDirectory = GetOpuDirectory(); for (const auto& moduleDirectory : ResourceSearchPath::ModuleDirectories()) { - if ((moduleDirectory != exeDirectory) && (moduleDirectory != opuDirectory)) { - // Search for resource in module folder - if (CheckResult(fs::path(moduleDirectory) / resourceName)) { + // Search for resource in module folder + if (CheckResult(fs::path(moduleDirectory) / resourceName)) { + return true; + } + else for (const auto& entry : fs::recursive_directory_iterator(moduleDirectory, SearchOptions)) { + if (IsDirectory(entry.path().string()) && CheckResult(entry / resourceName)) { return true; } - else for (const auto& entry : fs::recursive_directory_iterator(moduleDirectory, SearchOptions)) { - if (IsDirectory(entry.path().string()) && CheckResult(entry / resourceName)) { - return true; - } - } } } diff --git a/srcStatic/ResourceSearchPath.h b/srcStatic/ResourceSearchPath.h index b5804d3..ab01c68 100644 --- a/srcStatic/ResourceSearchPath.h +++ b/srcStatic/ResourceSearchPath.h @@ -15,6 +15,7 @@ class ResourceSearchPath { public: static void Set(std::vector paths); static void Activate(); + static const std::vector& GetSearchPaths() { return ModuleDirectories(); }; private: friend ResManager; diff --git a/testDll/op2ext.test.cpp b/testDll/op2ext.test.cpp index dab0d3c..43fc5a8 100644 --- a/testDll/op2ext.test.cpp +++ b/testDll/op2ext.test.cpp @@ -180,7 +180,7 @@ TEST(op2ext, GetLoadedModuleCount) { TEST(op2ext, GetLoadedModuleName) { constexpr char Nonce = 'Z'; - char moduleName[256] = {Nonce}; + char moduleName[256] = { Nonce, '\0' }; // No crash on nullptr buffer // Returns required buffer size (must include space for null terminator) @@ -198,5 +198,32 @@ TEST(op2ext, GetLoadedModuleName) { // Module name plus null terminator should fit within buffer size ASSERT_TRUE(moduleNameView.length() < sizeof(moduleName)); // Module name is null terminated (this is outside the string_view window) - EXPECT_EQ(0, moduleName[moduleNameView.length()]); + EXPECT_EQ('\0', moduleName[moduleNameView.length()]); +} + +TEST(op2ext, GetModuleDirectoryCount) { + EXPECT_EQ(0u, GetModuleDirectoryCount()); +} + +TEST(op2ext, GetModuleDirectory) { + constexpr char Nonce = 'Z'; + char moduleDir[MAX_PATH] = { Nonce, '\0' }; + + // No crash on nullptr buffer + // Returns required buffer size (must include space for null terminator) + EXPECT_NE(0u, GetModuleDirectory(0, nullptr, 0)); + EXPECT_NE(0u, GetModuleDirectory(0, nullptr, sizeof(moduleDir))); + + // Returns needed buffer size when supplied buffer is too small + // Buffer is not modified beyond specified length + EXPECT_NE(0u, GetModuleDirectory(0, moduleDir, 0)); + EXPECT_EQ(Nonce, moduleDir[0]); + + // On success return value is 0, and buffer contains module name + EXPECT_EQ(0u, GetModuleDirectory(0, moduleDir, sizeof(moduleDir))); + std::string_view moduleDirView{moduleDir}; + // Module name plus null terminator should fit within buffer size + ASSERT_TRUE(moduleDirView.length() < sizeof(moduleDir)); + // Module name is null terminated (this is outside the string_view window) + EXPECT_EQ('\0', moduleDir[moduleDirView.length()]); } From ac462d8b58bae8843b1883458d2314525da3b2e4 Mon Sep 17 00:00:00 2001 From: Arklon <384705-Arklon@users.noreply.gitlab.com> Date: Mon, 7 Dec 2020 01:48:00 -0500 Subject: [PATCH 04/16] Change GetFilePath() hook now that OPUPatch is mod directory-aware. --- srcStatic/ResourceSearchPath.cpp | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/srcStatic/ResourceSearchPath.cpp b/srcStatic/ResourceSearchPath.cpp index 16ab45b..f41d30c 100644 --- a/srcStatic/ResourceSearchPath.cpp +++ b/srcStatic/ResourceSearchPath.cpp @@ -104,31 +104,18 @@ bool ResourceSearchPath::CallOriginalGetFilePath(const char* resourceName, /* [o bool ResManager::GetFilePath(const char* resourceName, /* [out] */ char* filePath) const { - constexpr auto SearchOptions = fs::directory_options::follow_directory_symlink | - fs::directory_options::skip_permission_denied; - - auto CheckResult = [filePath](const fs::path& curPath) { - const std::string str = curPath.string(); - bool result = Exists(str); - if (result && (CopyStringViewIntoCharBuffer(str, filePath, MAX_PATH) != 0)) { - LogMessage("MAX_PATH exceeded while trying to return path to resource: " + curPath.filename().string()); - result = false; - } - return result; - }; - - const std::string exeDirectory = GetExeDirectory(); - const std::string opuDirectory = GetOpuDirectory(); - for (const auto& moduleDirectory : ResourceSearchPath::ModuleDirectories()) { // Search for resource in module folder - if (CheckResult(fs::path(moduleDirectory) / resourceName)) { - return true; - } - else for (const auto& entry : fs::recursive_directory_iterator(moduleDirectory, SearchOptions)) { - if (IsDirectory(entry.path().string()) && CheckResult(entry / resourceName)) { + const fs::path curPath = fs::path(moduleDirectory) / resourceName; + const std::string str = curPath.string(); + + if (Exists(str)) { + if (CopyStringViewIntoCharBuffer(str, filePath, MAX_PATH) == 0) { return true; } + else { + LogMessage("MAX_PATH exceeded while trying to return path to resource: " + curPath.filename().string()); + } } } From d32f11cbacb61d364902dd51dec42b1590695301 Mon Sep 17 00:00:00 2001 From: Arklon <384705-Arklon@users.noreply.gitlab.com> Date: Mon, 7 Dec 2020 01:50:57 -0500 Subject: [PATCH 05/16] Removed outdated comment in ModuleLoader.cpp. --- srcStatic/ModuleLoader.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/srcStatic/ModuleLoader.cpp b/srcStatic/ModuleLoader.cpp index 12fbc37..b55ca7f 100644 --- a/srcStatic/ModuleLoader.cpp +++ b/srcStatic/ModuleLoader.cpp @@ -82,11 +82,9 @@ void ModuleLoader::RegisterIniModules(std::vector& moduleDirectorie } modulePath /= ""; - if (((modulePath) != (fs::path(GetExeDirectory()) / "")) && - ((modulePath) != (fs::path(GetOpuDirectory()) / ""))) - { + if ((modulePath != (fs::path(GetExeDirectory()) / "")) && (modulePath != (fs::path(GetOpuDirectory()) / ""))) { moduleDirectories.push_back(modulePath.string()); - AddOsSearchPaths({ modulePath }); // ** TODO this function should build a vector of moduleDirectories as well + AddOsSearchPaths({ modulePath }); } RegisterModule(std::move(iniModule)); From 77fd2f2d746e7eb703631d2eac3e65a7f6795aa6 Mon Sep 17 00:00:00 2001 From: Arklon <384705-Arklon@users.noreply.gitlab.com> Date: Mon, 7 Dec 2020 01:52:56 -0500 Subject: [PATCH 06/16] Update comment in op2ext.h to reflect module dirs being relative to Outpost2/OPU. --- srcDLL/op2ext.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srcDLL/op2ext.h b/srcDLL/op2ext.h index b06280f..d4bc23c 100644 --- a/srcDLL/op2ext.h +++ b/srcDLL/op2ext.h @@ -81,7 +81,7 @@ OP2EXT_API size_t GetLoadedModuleCount(); // Use function GetLoadedModuleCount to determine how many module names exist. // If an index is beyond the loaded module count, returns 0 and clears the buffer. // A console module's name is the directory name parameter passed in via the /loadmod command. -// The directory name is relative to the executable's folder, with no trailing slash +// The directory name is relative to the OPU folder, with no trailing slash // An ini module name is the module's [section name] within the ini file. // Returns 0 on success. Returns the required minimum size of the buffer on failure. OP2EXT_API size_t GetLoadedModuleName(size_t moduleIndex, char* buffer, size_t bufferSize); From 303ee13005157edf482f7d5023ab658da3c8dd19 Mon Sep 17 00:00:00 2001 From: Arklon <384705-Arklon@users.noreply.gitlab.com> Date: Mon, 7 Dec 2020 02:12:41 -0500 Subject: [PATCH 07/16] Add [mod dir]/libs to OS DLL search paths. --- srcStatic/ModuleLoader.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/srcStatic/ModuleLoader.cpp b/srcStatic/ModuleLoader.cpp index b55ca7f..9fd11c3 100644 --- a/srcStatic/ModuleLoader.cpp +++ b/srcStatic/ModuleLoader.cpp @@ -53,7 +53,8 @@ void ModuleLoader::RegisterConsoleModules(std::vector& moduleDirect const fs::path modulePath = fs::path(GetOpuDirectory()) / consoleModule->Directory(); moduleDirectories.push_back(modulePath.string()); - AddOsSearchPaths({ modulePath }); // ** TODO moduleDirectories should be vector, then this can be outside + // ** TODO moduleDirectories should be vector, then this can be outside + AddOsSearchPaths({ modulePath, modulePath / "libs" }); RegisterModule(std::move(consoleModule)); } @@ -84,7 +85,7 @@ void ModuleLoader::RegisterIniModules(std::vector& moduleDirectorie if ((modulePath != (fs::path(GetExeDirectory()) / "")) && (modulePath != (fs::path(GetOpuDirectory()) / ""))) { moduleDirectories.push_back(modulePath.string()); - AddOsSearchPaths({ modulePath }); + AddOsSearchPaths({ modulePath, modulePath / "libs" }); } RegisterModule(std::move(iniModule)); From 9ff800e8acd722edb84c0631d88a293a81491fe9 Mon Sep 17 00:00:00 2001 From: Arklon <384705-Arklon@users.noreply.gitlab.com> Date: Sun, 17 Jan 2021 21:55:44 -0500 Subject: [PATCH 08/16] Bump version to 3.1.0. Update ReadMe.txt. Add license. --- LICENSE | 33 +++++++++++++++++++++++++++++++++ ReadMe.txt | 8 +++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a36efeb --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ +BSD 3-Clause License + +Copyright (c) 2021, Outpost Universe +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +THIS MATERIAL IS NOT MADE OR SUPPORTED BY ACTIVISION. + +Outpost 2 is (c) 1997, Activision Publishing, Inc. and its affiliates ("Activision"). diff --git a/ReadMe.txt b/ReadMe.txt index 997310f..cf19fff 100644 --- a/ReadMe.txt +++ b/ReadMe.txt @@ -125,6 +125,13 @@ Order of vol file precedence is below: Change Log ------------------------------------------ +Version 3.1.0 + * Check if the base module is Outpost2.exe and that the game version is valid before attempting to activate patches + * Look for Outpost2.ini and other OPU modded game files under the "Outpost2\OPU" directory instead of "Outpost2" + * Removed Earthworker Proximity Tasking as a Built in Module + * Add external API functions GetModuleDirectoryCount and GetModuleDirectory to get loaded module directories + * Changed how multiple console mods can be loaded - you should now specify "/loadmod" multiple times + Version 3.0.0 * Remove public functions IsConsoleModuleLoaded & IsIniModuleLoaded @@ -159,7 +166,6 @@ LoadAddons = "NetFix, NetHelper" Add the following sections to the ini file: [BuiltInModules] -EarthworkerProximityTasking = yes IPDropDown = yes [ExternalModules] From c707488bfca9b338f7a0d404639e2afe742754ec Mon Sep 17 00:00:00 2001 From: Arklon <384705-Arklon@users.noreply.gitlab.com> Date: Fri, 12 Feb 2021 23:24:33 -0500 Subject: [PATCH 09/16] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index a36efeb..d1cc2f8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2021, Outpost Universe +Copyright (c) 2021, The Outpost Universe All rights reserved. Redistribution and use in source and binary forms, with or without From cd4d9c501794e0c0b66e4158cf19a4df9cfc6d2f Mon Sep 17 00:00:00 2001 From: Arklon <16932296+Arklon1@users.noreply.github.com> Date: Sat, 13 Feb 2021 19:53:43 -0500 Subject: [PATCH 10/16] Fix GCC build errors --- srcStatic/ConsoleArgumentParser.cpp | 4 +++- srcStatic/StringConversion.cpp | 13 +++++++++++++ srcStatic/StringConversion.h | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/srcStatic/ConsoleArgumentParser.cpp b/srcStatic/ConsoleArgumentParser.cpp index 0c3738f..adc5b77 100644 --- a/srcStatic/ConsoleArgumentParser.cpp +++ b/srcStatic/ConsoleArgumentParser.cpp @@ -3,10 +3,12 @@ #include "StringConversion.h" #include "Log.h" +#include + static CommandIterator FindCommandSwitch(CommandIterator begin, CommandIterator end, const std::string& option) { return std::find_if(begin, end, [&option](const std::string& argument) - { return ((argument[0] == '/') || (argument[0] == '-')) && (_stricmp(&argument[1], option.data()) == 0); }); + { return ((argument[0] == '/') || (argument[0] == '-')) && (StringInsensitiveCompare(&argument[1], option) == 0); }); } diff --git a/srcStatic/StringConversion.cpp b/srcStatic/StringConversion.cpp index ac04623..e9b6c00 100644 --- a/srcStatic/StringConversion.cpp +++ b/srcStatic/StringConversion.cpp @@ -9,6 +9,10 @@ #include // std::chrono::system_clock::now #include // gmtime +#ifdef __GNUC__ +#include +#endif + std::string WrapRawString(const char* str) { @@ -64,6 +68,15 @@ std::string ToLower(std::string x) { return x; } +int StringInsensitiveCompare(const std::string_view& left, const std::string_view& right) +{ +#ifdef __GNUC__ + return strcasecmp(left.data(), right.data()); +#else + return _stricmp(left.data(), right.data()); +#endif +} + std::string AddrToHexString(std::uintptr_t addr) { diff --git a/srcStatic/StringConversion.h b/srcStatic/StringConversion.h index 6e7ca25..a26e407 100644 --- a/srcStatic/StringConversion.h +++ b/srcStatic/StringConversion.h @@ -31,6 +31,9 @@ std::string& ToLowerInPlace(std::string& x); // Returns a new string where all characters have been converted to lower case std::string ToLower(std::string x); +// Lexographically compares two strings in a case-insensitive manner +int StringInsensitiveCompare(const std::string_view& left, const std::string_view& right); + // Convert hex address value to string std::string AddrToHexString(std::uintptr_t addr); From 20c64d796928cc77cf31b9bc535b277b4413ed2e Mon Sep 17 00:00:00 2001 From: Arklon <16932296+Arklon1@users.noreply.github.com> Date: Sat, 13 Feb 2021 19:57:08 -0500 Subject: [PATCH 11/16] Fix more GCC build errors --- srcStatic/OP2Memory.cpp | 2 +- srcStatic/ResourceSearchPath.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/srcStatic/OP2Memory.cpp b/srcStatic/OP2Memory.cpp index 3a6bb3b..c05b250 100644 --- a/srcStatic/OP2Memory.cpp +++ b/srcStatic/OP2Memory.cpp @@ -18,7 +18,7 @@ static uint32_t GetOp2Version(HMODULE moduleHandle) // We avoid importing since it is possible op2ext.dll could get loaded into non-OP2 processes via the loader shim DLL. auto*const gTApp = reinterpret_cast(GetProcAddress(moduleHandle, "?gTApp@@3VTApp@@A")); auto*const GetVersion = - reinterpret_cast(GetProcAddress(moduleHandle, "?Version@TApp@@QAEKXZ")); + reinterpret_cast(GetProcAddress(moduleHandle, "?Version@TApp@@QAEKXZ")); return ((gTApp != nullptr) && (GetVersion != nullptr)) ? GetVersion(gTApp) : 0; } diff --git a/srcStatic/ResourceSearchPath.cpp b/srcStatic/ResourceSearchPath.cpp index f41d30c..c2a2183 100644 --- a/srcStatic/ResourceSearchPath.cpp +++ b/srcStatic/ResourceSearchPath.cpp @@ -1,6 +1,6 @@ #include "ResourceSearchPath.h" #include "StringConversion.h" -#include "FSInclude.h" +#include "FsInclude.h" #include "FileSystemHelper.h" #include "OP2Memory.h" #include "Log.h" From 784655663e3354957a260a10b5bf9f495f704c47 Mon Sep 17 00:00:00 2001 From: Brett208 <15183337+Brett208@users.noreply.github.com> Date: Sat, 20 Feb 2021 20:50:00 -0500 Subject: [PATCH 12/16] Fix unit test ConsoleModuleLoader.ModuleWithoutDLL Two problems were noted: 1. std::filesystem::create_directory was trying to create a directory and its parent directory, which is not allowed. The parent directory must be created first. This was caused by adding the OPU directory inbetween the Outpost 2 directory and console module's directory. 2. I had poorly written the test in the past to assume that only 1 module would register. 3.1.0 is causing the IP Drop Down module to also be registered as a module. I rewrote the test to assume another module could be present. There isn't a simple way to pull a module's index after registration, which could make reading this test easier. --- test/ConsoleModule.test.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/ConsoleModule.test.cpp b/test/ConsoleModule.test.cpp index faa51c7..9a2d10b 100644 --- a/test/ConsoleModule.test.cpp +++ b/test/ConsoleModule.test.cpp @@ -15,6 +15,10 @@ TEST(ConsoleModuleLoader, ModuleWithoutDLL) // Test will need temporary module directory with no DLL present // Ensure module directory ends with a trailing slash + + const auto opuDirectory = GetOpuDirectory(); + fs::create_directory(opuDirectory); // create_directory throws if parent directory does not exist + const auto moduleDirectory = fs::path(GetOpuDirectory()) / moduleName; fs::create_directory(moduleDirectory); @@ -25,14 +29,17 @@ TEST(ConsoleModuleLoader, ModuleWithoutDLL) EXPECT_NO_THROW(moduleLoader.LoadModules()); EXPECT_NO_THROW(moduleLoader.RunModules()); - EXPECT_EQ(moduleName + "\\", moduleLoader.GetModuleDirectory(0)); - EXPECT_EQ(moduleName, moduleLoader.GetModuleName(0)); - EXPECT_TRUE(moduleLoader.IsModuleLoaded(moduleName)); EXPECT_FALSE(moduleLoader.IsModuleLoaded("")); EXPECT_FALSE(moduleLoader.IsModuleLoaded("UnknownModule")); - EXPECT_EQ(1u, moduleLoader.Count()); + // Because IsModuleLoaded returned true previously, moduleName will find a match + for (std::size_t i = 0; i < moduleLoader.Count(); ++i) { + if (moduleName == moduleLoader.GetModuleName(i)) { + EXPECT_EQ(moduleName + "\\", moduleLoader.GetModuleDirectory(i)); + break; + } + } EXPECT_NO_THROW(moduleLoader.UnloadModules()); From 87851c8a828fe166837271c1c4813c32c23a19d0 Mon Sep 17 00:00:00 2001 From: Brett208 <15183337+Brett208@users.noreply.github.com> Date: Mon, 8 Mar 2021 20:00:25 -0500 Subject: [PATCH 13/16] Use std library call create_directories when creating module directories Allows creating directories recursively if needed. The OPU directory may not be present when unit tests begin executing. --- test/ConsoleModule.test.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/ConsoleModule.test.cpp b/test/ConsoleModule.test.cpp index 9a2d10b..8cfee60 100644 --- a/test/ConsoleModule.test.cpp +++ b/test/ConsoleModule.test.cpp @@ -16,11 +16,8 @@ TEST(ConsoleModuleLoader, ModuleWithoutDLL) // Test will need temporary module directory with no DLL present // Ensure module directory ends with a trailing slash - const auto opuDirectory = GetOpuDirectory(); - fs::create_directory(opuDirectory); // create_directory throws if parent directory does not exist - const auto moduleDirectory = fs::path(GetOpuDirectory()) / moduleName; - fs::create_directory(moduleDirectory); + fs::create_directories(moduleDirectory); const std::string iniFileName{ GetExeDirectory() + "TestIniFile.NonExistentData.ini" }; IniFile iniFile(iniFileName); @@ -58,7 +55,7 @@ TEST(ConsoleModuleLoader, ModuleWithEmptyDLL) const auto dllFile = moduleDirectory / "op2mod.dll"; // Create temporary module directory - fs::create_directory(moduleDirectory); + fs::create_directories(moduleDirectory); // Create empty DLL file // Temporary object, immediately destructed, side effect creates file of size 0 @@ -98,7 +95,7 @@ TEST(ConsoleModuleLoader, MultiModule) { // Create some empty test module directories for (const auto& moduleName : moduleNames) { - fs::create_directory(opuDirectory / moduleName); + fs::create_directories(opuDirectory / moduleName); } const std::string iniFileName{ GetExeDirectory() + "TestIniFile.NonExistentData.ini" }; From b2bf9909288e5b9f9242528171264465c05af659 Mon Sep 17 00:00:00 2001 From: Brett208 <15183337+Brett208@users.noreply.github.com> Date: Mon, 8 Mar 2021 20:56:36 -0500 Subject: [PATCH 14/16] Rewrite function name to be more specific --- test/IniModule.test.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/IniModule.test.cpp b/test/IniModule.test.cpp index 6b9e8d2..befde84 100644 --- a/test/IniModule.test.cpp +++ b/test/IniModule.test.cpp @@ -17,7 +17,7 @@ class IniModuleTest : public LogMessageTest { } // sectionPairs is a non-line terminated list of keys and values. EG: TestModule = No - void WriteExternalModuleIniFile(const std::vector& sectionPairs) + void WriteExternalModuleSectionToIniFile(const std::vector& sectionPairs) { std::ofstream iniFileStream(iniFilename.string()); @@ -34,6 +34,7 @@ class IniModuleTest : public LogMessageTest { TEST_F(IniModuleTest, NoDll) { WriteExternalModuleIniFile({ "Test = yes" }); + WriteExternalModuleSectionToIniFile({ "Test = yes" }); ModuleLoader moduleLoader(IniFile(iniFilename.string()), {}); @@ -45,7 +46,7 @@ TEST_F(IniModuleTest, NoDll) TEST_F(IniModuleTest, InappropriateValue) { - WriteExternalModuleIniFile({ "Test = InappropriateValue" }); + WriteExternalModuleSectionToIniFile({ "Test = InappropriateValue" }); ModuleLoader moduleLoader(IniFile(iniFilename.string()), {}); From 082a8dcc3179884d7052a9a21e60df1d37b0c2cf Mon Sep 17 00:00:00 2001 From: Brett208 <15183337+Brett208@users.noreply.github.com> Date: Mon, 8 Mar 2021 20:59:06 -0500 Subject: [PATCH 15/16] Remove tests that rely on specific module Count The minimum number of modules loaded is non-deterministic now that built in modules can be loaded even if they are not present in the ini file --- test/ConsoleModule.test.cpp | 18 ++++-------------- test/IniModule.test.cpp | 2 -- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/test/ConsoleModule.test.cpp b/test/ConsoleModule.test.cpp index 8cfee60..d3b40d1 100644 --- a/test/ConsoleModule.test.cpp +++ b/test/ConsoleModule.test.cpp @@ -65,21 +65,12 @@ TEST(ConsoleModuleLoader, ModuleWithEmptyDLL) IniFile iniFile(iniFileName); ModuleLoader moduleLoader(iniFile, {moduleName}); - // DLL file is empty and should be aborted + // Check that an invalid module does not throw an exception when loaded EXPECT_NO_THROW(moduleLoader.LoadModules()); - - // Functions should return without doing anything since module load is aborted EXPECT_NO_THROW(moduleLoader.RunModules()); - // Due to invalid dll, no module exists to get directory from - EXPECT_THROW(moduleLoader.GetModuleDirectory(0), std::out_of_range); - EXPECT_THROW(moduleLoader.GetModuleName(0), std::out_of_range); - + // Ensure module does not exist EXPECT_FALSE(moduleLoader.IsModuleLoaded(moduleName)); - EXPECT_FALSE(moduleLoader.IsModuleLoaded("")); - EXPECT_FALSE(moduleLoader.IsModuleLoaded("UnknownModule")); - - EXPECT_EQ(0u, moduleLoader.Count()); EXPECT_NO_THROW(moduleLoader.UnloadModules()); @@ -105,9 +96,8 @@ TEST(ConsoleModuleLoader, MultiModule) { EXPECT_NO_THROW(moduleLoader.LoadModules()); EXPECT_NO_THROW(moduleLoader.RunModules()); - EXPECT_EQ(2u, moduleLoader.Count()); - EXPECT_EQ(moduleNames[0], moduleLoader.GetModuleName(0)); - EXPECT_EQ(moduleNames[1], moduleLoader.GetModuleName(1)); + // Ensure both modules are loaded. (More modules could load if built in modules are present) + EXPECT_GE(moduleLoader.Count(), 2u); EXPECT_TRUE(moduleLoader.IsModuleLoaded(moduleNames[0])); EXPECT_TRUE(moduleLoader.IsModuleLoaded(moduleNames[1])); diff --git a/test/IniModule.test.cpp b/test/IniModule.test.cpp index befde84..9502e09 100644 --- a/test/IniModule.test.cpp +++ b/test/IniModule.test.cpp @@ -41,7 +41,6 @@ TEST_F(IniModuleTest, NoDll) // No DLL found. An error should post, but program continues to run EXPECT_CALL(loggerError, Log(::testing::HasSubstr("Unable to load dll for module"))).Times(1); EXPECT_NO_THROW(moduleLoader.LoadModules()); - EXPECT_EQ(0u, moduleLoader.Count()); } TEST_F(IniModuleTest, InappropriateValue) @@ -53,5 +52,4 @@ TEST_F(IniModuleTest, InappropriateValue) // Inappropriate value for if module should be loaded. An error should post, but program continues to run EXPECT_CALL(loggerError, Log(::testing::HasSubstr("contains an innapropriate setting of"))).Times(1); EXPECT_NO_THROW(moduleLoader.LoadModules()); - EXPECT_EQ(0u, moduleLoader.Count()); } From a738b749330f2f2e13a69563c5933585bec75751 Mon Sep 17 00:00:00 2001 From: Brett208 <15183337+Brett208@users.noreply.github.com> Date: Mon, 8 Mar 2021 21:01:10 -0500 Subject: [PATCH 16/16] Add a dll filename to ini file when testing op2ext will no longer log a message for missing dll modules unless a dll path is specifically called out in the ini file --- test/IniModule.test.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/IniModule.test.cpp b/test/IniModule.test.cpp index 9502e09..a476c1d 100644 --- a/test/IniModule.test.cpp +++ b/test/IniModule.test.cpp @@ -33,9 +33,15 @@ class IniModuleTest : public LogMessageTest { TEST_F(IniModuleTest, NoDll) { - WriteExternalModuleIniFile({ "Test = yes" }); + // Add active module named test to ini file WriteExternalModuleSectionToIniFile({ "Test = yes" }); + // Register a non-existent dll filename to the test module in the ini file + std::ofstream iniFileStream(iniFilename.string(), std::ios_base::app); + iniFileStream << std::endl; + iniFileStream << "[Test]" << std::endl; + iniFileStream << "Dll = Missing.dll" << std::endl; + ModuleLoader moduleLoader(IniFile(iniFilename.string()), {}); // No DLL found. An error should post, but program continues to run