├── tests ├── sorted_files │ ├── 1.txt │ ├── 10.txt │ ├── 5.txt │ ├── a.txt │ ├── b.txt │ └── c.txt ├── nested_exec.bl3hotfix ├── single_exec.bl3hotfix ├── news.bl3hotfix ├── mods_dir │ ├── _early_exec.bl3hotfix │ ├── unicode_statement.bl3hotfix │ └── easy_entry_to_fort_sunshine.bl3hotfix ├── unicode_statement.bl3hotfix ├── basic_mod.URL ├── multi_exec.URL ├── basic_mod.bl3hotfix ├── multi_exec.bl3hotfix └── easy_entry_to_fort_sunshine.bl3hotfix ├── src ├── pch.c ├── pch.cpp ├── test_runner.cpp ├── version.h.in ├── args.h ├── util.h ├── processing.h ├── hooks.h ├── pch.h ├── loader.h ├── dllmain.cpp ├── args.cpp ├── unreal.cpp ├── unreal.h ├── util.cpp ├── hooks.cpp ├── processing.cpp └── loader.cpp ├── news_icon.png ├── news_icon.xcf ├── .clang-format ├── .gitmodules ├── .gitignore ├── user-includes.cmake.template ├── postbuild.template ├── mingw-w64-x86_64.cmake ├── versioninfo.rc.in ├── CMakePresets.json ├── CMakeLists.txt ├── README.md └── LICENSE /tests/sorted_files/1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sorted_files/10.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sorted_files/5.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sorted_files/a.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sorted_files/b.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sorted_files/c.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pch.c: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | -------------------------------------------------------------------------------- /src/pch.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | -------------------------------------------------------------------------------- /tests/nested_exec.bl3hotfix: -------------------------------------------------------------------------------- 1 | exec single_exec.bl3hotfix 2 | -------------------------------------------------------------------------------- /tests/single_exec.bl3hotfix: -------------------------------------------------------------------------------- 1 | exec basic_mod.bl3hotfix 2 | -------------------------------------------------------------------------------- /tests/news.bl3hotfix: -------------------------------------------------------------------------------- 1 | InjectNewsItem,My News Item,,https://example.com 2 | -------------------------------------------------------------------------------- /news_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple1417/OpenHotfixLoader/HEAD/news_icon.png -------------------------------------------------------------------------------- /news_icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple1417/OpenHotfixLoader/HEAD/news_icon.xcf -------------------------------------------------------------------------------- /src/test_runner.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | -------------------------------------------------------------------------------- /tests/mods_dir/_early_exec.bl3hotfix: -------------------------------------------------------------------------------- 1 | exec unicode_statement.bl3hotfix 2 | exec ..\news.bl3hotfix 3 | -------------------------------------------------------------------------------- /tests/unicode_statement.bl3hotfix: -------------------------------------------------------------------------------- 1 | SparkPatchEntry,(1,1,0,),/Game/PatchDLC/Raid1/Gear/Weapons/Link/Name_MAL_SM_Link.Name_MAL_SM_Link,PartName,0,,Cú Chulainn 2 | -------------------------------------------------------------------------------- /tests/mods_dir/unicode_statement.bl3hotfix: -------------------------------------------------------------------------------- 1 | SparkPatchEntry,(1,1,0,),/Game/PatchDLC/Raid1/Gear/Weapons/Link/Name_MAL_SM_Link.Name_MAL_SM_Link,PartName,0,,Cú Chulainn 2 | -------------------------------------------------------------------------------- /tests/basic_mod.URL: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://github.com/apple1417/OpenHotfixLoader/raw/master/tests/basic_mod.bl3hotfix 3 | IDList= 4 | HotKey=0 5 | IconIndex=0 6 | -------------------------------------------------------------------------------- /tests/multi_exec.URL: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://github.com/apple1417/OpenHotfixLoader/raw/master/tests/multi_exec.bl3hotfix 3 | IDList= 4 | HotKey=0 5 | IconIndex=0 6 | -------------------------------------------------------------------------------- /tests/basic_mod.bl3hotfix: -------------------------------------------------------------------------------- 1 | SparkLevelPatchEntry,(1,1,1,Crypt_P),/Game/Cinematics/_Design/NPCs/BPCine_Actor_Typhon.BPCine_Actor_Typhon_C:SkeletalMesh_GEN_VARIABLE,bEnableUpdateRateOptimizations,4,True,False 2 | -------------------------------------------------------------------------------- /src/version.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // This header is modified by cmake to insert the relevant strings 4 | 5 | #define VERSION_MAJOR "@PROJECT_VERSION_MAJOR@" 6 | #define VERSION_MINOR "@PROJECT_VERSION_MINOR@" 7 | #define VERSION_STRING "@PROJECT_VERSION_PREFIX@" VERSION_MAJOR "." VERSION_MINOR 8 | -------------------------------------------------------------------------------- /tests/multi_exec.bl3hotfix: -------------------------------------------------------------------------------- 1 | exec basic_mod.bl3hotfix 2 | exec basic_mod.bl3hotfix 3 | 4 | exec unicode_statement.bl3hotfix 5 | 6 | exec basic_mod.bl3hotfix 7 | exec basic_mod.bl3hotfix 8 | 9 | SparkPatchEntry,(1,1,0,),/Game/PlayerCharacters/_Shared/_Design/Sliding/ControlledMove_Global_Sliding.Default__ControlledMove_Global_Sliding_C,Duration.BaseValueConstant,0,,5000 10 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: Chromium 4 | 5 | AllowShortBlocksOnASingleLine: Empty 6 | BreakBeforeBinaryOperators: NonAssignment 7 | ColumnLimit: 100 8 | IndentWidth: 4 9 | InsertBraces: true 10 | SortIncludes: CaseInsensitive 11 | --- 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "minhook"] 2 | path = minhook 3 | url = https://github.com/TsudaKageyu/minhook.git 4 | [submodule "cpr"] 5 | path = cpr 6 | url = https://github.com/libcpr/cpr.git 7 | branch = 1.9.x 8 | [submodule "plog"] 9 | path = plog 10 | url = https://github.com/SergiusTheBest/plog 11 | [submodule "doctest"] 12 | path = doctest 13 | url = https://github.com/doctest/doctest.git 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | out 3 | 4 | # User scripts with templates 5 | postbuild.bat 6 | user-includes.cmake 7 | 8 | # CMake (from https://github.com/github/gitignore/blob/main/CMake.gitignore) 9 | CMakeLists.txt.user 10 | CMakeCache.txt 11 | CMakeFiles 12 | CMakeScripts 13 | Testing 14 | Makefile 15 | cmake_install.cmake 16 | install_manifest.txt 17 | compile_commands.json 18 | CTestTestfile.cmake 19 | _deps 20 | CMakeUserPresets.json 21 | -------------------------------------------------------------------------------- /user-includes.cmake.template: -------------------------------------------------------------------------------- 1 | # This file can be used to customize the included libraries 2 | # I know it's not ideal, but as much as I tried I could not get FindZLIB to behave another way 3 | # If this file exists (as the non template, gitignore'd version), it will be included near the top 4 | # of the main CMakeLists.txt, before cpr is included. 5 | 6 | # Example implementation 7 | 8 | set(ZLIB_LIBRARY "[vcpkg]/packages/zlib_x64-windows-static/lib/zlib.lib") 9 | set(ZLIB_INCLUDE_DIR "[vcpkg]/packages/zlib_x64-windows-static/include") 10 | set(CURL_ZLIB ON) 11 | -------------------------------------------------------------------------------- /postbuild.template: -------------------------------------------------------------------------------- 1 | REM This script is run after a build completes, intended for advanced customization of install dir. 2 | REM If the script exists, it will be called with the location of the built dll. 3 | 4 | REM Example (batch) implementation: 5 | 6 | @echo off 7 | 8 | set "target=%~1" 9 | set "filename=%~nx1" 10 | 11 | set "PLUGINS_FOLDER=C:\Program Files (x86)\Steam\steamapps\common\Borderlands 3\OakGame\Binaries\Win64\Plugins" 12 | 13 | for %%n in (OpenHotfixLoader.dll OpenHotfixLoader-Debug.dll) do ( 14 | if exist "%PLUGINS_FOLDER%\%%n" ( 15 | del "%PLUGINS_FOLDER%\%%n" 16 | ) 17 | ) 18 | 19 | copy /Y "%target%" "%PLUGINS_FOLDER%\%filename%" 20 | -------------------------------------------------------------------------------- /src/args.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace ohl::args { 6 | 7 | /** 8 | * @brief Parses all command line args, and otherwise initalizes the args module. 9 | * 10 | * @param this_module Handle to this dll's module. 11 | */ 12 | void init(HMODULE this_module); 13 | 14 | /** 15 | * @brief Checks if debug mode is on. 16 | * 17 | * @return True if debug mode was specified, false otherwise. 18 | */ 19 | bool debug(void); 20 | 21 | /** 22 | * @brief Checks if to dump hotfixes. 23 | * 24 | * @return True if to dump hotfixes, false otherwise. 25 | */ 26 | bool dump_hotfixes(void); 27 | 28 | /** 29 | * @brief Gets the path to the current exe. 30 | * 31 | * @return The path to the current exe, or an empty string if not found. 32 | */ 33 | std::filesystem::path exe_path(void); 34 | 35 | /** 36 | * @brief Gets the path to the current dll. 37 | * 38 | * @return The path to the current dll, or an empty string if not found. 39 | */ 40 | std::filesystem::path dll_path(void); 41 | 42 | } // namespace ohl::args 43 | -------------------------------------------------------------------------------- /mingw-w64-x86_64.cmake: -------------------------------------------------------------------------------- 1 | # Sample toolchain file for building for Windows from an Ubuntu Linux system. 2 | # 3 | # Typical usage: 4 | # *) install cross compiler: `sudo apt-get install mingw-w64` 5 | # *) cd build 6 | # *) cmake -DCMAKE_TOOLCHAIN_FILE=~/mingw-w64-x86_64.cmake .. 7 | 8 | set(CMAKE_SYSTEM_NAME Windows) 9 | set(TOOLCHAIN_PREFIX x86_64-w64-mingw32) 10 | set(THREADS_PREFER_PTHREAD_FLAG ON) 11 | 12 | # Cross compilers to use for C, C++ and Fortran 13 | # Need to use posix threads, so explicitly specifying that version 14 | set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc-posix) 15 | set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++-posix) 16 | set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran) 17 | set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres) 18 | 19 | # Target environment on the build host system 20 | set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) 21 | 22 | # Modify default behavior of FIND_XXX() commands 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 26 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace ohl::util { 4 | 5 | /** 6 | * @brief Narrows a utf-16 wstring to a utf-8 string. 7 | * 8 | * @param str The input wstring. 9 | * @return The output string. 10 | */ 11 | std::string narrow(const std::wstring& wstr); 12 | 13 | /** 14 | * @brief Widens a utf-8 string to a utf-16 wstring. 15 | * 16 | * @param str The input string. 17 | * @return The output wstring. 18 | */ 19 | std::wstring widen(const std::string& str); 20 | 21 | /** 22 | * @brief Get all files in a directory, sorted numerically. 23 | * @note Returns 1, 5, 10, etc. 24 | * 25 | * @param path Path to the directory. 26 | * @return A list of file paths. 27 | */ 28 | std::vector get_sorted_files_in_dir(const std::filesystem::path& path); 29 | 30 | /** 31 | * @brief Unescapes a url. 32 | * 33 | * @param url The url to unescape. 34 | * @param extra_info True if to also unescape the `#` or `?`, and any characters after them. 35 | * @return The unescaped url. 36 | */ 37 | std::string unescape_url(const std::string& url, bool extra_info); 38 | 39 | } // namespace ohl::util 40 | -------------------------------------------------------------------------------- /versioninfo.rc.in: -------------------------------------------------------------------------------- 1 | 1 TYPELIB "versioninfo.rc" 2 | 3 | #include 4 | 5 | #ifdef NDEBUG 6 | #define DEBUG_FILE_FLAG 0 7 | #else 8 | #define DEBUG_FILE_FLAG VS_FF_DEBUG 9 | #endif 10 | #if @PROJECT_VERSION_MAJOR@ == 0 11 | #define PRERELEASE_FILE_FLAG VS_FF_PRERELEASE 12 | #else 13 | #define PRERELEASE_FILE_FLAG 0 14 | #endif 15 | 16 | 1 VERSIONINFO 17 | FILEVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, 0, 0 18 | PRODUCTVERSION @PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, 0, 0 19 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 20 | FILEFLAGS (DEBUG_FILE_FLAG | PRERELEASE_FILE_FLAG) 21 | FILEOS VOS_NT_WINDOWS32 22 | FILETYPE VFT_DLL 23 | FILESUBTYPE 0 24 | BEGIN 25 | BLOCK "StringFileInfo" 26 | BEGIN 27 | BLOCK "040904e4" 28 | BEGIN 29 | VALUE "FileVersion","@PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, 0, 0" 30 | VALUE "InternalName", "@PROJECT_NAME@" 31 | VALUE "ProductName", "@PROJECT_NAME@" 32 | VALUE "ProductVersion","@PROJECT_VERSION_PREFIX@@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@" 33 | END 34 | END 35 | BLOCK "VarFileInfo" 36 | BEGIN 37 | VALUE "Translation", 0x409, 1252 38 | END 39 | END 40 | -------------------------------------------------------------------------------- /src/processing.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "unreal.h" 4 | 5 | namespace ohl::processing { 6 | 7 | /** 8 | * @brief Handles `GbxSparkSdk::Discovery::Api::GetServicesVerification` calls, using them to start 9 | * reloading hotfixes. 10 | */ 11 | void handle_get_verification(void); 12 | 13 | /** 14 | * @brief Handles `GbxSparkSdk::Discovery::Services::FromJson` calls, inserting our custom hotfixes. 15 | * 16 | * @param json Unreal json objects containing the received data. 17 | */ 18 | void handle_discovery_from_json(ohl::unreal::FJsonObject** json); 19 | 20 | /** 21 | * @brief Handles `GbxSparkSdk::News::NewsResponse::FromJson` calls, inserting our custom article. 22 | * 23 | * @param json Unreal json objects containing the received data. 24 | */ 25 | void handle_news_from_json(ohl::unreal::FJsonObject** json); 26 | 27 | /** 28 | * @brief Handles `FOnlineImageManager::AddImageToFileCache` calls, checking if to block execution 29 | * to prevent injected images from being cached. 30 | * 31 | * @param req The spark request that downloaded this image. 32 | * @return True if the function is allowed to continue, false if to block execution. 33 | */ 34 | bool handle_add_image_to_cache(ohl::unreal::TSharedPtr* req); 35 | 36 | } // namespace ohl::processing 37 | -------------------------------------------------------------------------------- /src/hooks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ohl::hooks { 4 | 5 | /** 6 | * @brief Initalizes the hooks module. 7 | */ 8 | void init(void); 9 | 10 | /** 11 | * @brief Calls unreal's malloc function. 12 | * @note Throws a runtime error if the call fails. 13 | * 14 | * @param count How many bytes to allocate. 15 | * @return A pointer to the allocated memory. 16 | */ 17 | void* malloc_raw(size_t count); 18 | 19 | /** 20 | * @brief Calls unreal's realloc function. 21 | * @note Throws a runtime error if the call fails. 22 | * 23 | * @param original The original memory to re-allocate. 24 | * @param count How many bytes to allocate. 25 | * @return A pointer to the re-allocated memory. 26 | */ 27 | void* realloc_raw(void* original, size_t count); 28 | 29 | /** 30 | * @brief Wrapper around `malloc_raw` casts to the relevant type. 31 | * 32 | * @tparam T The type to cast to. 33 | * @param count How many bytes to allocate. 34 | * @return A pointer to the allocated memory. 35 | */ 36 | template 37 | T* malloc(size_t count) { 38 | return reinterpret_cast(malloc_raw(count)); 39 | } 40 | 41 | /** 42 | * @brief Wrapper around `realloc_raw` casts to the relevant type. 43 | * 44 | * @tparam T The type to cast to. 45 | * @param original The original memory to re-allocate. 46 | * @param count How many bytes to allocate. 47 | * @return A pointer to the re-allocated memory. 48 | */ 49 | template 50 | T* realloc(void* original, size_t count) { 51 | return reinterpret_cast(realloc_raw(original, count)); 52 | } 53 | 54 | /** 55 | * @brief Call's unreal's free function. 56 | * 57 | * @param data The data to free. 58 | */ 59 | void free(void* data); 60 | 61 | } // namespace ohl::hooks 62 | -------------------------------------------------------------------------------- /src/pch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #define NOMINMAX 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #ifdef __cplusplus 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | 40 | using std::int16_t; 41 | using std::int32_t; 42 | using std::int64_t; 43 | using std::int8_t; 44 | using std::uint16_t; 45 | using std::uint32_t; 46 | using std::uint64_t; 47 | using std::uint8_t; 48 | #endif 49 | 50 | #ifdef __MINGW32__ 51 | // blank out SetThreadDescription 52 | #define SetThreadDescription(x, y) 53 | // Missing UTF Escapes for Windows 8 and above 54 | #ifndef URL_UNESCAPE_AS_UTF8 55 | #define URL_UNESCAPE_AS_UTF8 0x00040000 56 | #endif 57 | #endif 58 | 59 | /** 60 | * @brief Shortcut macro which checks if two iterables are equal. 61 | * @note Iterables must define `.begin()` and `.end()` functions, returning the relevant iterators. 62 | * 63 | * @param A The first iterable. 64 | * @param B The second iterable. 65 | */ 66 | #define ITERABLE_EQUAL(A, B) std::equal((A).begin(), (A).end(), (B).begin(), (B).end()) 67 | 68 | /** 69 | * @brief The github URLs for the OHL project. 70 | * @note The raw url includes `/master/`. 71 | */ 72 | #define OHL_GITHUB_URL "https://github.com/apple1417/OpenHotfixLoader/" 73 | #define OHL_GITHUB_RAW_URL "https://raw.githubusercontent.com/apple1417/OpenHotfixLoader/master/" 74 | -------------------------------------------------------------------------------- /src/loader.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace ohl::loader { 6 | 7 | /** 8 | * @brief Struct representing a single hotfix entry. 9 | */ 10 | struct hotfix { 11 | std::string key; 12 | std::string value; 13 | 14 | hotfix(const std::string& key = "", const std::string& value = "") : key(key), value(value) {} 15 | 16 | bool operator==(const hotfix& rhs) const { 17 | return this->key == rhs.key && this->value == rhs.value; 18 | } 19 | bool operator!=(const hotfix& rhs) const { return !operator==(rhs); } 20 | }; 21 | 22 | /** 23 | * @brief Struct representing a single injected news item. * 24 | */ 25 | struct news_item { 26 | std::string header; 27 | std::string image_url; 28 | std::string article_url; 29 | std::string body; 30 | 31 | news_item(const std::string& header = "", 32 | const std::string& image_url = "", 33 | const std::string& article_url = "", 34 | const std::string& body = "") 35 | : header(header), image_url(image_url), article_url(article_url), body(body) {} 36 | 37 | bool operator==(const news_item& rhs) const { 38 | return this->header == rhs.header && this->image_url == rhs.image_url 39 | && this->article_url == rhs.article_url && this->body == rhs.body; 40 | } 41 | bool operator!=(const news_item& rhs) const { return !operator==(rhs); } 42 | }; 43 | 44 | /** 45 | * @brief Initalizes the loader module. 46 | */ 47 | void init(void); 48 | 49 | /** 50 | * @brief Starts reloads the hotfix list. 51 | * @note Runs in a thread, `get_hotfixes` or `get_news_items` calls will block until it compeltes. 52 | */ 53 | void reload(void); 54 | 55 | /** 56 | * @brief Get the list of hotfixes to inject. 57 | * 58 | * @return A list of hotfixes. 59 | */ 60 | std::deque get_hotfixes(void); 61 | 62 | /** 63 | * @brief Get the list of news items to inject. 64 | * 65 | * @return A list of news items. 66 | */ 67 | std::deque get_news_items(void); 68 | 69 | } // namespace ohl::loader 70 | -------------------------------------------------------------------------------- /src/dllmain.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "args.h" 4 | #include "hooks.h" 5 | #include "loader.h" 6 | #include "processing.h" 7 | #include "version.h" 8 | 9 | static const std::string LOG_FILE_NAME = "OpenHotfixLoader.log"; 10 | 11 | static HMODULE this_module; 12 | 13 | /** 14 | * @brief Main startup thread. 15 | * @note Instance of `ThreadProc`. 16 | * 17 | * @return unused. 18 | */ 19 | static int32_t startup_thread(void*) { 20 | try { 21 | SetThreadDescription(GetCurrentThread(), L"OpenHotfixLoader"); 22 | 23 | ohl::args::init(this_module); 24 | 25 | static plog::ConsoleAppender consoleAppender; 26 | plog::init(ohl::args::debug() ? plog::debug : plog::info, 27 | ohl::args::dll_path().replace_filename(LOG_FILE_NAME).c_str()) 28 | .addAppender(&consoleAppender); 29 | 30 | LOGI << "[OHL] Launched " VERSION_STRING; 31 | #ifdef DEBUG 32 | LOGD << "[OHL] Running debug build"; 33 | #endif 34 | 35 | ohl::hooks::init(); 36 | ohl::loader::init(); 37 | 38 | #ifdef DEBUG 39 | ohl::loader::reload(); 40 | #endif 41 | } catch (std::exception ex) { 42 | LOGF << "[OHL] Exception occured during initalization: " << ex.what(); 43 | } 44 | 45 | return 1; 46 | } 47 | 48 | /** 49 | * @brief Main entry point. 50 | * 51 | * @param hModule Handle to module for this dll. 52 | * @param ul_reason_for_call Reason this is being called. 53 | * @return True if loaded successfully, false otherwise. 54 | */ 55 | BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID) { 56 | switch (ul_reason_for_call) { 57 | case DLL_PROCESS_ATTACH: 58 | this_module = hModule; 59 | DisableThreadLibraryCalls(hModule); 60 | CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)startup_thread, NULL, 0, NULL); 61 | break; 62 | case DLL_THREAD_ATTACH: 63 | case DLL_THREAD_DETACH: 64 | case DLL_PROCESS_DETACH: 65 | break; 66 | } 67 | return TRUE; 68 | } 69 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "configurePresets": [ 4 | { 5 | "name": "_base", 6 | "hidden": true, 7 | "binaryDir": "${sourceDir}/out/build/${presetName}", 8 | "installDir": "${sourceDir}/out/install/${presetName}" 9 | }, 10 | { 11 | "name": "_msvc", 12 | "hidden": true, 13 | "condition": { 14 | "type": "equals", 15 | "lhs": "${hostSystemName}", 16 | "rhs": "Windows" 17 | }, 18 | "generator": "Ninja", 19 | "cacheVariables": { 20 | "CMAKE_C_COMPILER": "cl.exe", 21 | "CMAKE_CXX_COMPILER": "cl.exe" 22 | }, 23 | "architecture": { 24 | "value": "x64", 25 | "strategy": "external" 26 | } 27 | }, 28 | { 29 | "name": "_mingw", 30 | "hidden": true, 31 | "condition": { 32 | "type": "notEquals", 33 | "lhs": "${hostSystemName}", 34 | "rhs": "Windows" 35 | }, 36 | "toolchainFile": "mingw-w64-x86_64.cmake" 37 | }, 38 | { 39 | "name": "_debug", 40 | "hidden": true, 41 | "cacheVariables": { 42 | "CMAKE_BUILD_TYPE": "Debug" 43 | } 44 | }, 45 | { 46 | "name": "_release", 47 | "hidden": true, 48 | "cacheVariables": { 49 | "CMAKE_BUILD_TYPE": "Release" 50 | } 51 | }, 52 | { 53 | "name": "msvc-debug", 54 | "inherits": [ 55 | "_base", 56 | "_msvc", 57 | "_debug" 58 | ] 59 | }, 60 | { 61 | "name": "msvc-release", 62 | "inherits": [ 63 | "_base", 64 | "_msvc", 65 | "_release" 66 | ] 67 | }, 68 | { 69 | "name": "mingw-debug", 70 | "inherits": [ 71 | "_base", 72 | "_mingw", 73 | "_debug" 74 | ] 75 | }, 76 | { 77 | "name": "mingw-release", 78 | "inherits": [ 79 | "_base", 80 | "_mingw", 81 | "_release" 82 | ] 83 | } 84 | ], 85 | "testPresets": [ 86 | { 87 | "name": "msvc-debug", 88 | "configurePreset": "msvc-debug" 89 | } 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /tests/easy_entry_to_fort_sunshine.bl3hotfix: -------------------------------------------------------------------------------- 1 | 2 | @title Easy Entry to Fort Sunshine 3 | @version 1.0.0 4 | @author Apocalyptech 5 | @contact https://apocalyptech.com/contact.php 6 | @categories maps, qol 7 | 8 | @license Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) 9 | @license-url https://creativecommons.org/licenses/by-sa/4.0/ 10 | 11 | @screenshot https://raw.githubusercontent.com/BLCM/bl3mods/master/Apocalyptech/qol/easy_entry_to_fort_sunshine/switch.png 12 | 13 | ### 14 | ### Adds a door-opening lever to the outside of Fort Sunshine, in Floodmoor Basin. 15 | ### During the game plot, you'll still need to ride the lumber conveyors into the 16 | ### fort in order to make progress, but the interior will be much easier to access 17 | ### outside of that mission. 18 | ### 19 | ### This mod requires either OpenHotfixLoader or B3HM v1.0.2 (or higher) 20 | ### to work properly. Note that at time of release, B3HM v1.0.2 has not yet 21 | ### been released. 22 | ### 23 | ### Generated by gen_easy_entry_to_fort_sunshine.py 24 | ### 25 | 26 | # Create the new switch 27 | SparkEarlyLevelPatchEntry,(1,11,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands,/Game/InteractiveObjects/Switches/Lever/Design,IO_Switch_Industrial_Prison,80,"0.000000,0.000000,0.000000|0.000000,0.000000,0.000000|1.000000,1.000000,1.000000" 28 | SparkEarlyLevelPatchEntry,(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0.DefaultSceneRoot,RelativeLocation,0,,(X=34927.000000,Y=10709.000000,Z=3139.000000) 29 | SparkEarlyLevelPatchEntry,(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0.DefaultSceneRoot,RelativeRotation,0,,(Pitch=0.000000,Yaw=67.716370,Roll=0.000000) 30 | SparkEarlyLevelPatchEntry,(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0.DefaultSceneRoot,RelativeScale3D,0,,(X=1.000000,Y=1.000000,Z=1.000000) 31 | 32 | # Hook it up to the door 33 | SparkLevelPatchEntry,(1,1,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0,On_SwitchUsed,0,,(Wetlands_M_EP12JakobsRebellion_C_11.BndEvt__IO_Switch_Circuit_Breaker_V_2_K2Node_ActorBoundEvent_0_On_SwitchUsed__DelegateSignature) 34 | SparkLevelPatchEntry,(1,1,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0,SingleActivation,0,,False 35 | 36 | -------------------------------------------------------------------------------- /tests/mods_dir/easy_entry_to_fort_sunshine.bl3hotfix: -------------------------------------------------------------------------------- 1 | 2 | @title Easy Entry to Fort Sunshine 3 | @version 1.0.0 4 | @author Apocalyptech 5 | @contact https://apocalyptech.com/contact.php 6 | @categories maps, qol 7 | 8 | @license Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) 9 | @license-url https://creativecommons.org/licenses/by-sa/4.0/ 10 | 11 | @screenshot https://raw.githubusercontent.com/BLCM/bl3mods/master/Apocalyptech/qol/easy_entry_to_fort_sunshine/switch.png 12 | 13 | ### 14 | ### Adds a door-opening lever to the outside of Fort Sunshine, in Floodmoor Basin. 15 | ### During the game plot, you'll still need to ride the lumber conveyors into the 16 | ### fort in order to make progress, but the interior will be much easier to access 17 | ### outside of that mission. 18 | ### 19 | ### This mod requires either OpenHotfixLoader or B3HM v1.0.2 (or higher) 20 | ### to work properly. Note that at time of release, B3HM v1.0.2 has not yet 21 | ### been released. 22 | ### 23 | ### Generated by gen_easy_entry_to_fort_sunshine.py 24 | ### 25 | 26 | # Create the new switch 27 | SparkEarlyLevelPatchEntry,(1,11,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands,/Game/InteractiveObjects/Switches/Lever/Design,IO_Switch_Industrial_Prison,80,"0.000000,0.000000,0.000000|0.000000,0.000000,0.000000|1.000000,1.000000,1.000000" 28 | SparkEarlyLevelPatchEntry,(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0.DefaultSceneRoot,RelativeLocation,0,,(X=34927.000000,Y=10709.000000,Z=3139.000000) 29 | SparkEarlyLevelPatchEntry,(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0.DefaultSceneRoot,RelativeRotation,0,,(Pitch=0.000000,Yaw=67.716370,Roll=0.000000) 30 | SparkEarlyLevelPatchEntry,(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0.DefaultSceneRoot,RelativeScale3D,0,,(X=1.000000,Y=1.000000,Z=1.000000) 31 | 32 | # Hook it up to the door 33 | SparkLevelPatchEntry,(1,1,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0,On_SwitchUsed,0,,(Wetlands_M_EP12JakobsRebellion_C_11.BndEvt__IO_Switch_Circuit_Breaker_V_2_K2Node_ActorBoundEvent_0_On_SwitchUsed__DelegateSignature) 34 | SparkLevelPatchEntry,(1,1,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands/Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0,SingleActivation,0,,False 35 | 36 | -------------------------------------------------------------------------------- /src/args.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace ohl::args { 6 | TEST_SUITE_BEGIN("args"); 7 | 8 | typedef struct { 9 | bool debug; 10 | bool dump_hotfixes; 11 | std::filesystem::path exe_path; 12 | std::filesystem::path dll_path; 13 | } args_t; 14 | 15 | static args_t args = {false, false, "", ""}; 16 | 17 | /** 18 | * @brief Implementation of `parse`, allowing passing in a custom string. 19 | * 20 | * @param cmd The command line args. 21 | */ 22 | static void parse(std::string cmd) { 23 | args.debug = cmd.find("--ohl-debug") != std::string::npos; 24 | args.dump_hotfixes = cmd.find("--dump-hotfixes") != std::string::npos; 25 | } 26 | 27 | TEST_CASE("args::parse_str") { 28 | args.debug = false; 29 | args.dump_hotfixes = false; 30 | 31 | SUBCASE("debug") { 32 | parse("example.exe"); 33 | REQUIRE(args.debug == false); 34 | 35 | parse("example.exe --debug"); 36 | REQUIRE(args.debug == false); 37 | 38 | parse("example.exe --dump-hotfixes"); 39 | REQUIRE(args.debug == false); 40 | 41 | parse("example.exe --ohl-debug"); 42 | REQUIRE(args.debug == true); 43 | } 44 | 45 | SUBCASE("dump") { 46 | parse("example.exe"); 47 | REQUIRE(args.dump_hotfixes == false); 48 | 49 | parse("example.exe --dump-hotfixes"); 50 | REQUIRE(args.dump_hotfixes == true); 51 | } 52 | 53 | SUBCASE("debug + dump") { 54 | parse("example.exe"); 55 | REQUIRE(args.debug == false); 56 | REQUIRE(args.dump_hotfixes == false); 57 | 58 | parse("example.exe --ohl-debug --dump-hotfixes"); 59 | REQUIRE(args.debug == true); 60 | REQUIRE(args.dump_hotfixes == true); 61 | } 62 | } 63 | 64 | void init(HMODULE this_module) { 65 | parse(GetCommandLineA()); 66 | 67 | char buf[FILENAME_MAX]; 68 | if (GetModuleFileNameA(NULL, buf, sizeof(buf))) { 69 | args.exe_path = std::filesystem::path(buf); 70 | } 71 | if (GetModuleFileNameA(this_module, buf, sizeof(buf))) { 72 | args.dll_path = std::filesystem::path(buf); 73 | } 74 | } 75 | 76 | bool debug(void) { 77 | return args.debug; 78 | } 79 | 80 | bool dump_hotfixes(void) { 81 | return args.dump_hotfixes; 82 | } 83 | 84 | std::filesystem::path exe_path(void) { 85 | return args.exe_path; 86 | } 87 | 88 | std::filesystem::path dll_path(void) { 89 | return args.dll_path; 90 | } 91 | 92 | TEST_SUITE_END(); 93 | } // namespace ohl::args 94 | -------------------------------------------------------------------------------- /src/unreal.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "unreal.h" 4 | 5 | namespace ohl::unreal { 6 | 7 | #pragma region Casting 8 | 9 | template 10 | struct JTypeMapping { 11 | typedef T type; 12 | static inline const EJson enum_type; 13 | }; 14 | 15 | template <> 16 | struct JTypeMapping { 17 | typedef FJsonValueString type; 18 | static inline const EJson enum_type = EJson::String; 19 | }; 20 | 21 | template <> 22 | struct JTypeMapping { 23 | typedef FJsonValueArray type; 24 | static inline const EJson enum_type = EJson::Array; 25 | }; 26 | 27 | template <> 28 | struct JTypeMapping { 29 | typedef FJsonValueObject type; 30 | static inline const EJson enum_type = EJson::Object; 31 | }; 32 | 33 | template 34 | T* FJsonValue::cast(void) { 35 | if (this->type != JTypeMapping::enum_type) { 36 | throw std::runtime_error("JSON object was of unexpected type " 37 | + std::to_string((uint32_t)this->type)); 38 | } 39 | return reinterpret_cast(this); 40 | } 41 | 42 | #pragma endregion 43 | 44 | #pragma region Accessors 45 | 46 | std::wstring FString::to_wstr(void) const { 47 | return std::wstring(this->data); 48 | } 49 | 50 | std::wstring FJsonValueString::to_wstr(void) const { 51 | return this->str.to_wstr(); 52 | } 53 | 54 | std::wstring FSparkRequest::get_url(void) const { 55 | return this->url.to_wstr(); 56 | } 57 | 58 | uint32_t FJsonValueArray::count() const { 59 | return this->entries.count; 60 | } 61 | 62 | template 63 | T* FJsonValueArray::get(uint32_t idx) const { 64 | if (idx > this->count()) { 65 | throw std::out_of_range("Array index out of range"); 66 | } 67 | return this->entries.data[idx].obj->cast(); 68 | } 69 | 70 | template 71 | T* FJsonObject::get(std::wstring key) const { 72 | for (auto i = 0; i < this->entries.count; i++) { 73 | auto entry = this->entries.data[i]; 74 | if (entry.key.to_wstr() == key) { 75 | return entry.value.obj->cast(); 76 | } 77 | } 78 | throw std::runtime_error("Couldn't find key!"); 79 | } 80 | 81 | FJsonObject* FJsonValueObject::to_obj(void) const { 82 | return this->value.obj; 83 | } 84 | 85 | #pragma endregion 86 | 87 | #pragma region Explict Template Instantiation 88 | 89 | template FJsonValueString* FJsonValue::cast(void); 90 | template FJsonValueArray* FJsonValue::cast(void); 91 | template FJsonValueObject* FJsonValue::cast(void); 92 | 93 | template FJsonValueString* FJsonValueArray::get(uint32_t) const; 94 | template FJsonValueArray* FJsonValueArray::get(uint32_t) const; 95 | template FJsonValueObject* FJsonValueArray::get(uint32_t) const; 96 | 97 | template FJsonValueString* FJsonObject::get(std::wstring key) const; 98 | template FJsonValueArray* FJsonObject::get(std::wstring key) const; 99 | template FJsonValueObject* FJsonObject::get(std::wstring key) const; 100 | 101 | #pragma endregion 102 | 103 | } // namespace ohl::unreal 104 | -------------------------------------------------------------------------------- /src/unreal.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace ohl::unreal { 6 | 7 | template 8 | struct TArray { 9 | T* data; 10 | uint32_t count; 11 | uint32_t max; 12 | }; 13 | 14 | struct FString : TArray { 15 | /** 16 | * @brief Converts this string to a stl string. 17 | * 18 | * @return An stl string. 19 | */ 20 | std::wstring to_wstr(void) const; 21 | }; 22 | 23 | struct FReferenceControllerBase { 24 | void* vf_table; 25 | int32_t ref_count; 26 | int32_t weak_ref_count; 27 | void* obj; 28 | }; 29 | 30 | template 31 | struct TSharedPtr { 32 | T* obj; 33 | FReferenceControllerBase* ref_controller; 34 | }; 35 | 36 | enum class EJson { None = 0, Null = 1, String = 2, Number = 3, Boolean = 4, Array = 5, Object = 6 }; 37 | 38 | struct FJsonValue { 39 | void* vf_table; 40 | enum EJson type; 41 | 42 | /** 43 | * @brief Casts this object to a specific type. 44 | * @note Throws a runtime error if the type doesn't line up. 45 | * 46 | * @tparam T The type to cast to. 47 | * @return A pointer to this object casted to the relevant type. 48 | */ 49 | template 50 | T* cast(void); 51 | }; 52 | 53 | template 54 | struct KeyValuePair { 55 | K key; 56 | V value; 57 | int32_t hash_next_id; 58 | int32_t hash_idx; 59 | }; 60 | 61 | using JSONObjectEntry = KeyValuePair>; 62 | 63 | struct FJsonObject { 64 | TArray entries; 65 | 66 | // Unknown what this data represents. 67 | uint32_t pattern[16]; 68 | 69 | /** 70 | * @brief Gets a value on this object given it's key. 71 | * @note Throws a runtime error if the key is not found. 72 | * 73 | * @tparam T The type to cast the value to. 74 | * @param key The key to look up. 75 | * @return A pointer to the value object. 76 | */ 77 | template 78 | T* get(std::wstring key) const; 79 | }; 80 | 81 | struct FJsonValueString : FJsonValue { 82 | FString str; 83 | 84 | /** 85 | * @brief Converts this string to a stl string. 86 | * 87 | * @return An stl string. 88 | */ 89 | std::wstring to_wstr(void) const; 90 | }; 91 | 92 | struct FJsonValueArray : FJsonValue { 93 | TArray> entries; 94 | 95 | /** 96 | * @brief Gets the amount of items in this array. 97 | * 98 | * @return The amount of items. 99 | */ 100 | uint32_t count(void) const; 101 | 102 | /** 103 | * @brief Gets an entry in the array given it's index. 104 | * @note May throw an out of range error. 105 | * 106 | * @tparam T The type to cast the value to. 107 | * @param idx The index to get. 108 | * @return A pointer to the value object. 109 | */ 110 | template 111 | T* get(uint32_t idx) const; 112 | }; 113 | 114 | struct FJsonValueObject : FJsonValue { 115 | TSharedPtr value; 116 | 117 | /** 118 | * @brief Gets the object internal to this value. 119 | * 120 | * @return A pointer to the object. 121 | */ 122 | FJsonObject* to_obj(void) const; 123 | }; 124 | 125 | struct FSparkRequest { 126 | uint32_t unknown[4]; 127 | FString url; 128 | 129 | /** 130 | * @brief Get the url of this request. 131 | * 132 | * @return the url string 133 | */ 134 | std::wstring get_url(void) const; 135 | }; 136 | 137 | } // namespace ohl::unreal 138 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.20) 2 | 3 | set(CMAKE_CXX_STANDARD 17) 4 | set(CMAKE_CXX_STANDARD_REQUIRED True) 5 | set(CMAKE_CXX_EXTENSIONS False) 6 | 7 | project(OpenHotfixLoader VERSION 1.6) 8 | set(PROJECT_VERSION_PREFIX "v") 9 | 10 | set(CMAKE_SHARED_LIBRARY_PREFIX "") 11 | set(CMAKE_DEBUG_POSTFIX "-Debug") 12 | 13 | # CPR turns this on, so we need to force it back off 14 | option(BUILD_SHARED_LIBS "" OFF) 15 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) 16 | 17 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/user-includes.cmake") 18 | include("${CMAKE_CURRENT_SOURCE_DIR}/user-includes.cmake") 19 | endif() 20 | 21 | enable_testing() 22 | 23 | # Libraries 24 | add_subdirectory(cpr) 25 | add_subdirectory(doctest) 26 | add_subdirectory(minhook) 27 | add_subdirectory(plog) 28 | 29 | include(doctest/scripts/cmake/doctest.cmake) 30 | 31 | set(LIBSHLWAPI "Shlwapi.lib") 32 | if(MINGW) 33 | # Mingw has different names for shlwapi 34 | set(LIBSHLWAPI "libshlwapi") 35 | endif() 36 | 37 | find_library(shlwapi ${LIBSHLWAPI} REQUIRED) 38 | 39 | # Sources 40 | configure_file(versioninfo.rc.in versioninfo.rc) 41 | configure_file(src/version.h.in inc/version.h) 42 | 43 | file(GLOB_RECURSE sources CONFIGURE_DEPENDS "src/*.c" "src/*.cpp" "src/*.h" "src/*.hpp") 44 | 45 | # Root target 46 | add_library(ohl_root OBJECT ${sources}) 47 | target_include_directories(ohl_root PUBLIC "${PROJECT_BINARY_DIR}/inc" "src") 48 | 49 | target_link_libraries(ohl_root PUBLIC cpr::cpr doctest::doctest minhook plog shlwapi) 50 | 51 | # CMake by default defines NDEBUG in release, we also want the opposite 52 | target_compile_definitions(ohl_root PUBLIC "$<$:DEBUG>") 53 | target_compile_definitions(ohl_root PUBLIC "$<$>:DOCTEST_CONFIG_DISABLE>") 54 | target_compile_definitions(ohl_root PUBLIC "UNICODE" "_UNICODE") 55 | 56 | # The precompiled header must be defined AFTER the compile defines 57 | target_precompile_headers(ohl_root PUBLIC "src/pch.c" "src/pch.cpp") 58 | 59 | if(MSVC) 60 | # Enable Edit and Continue. 61 | string(REPLACE "/Zi" "" CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}") 62 | string(REPLACE "/Zi" "" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") 63 | string(REPLACE "/Zi" "" CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO}") 64 | string(REPLACE "/Zi" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") 65 | 66 | target_compile_options(ohl_root PUBLIC "$<$:/ZI>") 67 | target_link_options(ohl_root PUBLIC "/INCREMENTAL") 68 | 69 | # UTF-8 encoded source files 70 | target_compile_options(ohl_root PUBLIC "/utf-8") 71 | endif() 72 | 73 | if (MINGW) 74 | # Want to link statically into a single dll 75 | target_link_options(ohl_root PUBLIC "-static") 76 | endif() 77 | 78 | # Targets 79 | add_library(OpenHotfixLoader SHARED "${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc") 80 | target_link_libraries(OpenHotfixLoader PUBLIC ohl_root) 81 | 82 | add_executable(ohl_tests) 83 | target_link_libraries(ohl_tests PUBLIC ohl_root) 84 | if(MSVC) 85 | doctest_discover_tests(ohl_tests WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") 86 | endif() 87 | 88 | # Postbuild 89 | set(POSTBUILD_SCRIPT "postbuild") 90 | if(CMAKE_HOST_WIN32) 91 | set(POSTBUILD_SCRIPT "${POSTBUILD_SCRIPT}.bat") 92 | endif() 93 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${POSTBUILD_SCRIPT}") 94 | add_custom_command( 95 | TARGET OpenHotfixLoader 96 | POST_BUILD 97 | COMMAND ${POSTBUILD_SCRIPT} "\"$>\" \"$,DEBUG,RELEASE>\"" 98 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 99 | ) 100 | endif() 101 | -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace ohl::util { 6 | TEST_SUITE_BEGIN("utils"); 7 | 8 | std::string narrow(const std::wstring& wstr) { 9 | if (wstr.empty()) { 10 | return std::string(); 11 | } 12 | 13 | auto num_chars = 14 | WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), wstr.size(), NULL, 0, NULL, NULL); 15 | char* str = reinterpret_cast(malloc((num_chars + 1) * sizeof(char))); 16 | if (!str) { 17 | throw std::runtime_error("Failed to convert utf16 string!"); 18 | } 19 | 20 | WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), wstr.size(), str, num_chars, NULL, NULL); 21 | str[num_chars] = L'\0'; 22 | 23 | std::string ret{str}; 24 | free(str); 25 | 26 | return ret; 27 | } 28 | 29 | std::wstring widen(const std::string& str) { 30 | if (str.empty()) { 31 | return std::wstring(); 32 | } 33 | 34 | auto num_chars = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), str.size(), NULL, 0); 35 | wchar_t* wstr = reinterpret_cast(malloc((num_chars + 1) * sizeof(wchar_t))); 36 | if (!wstr) { 37 | throw std::runtime_error("Failed to convert utf8 string!"); 38 | } 39 | 40 | MultiByteToWideChar(CP_UTF8, 0, str.c_str(), str.size(), wstr, num_chars); 41 | wstr[num_chars] = L'\0'; 42 | 43 | std::wstring ret{wstr}; 44 | free(wstr); 45 | 46 | return ret; 47 | } 48 | 49 | TEST_CASE("utils::narrow") { 50 | // Test using unicode literals directly to be safe of any encoding issues 51 | // UTF16 literals are `char16_t[]`s, which won't implicitly cast to wstring 52 | static_assert(sizeof(wchar_t) == sizeof(char16_t)); 53 | 54 | CHECK(narrow((wchar_t*)u"test case") == u8"test case"); 55 | CHECK(narrow((wchar_t*)u"υπόθεση δοκιμής") == u8"υπόθεση δοκιμής"); 56 | CHECK(narrow((wchar_t*)u"прецедент") == u8"прецедент"); 57 | CHECK(narrow((wchar_t*)u"テストケース") == u8"テストケース"); 58 | CHECK(narrow((wchar_t*)u"\u0000\u007F\u0080\u1234") == u8"\u0000\u007F\u0080\u1234"); 59 | 60 | CHECK(narrow((wchar_t*)u"test case") != u8"other string"); 61 | } 62 | 63 | TEST_CASE("utils::widen") { 64 | static_assert(sizeof(wchar_t) == sizeof(char16_t)); 65 | 66 | CHECK(widen(u8"test case") == (wchar_t*)u"test case"); 67 | CHECK(widen(u8"υπόθεση δοκιμής") == (wchar_t*)u"υπόθεση δοκιμής"); 68 | CHECK(widen(u8"прецедент") == (wchar_t*)u"прецедент"); 69 | CHECK(widen(u8"テストケース") == (wchar_t*)u"テストケース"); 70 | CHECK(widen(u8"\u0000\u007F\u0080\u1234") == (wchar_t*)u"\u0000\u007F\u0080\u1234"); 71 | 72 | CHECK(widen(u8"test case") != (wchar_t*)u"other string"); 73 | } 74 | 75 | TEST_CASE("utils::narrow - utils::widen round trip") { 76 | static_assert(sizeof(wchar_t) == sizeof(char16_t)); 77 | 78 | CHECK(widen(narrow((wchar_t*)u"test case")) == (wchar_t*)u"test case"); 79 | CHECK(widen(narrow((wchar_t*)u"υπόθεση δοκιμής")) == (wchar_t*)u"υπόθεση δοκιμής"); 80 | CHECK(widen(narrow((wchar_t*)u"прецедент")) == (wchar_t*)u"прецедент"); 81 | CHECK(widen(narrow((wchar_t*)u"テストケース")) == (wchar_t*)u"テストケース"); 82 | CHECK(widen(narrow((wchar_t*)u"\u0000\u007F\u0080\u1234")) 83 | == (wchar_t*)u"\u0000\u007F\u0080\u1234"); 84 | 85 | CHECK(widen(narrow((wchar_t*)u"test case")) != (wchar_t*)u"other string"); 86 | 87 | CHECK(narrow(widen(u8"test case")) == u8"test case"); 88 | CHECK(narrow(widen(u8"υπόθεση δοκιμής")) == u8"υπόθεση δοκιμής"); 89 | CHECK(narrow(widen(u8"прецедент")) == u8"прецедент"); 90 | CHECK(narrow(widen(u8"テストケース")) == u8"テストケース"); 91 | CHECK(narrow(widen(u8"\u0000\u007F\u0080\u1234")) == u8"\u0000\u007F\u0080\u1234"); 92 | 93 | CHECK(narrow(widen(u8"test case")) != u8"other string"); 94 | } 95 | 96 | std::vector get_sorted_files_in_dir(const std::filesystem::path& path) { 97 | std::vector files{}; 98 | for (const auto& dir_entry : std::filesystem::directory_iterator{path}) { 99 | if (dir_entry.is_directory()) { 100 | continue; 101 | } 102 | files.push_back(dir_entry.path()); 103 | } 104 | std::sort(files.begin(), files.end(), [](const auto& a, const auto& b) -> bool { 105 | return StrCmpLogicalW(a.c_str(), b.c_str()) < 0; 106 | }); 107 | 108 | return files; 109 | } 110 | 111 | TEST_CASE("utils::get_sorted_files_in_dir") { 112 | const std::filesystem::path dir = std::filesystem::path("tests") / "sorted_files"; 113 | const std::vector expected = { 114 | dir / "1.txt", dir / "5.txt", dir / "10.txt", dir / "a.txt", dir / "b.txt", dir / "c.txt", 115 | }; 116 | 117 | CHECK(get_sorted_files_in_dir(dir) == expected); 118 | } 119 | 120 | std::string unescape_url(const std::string& url, bool extra_info) { 121 | DWORD len = (url.size() + 1) * sizeof(char); 122 | 123 | char* unescaped = reinterpret_cast(malloc(len)); 124 | if (!unescaped) { 125 | throw std::runtime_error("Failed to unescape url!"); 126 | } 127 | 128 | auto r = UrlUnescapeA(const_cast(url.c_str()), unescaped, &len, 129 | (extra_info ? 0 : URL_DONT_UNESCAPE_EXTRA_INFO)); 130 | 131 | std::string ret{unescaped}; 132 | free(unescaped); 133 | 134 | return ret; 135 | } 136 | 137 | TEST_CASE("utils::unescape_url") { 138 | CHECK(unescape_url("https://example.com", false) == "https://example.com"); 139 | CHECK(unescape_url("https://example.com#test", false) == "https://example.com#test"); 140 | CHECK(unescape_url("https://exa%6Dple%2ecom", false) == "https://example.com"); 141 | CHECK(unescape_url("https://exa%6Dple%2ecom?test", false) == "https://example.com?test"); 142 | CHECK(unescape_url("https://exa%6Dple%2ecom#test", false) == "https://example.com#test"); 143 | CHECK(unescape_url("https://exa%6Dple%2ecom#t%65st", false) == "https://example.com#t%65st"); 144 | CHECK(unescape_url("https://exa%6Dple%2ecom#t%65st", true) == "https://example.com#test"); 145 | CHECK(unescape_url("https://exa%6Dple%2ecom%23t%65st", true) == "https://example.com#test"); 146 | } 147 | 148 | TEST_SUITE_END(); 149 | } // namespace ohl::util 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenHotfixLoader 2 | [![Support Discord](https://img.shields.io/static/v1?label=&message=Support%20Discord&logo=discord&color=424)](https://discord.gg/bXeqV8Ef9R) 3 | 4 | OpenHotfixLoader is a simple, bloat-free, hotfix injector for BL3 and Wonderlands modding. 5 | 6 | Traditional injectors have worked through creating a proxy, meaning they redirect all your internet 7 | traffic through themselve. OpenHotfixLoader is the first proxyless injector, so there's no risk of 8 | it accidentally leaking your data, or impacting other internet usage. 9 | 10 | # Installation 11 | Alternatively, see [this video guide](https://youtu.be/gHX3dtZIojY) 12 | 13 | 1. Open up your game's local files. 14 | 2. Navigate to `\OakGame\Binaries\Win64`. 15 | 3. Download the [latest plugin loader release](https://github.com/FromDarkHell/BL3DX11Injection/releases/). 16 | Make sure to download `D3D11.zip`, *not* either of the source code links. 17 | 4. Extract the plugin loader zip into this folder, so that you have a file `Win64\d3d11.dll` and a 18 | folder `Win64\Plugins`. 19 | 5. Download the [latest OpenHotfixLoader release](https://github.com/apple1417/OpenHotfixLoader/releases). 20 | Make sure to grab `OpenHotfixLoader.zip`, *not* either of the source code links. 21 | 6. Open up the `Plugins` folder you just extracted, and extract the OpenHotfixLoader zip into, so 22 | that you have a file `Plugins\OpenHotfixLoader.dll` and a folder `Plugins\ohl-mods`. 23 | 24 | At this point, OpenHotfixLoader should be fully installed, and you should see a new news message on 25 | the main menu. If you're still having trouble, try install the latest 26 | [Microsoft Visual C Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe). 27 | 28 | ## Installing Hotfix Mods 29 | To install a hotfix mod, simply download it, and add it to the `ohl-mods` folder you just extracted. 30 | If you want to automatically download the latest version, simply create a URL shortcut and put it in 31 | this folder - just drag the download link out of your browser into Windows Explorer. 32 | 33 | If you edit mods while the game is open, you need to quit to title screen, then load back onto the 34 | main menu in order to reload them. 35 | 36 | Mods are loaded in numeric-alphabetical order - `A.txt` loads before `B.txt`, and `9.txt` before 37 | `10.txt`. This should be the same order which Windows Explorer displays when you sort by name. If 38 | one mod needs to be loaded before another, simply rename it so sorts later. 39 | 40 | # Notes for Modders 41 | ## Commands 42 | OpenHotfixLoader supports a number of commands. All commands strip leading whitespace, and are 43 | detected case insensitively (though this doesn't mean case doesn't matter when they're intepreted). 44 | 45 | ### Hotfixes 46 | OpenHotfixLoader follows the standard hotfix format. 47 | 48 | ``` 49 | SparkPatchEntry,(1,1,0,),/Game/PlayerCharacters/_Shared/_Design/Sliding/ControlledMove_Global_Sliding.Default__ControlledMove_Global_Sliding_C,Duration.BaseValueConstant,0,,5000 50 | ``` 51 | 52 | Any line starting with `Spark` is considered to be a hotfix. Everything before the first comma is 53 | taken as the hotfix key, and will have an index automatically appended. Everything after the first 54 | comma is taken as the value. 55 | 56 | Type 11 hotfixes are automatically detected, moved to the front of the hotfix list, and have their 57 | delay hotfixes auto generated. 58 | 59 | ### Inject News Item 60 | You can inject custom news items using `InjectNewsItem` commands. 61 | 62 | ``` 63 | InjectNewsItem,Header,https://url.to/image.png,https://url.to/article,News body, not visible in BL3 64 | ``` 65 | 66 | These commands consist of five comma seperated fields: the command, the header, the image url, the 67 | article url, and the body. You only have to specify the fields you're using, no need for trailing 68 | commas if you only have a header. If you want to include a comma in the header or urls, use csv 69 | escaping - quote it, and use double quotes to insert a literal quote. The body text is always taken 70 | literally (if it exists), no need to escape it. 71 | 72 | ``` 73 | InjectNewsItem,"Header, which contains a comma and a pair of ""quotes""",https://url.to/image.png 74 | ``` 75 | 76 | ### Exec 77 | You can execute another mod file using the `exec` command. This acts as if all the contents of that 78 | file were inserted directly into yours at the command's location. Note that a file can only be 79 | included once, whether that's from an `exec` command, or simply from being in `ohl-mods`, it will 80 | just get loaded the first time it's referenced. 81 | 82 | This command mirrors the format of the actual UE console command - there must be a whitespace 83 | seperator, and you must quote paths including whitespace. 84 | 85 | ``` 86 | exec my_mod.bl3hotfix 87 | exec "D:\My Mods\testing_mod.txt" 88 | ``` 89 | 90 | Paths are taken relative to the `ohl-mods` folder (unless they're absolute to begin with). 91 | 92 | Note that, as a security precaution, exec commands *cannot* be run from files downloaded from a url 93 | (see below). They are simply ignored. 94 | 95 | ### URL 96 | You can download and execute a mod from a url using the `URL=` command. This has all the same 97 | semantics as executing a local file, as discussed above. 98 | 99 | ``` 100 | URL=https://url.to/mod.file 101 | ``` 102 | All content after the `=` is considered part of the url. This syntax is chosen to support Windows' 103 | URL shortcut files. 104 | 105 | ## Misc Notes 106 | If you ever need to debug the exact hotfixes being applied, launch the game with the 107 | `--dump-hotfixes` command line argument. This will create a `hotfixes.dump` in win64 every time 108 | they're loaded. 109 | 110 | If you launch the game with the `--ohl-debug` command line argument, OpenHotfixLoader will print 111 | some more detailed logs messages. 112 | 113 | While not strictly part of OpenHotfixLoader, launching with the `--debug` command line argument will 114 | cause pluginloader to generate an external console window. OpenHotfixLoader's log messages will also 115 | appear here. 116 | 117 | Mod files are expected to be utf8 encoded. 118 | 119 | # Developing 120 | To get started developing: 121 | 122 | 1. Clone the repo (including submodules). 123 | ``` 124 | git clone --recursive https://github.com/apple1417/OpenHotfixLoader/ 125 | ``` 126 | 127 | 2. Choose a preset, and run CMake. Most IDEs will have some form of CMake intergration, or you can 128 | run the commands manually. 129 | ``` 130 | cmake --preset msvc-debug . 131 | cmake --build out/build/msvc-debug 132 | ``` 133 | 134 | Cross compilation on Linux is supported through the `mingw-debug` and `mingw-release` presets. 135 | 136 | 3. (OPTIONAL) Copy `postbuild.template`, and edit it to copy files to your game install directories. 137 | Re-run CMake after doing this, existence is only checked during configuration. 138 | 139 | 4. (OPTIONAL) Copy `user-includes.cmake.template`, and edit it to customize the CMake includes. 140 | One notable use of this is to make sure libcurl gets properly built with zlib (though the code 141 | will work without). 142 | 143 | As before, re-run CMake after doing this, as existence is only checked during configuration. 144 | 145 | 5. (OPTIONAL) If you're debugging a game on Steam, add a `steam_appid.txt` in the same folder as the 146 | executable, containing the game's Steam App Id. 147 | 148 | Normally, games compiled with Steamworks will call 149 | [`SteamAPI_RestartAppIfNecessary`](https://partner.steamgames.com/doc/sdk/api#SteamAPI_RestartAppIfNecessary), 150 | which will drop your debugger session when launching the exe directly - adding this file prevents 151 | that. Not only does this let you debug from entry, it also unlocks some really useful debugger 152 | features which you can't access from just an attach (i.e. Visual Studio's Edit and Continue). 153 | 154 | ## Testing 155 | This project also contains a number of tests, built into a seperate test target using 156 | [doctest](https://github.com/doctest/doctest/blob/master/doc/markdown/commandline.md). The test 157 | executable should be run from the repo root - it looks at some of the files in the `tests` folder, 158 | including via url (on master). 159 | 160 | The test cases mostly just cover the mod loading process, to ensure files are intepreted correctly. 161 | The only way to test the hooks/hotfix injection is to inject the dll and see if it works manually. 162 | -------------------------------------------------------------------------------- /src/hooks.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "processing.h" 4 | #include "unreal.h" 5 | 6 | using ohl::unreal::FJsonObject; 7 | using ohl::unreal::FSparkRequest; 8 | using ohl::unreal::FString; 9 | using ohl::unreal::TSharedPtr; 10 | 11 | namespace ohl::hooks { 12 | 13 | #pragma region Types 14 | 15 | typedef void* (*fmemory_malloc)(size_t count, uint32_t align); 16 | typedef void* (*fmemory_realloc)(void* original, size_t count, uint32_t align); 17 | typedef void (*fmemory_free)(void* data); 18 | typedef int32_t (*get_services_verification)(void* this_api, 19 | FString* uuid, 20 | FString* consumer, 21 | FString* platform, 22 | FString* hardware, 23 | FString* title, 24 | void* g, 25 | void* h, 26 | void* i, 27 | void* j); 28 | typedef bool (*discovery_from_json)(void* this_service, FJsonObject** json); 29 | typedef bool (*news_from_json)(void* this_response, FJsonObject** json); 30 | typedef void (*add_image_to_cache)(void* this_image_manager, TSharedPtr* req); 31 | 32 | /** 33 | * @brief Struct holding all the game functions we scan for. 34 | */ 35 | struct game_functions { 36 | fmemory_malloc malloc; 37 | fmemory_realloc realloc; 38 | fmemory_free free; 39 | get_services_verification get_verification; 40 | discovery_from_json discovery; 41 | news_from_json news; 42 | add_image_to_cache image_cache; 43 | }; 44 | 45 | static game_functions funcs = {}; 46 | 47 | #pragma endregion 48 | 49 | #pragma region Sig Scanning 50 | 51 | /** 52 | * @brief Struct holding information about a sigscan. 53 | */ 54 | struct sigscan_pattern { 55 | const uint8_t* bytes; 56 | const uint8_t* mask; 57 | const size_t size; 58 | }; 59 | 60 | /** 61 | * @brief Helper to convert strings into a sigscan pattern. 62 | * 63 | * @tparam n The length of the strings (should be picked up automatically). 64 | * @return A sigscan pattern. 65 | */ 66 | template 67 | constexpr sigscan_pattern make_pattern(const char (&bytes)[n], const char (&mask)[n]) { 68 | return sigscan_pattern{reinterpret_cast(bytes), 69 | reinterpret_cast(mask), n - 1}; 70 | } 71 | 72 | static const sigscan_pattern malloc_pattern = make_pattern( 73 | "\x48\x89\x5C\x24\x00\x57\x48\x83\xEC\x20\x48\x8B\xF9\x8B\xDA\x48\x8B\x0D\x00\x00\x00\x00\x48" 74 | "\x85\xC9", 75 | "\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF" 76 | "\xFF\xFF"); 77 | 78 | static const sigscan_pattern realloc_pattern = make_pattern( 79 | "\x48\x89\x5C\x24\x00\x48\x89\x74\x24\x00\x57\x48\x83\xEC\x20\x48\x8B\xF1\x41\x8B\xD8\x48\x8B" 80 | "\x0D\x00\x00\x00\x00\x48\x8B\xFA", 81 | "\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" 82 | "\xFF\x00\x00\x00\x00\xFF\xFF\xFF"); 83 | 84 | static const sigscan_pattern free_pattern = make_pattern( 85 | "\x48\x85\xC9\x74\x00\x53\x48\x83\xEC\x20\x48\x8B\xD9\x48\x8B\x0D\x00\x00\x00\x00", 86 | "\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00"); 87 | 88 | static const sigscan_pattern get_verification_pattern = make_pattern( 89 | "\x40\x55\x53\x56\x57\x41\x54\x41\x55\x41\x56\x41\x57\x48\x8D\xAC\x24\x00\x00\x00\x00\x48\x81" 90 | "\xEC\xE8\x02\x00\x00\x48\x8B\x05\x00\x00\x00\x00\x48\x33\xC4\x48\x89\x85\x00\x00\x00\x00\x48" 91 | "\x8B\x85\x00\x00\x00\x00\x4D\x8B\xE0", 92 | "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF" 93 | "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF" 94 | "\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF"); 95 | 96 | static const sigscan_pattern discovery_pattern = make_pattern( 97 | "\x40\x55\x53\x57\x48\x8D\x6C\x24\x00\x48\x81\xEC\x90\x00\x00\x00\x48\x83\x3A\x00\x48\x8B\xDA" 98 | "\x48\x8B\xF9\x75\x00\x32\xC0\x48\x81\xC4\x90\x00\x00\x00\x5F\x5B\x5D\xC3\x4C\x89\xBC\x24\x00" 99 | "\x00\x00\x00", 100 | "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" 101 | "\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00" 102 | "\x00\x00\x00"); 103 | 104 | static const sigscan_pattern news_pattern = make_pattern( 105 | "\x40\x55\x53\x57\x48\x8D\x6C\x24\x00\x48\x81\xEC\x90\x00\x00\x00\x48\x83\x3A\x00\x48\x8B\xDA" 106 | "\x48\x8B\xF9\x75\x00\x32\xC0\x48\x81\xC4\x90\x00\x00\x00\x5F\x5B\x5D\xC3\x48\x89\xB4\x24\x00" 107 | "\x00\x00\x00", 108 | "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" 109 | "\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00" 110 | "\x00\x00\x00"); 111 | 112 | static const sigscan_pattern image_cache_pattern = make_pattern( 113 | "\x40\x55\x41\x54\x41\x55\x41\x56\x48\x8D\x6C\x24\x00\x48\x81\xEC\xA8\x00\x00\x00", 114 | "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF"); 115 | 116 | /** 117 | * @brief Performs a sigscan. 118 | * @note Throws a runtime error on scan failure. 119 | * 120 | * @tparam T The type to cast the return to. 121 | * @param start The address to start the search at. 122 | * @param size The length of the region to search. 123 | * @param pattern The pattern to search for. 124 | * @return The found location, cast to the relevant type. 125 | */ 126 | template 127 | static T sigscan(void* start, size_t size, const sigscan_pattern& pattern) { 128 | // The naive O(nm) search works well enough, even repeating it for each different pattern 129 | for (auto i = 0; i < (size - pattern.size); i++) { 130 | bool found = true; 131 | for (auto j = 0; j < pattern.size; j++) { 132 | auto val = reinterpret_cast(start)[i + j]; 133 | if ((val & pattern.mask[j]) != pattern.bytes[j]) { 134 | found = false; 135 | break; 136 | } 137 | } 138 | if (found) { 139 | return reinterpret_cast(&reinterpret_cast(start)[i]); 140 | } 141 | } 142 | 143 | throw std::runtime_error("Sigscan failed"); 144 | } 145 | 146 | #pragma endregion 147 | 148 | #pragma region Wrappers 149 | 150 | /** 151 | * @brief Detour function for `GbxSparkSdk::Discovery::Api::GetServicesVerification` 152 | * 153 | * @param this_api The api object this was called on. 154 | * @param uuid The authentication uuid. 155 | * @param consumer The consumer of this api call (i.e. `client`). 156 | * @param platform The platform this call is on (i.e. `steam`/`epic`) 157 | * @param hardware The hardware this call is on (i.e. `pc`) 158 | * @param title The title this call is for (i.e. `oak`/`daffodil`) 159 | * @param g ¯\_(ツ)_/¯ 160 | * @param h ¯\_(ツ)_/¯ 161 | * @param i ¯\_(ツ)_/¯ 162 | * @param j ¯\_(ツ)_/¯ 163 | * @return ¯\_(ツ)_/¯ 164 | */ 165 | static get_services_verification original_get_verification = nullptr; 166 | bool detour_get_verification(void* this_api, 167 | FString* uuid, 168 | FString* consumer, 169 | FString* platform, 170 | FString* hardware, 171 | FString* title, 172 | void* g, 173 | void* h, 174 | void* i, 175 | void* j) { 176 | try { 177 | LOGD << "[OHL] Hit GetServicesVerification detour"; 178 | ohl::processing::handle_get_verification(); 179 | } catch (std::exception ex) { 180 | LOGE << "[OHL] Exception occured in get verification hook: " << ex.what(); 181 | } 182 | 183 | return original_get_verification(this_api, uuid, consumer, platform, hardware, title, g, h, i, 184 | j); 185 | } 186 | 187 | /** 188 | * @brief Detour function for `GbxSparkSdk::Discovery::Services::FromJson`. 189 | * 190 | * @param this_service The service object this was called on. 191 | * @param json Unreal json objects containing the received data. 192 | * @return ¯\_(ツ)_/¯ 193 | */ 194 | static discovery_from_json original_discovery_from_json = nullptr; 195 | bool detour_discovery_from_json(void* this_service, FJsonObject** json) { 196 | try { 197 | LOGD << "[OHL] Hit Discovery::Services::FromJson detour"; 198 | ohl::processing::handle_discovery_from_json(json); 199 | } catch (std::exception ex) { 200 | LOGE << "[OHL] Exception occured in discovery hook: " << ex.what(); 201 | } 202 | 203 | return original_discovery_from_json(this_service, json); 204 | } 205 | 206 | /** 207 | * @brief Detour function for `GbxSparkSdk::News::NewsResponse::FromJson`. 208 | * 209 | * @param this_service The service object this was called on. 210 | * @param json Unreal json objects containing the received data. 211 | * @return ¯\_(ツ)_/¯ 212 | */ 213 | static news_from_json original_news_from_json = nullptr; 214 | bool detour_news_from_json(void* this_service, FJsonObject** json) { 215 | try { 216 | LOGD << "[OHL] Hit News::NewsResponse::FromJson detour"; 217 | ohl::processing::handle_news_from_json(json); 218 | } catch (std::exception ex) { 219 | LOGE << "[OHL] Exception occured in news hook: " << ex.what(); 220 | } 221 | 222 | return original_news_from_json(this_service, json); 223 | } 224 | 225 | /** 226 | * @brief Detour function for `FOnlineImageManager::AddImageToFileCache`. 227 | * 228 | * @param this_service The image manager object this was called on. 229 | * @param json A pointer to the spark request for this image. 230 | * @return ¯\_(ツ)_/¯ 231 | */ 232 | static add_image_to_cache original_add_image_to_cache = nullptr; 233 | void detour_add_image_to_cache(void* this_image_manager, TSharedPtr* req) { 234 | bool may_continue = true; 235 | try { 236 | LOGD << "[OHL] Hit AddImageToFileCache detour"; 237 | may_continue = ohl::processing::handle_add_image_to_cache(req); 238 | } catch (std::exception ex) { 239 | LOGE << "[OHL] Exception occured in image cache hook: " << ex.what(); 240 | } 241 | 242 | if (may_continue) { 243 | original_add_image_to_cache(this_image_manager, req); 244 | } 245 | } 246 | 247 | void* malloc_raw(size_t count) { 248 | if (funcs.malloc == nullptr) { 249 | throw std::runtime_error("Tried to call malloc, which was not found!"); 250 | } 251 | auto ret = funcs.malloc(count, 8); 252 | if (ret == nullptr) { 253 | throw std::runtime_error("Failed to allocate memory!"); 254 | } 255 | memset(ret, 0, count); 256 | return ret; 257 | } 258 | 259 | void* realloc_raw(void* original, size_t count) { 260 | if (funcs.realloc == nullptr) { 261 | throw std::runtime_error("Tried to call realloc, which was not found!"); 262 | } 263 | auto ret = funcs.realloc(original, count, 8); 264 | if (ret == nullptr) { 265 | throw std::runtime_error("Failed to re-allocate memory!"); 266 | } 267 | return ret; 268 | } 269 | 270 | void free(void* data) { 271 | if (funcs.free == nullptr) { 272 | throw std::runtime_error("Tried to call free, which was not found!"); 273 | } 274 | funcs.free(data); 275 | } 276 | 277 | #pragma endregion 278 | 279 | void init(void) { 280 | LOGD << "[OHL] Initalizing hooks"; 281 | 282 | auto exe_module = GetModuleHandle(NULL); 283 | 284 | MEMORY_BASIC_INFORMATION mem; 285 | if (!VirtualQuery(reinterpret_cast(exe_module), &mem, sizeof(mem))) { 286 | throw std::runtime_error("VirtualQuery failed!"); 287 | } 288 | 289 | uint8_t* allocation_base = (uint8_t*)mem.AllocationBase; 290 | if (allocation_base == nullptr) { 291 | throw std::runtime_error("AllocationBase was NULL!"); 292 | } 293 | 294 | auto dos = reinterpret_cast(allocation_base); 295 | auto pe = reinterpret_cast(allocation_base + dos->e_lfanew); 296 | auto module_length = pe->OptionalHeader.SizeOfImage; 297 | 298 | LOGD << "[OHL] Sigscanning"; 299 | 300 | funcs.malloc = sigscan(allocation_base, module_length, malloc_pattern); 301 | funcs.realloc = sigscan(allocation_base, module_length, realloc_pattern); 302 | funcs.free = sigscan(allocation_base, module_length, free_pattern); 303 | funcs.get_verification = sigscan(allocation_base, module_length, 304 | get_verification_pattern); 305 | funcs.discovery = 306 | sigscan(allocation_base, module_length, discovery_pattern); 307 | funcs.news = sigscan(allocation_base, module_length, news_pattern); 308 | funcs.image_cache = 309 | sigscan(allocation_base, module_length, image_cache_pattern); 310 | 311 | LOGD << "[OHL] Injecting detours"; 312 | 313 | auto ret = MH_Initialize(); 314 | if (ret != MH_OK) { 315 | throw std::runtime_error("MH_Initialize failed " + std::to_string(ret)); 316 | } 317 | 318 | ret = MH_CreateHook((LPVOID)funcs.get_verification, (LPVOID)&detour_get_verification, 319 | reinterpret_cast(&original_get_verification)); 320 | if (ret != MH_OK) { 321 | throw std::runtime_error("MH_CreateHook failed " + std::to_string(ret)); 322 | } 323 | ret = MH_EnableHook((LPVOID)funcs.get_verification); 324 | if (ret != MH_OK) { 325 | throw std::runtime_error("MH_EnableHook failed " + std::to_string(ret)); 326 | } 327 | 328 | ret = MH_CreateHook((LPVOID)funcs.discovery, (LPVOID)&detour_discovery_from_json, 329 | reinterpret_cast(&original_discovery_from_json)); 330 | if (ret != MH_OK) { 331 | throw std::runtime_error("MH_CreateHook failed " + std::to_string(ret)); 332 | } 333 | ret = MH_EnableHook((LPVOID)funcs.discovery); 334 | if (ret != MH_OK) { 335 | throw std::runtime_error("MH_EnableHook failed " + std::to_string(ret)); 336 | } 337 | 338 | ret = MH_CreateHook((LPVOID)funcs.news, (LPVOID)&detour_news_from_json, 339 | reinterpret_cast(&original_news_from_json)); 340 | if (ret != MH_OK) { 341 | throw std::runtime_error("MH_CreateHook failed " + std::to_string(ret)); 342 | } 343 | ret = MH_EnableHook((LPVOID)funcs.news); 344 | if (ret != MH_OK) { 345 | throw std::runtime_error("MH_EnableHook failed " + std::to_string(ret)); 346 | } 347 | 348 | ret = MH_CreateHook((LPVOID)funcs.image_cache, (LPVOID)&detour_add_image_to_cache, 349 | reinterpret_cast(&original_add_image_to_cache)); 350 | if (ret != MH_OK) { 351 | throw std::runtime_error("MH_CreateHook failed " + std::to_string(ret)); 352 | } 353 | ret = MH_EnableHook((LPVOID)funcs.image_cache); 354 | if (ret != MH_OK) { 355 | throw std::runtime_error("MH_EnableHook failed " + std::to_string(ret)); 356 | } 357 | 358 | LOGI << "[OHL] Hooks injected successfully"; 359 | } 360 | 361 | } // namespace ohl::hooks 362 | -------------------------------------------------------------------------------- /src/processing.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "args.h" 4 | #include "hooks.h" 5 | #include "loader.h" 6 | #include "unreal.h" 7 | #include "util.h" 8 | 9 | using namespace ohl::unreal; 10 | 11 | namespace ohl::processing { 12 | 13 | static const auto HOTFIX_COUNTER_OFFSET = 100000; 14 | static const std::filesystem::path HOTFIX_DUMP_FILE = "hotfixes.dump"; 15 | 16 | /** 17 | * @brief Struct holding all the vf tables we need to grab copies of. 18 | */ 19 | struct vf_tables { 20 | bool found; 21 | void* json_value_string; 22 | void* json_value_array; 23 | void* json_value_object; 24 | void* shared_ptr_json_object; 25 | void* shared_ptr_json_value; 26 | }; 27 | 28 | static vf_tables vf_table = {}; 29 | 30 | // Haven't managed to fully reverse engineer json objects, but their extra data is always constant 31 | // for objects with the same amount of entries, so as a hack we can just hardcode it 32 | 33 | // clang-format off 34 | static const uint32_t KNOWN_OBJECT_PATTERNS[3][16] = {{ 35 | // Objects of size 1 36 | 0x00000001, 0x00000000, 0x00000000, 0x00000000, 37 | 0x00000000, 0x00000000, 0x00000001, 0x00000080, 38 | 0xFFFFFFFF, 0x00000000, 0x00000000, 0x00000000, 39 | 0x00000000, 0x00000000, 0x00000001, 0x00000000, 40 | }, { 41 | // Objects of size 2 42 | 0x00000003, 0x00000000, 0x00000000, 0x00000000, 43 | 0x00000000, 0x00000000, 0x00000002, 0x00000080, 44 | 0xFFFFFFFF, 0x00000000, 0x00000001, 0x00000000, 45 | 0x00000000, 0x00000000, 0x00000001, 0x00000000, 46 | }, { 47 | // Objects of size 3 48 | 0x00000007, 0x00000000, 0x00000000, 0x00000000, 49 | 0x00000000, 0x00000000, 0x00000003, 0x00000080, 50 | 0xFFFFFFFF, 0x00000000, 0x00000002, 0x00000000, 51 | 0x00000000, 0x00000000, 0x00000001, 0x00000000, 52 | }}; 53 | // clang-format on 54 | 55 | static_assert(sizeof(*KNOWN_OBJECT_PATTERNS) == sizeof(FJsonObject::pattern)); 56 | 57 | /** 58 | * @brief Gathers all required vf table pointers and fills in the vf table struct. 59 | * 60 | * @param discovery_json The json object received by the discovery hook. 61 | */ 62 | static void gather_vf_tables(const FJsonObject* discovery_json) { 63 | if (vf_table.found) { 64 | return; 65 | } 66 | 67 | LOGD << "[OHL] Grabbing vftable pointers"; 68 | 69 | auto services = discovery_json->get(L"services"); 70 | vf_table.json_value_array = services->vf_table; 71 | vf_table.shared_ptr_json_value = services->entries.data[0].ref_controller->vf_table; 72 | 73 | auto first_service = services->get(0); 74 | vf_table.json_value_object = first_service->vf_table; 75 | vf_table.shared_ptr_json_object = first_service->value.ref_controller->vf_table; 76 | 77 | auto group = first_service->to_obj()->get(L"configuration_group"); 78 | vf_table.json_value_string = group->vf_table; 79 | 80 | vf_table.found = true; 81 | } 82 | 83 | /** 84 | * @brief Adds a reference controller to a shared pointer. 85 | * @note The object must already be set before calling this. 86 | * 87 | * @tparam T The type of the shared pointer. 88 | * @param ptr The shared pointer to edit. 89 | * @param vf_table The vf table for the shared pointer's type. 90 | */ 91 | template 92 | static void add_ref_controller(TSharedPtr* ptr, void* vf_table) { 93 | ptr->ref_controller = 94 | ohl::hooks::malloc(sizeof(FReferenceControllerBase)); 95 | ptr->ref_controller->vf_table = vf_table; 96 | ptr->ref_controller->ref_count = 1; 97 | ptr->ref_controller->weak_ref_count = 1; 98 | ptr->ref_controller->obj = ptr->obj; 99 | } 100 | 101 | /** 102 | * @brief Allocated memory to set an FString to a given value. 103 | * @note Also sets the string count. 104 | * 105 | * @param str The FString to fill. 106 | * @param value The value to set. 107 | */ 108 | static void alloc_string(FString* str, const std::string& value) { 109 | auto wide = ohl::util::widen(value); 110 | str->count = wide.size() + 1; 111 | str->max = str->count; 112 | str->data = ohl::hooks::malloc(str->count * sizeof(wchar_t)); 113 | wcsncpy(str->data, wide.c_str(), str->count - 1); 114 | str->data[str->count - 1] = '\0'; 115 | } 116 | 117 | /** 118 | * @brief Creates a json string object. 119 | * 120 | * @param value The value of the string. 121 | * @return A pointer to the new object. 122 | */ 123 | static FJsonValueString* create_json_string(const std::string& value) { 124 | auto obj = ohl::hooks::malloc(sizeof(FJsonValueString)); 125 | obj->vf_table = vf_table.json_value_string; 126 | obj->type = EJson::String; 127 | alloc_string(&obj->str, value); 128 | 129 | return obj; 130 | } 131 | 132 | /** 133 | * @brief Creates a json object. 134 | * 135 | * @tparam n The amount of entries in the object. 136 | * @param entries Key-value pairs of the object's entries. 137 | * @return A pointer to the new object. 138 | */ 139 | template 140 | static FJsonObject* create_json_object( 141 | const std::array, n>& entries) { 142 | static_assert(0 < n && n <= ARRAYSIZE(KNOWN_OBJECT_PATTERNS)); 143 | 144 | auto obj = ohl::hooks::malloc(sizeof(FJsonObject)); 145 | memcpy(obj->pattern, KNOWN_OBJECT_PATTERNS[n - 1], sizeof(obj->pattern)); 146 | 147 | obj->entries.count = n; 148 | obj->entries.max = n; 149 | obj->entries.data = ohl::hooks::malloc(n * sizeof(JSONObjectEntry)); 150 | 151 | for (auto i = 0; i < n; i++) { 152 | obj->entries.data[i].hash_next_id = ((int32_t)i) - 1; 153 | 154 | alloc_string(&obj->entries.data[i].key, entries[i].first); 155 | 156 | obj->entries.data[i].value.obj = entries[i].second; 157 | add_ref_controller(&obj->entries.data[i].value, vf_table.shared_ptr_json_value); 158 | } 159 | 160 | return obj; 161 | } 162 | 163 | /** 164 | * @brief Create a json array object. 165 | * 166 | * @param entries The entries in the array. 167 | * @return A pointer to the new object 168 | */ 169 | static FJsonValueArray* create_json_array(const std::vector& entries) { 170 | auto obj = ohl::hooks::malloc(sizeof(FJsonValueArray)); 171 | obj->vf_table = vf_table.json_value_array; 172 | obj->type = EJson::Array; 173 | 174 | const auto n = entries.size(); 175 | 176 | obj->entries.count = n; 177 | obj->entries.max = n; 178 | obj->entries.data = 179 | ohl::hooks::malloc>(n * sizeof(TSharedPtr)); 180 | 181 | for (auto i = 0; i < n; i++) { 182 | obj->entries.data[i].obj = entries[i]; 183 | add_ref_controller(&obj->entries.data[i], vf_table.shared_ptr_json_value); 184 | } 185 | 186 | return obj; 187 | } 188 | 189 | /** 190 | * @brief Create a json value object from a raw object. 191 | * 192 | * @param obj The object to create a value object of. 193 | * @return A pointer to the new value object. 194 | */ 195 | static FJsonValueObject* create_json_value_object(const FJsonObject* obj) { 196 | auto val_obj = ohl::hooks::malloc(sizeof(FJsonValueObject)); 197 | val_obj->vf_table = vf_table.json_value_object; 198 | val_obj->type = EJson::Object; 199 | 200 | val_obj->value.obj = const_cast(obj); 201 | add_ref_controller(&val_obj->value, vf_table.shared_ptr_json_object); 202 | 203 | return val_obj; 204 | } 205 | 206 | /** 207 | * @brief Gets the current time in an iso8601-formatted string. 208 | * 209 | * @return The current time string. 210 | */ 211 | static std::string get_current_time_str(void) { 212 | auto time = std::time(nullptr); 213 | auto utc = std::gmtime(&time); 214 | 215 | // Only need 25 but let's be safe 216 | char buf[64] = {}; 217 | strftime(buf, sizeof(buf), "%FT%T.000Z", utc); 218 | 219 | return std::string(buf); 220 | } 221 | 222 | void handle_get_verification(void) { 223 | LOGI << "[OHL] Starting to reload mods"; 224 | ohl::loader::reload(); 225 | } 226 | 227 | void handle_discovery_from_json(FJsonObject** json) { 228 | gather_vf_tables(*json); 229 | 230 | auto services = (*json)->get(L"services"); 231 | 232 | FJsonObject* micropatch = nullptr; 233 | for (auto i = 0; i < services->count(); i++) { 234 | auto service = services->get(i)->to_obj(); 235 | if (service->get(L"service_name")->to_wstr() == L"Micropatch") { 236 | micropatch = service; 237 | break; 238 | } 239 | } 240 | 241 | // This happens during the first verify call so don't throw 242 | if (micropatch == nullptr) { 243 | return; 244 | } 245 | 246 | if (!vf_table.found) { 247 | throw std::runtime_error("Didn't find vf tables in time!"); 248 | } 249 | 250 | auto hotfixes = ohl::loader::get_hotfixes(); 251 | 252 | LOGD << "[OHL] Allocating space for hotfixes"; 253 | 254 | auto params = micropatch->get(L"parameters"); 255 | auto new_hotfix_count = params->entries.count + hotfixes.size(); 256 | if (new_hotfix_count > params->entries.max) { 257 | params->entries.max = new_hotfix_count; 258 | params->entries.data = ohl::hooks::realloc>( 259 | params->entries.data, params->entries.max * sizeof(TSharedPtr)); 260 | } 261 | 262 | LOGD << "[OHL] Injecting hotfixes"; 263 | 264 | auto i = params->entries.count; 265 | for (const auto& [key, value] : hotfixes) { 266 | auto hotfix_entry = create_json_object<2>( 267 | {{{"key", create_json_string(key + std::to_string(i + HOTFIX_COUNTER_OFFSET))}, 268 | {"value", create_json_string(value)}}}); 269 | 270 | params->entries.data[i].obj = create_json_value_object(hotfix_entry); 271 | add_ref_controller(¶ms->entries.data[i], vf_table.shared_ptr_json_value); 272 | i++; 273 | } 274 | 275 | params->entries.count = new_hotfix_count; 276 | 277 | LOGI << "[OHL] Injected hotfixes"; 278 | 279 | if (ohl::args::dump_hotfixes()) { 280 | LOGD << "[OHL] Dumping hotfixes"; 281 | 282 | // For some god forsaken reason the default behaviour of **w**ofstream is to output ascii. 283 | // Since encodings are a pain, just write directly in binary. 284 | // This also means we can actually use this to double check our utf8-utf16 conversion works 285 | std::fstream dump(HOTFIX_DUMP_FILE, std::ios::out | std::ios::binary | std::ios::trunc); 286 | if (!dump.is_open()) { 287 | LOGE << "[OHL] Failed to open dump file!"; 288 | return; 289 | } 290 | 291 | // Since it should look like utf16, add a BOM 292 | dump.put(0xFF); 293 | dump.put(0xFE); 294 | 295 | for (auto i = 0; i < params->count(); i++) { 296 | auto entry = params->get(i)->to_obj(); 297 | auto key = entry->get(L"key")->str; 298 | auto value = entry->get(L"value")->str; 299 | 300 | static_assert(sizeof(wchar_t) % sizeof(char) == 0); 301 | const auto chars_per_wchar = sizeof(wchar_t) / sizeof(char); 302 | 303 | dump.write(reinterpret_cast(key.data), (key.count - 1) * chars_per_wchar); 304 | 305 | dump.put(':'); 306 | dump.put(0x00); 307 | dump.put(' '); 308 | dump.put(0x00); 309 | 310 | dump.write(reinterpret_cast(value.data), (value.count - 1) * chars_per_wchar); 311 | 312 | dump.put('\n'); 313 | dump.put(0x00); 314 | } 315 | dump.close(); 316 | LOGI << "[OHL] Dumped hotfixes to file"; 317 | } 318 | } 319 | 320 | void handle_news_from_json(ohl::unreal::FJsonObject** json) { 321 | if (!vf_table.found) { 322 | throw std::runtime_error("Didn't find vf tables in time!"); 323 | } 324 | 325 | auto news_items = ohl::loader::get_news_items(); 326 | 327 | LOGD << "[OHL] Allocating space for news items"; 328 | 329 | auto news_data = (*json)->get(L"data"); 330 | auto new_news_data_size = news_data->entries.count + news_items.size(); 331 | if (new_news_data_size > news_data->entries.max) { 332 | news_data->entries.max = new_news_data_size; 333 | news_data->entries.data = ohl::hooks::realloc>( 334 | news_data->entries.data, news_data->entries.max * sizeof(TSharedPtr)); 335 | } 336 | 337 | // Shift the existing entries so that the injected ones appear at the front 338 | memmove(&news_data->entries.data[news_items.size()], &news_data->entries.data[0], 339 | news_data->entries.count * sizeof(TSharedPtr)); 340 | 341 | LOGD << "[OHL] Injecting news items"; 342 | 343 | auto i = 0; 344 | for (const auto& news_item : news_items) { 345 | auto contents_obj = 346 | create_json_object<2>({{{"header", create_json_string(news_item.header)}, 347 | {"body", create_json_string(news_item.body)}}}); 348 | auto contents_arr = create_json_array({create_json_value_object(contents_obj)}); 349 | 350 | auto image_meta_tag_obj = 351 | create_json_object<1>({{{"tag", create_json_string("img_game_sm_noloc")}}}); 352 | auto image_tags_obj = 353 | create_json_object<2>({{{"meta_tag", create_json_value_object(image_meta_tag_obj)}, 354 | {"value", create_json_string(news_item.image_url)}}}); 355 | 356 | auto article_meta_tag_obj = 357 | create_json_object<1>({{{"tag", create_json_string("url_learn_more_noloc")}}}); 358 | auto article_tags_obj = 359 | create_json_object<2>({{{"meta_tag", create_json_value_object(article_meta_tag_obj)}, 360 | {"value", create_json_string(news_item.article_url)}}}); 361 | 362 | auto tags_arr = create_json_array( 363 | {create_json_value_object(image_tags_obj), create_json_value_object(article_tags_obj)}); 364 | 365 | auto availablities_obj = 366 | create_json_object<1>({{{"startTime", create_json_string(get_current_time_str())}}}); 367 | auto availabilities_arr = create_json_array({create_json_value_object(availablities_obj)}); 368 | 369 | auto news_obj = create_json_object<3>({{{"contents", contents_arr}, 370 | {"article_tags", tags_arr}, 371 | {"availabilities", availabilities_arr}}}); 372 | 373 | news_data->entries.data[i].obj = create_json_value_object(news_obj); 374 | add_ref_controller(&news_data->entries.data[i], vf_table.shared_ptr_json_value); 375 | i++; 376 | } 377 | 378 | news_data->entries.count = new_news_data_size; 379 | 380 | LOGI << "[OHL] Injected news"; 381 | } 382 | 383 | bool handle_add_image_to_cache(TSharedPtr* req) { 384 | auto url = ohl::util::narrow(req->obj->get_url()); 385 | 386 | auto news_items = ohl::loader::get_news_items(); 387 | auto may_continue = std::find_if(news_items.begin(), news_items.end(), 388 | [&](auto item) { return item.image_url == url; }) 389 | == news_items.end(); 390 | 391 | if (!may_continue) { 392 | LOGI << "[OHL] Prevented news icon from being cached: " << url; 393 | } 394 | 395 | return may_continue; 396 | } 397 | 398 | } // namespace ohl::processing 399 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | 676 | -------------------------------------------------------------------------------- 677 | 678 | MinHook - The Minimalistic API Hooking Library for x64/x86 679 | Copyright (C) 2009-2017 Tsuda Kageyu. 680 | All rights reserved. 681 | 682 | Redistribution and use in source and binary forms, with or without 683 | modification, are permitted provided that the following conditions 684 | are met: 685 | 686 | 1. Redistributions of source code must retain the above copyright 687 | notice, this list of conditions and the following disclaimer. 688 | 2. Redistributions in binary form must reproduce the above copyright 689 | notice, this list of conditions and the following disclaimer in the 690 | documentation and/or other materials provided with the distribution. 691 | 692 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 693 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 694 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 695 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER 696 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 697 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 698 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 699 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 700 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 701 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 702 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 703 | 704 | ================================================================================ 705 | Portions of this software are Copyright (c) 2008-2009, Vyacheslav Patkov. 706 | ================================================================================ 707 | Hacker Disassembler Engine 32 C 708 | Copyright (c) 2008-2009, Vyacheslav Patkov. 709 | All rights reserved. 710 | 711 | Redistribution and use in source and binary forms, with or without 712 | modification, are permitted provided that the following conditions 713 | are met: 714 | 715 | 1. Redistributions of source code must retain the above copyright 716 | notice, this list of conditions and the following disclaimer. 717 | 2. Redistributions in binary form must reproduce the above copyright 718 | notice, this list of conditions and the following disclaimer in the 719 | documentation and/or other materials provided with the distribution. 720 | 721 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 722 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 723 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 724 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR 725 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 726 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 727 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 728 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 729 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 730 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 731 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 732 | 733 | ------------------------------------------------------------------------------- 734 | Hacker Disassembler Engine 64 C 735 | Copyright (c) 2008-2009, Vyacheslav Patkov. 736 | All rights reserved. 737 | 738 | Redistribution and use in source and binary forms, with or without 739 | modification, are permitted provided that the following conditions 740 | are met: 741 | 742 | 1. Redistributions of source code must retain the above copyright 743 | notice, this list of conditions and the following disclaimer. 744 | 2. Redistributions in binary form must reproduce the above copyright 745 | notice, this list of conditions and the following disclaimer in the 746 | documentation and/or other materials provided with the distribution. 747 | 748 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 749 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 750 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 751 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR 752 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 753 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 754 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 755 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 756 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 757 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 758 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 759 | 760 | -------------------------------------------------------------------------------- 761 | 762 | cpr - C++ Requests: Curl for People, a spiritual port of Python Requests. 763 | 764 | This license applies to everything except the contents of the "test" 765 | directory and its subdirectories. 766 | 767 | MIT License 768 | 769 | Copyright (c) 2017-2021 Huu Nguyen 770 | Copyright (c) 2022 libcpr and many other contributors 771 | 772 | Permission is hereby granted, free of charge, to any person obtaining a copy 773 | of this software and associated documentation files (the "Software"), to deal 774 | in the Software without restriction, including without limitation the rights 775 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 776 | copies of the Software, and to permit persons to whom the Software is 777 | furnished to do so, subject to the following conditions: 778 | 779 | The above copyright notice and this permission notice shall be included in all 780 | copies or substantial portions of the Software. 781 | 782 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 783 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 784 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 785 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 786 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 787 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 788 | SOFTWARE. 789 | 790 | -------------------------------------------------------------------------------- 791 | 792 | plog - Portable, simple and extensible C++ logging library 793 | 794 | MIT License 795 | 796 | Copyright (c) 2022 Sergey Podobry 797 | 798 | Permission is hereby granted, free of charge, to any person obtaining a copy 799 | of this software and associated documentation files (the "Software"), to deal 800 | in the Software without restriction, including without limitation the rights 801 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 802 | copies of the Software, and to permit persons to whom the Software is 803 | furnished to do so, subject to the following conditions: 804 | 805 | The above copyright notice and this permission notice shall be included in all 806 | copies or substantial portions of the Software. 807 | 808 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 809 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 810 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 811 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 812 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 813 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 814 | SOFTWARE. 815 | -------------------------------------------------------------------------------- /src/loader.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "args.h" 6 | #include "loader.h" 7 | #include "util.h" 8 | #include "version.h" 9 | 10 | using mod_file_identifier = std::string; 11 | 12 | namespace ohl::loader { 13 | TEST_SUITE_BEGIN("loader"); 14 | 15 | #pragma region Consts 16 | 17 | class mod_file; 18 | 19 | static const std::string WHITESPACE = " \f\n\r\t\b"; 20 | 21 | static const std::string HOTFIX_COMMAND = "spark"; 22 | static const std::string NEWS_COMMAND = "injectnewsitem"; 23 | static const std::string EXEC_COMMAND = "exec"; 24 | static const std::string URL_COMMAND = "url="; 25 | 26 | static const std::string TYPE_11_DELAY_TYPE = "SparkEarlyLevelPatchEntry"; 27 | static const std::string TYPE_11_DELAY_VALUE = 28 | "(1,1,0,{map}),/Game/Pickups/Ammo/" 29 | "BPAmmoItem_Pistol.Default__BPAmmoItem_Pistol_C,ItemMeshComponent.Object..StaticMesh,0,," 30 | "StaticMesh'\"{mesh}\"'"; 31 | static const std::vector TYPE_11_DELAY_MESHES = { 32 | "/Engine/EditorMeshes/Camera/SM_CraneRig_Arm.SM_CraneRig_Arm", 33 | "/Engine/EditorMeshes/Camera/SM_CraneRig_Base.SM_CraneRig_Base", 34 | "/Engine/EditorMeshes/Camera/SM_CraneRig_Body.SM_CraneRig_Body", 35 | "/Engine/EditorMeshes/Camera/SM_CraneRig_Mount.SM_CraneRig_Mount", 36 | "/Engine/EditorMeshes/Camera/SM_RailRig_Mount.SM_RailRig_Mount", 37 | "/Engine/EditorMeshes/Camera/SM_RailRig_Track.SM_RailRig_Track", 38 | "/Game/Pickups/Ammo/Model/Meshes/SM_ammo_pistol.SM_ammo_pistol", 39 | }; 40 | 41 | static const std::string OHL_NEWS_ITEM_IMAGE_URL = OHL_GITHUB_RAW_URL "news_icon.png"; 42 | 43 | static const std::string OHL_NEWS_ITEM_ARTICLE_URL = OHL_GITHUB_URL "releases"; 44 | 45 | #pragma endregion 46 | 47 | // This default works relative to the cwd, we'll try replace it later. 48 | static std::filesystem::path mod_dir = "ohl-mods"; 49 | 50 | static std::mutex known_mod_files_mutex; 51 | static std::unordered_map> known_mod_files{}; 52 | 53 | #pragma region Types 54 | 55 | /** 56 | * @brief Class holding all the data that can be extracted from a region of a mod file. 57 | */ 58 | class mod_data { 59 | public: 60 | std::deque hotfixes{}; 61 | std::vector type_11_hotfixes{}; 62 | std::unordered_set type_11_maps{}; 63 | std::deque news_items{}; 64 | 65 | /** 66 | * @brief Checks if this object holds no mod data. 67 | * 68 | * @return True if empty, false otherwise. 69 | */ 70 | bool is_empty(void) const { 71 | return this->hotfixes.size() == 0 && this->type_11_hotfixes.size() == 0 72 | && this->type_11_maps.size() == 0 && this->news_items.size() == 0; 73 | } 74 | 75 | /** 76 | * @brief Appends the mod data from this object to the end of that of another. 77 | * 78 | * @param other The other mod data object to append to. 79 | */ 80 | void append_to(mod_data& other) const { 81 | other.hotfixes.insert(other.hotfixes.end(), this->hotfixes.begin(), this->hotfixes.end()); 82 | other.type_11_hotfixes.insert(other.type_11_hotfixes.end(), this->type_11_hotfixes.begin(), 83 | this->type_11_hotfixes.end()); 84 | other.type_11_maps.insert(this->type_11_maps.begin(), this->type_11_maps.end()); 85 | other.news_items.insert(other.news_items.end(), this->news_items.begin(), 86 | this->news_items.end()); 87 | } 88 | }; 89 | 90 | TEST_CASE("loader::mod_data::append_to") { 91 | const mod_data data_a{{{"SparkPatchEntry", "(1,1,0,),/Some/Hotfix/Here"}, 92 | {"SparkPatchEntry", "(1,1,0,),/Another/Hotfix/"}}, 93 | {{"SparkEarlyLevelPatchEntry", "(1,11,0,SomeMap_P)"}}, 94 | {"SomeMap_P"}, 95 | {{"header", "image", "article", "body"}}}; 96 | 97 | const mod_data data_b{{{"Key", "Value"}}, {}, {}, {}}; 98 | 99 | const mod_data data_empty{}; 100 | 101 | const std::deque expected_a_append_to_b_hotfixes = { 102 | {"Key", "Value"}, 103 | {"SparkPatchEntry", "(1,1,0,),/Some/Hotfix/Here"}, 104 | {"SparkPatchEntry", "(1,1,0,),/Another/Hotfix/"}, 105 | }; 106 | 107 | const std::deque expected_b_append_to_a_hotfixes = { 108 | {"SparkPatchEntry", "(1,1,0,),/Some/Hotfix/Here"}, 109 | {"SparkPatchEntry", "(1,1,0,),/Another/Hotfix/"}, 110 | {"Key", "Value"}, 111 | }; 112 | 113 | SUBCASE("against empty") { 114 | mod_data data_a_copy = data_a; 115 | 116 | data_empty.append_to(data_a_copy); 117 | 118 | CHECK(ITERABLE_EQUAL(data_a_copy.hotfixes, data_a.hotfixes)); 119 | CHECK(ITERABLE_EQUAL(data_a_copy.type_11_hotfixes, data_a.type_11_hotfixes)); 120 | CHECK(ITERABLE_EQUAL(data_a_copy.type_11_maps, data_a.type_11_maps)); 121 | CHECK(ITERABLE_EQUAL(data_a_copy.news_items, data_a.news_items)); 122 | } 123 | 124 | SUBCASE("against filled") { 125 | mod_data data_a_copy = data_a; 126 | mod_data data_b_copy = data_b; 127 | 128 | data_a.append_to(data_b_copy); 129 | data_b.append_to(data_a_copy); 130 | 131 | CHECK(ITERABLE_EQUAL(data_a_copy.hotfixes, expected_b_append_to_a_hotfixes)); 132 | 133 | CHECK(ITERABLE_EQUAL(data_a_copy.type_11_hotfixes, data_a.type_11_hotfixes)); 134 | CHECK(ITERABLE_EQUAL(data_a_copy.type_11_maps, data_a.type_11_maps)); 135 | CHECK(ITERABLE_EQUAL(data_a_copy.news_items, data_a.news_items)); 136 | 137 | CHECK(ITERABLE_EQUAL(data_b_copy.hotfixes, expected_a_append_to_b_hotfixes)); 138 | 139 | CHECK(ITERABLE_EQUAL(data_b_copy.type_11_hotfixes, data_a.type_11_hotfixes)); 140 | CHECK(ITERABLE_EQUAL(data_b_copy.type_11_maps, data_a.type_11_maps)); 141 | CHECK(ITERABLE_EQUAL(data_b_copy.news_items, data_a.news_items)); 142 | } 143 | } 144 | 145 | TEST_CASE("loader::mod_data::is_empty") { 146 | mod_data data{}; 147 | REQUIRE(data.is_empty() == true); 148 | 149 | mod_data data_hotfix{}; 150 | data_hotfix.hotfixes.emplace_back(); 151 | 152 | mod_data data_type_11_hotfix{}; 153 | data_type_11_hotfix.type_11_hotfixes.emplace_back(); 154 | 155 | mod_data data_type_11_map{}; 156 | data_type_11_map.type_11_maps.insert(""); 157 | 158 | mod_data data_news{}; 159 | data_news.news_items.emplace_back(); 160 | 161 | CHECK(data_hotfix.is_empty() == false); 162 | CHECK(data_type_11_hotfix.is_empty() == false); 163 | CHECK(data_type_11_map.is_empty() == false); 164 | CHECK(data_news.is_empty() == false); 165 | } 166 | 167 | /** 168 | * @brief Class representing mod data coming from another file. 169 | */ 170 | class remote_mod_data { 171 | public: 172 | const mod_file_identifier identifier; 173 | 174 | remote_mod_data(const mod_file_identifier& identifier) : identifier(identifier) {} 175 | }; 176 | 177 | /** 178 | * @brief Base class holding a section of a mod file, and some metadata about it. 179 | */ 180 | class mod_file { 181 | protected: 182 | void load_from_stream(std::istream& stream, bool allow_exec); 183 | 184 | /** 185 | * @brief Adds a mod data object to this file's sections. 186 | * 187 | * @param data The mod data to add. 188 | */ 189 | void push_mod_data(const mod_data& data) { 190 | if (!data.is_empty()) { 191 | this->sections.push_back(data); 192 | } 193 | } 194 | 195 | /** 196 | * @brief Adds a remote to this file's sections, as well as to the global list of known files. 197 | * @note If the file was not previously known, starts loading it. 198 | * @note Edits the global list atomically. 199 | * 200 | * @param file The file to register. 201 | */ 202 | void register_remote_file(const std::shared_ptr file) { 203 | auto identifier = file->get_identifier(); 204 | this->sections.emplace_back(identifier); 205 | 206 | bool is_known = true; 207 | 208 | { 209 | std::lock_guard lock(known_mod_files_mutex); 210 | 211 | if (known_mod_files.find(identifier) == known_mod_files.end()) { 212 | known_mod_files[identifier] = file; 213 | is_known = false; 214 | } 215 | } 216 | 217 | if (!is_known) { 218 | file->load(); 219 | } 220 | } 221 | 222 | public: 223 | std::vector> sections; 224 | 225 | /** 226 | * @brief Appends all the mod data from this file to the end of a mod data object. 227 | * @note Recursively looks up remote files. 228 | * @note Calls join, will block until the data is ready. 229 | * 230 | * @param data The mod data object to append to. 231 | * @param seen_files A list of files which have already been seen. Any nested files which have 232 | * yet to be seen will be appended in the order encountered. Optional. 233 | */ 234 | void append_to(mod_data& data) { 235 | std::vector seen_files; 236 | this->append_to(data, seen_files); 237 | } 238 | void append_to(mod_data& data, std::vector& seen_files) { 239 | this->join(); 240 | 241 | for (const auto& section : this->sections) { 242 | if (std::holds_alternative(section)) { 243 | std::get(section).append_to(data); 244 | } else { 245 | auto file = known_mod_files.at(std::get(section).identifier); 246 | 247 | auto identifier = file->get_identifier(); 248 | if (std::find(seen_files.begin(), seen_files.end(), identifier) 249 | != seen_files.end()) { 250 | LOGD << "[OHL] Already seen " << identifier; 251 | continue; 252 | } else { 253 | LOGD << "[OHL] Appending mod data for " << identifier; 254 | } 255 | seen_files.push_back(identifier); 256 | 257 | file->append_to(data, seen_files); 258 | } 259 | } 260 | } 261 | 262 | /** 263 | * @brief Gets a unique identifier for this mod file. 264 | * 265 | * @return The mod's identifier. 266 | */ 267 | virtual mod_file_identifier get_identifier(void) const = 0; 268 | 269 | /** 270 | * @brief Gets the display name of this mod file. 271 | * 272 | * @return The mod's display name. 273 | */ 274 | virtual std::string get_display_name(void) const = 0; 275 | 276 | /** 277 | * @brief Attempts to load the contents of this mod file. 278 | * @note May be async, must call join to ensure this has finished. 279 | */ 280 | virtual void load(void) = 0; 281 | 282 | /** 283 | * @brief Joins any threads started by loading this mod file. 284 | */ 285 | virtual void join(void) {} 286 | }; 287 | 288 | /** 289 | * @brief Class for mod file data based on a local file. 290 | */ 291 | class mod_file_local : public mod_file { 292 | public: 293 | const std::filesystem::path path; 294 | 295 | mod_file_local(const std::filesystem::path& path) : path(path) {} 296 | 297 | virtual mod_file_identifier get_identifier(void) const { return this->path.string(); } 298 | 299 | virtual std::string get_display_name(void) const { 300 | if (this->path.parent_path() == mod_dir) { 301 | return this->path.filename().string(); 302 | } else { 303 | return this->path.string(); 304 | } 305 | } 306 | 307 | TEST_CASE_CLASS("loader::mod_file_local::get_display_name") { 308 | auto original_mod_dir = mod_dir; 309 | mod_dir = "tests"; 310 | 311 | // Use an empty subcase to make sure we restore the mod dir if this fails 312 | SUBCASE("") { 313 | CHECK(mod_file_local{mod_dir / "mod_file.bl3hotfix"}.get_display_name() 314 | == "mod_file.bl3hotfix"); 315 | 316 | CHECK(mod_file_local{mod_dir / "nested_folder" / "mod_in_nested.txt"}.get_display_name() 317 | == "tests\\nested_folder\\mod_in_nested.txt"); 318 | 319 | CHECK(mod_file_local{mod_dir / "mod_without_extension"}.get_display_name() 320 | == "mod_without_extension"); 321 | 322 | CHECK(mod_file_local{mod_dir / "mod.with.multiple.dots.txt"}.get_display_name() 323 | == "mod.with.multiple.dots.txt"); 324 | 325 | CHECK(mod_file_local{mod_dir / ".mod- !#with ()'symbols;'+ .txt"}.get_display_name() 326 | == ".mod- !#with ()'symbols;'+ .txt"); 327 | } 328 | 329 | mod_dir = original_mod_dir; 330 | } 331 | 332 | virtual void load(void) { 333 | LOGD << "[OHL] Loading " << path; 334 | 335 | std::ifstream stream{path}; 336 | if (!stream.is_open()) { 337 | LOGE << "[OHL]: Error opening file '" << path; 338 | return; 339 | } 340 | 341 | this->load_from_stream(stream, true); 342 | } 343 | 344 | TEST_CASE_CLASS("loader::mod_file_local::load - load_from_stream identical") { 345 | mod_file_local local_file{std::filesystem::path("tests") / "basic_mod.bl3hotfix"}; 346 | mod_file_local stream_file{"dummy"}; 347 | 348 | std::stringstream stream{}; 349 | stream << "SparkLevelPatchEntry,(1,1,1,Crypt_P),/Game/Cinematics/_Design/NPCs/" 350 | "BPCine_Actor_Typhon.BPCine_Actor_Typhon_C:SkeletalMesh_GEN_VARIABLE," 351 | "bEnableUpdateRateOptimizations,4,True,False\n"; 352 | 353 | const hotfix expected_hotfix{"SparkLevelPatchEntry", 354 | "(1,1,1,Crypt_P),/Game/Cinematics/_Design/NPCs/" 355 | "BPCine_Actor_Typhon.BPCine_Actor_Typhon_C:SkeletalMesh_GEN_" 356 | "VARIABLE,bEnableUpdateRateOptimizations,4,True,False"}; 357 | 358 | local_file.load(); 359 | local_file.join(); 360 | stream_file.load_from_stream(stream, true); 361 | 362 | REQUIRE(local_file.sections.size() == 1); 363 | REQUIRE(std::holds_alternative(local_file.sections[0])); 364 | auto local_data = std::get(local_file.sections[0]); 365 | 366 | REQUIRE(stream_file.sections.size() == 1); 367 | REQUIRE(std::holds_alternative(stream_file.sections[0])); 368 | auto stream_data = std::get(stream_file.sections[0]); 369 | 370 | REQUIRE(local_data.hotfixes.size() == 1); 371 | CHECK(local_data.type_11_hotfixes.empty()); 372 | CHECK(local_data.type_11_maps.empty()); 373 | CHECK(local_data.news_items.empty()); 374 | 375 | REQUIRE(stream_data.hotfixes.size() == 1); 376 | CHECK(stream_data.type_11_hotfixes.empty()); 377 | CHECK(stream_data.type_11_maps.empty()); 378 | CHECK(stream_data.news_items.empty()); 379 | 380 | CHECK(local_data.hotfixes[0] == stream_data.hotfixes[0]); 381 | CHECK(local_data.hotfixes[0] == expected_hotfix); 382 | CHECK(stream_data.hotfixes[0] == expected_hotfix); 383 | } 384 | 385 | // Have to put this test here since it uses a protected method, and `mod_file` is abstract 386 | TEST_CASE_CLASS("loader::mod_file::register_remote_file") { 387 | known_mod_files.clear(); 388 | REQUIRE(known_mod_files.empty()); 389 | 390 | mod_file_local base{"base.bl3hotfix"}; 391 | REQUIRE(base.sections.empty()); 392 | 393 | std::shared_ptr remote = std::make_shared("remote.bl3hotfix"); 394 | 395 | SUBCASE("Unknown file") { 396 | base.register_remote_file(remote); 397 | 398 | REQUIRE(base.sections.size() == 1); 399 | auto section = base.sections[0]; 400 | 401 | REQUIRE(std::holds_alternative(section)); 402 | auto remote_data = std::get(section); 403 | 404 | REQUIRE(remote_data.identifier == remote->get_identifier()); 405 | 406 | REQUIRE(known_mod_files.count(remote_data.identifier) == 1); 407 | REQUIRE(known_mod_files.at(remote_data.identifier) == remote); 408 | } 409 | 410 | SUBCASE("Known file") { 411 | std::shared_ptr known_remote = 412 | std::make_shared("remote.bl3hotfix"); 413 | REQUIRE(known_remote->get_identifier() == remote->get_identifier()); 414 | known_mod_files[known_remote->get_identifier()] = known_remote; 415 | 416 | base.register_remote_file(remote); 417 | 418 | REQUIRE(base.sections.size() == 1); 419 | auto section = base.sections[0]; 420 | 421 | REQUIRE(std::holds_alternative(section)); 422 | auto remote_data = std::get(section); 423 | 424 | REQUIRE(remote_data.identifier == remote->get_identifier()); 425 | 426 | REQUIRE(known_mod_files.count(remote_data.identifier) == 1); 427 | REQUIRE(known_mod_files.at(remote_data.identifier) != remote); 428 | REQUIRE(known_mod_files.at(remote_data.identifier) == known_remote); 429 | } 430 | 431 | known_mod_files.clear(); 432 | } 433 | }; 434 | 435 | TEST_CASE("loader::mod_file::append_to") { 436 | mod_file_local file{std::filesystem::path("tests") / "basic_mod.bl3hotfix"}; 437 | file.load(); 438 | 439 | REQUIRE(file.sections.size() == 1); 440 | REQUIRE(std::holds_alternative(file.sections[0])); 441 | auto file_data = std::get(file.sections[0]); 442 | 443 | mod_data empty_data{}; 444 | mod_data filled_data{{{"Key", "Value"}}, {}, {}, {}}; 445 | 446 | const std::vector expected_append_to_filled_hotfixes = { 447 | {"Key", "Value"}, 448 | {"SparkLevelPatchEntry", 449 | "(1,1,1,Crypt_P),/Game/Cinematics/_Design/NPCs/" 450 | "BPCine_Actor_Typhon.BPCine_Actor_Typhon_C:SkeletalMesh_GEN_VARIABLE," 451 | "bEnableUpdateRateOptimizations,4,True,False"}}; 452 | 453 | std::vector seen_files{}; 454 | 455 | file.append_to(empty_data, seen_files); 456 | CHECK(seen_files.empty()); // Only adds nested files 457 | 458 | file.append_to(filled_data, seen_files); 459 | CHECK(seen_files.empty()); 460 | CHECK(ITERABLE_EQUAL(empty_data.hotfixes, file_data.hotfixes)); 461 | CHECK(ITERABLE_EQUAL(empty_data.type_11_hotfixes, file_data.type_11_hotfixes)); 462 | CHECK(ITERABLE_EQUAL(empty_data.type_11_maps, file_data.type_11_maps)); 463 | CHECK(ITERABLE_EQUAL(empty_data.news_items, file_data.news_items)); 464 | 465 | CHECK(ITERABLE_EQUAL(filled_data.hotfixes, expected_append_to_filled_hotfixes)); 466 | 467 | CHECK(ITERABLE_EQUAL(filled_data.type_11_hotfixes, file_data.type_11_hotfixes)); 468 | CHECK(ITERABLE_EQUAL(filled_data.type_11_maps, file_data.type_11_maps)); 469 | CHECK(ITERABLE_EQUAL(filled_data.news_items, file_data.news_items)); 470 | } 471 | 472 | /** 473 | * @brief Class for mod file data based on a url. 474 | */ 475 | class mod_file_url : public mod_file { 476 | private: 477 | std::future download; 478 | 479 | public: 480 | const std::string url; 481 | 482 | mod_file_url(const std::string& url) : url(url) {} 483 | 484 | virtual mod_file_identifier get_identifier(void) const { return this->url; } 485 | 486 | virtual std::string get_display_name(void) const { 487 | if (this->url.find_last_of('/') == std::string::npos) { 488 | return this->url; 489 | } else { 490 | auto unescaped = ohl::util::unescape_url(this->url, false); 491 | auto name_start_pos = unescaped.find_last_of('/') + 1; 492 | auto name_end_pos = unescaped.find_first_of("#?", name_start_pos); 493 | 494 | return unescaped.substr(name_start_pos, name_end_pos - name_start_pos) + " (url)"; 495 | } 496 | } 497 | 498 | TEST_CASE_CLASS("loader::mod_file_url::get_display_name") { 499 | CHECK(mod_file_url{"https://example.com/mod.bl3hotfix"}.get_display_name() 500 | == "mod.bl3hotfix (url)"); 501 | 502 | CHECK(mod_file_url{"https://example.com/nested/nested/mod.bl3hotfix"}.get_display_name() 503 | == "mod.bl3hotfix (url)"); 504 | 505 | CHECK(mod_file_url{"https://exa%6Dple.com/%6Dy%20mod.bl3hotfix"}.get_display_name() 506 | == "my mod.bl3hotfix (url)"); 507 | 508 | CHECK(mod_file_url{"https://example.com/mod.bl3hotfix?query=abc"}.get_display_name() 509 | == "mod.bl3hotfix (url)"); 510 | 511 | CHECK(mod_file_url{"https://example.com/mod.bl3hotfix#anchor"}.get_display_name() 512 | == "mod.bl3hotfix (url)"); 513 | } 514 | 515 | virtual void load(void) { 516 | LOGD << "[OHL] Loading " << this->url; 517 | 518 | this->download = cpr::GetCallback( 519 | [&](const cpr::Response& resp) { 520 | LOGD << "[OHL] Finished downloading " << this->url; 521 | 522 | if (resp.status_code == 0) { 523 | LOGE << "[OHL] Error downloading '" << this->url << "': " << resp.error.message; 524 | return; 525 | } else if (resp.status_code >= 400) { 526 | LOGE << "[OHL] Error downloading '" << this->url << "': " << resp.status_code; 527 | return; 528 | } 529 | 530 | std::stringstream stream{resp.text}; 531 | 532 | this->load_from_stream(stream, false); 533 | }, 534 | cpr::Url{this->url}, 535 | // An empty string tells libcurl to accept whatever encodings it can 536 | cpr::AcceptEncoding{{""}}); 537 | }; 538 | 539 | TEST_CASE_CLASS("loader::mod_file_url::load - load_from_stream identica") { 540 | mod_file_url url_file{OHL_GITHUB_RAW_URL "tests/basic_mod.bl3hotfix"}; 541 | mod_file_url stream_file{"dummy"}; 542 | 543 | std::stringstream stream{}; 544 | stream << "SparkLevelPatchEntry,(1,1,1,Crypt_P),/Game/Cinematics/_Design/NPCs/" 545 | "BPCine_Actor_Typhon.BPCine_Actor_Typhon_C:SkeletalMesh_GEN_VARIABLE," 546 | "bEnableUpdateRateOptimizations,4,True,False\n"; 547 | 548 | const hotfix expected_hotfix{ 549 | "SparkLevelPatchEntry", 550 | "(1,1,1,Crypt_P),/Game/Cinematics/_Design/NPCs/" 551 | "BPCine_Actor_Typhon.BPCine_Actor_Typhon_C:SkeletalMesh_GEN_VARIABLE," 552 | "bEnableUpdateRateOptimizations,4,True,False"}; 553 | 554 | url_file.load(); 555 | url_file.join(); 556 | stream_file.load_from_stream(stream, true); 557 | 558 | REQUIRE(url_file.sections.size() == 1); 559 | REQUIRE(std::holds_alternative(url_file.sections[0])); 560 | auto url_data = std::get(url_file.sections[0]); 561 | 562 | REQUIRE(stream_file.sections.size() == 1); 563 | REQUIRE(std::holds_alternative(stream_file.sections[0])); 564 | auto stream_data = std::get(stream_file.sections[0]); 565 | 566 | REQUIRE(url_data.hotfixes.size() == 1); 567 | CHECK(url_data.type_11_hotfixes.empty()); 568 | CHECK(url_data.type_11_maps.empty()); 569 | CHECK(url_data.news_items.empty()); 570 | 571 | REQUIRE(stream_data.hotfixes.size() == 1); 572 | CHECK(stream_data.type_11_hotfixes.empty()); 573 | CHECK(stream_data.type_11_maps.empty()); 574 | CHECK(stream_data.news_items.empty()); 575 | 576 | CHECK(url_data.hotfixes[0] == stream_data.hotfixes[0]); 577 | CHECK(url_data.hotfixes[0] == expected_hotfix); 578 | CHECK(stream_data.hotfixes[0] == expected_hotfix); 579 | } 580 | 581 | virtual void join(void) { 582 | if (!this->download.valid()) { 583 | throw std::runtime_error("Tried to join a url download before starting it!"); 584 | } 585 | this->download.get(); 586 | } 587 | }; 588 | 589 | /** 590 | * @brief Class for the `ohl-mods` mod "file". 591 | * @note Inheriting from the mod file base class for the merging logic. 592 | */ 593 | class mods_folder : public mod_file { 594 | public: 595 | virtual mod_file_identifier get_identifier(void) const { 596 | throw std::runtime_error("Mods folder should not be treated as a mod file!"); 597 | } 598 | 599 | virtual std::string get_display_name(void) const { 600 | throw std::runtime_error("Mods folder should not be treated as a mod file!"); 601 | } 602 | 603 | virtual void load(void) { 604 | LOGI << "[OHL] Loading mods folder"; 605 | 606 | for (const auto& path : ohl::util::get_sorted_files_in_dir(mod_dir)) { 607 | this->register_remote_file(std::make_shared(path)); 608 | } 609 | } 610 | 611 | TEST_CASE_CLASS("loader::mods_folder::load") { 612 | auto original_mod_dir = mod_dir; 613 | mod_dir = std::filesystem::path("tests") / "sorted_files"; 614 | 615 | const std::vector expected_identifiers = { 616 | "tests\\sorted_files\\1.txt", "tests\\sorted_files\\5.txt", 617 | "tests\\sorted_files\\10.txt", "tests\\sorted_files\\a.txt", 618 | "tests\\sorted_files\\b.txt", "tests\\sorted_files\\c.txt", 619 | }; 620 | 621 | SUBCASE("") { 622 | mods_folder folder; 623 | folder.load(); 624 | folder.join(); 625 | 626 | std::vector found_identifiers{}; 627 | std::transform(folder.sections.begin(), folder.sections.end(), 628 | std::back_inserter(found_identifiers), [](const auto& section) { 629 | REQUIRE(std::holds_alternative(section)); 630 | return std::get(section).identifier; 631 | }); 632 | 633 | CHECK(ITERABLE_EQUAL(found_identifiers, expected_identifiers)); 634 | } 635 | 636 | mod_dir = original_mod_dir; 637 | } 638 | }; 639 | 640 | #pragma endregion 641 | 642 | #pragma region Parsing 643 | 644 | /** 645 | * @brief Parses a hotfix command, and appends it to the given mod data. 646 | * 647 | * @param line The line to parse, without leading whitespace. 648 | * @param data The mod data object to append hotfixes to. 649 | */ 650 | static void parse_and_append_hotfix_cmd(const std::string_view& line, mod_data& data) { 651 | auto key_end_pos = line.find_first_of(','); 652 | if (key_end_pos == std::string::npos) { 653 | return; 654 | } 655 | 656 | // SparkEarlyLevelPatchEntry,(1,11,0,MAPNAME),... 657 | // ^ ^ ^ ^ ^ 658 | // key_end_pos -------------+ | | | | 659 | // type_start_pos --------------+ | | | 660 | // type_end_pos ------------------+ | | 661 | // map_start_pos --------------------+ | 662 | // map_end_pos -----------------------------+ 663 | 664 | std::string key(line, 0, key_end_pos); 665 | std::string value(line, key_end_pos + 1, std::string::npos); 666 | 667 | // Check if it's a type 11, and extract the map 668 | auto type_start_pos = line.find_first_of(',', key_end_pos + 1) + 1; 669 | auto type_end_pos = line.find_first_of(',', type_start_pos + 1); 670 | if (type_end_pos != std::string::npos 671 | && line.substr(type_start_pos, type_end_pos - type_start_pos) == "11") { 672 | auto map_start_pos = line.find_first_of(',', type_end_pos + 1) + 1; 673 | auto map_end_pos = line.find_first_of(')', map_start_pos); 674 | if (map_end_pos != std::string::npos) { 675 | data.type_11_hotfixes.emplace_back(key, value); 676 | data.type_11_maps.emplace(line, map_start_pos, map_end_pos - map_start_pos); 677 | return; 678 | } 679 | } 680 | 681 | data.hotfixes.emplace_back(key, value); 682 | } 683 | 684 | TEST_CASE("loader::parse_and_append_hotfix_cmd") { 685 | mod_data data; 686 | 687 | parse_and_append_hotfix_cmd("Spark", data); 688 | parse_and_append_hotfix_cmd("SparkLevelPatchEntry", data); 689 | REQUIRE(data.is_empty()); 690 | 691 | parse_and_append_hotfix_cmd("Spark,", data); 692 | REQUIRE(data.hotfixes.size() == 1); 693 | REQUIRE(data.hotfixes[0] == hotfix{"Spark", ""}); 694 | 695 | parse_and_append_hotfix_cmd("Spark,abcde", data); 696 | REQUIRE(data.hotfixes.size() == 2); 697 | REQUIRE(data.hotfixes[1] == hotfix{"Spark", "abcde"}); 698 | 699 | parse_and_append_hotfix_cmd("Spark,(1,111,1,NotA),Type11", data); 700 | REQUIRE(data.hotfixes.size() == 3); 701 | REQUIRE(data.hotfixes[2] == hotfix{"Spark", "(1,111,1,NotA),Type11"}); 702 | 703 | REQUIRE(data.type_11_hotfixes.empty()); 704 | REQUIRE(data.type_11_maps.empty()); 705 | 706 | parse_and_append_hotfix_cmd("SparkLevelPatchEntry,(1,11,0,MapName),/Is/Actually.A,Type,11", 707 | data); 708 | REQUIRE(data.hotfixes.size() == 3); 709 | REQUIRE(data.type_11_hotfixes.size() == 1); 710 | REQUIRE(data.type_11_hotfixes[0] 711 | == hotfix{"SparkLevelPatchEntry", "(1,11,0,MapName),/Is/Actually.A,Type,11"}); 712 | 713 | REQUIRE(data.type_11_maps.size() == 1); 714 | REQUIRE(data.type_11_maps.count("MapName") == 1); 715 | 716 | REQUIRE(data.news_items.size() == 0); 717 | } 718 | 719 | /** 720 | * @brief Extracts the next csv field from a line, unescaping it if necessary. 721 | * @note Callers must ensure the line is not empty. 722 | * 723 | * @param line A view of the current line. Will be modified to advance past the extracted field. 724 | * @return A new string containing the extracted/escaped field. 725 | */ 726 | static std::string extract_csv_escaped(std::string_view& line) { 727 | // If the field is not escaped, simple comma search + return 728 | // This will (deliberately) throw an out of range if the line is empty 729 | if (line[0] != '"') { 730 | auto comma_pos = line.find_first_of(',', 0); 731 | std::string ret(line, 0, comma_pos); 732 | 733 | if (comma_pos == std::string::npos) { 734 | line = ""; 735 | } else { 736 | line = line.substr(comma_pos + 1); 737 | } 738 | 739 | return ret; 740 | } 741 | 742 | std::stringstream stream{}; 743 | auto quoted_start_pos = 1; 744 | auto quoted_end_pos = std::string::npos; 745 | while (true) { 746 | quoted_end_pos = line.find_first_of('"', quoted_start_pos); 747 | stream << line.substr(quoted_start_pos, quoted_end_pos - quoted_start_pos); 748 | 749 | // If we reached the end of the line 750 | if (quoted_end_pos >= (line.size() - 1)) { 751 | break; 752 | } 753 | 754 | // If this is not an escaped quote 755 | if (line[quoted_end_pos + 1] != '"') { 756 | break; 757 | } 758 | 759 | stream << '"'; 760 | // This might move past the end of the string, but will be caught on next loop 761 | quoted_start_pos = quoted_end_pos + 2; 762 | } 763 | 764 | auto comma_pos = line.find_first_of(',', quoted_end_pos); 765 | if (comma_pos == std::string::npos) { 766 | line = ""; 767 | } else { 768 | line = line.substr(comma_pos + 1); 769 | } 770 | 771 | return stream.str(); 772 | } 773 | 774 | TEST_CASE("loader::extract_csv_escaped") { 775 | const std::string CSV_STRING = "normal,,,\"quoted\",\"escaped,comma\",\"escaped\"\"quote\""; 776 | std::string_view csv{CSV_STRING}; 777 | 778 | CHECK(extract_csv_escaped(csv) == "normal"); 779 | REQUIRE(csv == ",,\"quoted\",\"escaped,comma\",\"escaped\"\"quote\""); 780 | 781 | CHECK(extract_csv_escaped(csv) == ""); 782 | REQUIRE(csv == ",\"quoted\",\"escaped,comma\",\"escaped\"\"quote\""); 783 | CHECK(extract_csv_escaped(csv) == ""); 784 | REQUIRE(csv == "\"quoted\",\"escaped,comma\",\"escaped\"\"quote\""); 785 | 786 | CHECK(extract_csv_escaped(csv) == "quoted"); 787 | REQUIRE(csv == "\"escaped,comma\",\"escaped\"\"quote\""); 788 | 789 | CHECK(extract_csv_escaped(csv) == "escaped,comma"); 790 | REQUIRE(csv == "\"escaped\"\"quote\""); 791 | 792 | CHECK(extract_csv_escaped(csv) == "escaped\"quote"); 793 | REQUIRE(csv == ""); 794 | } 795 | 796 | /** 797 | * @brief Parses a news item command, and appends it to the given mod data. 798 | * 799 | * @param line The line to parse, without leading whitespace. 800 | * @param data The mod data object to append hotfixes to. 801 | */ 802 | static void parse_and_append_news_item_cmd(const std::string_view& line, mod_data& data) { 803 | auto cmd_end_pos = line.find_first_of(','); 804 | auto csv_view = line.substr(cmd_end_pos + 1); 805 | 806 | std::string header; 807 | std::string image_url; 808 | std::string article_url; 809 | std::string body; 810 | 811 | header = extract_csv_escaped(csv_view); 812 | if (!csv_view.empty()) { 813 | image_url = extract_csv_escaped(csv_view); 814 | if (!csv_view.empty()) { 815 | article_url = extract_csv_escaped(csv_view); 816 | if (!csv_view.empty()) { 817 | body = std::string(csv_view); 818 | } 819 | } 820 | } 821 | 822 | data.news_items.emplace_back(header, image_url, article_url, body); 823 | } 824 | 825 | TEST_CASE("loader::parse_and_append_news_item_cmd") { 826 | mod_data data; 827 | 828 | parse_and_append_news_item_cmd("InjectNewsItem,Header", data); 829 | REQUIRE(data.news_items.size() == 1); 830 | REQUIRE(data.news_items[0] == news_item{"Header"}); 831 | 832 | parse_and_append_news_item_cmd("InjectNewsItem, Header ,image.png", data); 833 | REQUIRE(data.news_items.size() == 2); 834 | REQUIRE(data.news_items[1] == news_item{" Header ", "image.png"}); 835 | 836 | parse_and_append_news_item_cmd("InjectNewsItem,Header,\"escaped,image.png\"", data); 837 | REQUIRE(data.news_items.size() == 3); 838 | REQUIRE(data.news_items[2] == news_item{"Header", "escaped,image.png"}); 839 | 840 | parse_and_append_news_item_cmd("InjectNewsItem,\"Header\",\"escaped,image.png\"", data); 841 | REQUIRE(data.news_items.size() == 4); 842 | REQUIRE(data.news_items[3] == news_item{"Header", "escaped,image.png"}); 843 | 844 | parse_and_append_news_item_cmd("InjectNewsItem,\"Header\",,Article", data); 845 | REQUIRE(data.news_items.size() == 5); 846 | REQUIRE(data.news_items[4] == news_item{"Header", "", "Article"}); 847 | 848 | parse_and_append_news_item_cmd("InjectNewsItem,, \t ,\"Article\"", data); 849 | REQUIRE(data.news_items.size() == 6); 850 | REQUIRE(data.news_items[5] == news_item{"", " \t ", "Article"}); 851 | 852 | parse_and_append_news_item_cmd("InjectNewsItem,Header,image.png,\"Article \"\"URL\"\"", data); 853 | REQUIRE(data.news_items.size() == 7); 854 | REQUIRE(data.news_items[6] == news_item{"Header", "image.png", "Article \"URL\""}); 855 | 856 | parse_and_append_news_item_cmd("InjectNewsItem,,image.png,Article,Body", data); 857 | REQUIRE(data.news_items.size() == 8); 858 | REQUIRE(data.news_items[7] == news_item{"", "image.png", "Article", "Body"}); 859 | 860 | parse_and_append_news_item_cmd( 861 | "InjectNewsItem,Header,image.png,Article,Body, with commas, and \"quotes\"", data); 862 | REQUIRE(data.news_items.size() == 9); 863 | REQUIRE(data.news_items[8] 864 | == news_item{"Header", "image.png", "Article", "Body, with commas, and \"quotes\""}); 865 | 866 | parse_and_append_news_item_cmd("InjectNewsItem,,,,,,Body, with commas, and \"quotes\"", data); 867 | REQUIRE(data.news_items.size() == 10); 868 | REQUIRE(data.news_items[9] == news_item{"", "", "", ",,Body, with commas, and \"quotes\""}); 869 | 870 | REQUIRE(data.hotfixes.size() == 0); 871 | REQUIRE(data.type_11_hotfixes.size() == 0); 872 | REQUIRE(data.type_11_maps.size() == 0); 873 | } 874 | 875 | /** 876 | * @brief Parses an exec command and extracts the path to execute. 877 | * 878 | * @param line The line to parse, without leading whitespace. 879 | * @return The path to the file to execute, or std::nullopt if parsing failed. 880 | */ 881 | static std::optional parse_exec_cmd(const std::string_view& line) { 882 | auto whitespace_start = line.find_first_of(WHITESPACE); 883 | auto path_start = line.find_first_not_of(WHITESPACE, whitespace_start + 1); 884 | if (path_start == std::string::npos) { 885 | return std::nullopt; 886 | } 887 | 888 | // The standard exec command actually only grabs the first word 889 | // We will instead be permissive, and grab all text (except trailing whitespace), so that things 890 | // just work more often - though strictly this shouldn't be considered a defined feature 891 | auto path_end = line.find_last_not_of(WHITESPACE); 892 | if (path_end == std::string::npos) { 893 | path_end = line.size() - 1; 894 | } 895 | 896 | // If someone quoted the path (e.g. for another, less permissive tool), strip quotes 897 | if ((line[path_start] == '"' && line[path_end] == '"') 898 | || (line[path_start] == '\'' && line[path_end] == '\'')) { 899 | path_start++; 900 | path_end--; 901 | } 902 | 903 | return (mod_dir / line.substr(path_start, (path_end + 1) - path_start)).lexically_normal(); 904 | } 905 | 906 | TEST_CASE("loader::parse_exec_cmd") { 907 | CHECK(parse_exec_cmd("exec abc.bl3hotfix") == (mod_dir / "abc.bl3hotfix")); 908 | CHECK(parse_exec_cmd("exec \t abc.bl3hotfix ") == (mod_dir / "abc.bl3hotfix")); 909 | CHECK(parse_exec_cmd("exec nested/path.bl3hotfix") == (mod_dir / "nested" / "path.bl3hotfix")); 910 | CHECK(parse_exec_cmd("exec D:\\absolute\\path.bl3hotfix") 911 | == std::filesystem::path("D:\\") / "absolute" / "path.bl3hotfix"); 912 | CHECK(parse_exec_cmd("exec \"path with spaces.bl3hotfix\"") 913 | == (mod_dir / "path with spaces.bl3hotfix")); 914 | CHECK(parse_exec_cmd("exec ' more spaces.bl3hotfix'") 915 | == (mod_dir / " more spaces.bl3hotfix")); 916 | } 917 | 918 | /** 919 | * @brief Parses a url command. 920 | * 921 | * @param line The line to parse, without leading whitespace. 922 | * @return The parsed url, or std::nullopt if unable to parse. 923 | */ 924 | static std::optional parse_url_cmd(const std::string_view& line) { 925 | return std::string(line, URL_COMMAND.size(), line.size() - URL_COMMAND.size()); 926 | } 927 | 928 | TEST_CASE("loader::parse_url_cmd") { 929 | CHECK(parse_url_cmd("url=https://example.com/mod.bl3hotfix") 930 | == "https://example.com/mod.bl3hotfix"); 931 | CHECK(parse_url_cmd("url= \"https://example.com/mod.bl3hotfix\"") 932 | == " \"https://example.com/mod.bl3hotfix\""); 933 | CHECK(parse_url_cmd("URL=1234") == "1234"); 934 | } 935 | 936 | /** 937 | * @brief Loads this mod file from a stream. 938 | * 939 | * @param stream The stream to read from. 940 | * @param allow_exec True if to allow running exec commands. 941 | */ 942 | void mod_file::load_from_stream(std::istream& stream, bool allow_exec) { 943 | mod_data data{}; 944 | 945 | for (std::string mod_line_str; std::getline(stream, mod_line_str);) { 946 | std::string_view mod_line{mod_line_str}; 947 | 948 | auto whitespace_end_pos = mod_line.find_first_not_of(WHITESPACE); 949 | if (whitespace_end_pos == std::string::npos) { 950 | continue; 951 | } 952 | mod_line = mod_line.substr(whitespace_end_pos); 953 | 954 | auto lower_mod_line = mod_line_str; 955 | std::transform(lower_mod_line.begin(), lower_mod_line.end(), lower_mod_line.begin(), 956 | [](char c) { return std::tolower(c); }); 957 | 958 | /** 959 | * @brief Checks if the current line starts with the specified command. 960 | * @note Should compare against lowercase strings. 961 | * 962 | * @param cmd The command string to check. 963 | * @return True if the line starts with the command, false otherwise. 964 | */ 965 | auto is_command = [&](auto cmd) { 966 | return lower_mod_line.compare(whitespace_end_pos, cmd.size(), cmd) == 0; 967 | }; 968 | 969 | if (is_command(HOTFIX_COMMAND)) { 970 | parse_and_append_hotfix_cmd(mod_line, data); 971 | } else if (is_command(NEWS_COMMAND)) { 972 | parse_and_append_news_item_cmd(mod_line, data); 973 | } else if (allow_exec && is_command(EXEC_COMMAND)) { 974 | auto path = parse_exec_cmd(mod_line); 975 | 976 | if (path) { 977 | // Push our current data, and create a new one for use after loading the file. 978 | this->push_mod_data(data); 979 | this->register_remote_file(std::make_shared(*path)); 980 | data = mod_data{}; 981 | } 982 | } else if (is_command(URL_COMMAND)) { 983 | auto url = parse_url_cmd(mod_line); 984 | 985 | if (url) { 986 | this->push_mod_data(data); 987 | this->register_remote_file(std::make_shared(*url)); 988 | data = mod_data{}; 989 | } 990 | } 991 | } 992 | 993 | this->push_mod_data(data); 994 | } 995 | 996 | TEST_CASE("loader::mod_file_local::load") { 997 | const hotfix basic_mod_hotfix{ 998 | "SparkLevelPatchEntry", 999 | "(1,1,1,Crypt_P),/Game/Cinematics/_Design/NPCs/" 1000 | "BPCine_Actor_Typhon.BPCine_Actor_Typhon_C:SkeletalMesh_GEN_VARIABLE," 1001 | "bEnableUpdateRateOptimizations,4,True,False"}; 1002 | const hotfix unicode_statement_hotfix{ 1003 | "SparkPatchEntry", 1004 | u8"(1,1,0,),/Game/PatchDLC/Raid1/Gear/Weapons/Link/" 1005 | u8"Name_MAL_SM_Link.Name_MAL_SM_Link,PartName,0,,Cú Chulainn"}; 1006 | const hotfix sliding_hotfix{"SparkPatchEntry", 1007 | "(1,1,0,),/Game/PlayerCharacters/_Shared/_Design/Sliding/" 1008 | "ControlledMove_Global_Sliding.Default__ControlledMove_Global_" 1009 | "Sliding_C,Duration.BaseValueConstant,0,,5000"}; 1010 | 1011 | const std::vector> PARAMETERIZED_CASES{ 1012 | {"basic_mod.bl3hotfix", {{basic_mod_hotfix}, {}, {}, {}}}, 1013 | {"basic_mod.URL", {{basic_mod_hotfix}, {}, {}, {}}}, 1014 | {"unicode_statement.bl3hotfix", {{unicode_statement_hotfix}, {}, {}, {}}}, 1015 | {"easy_entry_to_fort_sunshine.bl3hotfix", 1016 | {{{"SparkEarlyLevelPatchEntry", 1017 | "(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1018 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0." 1019 | "DefaultSceneRoot," 1020 | "RelativeLocation,0,,(X=34927.000000,Y=10709.000000,Z=3139.000000)"}, 1021 | {"SparkEarlyLevelPatchEntry", 1022 | "(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1023 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0." 1024 | "DefaultSceneRoot," 1025 | "RelativeRotation,0,,(Pitch=0.000000,Yaw=67.716370,Roll=0.000000)"}, 1026 | {"SparkEarlyLevelPatchEntry", 1027 | "(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1028 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0." 1029 | "DefaultSceneRoot," 1030 | "RelativeScale3D,0,,(X=1.000000,Y=1.000000,Z=1.000000)"}, 1031 | {"SparkLevelPatchEntry", 1032 | "(1,1,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1033 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0,On_SwitchUsed," 1034 | "0,,(" 1035 | "Wetlands_M_EP12JakobsRebellion_C_11.BndEvt__IO_Switch_Circuit_Breaker_V_2_K2Node_" 1036 | "ActorBoundEvent_0_On_SwitchUsed__DelegateSignature)"}, 1037 | {"SparkLevelPatchEntry", 1038 | "(1,1,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1039 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0," 1040 | "SingleActivation," 1041 | "0,,False"}}, 1042 | {{"SparkEarlyLevelPatchEntry", 1043 | "(1,11,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands,/Game/InteractiveObjects/Switches/" 1044 | "Lever/" 1045 | "Design,IO_Switch_Industrial_Prison,80,\"0.000000,0.000000,0.000000|0.000000,0.000000," 1046 | "0." 1047 | "000000|1.000000,1.000000,1.000000\""}}, 1048 | {"Wetlands_P"}, 1049 | {}}}, 1050 | {"single_exec.bl3hotfix", {{basic_mod_hotfix}, {}, {}, {}}}, 1051 | {"nested_exec.bl3hotfix", {{basic_mod_hotfix}, {}, {}, {}}}, 1052 | {"multi_exec.bl3hotfix", 1053 | {{basic_mod_hotfix, unicode_statement_hotfix, sliding_hotfix}, {}, {}, {}}}, 1054 | {"multi_exec.URL", {{sliding_hotfix}, {}, {}, {}}}, 1055 | {"news.bl3hotfix", {{}, {}, {}, {{"My News Item", "", "https://example.com"}}}}, 1056 | }; 1057 | 1058 | auto original_mod_dir = mod_dir; 1059 | mod_dir = std::filesystem::path("tests"); 1060 | 1061 | std::string filename; 1062 | mod_data expected_data; 1063 | 1064 | std::for_each(PARAMETERIZED_CASES.begin(), PARAMETERIZED_CASES.end(), [&](const auto& in) { 1065 | SUBCASE(in.first.c_str()) { 1066 | filename = in.first; 1067 | expected_data = in.second; 1068 | } 1069 | }); 1070 | 1071 | known_mod_files.clear(); 1072 | 1073 | mod_file_local file{mod_dir / filename}; 1074 | file.load(); 1075 | 1076 | mod_data data; 1077 | file.append_to(data); 1078 | 1079 | CHECK(ITERABLE_EQUAL(data.hotfixes, expected_data.hotfixes)); 1080 | CHECK(ITERABLE_EQUAL(data.type_11_hotfixes, expected_data.type_11_hotfixes)); 1081 | CHECK(ITERABLE_EQUAL(data.type_11_maps, expected_data.type_11_maps)); 1082 | CHECK(ITERABLE_EQUAL(data.news_items, expected_data.news_items)); 1083 | 1084 | mod_dir = original_mod_dir; 1085 | } 1086 | 1087 | #pragma endregion 1088 | 1089 | /** 1090 | * @brief Creates the news item for OHL. 1091 | * 1092 | * @param hotfix_count The amount of injected hotfixes 1093 | * @param file_order The injected mod files, in the order they were loaded. 1094 | * @return The OHL news item. 1095 | */ 1096 | static news_item get_ohl_news_item(size_t hotfix_count, 1097 | std::vector> file_order) { 1098 | // If we're in BL3, colour the name. 1099 | // WL doesn't support font tags :( 1100 | std::string ohl_name; 1101 | if (ohl::args::exe_path().stem() == "Borderlands3") { 1102 | ohl_name = 1103 | "OHL " VERSION_STRING 1104 | ""; 1105 | } else { 1106 | ohl_name = "OHL " VERSION_STRING; 1107 | } 1108 | 1109 | std::string header; 1110 | if (hotfix_count > 1) { 1111 | header = ohl_name + ": " + std::to_string(hotfix_count) + " hotfixes loaded"; 1112 | } else if (hotfix_count == 1) { 1113 | header = ohl_name + ": 1 hotfix loaded"; 1114 | } else { 1115 | header = ohl_name + ": No hotfixes loaded"; 1116 | } 1117 | 1118 | std::string body; 1119 | if (hotfix_count == 0) { 1120 | body = "No hotfixes loaded"; 1121 | } else { 1122 | std::stringstream stream{}; 1123 | stream << "Loaded files:\n"; 1124 | 1125 | for (const auto& file : file_order) { 1126 | stream << file->get_display_name() << ",\n"; 1127 | } 1128 | 1129 | body = stream.str(); 1130 | } 1131 | 1132 | return {header, OHL_NEWS_ITEM_IMAGE_URL, OHL_NEWS_ITEM_ARTICLE_URL, body}; 1133 | } 1134 | 1135 | TEST_CASE("loader::get_ohl_news_item") { 1136 | auto empty_news = get_ohl_news_item(0, {}); 1137 | CHECK(empty_news.header == "OHL " VERSION_STRING ": No hotfixes loaded"); 1138 | CHECK(empty_news.image_url == OHL_NEWS_ITEM_IMAGE_URL); 1139 | CHECK(empty_news.article_url == OHL_NEWS_ITEM_ARTICLE_URL); 1140 | CHECK(empty_news.body == "No hotfixes loaded"); 1141 | 1142 | auto one_news = get_ohl_news_item(1, {}); 1143 | CHECK(one_news.header == "OHL " VERSION_STRING ": 1 hotfix loaded"); 1144 | CHECK(one_news.image_url == OHL_NEWS_ITEM_IMAGE_URL); 1145 | CHECK(one_news.article_url == OHL_NEWS_ITEM_ARTICLE_URL); 1146 | // Going to leave body formatting indeterminate for >0 1147 | 1148 | auto multi_news = get_ohl_news_item(1234, {}); 1149 | CHECK(multi_news.header == "OHL " VERSION_STRING ": 1234 hotfixes loaded"); 1150 | CHECK(multi_news.image_url == OHL_NEWS_ITEM_IMAGE_URL); 1151 | CHECK(multi_news.article_url == OHL_NEWS_ITEM_ARTICLE_URL); 1152 | } 1153 | 1154 | #pragma region Public interface 1155 | 1156 | static std::mutex reloading_mutex; 1157 | static std::atomic reloading_started{false}; 1158 | static mod_data loaded_mod_data; 1159 | 1160 | /** 1161 | * @brief Implementation of `reload`, which reloads the hotfix list. 1162 | * @note Intended to be run in a thread. 1163 | */ 1164 | static void reload_impl(void) { 1165 | std::lock_guard lock(reloading_mutex); 1166 | reloading_started = true; 1167 | 1168 | SetThreadDescription(GetCurrentThread(), L"OpenHotfixLoader Loader"); 1169 | 1170 | // If the mod folder doesn't exist, create it, and then just quit early since we know we won't 1171 | // load anything 1172 | if (!std::filesystem::exists(mod_dir)) { 1173 | std::filesystem::create_directories(mod_dir); 1174 | return; 1175 | } 1176 | 1177 | // No need to lock here since we haven't started loading 1178 | known_mod_files.clear(); 1179 | 1180 | mods_folder folder_data{}; 1181 | folder_data.load(); 1182 | 1183 | LOGD << "[OHL] Combining mod data"; 1184 | mod_data combined_mod_data{}; 1185 | std::vector seen_files; 1186 | folder_data.append_to(combined_mod_data, seen_files); 1187 | 1188 | LOGD << "[OHL] Processing type 11s"; 1189 | 1190 | // Add type 11s to the front of the list, and their delays after them but before the rest 1191 | for (const auto& map : combined_mod_data.type_11_maps) { 1192 | static const auto map_start_pos = TYPE_11_DELAY_VALUE.find("{map}"); 1193 | static const auto map_length = 5; 1194 | static const auto mesh_start_pos = TYPE_11_DELAY_VALUE.find("{mesh}"); 1195 | static const auto mesh_length = 6; 1196 | 1197 | for (const auto& mesh : TYPE_11_DELAY_MESHES) { 1198 | // Make sure to replace mesh first, since it appears later in the string 1199 | auto hotfix = std::string(TYPE_11_DELAY_VALUE) 1200 | .replace(mesh_start_pos, mesh_length, mesh) 1201 | .replace(map_start_pos, map_length, map); 1202 | combined_mod_data.type_11_hotfixes.emplace_back(TYPE_11_DELAY_TYPE, hotfix); 1203 | } 1204 | } 1205 | for (auto it = combined_mod_data.type_11_hotfixes.rbegin(); 1206 | it != combined_mod_data.type_11_hotfixes.rend(); it++) { 1207 | combined_mod_data.hotfixes.push_front(*it); 1208 | } 1209 | 1210 | LOGD << "[OHL] Adding OHL news item"; 1211 | 1212 | std::vector> file_order; 1213 | for (const auto& identifier : seen_files) { 1214 | auto file = known_mod_files.at(identifier); 1215 | if (file->sections.size() == 0) { 1216 | continue; 1217 | } 1218 | 1219 | // Special case ignoring a file holding a single remote url reference - i.e. the url 1220 | // shortcut files we'll be seeing in 99% of cases 1221 | if (file->sections.size() == 1 1222 | && std::holds_alternative(file->sections[0])) { 1223 | auto remote_file = 1224 | known_mod_files.at(std::get(file->sections[0]).identifier); 1225 | if (dynamic_cast(remote_file.get()) != nullptr) { 1226 | continue; 1227 | } 1228 | } 1229 | 1230 | file_order.push_back(file); 1231 | } 1232 | 1233 | combined_mod_data.news_items.push_front( 1234 | get_ohl_news_item(combined_mod_data.hotfixes.size(), file_order)); 1235 | 1236 | LOGD << "[OHL] Replacing globals"; 1237 | 1238 | loaded_mod_data = combined_mod_data; 1239 | 1240 | LOGI << "[OHL] Loading finished, loaded files:"; 1241 | for (const auto& file : file_order) { 1242 | LOGI << "[OHL] " << file->get_display_name(); 1243 | } 1244 | } 1245 | 1246 | void init(void) { 1247 | auto dll_path = ohl::args::dll_path(); 1248 | if (std::filesystem::exists(dll_path)) { 1249 | mod_dir = dll_path.remove_filename() / mod_dir; 1250 | } 1251 | } 1252 | 1253 | void reload(void) { 1254 | reloading_started = false; 1255 | 1256 | std::thread thread(reload_impl); 1257 | thread.detach(); 1258 | 1259 | // Busy wait for the thread to start before exiting 1260 | while (!reloading_started) {} 1261 | reloading_started = false; 1262 | } 1263 | 1264 | std::deque get_hotfixes(void) { 1265 | std::lock_guard lock(reloading_mutex); 1266 | 1267 | return loaded_mod_data.hotfixes; 1268 | } 1269 | 1270 | std::deque get_news_items(void) { 1271 | std::lock_guard lock(reloading_mutex); 1272 | 1273 | return loaded_mod_data.news_items; 1274 | } 1275 | 1276 | TEST_CASE("loader integration") { 1277 | const std::vector expected_hotfixes{ 1278 | // Type 11s 1279 | {"SparkEarlyLevelPatchEntry", 1280 | "(1,11,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands,/Game/InteractiveObjects/Switches/Lever/" 1281 | "Design,IO_Switch_Industrial_Prison,80,\"0.000000,0.000000,0.000000|0.000000,0.000000,0." 1282 | "000000|1.000000,1.000000,1.000000\""}, 1283 | 1284 | // Type 11 Delays 1285 | {"SparkEarlyLevelPatchEntry", 1286 | "(1,1,0,Wetlands_P),/Game/Pickups/Ammo/" 1287 | "BPAmmoItem_Pistol.Default__BPAmmoItem_Pistol_C,ItemMeshComponent.Object..StaticMesh,0,," 1288 | "StaticMesh'\"/Engine/EditorMeshes/Camera/SM_CraneRig_Arm.SM_CraneRig_Arm\"'"}, 1289 | {"SparkEarlyLevelPatchEntry", 1290 | "(1,1,0,Wetlands_P),/Game/Pickups/Ammo/" 1291 | "BPAmmoItem_Pistol.Default__BPAmmoItem_Pistol_C,ItemMeshComponent.Object..StaticMesh,0,," 1292 | "StaticMesh'\"/Engine/EditorMeshes/Camera/SM_CraneRig_Base.SM_CraneRig_Base\"'"}, 1293 | {"SparkEarlyLevelPatchEntry", 1294 | "(1,1,0,Wetlands_P),/Game/Pickups/Ammo/" 1295 | "BPAmmoItem_Pistol.Default__BPAmmoItem_Pistol_C,ItemMeshComponent.Object..StaticMesh,0,," 1296 | "StaticMesh'\"/Engine/EditorMeshes/Camera/SM_CraneRig_Body.SM_CraneRig_Body\"'"}, 1297 | {"SparkEarlyLevelPatchEntry", 1298 | "(1,1,0,Wetlands_P),/Game/Pickups/Ammo/" 1299 | "BPAmmoItem_Pistol.Default__BPAmmoItem_Pistol_C,ItemMeshComponent.Object..StaticMesh,0,," 1300 | "StaticMesh'\"/Engine/EditorMeshes/Camera/SM_CraneRig_Mount.SM_CraneRig_Mount\"'"}, 1301 | {"SparkEarlyLevelPatchEntry", 1302 | "(1,1,0,Wetlands_P),/Game/Pickups/Ammo/" 1303 | "BPAmmoItem_Pistol.Default__BPAmmoItem_Pistol_C,ItemMeshComponent.Object..StaticMesh,0,," 1304 | "StaticMesh'\"/Engine/EditorMeshes/Camera/SM_RailRig_Mount.SM_RailRig_Mount\"'"}, 1305 | {"SparkEarlyLevelPatchEntry", 1306 | "(1,1,0,Wetlands_P),/Game/Pickups/Ammo/" 1307 | "BPAmmoItem_Pistol.Default__BPAmmoItem_Pistol_C,ItemMeshComponent.Object..StaticMesh,0,," 1308 | "StaticMesh'\"/Engine/EditorMeshes/Camera/SM_RailRig_Track.SM_RailRig_Track\"'"}, 1309 | {"SparkEarlyLevelPatchEntry", 1310 | "(1,1,0,Wetlands_P),/Game/Pickups/Ammo/" 1311 | "BPAmmoItem_Pistol.Default__BPAmmoItem_Pistol_C,ItemMeshComponent.Object..StaticMesh,0,," 1312 | "StaticMesh'\"/Game/Pickups/Ammo/Model/Meshes/SM_ammo_pistol.SM_ammo_pistol\"'"}, 1313 | 1314 | // Regular hotfixes 1315 | {"SparkPatchEntry", 1316 | u8"(1,1,0,),/Game/PatchDLC/Raid1/Gear/Weapons/Link/" 1317 | u8"Name_MAL_SM_Link.Name_MAL_SM_Link,PartName,0,,Cú Chulainn"}, 1318 | 1319 | {"SparkEarlyLevelPatchEntry", 1320 | "(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1321 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0." 1322 | "DefaultSceneRoot," 1323 | "RelativeLocation,0,,(X=34927.000000,Y=10709.000000,Z=3139.000000)"}, 1324 | {"SparkEarlyLevelPatchEntry", 1325 | "(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1326 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0." 1327 | "DefaultSceneRoot," 1328 | "RelativeRotation,0,,(Pitch=0.000000,Yaw=67.716370,Roll=0.000000)"}, 1329 | {"SparkEarlyLevelPatchEntry", 1330 | "(1,1,1,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1331 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0." 1332 | "DefaultSceneRoot," 1333 | "RelativeScale3D,0,,(X=1.000000,Y=1.000000,Z=1.000000)"}, 1334 | {"SparkLevelPatchEntry", 1335 | "(1,1,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1336 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0,On_SwitchUsed," 1337 | "0,,(" 1338 | "Wetlands_M_EP12JakobsRebellion_C_11.BndEvt__IO_Switch_Circuit_Breaker_V_2_K2Node_" 1339 | "ActorBoundEvent_0_On_SwitchUsed__DelegateSignature)"}, 1340 | {"SparkLevelPatchEntry", 1341 | "(1,1,0,Wetlands_P),/Game/Maps/Zone_2/Wetlands/" 1342 | "Wetlands_P.Wetlands_P:PersistentLevel.IO_Switch_Industrial_Prison_C_0," 1343 | "SingleActivation," 1344 | "0,,False"}, 1345 | 1346 | }; 1347 | const std::vector expected_news_items{ 1348 | {"My News Item", "", "https://example.com"}, 1349 | }; 1350 | 1351 | auto original_mod_dir = mod_dir; 1352 | mod_dir = std::filesystem::path("tests") / "mods_dir"; 1353 | 1354 | reload(); 1355 | auto hotfixes = get_hotfixes(); 1356 | auto news_items = get_news_items(); 1357 | 1358 | news_item ohl_news = news_items[0]; 1359 | news_items.pop_front(); 1360 | 1361 | CHECK(ohl_news.header == "OHL " VERSION_STRING ": 14 hotfixes loaded"); 1362 | CHECK(ohl_news.image_url == OHL_NEWS_ITEM_IMAGE_URL); 1363 | CHECK(ohl_news.article_url == OHL_NEWS_ITEM_ARTICLE_URL); 1364 | 1365 | CHECK(ITERABLE_EQUAL(hotfixes, expected_hotfixes)); 1366 | CHECK(ITERABLE_EQUAL(news_items, expected_news_items)); 1367 | 1368 | mod_dir = original_mod_dir; 1369 | } 1370 | 1371 | #pragma endregion 1372 | 1373 | TEST_SUITE_END(); 1374 | } // namespace ohl::loader 1375 | --------------------------------------------------------------------------------