From e570edaa660e8c5aad611121f5a6b67fa9b06aee Mon Sep 17 00:00:00 2001 From: acidicoala Date: Fri, 12 Mar 2021 09:46:23 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + Common/Common.vcxproj | 211 ++++++++++ Common/Common.vcxproj.filters | 47 +++ Common/src/Config.cpp | 55 +++ Common/src/Config.h | 27 ++ Common/src/Logger.cpp | 35 ++ Common/src/Logger.h | 11 + Common/src/constants.h | 23 ++ Common/src/framework.h | 35 ++ Common/src/pch.cpp | 1 + Common/src/pch.h | 2 + Common/src/util.cpp | 227 ++++++++++ Common/src/util.h | 65 +++ Injector/Injector.vcxproj | 212 ++++++++++ Injector/Injector.vcxproj.filters | 35 ++ Injector/src/Injector.cpp | 51 +++ Injector/src/Injector.h | 17 + Injector/src/framework.h | 7 + Injector/src/main.cpp | 49 +++ Injector/src/pch.cpp | 1 + Injector/src/pch.h | 2 + Integration/Integration.vcxproj | 213 ++++++++++ Integration/Integration.vcxproj.filters | 32 ++ Integration/src/dllmain.cpp | 59 +++ Integration/src/framework.h | 8 + Integration/src/pch.cpp | 5 + Integration/src/pch.h | 13 + Integration/src/version_exports.h | 19 + IntegrationWizard/Config.jsonc | 84 ++++ IntegrationWizard/IntegrationWizard.vcxproj | 237 +++++++++++ .../IntegrationWizard.vcxproj.filters | 52 +++ IntegrationWizard/Resource.rc | 79 ++++ IntegrationWizard/resource.h | 17 + IntegrationWizard/src/IntegrationWizard.cpp | 165 ++++++++ IntegrationWizard/src/IntegrationWizard.h | 17 + IntegrationWizard/src/framework.h | 20 + IntegrationWizard/src/pch.cpp | 1 + IntegrationWizard/src/pch.h | 3 + IntegrationWizard/src/winmain.cpp | 128 ++++++ Koalageddon.sln | 81 ++++ LICENSE.txt | 12 + README.md | 74 ++++ Unlocker/Unlocker.vcxproj | 260 ++++++++++++ Unlocker/Unlocker.vcxproj.filters | 158 +++++++ Unlocker/src/DLLMonitor.cpp | 152 +++++++ Unlocker/src/DLLMonitor.h | 9 + Unlocker/src/ProcessHooker.cpp | 262 ++++++++++++ Unlocker/src/ProcessHooker.h | 9 + Unlocker/src/Unlocker.cpp | 51 +++ Unlocker/src/Unlocker.h | 10 + Unlocker/src/dllmain.cpp | 17 + Unlocker/src/framework.h | 19 + Unlocker/src/hook_util.cpp | 8 + Unlocker/src/hook_util.h | 42 ++ Unlocker/src/ntapi.h | 76 ++++ Unlocker/src/pch.cpp | 5 + Unlocker/src/pch.h | 2 + Unlocker/src/platforms/BasePlatform.cpp | 86 ++++ Unlocker/src/platforms/BasePlatform.h | 34 ++ Unlocker/src/platforms/epic/Epic.cpp | 41 ++ Unlocker/src/platforms/epic/Epic.h | 29 ++ Unlocker/src/platforms/epic/eos_base.h | 148 +++++++ Unlocker/src/platforms/epic/eos_common.h | 39 ++ Unlocker/src/platforms/epic/eos_ecom_types.h | 387 ++++++++++++++++++ Unlocker/src/platforms/epic/eos_hooks.cpp | 145 +++++++ Unlocker/src/platforms/epic/eos_hooks.h | 82 ++++ Unlocker/src/platforms/epic/eos_result.h | 384 +++++++++++++++++ Unlocker/src/platforms/origin/Origin.cpp | 174 ++++++++ Unlocker/src/platforms/origin/Origin.h | 16 + .../src/platforms/origin/origin_hooks.cpp | 73 ++++ Unlocker/src/platforms/origin/origin_hooks.h | 14 + Unlocker/src/platforms/steam/Steam.cpp | 45 ++ Unlocker/src/platforms/steam/Steam.h | 14 + Unlocker/src/platforms/steam/steam_hooks.cpp | 238 +++++++++++ Unlocker/src/platforms/steam/steam_hooks.h | 15 + Unlocker/src/platforms/steam/steam_ordinals.h | 52 +++ Unlocker/src/platforms/steam/steamtypes.h | 203 +++++++++ Unlocker/src/platforms/ubisoft/Ubisoft.cpp | 36 ++ Unlocker/src/platforms/ubisoft/Ubisoft.h | 13 + .../src/platforms/ubisoft/ubisoft_hooks.cpp | 140 +++++++ .../src/platforms/ubisoft/ubisoft_hooks.h | 65 +++ build_installer.bat | 8 + icon.ico | Bin 0 -> 69313 bytes inno_setup.iss | 59 +++ 84 files changed, 6055 insertions(+) create mode 100644 .gitignore create mode 100644 Common/Common.vcxproj create mode 100644 Common/Common.vcxproj.filters create mode 100644 Common/src/Config.cpp create mode 100644 Common/src/Config.h create mode 100644 Common/src/Logger.cpp create mode 100644 Common/src/Logger.h create mode 100644 Common/src/constants.h create mode 100644 Common/src/framework.h create mode 100644 Common/src/pch.cpp create mode 100644 Common/src/pch.h create mode 100644 Common/src/util.cpp create mode 100644 Common/src/util.h create mode 100644 Injector/Injector.vcxproj create mode 100644 Injector/Injector.vcxproj.filters create mode 100644 Injector/src/Injector.cpp create mode 100644 Injector/src/Injector.h create mode 100644 Injector/src/framework.h create mode 100644 Injector/src/main.cpp create mode 100644 Injector/src/pch.cpp create mode 100644 Injector/src/pch.h create mode 100644 Integration/Integration.vcxproj create mode 100644 Integration/Integration.vcxproj.filters create mode 100644 Integration/src/dllmain.cpp create mode 100644 Integration/src/framework.h create mode 100644 Integration/src/pch.cpp create mode 100644 Integration/src/pch.h create mode 100644 Integration/src/version_exports.h create mode 100644 IntegrationWizard/Config.jsonc create mode 100644 IntegrationWizard/IntegrationWizard.vcxproj create mode 100644 IntegrationWizard/IntegrationWizard.vcxproj.filters create mode 100644 IntegrationWizard/Resource.rc create mode 100644 IntegrationWizard/resource.h create mode 100644 IntegrationWizard/src/IntegrationWizard.cpp create mode 100644 IntegrationWizard/src/IntegrationWizard.h create mode 100644 IntegrationWizard/src/framework.h create mode 100644 IntegrationWizard/src/pch.cpp create mode 100644 IntegrationWizard/src/pch.h create mode 100644 IntegrationWizard/src/winmain.cpp create mode 100644 Koalageddon.sln create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Unlocker/Unlocker.vcxproj create mode 100644 Unlocker/Unlocker.vcxproj.filters create mode 100644 Unlocker/src/DLLMonitor.cpp create mode 100644 Unlocker/src/DLLMonitor.h create mode 100644 Unlocker/src/ProcessHooker.cpp create mode 100644 Unlocker/src/ProcessHooker.h create mode 100644 Unlocker/src/Unlocker.cpp create mode 100644 Unlocker/src/Unlocker.h create mode 100644 Unlocker/src/dllmain.cpp create mode 100644 Unlocker/src/framework.h create mode 100644 Unlocker/src/hook_util.cpp create mode 100644 Unlocker/src/hook_util.h create mode 100644 Unlocker/src/ntapi.h create mode 100644 Unlocker/src/pch.cpp create mode 100644 Unlocker/src/pch.h create mode 100644 Unlocker/src/platforms/BasePlatform.cpp create mode 100644 Unlocker/src/platforms/BasePlatform.h create mode 100644 Unlocker/src/platforms/epic/Epic.cpp create mode 100644 Unlocker/src/platforms/epic/Epic.h create mode 100644 Unlocker/src/platforms/epic/eos_base.h create mode 100644 Unlocker/src/platforms/epic/eos_common.h create mode 100644 Unlocker/src/platforms/epic/eos_ecom_types.h create mode 100644 Unlocker/src/platforms/epic/eos_hooks.cpp create mode 100644 Unlocker/src/platforms/epic/eos_hooks.h create mode 100644 Unlocker/src/platforms/epic/eos_result.h create mode 100644 Unlocker/src/platforms/origin/Origin.cpp create mode 100644 Unlocker/src/platforms/origin/Origin.h create mode 100644 Unlocker/src/platforms/origin/origin_hooks.cpp create mode 100644 Unlocker/src/platforms/origin/origin_hooks.h create mode 100644 Unlocker/src/platforms/steam/Steam.cpp create mode 100644 Unlocker/src/platforms/steam/Steam.h create mode 100644 Unlocker/src/platforms/steam/steam_hooks.cpp create mode 100644 Unlocker/src/platforms/steam/steam_hooks.h create mode 100644 Unlocker/src/platforms/steam/steam_ordinals.h create mode 100644 Unlocker/src/platforms/steam/steamtypes.h create mode 100644 Unlocker/src/platforms/ubisoft/Ubisoft.cpp create mode 100644 Unlocker/src/platforms/ubisoft/Ubisoft.h create mode 100644 Unlocker/src/platforms/ubisoft/ubisoft_hooks.cpp create mode 100644 Unlocker/src/platforms/ubisoft/ubisoft_hooks.h create mode 100644 build_installer.bat create mode 100644 icon.ico create mode 100644 inno_setup.iss diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e413c59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vs +_Build +*.vcxproj.user \ No newline at end of file diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj new file mode 100644 index 0000000..2d51f18 --- /dev/null +++ b/Common/Common.vcxproj @@ -0,0 +1,211 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + 16.0 + Win32Proj + {f364b2ab-f34a-4bcf-9362-22720020ddbf} + Common + 10.0 + + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + StaticLibrary + true + v142 + Unicode + + + StaticLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + + + false + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + + + true + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + + + false + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + + + $(PlatformTarget)-windows-static + true + + + $(PlatformTarget)-windows-static + true + + + $(PlatformTarget)-windows-static + true + + + $(PlatformTarget)-windows-static + true + + + + Level3 + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + %(AdditionalIncludeDirectories) + MultiThreadedDebug + stdcpp17 + + + + + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + %(AdditionalIncludeDirectories) + MultiThreaded + stdcpp17 + + + + + true + true + true + + + + + Level3 + true + _DEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + %(AdditionalIncludeDirectories) + MultiThreadedDebug + stdcpp17 + + + + + true + + + + + Level3 + true + true + true + NDEBUG;_LIB;%(PreprocessorDefinitions) + true + Use + pch.h + %(AdditionalIncludeDirectories) + MultiThreaded + stdcpp17 + + + + + true + true + true + + + + + + \ No newline at end of file diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters new file mode 100644 index 0000000..cd51572 --- /dev/null +++ b/Common/Common.vcxproj.filters @@ -0,0 +1,47 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/Common/src/Config.cpp b/Common/src/Config.cpp new file mode 100644 index 0000000..3cef775 --- /dev/null +++ b/Common/src/Config.cpp @@ -0,0 +1,55 @@ +#include "pch.h" +#include "Config.h" +#include "util.h" + +using nlohmann::json; + +// Source: https://stackoverflow.com/a/54394658/3805929 +#define GET(j, key) this->key = j[#key].get() + +void from_json(const json& j, Platform& p) +{ + j["enabled"].get_to(p.enabled); + j["process"].get_to(p.process); + j["replicate"].get_to(p.replicate); + j["ignore"].get_to(p.ignore); + j["blacklist"].get_to(p.blacklist); +} + +Config::Config() +{ + auto fullPath = getWorkingDirPath() / L"Config.jsonc"; + + std::ifstream ifs(fullPath, std::ios::in); + + if(!ifs.good()) + { + MessageBox(NULL, fullPath.c_str(), L"Config not found at: ", MB_ICONERROR | MB_ICONERROR); + exit(1); + } + + try + { + auto j = json::parse(ifs, nullptr, true, true); + + GET(j, log_level); + GET(j, platforms); + GET(j, ignore); + GET(j, terminate); + } catch(json::exception e) + { + MessageBoxA(NULL, e.what(), "Error parsing config file", MB_ICONERROR | MB_ICONERROR); + exit(1); + } +} + +void Config::init() +{ + if(config != nullptr) + return; + + config = new Config(); +} + +// Every app must call the config constructor first. +Config* config = nullptr; diff --git a/Common/src/Config.h b/Common/src/Config.h new file mode 100644 index 0000000..5d474e5 --- /dev/null +++ b/Common/src/Config.h @@ -0,0 +1,27 @@ +#pragma once +#include "framework.h" +#include "util.h" + +struct Platform +{ + bool enabled = true; + string process; + bool replicate = false; + vector ignore; + vector blacklist; +}; + +class Config +{ +protected: + Config(); +public: + string log_level; + map platforms; + vector ignore; + vector terminate; + + static void init(); +}; + +extern Config* config; diff --git a/Common/src/Logger.cpp b/Common/src/Logger.cpp new file mode 100644 index 0000000..717c586 --- /dev/null +++ b/Common/src/Logger.cpp @@ -0,0 +1,35 @@ +#include "pch.h" +#include "Logger.h" +#include "Config.h" + +namespace Logger +{ + +void init(string loggerName, bool truncate) +{ + Config::init(); + if(config->log_level == "off") + return; + + try + { + auto processPath = getProcessPath(); + auto fileName = fmt::format("{}.{}.log", loggerName, processPath.stem().string()); + auto path = getWorkingDirPath() / "logs" / fileName; + logger = spdlog::basic_logger_mt(loggerName, path.u8string(), truncate); + logger->set_pattern("[%H:%M:%S.%e] [%l]\t%v"); + logger->set_level(spdlog::level::from_str(config->log_level)); + logger->flush_on(spdlog::level::debug); + } catch(const spdlog::spdlog_ex& ex) + { + // Now if we can't open log file, something must be really wrong, hence we exit. + auto message = stow(string(ex.what())); + MessageBox(NULL, message.c_str(), L"Failed to initialize the log file", MB_ICONERROR | MB_OK); + exit(1); + } + +} + +} + +shared_ptr logger = spdlog::null_logger_mt("null"); diff --git a/Common/src/Logger.h b/Common/src/Logger.h new file mode 100644 index 0000000..c30afe1 --- /dev/null +++ b/Common/src/Logger.h @@ -0,0 +1,11 @@ +#pragma once +#include "util.h" + +extern shared_ptr logger; + +namespace Logger +{ + +void init(string loggerName, bool truncate); + +} diff --git a/Common/src/constants.h b/Common/src/constants.h new file mode 100644 index 0000000..21a2e84 --- /dev/null +++ b/Common/src/constants.h @@ -0,0 +1,23 @@ +#pragma once + +constexpr auto VERSION = "1.0.0"; +constexpr auto WORKING_DIR = L"WORKING_DIR"; + +constexpr auto INTEGRATION_64 = L"Integration64.dll"; +constexpr auto INTEGRATION_32 = L"Integration32.dll"; + +#ifdef _WIN64 + +constexpr auto EOSSDK = L"EOSSDK-Win64-Shipping.dll"; +constexpr auto STEAMAPI = L"steam_api64.dll"; +constexpr auto UPLAY_R2 = L"uplay_r2_loader64.dll"; + +#else + +constexpr auto EOSSDK = L"EOSSDK-Win32-Shipping.dll"; +constexpr auto STEAMAPI = L"steam_api.dll"; +constexpr auto UPLAY_R2 = L"uplay_r2_loader.dll"; + +#endif + +constexpr auto ORIGINCLIENT = L"OriginClient.dll"; diff --git a/Common/src/framework.h b/Common/src/framework.h new file mode 100644 index 0000000..c8926e6 --- /dev/null +++ b/Common/src/framework.h @@ -0,0 +1,35 @@ +#pragma once +#include + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Following definitions are required for static build of libcurl +#pragma comment(lib,"Ws2_32.lib") +#pragma comment(lib,"Wldap32.lib") +#pragma comment(lib,"Crypt32.lib") + +#pragma warning(push) // Disable 3rd party library warnings +#pragma warning(disable: ALL_CODE_ANALYSIS_WARNINGS) + +#define SPDLOG_WCHAR_TO_UTF8_SUPPORT +#include +#include +#include +#include +#include +#include +#include +#include +#pragma warning(pop) diff --git a/Common/src/pch.cpp b/Common/src/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/Common/src/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/Common/src/pch.h b/Common/src/pch.h new file mode 100644 index 0000000..c9c7883 --- /dev/null +++ b/Common/src/pch.h @@ -0,0 +1,2 @@ +#pragma once +#include "framework.h" diff --git a/Common/src/util.cpp b/Common/src/util.cpp new file mode 100644 index 0000000..58ee7d2 --- /dev/null +++ b/Common/src/util.cpp @@ -0,0 +1,227 @@ +#include "pch.h" +#include "util.h" +#include "Logger.h" +#include "constants.h" +#include // ??? + + +bool contains(wstring haystack, wstring needle) +{ + return haystack.find(needle) != wstring::npos; +} + +bool startsWith(string word, string prefix) +{ + return word.find(prefix, 0) == 0; +} + +bool endsWith(string word, string postfix) +{ + return word.find(postfix, 0) == word.length() - postfix.length(); +} + +string toLower(string str) +{ + string lowerString = str; // Copy constructor. + std::for_each(lowerString.begin(), lowerString.end(), [](char& c){ + c = ::tolower(c); + }); + return lowerString; +} + +/// Case-insensitive string comparison. Returns true if strings are equal. +bool stringsAreEqual(string one, string two) +{ + return toLower(one) == toLower(two); +} + +wstring getProcessName(DWORD pid) +{ + auto defaultName = wstring(L""); + auto hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); + if(hProcess == NULL) + { + logger->error("Failed to open handle to a process with id: {}. Error code: 0x{0:}", pid, GetLastError()); + return defaultName; + } + + TCHAR buffer[MAX_PATH]; + auto result = GetModuleFileNameEx(hProcess, NULL, buffer, MAX_PATH); + if(result == NULL) + { + logger->error("Failed to get process name with id: {}s. Error code: 0x{0:}", pid, GetLastError()); + return defaultName; + } + + return wstring(buffer); +} + +wstring getCurrentModuleName() +{ + wchar_t name[MAX_PATH]; + auto result = GetModuleFileName(NULL, name, MAX_PATH); + + if(result == NULL) + logger->error("Failed to get current module's file name. Error code: {}", GetLastError()); + + return wstring(name); +} + +/** +Returns std::filesystem::path of the process identified by the +provided handle. If handle is not provided, then currently running +process path is returned. +*/ +path getProcessPath(HANDLE handle) +{ + TCHAR buffer[MAX_PATH]; + DWORD result = 0; + if(handle == NULL) + result = GetModuleFileName(NULL, buffer, MAX_PATH); + else + result = GetModuleFileNameEx(handle, NULL, buffer, MAX_PATH); + + if(result == NULL) + { + auto message = fmt::format("Failed to obtain process path. Error code: 0x{:X}", GetLastError()); + throw std::exception(message.c_str()); + } + + return absolute(buffer); +} + +path getWorkingDirPath() +{ + return absolute(getReg(WORKING_DIR)); +} + +bool is32bit(DWORD PID) +{ + BOOL isWow64; + + auto process = OpenProcess(PROCESS_QUERY_INFORMATION, TRUE, PID); + if(process == NULL) + { // Should not happen often. If it does, worst case is failed injection. + logger->error("Failed to get a handle to process with PID: {}", PID); + return true; + } + + IsWow64Process(process, &isWow64); + CloseHandle(process); + + // If a process is running under WOW64, then it means it is 32-bit. + return isWow64; +} + +bool is32bit(HANDLE hProcess) +{ + // Cannot use IsWow64Process2 since it is supported by Win 10 only + BOOL isWow64 = NULL; + IsWow64Process(hProcess, &isWow64); + return isWow64; +} + +void killProcess(HANDLE hProcess, DWORD sleepMS) +{ + TerminateProcess(hProcess, 0); + auto result = WaitForSingleObject(hProcess, 3 * 1000); // 3s should be enough + if(result != WAIT_OBJECT_0) + { + auto processName = getProcessPath(hProcess).string(); + throw std::exception(fmt::format("Failed to terminate process: {}", processName).c_str()); + } + else + { + Sleep(sleepMS); // Some other process may still be locking the file + } +} + + +// Source: https://stackoverflow.com/a/3999597/3805929 + +// Convert a wide Unicode string to a UTF8 string +std::string wtos(const std::wstring& wstr) +{ + if(wstr.empty()) return std::string(); + int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int) wstr.size(), NULL, 0, NULL, NULL); + std::string strTo(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int) wstr.size(), &strTo[0], size_needed, NULL, NULL); + return strTo; +} + +// Convert a UTF8 string to a wide Unicode String +std::wstring stow(const std::string& str) +{ + if(str.empty()) return std::wstring(); + int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], (int) str.size(), NULL, 0); + std::wstring wstrTo(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, &str[0], (int) str.size(), &wstrTo[0], size_needed); + return wstrTo; +} + + +char* makeCStringCopy(string src) +{ + const auto len = src.length(); + char* dest = new char[len + 1]; + std::size_t length = src.copy(dest, len, 0); + dest[len] = '\0'; + return dest; +} + +wstring getReg(LPCWSTR key) +{ + static winreg::RegKey regKey{ HKEY_CURRENT_USER, L"SOFTWARE\\acidicoala\\Koalageddon" }; + return regKey.GetStringValue(key); +} + +void setReg(LPCWSTR key, LPCWSTR val) +{ + static winreg::RegKey regKey{ HKEY_CURRENT_USER, L"SOFTWARE\\acidicoala\\Koalageddon" }; + regKey.SetStringValue(key, val); +} +string readFileContents(string path) +{ + std::ifstream fileStream(path); + if(fileStream.good()) + return string(std::istreambuf_iterator{fileStream}, {}); + else + return ""; +} + +bool writeFileContents(path filePath, string contents) +{ + if(!std::filesystem::exists(filePath)) + std::filesystem::create_directories(filePath.parent_path()); + + std::ofstream fileStream(filePath); + if(fileStream.good()) + { + fileStream << contents; + return true; + } + else + { + logger->error("Failed to write to file: {}", filePath.string()); + return false; + } +} + +void showFatalError(string message, bool terminate) +{ + message = fmt::format("{}\nLast error: 0x{:X}", message, GetLastError()); + logger->error(message); + MessageBoxA(NULL, message.c_str(), "Fatal Error", MB_ICONERROR | MB_OK); + + if(terminate) + exit(1); +} + +void showInfo(string message, string title, bool shouldLog) +{ + if(shouldLog) + logger->info(message); + + MessageBoxA(NULL, message.c_str(), title.c_str(), MB_ICONINFORMATION | MB_OK); +} + diff --git a/Common/src/util.h b/Common/src/util.h new file mode 100644 index 0000000..4d8e4ed --- /dev/null +++ b/Common/src/util.h @@ -0,0 +1,65 @@ +#pragma once +#include "framework.h" + +// Import into global namespace commonly used classes +using std::string; +using std::wstring; +using std::vector; +using std::pair; +using std::map; +using std::shared_ptr; +using std::unique_ptr; +using std::make_unique; +using std::filesystem::absolute; +using std::filesystem::path; +using std::filesystem::copy_options; + +constexpr auto INJECTOR_64 = L"Injector64.exe"; +constexpr auto INJECTOR_32 = L"Injector32.exe"; + +constexpr auto UNLOCKER_64 = L"Unlocker64.dll"; +constexpr auto UNLOCKER_32 = L"Unlocker32.dll"; + +#ifdef _WIN64 +constexpr auto UNLOCKER_NAME = "Unlocker64"; +#else +constexpr auto UNLOCKER_NAME = "Unlocker32"; +#endif + +// Process info +wstring getProcessName(DWORD pid); +wstring getCurrentModuleName(); +path getProcessPath(HANDLE handle = NULL); +bool is32bit(DWORD PID); +bool is32bit(HANDLE hProcess); +void killProcess(HANDLE hProcess, DWORD sleepMS = 0); + +// String utils +string wtos(const wstring& wstr); +wstring stow(const string& wstr); +char* makeCStringCopy(string src); +bool contains(wstring haystack, wstring needle); +bool startsWith(string word, string prefix); +bool endsWith(string word, string postfix); +string toLower(string str); +bool stringsAreEqual(string one, string two); + +// Registry +wstring getReg(LPCWSTR key); +void setReg(LPCWSTR key, LPCWSTR val); +path getWorkingDirPath(); + +// File access +string readFileContents(string path); +bool writeFileContents(path filePath, string contents); + +// Message Box helpers +void showFatalError(string message, bool terminate); +void showInfo(string message, string title = "Information", bool shouldLog = false); + + +template +bool vectorContains(vector elements, T element) +{ + return std::find(elements.begin(), elements.end(), element) != elements.end(); +} diff --git a/Injector/Injector.vcxproj b/Injector/Injector.vcxproj new file mode 100644 index 0000000..54cea96 --- /dev/null +++ b/Injector/Injector.vcxproj @@ -0,0 +1,212 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {5d2b330b-73e9-4b02-8203-be7078da646b} + Injector + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + false + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + true + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + false + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + $(SolutionDir)Common\src + Use + pch.h + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + $(SolutionDir)Common\src + Use + pch.h + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreadedDebug + $(SolutionDir)Common\src + Use + pch.h + + + Console + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + MultiThreaded + $(SolutionDir)Common\src + Use + pch.h + + + Console + true + true + true + + + + + {f364b2ab-f34a-4bcf-9362-22720020ddbf} + + + + + + + + + + + + Create + Create + Create + Create + + + + + + \ No newline at end of file diff --git a/Injector/Injector.vcxproj.filters b/Injector/Injector.vcxproj.filters new file mode 100644 index 0000000..50622c9 --- /dev/null +++ b/Injector/Injector.vcxproj.filters @@ -0,0 +1,35 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/Injector/src/Injector.cpp b/Injector/src/Injector.cpp new file mode 100644 index 0000000..9f5d5c7 --- /dev/null +++ b/Injector/src/Injector.cpp @@ -0,0 +1,51 @@ +#include "pch.h" +#include "Injector.h" +#include "Logger.h" + +// Source: https://github.com/saeedirha/DLL-Injector +int injectDLL(const int pid, wstring dllPath) +{ + // 1. Get handle to the process + auto processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); + if(processHandle == NULL) + { + logger->error("Failed to open the target process. Error code: {}", GetLastError()); + return ERROR_PROCESS_OPEN; + } + + // 2. Allocte memory in that process + auto dllPathSize = 2 * (dllPath.length() + 1); + auto dllPathAddress = VirtualAllocEx(processHandle, NULL, dllPathSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if(dllPathAddress == NULL) + { + logger->error("Failed to allocate memory in the target process. Error code: {}", GetLastError()); + CloseHandle(processHandle); + return ERROR_TARGET_MALLOC; + } + + // 3. Write DLL path into the newly allocated memory space + auto writeSuccess = WriteProcessMemory(processHandle, dllPathAddress, dllPath.c_str(), dllPathSize, NULL); + if(!writeSuccess) + { + logger->error("Failed to write in memory of the target process. Error code: {}", GetLastError()); + CloseHandle(processHandle); + return ERROR_TARGET_WRITE; + } + + // 4. Create new thread in the target process + auto threadHandle = CreateRemoteThread(processHandle, NULL, NULL, (LPTHREAD_START_ROUTINE) LoadLibraryW, dllPathAddress, NULL, NULL); + if(threadHandle == NULL) + { + logger->error("Failed to create a remote thread in the target process. Error code: {}", GetLastError()); + logger->error("\tprocHandle: {}", (void*) processHandle); + logger->error("\tloadLibraryAddress: {}", (void*) &LoadLibraryA); + logger->error("\tdllPathAddress: {}", (void*) dllPathAddress); + CloseHandle(processHandle); + return ERROR_REMOTE_THREAD; + } + + CloseHandle(threadHandle); + CloseHandle(processHandle); + + return OK; +} diff --git a/Injector/src/Injector.h b/Injector/src/Injector.h new file mode 100644 index 0000000..c7d6609 --- /dev/null +++ b/Injector/src/Injector.h @@ -0,0 +1,17 @@ +#pragma once +#include "framework.h" +#include "util.h" + +enum ExitCode +{ + OK, + ERROR_INVALID_ARGUMENTS, + ERROR_PROCESS_OPEN, + ERROR_TARGET_MALLOC, + ERROR_TARGET_WRITE, + ERROR_KERNEL_HANDLE, + ERROR_PROC_ADDRESS, + ERROR_REMOTE_THREAD +}; + +int injectDLL(const int pid, wstring DLL_Path); diff --git a/Injector/src/framework.h b/Injector/src/framework.h new file mode 100644 index 0000000..05653a6 --- /dev/null +++ b/Injector/src/framework.h @@ -0,0 +1,7 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#include + +#include +#include diff --git a/Injector/src/main.cpp b/Injector/src/main.cpp new file mode 100644 index 0000000..0e17279 --- /dev/null +++ b/Injector/src/main.cpp @@ -0,0 +1,49 @@ +#include "pch.h" +#include "util.h" +#include "constants.h" +#include "Injector.h" +#include "Logger.h" +#include "Config.h" + +// Hide console window +#pragma comment(linker, "/SUBSYSTEM:windows /ENTRY:mainCRTStartup") + +int main(int argc, char** argv) +{ + Config::init(); + Logger::init("Injector", false); + + logger->debug("Injector v{}", VERSION); + + if(argc != 3) + { + logger->error("Expected 2 arguments, received: {}", argc - 1); + return ERROR_INVALID_ARGUMENTS; + } + + try + { + auto PID = (DWORD) std::stoi(argv[1]); + auto dllPath = stow(argv[2]); + + logger->info(L"Injecting DLL into \"{}\"", getProcessName(PID)); + logger->debug(L"PID: {}, dllPath: \"{}\"", PID, dllPath); + + auto result = injectDLL(PID, dllPath); + + if(result == OK) + { + logger->info("DLL was successully injected"); + } + else + { + logger->error("Failed to inject the DLL. Error code: 0x{0:}", result); + } + + return result; + } catch(std::logic_error&) + { + logger->error("Failed to convert PID {} to int", argv[1]); + return ERROR_INVALID_ARGUMENTS; + } +} diff --git a/Injector/src/pch.cpp b/Injector/src/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/Injector/src/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/Injector/src/pch.h b/Injector/src/pch.h new file mode 100644 index 0000000..c9c7883 --- /dev/null +++ b/Injector/src/pch.h @@ -0,0 +1,2 @@ +#pragma once +#include "framework.h" diff --git a/Integration/Integration.vcxproj b/Integration/Integration.vcxproj new file mode 100644 index 0000000..26de0de --- /dev/null +++ b/Integration/Integration.vcxproj @@ -0,0 +1,213 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {6b6f6e00-a196-4815-8d24-d9890798fd09} + Integration + 10.0 + + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + false + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + true + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + false + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + + Level3 + true + WIN32;_DEBUG;INTEGRATION_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + MultiThreadedDebug + $(SolutionDir)Common\src + + + Windows + true + false + + + + + Level3 + true + true + true + WIN32;NDEBUG;INTEGRATION_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + MultiThreaded + $(SolutionDir)Common\src + + + Windows + true + true + true + false + + + + + Level3 + true + _DEBUG;INTEGRATION_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + MultiThreadedDebug + $(SolutionDir)Common\src + + + Windows + true + false + + + + + Level3 + true + true + true + NDEBUG;INTEGRATION_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + MultiThreaded + $(SolutionDir)Common\src + + + Windows + true + true + true + false + + + + + {f364b2ab-f34a-4bcf-9362-22720020ddbf} + + + + + + + + + + + Create + Create + + + + + + \ No newline at end of file diff --git a/Integration/Integration.vcxproj.filters b/Integration/Integration.vcxproj.filters new file mode 100644 index 0000000..5141be1 --- /dev/null +++ b/Integration/Integration.vcxproj.filters @@ -0,0 +1,32 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/Integration/src/dllmain.cpp b/Integration/src/dllmain.cpp new file mode 100644 index 0000000..e51b1d8 --- /dev/null +++ b/Integration/src/dllmain.cpp @@ -0,0 +1,59 @@ +#include "pch.h" +#include "Logger.h" +#include "Config.h" +#include "constants.h" + +using std::filesystem::path; + +path thisDllPath; +HMODULE hUnlocker = NULL; + +void init(HMODULE hModule) +{ + + Logger::init("Integration", true); + logger->info("Integration v{}", VERSION); + + DisableThreadLibraryCalls(hModule); + + auto currentProcess = getProcessPath(); + logger->debug("Current process: {}", currentProcess.string()); + + for(const auto& [key, platform] : config->platforms) + { + if(stringsAreEqual(currentProcess.filename().string(), platform.process)) + { + logger->info("Target platform detected: {}", platform.process); + auto unlockerPath = absolute(getReg(WORKING_DIR)) / (UNLOCKER_NAME + string(".dll")); + logger->debug("Unlocker path: {}", unlockerPath.string()); + hUnlocker = LoadLibrary(unlockerPath.wstring().c_str()); + if(hUnlocker == NULL) + { + logger->error("Failed to load the Unlocker. Error code: 0x{:X}", GetLastError()); + } + else + { + logger->info("Successfully loaded the Unlocker"); + } + } + } +} + +void shutdown() +{ + if(hUnlocker != NULL) + FreeLibrary(hUnlocker); + +} + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +{ + + if(ul_reason_for_call == DLL_PROCESS_ATTACH) + init(hModule); + else if(ul_reason_for_call == DLL_PROCESS_DETACH) + shutdown(); + + return TRUE; +} + diff --git a/Integration/src/framework.h b/Integration/src/framework.h new file mode 100644 index 0000000..3965caf --- /dev/null +++ b/Integration/src/framework.h @@ -0,0 +1,8 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +#include + +#include "version_exports.h" diff --git a/Integration/src/pch.cpp b/Integration/src/pch.cpp new file mode 100644 index 0000000..64b7eef --- /dev/null +++ b/Integration/src/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/Integration/src/pch.h b/Integration/src/pch.h new file mode 100644 index 0000000..885d5d6 --- /dev/null +++ b/Integration/src/pch.h @@ -0,0 +1,13 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +// add headers that you want to pre-compile here +#include "framework.h" + +#endif //PCH_H diff --git a/Integration/src/version_exports.h b/Integration/src/version_exports.h new file mode 100644 index 0000000..e9535f5 --- /dev/null +++ b/Integration/src/version_exports.h @@ -0,0 +1,19 @@ +#pragma once + +#pragma comment(linker, "/export:GetFileVersionInfoA=version_o.GetFileVersionInfoA") +#pragma comment(linker, "/export:GetFileVersionInfoByHandle=version_o.GetFileVersionInfoByHandle") +#pragma comment(linker, "/export:GetFileVersionInfoExA=version_o.GetFileVersionInfoExA") +#pragma comment(linker, "/export:GetFileVersionInfoExW=version_o.GetFileVersionInfoExW") +#pragma comment(linker, "/export:GetFileVersionInfoSizeA=version_o.GetFileVersionInfoSizeA") +#pragma comment(linker, "/export:GetFileVersionInfoSizeExA=version_o.GetFileVersionInfoSizeExA") +#pragma comment(linker, "/export:GetFileVersionInfoSizeExW=version_o.GetFileVersionInfoSizeExW") +#pragma comment(linker, "/export:GetFileVersionInfoSizeW=version_o.GetFileVersionInfoSizeW") +#pragma comment(linker, "/export:GetFileVersionInfoW=version_o.GetFileVersionInfoW") +#pragma comment(linker, "/export:VerFindFileA=version_o.VerFindFileA") +#pragma comment(linker, "/export:VerFindFileW=version_o.VerFindFileW") +#pragma comment(linker, "/export:VerInstallFileA=version_o.VerInstallFileA") +#pragma comment(linker, "/export:VerInstallFileW=version_o.VerInstallFileW") +#pragma comment(linker, "/export:VerLanguageNameA=version_o.VerLanguageNameA") +#pragma comment(linker, "/export:VerLanguageNameW=version_o.VerLanguageNameW") +#pragma comment(linker, "/export:VerQueryValueA=version_o.VerQueryValueA") +#pragma comment(linker, "/export:VerQueryValueW=version_o.VerQueryValueW") diff --git a/IntegrationWizard/Config.jsonc b/IntegrationWizard/Config.jsonc new file mode 100644 index 0000000..9b09d47 --- /dev/null +++ b/IntegrationWizard/Config.jsonc @@ -0,0 +1,84 @@ +{ + "config_version": 1, // DO NOT EDIT THIS VALUE + "log_level": "debug", + "platforms": { + "Steam": { + "enabled": true, + "process": "steam.exe", + "replicate": true, + "ignore": [ + "x86launcher.exe", + "x64launcher.exe", + "SteamService.exe", + "steamwebhelper.exe", + "GameOverlayUI.exe", + "gldriverquery.exe", + "gldriverquery64.exe", + "vulkandriverquery.exe", + "vulkandriverquery64.exe" + ], + "blacklist": [ // Get App ID from SteamDB + "22618", // Alien Breed: Impact - PL Check [Do not force polish language] + "67379" // Darkness II Low Violence [Do not censor violence] + ] + }, + "Epic Games": { + "enabled": true, + "process": "EpicGamesLauncher.exe", + "replicate": true, + "ignore": [ + "EpicWebHelper.exe", + "EpicOnlineServicesHost.exe", + "EpicOnlineServicesUserHelper.exe", + "UnrealCEFSubProcess.exe" + ], + "blacklist": [ // Get DLC ID from ScreamDB + "ffffffffffffffffffffffffffffffff" // A Total War Sage: TROY [It actually asks this ID...] + ] + }, + "Origin": { + "enabled": true, + "process": "Origin.exe", + "replicate": false, + "ignore": [], + "blacklist": [ // Use ItemId from Unlocker32.Origin.log + // "SIMS4.OFF.SOLP.0x0000000000030553" // Sims 4: Get Famous [Better stay anonymous] + ] + }, + "Ubisoft Connect": { + "enabled": false, // Ubisoft games unload the Unlocker DLL :( + "process": "upc.exe", + "replicate": true, + "ignore": [ + "UplayService.exe", + "UplayWebCore.exe" + ], + "blacklist": [] + } + }, + "ignore": [ + // Do not inject our own injector + "Injector32.exe", + "Injector64.exe", + // System + "reg.exe", + "cmd.exe", + "regsvr32.exe", + // Unreal Engine + "CrashReportClient.exe", + // Origin integration with other stores + "EALink.exe", + // Ubisoft integration with other stores + "UbisoftGameLauncher.exe", + "UbisoftGameLauncher64.exe" + ], + "terminate": [ + // Steam + "steamerrorreporter.exe", + // Origin + "OriginER.exe", + "OriginCrashReporter.exe", + // Ubisoft + "UplayCrashReporter.exe" + ] +} diff --git a/IntegrationWizard/IntegrationWizard.vcxproj b/IntegrationWizard/IntegrationWizard.vcxproj new file mode 100644 index 0000000..0a06000 --- /dev/null +++ b/IntegrationWizard/IntegrationWizard.vcxproj @@ -0,0 +1,237 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {4e88d0ed-22df-4db8-985b-22b778059f11} + IntegrationWizard + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + false + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + true + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + false + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + $(SolutionDir)Common\src + MultiThreadedDebug + Use + pch.h + + + Windows + true + RequireAdministrator + + + + + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + $(SolutionDir)Common\src + MultiThreaded + Use + pch.h + + + Windows + true + true + true + RequireAdministrator + + + + + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + $(SolutionDir)Common\src + MultiThreadedDebug + Use + pch.h + + + Windows + true + RequireAdministrator + + + + + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp17 + $(SolutionDir)Common\src + MultiThreaded + Use + pch.h + + + Windows + true + true + true + RequireAdministrator + + + + + + + + + {f364b2ab-f34a-4bcf-9362-22720020ddbf} + + + + + + + + + + + + + + + + + + Create + Create + + + + + + + \ No newline at end of file diff --git a/IntegrationWizard/IntegrationWizard.vcxproj.filters b/IntegrationWizard/IntegrationWizard.vcxproj.filters new file mode 100644 index 0000000..5266d91 --- /dev/null +++ b/IntegrationWizard/IntegrationWizard.vcxproj.filters @@ -0,0 +1,52 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Resource Files + + + + + Resource Files + + + + + Header Files + + + Header Files + + + Header Files + + + Resource Files + + + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/IntegrationWizard/Resource.rc b/IntegrationWizard/Resource.rc new file mode 100644 index 0000000..3093b7f --- /dev/null +++ b/IntegrationWizard/Resource.rc @@ -0,0 +1,79 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// CONFIG +// + +IDR_DEFAULT_CONFIG CONFIG ".\\Config.jsonc" + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON "D:\\Dev\\VisualStudioProjects\\Koalageddon\\icon.ico" + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/IntegrationWizard/resource.h b/IntegrationWizard/resource.h new file mode 100644 index 0000000..176c9aa --- /dev/null +++ b/IntegrationWizard/resource.h @@ -0,0 +1,17 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Resource.rc +// +#define IDR_DEFAULT_CONFIG 101 +#define IDI_ICON1 103 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 104 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/IntegrationWizard/src/IntegrationWizard.cpp b/IntegrationWizard/src/IntegrationWizard.cpp new file mode 100644 index 0000000..8437d12 --- /dev/null +++ b/IntegrationWizard/src/IntegrationWizard.cpp @@ -0,0 +1,165 @@ +#include "pch.h" +#include "IntegrationWizard.h" +#include "Logger.h" +#include "Config.h" +#include "constants.h" + +vector alteredPlatforms; + + +/* +Returns the path to original version.dll based on the architecture +of the provided process handle. +*/ +path getVersionDllPath(HANDLE hProcess) +{ + PWSTR rawPath; + auto folderID = is32bit(hProcess) ? FOLDERID_SystemX86 : FOLDERID_System; + SHGetKnownFolderPath(folderID, NULL, NULL, &rawPath); + auto systemPath = absolute(rawPath); + CoTaskMemFree(rawPath); + return systemPath / "version.dll"; +} + +// Source: https://docs.microsoft.com/en-us/windows/win32/psapi/enumerating-all-processes +void enumerateProcesses(function callback) +{ + DWORD aProcesses[1024], cbNeeded, cProcesses; + + if(!EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded)) + showFatalError("Failed to enumerate processes.", true); + + // Calculate how many process identifiers were returned. + cProcesses = cbNeeded / sizeof(DWORD); + + logger->debug("Found running processes:"); + + // Print the name and process identifier for each process. + for(DWORD i = 0; i < cProcesses; i++) + { + auto pid = aProcesses[i]; + if(pid == 0) + continue; + + // Get a handle to the process. + HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); + + if(hProcess == NULL) + continue; // Not a big deal, just skip it. + + try + { + auto processPath = getProcessPath(hProcess); + auto processName = processPath.filename().string(); + logger->debug("\tName: '{}', pid: {}", processName, pid); + + for(const auto& [key, platform] : config->platforms) + { + if(stringsAreEqual(processName, platform.process) && platform.enabled) + { + logger->info("Target process detected: '{}'", processName); + callback(hProcess, processPath, processName); + } + } + } catch(std::exception& ex) + { + logger->warn("Error in handling a process with PID: {}. Reason: {}", pid, ex.what()); + } + + + CloseHandle(hProcess); + } +} + +void installCallback(HANDLE hProcess, path processPath, string processName) +{ + try + { + logger->info("Installing platform integration"); + + auto originalDllPath = getVersionDllPath(hProcess); + auto originalVersionDllPath = processPath.parent_path() / "version_o.dll"; + logger->debug("Original DLL path: '{}', Destination: '{}'", originalDllPath.string(), originalVersionDllPath.string()); + + auto integrationDllPath = getWorkingDirPath() / (is32bit(hProcess) ? INTEGRATION_32 : INTEGRATION_64); + auto versionDLLPath = processPath.parent_path() / "version.dll"; + logger->debug("Integration DLL path: '{}', Destination: '{}'", integrationDllPath.string(), versionDLLPath.string()); + + // Terminate the process to release a possible lock on the files + killProcess(hProcess); + + copy_file(originalDllPath, originalVersionDllPath, copy_options::overwrite_existing); + copy_file(integrationDllPath, versionDLLPath, copy_options::overwrite_existing); + + alteredPlatforms.push_back(processName); + logger->info("Platform integration was successfully installed"); + + } catch(std::exception& ex) + { + showFatalError(fmt::format("Failed to install integrations: {}", ex.what()), false); + } +} + +void removeCallback(HANDLE hProcess, path processPath, string processName) +{ + try + { + logger->info("Removing platform integration"); + + auto versionDLLPath = stow((processPath.parent_path() / "version.dll").string()); + auto originalDllPath = stow((processPath.parent_path() / "version_o.dll").string()); + logger->debug(L"Version DLL path: '{}', Original DLL path: '{}'", versionDLLPath, originalDllPath); + + // Terminate the process to release a lock on the files + killProcess(hProcess, 250); + + DeleteFile(versionDLLPath.c_str()); + DeleteFile(originalDllPath.c_str()); + + alteredPlatforms.push_back(processName); + logger->info("Platform integration was successfully removed"); + + } catch(std::exception& ex) + { + showFatalError(fmt::format("Failed to remove integrations: {}", ex.what()), false); + } +} + +void showPostActionReport(Action action) +{ + if(alteredPlatforms.size() == 0) + { + showInfo("No target processes were found. No actions were taken.", "Nothing found", true); + } + else + { + string targets; + for(const auto& target : alteredPlatforms) + { + auto symbol = action == Action::INSTALL_INTEGRATIONS ? "+" : "-"; + targets += fmt::format("[{}] {}\n", symbol, target); + } + + auto actionStr = action == Action::INSTALL_INTEGRATIONS ? "installed" : "removed"; + auto message = fmt::format("The following target platforms integrations were sucessfully {}:\n\n{}", actionStr, targets); + showInfo(message, "Success"); + } +} + +void IntegrationWizard::install() +{ + logger->info("Installing integrations"); + + enumerateProcesses(installCallback); + + showPostActionReport(Action::INSTALL_INTEGRATIONS); +} + +void IntegrationWizard::remove() +{ + logger->info("Removing integrations"); + + enumerateProcesses(removeCallback); + + showPostActionReport(Action::REMOVE_INTEGRATIONS); +} diff --git a/IntegrationWizard/src/IntegrationWizard.h b/IntegrationWizard/src/IntegrationWizard.h new file mode 100644 index 0000000..3204d75 --- /dev/null +++ b/IntegrationWizard/src/IntegrationWizard.h @@ -0,0 +1,17 @@ +#pragma once + +namespace IntegrationWizard +{ + +void install(); +void remove(); + +} + +enum class Action +{ + NO_ACTION = 1000, + UNEXPECTED_ERROR = 1001, + INSTALL_INTEGRATIONS = 1002, + REMOVE_INTEGRATIONS = 1003, +}; diff --git a/IntegrationWizard/src/framework.h b/IntegrationWizard/src/framework.h new file mode 100644 index 0000000..fd05e2d --- /dev/null +++ b/IntegrationWizard/src/framework.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +#include +#pragma comment(lib, "comctl32.lib") +#if defined _M_IX86 +#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") +#elif defined _M_IA64 +#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df' language='*'\"") +#elif defined _M_X64 +#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") +#else +#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") +#endif + +#include + +using std::function; diff --git a/IntegrationWizard/src/pch.cpp b/IntegrationWizard/src/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/IntegrationWizard/src/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/IntegrationWizard/src/pch.h b/IntegrationWizard/src/pch.h new file mode 100644 index 0000000..8909fe2 --- /dev/null +++ b/IntegrationWizard/src/pch.h @@ -0,0 +1,3 @@ +#pragma once + +#include "framework.h" diff --git a/IntegrationWizard/src/winmain.cpp b/IntegrationWizard/src/winmain.cpp new file mode 100644 index 0000000..f466a33 --- /dev/null +++ b/IntegrationWizard/src/winmain.cpp @@ -0,0 +1,128 @@ +#include "pch.h" +#include "Logger.h" +#include "constants.h" +#include "IntegrationWizard.h" +#include "../resource.h" + +Action askForAction(HINSTANCE hInstance) +{ + TASKDIALOGCONFIG tdc = { sizeof(TASKDIALOGCONFIG) }; + int nClickedBtn; + auto szTitle = L"Koalageddon"; + auto szHeader = L"Welcome to the Koalageddon wizard. Please choose the desired action."; + LPCWSTR szBodyText = + L"The wizard will scan running processes to find target platforms. " \ + L"During installation/removal target processes will be terminated, " \ + L"so make sure to close all games and save all data."; + + TASKDIALOG_BUTTON aCustomButtons[] = { + { (int) Action::INSTALL_INTEGRATIONS, L"&Install platform integrations" }, + { (int) Action::REMOVE_INTEGRATIONS, L"&Remove platform integrations" } + }; + + tdc.hInstance = hInstance; + tdc.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS | TDF_EXPAND_FOOTER_AREA; + tdc.pButtons = aCustomButtons; + tdc.cButtons = _countof(aCustomButtons); + tdc.pszWindowTitle = szTitle; + tdc.pszMainIcon = TD_INFORMATION_ICON; + tdc.pszMainInstruction = szHeader; + //tdc.pszContent = szBodyText; + tdc.pszExpandedInformation = szBodyText; + + if(SUCCEEDED(TaskDialogIndirect(&tdc, &nClickedBtn, NULL, NULL))) + { + logger->debug("Clicked button: {}", nClickedBtn); + + if(nClickedBtn == (int) Action::INSTALL_INTEGRATIONS || + nClickedBtn == (int) Action::REMOVE_INTEGRATIONS) + return (Action) nClickedBtn; + else + return Action::NO_ACTION; + } + else + { + return Action::UNEXPECTED_ERROR; + } + +} + +void fatalError(string message) +{ + message = fmt::format("{}. Error code: 0x{:X}", message, GetLastError()); + MessageBoxA(NULL, message.c_str(), "Fatal Error", MB_ICONERROR | MB_OK); + exit(1); +} + +void firstSetup() +{ + setReg(WORKING_DIR, getProcessPath().parent_path().c_str()); + + auto configPath = getWorkingDirPath() / L"Config.jsonc"; + + if(!std::filesystem::exists(configPath)) + { // Copy the default config is none was found + HRSRC hResource = FindResource(nullptr, MAKEINTRESOURCE(IDR_DEFAULT_CONFIG), L"CONFIG"); + if(hResource == NULL) + fatalError("Failed to find config resource"); + + HGLOBAL hMemory = LoadResource(nullptr, hResource); + if(hMemory == NULL) + fatalError("Failed to load config resource"); + + auto size = SizeofResource(nullptr, hResource); + auto dataPtr = LockResource(hMemory); + + std::ofstream configFile(configPath, std::ios::out | std::ios::binary); + if(!configFile.good()) + fatalError("Failed to open output file stream"); + + configFile.write((char*) dataPtr, size); + configFile.close(); + } +} + + +int APIENTRY wWinMain( + _In_ HINSTANCE hInstance, + _In_opt_ HINSTANCE hPrevInstance, + _In_ LPWSTR lpCmdLine, + _In_ int nCmdShow +) +{ + firstSetup(); + Logger::init("IntegrationWizard", true); + logger->info("Integration Wizard v{}", VERSION); + + // We need to disable WOW64 redirections because otherwise 32bit application + // uses SysWOW64 directory even if System32 was explicitly provided. + void* oldVal = NULL; + Wow64DisableWow64FsRedirection(&oldVal); + + auto action = askForAction(hInstance); + + switch(action) + { + case Action::UNEXPECTED_ERROR: + logger->error("Unexpected action result. Error code: 0x{:X}", GetLastError()); + exit(1); + break; + case Action::NO_ACTION: + logger->info("No action was taken. Terminating."); + exit(0); + break; + case Action::INSTALL_INTEGRATIONS: + IntegrationWizard::install(); + break; + case Action::REMOVE_INTEGRATIONS: + IntegrationWizard::remove(); + break; + default: + logger->error("Unexpected action result"); + exit(1); + } + + Wow64RevertWow64FsRedirection(oldVal); + return 0; +} + diff --git a/Koalageddon.sln b/Koalageddon.sln new file mode 100644 index 0000000..48e5069 --- /dev/null +++ b/Koalageddon.sln @@ -0,0 +1,81 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30804.86 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Injector", "Injector\Injector.vcxproj", "{5D2B330B-73E9-4B02-8203-BE7078DA646B}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Unlocker", "Unlocker\Unlocker.vcxproj", "{0AB35179-BF08-40C1-8961-0D6AEAF22770}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C813944E-EC8B-46E0-9251-D5856A1E2F06}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + build_installer.bat = build_installer.bat + IntegrationWizard\Config.jsonc = IntegrationWizard\Config.jsonc + inno_setup.iss = inno_setup.iss + LICENSE.txt = LICENSE.txt + README.md = README.md + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Common", "Common\Common.vcxproj", "{F364B2AB-F34A-4BCF-9362-22720020DDBF}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Integration", "Integration\Integration.vcxproj", "{6B6F6E00-A196-4815-8D24-D9890798FD09}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "IntegrationWizard", "IntegrationWizard\IntegrationWizard.vcxproj", "{4E88D0ED-22DF-4DB8-985B-22B778059F11}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5D2B330B-73E9-4B02-8203-BE7078DA646B}.Debug|x64.ActiveCfg = Debug|x64 + {5D2B330B-73E9-4B02-8203-BE7078DA646B}.Debug|x64.Build.0 = Debug|x64 + {5D2B330B-73E9-4B02-8203-BE7078DA646B}.Debug|x86.ActiveCfg = Debug|Win32 + {5D2B330B-73E9-4B02-8203-BE7078DA646B}.Debug|x86.Build.0 = Debug|Win32 + {5D2B330B-73E9-4B02-8203-BE7078DA646B}.Release|x64.ActiveCfg = Release|x64 + {5D2B330B-73E9-4B02-8203-BE7078DA646B}.Release|x64.Build.0 = Release|x64 + {5D2B330B-73E9-4B02-8203-BE7078DA646B}.Release|x86.ActiveCfg = Release|Win32 + {5D2B330B-73E9-4B02-8203-BE7078DA646B}.Release|x86.Build.0 = Release|Win32 + {0AB35179-BF08-40C1-8961-0D6AEAF22770}.Debug|x64.ActiveCfg = Debug|x64 + {0AB35179-BF08-40C1-8961-0D6AEAF22770}.Debug|x64.Build.0 = Debug|x64 + {0AB35179-BF08-40C1-8961-0D6AEAF22770}.Debug|x86.ActiveCfg = Debug|Win32 + {0AB35179-BF08-40C1-8961-0D6AEAF22770}.Debug|x86.Build.0 = Debug|Win32 + {0AB35179-BF08-40C1-8961-0D6AEAF22770}.Release|x64.ActiveCfg = Release|x64 + {0AB35179-BF08-40C1-8961-0D6AEAF22770}.Release|x64.Build.0 = Release|x64 + {0AB35179-BF08-40C1-8961-0D6AEAF22770}.Release|x86.ActiveCfg = Release|Win32 + {0AB35179-BF08-40C1-8961-0D6AEAF22770}.Release|x86.Build.0 = Release|Win32 + {F364B2AB-F34A-4BCF-9362-22720020DDBF}.Debug|x64.ActiveCfg = Debug|x64 + {F364B2AB-F34A-4BCF-9362-22720020DDBF}.Debug|x64.Build.0 = Debug|x64 + {F364B2AB-F34A-4BCF-9362-22720020DDBF}.Debug|x86.ActiveCfg = Debug|Win32 + {F364B2AB-F34A-4BCF-9362-22720020DDBF}.Debug|x86.Build.0 = Debug|Win32 + {F364B2AB-F34A-4BCF-9362-22720020DDBF}.Release|x64.ActiveCfg = Release|x64 + {F364B2AB-F34A-4BCF-9362-22720020DDBF}.Release|x64.Build.0 = Release|x64 + {F364B2AB-F34A-4BCF-9362-22720020DDBF}.Release|x86.ActiveCfg = Release|Win32 + {F364B2AB-F34A-4BCF-9362-22720020DDBF}.Release|x86.Build.0 = Release|Win32 + {6B6F6E00-A196-4815-8D24-D9890798FD09}.Debug|x64.ActiveCfg = Debug|x64 + {6B6F6E00-A196-4815-8D24-D9890798FD09}.Debug|x64.Build.0 = Debug|x64 + {6B6F6E00-A196-4815-8D24-D9890798FD09}.Debug|x86.ActiveCfg = Debug|Win32 + {6B6F6E00-A196-4815-8D24-D9890798FD09}.Debug|x86.Build.0 = Debug|Win32 + {6B6F6E00-A196-4815-8D24-D9890798FD09}.Release|x64.ActiveCfg = Release|x64 + {6B6F6E00-A196-4815-8D24-D9890798FD09}.Release|x64.Build.0 = Release|x64 + {6B6F6E00-A196-4815-8D24-D9890798FD09}.Release|x86.ActiveCfg = Release|Win32 + {6B6F6E00-A196-4815-8D24-D9890798FD09}.Release|x86.Build.0 = Release|Win32 + {4E88D0ED-22DF-4DB8-985B-22B778059F11}.Debug|x64.ActiveCfg = Debug|x64 + {4E88D0ED-22DF-4DB8-985B-22B778059F11}.Debug|x64.Build.0 = Debug|x64 + {4E88D0ED-22DF-4DB8-985B-22B778059F11}.Debug|x86.ActiveCfg = Debug|Win32 + {4E88D0ED-22DF-4DB8-985B-22B778059F11}.Debug|x86.Build.0 = Debug|Win32 + {4E88D0ED-22DF-4DB8-985B-22B778059F11}.Release|x64.ActiveCfg = Release|x64 + {4E88D0ED-22DF-4DB8-985B-22B778059F11}.Release|x64.Build.0 = Release|x64 + {4E88D0ED-22DF-4DB8-985B-22B778059F11}.Release|x86.ActiveCfg = Release|Win32 + {4E88D0ED-22DF-4DB8-985B-22B778059F11}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F89341C3-622D-4EE2-B1D8-25BCE64E1F13} + EndGlobalSection +EndGlobal diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..63a2560 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (C) 2021 by acidicoala + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fbf9e4d --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# 🐨 Koalageddon 💥 +#### Legit DLC Unlocker for Steam, Epic & Origin +Welcome to the DreamAPI repository. +For user-friendly introduction or support, please check out the official forum thread. This document is meant for software developers. + +## 🗜 Solution Projects +#### 🧰 Common +This project is a static library that houses common functions of all other projects. For example, all projects need to access config file and loggin utilites, so they are defined in this module. + +#### 💉 Injector +This project is a simple DLL injector executable. The injector can be used as a command line utility that accepts 2 arguments: ID of the process which should be injected and DLL to inject. + +#### 🔗 Integration +This project is a dynamic library that pretends to be `version.dll`. Nothing much going on here except for loading of the unlocker module. + +#### 🧙🏼‍ Integration Wizard +This project is a trivial GUI utility that automatically installs the integration files and copies the original ones. The GUI is using [Task Dialog] available in Windows API. + +#### 🔓 Unlocker +This project is a dynamic library which performs the main function of Koalageddon - DLC unlocking. It monitors DRM DLLs using undocumented WinAPI functions and suspends new processes before injection using undocumented functions as well. Once target DLLs have been identified, appropriate functions are hooked using the great PolyHook 2 library. A total of 4 hooking techniques are used in this project. + +## 🛠 Dependencies +The solution uses a number of third party dependencies, which are available via [vcpkg]. +Projects in the solution are configured to use static libraries instead of dynamic. If you wish to build the solution yourself, you would need to install following libraries: + +* [PolyHook 2.0]: + ``` + vcpkg install polyhook2:x86-windows-static + vcpkg install polyhook2:x64-windows-static + ``` +* [WinReg]: + ``` + vcpkg install winreg:x86-windows-static + vcpkg install winreg:x64-windows-static + ``` +* [spdlog]: + ``` + vcpkg install spdlog:x86-windows-static + vcpkg install spdlog:x64-windows-static + ``` +* [nlohmann JSON]: + ``` + vcpkg install nlohmann-json:x86-windows-static + vcpkg install nlohmann-json:x64-windows-static + ``` +* [TinyXML-2] + ``` + vcpkg install tinyxml2:x86-windows-static + vcpkg install tinyxml2:x64-windows-static + ``` +* [C++ Requests] + ``` + vcpkg install cpr:x86-windows-static + vcpkg install cpr:x64-windows-static + ``` + +You can verify installations via `vcpkg list` + +## 📄 License +This software is licensed under [Zero Clause BSD] license, terms of which are available in [LICENSE.txt] + +___ + +[Task Dialog]: https://docs.microsoft.com/en-us/windows/win32/controls/task-dialogs-overview#:~:text=A%20task%20dialog%20is%20a,features%20than%20a%20message%20box. +[vcpkg]: https://github.com/Microsoft/vcpkg#quick-start-windows +[spdlog]: https://github.com/gabime/spdlog +[nlohmann JSON]: https://github.com/nlohmann/json/ +[PolyHook 2.0]: https://github.com/stevemk14ebr/PolyHook_2_0 +[WinReg]: https://github.com/GiovanniDicanio/WinReg +[C++ Requests]: https://github.com/whoshuu/cpr +[TinyXML-2]: https://github.com/leethomason/tinyxml2 + +[Zero Clause BSD]: https://choosealicense.com/licenses/0bsd/ +[LICENSE.txt]: ./LICENSE.txt diff --git a/Unlocker/Unlocker.vcxproj b/Unlocker/Unlocker.vcxproj new file mode 100644 index 0000000..7bc958d --- /dev/null +++ b/Unlocker/Unlocker.vcxproj @@ -0,0 +1,260 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + {0ab35179-bf08-40c1-8961-0d6aeaf22770} + Hooker + 10.0 + Unlocker + + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + DynamicLibrary + true + v142 + Unicode + + + DynamicLibrary + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + false + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + true + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + false + $(SolutionDir)_Build\$(Configuration)\ + $(ProjectDir)_Build\$(Configuration)\$(Platform)\ + $(ProjectName)$(PlatformArchitecture) + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + true + $(PlatformTarget)-windows-static + + + + Level3 + true + WIN32;_DEBUG;_WINDOWS;_USRDLL;_SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + MultiThreadedDebug + $(SolutionDir)Common\src;$(ProjectDir)src + + + + + Windows + true + false + ntdll.lib;%(AdditionalDependencies) + %(AdditionalLibraryDirectories) + + + + + Level3 + true + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;_SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + MultiThreaded + $(SolutionDir)Common\src;$(ProjectDir)src + + + Windows + true + true + true + false + ntdll.lib;%(AdditionalDependencies) + %(AdditionalLibraryDirectories) + + + + + Level3 + true + _DEBUG;HOOKER_EXPORTS;_WINDOWS;_USRDLL;_SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + MultiThreadedDebug + $(SolutionDir)Common\src;$(ProjectDir)src + + + + + Windows + true + false + ntdll.lib;%(AdditionalDependencies) + %(AdditionalLibraryDirectories) + + + + + Level3 + true + true + true + NDEBUG;_WINDOWS;_USRDLL;_SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpp17 + MultiThreaded + $(SolutionDir)Common\src;$(ProjectDir)src + + + Windows + true + true + true + false + ntdll.lib;%(AdditionalDependencies) + %(AdditionalLibraryDirectories) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + + + {f364b2ab-f34a-4bcf-9362-22720020ddbf} + + + + + + \ No newline at end of file diff --git a/Unlocker/Unlocker.vcxproj.filters b/Unlocker/Unlocker.vcxproj.filters new file mode 100644 index 0000000..8aef070 --- /dev/null +++ b/Unlocker/Unlocker.vcxproj.filters @@ -0,0 +1,158 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {2f8b53cc-53f7-4c05-b716-9a9dce829c39} + + + {98be62ef-dbe5-4a06-8a7d-ac1b58d5b605} + + + {5965e26f-a8eb-4666-9ceb-f8518526d6b6} + + + {9377a090-30bd-4d9d-9536-e01ec9e80c6b} + + + {57aa9d64-b2f5-43dd-a97e-06c77c18d5cf} + + + {a416250b-0b77-4aa1-a50e-d7e34fa6b974} + + + {a09035ba-a888-4765-86c3-5d1df17bdf5a} + + + {3fa38e81-3df5-4991-ab87-30a327edacfe} + + + {e6dd8cef-5536-4417-88af-da4931ab890d} + + + {dead7396-6b35-46bc-9107-d06731daa0cc} + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files\platforms\epic + + + Header Files\platforms\epic + + + Header Files\platforms\epic + + + Header Files\platforms\epic + + + Header Files\platforms\epic + + + Header Files\platforms\epic + + + Header Files\platforms\epic + + + Header Files\platforms\steam + + + Header Files\platforms\steam + + + Header Files\platforms\steam + + + Header Files + + + Header Files\platforms\origin + + + Header Files\platforms\origin + + + Header Files\platforms\ubisoft + + + Header Files\platforms\ubisoft + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files\platforms\epic + + + Source Files\platforms\epic + + + Source Files + + + Source Files\platforms\steam + + + Source Files\platforms\steam + + + Source Files\platforms\origin + + + Source Files\platforms\origin + + + Source Files\platforms\ubisoft + + + Source Files\platforms\ubisoft + + + \ No newline at end of file diff --git a/Unlocker/src/DLLMonitor.cpp b/Unlocker/src/DLLMonitor.cpp new file mode 100644 index 0000000..4c2ed2d --- /dev/null +++ b/Unlocker/src/DLLMonitor.cpp @@ -0,0 +1,152 @@ +#include "pch.h" +#include "DLLMonitor.h" +#include "ntapi.h" +#include "constants.h" +#include "platforms/epic/Epic.h" +#include "platforms/steam/Steam.h" +#include +#include + + +_LdrRegisterDllNotification LdrRegisterDllNotification = NULL; +_LdrUnregisterDllNotification LdrUnregisterDllNotification = NULL; + +vector> platforms; + +bool getNtFunctions() +{ + HMODULE hNtDll = GetModuleHandle(L"ntdll.dll"); + if(hNtDll == NULL) + { + logger->error("Failed to get a handle for ntdll.dll module. Error code: {}", GetLastError()); + return false; + } + LdrRegisterDllNotification = (_LdrRegisterDllNotification) GetProcAddress(hNtDll, "LdrRegisterDllNotification"); + LdrUnregisterDllNotification = (_LdrUnregisterDllNotification) GetProcAddress(hNtDll, "LdrUnregisterDllNotification"); + + if(!LdrRegisterDllNotification || !LdrUnregisterDllNotification) + { + logger->error("Some ntdll procedures were not found. Error code: {}", GetLastError()); + return false; + } + + return true; +} + + +void CALLBACK dllCallback(ULONG NotificationReason, PLDR_DLL_NOTIFICATION_DATA NotificationData, PVOID Context) +{ + if(NotificationReason == LDR_DLL_NOTIFICATION_REASON_LOADED) + { + auto dllName = wstring(NotificationData->Loaded.BaseDllName->Buffer); + + // This log line sometimes crashes some games on Steam >:( + // logger->debug(L"DLL has been loaded: {}", dllName); + + if(dllName == EOSSDK) + { + logger->info(L"Epic Games DLL has been detected"); + platforms.push_back(make_unique(NotificationData->Loaded.FullDllName->Buffer)); + platforms.back()->init(); + } + else if(dllName == STEAMAPI) + { + logger->info(L"Steam DLL has been detected"); + platforms.push_back(make_unique(NotificationData->Loaded.FullDllName->Buffer)); + platforms.back()->init(); + } + else if(dllName == ORIGINCLIENT) + { + logger->info(L"Origin DLL has been detected"); + platforms.push_back(make_unique(NotificationData->Loaded.FullDllName->Buffer)); + platforms.back()->init(); + } + else if(dllName == UPLAY_R2) + { + logger->info(L"Ubisoft DLL has been detected"); + platforms.push_back(make_unique(NotificationData->Loaded.FullDllName->Buffer)); + platforms.back()->init(); + } + } +} + + +void checkLoadedDlls() +{ + logger->debug("Checking already loaded DLLs"); + + HMODULE handle; + + if(handle = GetModuleHandle(EOSSDK)) + { + logger->info("Epic DLL is already loaded"); + platforms.push_back(make_unique(handle)); + platforms.back()->init(); + } + else if(handle = GetModuleHandle(STEAMAPI)) + { + logger->info("Steam DLL is already loaded"); + platforms.push_back(make_unique(handle)); + platforms.back()->init(); + } + else if(handle = GetModuleHandle(ORIGINCLIENT)) + { + logger->info(L"Origin DLL is already loaded"); + platforms.push_back(make_unique(handle)); + platforms.back()->init(); + } + else if(handle = GetModuleHandle(UPLAY_R2)) + { + logger->info(L"Ubisoft DLL is already loaded"); + platforms.push_back(make_unique(handle)); + platforms.back()->init(); + } + else + { + return; + } + + CloseHandle(handle); +} + +PVOID cookie = NULL; + +void DLLMonitor::init() +{ + logger->debug("Initializing DLL monitor"); + + if(!getNtFunctions()) + { + return; + } + + auto status = LdrRegisterDllNotification(0, &dllCallback, NULL, &cookie); + if(status != STATUS_SUCCESS) + { + logger->error("Failed to register DLL notifications. Status code: {}", (unsigned long) status); + } + else + { + logger->debug("Registered DLL listener"); + } + + checkLoadedDlls(); + + logger->debug("DLL monitor was successfully initialized"); +} + +void DLLMonitor::shutdown() +{ + logger->debug("Shutting down DLL monitor"); + + for(auto& platform : platforms) + { + platform->shutdown(); + } + + platforms.clear(); + + LdrUnregisterDllNotification(cookie); + + logger->debug("DLL monitor was successfully shut down"); +} \ No newline at end of file diff --git a/Unlocker/src/DLLMonitor.h b/Unlocker/src/DLLMonitor.h new file mode 100644 index 0000000..12472bc --- /dev/null +++ b/Unlocker/src/DLLMonitor.h @@ -0,0 +1,9 @@ +#pragma once + +namespace DLLMonitor +{ + +void init(); +void shutdown(); + +} diff --git a/Unlocker/src/ProcessHooker.cpp b/Unlocker/src/ProcessHooker.cpp new file mode 100644 index 0000000..ca3edd4 --- /dev/null +++ b/Unlocker/src/ProcessHooker.cpp @@ -0,0 +1,262 @@ +#include "pch.h" +#include "ProcessHooker.h" +#include "util.h" +#include "Logger.h" +#include "Config.h" +#include "constants.h" +#include "hook_util.h" + +typedef LONG NTSTATUS; + +// Undocumented functions + +#pragma comment(lib,"ntdll.lib") +EXTERN_C NTSTATUS NTAPI NtSuspendProcess(HANDLE ProcessHandle); + +#pragma comment(lib,"ntdll.lib") +EXTERN_C NTSTATUS NTAPI NtResumeProcess(HANDLE ProcessHandle); + + +uint64_t createProcTrampoline = NULL; + +void inject(std::wstring wPID, uint64_t funcAddr) +{ + DWORD PID = std::stoi(wPID); + + // Determine the target executable's architecture to launch correponding injector + bool is32 = is32bit(PID); + + auto workingDir = getWorkingDirPath(); + auto injectorPath = workingDir / (is32 ? INJECTOR_32 : INJECTOR_64); + auto unlockerPath = workingDir / (is32 ? UNLOCKER_32 : UNLOCKER_64); + + // Validate paths + if(!std::filesystem::exists(injectorPath)) + { + logger->error(L"Injector exe was not found at: {}", injectorPath.c_str()); + return; + } + if(!std::filesystem::exists(unlockerPath)) + { + logger->error(L"Unlocker dll was not found at: {}", unlockerPath.c_str()); + return; + } + + auto args = fmt::format(L"\"{}\" {} \"{}\"", injectorPath.c_str(), wPID, unlockerPath.c_str()); + logger->debug(L"Starting Injector with cmdline: {}", args); + + // Need to make a non const copy since CreateProcessW might modify the argument + auto size = args.size() + 1; + auto cArgs = new WCHAR[size]; + wcscpy_s(cArgs, size, args.c_str()); + cArgs[size - 1] = '\0'; + + // additional information + STARTUPINFO si = {}; + PROCESS_INFORMATION pi = {}; + + // We make sure to call the original function, to avoid recursively calling ourselves + if(funcAddr == 0) + funcAddr = (uint64_t) &CreateProcessW; + + auto Original_CreateProcessW = PLH::FnCast(funcAddr, &CreateProcessW); + + auto success = Original_CreateProcessW( + injectorPath.c_str(), // the path + cArgs, // Command line + NULL, // Process handle not inheritable + NULL, // Thread handle not inheritable + FALSE, // Set handle inheritance to FALSE + 0, // No creation flags + NULL, // Use parent's environment block + NULL, // Use parent's starting directory + &si, // Pointer to STARTUPINFO structure + &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) + ); + + if(!success) + { + logger->error("Failed to start Injector process. Error code: 0x{:X}", GetLastError()); + return; + } + + delete[] cArgs; + + auto hProcess = pi.hProcess; + if(hProcess == NULL) + { + logger->error("Process handle is NULL. Error code: 0x{:X}", GetLastError()); + return; + } + + // Wait until injector process exits + WaitForSingleObject(hProcess, INFINITE); + + DWORD exit_code; + GetExitCodeProcess(hProcess, &exit_code); + + if(exit_code == 0) + { + logger->info(L"Successfully injected DLL into: {}", getProcessName(PID)); + } + else + { + logger->error("Failed to inject DLL. Exit code: 0x{:X}", exit_code); + } + + // Close process and thread handles + CloseHandle(hProcess); + + if(pi.hThread != NULL) + CloseHandle(pi.hThread); +} + + +void injectIfNecessary(wstring cmdLine, LPPROCESS_INFORMATION lpProcessInformation) +{ + std::wsmatch match; + std::wregex pattern(LR"(\w+\.exe)"); + + if(!regex_search(cmdLine.cbegin(), cmdLine.cend(), match, pattern)) + { + logger->debug("Failed to find exe name in the command line"); + return; + } + + auto newProcName = wtos(match.str()); + + // Iterate over platforms + for(const auto& [key, platform] : config->platforms) + { + if(stringsAreEqual(getProcessPath().filename().string(), platform.process)) + { + // This is a platform process + if(!platform.replicate) + { + // Do not inject since platform is configured with disabled replication + logger->debug("Skipping injection since platform is set to disable replication"); + return; + } + for(const auto& ignoredProcess : platform.ignore) + { + if(stringsAreEqual(newProcName, ignoredProcess)) + { + // Do not inject since the process is ignored for this platforms + logger->debug("Skipping injection since the new process is ignored for this platform"); + return; + } + } + } + } + + // Iterate over ignored processes + for(const auto& ignoredProcess : config->ignore) + { + if(ignoredProcess == newProcName) + { + // Don't inject the DLL, just let it run as usual + logger->debug("Skipping injection for the globally ignored process: {}", ignoredProcess); + return; + } + } + + // Iterate over terminate processes + for(const auto& terminatedProcess : config->terminate) + { + if(terminatedProcess == newProcName) + { + // Kill the process if it is in the terminate list + logger->warn("Terminating the process: {}", terminatedProcess); + killProcess(lpProcessInformation->hProcess); + return; + } + } + + // At this point we are sure that we want to inject the DLL + + inject(std::to_wstring(lpProcessInformation->dwProcessId), createProcTrampoline); +} + +/** + * We hook CreateProcessW to catch any new process that the current process creates, + * so that we could inject unlocker there too. This is effectively a recursive + * DLL injection which makes sure that we always reach the game executable, + * even if it was started by a chain of launchers. + */ +BOOL WINAPI Hooked_CreateProcessW( + LPCWSTR lpApplicationName, + LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, + DWORD dwCreationFlags, + LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation +) +{ + std::wstring appName(L""); + std::wstring cmdLine(L""); + + if(lpApplicationName != NULL) + appName = lpApplicationName; + + if(lpCommandLine != NULL) + cmdLine = lpCommandLine; + + // Get the original function + static auto Original_CreateProcessW = PLH::FnCast(createProcTrampoline, &CreateProcessW); + + // Call the original function + auto result = Original_CreateProcessW( + lpApplicationName, + lpCommandLine, + lpProcessAttributes, + lpThreadAttributes, + bInheritHandles, + dwCreationFlags, + lpEnvironment, + lpCurrentDirectory, + lpStartupInfo, + lpProcessInformation + ); + + logger->debug(L"CreateProcessW -> PID: {}, command line: {}", lpProcessInformation->dwProcessId, cmdLine); + + if(result == FALSE) + return result; // Some app made a messed up WINAPI call. Nothing to do for us here. + + NtSuspendProcess(lpProcessInformation->hProcess); + + injectIfNecessary(cmdLine, lpProcessInformation); + + NtResumeProcess(lpProcessInformation->hProcess); + + return result; +} +Detour detour((char*) &CreateProcessW, (char*) &Hooked_CreateProcessW, &createProcTrampoline, disassembler); + +void ProcessHooker::init() +{ + logger->debug("Setting up process hooks"); + + if(detour.hook()) + { + logger->debug("Process hooks were successfully set up"); + } + else + { + logger->error("Failed to hook CreateProcessW"); + } + +} + +void ProcessHooker::shutdown() +{ + logger->debug("Removing process hooks"); + + detour.unHook(); + + logger->debug("Process hooks were successfully removed"); +} diff --git a/Unlocker/src/ProcessHooker.h b/Unlocker/src/ProcessHooker.h new file mode 100644 index 0000000..db09739 --- /dev/null +++ b/Unlocker/src/ProcessHooker.h @@ -0,0 +1,9 @@ +#pragma once + +namespace ProcessHooker +{ + +void init(); +void shutdown(); + +} diff --git a/Unlocker/src/Unlocker.cpp b/Unlocker/src/Unlocker.cpp new file mode 100644 index 0000000..0b4ba71 --- /dev/null +++ b/Unlocker/src/Unlocker.cpp @@ -0,0 +1,51 @@ +#include "pch.h" +#include "Unlocker.h" +#include "constants.h" +#include "constants.h" +#include "DLLMonitor.h" +#include "ProcessHooker.h" +#include "Config.h" + +static bool initialized = false; + +void Unlocker::init(HMODULE hModule) +{ + // Lock the thread to prevent init deadlock + static std::mutex mutex; + const std::lock_guard lock(mutex); + + if(initialized) + return; + + DisableThreadLibraryCalls(hModule); + + Config::init(); + + Logger::init(UNLOCKER_NAME, true); + + logger->info("Unlocker v{}", VERSION); + logger->info(L"Hooking into \"{}\"", getCurrentModuleName()); + + DLLMonitor::init(); + ProcessHooker::init(); + + initialized = true; + logger->debug("Unlocker initialization complete"); +} + +void Unlocker::shutdown() +{ + static std::mutex mutex; + const std::lock_guard lock(mutex); + + if(!initialized) + return; + + logger->debug("Unlocker shutting down"); + + DLLMonitor::shutdown(); + ProcessHooker::shutdown(); + + initialized = false; + logger->debug("Unlocker was successfully shut down"); +} diff --git a/Unlocker/src/Unlocker.h b/Unlocker/src/Unlocker.h new file mode 100644 index 0000000..18866bd --- /dev/null +++ b/Unlocker/src/Unlocker.h @@ -0,0 +1,10 @@ +#pragma once +#include "framework.h" + +namespace Unlocker +{ + +void init(HMODULE hModule); +void shutdown(); + +} diff --git a/Unlocker/src/dllmain.cpp b/Unlocker/src/dllmain.cpp new file mode 100644 index 0000000..15c1f3a --- /dev/null +++ b/Unlocker/src/dllmain.cpp @@ -0,0 +1,17 @@ +#include "pch.h" +#include "Unlocker.h" + +BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +{ + switch(ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + Unlocker::init(hModule); + break; + case DLL_PROCESS_DETACH: + Unlocker::shutdown(); + break; + } + + return TRUE; +} diff --git a/Unlocker/src/framework.h b/Unlocker/src/framework.h new file mode 100644 index 0000000..b8ad5bd --- /dev/null +++ b/Unlocker/src/framework.h @@ -0,0 +1,19 @@ +#pragma once +#include + +#include +#include + +// Disable 3rd party library warnings +#pragma warning(push) +#pragma warning(disable: ALL_CODE_ANALYSIS_WARNINGS) + +#include +#include +#include "polyhook2/PE/EatHook.hpp" +#include "polyhook2/PE/IatHook.hpp" +#include +#include +#include + +#pragma warning(pop) diff --git a/Unlocker/src/hook_util.cpp b/Unlocker/src/hook_util.cpp new file mode 100644 index 0000000..1af0d33 --- /dev/null +++ b/Unlocker/src/hook_util.cpp @@ -0,0 +1,8 @@ +#include "pch.h" +#include "hook_util.h" + +#ifdef _WIN64 +PLH::CapstoneDisassembler disassembler(PLH::Mode::x64); +#else +PLH::CapstoneDisassembler disassembler(PLH::Mode::x86); +#endif diff --git a/Unlocker/src/hook_util.h b/Unlocker/src/hook_util.h new file mode 100644 index 0000000..6f6afc9 --- /dev/null +++ b/Unlocker/src/hook_util.h @@ -0,0 +1,42 @@ +#pragma once +#include "framework.h" + +#ifdef _WIN64 +typedef PLH::x64Detour Detour; +#else +typedef PLH::x86Detour Detour; +#endif + +extern PLH::CapstoneDisassembler disassembler; + +/** + * By default, virtual functions are declared with __thiscall + * convention, which is normal since they are class members. + * But it presents an issue for us, since we cannot pass *this + * pointer as a function argument. This is because *this + * pointer is passed via register ECX in __thiscall + * convention. Hence, to resolve this issue we declare our + * hooked functions with __fastcall convention, to trick + * the compiler into reading ECX & EDX registers as 1st + * and 2nd function arguments respectively. Similarly, __fastcall + * makes the compiler push the first argument into the ECX register, + * which mimics the __thiscall calling convention. Register EDX + * is not used anywhere in this case, but we still pass it along + * to conform to the __fastcall convention. This all applies + * to the x86 architecture. + * + * In x86-64 however, there is only one calling convention, + * so __fastcall is simply ignored. However, RDX in this case + * will store the 1st actual argument to the function, so we + * have to omit it from the function signature. + * + * The macros below implement the above-mentioned considerations. + */ + +#ifdef _WIN64 +#define PARAMS(...) void* RCX, ##__VA_ARGS__ +#define ARGS(...) RCX, ##__VA_ARGS__ +#else +#define PARAMS(...) void* ECX, void* EDX, ##__VA_ARGS__ +#define ARGS(...) ECX, EDX, ##__VA_ARGS__ +#endif diff --git a/Unlocker/src/ntapi.h b/Unlocker/src/ntapi.h new file mode 100644 index 0000000..18a80ea --- /dev/null +++ b/Unlocker/src/ntapi.h @@ -0,0 +1,76 @@ +#pragma once + +// Source: https://github.com/blaquee/dllnotif/blob/master/LdrDllNotification/ntapi.h + +#include + +typedef __success(return >= 0) LONG NTSTATUS; + +#ifndef NT_STATUS_OK +#define NT_STATUS_OK 0 +#endif + + +#define STATUS_SUCCESS ((NTSTATUS)0) +#define STATUS_UNSUCCESSFUL ((NTSTATUS)0xC0000001) +#define STATUS_PROCEDURE_NOT_FOUND ((NTSTATUS)0xC000007A) +#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004) +#define STATUS_NOT_FOUND ((NTSTATUS)0xC0000225) +#define STATUS_THREAD_IS_TERMINATING ((NTSTATUS)0xc000004b) +#define STATUS_NOT_SUPPORTED ((NTSTATUS)0xC00000BB) + +enum LDR_DLL_NOTIFICATION_REASON +{ + LDR_DLL_NOTIFICATION_REASON_LOADED = 1, + LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2, +}; + +typedef struct tag_UNICODE_STRING +{ + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +} __UNICODE_STRING, * PCUNICODE_STRING; // Removed * PUNICODE_STRING to avoid conflicts with PLH's PEB.hpp + +typedef struct _LDR_DLL_LOADED_NOTIFICATION_DATA +{ + ULONG Flags; //Reserved. + PCUNICODE_STRING FullDllName; //The full path name of the DLL module. + PCUNICODE_STRING BaseDllName; //The base file name of the DLL module. + PVOID DllBase; //A pointer to the base address for the DLL in memory. + ULONG SizeOfImage; //The size of the DLL image, in bytes. +} LDR_DLL_LOADED_NOTIFICATION_DATA, * PLDR_DLL_LOADED_NOTIFICATION_DATA; + +typedef struct _LDR_DLL_UNLOADED_NOTIFICATION_DATA +{ + ULONG Flags; //Reserved. + PCUNICODE_STRING FullDllName; //The full path name of the DLL module. + PCUNICODE_STRING BaseDllName; //The base file name of the DLL module. + PVOID DllBase; //A pointer to the base address for the DLL in memory. + ULONG SizeOfImage; //The size of the DLL image, in bytes. +} LDR_DLL_UNLOADED_NOTIFICATION_DATA, * PLDR_DLL_UNLOADED_NOTIFICATION_DATA; + +typedef union _LDR_DLL_NOTIFICATION_DATA +{ + LDR_DLL_LOADED_NOTIFICATION_DATA Loaded; + LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded; +} LDR_DLL_NOTIFICATION_DATA, * PLDR_DLL_NOTIFICATION_DATA; +typedef const LDR_DLL_NOTIFICATION_DATA* PCLDR_DLL_NOTIFICATION_DATA; + + +typedef VOID(CALLBACK* PLDR_DLL_NOTIFICATION_FUNCTION)( + _In_ ULONG NotificationReason, + _In_ PLDR_DLL_NOTIFICATION_DATA NotificationData, + _In_opt_ PVOID Context +); + +typedef NTSTATUS(NTAPI* _LdrRegisterDllNotification)( + _In_ ULONG Flags, + _In_ PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, + _In_opt_ PVOID Context, + _Out_ PVOID* cookie +); + +typedef NTSTATUS(NTAPI* _LdrUnregisterDllNotification)( + _In_ PVOID cookie +); diff --git a/Unlocker/src/pch.cpp b/Unlocker/src/pch.cpp new file mode 100644 index 0000000..64b7eef --- /dev/null +++ b/Unlocker/src/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/Unlocker/src/pch.h b/Unlocker/src/pch.h new file mode 100644 index 0000000..c9c7883 --- /dev/null +++ b/Unlocker/src/pch.h @@ -0,0 +1,2 @@ +#pragma once +#include "framework.h" diff --git a/Unlocker/src/platforms/BasePlatform.cpp b/Unlocker/src/platforms/BasePlatform.cpp new file mode 100644 index 0000000..5219d90 --- /dev/null +++ b/Unlocker/src/platforms/BasePlatform.cpp @@ -0,0 +1,86 @@ +#include "pch.h" +#include "BasePlatform.h" + +BasePlatform::BasePlatform(const HMODULE handle) +{ + this->handle = handle; + + auto buffer = new WCHAR[MAX_PATH]; + GetModuleBaseName(GetCurrentProcess(), handle, buffer, MAX_PATH); + this->moduleName = wtos(wstring(buffer)); + delete[] buffer; +} + +BasePlatform::BasePlatform(const wstring& fullDllName) +{ + moduleName = wtos(fullDllName); + + auto handle = GetModuleHandle(fullDllName.c_str()); + if(handle == NULL) + { + logger->error(L"Failed to get a handle to the module: {}", fullDllName); + return; + } + + this->handle = handle; +} + +void BasePlatform::installDetourHook(Hooks& hooks, void* hookedFunc, const char* funcName) +{ + static std::mutex mutex; + const std::lock_guard lock(mutex); + + if(auto original_func_address = GetProcAddress(handle, funcName)) + { + hooks.push_back(make_unique + ((char*) original_func_address, (char*) hookedFunc, &trampolineMap[funcName], disassembler) + ); + + if(hooks.back()->hook()) + { + logger->debug("Hooked \"{}\" via Detour.", funcName); + } + else + { + hooks.pop_back(); + logger->warn("Failed to hook \"{}\" via Detour. Trying with IAT.", funcName); + installIatHook(hooks, hookedFunc, funcName); + } + } +} + +void BasePlatform::installIatHook(Hooks& hooks, void* hookedFunc, const char* funcName) +{ + hooks.push_back(make_unique + (moduleName, funcName, (char*) hookedFunc, &trampolineMap[funcName], L"") + ); + + if(hooks.back()->hook()) + { + logger->debug("Hooked \"{}\" via IAT", funcName); + } + else + { + hooks.pop_back(); + logger->warn("Failed to hook \"{}\" via IAT. Trying with EAT.", funcName); + installEatHook(hooks, hookedFunc, funcName); + } +} + +void BasePlatform::installEatHook(Hooks& hooks, void* hookedFunc, const char* funcName) +{ + hooks.push_back(make_unique + (funcName, stow(moduleName), (char*) hookedFunc, &trampolineMap[funcName]) + ); + + if(hooks.back()->hook()) + { + logger->debug("Hooked \"{}\" via EAT", funcName); + } + else + { + hooks.pop_back(); + logger->error("Failed to hook \"{}\" via EAT.", funcName); + } +} + diff --git a/Unlocker/src/platforms/BasePlatform.h b/Unlocker/src/platforms/BasePlatform.h new file mode 100644 index 0000000..7fbfd43 --- /dev/null +++ b/Unlocker/src/platforms/BasePlatform.h @@ -0,0 +1,34 @@ +#pragma once +#include "util.h" +#include "hook_util.h" + +typedef vector> Hooks; + +class BasePlatform +{ +protected: + string moduleName; + HMODULE handle = NULL; + bool initialized = false; + + // To learn the basics of each type of hook, you can consult this article by the + // author of the hooking library that is used by this projet: PolyHook 2. + // The article was written for the v1 of the library, but the principles are the same in v2. + // https://www.codeproject.com/articles/1100579/polyhook-the-cplusplus-x-x-hooking-library + void installDetourHook(Hooks& hooks, void* hookedFunc, const char* funcName); + void installIatHook(Hooks& hooks, void* hookedFunc, const char* funcName); + void installEatHook(Hooks& hooks, void* hookedFunc, const char* funcName); + +public: + // we can safely store original functions from all platforms + // in the corresponding single static container. + inline static map trampolineMap; + inline static map origVFuncMap; + + BasePlatform() = delete; + BasePlatform(const HMODULE handle); + BasePlatform(const wstring& fullDllName); + + virtual void init() = 0; + virtual void shutdown() = 0; +}; diff --git a/Unlocker/src/platforms/epic/Epic.cpp b/Unlocker/src/platforms/epic/Epic.cpp new file mode 100644 index 0000000..58a198a --- /dev/null +++ b/Unlocker/src/platforms/epic/Epic.cpp @@ -0,0 +1,41 @@ +#include "pch.h" +#include "Epic.h" +#include "util.h" +#include "constants.h" +#include "eos_hooks.h" + +// Macro to avoid repetition +# define HOOK(FUNC) installDetourHook(hooks, FUNC, STR_##FUNC) + +void Epic::init() +{ + if(initialized || handle == NULL) + return; + + logger->debug("Initializing Epic platform"); + + HOOK(EOS_Ecom_QueryOwnership); + HOOK(EOS_Ecom_QueryEntitlements); + HOOK(EOS_Ecom_GetEntitlementsCount); + HOOK(EOS_Ecom_CopyEntitlementByIndex); + HOOK(EOS_Ecom_Entitlement_Release); + + logger->info("Epic platform was initialized"); + initialized = true; +} + +void Epic::shutdown() +{ + if(!initialized) + return; + + logger->debug("Shutting down Epic platform"); + + for(auto& hook : hooks) + { + hook->unHook(); + } + hooks.clear(); + + logger->debug("Epic platform was shut down"); +} diff --git a/Unlocker/src/platforms/epic/Epic.h b/Unlocker/src/platforms/epic/Epic.h new file mode 100644 index 0000000..26077bd --- /dev/null +++ b/Unlocker/src/platforms/epic/Epic.h @@ -0,0 +1,29 @@ +#pragma once + +#include "platforms/BasePlatform.h" +#include "util.h" + +#ifdef _WIN64 +constexpr auto STR_EOS_Ecom_QueryOwnership = "EOS_Ecom_QueryOwnership"; +constexpr auto STR_EOS_Ecom_QueryEntitlements = "EOS_Ecom_QueryEntitlements"; +constexpr auto STR_EOS_Ecom_GetEntitlementsCount = "EOS_Ecom_GetEntitlementsCount"; +constexpr auto STR_EOS_Ecom_CopyEntitlementByIndex = "EOS_Ecom_CopyEntitlementByIndex"; +constexpr auto STR_EOS_Ecom_Entitlement_Release = "EOS_Ecom_Entitlement_Release"; +#else +constexpr auto STR_EOS_Ecom_QueryOwnership = "_EOS_Ecom_QueryOwnership@16"; +constexpr auto STR_EOS_Ecom_QueryEntitlements = "_EOS_Ecom_QueryEntitlements@16"; +constexpr auto STR_EOS_Ecom_GetEntitlementsCount = "_EOS_Ecom_GetEntitlementsCount@8"; +constexpr auto STR_EOS_Ecom_CopyEntitlementByIndex = "_EOS_Ecom_CopyEntitlementByIndex@12"; +constexpr auto STR_EOS_Ecom_Entitlement_Release = "_EOS_Ecom_Entitlement_Release@4"; +#endif + +class Epic : public BasePlatform +{ +public: + using BasePlatform::BasePlatform; + + inline static Hooks hooks; + + void init() override; + void shutdown() override; +}; diff --git a/Unlocker/src/platforms/epic/eos_base.h b/Unlocker/src/platforms/epic/eos_base.h new file mode 100644 index 0000000..20e9453 --- /dev/null +++ b/Unlocker/src/platforms/epic/eos_base.h @@ -0,0 +1,148 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + + +#if !defined(EOS_MEMORY_CALL) || !defined(EOS_CALL) || !defined(EOS_USE_DLLEXPORT) +#if !defined(_WIN32) && !defined(_WIN64) && !defined(__ANDROID__) && !defined(__linux__) && !defined(__APPLE__) +#error \ +This platform expected a `eos__base.h` include before this header. \ +Please refer to https://dev.epicgames.com/docs/services or `eos_platform_prereqs.h` for details. +#endif +#endif + +#ifndef EOS_USE_DLLEXPORT +#if defined(_WIN32) || defined(__CYGWIN__) +#define EOS_USE_DLLEXPORT 1 +#else +#define EOS_USE_DLLEXPORT 0 +#endif +#endif + +#ifndef EOS_CALL +#if defined(_WIN32) && (defined(__i386) || defined(_M_IX86)) +#define EOS_CALL __stdcall +#define EOS_MEMORY_CALL __stdcall +#else +#define EOS_CALL +#define EOS_MEMORY_CALL +#endif +#endif + +#if !defined(EOS_MEMORY_CALL) || !defined(EOS_CALL) || !defined(EOS_USE_DLLEXPORT) +#error \ +The expected macros EOS_MEMORY_CALL, EOS_CALL, and EOS_USE_DLLEXPORT where not all defined. \ +Please refer to https://dev.epicgames.com/docs/services or `eos_platform_prereqs.h` for details. +#endif + + +#if defined(__cplusplus) + #if defined(_MSC_VER) && _MSC_VER >= 1800 + /* Visual Studio 2013 or later */ + #include + #else + #include + #include + #endif + + #if __cplusplus >= 201103L + #define EOS_HAS_ENUM_CLASS + #elif defined(_MSC_VER) && defined(_MSVC_LANG) && _MSVC_LANG >= 201103L + #define EOS_HAS_ENUM_CLASS + #endif +#else + /* C Compiler */ + #include + #include +#endif + + +typedef int32_t EOS_Bool; +#define EOS_TRUE 1 +#define EOS_FALSE 0 + + +#if defined(EOS_BUILDING_SDK) && EOS_BUILDING_SDK > 0 + #if EOS_USE_DLLEXPORT + #ifdef __GNUC__ + #define EOS_API __attribute__ ((dllexport)) + #else + #define EOS_API __declspec(dllexport) + #endif + #else + #if __GNUC__ >= 4 + #define EOS_API __attribute__ ((visibility ("default"))) + #else + #define EOS_API + #endif + #endif + +#else + + #if EOS_USE_DLLEXPORT + #if defined(EOS_MONOLITHIC) && EOS_MONOLITHIC > 0 + #define EOS_API + #elif defined(EOS_BUILD_DLL) && EOS_BUILD_DLL > 0 + #ifdef __GNUC__ + #define EOS_API __attribute__ ((dllexport)) + #else + #define EOS_API __declspec(dllexport) + #endif + #else + #ifdef __GNUC__ + #define EOS_API __attribute__ ((dllimport)) + #else + #define EOS_API __declspec(dllimport) + #endif + #endif + #else + #if __GNUC__ >= 4 + #define EOS_API __attribute__ ((visibility ("default"))) + #else + #define EOS_API + #endif + #endif +#endif + +#ifdef __cplusplus +#define EXTERN_C extern "C" +#else +#define EXTERN_C +#endif + +#define EOS_DECLARE_FUNC(return_type) EXTERN_C EOS_API return_type EOS_CALL +#define EOS_DECLARE_CALLBACK(CallbackName, ...) EXTERN_C typedef void (EOS_CALL * CallbackName)(__VA_ARGS__) +#define EOS_DECLARE_CALLBACK_RETVALUE(ReturnType, CallbackName, ...) EXTERN_C typedef ReturnType (EOS_CALL * CallbackName)(__VA_ARGS__) +#define EOS_PASTE(...) __VA_ARGS__ +#define EOS_STRUCT(struct_name, struct_def) \ + EXTERN_C typedef struct _tag ## struct_name { \ + EOS_PASTE struct_def \ + } struct_name + + +#ifdef EOS_HAS_ENUM_CLASS +#define EOS_ENUM_START(name) enum class name : int32_t { +#define EOS_ENUM_END(name) } +#else +#define EOS_ENUM_START(name) typedef enum name { +#define EOS_ENUM_END(name) , __##name##_PAD_INT32__ = 0x7FFFFFFF } name +#endif +#define EOS_ENUM(name, ...) EOS_ENUM_START(name) __VA_ARGS__ EOS_ENUM_END(name) + + +#ifdef EOS_HAS_ENUM_CLASS +#define EOS_ENUM_BOOLEAN_OPERATORS(name) \ +/** A set of bitwise operators provided when the enum is provided as an `enum class`. */ \ +inline constexpr name operator|(name Left, name Right) { return static_cast((__underlying_type(name))Left | (__underlying_type(name))Right); } \ +inline constexpr name operator&(name Left, name Right) { return static_cast((__underlying_type(name))Left & (__underlying_type(name))Right); } \ +inline constexpr name operator^(name Left, name Right) { return static_cast((__underlying_type(name))Left ^ (__underlying_type(name))Right); } \ +inline constexpr name& operator|=(name& Left, name Right) { return Left = Left | Right; } \ +inline constexpr name& operator&=(name& Left, name Right) { return Left = Left & Right; } \ +inline constexpr name& operator^=(name& Left, name Right) { return Left = Left ^ Right; } \ +/**/ +#else +#define EOS_ENUM_BOOLEAN_OPERATORS(name) +#endif + + +#undef EOS_HAS_ENUM_CLASS diff --git a/Unlocker/src/platforms/epic/eos_common.h b/Unlocker/src/platforms/epic/eos_common.h new file mode 100644 index 0000000..d08d436 --- /dev/null +++ b/Unlocker/src/platforms/epic/eos_common.h @@ -0,0 +1,39 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "eos_base.h" + +#pragma pack(push, 8) + +#undef EOS_RESULT_VALUE +#undef EOS_RESULT_VALUE_LAST +#define EOS_RESULT_VALUE(Name, Value) Name = Value, +#define EOS_RESULT_VALUE_LAST(Name, Value) Name = Value + +EOS_ENUM_START(EOS_EResult) +#include "eos_result.h" +EOS_ENUM_END(EOS_EResult); + +#undef EOS_RESULT_VALUE +#undef EOS_RESULT_VALUE_LAST + +/** + * A handle to a user's Epic Online Services Account ID + * This ID is associated with a specific login associated with Epic Account Services + * + * @see EOS_Auth_Login + */ +typedef struct EOS_EpicAccountIdDetails* EOS_EpicAccountId; + +/** + * A handle to a user's Product User ID (game services related ecosystem) + * This ID is associated with any of the external account providers (of which Epic Account Services is one) + * + * @see EOS_Connect_Login + * @see EOS_EExternalCredentialType + */ +typedef struct EOS_ProductUserIdDetails* EOS_ProductUserId; + + +#pragma pack(pop) diff --git a/Unlocker/src/platforms/epic/eos_ecom_types.h b/Unlocker/src/platforms/epic/eos_ecom_types.h new file mode 100644 index 0000000..3bad878 --- /dev/null +++ b/Unlocker/src/platforms/epic/eos_ecom_types.h @@ -0,0 +1,387 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "eos_common.h" + +#pragma pack(push, 8) + +EXTERN_C typedef struct EOS_EcomHandle* EOS_HEcom; + +/** + * This handle is copied when EOS_Ecom_CopyTransactionById or EOS_Ecom_CopyTransactionByIndex is called. + * A EOS_Ecom_CheckoutCallbackInfo provides the ID for the copy. + * After being copied, EOS_Ecom_Transaction_Release must be called. + * + * @see EOS_Ecom_CheckoutCallbackInfo + * @see EOS_Ecom_CopyTransactionById + * @see EOS_Ecom_CopyTransactionByIndex + * @see EOS_Ecom_Transaction_Release + */ +EXTERN_C typedef struct EOS_Ecom_TransactionHandle* EOS_Ecom_HTransaction; + +/** + * A unique identifier for a catalog item defined and stored with the backend catalog service. + * A catalog item represents a distinct object within the catalog. When acquired by an account, an + * entitlement is granted that references a specific catalog item. + */ +EXTERN_C typedef const char* EOS_Ecom_CatalogItemId; + +/** + * A unique identifier for a catalog offer defined and stored with the backend catalog service. + * A catalog offer is a purchasable collection of 1 or more items, associated with a price (which + * could be 0). When an offer is purchased an entitlement is granted for each of the items + * referenced by the offer. + */ +EXTERN_C typedef const char* EOS_Ecom_CatalogOfferId; + +/** + * An identifier which is defined on a catalog item and stored with the backend catalog service. + * The entitlement name may not be unique. A catalog may be configured with multiple items with the + * same entitlement name in order to define a logical grouping of entitlements. This is used to + * retrieve all entitlements granted to an account grouped in this way. + * + * @see EOS_Ecom_QueryEntitlements + */ +EXTERN_C typedef const char* EOS_Ecom_EntitlementName; + +/** + * A unique identifier for an entitlement owned by an account. An entitlement is always associated + * with a single account. The entitlement ID is provided to allow redeeming the entitlement as + * well as identify individual entitlement grants. + * + * @see EOS_Ecom_QueryEntitlements + * @see EOS_Ecom_RedeemEntitlements + */ +EXTERN_C typedef const char* EOS_Ecom_EntitlementId; + + +/** + * An enumeration of the different ownership statuses. + */ +EOS_ENUM(EOS_EOwnershipStatus, + /** The catalog item is not owned by the local user */ + EOS_OS_NotOwned = 0, + /** The catalog item is owned by the local user */ + EOS_OS_Owned = 1 +); + +/** + * An enumeration defining the type of catalog item. The primary use is to identify how the item is expended. + */ +EOS_ENUM(EOS_EEcomItemType, + /** This entitlement is intended to persist. */ + EOS_EIT_Durable = 0, + /** + * This entitlement is intended to be transient and redeemed. + * + * @see EOS_Ecom_RedeemEntitlements + */ + EOS_EIT_Consumable = 1, + /** This entitlement has a type that is not currently intneded for an in-game store. */ + EOS_EIT_Other = 2 +); + +/** The most recent version of the EOS_Ecom_Entitlement struct. */ +#define EOS_ECOM_ENTITLEMENT_API_LATEST 2 + +/** Timestamp value representing an undefined EndTimestamp for EOS_Ecom_Entitlement */ +#define EOS_ECOM_ENTITLEMENT_ENDTIMESTAMP_UNDEFINED -1 + +/** + * Contains information about a single entitlement associated with an account. Instances of this structure are + * created by EOS_Ecom_CopyEntitlementByIndex, EOS_Ecom_CopyEntitlementByNameAndIndex, or EOS_Ecom_CopyEntitlementById. + * They must be passed to EOS_Ecom_Entitlement_Release. + */ +EOS_STRUCT(EOS_Ecom_Entitlement, ( + /** API Version: Set this to EOS_ECOM_ENTITLEMENT_API_LATEST. */ + int32_t ApiVersion; + /** Name of the entitlement */ + EOS_Ecom_EntitlementName EntitlementName; + /** ID of the entitlement owned by an account */ + EOS_Ecom_EntitlementId EntitlementId; + /** ID of the item associated with the offer which granted this entitlement */ + EOS_Ecom_CatalogItemId CatalogItemId; + /** + * If queried using pagination then ServerIndex represents the index of the entitlement as it + * exists on the server. If not queried using pagination then ServerIndex will be -1. + */ + int32_t ServerIndex; + /** If true then the catalog has this entitlement marked as redeemed */ + EOS_Bool bRedeemed; + /** If not -1 then this is a POSIX timestamp that this entitlement will end */ + int64_t EndTimestamp; +)); + + +/** The most recent version of the EOS_Ecom_ItemOwnership struct. */ +#define EOS_ECOM_ITEMOWNERSHIP_API_LATEST 1 + +/** + * Contains information about a single item ownership associated with an account. This structure is + * returned as part of the EOS_Ecom_QueryOwnershipCallbackInfo structure. + */ +EOS_STRUCT(EOS_Ecom_ItemOwnership, ( + /** API Version: Set this to EOS_ECOM_ITEMOWNERSHIP_API_LATEST. */ + int32_t ApiVersion; + /** ID of the catalog item */ + EOS_Ecom_CatalogItemId Id; + /** Is this catalog item owned by the local user */ + EOS_EOwnershipStatus OwnershipStatus; +)); + +/** The most recent version of the EOS_Ecom_QueryOwnership API. */ +#define EOS_ECOM_QUERYOWNERSHIP_API_LATEST 2 + +/** + * The maximum number of catalog items that may be queried in a single pass + */ +#define EOS_ECOM_QUERYOWNERSHIP_MAX_CATALOG_IDS 32 + +/** + * Input parameters for the EOS_Ecom_QueryOwnership function. + */ +EOS_STRUCT(EOS_Ecom_QueryOwnershipOptions, ( + /** API Version: Set this to EOS_ECOM_QUERYOWNERSHIP_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user whose ownership to query */ + EOS_EpicAccountId LocalUserId; + /** The array of Catalog Item IDs to check for ownership */ + EOS_Ecom_CatalogItemId* CatalogItemIds; + /** The number of Catalog Item IDs to in the array */ + uint32_t CatalogItemIdCount; + /** Optional product namespace, if not the one specified during initialization */ + const char* CatalogNamespace; +)); + +/** + * Output parameters for the EOS_Ecom_QueryOwnership Function. + */ +EOS_STRUCT(EOS_Ecom_QueryOwnershipCallbackInfo, ( + /** The EOS_EResult code for the operation. EOS_Success indicates that the operation succeeded; other codes indicate errors. */ + EOS_EResult ResultCode; + /** Context that was passed into EOS_Ecom_QueryOwnership */ + void* ClientData; + /** The Epic Online Services Account ID of the local user whose ownership was queried */ + EOS_EpicAccountId LocalUserId; + /** List of catalog items and their ownership status */ + const EOS_Ecom_ItemOwnership* ItemOwnership; + /** Number of ownership results are included in this callback */ + uint32_t ItemOwnershipCount; +)); + +/** + * Function prototype definition for callbacks passed to EOS_Ecom_QueryOwnership + * @param Data A EOS_Ecom_QueryOwnershipCallbackInfo containing the output information and result + */ +EOS_DECLARE_CALLBACK(EOS_Ecom_OnQueryOwnershipCallback, const EOS_Ecom_QueryOwnershipCallbackInfo* Data); + +/** The most recent version of the EOS_Ecom_QueryOwnershipToken API. */ +#define EOS_ECOM_QUERYOWNERSHIPTOKEN_API_LATEST 2 + +/** + * The maximum number of catalog items that may be queried in a single pass + */ +#define EOS_ECOM_QUERYOWNERSHIPTOKEN_MAX_CATALOGITEM_IDS 32 + +/** + * Input parameters for the EOS_Ecom_QueryOwnershipToken function. + */ +EOS_STRUCT(EOS_Ecom_QueryOwnershipTokenOptions, ( + /** API Version: Set this to EOS_ECOM_QUERYOWNERSHIPTOKEN_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user whose ownership token you want to query */ + EOS_EpicAccountId LocalUserId; + /** The array of Catalog Item IDs to check for ownership, matching in number to the CatalogItemIdCount */ + EOS_Ecom_CatalogItemId* CatalogItemIds; + /** The number of catalog item IDs to query */ + uint32_t CatalogItemIdCount; + /** Optional product namespace, if not the one specified during initialization */ + const char* CatalogNamespace; +)); + +/** + * Output parameters for the EOS_Ecom_QueryOwnershipToken Function. + */ +EOS_STRUCT(EOS_Ecom_QueryOwnershipTokenCallbackInfo, ( + /** The EOS_EResult code for the operation. EOS_Success indicates that the operation succeeded; other codes indicate errors. */ + EOS_EResult ResultCode; + /** Context that was passed into EOS_Ecom_QueryOwnershipToken */ + void* ClientData; + /** The Epic Online Services Account ID of the local user whose ownership token was queried */ + EOS_EpicAccountId LocalUserId; + /** Ownership token containing details about the catalog items queried */ + const char* OwnershipToken; +)); + +/** + * Function prototype definition for callbacks passed to EOS_Ecom_QueryOwnershipToken + * @param Data A EOS_Ecom_QueryOwnershipTokenCallbackInfo containing the output information and result + */ +EOS_DECLARE_CALLBACK(EOS_Ecom_OnQueryOwnershipTokenCallback, const EOS_Ecom_QueryOwnershipTokenCallbackInfo* Data); + +/** The most recent version of the EOS_Ecom_QueryEntitlements API. */ +#define EOS_ECOM_QUERYENTITLEMENTS_API_LATEST 2 + +/** + * The maximum number of entitlements that may be queried in a single pass + */ +#define EOS_ECOM_QUERYENTITLEMENTS_MAX_ENTITLEMENT_IDS 32 + +/** + * Input parameters for the EOS_Ecom_QueryEntitlements function. + */ +EOS_STRUCT(EOS_Ecom_QueryEntitlementsOptions, ( + /** API Version: Set this to EOS_ECOM_QUERYENTITLEMENTS_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user whose Entitlements you want to retrieve */ + EOS_EpicAccountId LocalUserId; + /** An array of Entitlement Names that you want to check */ + EOS_Ecom_EntitlementName* EntitlementNames; + /** The number of Entitlement Names included in the array, up to EOS_ECOM_QUERYENTITLEMENTS_MAX_ENTITLEMENT_IDS; use zero to request all Entitlements associated with the user's Epic Online Services account. */ + uint32_t EntitlementNameCount; + /** If true, Entitlements that have been redeemed will be included in the results. */ + EOS_Bool bIncludeRedeemed; +)); + +/** + * Output parameters for the EOS_Ecom_QueryEntitlements Function. + */ +EOS_STRUCT(EOS_Ecom_QueryEntitlementsCallbackInfo, ( + EOS_EResult ResultCode; + /** Context that was passed into EOS_Ecom_QueryEntitlements */ + void* ClientData; + /** The Epic Online Services Account ID of the local user whose entitlement was queried */ + EOS_EpicAccountId LocalUserId; +)); + +/** + * Function prototype definition for callbacks passed to EOS_Ecom_QueryOwnershipToken + * @param Data A EOS_Ecom_QueryEntitlementsCallbackInfo containing the output information and result + */ +EOS_DECLARE_CALLBACK(EOS_Ecom_OnQueryEntitlementsCallback, const EOS_Ecom_QueryEntitlementsCallbackInfo* Data); + +/** The most recent version of the EOS_Ecom_GetEntitlementsCount API. */ +#define EOS_ECOM_GETENTITLEMENTSCOUNT_API_LATEST 1 + +/** + * Input parameters for the EOS_Ecom_GetEntitlementsCount function. + */ +EOS_STRUCT(EOS_Ecom_GetEntitlementsCountOptions, ( + /** API Version: Set this to EOS_ECOM_GETENTITLEMENTSCOUNT_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user for which to retrieve the entitlement count */ + EOS_EpicAccountId LocalUserId; +)); + +/** The most recent version of the EOS_Ecom_GetEntitlementsByNameCount API. */ +#define EOS_ECOM_GETENTITLEMENTSBYNAMECOUNT_API_LATEST 1 + +/** + * Input parameters for the EOS_Ecom_GetEntitlementsByNameCount function. + */ +EOS_STRUCT(EOS_Ecom_GetEntitlementsByNameCountOptions, ( + /** API Version: Set this to EOS_ECOM_GETENTITLEMENTSBYNAMECOUNT_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user for which to retrieve the entitlement count */ + EOS_EpicAccountId LocalUserId; + /** Name of the entitlement to count in the cache */ + EOS_Ecom_EntitlementName EntitlementName; +)); + +/** The most recent version of the EOS_Ecom_CopyEntitlementByIndex API. */ +#define EOS_ECOM_COPYENTITLEMENTBYINDEX_API_LATEST 1 + +/** + * Input parameters for the EOS_Ecom_CopyEntitlementByIndex function. + */ +EOS_STRUCT(EOS_Ecom_CopyEntitlementByIndexOptions, ( + /** API Version: Set this to EOS_ECOM_COPYENTITLEMENTBYINDEX_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user whose entitlement is being copied */ + EOS_EpicAccountId LocalUserId; + /** Index of the entitlement to retrieve from the cache */ + uint32_t EntitlementIndex; +)); + +/** The most recent version of the EOS_Ecom_CopyEntitlementByNameAndIndex API. */ +#define EOS_ECOM_COPYENTITLEMENTBYNAMEANDINDEX_API_LATEST 1 + +/** + * Input parameters for the EOS_Ecom_CopyEntitlementByNameAndIndex function. + */ +EOS_STRUCT(EOS_Ecom_CopyEntitlementByNameAndIndexOptions, ( + /** API Version: Set this to EOS_ECOM_COPYENTITLEMENTBYNAMEANDINDEX_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user whose entitlement is being copied */ + EOS_EpicAccountId LocalUserId; + /** Name of the entitlement to retrieve from the cache */ + EOS_Ecom_EntitlementName EntitlementName; + /** Index of the entitlement within the named set to retrieve from the cache. */ + uint32_t Index; +)); + +/** The most recent version of the EOS_Ecom_CopyEntitlementById API. */ +#define EOS_ECOM_COPYENTITLEMENTBYID_API_LATEST 2 + +/** + * Input parameters for the EOS_Ecom_CopyEntitlementById function. + */ +EOS_STRUCT(EOS_Ecom_CopyEntitlementByIdOptions, ( + /** API Version: Set this to EOS_ECOM_COPYENTITLEMENTBYID_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user whose entitlement is being copied */ + EOS_EpicAccountId LocalUserId; + /** ID of the entitlement to retrieve from the cache */ + EOS_Ecom_EntitlementId EntitlementId; +)); + +/** The most recent version of the EOS_Ecom_CopyItemById API. */ +#define EOS_ECOM_COPYITEMBYID_API_LATEST 1 + +/** + * Input parameters for the EOS_Ecom_CopyItemById function. + */ +EOS_STRUCT(EOS_Ecom_CopyItemByIdOptions, ( + /** API Version: Set this to EOS_ECOM_COPYITEMBYID_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user whose item is being copied */ + EOS_EpicAccountId LocalUserId; + /** The ID of the item to get. */ + EOS_Ecom_CatalogItemId ItemId; +)); + + +/** The most recent version of the EOS_Ecom_GetItemReleaseCount API. */ +#define EOS_ECOM_GETITEMRELEASECOUNT_API_LATEST 1 + +/** + * Input parameters for the EOS_Ecom_GetItemReleaseCount function. + */ +EOS_STRUCT(EOS_Ecom_GetItemReleaseCountOptions, ( + /** API Version: Set this to EOS_ECOM_GETITEMRELEASECOUNT_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user whose item release is being accessed */ + EOS_EpicAccountId LocalUserId; + /** The ID of the item to get the releases for. */ + EOS_Ecom_CatalogItemId ItemId; +)); + +/** The most recent version of the EOS_Ecom_CopyItemReleaseByIndex API. */ +#define EOS_ECOM_COPYITEMRELEASEBYINDEX_API_LATEST 1 + +/** + * Input parameters for the EOS_Ecom_CopyItemReleaseByIndex function. + */ +EOS_STRUCT(EOS_Ecom_CopyItemReleaseByIndexOptions, ( + /** API Version: Set this to EOS_ECOM_COPYITEMRELEASEBYINDEX_API_LATEST. */ + int32_t ApiVersion; + /** The Epic Online Services Account ID of the local user whose item release is being copied */ + EOS_EpicAccountId LocalUserId; + /** The ID of the item to get the releases for. */ + EOS_Ecom_CatalogItemId ItemId; + /** The index of the release to get. */ + uint32_t ReleaseIndex; +)); + +#pragma pack(pop) diff --git a/Unlocker/src/platforms/epic/eos_hooks.cpp b/Unlocker/src/platforms/epic/eos_hooks.cpp new file mode 100644 index 0000000..a72207a --- /dev/null +++ b/Unlocker/src/platforms/epic/eos_hooks.cpp @@ -0,0 +1,145 @@ +#include "pch.h" +#include "eos_hooks.h" +#include "Epic.h" + +#define GET_PROXY_FUNC(FUNC) \ + static auto proxyFunc = PLH::FnCast(BasePlatform::trampolineMap[#FUNC], FUNC); + +static vector entitlements; + +auto getEpicConfig() +{ + return config->platforms["Epic Games"]; +} + +void EOS_CALL EOS_Ecom_QueryOwnership( + EOS_HEcom Handle, + const EOS_Ecom_QueryOwnershipOptions* Options, + void* ClientData, + const EOS_Ecom_OnQueryOwnershipCallback CompletionDelegate +) +{ + auto itemCount = Options->CatalogItemIdCount; + auto ownerships = new EOS_Ecom_ItemOwnership[itemCount]; + + logger->info("Game requested ownership of {} items", itemCount); + for(uint32_t i = 0; i < itemCount; i++) + { + // Epic magic happens here + ownerships[i].ApiVersion = EOS_ECOM_ITEMOWNERSHIP_API_LATEST; + ownerships[i].Id = Options->CatalogItemIds[i]; + auto isBlacklisted = vectorContains(getEpicConfig().blacklist, string(ownerships[i].Id)); + ownerships[i].OwnershipStatus = isBlacklisted ? EOS_EOwnershipStatus::EOS_OS_NotOwned : EOS_EOwnershipStatus::EOS_OS_Owned; + + logger->info("\t{} [{}]", ownerships[i].Id, (bool) ownerships[i].OwnershipStatus ? "Owned" : "NotOwned"); + } + + EOS_Ecom_QueryOwnershipCallbackInfo callbackInfo = + { + EOS_EResult::EOS_Success, + ClientData, + Options->LocalUserId, + ownerships, + Options->CatalogItemIdCount + }; + + CompletionDelegate(&callbackInfo); + + delete[] ownerships; +} + +void EOS_CALL EOS_Ecom_QueryEntitlements( + EOS_HEcom Handle, + const EOS_Ecom_QueryEntitlementsOptions* Options, + void* ClientData, + const EOS_Ecom_OnQueryEntitlementsCallback CompletionDelegate +) +{ + auto entitlementCount = Options->EntitlementNameCount; + + logger->info("Game requested ownership of {} entitlements", entitlementCount); + + entitlements.clear(); + for(uint32_t i = 0; i < entitlementCount; i++) + { + auto isBlacklisted = vectorContains(getEpicConfig().blacklist, string(Options->EntitlementNames[i])); + if(!isBlacklisted) + { + // Save the entitlements id for response in subsequent queries + entitlements.push_back(Options->EntitlementNames[i]); + } + } + + EOS_Ecom_QueryEntitlementsCallbackInfo callbackData = { + EOS_EResult::EOS_Success, + ClientData, + Options->LocalUserId + }; + + CompletionDelegate(&callbackData); +} + +uint32_t EOS_CALL EOS_Ecom_GetEntitlementsCount( + EOS_HEcom Handle, + const EOS_Ecom_GetEntitlementsCountOptions* Options +) +{ + logger->debug("Game requested count of user entitlements"); + + auto entitlementCount = (uint32_t) entitlements.size(); + if(entitlementCount == 0) + { + logger->warn("No entitlements were queried. Redirecting to original function."); + GET_PROXY_FUNC(EOS_Ecom_GetEntitlementsCount); + entitlementCount = proxyFunc(Handle, Options); + } + + logger->info("Responding with {} entitlements", entitlementCount); + + return entitlementCount; +} + +EOS_EResult EOS_CALL EOS_Ecom_CopyEntitlementByIndex( + EOS_HEcom Handle, + const EOS_Ecom_CopyEntitlementByIndexOptions* Options, + EOS_Ecom_Entitlement** OutEntitlement +) +{ + logger->debug("EOS_Ecom_CopyEntitlementByIndex -> ApiVersion: {}", Options->ApiVersion); + logger->debug("Game requested entitlement with index: {}", Options->EntitlementIndex); + + if(Options->EntitlementIndex >= entitlements.size()) + { + logger->warn("Out of bounds entitlement index. Redirecting to original function."); + GET_PROXY_FUNC(EOS_Ecom_CopyEntitlementByIndex); + return proxyFunc(Handle, Options, OutEntitlement); + } + else + { + // Epic magic happens here (2) + + const char* id = makeCStringCopy(entitlements.at(Options->EntitlementIndex)); + + logger->info("\t{}", id); + + *OutEntitlement = new EOS_Ecom_Entitlement{ + EOS_ECOM_ENTITLEMENT_API_LATEST, + id, // EntitlementName + id, // EntitlementId + id, // CatalogItemId + -1, // ServerIndex + false, // bRedeemed + -1 // EndTimestamp + }; + + return EOS_EResult::EOS_Success; + } +} + +void EOS_CALL EOS_Ecom_Entitlement_Release(EOS_Ecom_Entitlement* Entitlement) +{ + logger->debug("Game requested to release entitlement: {}", Entitlement->EntitlementId); + + delete[] Entitlement->EntitlementId; + delete Entitlement; +} diff --git a/Unlocker/src/platforms/epic/eos_hooks.h b/Unlocker/src/platforms/epic/eos_hooks.h new file mode 100644 index 0000000..d68a048 --- /dev/null +++ b/Unlocker/src/platforms/epic/eos_hooks.h @@ -0,0 +1,82 @@ +#pragma once +#include "eos_ecom_types.h" + +/** + * Query the ownership status for a given list of catalog item IDs defined with Epic Online Services. + * This data will be cached for a limited time and retrieved again from the backend when necessary + * + * @param Options structure containing the account and catalog item IDs to retrieve + * @param ClientData arbitrary data that is passed back to you in the CompletionDelegate + * @param CompletionDelegate a callback that is fired when the async operation completes, either successfully or in error + */ +void EOS_CALL EOS_Ecom_QueryOwnership( + EOS_HEcom Handle, + const EOS_Ecom_QueryOwnershipOptions* Options, + void* ClientData, + const EOS_Ecom_OnQueryOwnershipCallback CompletionDelegate +); + +/** + * Query the entitlement information defined with Epic Online Services. + * A set of entitlement names can be provided to filter the set of entitlements associated with the account. + * This data will be cached for a limited time and retrieved again from the backend when necessary. + * Use EOS_Ecom_CopyEntitlementByIndex, EOS_Ecom_CopyEntitlementByNameAndIndex, and EOS_Ecom_CopyEntitlementById to get the entitlement details. + * Use EOS_Ecom_GetEntitlementsByNameCount to retrieve the number of entitlements with a specific entitlement name. + * + * @param Options structure containing the account and entitlement names to retrieve + * @param ClientData arbitrary data that is passed back to you in the CompletionDelegate + * @param CompletionDelegate a callback that is fired when the async operation completes, either successfully or in error + */ +void EOS_CALL EOS_Ecom_QueryEntitlements( + EOS_HEcom Handle, + const EOS_Ecom_QueryEntitlementsOptions* Options, + void* ClientData, + const EOS_Ecom_OnQueryEntitlementsCallback CompletionDelegate +); + +/** + * Fetch the number of entitlements that are cached for a given local user. + * + * @param Options structure containing the Epic Online Services Account ID being accessed + * + * @see EOS_Ecom_CopyEntitlementByIndex + * + * @return the number of entitlements found. + */ +uint32_t EOS_CALL EOS_Ecom_GetEntitlementsCount( + EOS_HEcom Handle, + const EOS_Ecom_GetEntitlementsCountOptions* Options +); + +/** + * Fetches an entitlement from a given index. + * + * @param Options structure containing the Epic Online Services Account ID and index being accessed + * @param OutEntitlement the entitlement for the given index, if it exists and is valid, use EOS_Ecom_Entitlement_Release when finished + * + * @see EOS_Ecom_Entitlement_Release + * + * @return EOS_Success if the information is available and passed out in OutEntitlement + * EOS_Ecom_EntitlementStale if the entitlement information is stale and passed out in OutEntitlement + * EOS_InvalidParameters if you pass a null pointer for the out parameter + * EOS_NotFound if the entitlement is not found + */ +EOS_EResult EOS_CALL EOS_Ecom_CopyEntitlementByIndex( + EOS_HEcom Handle, + const EOS_Ecom_CopyEntitlementByIndexOptions* Options, + EOS_Ecom_Entitlement** OutEntitlement +); + +/** + * Release the memory associated with an EOS_Ecom_Entitlement structure. This must be called on data + * retrieved from EOS_Ecom_CopyEntitlementByIndex and EOS_Ecom_CopyEntitlementById. + * + * @param Entitlement - The entitlement structure to be released + * + * @see EOS_Ecom_Entitlement + * @see EOS_Ecom_CopyEntitlementByIndex + * @see EOS_Ecom_CopyEntitlementById + */ +void EOS_CALL EOS_Ecom_Entitlement_Release( + EOS_Ecom_Entitlement* Entitlement +); diff --git a/Unlocker/src/platforms/epic/eos_result.h b/Unlocker/src/platforms/epic/eos_result.h new file mode 100644 index 0000000..be515d2 --- /dev/null +++ b/Unlocker/src/platforms/epic/eos_result.h @@ -0,0 +1,384 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +// This file is not intended to be included directly. Include eos_common.h instead. + +/** Successful result. no further error processing needed */ +EOS_RESULT_VALUE(EOS_Success, 0) + +/** Failed due to no connection */ +EOS_RESULT_VALUE(EOS_NoConnection, 1) +/** Failed login due to invalid credentials */ +EOS_RESULT_VALUE(EOS_InvalidCredentials, 2) +/** Failed due to invalid or missing user */ +EOS_RESULT_VALUE(EOS_InvalidUser, 3) +/** Failed due to invalid or missing authentication token for user (e.g. not logged in) */ +EOS_RESULT_VALUE(EOS_InvalidAuth, 4) +/** Failed due to invalid access */ +EOS_RESULT_VALUE(EOS_AccessDenied, 5) +/** If the client does not possess the permission required */ +EOS_RESULT_VALUE(EOS_MissingPermissions, 6) +/** If the token provided does not represent an account */ +EOS_RESULT_VALUE(EOS_Token_Not_Account, 7) +/** Throttled due to too many requests */ +EOS_RESULT_VALUE(EOS_TooManyRequests, 8) +/** Async request was already pending */ +EOS_RESULT_VALUE(EOS_AlreadyPending, 9) +/** Invalid parameters specified for request */ +EOS_RESULT_VALUE(EOS_InvalidParameters, 10) +/** Invalid request */ +EOS_RESULT_VALUE(EOS_InvalidRequest, 11) +/** Failed due to unable to parse or recognize a backend response */ +EOS_RESULT_VALUE(EOS_UnrecognizedResponse, 12) +/** Incompatible client for backend version */ +EOS_RESULT_VALUE(EOS_IncompatibleVersion, 13) +/** Not configured correctly for use */ +EOS_RESULT_VALUE(EOS_NotConfigured, 14) +/** Already configured for use. */ +EOS_RESULT_VALUE(EOS_AlreadyConfigured, 15) +/** Feature not available on this implementation */ +EOS_RESULT_VALUE(EOS_NotImplemented, 16) +/** Operation was canceled (likely by user) */ +EOS_RESULT_VALUE(EOS_Canceled, 17) +/** The requested information was not found */ +EOS_RESULT_VALUE(EOS_NotFound, 18) +/** An error occurred during an asynchronous operation, and it will be retried. Callbacks receiving this result will be called again in the future. */ +EOS_RESULT_VALUE(EOS_OperationWillRetry, 19) +/** The request had no effect */ +EOS_RESULT_VALUE(EOS_NoChange, 20) +/** The request attempted to use multiple or inconsistent API versions */ +EOS_RESULT_VALUE(EOS_VersionMismatch, 21) +/** A maximum limit was exceeded on the client, different from EOS_TooManyRequests */ +EOS_RESULT_VALUE(EOS_LimitExceeded, 22) +/** Feature or client ID performing the operation has been disabled. */ +EOS_RESULT_VALUE(EOS_Disabled, 23) +/** Duplicate entry not allowed */ +EOS_RESULT_VALUE(EOS_DuplicateNotAllowed, 24) +/** Required parameters are missing. DEPRECATED: This error is no longer used. */ +EOS_RESULT_VALUE(EOS_MissingParameters_DEPRECATED, 25) +/** Sandbox ID is invalid */ +EOS_RESULT_VALUE(EOS_InvalidSandboxId, 26) +/** Request timed out */ +EOS_RESULT_VALUE(EOS_TimedOut, 27) +/** A query returned some but not all of the requested results. */ +EOS_RESULT_VALUE(EOS_PartialResult, 28) +/** Client is missing the whitelisted role */ +EOS_RESULT_VALUE(EOS_Missing_Role, 29) +/** Client is missing the whitelisted feature */ +EOS_RESULT_VALUE(EOS_Missing_Feature, 30) +/** The sandbox given to the backend is invalid */ +EOS_RESULT_VALUE(EOS_Invalid_Sandbox, 31) +/** The deployment given to the backend is invalid */ +EOS_RESULT_VALUE(EOS_Invalid_Deployment, 32) +/** The product ID specified to the backend is invalid */ +EOS_RESULT_VALUE(EOS_Invalid_Product, 33) +/** The product user ID specified to the backend is invalid */ +EOS_RESULT_VALUE(EOS_Invalid_ProductUserID, 34) +/** There was a failure with the backend service */ +EOS_RESULT_VALUE(EOS_ServiceFailure, 35) +/** Cache directory is not set in platform options. */ +EOS_RESULT_VALUE(EOS_CacheDirectoryMissing, 36) +/** Cache directory is not accessible. */ +EOS_RESULT_VALUE(EOS_CacheDirectoryInvalid, 37) +/** The request failed because resource was in an invalid state */ +EOS_RESULT_VALUE(EOS_InvalidState, 38) +/** Request is in progress */ +EOS_RESULT_VALUE(EOS_RequestInProgress, 39) + +/** Account locked due to login failures */ +EOS_RESULT_VALUE(EOS_Auth_AccountLocked, 1001) +/** Account locked by update operation. */ +EOS_RESULT_VALUE(EOS_Auth_AccountLockedForUpdate, 1002) +/** Refresh token used was invalid */ +EOS_RESULT_VALUE(EOS_Auth_InvalidRefreshToken, 1003) +/** Invalid access token, typically when switching between backend environments */ +EOS_RESULT_VALUE(EOS_Auth_InvalidToken, 1004) +/** Invalid bearer token */ +EOS_RESULT_VALUE(EOS_Auth_AuthenticationFailure, 1005) +/** Invalid platform token */ +EOS_RESULT_VALUE(EOS_Auth_InvalidPlatformToken, 1006) +/** Auth parameters are not associated with this account */ +EOS_RESULT_VALUE(EOS_Auth_WrongAccount, 1007) +/** Auth parameters are not associated with this client */ +EOS_RESULT_VALUE(EOS_Auth_WrongClient, 1008) +/** Full account is required */ +EOS_RESULT_VALUE(EOS_Auth_FullAccountRequired, 1009) +/** Headless account is required */ +EOS_RESULT_VALUE(EOS_Auth_HeadlessAccountRequired, 1010) +/** Password reset is required */ +EOS_RESULT_VALUE(EOS_Auth_PasswordResetRequired, 1011) +/** Password was previously used and cannot be reused */ +EOS_RESULT_VALUE(EOS_Auth_PasswordCannotBeReused, 1012) +/** Authorization code/exchange code has expired */ +EOS_RESULT_VALUE(EOS_Auth_Expired, 1013) +/** Consent has not been given by the user */ +EOS_RESULT_VALUE(EOS_Auth_ScopeConsentRequired, 1014) +/** The application has no profile on the backend */ +EOS_RESULT_VALUE(EOS_Auth_ApplicationNotFound, 1015) +/** The requested consent wasn't found on the backend */ +EOS_RESULT_VALUE(EOS_Auth_ScopeNotFound, 1016) +/** This account has been denied access to login */ +EOS_RESULT_VALUE(EOS_Auth_AccountFeatureRestricted, 1017) + +/** Pin grant code initiated */ +EOS_RESULT_VALUE(EOS_Auth_PinGrantCode, 1020) +/** Pin grant code attempt expired */ +EOS_RESULT_VALUE(EOS_Auth_PinGrantExpired, 1021) +/** Pin grant code attempt pending */ +EOS_RESULT_VALUE(EOS_Auth_PinGrantPending, 1022) + +/** External auth source did not yield an account */ +EOS_RESULT_VALUE(EOS_Auth_ExternalAuthNotLinked, 1030) +/** External auth access revoked */ +EOS_RESULT_VALUE(EOS_Auth_ExternalAuthRevoked, 1032) +/** External auth token cannot be interpreted */ +EOS_RESULT_VALUE(EOS_Auth_ExternalAuthInvalid, 1033) +/** External auth cannot be linked to his account due to restrictions */ +EOS_RESULT_VALUE(EOS_Auth_ExternalAuthRestricted, 1034) +/** External auth cannot be used for login */ +EOS_RESULT_VALUE(EOS_Auth_ExternalAuthCannotLogin, 1035) +/** External auth is expired */ +EOS_RESULT_VALUE(EOS_Auth_ExternalAuthExpired, 1036) +/** External auth cannot be removed since it's the last possible way to login */ +EOS_RESULT_VALUE(EOS_Auth_ExternalAuthIsLastLoginType, 1037) + +/** Exchange code not found */ +EOS_RESULT_VALUE(EOS_Auth_ExchangeCodeNotFound, 1040) +/** Originating exchange code session has expired */ +EOS_RESULT_VALUE(EOS_Auth_OriginatingExchangeCodeSessionExpired, 1041) + +/** The account has been disabled and cannot be used for authentication */ +EOS_RESULT_VALUE(EOS_Auth_PersistentAuth_AccountNotActive, 1050) + +/** MFA challenge required */ +EOS_RESULT_VALUE(EOS_Auth_MFARequired, 1060) + +/** Parental locks are in place */ +EOS_RESULT_VALUE(EOS_Auth_ParentalControls, 1070) + +/** Korea real ID association required but missing */ +EOS_RESULT_VALUE(EOS_Auth_NoRealId, 1080) + +/** An outgoing friend invitation is awaiting acceptance; sending another invite to the same user is erroneous */ +EOS_RESULT_VALUE(EOS_Friends_InviteAwaitingAcceptance, 2000) +/** There is no friend invitation to accept/reject */ +EOS_RESULT_VALUE(EOS_Friends_NoInvitation, 2001) +/** Users are already friends, so sending another invite is erroneous */ +EOS_RESULT_VALUE(EOS_Friends_AlreadyFriends, 2003) +/** Users are not friends, so deleting the friend is erroneous */ +EOS_RESULT_VALUE(EOS_Friends_NotFriends, 2004) +/** Remote user has too many invites to receive new invites */ +EOS_RESULT_VALUE(EOS_Friends_TargetUserTooManyInvites, 2005) +/** Local user has too many invites to send new invites */ +EOS_RESULT_VALUE(EOS_Friends_LocalUserTooManyInvites, 2006) +/** Remote user has too many friends to make a new friendship */ +EOS_RESULT_VALUE(EOS_Friends_TargetUserFriendLimitExceeded, 2007) +/** Local user has too many friends to make a new friendship */ +EOS_RESULT_VALUE(EOS_Friends_LocalUserFriendLimitExceeded, 2008) + +/** Request data was null or invalid */ +EOS_RESULT_VALUE(EOS_Presence_DataInvalid, 3000) +/** Request contained too many or too few unique data items, or the request would overflow the maximum amount of data allowed */ +EOS_RESULT_VALUE(EOS_Presence_DataLengthInvalid, 3001) +/** Request contained data with an invalid key */ +EOS_RESULT_VALUE(EOS_Presence_DataKeyInvalid, 3002) +/** Request contained data with a key too long or too short */ +EOS_RESULT_VALUE(EOS_Presence_DataKeyLengthInvalid, 3003) +/** Request contained data with an invalid value */ +EOS_RESULT_VALUE(EOS_Presence_DataValueInvalid, 3004) +/** Request contained data with a value too long */ +EOS_RESULT_VALUE(EOS_Presence_DataValueLengthInvalid, 3005) +/** Request contained an invalid rich text string */ +EOS_RESULT_VALUE(EOS_Presence_RichTextInvalid, 3006) +/** Request contained a rich text string that was too long */ +EOS_RESULT_VALUE(EOS_Presence_RichTextLengthInvalid, 3007) +/** Request contained an invalid status state */ +EOS_RESULT_VALUE(EOS_Presence_StatusInvalid, 3008) + +/** The entitlement retrieved is stale, requery for updated information */ +EOS_RESULT_VALUE(EOS_Ecom_EntitlementStale, 4000) +/** The offer retrieved is stale, requery for updated information */ +EOS_RESULT_VALUE(EOS_Ecom_CatalogOfferStale, 4001) +/** The item or associated structure retrieved is stale, requery for updated information */ +EOS_RESULT_VALUE(EOS_Ecom_CatalogItemStale, 4002) +/** The one or more offers has an invalid price. This may be caused by the price setup. */ +EOS_RESULT_VALUE(EOS_Ecom_CatalogOfferPriceInvalid, 4003) +/** The checkout page failed to load */ +EOS_RESULT_VALUE(EOS_Ecom_CheckoutLoadError, 4004) + +/** Session is already in progress */ +EOS_RESULT_VALUE(EOS_Sessions_SessionInProgress, 5000) +/** Too many players to register with this session */ +EOS_RESULT_VALUE(EOS_Sessions_TooManyPlayers, 5001) +/** Client has no permissions to access this session */ +EOS_RESULT_VALUE(EOS_Sessions_NoPermission, 5002) +/** Session already exists in the system */ +EOS_RESULT_VALUE(EOS_Sessions_SessionAlreadyExists, 5003) +/** Session lock required for operation */ +EOS_RESULT_VALUE(EOS_Sessions_InvalidLock, 5004) +/** Invalid session reference */ +EOS_RESULT_VALUE(EOS_Sessions_InvalidSession, 5005) +/** Sandbox ID associated with auth didn't match */ +EOS_RESULT_VALUE(EOS_Sessions_SandboxNotAllowed, 5006) +/** Invite failed to send */ +EOS_RESULT_VALUE(EOS_Sessions_InviteFailed, 5007) +/** Invite was not found with the service */ +EOS_RESULT_VALUE(EOS_Sessions_InviteNotFound, 5008) +/** This client may not modify the session */ +EOS_RESULT_VALUE(EOS_Sessions_UpsertNotAllowed, 5009) +/** Backend nodes unavailable to process request */ +EOS_RESULT_VALUE(EOS_Sessions_AggregationFailed, 5010) +/** Individual backend node is as capacity */ +EOS_RESULT_VALUE(EOS_Sessions_HostAtCapacity, 5011) +/** Sandbox on node is at capacity */ +EOS_RESULT_VALUE(EOS_Sessions_SandboxAtCapacity, 5012) +/** An anonymous operation was attempted on a non anonymous session */ +EOS_RESULT_VALUE(EOS_Sessions_SessionNotAnonymous, 5013) +/** Session is currently out of sync with the backend, data is saved locally but needs to sync with backend */ +EOS_RESULT_VALUE(EOS_Sessions_OutOfSync, 5014) +/** User has received too many invites */ +EOS_RESULT_VALUE(EOS_Sessions_TooManyInvites, 5015) +/** Presence session already exists for the client */ +EOS_RESULT_VALUE(EOS_Sessions_PresenceSessionExists, 5016) +/** Deployment on node is at capacity */ +EOS_RESULT_VALUE(EOS_Sessions_DeploymentAtCapacity, 5017) +/** Session operation not allowed */ +EOS_RESULT_VALUE(EOS_Sessions_NotAllowed, 5018) + +/** Request filename was invalid */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_FilenameInvalid, 6000) +/** Request filename was too long */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_FilenameLengthInvalid, 6001) +/** Request filename contained invalid characters */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_FilenameInvalidChars, 6002) +/** Request operation would grow file too large */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_FileSizeTooLarge, 6003) +/** Request file length is not valid */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_FileSizeInvalid, 6004) +/** Request file handle is not valid */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_FileHandleInvalid, 6005) +/** Request data is invalid */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_DataInvalid, 6006) +/** Request data length was invalid */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_DataLengthInvalid, 6007) +/** Request start index was invalid */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_StartIndexInvalid, 6008) +/** Request is in progress */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_RequestInProgress, 6009) +/** User is marked as throttled which means he can't perform some operations because limits are exceeded. */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_UserThrottled, 6010) +/** Encryption key is not set during SDK init. */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_EncryptionKeyNotSet, 6011) +/** User data callback returned error (EOS_PlayerDataStorage_EWriteResult::EOS_WR_FailRequest or EOS_PlayerDataStorage_EReadResult::EOS_RR_FailRequest) */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_UserErrorFromDataCallback, 6012) +/** User is trying to read file that has header from newer version of SDK. Game/SDK needs to be updated. */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_FileHeaderHasNewerVersion, 6013) +/** The file is corrupted. In some cases retry can fix the issue. */ +EOS_RESULT_VALUE(EOS_PlayerDataStorage_FileCorrupted, 6014) + +/** EOS Auth service deemed the external token invalid */ +EOS_RESULT_VALUE(EOS_Connect_ExternalTokenValidationFailed, 7000) +/** EOS Auth user already exists */ +EOS_RESULT_VALUE(EOS_Connect_UserAlreadyExists, 7001) +/** EOS Auth expired */ +EOS_RESULT_VALUE(EOS_Connect_AuthExpired, 7002) +/** EOS Auth invalid token */ +EOS_RESULT_VALUE(EOS_Connect_InvalidToken, 7003) +/** EOS Auth doesn't support this token type */ +EOS_RESULT_VALUE(EOS_Connect_UnsupportedTokenType, 7004) +/** EOS Auth Account link failure */ +EOS_RESULT_VALUE(EOS_Connect_LinkAccountFailed, 7005) +/** EOS Auth External service for validation was unavailable */ +EOS_RESULT_VALUE(EOS_Connect_ExternalServiceUnavailable, 7006) +/** EOS Auth External Service configuration failure with Dev Portal */ +EOS_RESULT_VALUE(EOS_Connect_ExternalServiceConfigurationFailure, 7007) +/** EOS Auth Account link failure. Tried to link Nintendo Network Service Account without first linking Nintendo Account. DEPRECATED: The requirement has been removed and this error is no longer used. */ +EOS_RESULT_VALUE(EOS_Connect_LinkAccountFailedMissingNintendoIdAccount_DEPRECATED, 7008) + +/** The social overlay page failed to load */ +EOS_RESULT_VALUE(EOS_UI_SocialOverlayLoadError, 8000) + +/** Client has no permissions to modify this lobby */ +EOS_RESULT_VALUE(EOS_Lobby_NotOwner, 9000) +/** Lobby lock required for operation */ +EOS_RESULT_VALUE(EOS_Lobby_InvalidLock, 9001) +/** Lobby already exists in the system */ +EOS_RESULT_VALUE(EOS_Lobby_LobbyAlreadyExists, 9002) +/** Lobby is already in progress */ +EOS_RESULT_VALUE(EOS_Lobby_SessionInProgress, 9003) +/** Too many players to register with this lobby */ +EOS_RESULT_VALUE(EOS_Lobby_TooManyPlayers, 9004) +/** Client has no permissions to access this lobby */ +EOS_RESULT_VALUE(EOS_Lobby_NoPermission, 9005) +/** Invalid lobby session reference */ +EOS_RESULT_VALUE(EOS_Lobby_InvalidSession, 9006) +/** Sandbox ID associated with auth didn't match */ +EOS_RESULT_VALUE(EOS_Lobby_SandboxNotAllowed, 9007) +/** Invite failed to send */ +EOS_RESULT_VALUE(EOS_Lobby_InviteFailed, 9008) +/** Invite was not found with the service */ +EOS_RESULT_VALUE(EOS_Lobby_InviteNotFound, 9009) +/** This client may not modify the lobby */ +EOS_RESULT_VALUE(EOS_Lobby_UpsertNotAllowed, 9010) +/** Backend nodes unavailable to process request */ +EOS_RESULT_VALUE(EOS_Lobby_AggregationFailed, 9011) +/** Individual backend node is as capacity */ +EOS_RESULT_VALUE(EOS_Lobby_HostAtCapacity, 9012) +/** Sandbox on node is at capacity */ +EOS_RESULT_VALUE(EOS_Lobby_SandboxAtCapacity, 9013) +/** User has received too many invites */ +EOS_RESULT_VALUE(EOS_Lobby_TooManyInvites, 9014) +/** Deployment on node is at capacity */ +EOS_RESULT_VALUE(EOS_Lobby_DeploymentAtCapacity, 9015) +/** Lobby operation not allowed */ +EOS_RESULT_VALUE(EOS_Lobby_NotAllowed, 9016) +/** While restoring a lost connection lobby ownership changed and only local member data was updated */ +EOS_RESULT_VALUE(EOS_Lobby_MemberUpdateOnly, 9017) +/** Presence lobby already exists for the client */ +EOS_RESULT_VALUE(EOS_Lobby_PresenceLobbyExists, 9018) + +/** User callback that receives data from storage returned error. */ +EOS_RESULT_VALUE(EOS_TitleStorage_UserErrorFromDataCallback, 10000) +/** User forgot to set Encryption key during platform init. Title Storage can't work without it. */ +EOS_RESULT_VALUE(EOS_TitleStorage_EncryptionKeyNotSet, 10001) +/** Downloaded file is corrupted. */ +EOS_RESULT_VALUE(EOS_TitleStorage_FileCorrupted, 10002) +/** Downloaded file's format is newer than client SDK version. */ +EOS_RESULT_VALUE(EOS_TitleStorage_FileHeaderHasNewerVersion, 10003) + +/** ModSdk process is already running. This error comes from the EOSSDK. */ +EOS_RESULT_VALUE(EOS_Mods_ModSdkProcessIsAlreadyRunning, 11000) +/** ModSdk command is empty. Either the ModSdk configuration file is missing or the manifest location is empty. */ +EOS_RESULT_VALUE(EOS_Mods_ModSdkCommandIsEmpty, 11001) +/** Creation of the ModSdk process failed. This error comes from the SDK. */ +EOS_RESULT_VALUE(EOS_Mods_ModSdkProcessCreationFailed, 11002) +/** A critical error occurred in the external ModSdk process that we were unable to resolve. */ +EOS_RESULT_VALUE(EOS_Mods_CriticalError, 11003) +/** A internal error occurred in the external ModSdk process that we were unable to resolve. */ +EOS_RESULT_VALUE(EOS_Mods_ToolInternalError, 11004) +/** A IPC failure occurred in the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_IPCFailure, 11005) +/** A invalid IPC response received in the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_InvalidIPCResponse, 11006) +/** A URI Launch failure occurred in the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_URILaunchFailure, 11007) +/** Attempting to perform an action with a mod that is not installed. This error comes from the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_ModIsNotInstalled, 11008) +/** Attempting to perform an action on a game that the user doesn't own. This error comes from the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_UserDoesNotOwnTheGame, 11009) +/** Invalid result of the request to get the offer for the mod. This error comes from the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_OfferRequestByIdInvalidResult, 11010) +/** Could not find the offer for the mod. This error comes from the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_CouldNotFindOffer, 11011) +/** Request to get the offer for the mod failed. This error comes from the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_OfferRequestByIdFailure, 11012) +/** Request to purchase the mod failed. This error comes from the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_PurchaseFailure, 11013) +/** Attempting to perform an action on a game that is not installed or is partially installed. This error comes from the external ModSdk process. */ +EOS_RESULT_VALUE(EOS_Mods_InvalidGameInstallInfo, 11014) +/** Failed to get manifest location. Either the ModSdk configuration file is missing or the manifest location is empty */ +EOS_RESULT_VALUE(EOS_Mods_CannotGetManifestLocation, 11015) +/** Attempting to perform an action with a mod that does not support the current operating system. */ +EOS_RESULT_VALUE(EOS_Mods_UnsupportedOS, 11016) + +/** An unexpected error that we cannot identify has occurred. */ +EOS_RESULT_VALUE_LAST(EOS_UnexpectedError, 0x7FFFFFFF) diff --git a/Unlocker/src/platforms/origin/Origin.cpp b/Unlocker/src/platforms/origin/Origin.cpp new file mode 100644 index 0000000..a486503 --- /dev/null +++ b/Unlocker/src/platforms/origin/Origin.cpp @@ -0,0 +1,174 @@ +#include "pch.h" +#include "Origin.h" +#include "origin_hooks.h" +#include "util.h" + +// Can't include it globally because it pollutes global namespace. +// More specifically, the `interface` name is colliding with steam hooks +#pragma warning(push) // Disable 3rd party library warnings +#pragma warning(disable: ALL_CODE_ANALYSIS_WARNINGS) +#include +#include +#pragma warning(pop) + +using nlohmann::json; + +constexpr auto url = "https://raw.githubusercontent.com/acidicoala/public-entitlements/main/origin/v1/entitlements.json"; + +const auto XML_PATH = getWorkingDirPath() / "cache" / "origin-entitlements.xml"; +const auto ETAG_PATH = getWorkingDirPath() / "cache" / "origin-entitlements.etag"; + +struct Entitlement +{ + string entitlementTag; + string entitlementType; + string groupName; + string productId; +}; + +void from_json(const json& j, Entitlement& p) +{ + j["entitlementTag"].get_to(p.entitlementTag); + j["entitlementType"].get_to(p.entitlementType); + j["groupName"].get_to(p.groupName); + j["productId"].get_to(p.productId); +} + +void fetchEntitlements() +{ + logger->debug("Fetching Origin entitlements"); + + auto xml = readFileContents(XML_PATH.string()); + auto etag = readFileContents(ETAG_PATH.string()); + + // If the file is empty, then reset etag + if(xml.empty()) + etag = ""; + + cpr::Response r = cpr::Get( + cpr::Url{ url }, + cpr::Header{ {"If-None-Match", etag} }, + cpr::Timeout{ 3 * 1000 } // 3s + ); + + if(r.status_code == 304) + { + logger->debug("Cached Origin entitlements have not changed"); + return; + } + + if(r.status_code != 200) + { + logger->error("Failed to fetch Origin entitlements: {} - {}", r.error.code, r.error.message); + return; + } + + vector jsonEntitlements; + + try + { + // Parse json into our vector + json::parse(r.text, nullptr, true, true).get_to(jsonEntitlements); + } catch(json::exception& ex) + { + logger->error("Error parsing Origin entitlements json: {}", ex.what()); + return; + } + + entitlementsXML.Clear(); + auto pEntitlements = entitlementsXML.NewElement("Entitlements"); + entitlementsXML.InsertFirstChild(pEntitlements); + + int index = 1000000; + for(const auto& e : jsonEntitlements) + { + auto pEntitlement = entitlementsXML.NewElement("Entitlement"); + pEntitlement->SetAttribute("EntitlementTag", e.entitlementTag.c_str()); + pEntitlement->SetAttribute("ItemId", e.productId.c_str()); + pEntitlement->SetAttribute("Group", e.groupName.c_str()); + pEntitlement->SetAttribute("Type", e.entitlementType.c_str()); + pEntitlement->SetAttribute("EntitlementId", index++); + pEntitlement->SetAttribute("Source", "ORIGIN-OIG"); + pEntitlement->SetAttribute("UseCount", 0); + pEntitlement->SetAttribute("Version", 0); + pEntitlement->SetAttribute("ResourceId", ""); + pEntitlement->SetAttribute("LastModifiedDate", "2021-01-01T00:00:00Z"); + pEntitlement->SetAttribute("Expiration", "0000-00-00T00:00:00"); + pEntitlement->SetAttribute("GrantDate", "2021-01-01T00:00:00Z"); + + pEntitlements->InsertEndChild(pEntitlement); + } + + // Make a printer to convert the document object into string + XMLPrinter printer; + entitlementsXML.Print(&printer); + + // Cache entitlements + auto r1 = writeFileContents(XML_PATH, printer.CStr()); + + // Cache etag + auto r2 = writeFileContents(ETAG_PATH, r.header["etag"]); + + if(r1 && r2) + logger->info("Origin entitlements were successfully fetched and cached"); +} + +void readEntitlementsFromFile() +{ + logger->debug("Reading origin entitlements from cache"); + + auto text = readFileContents(XML_PATH.string()); + + if(text.empty()) + { + logger->error("Origin entitlements file is empty"); + return; + } + + auto result = entitlementsXML.Parse(text.c_str()); + if(result != XMLError::XML_SUCCESS) + { + logger->error("Failed to parse entitlements xml file"); + return; + } + + logger->info("Origin entitlements were successfully read from file"); +} + +void Origin::init() +{ + if(initialized || handle == NULL) + return; + + logger->debug("Initializing Origin platform"); + + // Execute blocking operations in a new thread + std::thread fetchingThread([]{ + logger->debug("Entitlement fetching thread started"); + fetchEntitlements(); + readEntitlementsFromFile(); + logger->debug("Entitlement fetching thread finished"); + }); + fetchingThread.detach(); + + installDetourHook(hooks, encrypt, mangled_encrypt); + + logger->info("Origin platform was initialized"); + initialized = true; +} + +void Origin::shutdown() +{ + if(!initialized) + return; + + logger->debug("Shutting down Origin platform"); + + for(auto& hook : hooks) + { + hook->unHook(); + } + hooks.clear(); + + logger->debug("Origin platform was shut down"); +} diff --git a/Unlocker/src/platforms/origin/Origin.h b/Unlocker/src/platforms/origin/Origin.h new file mode 100644 index 0000000..f8f18f3 --- /dev/null +++ b/Unlocker/src/platforms/origin/Origin.h @@ -0,0 +1,16 @@ +#pragma once + +#include "platforms/BasePlatform.h" +#include "util.h" + +class Origin : public BasePlatform +{ +public: + using BasePlatform::BasePlatform; + + inline static Hooks hooks; + + void init() override; + void shutdown() override; +}; + diff --git a/Unlocker/src/platforms/origin/origin_hooks.cpp b/Unlocker/src/platforms/origin/origin_hooks.cpp new file mode 100644 index 0000000..bf46d52 --- /dev/null +++ b/Unlocker/src/platforms/origin/origin_hooks.cpp @@ -0,0 +1,73 @@ +#include "pch.h" +#include "origin_hooks.h" +#include "platforms/origin/Origin.h" + +XMLDocument entitlementsXML; + +auto getOriginConfig() +{ + return config->platforms["Origin"]; +} + +bool isEntitlementBlacklisted(XMLElement* pEntitlement) +{ + return vectorContains(config->platforms["Origin"].blacklist, string(pEntitlement->FindAttribute("ItemId")->Value())); + +} +string* __fastcall encrypt(PARAMS(void* aes, string* message)) +{ + do + { + XMLDocument xmlDoc; + if(xmlDoc.Parse(message->c_str()) != XMLError::XML_SUCCESS) + break; + + auto pLSX = xmlDoc.FirstChildElement("LSX"); + if(pLSX == nullptr) + break; + + auto pResponse = pLSX->FirstChildElement("Response"); + if(pResponse == nullptr) + break; + + auto pQueryEntitlementsResponse = pResponse->FirstChildElement("QueryEntitlementsResponse"); + if(pQueryEntitlementsResponse == nullptr) + break; + + logger->info("Intercepted QueryEntitlementsResponse"); + + // Origin magic happens here + + // First filter out blacklisted DLCs from the legit response + auto pEntitlement = pQueryEntitlementsResponse->FirstChildElement("Entitlement"); + while(pEntitlement != nullptr) + { + if(isEntitlementBlacklisted(pEntitlement)) + pQueryEntitlementsResponse->DeleteChild(pEntitlement); + pEntitlement = pEntitlement->NextSiblingElement("Entitlement"); + } + + // Insert our entitlements into the original response + pEntitlement = entitlementsXML.FirstChildElement("Entitlements")->FirstChildElement("Entitlement"); + while(pEntitlement != nullptr) + { + // Have to make a copy because TinyXML2 doesn't allow insertion of elements from another doc... + if(!isEntitlementBlacklisted(pEntitlement)) + pQueryEntitlementsResponse->InsertEndChild(pEntitlement->DeepClone(&xmlDoc)); + pEntitlement = pEntitlement->NextSiblingElement("Entitlement"); + } + + XMLPrinter printer; + xmlDoc.Print(&printer); + *message = printer.CStr(); // copy constructor + + logger->info("Modified response: \n{}", printer.CStr()); + } while(false); + + auto result = PLH::FnCast( + BasePlatform::trampolineMap[mangled_encrypt], + encrypt + )(ARGS(aes, message)); + + return result; +} diff --git a/Unlocker/src/platforms/origin/origin_hooks.h b/Unlocker/src/platforms/origin/origin_hooks.h new file mode 100644 index 0000000..3830ed6 --- /dev/null +++ b/Unlocker/src/platforms/origin/origin_hooks.h @@ -0,0 +1,14 @@ +#pragma once +#include "util.h" +#include "hook_util.h" + +constexpr auto mangled_encrypt = "?encrypt@SimpleEncryption@Crypto@Services@Origin@@QAE?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@ABV56@@Z"; + +using namespace tinyxml2; + +extern XMLDocument entitlementsXML; + +// The demangled signature is this: +// std::string __thiscall Origin::Services::Crypto::SimpleEncryption::encrypt(std::string const &) +// But it doesn't work. It needs strings to be pointers and some mystery argument. Why? +string* __fastcall encrypt(PARAMS(void* aes, string* message)); diff --git a/Unlocker/src/platforms/steam/Steam.cpp b/Unlocker/src/platforms/steam/Steam.cpp new file mode 100644 index 0000000..c5762da --- /dev/null +++ b/Unlocker/src/platforms/steam/Steam.cpp @@ -0,0 +1,45 @@ +#include "pch.h" +#include "Steam.h" +#include "steam_hooks.h" +#include "constants.h" + +#define HOOK(FUNC) installDetourHook(hooks, FUNC, #FUNC) + +void Steam::init() +{ + if(initialized || handle == NULL) + return; + + logger->debug("Initializing Steam platform"); + + HOOK(SteamInternal_FindOrCreateUserInterface); + HOOK(SteamInternal_CreateInterface); + HOOK(SteamApps); + HOOK(SteamClient); + + logger->info("Steam platform was initialized"); + initialized = true; +} + +void Steam::shutdown() +{ + if(!initialized) + return; + + logger->debug("Shutting down Steam platform"); + + + for(auto& hook : hooks) + { + try + { + hook->unHook(); + } catch(std::exception& e) + { + logger->error("Failed to unhook: {}", e.what()); + } + } + hooks.clear(); + + logger->debug("Steam platform was shut down"); +} diff --git a/Unlocker/src/platforms/steam/Steam.h b/Unlocker/src/platforms/steam/Steam.h new file mode 100644 index 0000000..4ea930d --- /dev/null +++ b/Unlocker/src/platforms/steam/Steam.h @@ -0,0 +1,14 @@ +#pragma once +#include "../BasePlatform.h" +#include "steamtypes.h" + +class Steam : public BasePlatform +{ +public: + using BasePlatform::BasePlatform; + + inline static Hooks hooks; + + void init() override; + void shutdown() override; +}; diff --git a/Unlocker/src/platforms/steam/steam_hooks.cpp b/Unlocker/src/platforms/steam/steam_hooks.cpp new file mode 100644 index 0000000..5fc41ac --- /dev/null +++ b/Unlocker/src/platforms/steam/steam_hooks.cpp @@ -0,0 +1,238 @@ +#include "pch.h" +#include "steam_hooks.h" +#include "hook_util.h" +#include "steam_ordinals.h" +#include "platforms/steam/Steam.h" + +auto getSteamConfig() +{ + return config->platforms["Steam"]; +} + +// forward declaration +void hookVirtualFunctions(void* interface, string version); + +///////////////////////// +/// Virtual functions /// +///////////////////////// + +bool __fastcall ISteamApps_BIsSubscribedApp(PARAMS(AppId_t appID)) +{ + // Steam magic happens here + auto subscribed = !vectorContains(getSteamConfig().blacklist, std::to_string(appID)); + logger->info("\tISteamApps_BIsSubscribedApp -> App ID: {}, Subscribed: {}", appID, subscribed); + return subscribed; +} + +bool __fastcall ISteamApps_BIsDlcInstalled(PARAMS(AppId_t appID)) +{ + // Steam magic happens here (2) + auto installed = !vectorContains(getSteamConfig().blacklist, std::to_string(appID)); + logger->info("\tISteamApps_BIsDlcInstalled -> App ID: {}, Installed: {}", appID, installed); + return installed; +} + +int __fastcall ISteamApps_GetDLCCount(PARAMS()) +{ + logger->debug("ISteamApps_GetDLCCount"); + + auto result = PLH::FnCast( + BasePlatform::origVFuncMap[STEAM_APPS][ordinalMap[STEAM_APPS]["GetDLCCount"]], + ISteamApps_GetDLCCount + )(ARGS()); + + logger->info("\tDLC count: {}", result); + + return result; +} + +bool __fastcall ISteamApps_BGetDLCDataByIndex(PARAMS(int iDLC, AppId_t* pAppID, bool* pbAvailable, char* pchName, int cchNameBufferSize)) +{ + logger->debug("\tISteamApps_BGetDLCDataByIndex -> index: {}", iDLC); + + auto result = PLH::FnCast( + BasePlatform::origVFuncMap[STEAM_APPS][ordinalMap[STEAM_APPS]["BGetDLCDataByIndex"]], + ISteamApps_BGetDLCDataByIndex + )(ARGS(iDLC, pAppID, pbAvailable, pchName, cchNameBufferSize)); + + // Steam magic happens here (3) + *pbAvailable = !vectorContains(getSteamConfig().blacklist, std::to_string(*pAppID)); + + logger->info("\tDLC Data -> index: {}, App ID: {}, available: {}, name: {}", iDLC, *pAppID, *pbAvailable, pchName); + + return result; +} + +// I'm not sure if we need to hook this +EUserHasLicenseForAppResult __stdcall ISteamUser_UserHasLicenseForApp(PARAMS(CSteamID* steamID, AppId_t appID)) +{ + logger->debug("UserHasLicenseForApp -> appID: {}", appID); + + return EUserHasLicenseForAppResult::k_EUserHasLicenseResultHasLicense; +} + +ISteamApps* __fastcall ISteamClient_GetISteamApps(PARAMS(HSteamUser hSteamUser, HSteamPipe hSteamPipe, const char* version)) +{ + logger->debug("ISteamClient_GetISteamApps -> version: \"{}\"", version); + + auto interface = PLH::FnCast( + BasePlatform::origVFuncMap[STEAM_CLIENT][ordinalMap[STEAM_CLIENT]["GetISteamApps"]], + ISteamClient_GetISteamApps + )(ARGS(hSteamUser, hSteamPipe, version)); + + hookVirtualFunctions(interface, version); + + return interface; +} + +/////////////////////// +// Global functions /// +/////////////////////// + +void* SteamInternal_FindOrCreateUserInterface(HSteamUser hSteamUser, const char* version) +{ + logger->debug("SteamInternal_FindOrCreateUserInterface -> User: {}, Version: \"{}\"", hSteamUser, version); + + auto interface = PLH::FnCast( + BasePlatform::trampolineMap[__func__], + SteamInternal_FindOrCreateUserInterface + )(hSteamUser, version); + + hookVirtualFunctions(interface, version); + + return interface; +} + +void* SteamInternal_CreateInterface(const char* version) +{ + logger->debug("SteamInternal_CreateInterface -> Version: \"{}\"", version); + + auto interface = PLH::FnCast( + BasePlatform::trampolineMap[__func__], + SteamInternal_CreateInterface + )(version); + + hookVirtualFunctions(interface, version); + + return interface; +} + + +// TODO: We don't know the interface version that games expects when it uses unversioned interface. +// For now let's assume earliest supported, but ideally we need to somehow determine exact version, +// since different SteamClient versions have different ordinals for interfaces. + +void* SteamApps() +{ + logger->debug("SteamApps -> Version: {}", "UNVERSIONED"); + + auto interface = PLH::FnCast( + BasePlatform::trampolineMap["SteamApps"], + SteamApps + )(); + + hookVirtualFunctions(interface, STEAM_APPS + "003"); + + return interface; +} + +void* SteamClient() +{ + logger->debug("SteamClient -> Version: {}", "UNVERSIONED"); + + auto interface = PLH::FnCast( + BasePlatform::trampolineMap[STEAM_CLIENT], + SteamClient + )(); + + hookVirtualFunctions(interface, STEAM_CLIENT + "012"); + + return interface; +} + + +void hookVirtualFunctions(void* interface, string version) +{ + logger->debug("hookVirtualFunctions -> interface: 0x{:X}, version: {}", (uint64_t) interface, version); + + if(interface == NULL) // Nothing to hook + return; // This means that the game has tried to use an interface before initializing steam api + + if(startsWith(version, STEAM_CLIENT) && BasePlatform::origVFuncMap.count(STEAM_CLIENT) == 0) + { + logger->info("Hooking SteamClient interface: \"{}\"", version); + + const auto versionNumber = stoi(version.substr(STEAM_CLIENT.length())); + if(versionNumber < 12) + { + logger->error("Not implemented version of SteamUser"); + return; + } + + PLH::VFuncMap redirect = { + { ordinalMap[STEAM_CLIENT]["GetISteamApps"], (uint64_t) ISteamClient_GetISteamApps}, + }; + + Steam::hooks.push_back(make_unique((char*) interface, redirect, &BasePlatform::origVFuncMap[STEAM_CLIENT])); + if(!Steam::hooks.back()->hook()) + { + logger->error("Failed to hook"); + Steam::hooks.pop_back(); + } + } + + + if(startsWith(version, STEAM_APPS) && BasePlatform::origVFuncMap.count(STEAM_APPS) == 0) + { + logger->debug("Hooking SteamApps interface: \"{}\"", version); + + PLH::VFuncMap redirect = { + {ordinalMap[STEAM_APPS]["BIsSubscribedApp"], (uint64_t) ISteamApps_BIsSubscribedApp}, + {ordinalMap[STEAM_APPS]["BIsDlcInstalled"], (uint64_t) ISteamApps_BIsDlcInstalled}, + }; + const auto versionNumber = stoi(version.substr(STEAM_APPS.length())); + if(versionNumber >= 4) + { + redirect.emplace(ordinalMap[STEAM_APPS]["GetDLCCount"], (uint64_t) ISteamApps_GetDLCCount); + redirect.emplace(ordinalMap[STEAM_APPS]["BGetDLCDataByIndex"], (uint64_t) ISteamApps_BGetDLCDataByIndex); + } + + Steam::hooks.push_back(make_unique((char*) interface, redirect, &BasePlatform::origVFuncMap[STEAM_APPS])); + if(Steam::hooks.back()->hook()) + { + logger->debug("Steam apps interface was successfully hooked"); + } + else + { + logger->error("Failed to hook"); + Steam::hooks.pop_back(); + } + } + + if(startsWith(version, STEAM_USER) && Steam::origVFuncMap.count(STEAM_USER) == 0) + { + logger->debug("Hooking SteamUser interface: \"{}\"", version); + + const auto versionNumber = stoi(version.substr(STEAM_USER.length())); + if(versionNumber < 15) + { + logger->error("Not implemented version of SteamUser"); + return; + } + + PLH::VFuncMap redirect = { + {ordinalMap[STEAM_USER]["UserHasLicenseForApp"], (uint64_t) ISteamUser_UserHasLicenseForApp}, + }; + + Steam::hooks.push_back(make_unique((char*) interface, redirect, &Steam::origVFuncMap[STEAM_USER])); + if(Steam::hooks.back()->hook()) + { + logger->debug("Steam user interface was successfully hooked"); + } + else + { + logger->error("Failed to hook"); + Steam::hooks.pop_back(); + } + } +} diff --git a/Unlocker/src/platforms/steam/steam_hooks.h b/Unlocker/src/platforms/steam/steam_hooks.h new file mode 100644 index 0000000..7677778 --- /dev/null +++ b/Unlocker/src/platforms/steam/steam_hooks.h @@ -0,0 +1,15 @@ +#include "steamtypes.h" + +// TODO: This only works if SteamAPI dll is loaded only once. +// Multiple loaded SteamAPI dlls will result in undefined behaviour. +// One solution is to store all global objects into a map, +// with key being the handle to the dll. + + +// Safe interfaces +void* S_CALLTYPE SteamInternal_FindOrCreateUserInterface(HSteamUser hSteamUser, const char* pszVersion); +void* S_CALLTYPE SteamInternal_CreateInterface(const char* pszVersion); + +// Legacy interfaces +void* S_CALLTYPE SteamApps(); +void* S_CALLTYPE SteamClient(); diff --git a/Unlocker/src/platforms/steam/steam_ordinals.h b/Unlocker/src/platforms/steam/steam_ordinals.h new file mode 100644 index 0000000..5498a64 --- /dev/null +++ b/Unlocker/src/platforms/steam/steam_ordinals.h @@ -0,0 +1,52 @@ +#pragma once +#include "util.h" + +// IMPORTANT: Since this files directly defines members, +// it can only be included once. + +const auto STEAM_APPS = string("STEAMAPPS_INTERFACE_VERSION"); +const auto STEAM_CLIENT = string("SteamClient"); +const auto STEAM_USER = string("SteamUser"); + +typedef map VFuncOrdinalMap; +typedef map InterfaceMap; + +// https://github.com/SteamRE/open-steamworks/tree/master/Steam4NET2/Steam4NET2/autogen +// Maps interfaces to their function ordinals +InterfaceMap ordinalMap = { + {STEAM_APPS, { // This is a rather stable interface + {"BIsSubscribedApp", 6}, // [002 - 008]. Missing in [001] + {"BIsDlcInstalled", 7}, // [003 - 008]. Missing in [001 - 002] + {"GetDLCCount", 10}, // [004 - 008]. Missing in [001 - 003] + {"BGetDLCDataByIndex", 11} // [004 - 008]. Missing in [001 - 003] + }}, + {STEAM_CLIENT, { // TODO: Patche the ordinal dynamically? + /* + 001: N/A. + ... + 006: 16 + 007: 18 + 008: 15 + 009: 16 + ... + 011: 16 + 012: 15 + ... + 020: 15 + */ + {"GetISteamApps", 15} // Missing in [001 - 005] + }}, + {STEAM_USER, { // Do we really need it anyway? + /* + 001: N/A + ... + 012: 15 + 013: 16 + ... + 015: 17 + ... + 021: 17 + */ + {"UserHasLicenseForApp", 17} // Missing in [001 - 011]. + }} +}; diff --git a/Unlocker/src/platforms/steam/steamtypes.h b/Unlocker/src/platforms/steam/steamtypes.h new file mode 100644 index 0000000..c2f62e9 --- /dev/null +++ b/Unlocker/src/platforms/steam/steamtypes.h @@ -0,0 +1,203 @@ +//========= Copyright 1996-2008, Valve LLC, All rights reserved. ============ +// +// Purpose: +// +//============================================================================= + +#ifndef STEAMTYPES_H +#define STEAMTYPES_H +#ifdef _WIN32 +#pragma once +#endif + +#define S_CALLTYPE __cdecl + +// Steam-specific types. Defined here so this header file can be included in other code bases. +#ifndef WCHARTYPES_H +typedef unsigned char uint8; +#endif + +#if defined( __GNUC__ ) && !defined(POSIX) + #if __GNUC__ < 4 + #error "Steamworks requires GCC 4.X (4.2 or 4.4 have been tested)" + #endif + #define POSIX 1 +#endif + +#if defined(__x86_64__) || defined(_WIN64) || defined(__aarch64__) +#define X64BITS +#endif + +// Make sure VALVE_BIG_ENDIAN gets set on PS3, may already be set previously in Valve internal code. +#if !defined(VALVE_BIG_ENDIAN) && defined(_PS3) +#define VALVE_BIG_ENDIAN +#endif + +typedef unsigned char uint8; +typedef signed char int8; + +#if defined( _WIN32 ) + +typedef __int16 int16; +typedef unsigned __int16 uint16; +typedef __int32 int32; +typedef unsigned __int32 uint32; +typedef __int64 int64; +typedef unsigned __int64 uint64; + +typedef int64 lint64; +typedef uint64 ulint64; + +#ifdef X64BITS +typedef __int64 intp; // intp is an integer that can accomodate a pointer +typedef unsigned __int64 uintp; // (ie, sizeof(intp) >= sizeof(int) && sizeof(intp) >= sizeof(void *) +#else +typedef __int32 intp; +typedef unsigned __int32 uintp; +#endif + +#else // _WIN32 + +typedef short int16; +typedef unsigned short uint16; +typedef int int32; +typedef unsigned int uint32; +typedef long long int64; +typedef unsigned long long uint64; + +// [u]int64 are actually defined as 'long long' and gcc 64-bit +// doesn't automatically consider them the same as 'long int'. +// Changing the types for [u]int64 is complicated by +// there being many definitions, so we just +// define a 'long int' here and use it in places that would +// otherwise confuse the compiler. +typedef long int lint64; +typedef unsigned long int ulint64; + +#ifdef X64BITS +typedef long long intp; +typedef unsigned long long uintp; +#else +typedef int intp; +typedef unsigned int uintp; +#endif + +#endif // else _WIN32 + +#ifdef API_GEN +# define STEAM_CLANG_ATTR(ATTR) __attribute__((annotate( ATTR ))) +#else +# define STEAM_CLANG_ATTR(ATTR) +#endif + +#define STEAM_METHOD_DESC(DESC) STEAM_CLANG_ATTR( "desc:" #DESC ";" ) +#define STEAM_IGNOREATTR() STEAM_CLANG_ATTR( "ignore" ) +#define STEAM_OUT_STRUCT() STEAM_CLANG_ATTR( "out_struct: ;" ) +#define STEAM_OUT_STRING() STEAM_CLANG_ATTR( "out_string: ;" ) +#define STEAM_OUT_ARRAY_CALL(COUNTER,FUNCTION,PARAMS) STEAM_CLANG_ATTR( "out_array_call:" #COUNTER "," #FUNCTION "," #PARAMS ";" ) +#define STEAM_OUT_ARRAY_COUNT(COUNTER, DESC) STEAM_CLANG_ATTR( "out_array_count:" #COUNTER ";desc:" #DESC ) +#define STEAM_ARRAY_COUNT(COUNTER) STEAM_CLANG_ATTR( "array_count:" #COUNTER ";" ) +#define STEAM_ARRAY_COUNT_D(COUNTER, DESC) STEAM_CLANG_ATTR( "array_count:" #COUNTER ";desc:" #DESC ) +#define STEAM_BUFFER_COUNT(COUNTER) STEAM_CLANG_ATTR( "buffer_count:" #COUNTER ";" ) +#define STEAM_OUT_BUFFER_COUNT(COUNTER) STEAM_CLANG_ATTR( "out_buffer_count:" #COUNTER ";" ) +#define STEAM_OUT_STRING_COUNT(COUNTER) STEAM_CLANG_ATTR( "out_string_count:" #COUNTER ";" ) +#define STEAM_DESC(DESC) STEAM_CLANG_ATTR("desc:" #DESC ";") +#define STEAM_CALL_RESULT(RESULT_TYPE) STEAM_CLANG_ATTR("callresult:" #RESULT_TYPE ";") +#define STEAM_CALL_BACK(RESULT_TYPE) STEAM_CLANG_ATTR("callback:" #RESULT_TYPE ";") +#define STEAM_FLAT_NAME(NAME) STEAM_CLANG_ATTR("flat_name:" #NAME ";") + +const int k_cubSaltSize = 8; +typedef uint8 Salt_t[ k_cubSaltSize ]; + +//----------------------------------------------------------------------------- +// GID (GlobalID) stuff +// This is a globally unique identifier. It's guaranteed to be unique across all +// racks and servers for as long as a given universe persists. +//----------------------------------------------------------------------------- +// NOTE: for GID parsing/rendering and other utils, see gid.h +typedef uint64 GID_t; + +const GID_t k_GIDNil = 0xffffffffffffffffull; + +// For convenience, we define a number of types that are just new names for GIDs +typedef uint64 JobID_t; // Each Job has a unique ID +typedef GID_t TxnID_t; // Each financial transaction has a unique ID + +const GID_t k_TxnIDNil = k_GIDNil; +const GID_t k_TxnIDUnknown = 0; + +const JobID_t k_JobIDNil = 0xffffffffffffffffull; + +// this is baked into client messages and interfaces as an int, +// make sure we never break this. +typedef uint32 PackageId_t; +const PackageId_t k_uPackageIdInvalid = 0xFFFFFFFF; + +typedef uint32 BundleId_t; +const BundleId_t k_uBundleIdInvalid = 0; + +// this is baked into client messages and interfaces as an int, +// make sure we never break this. +typedef uint32 AppId_t; +const AppId_t k_uAppIdInvalid = 0x0; + +typedef uint64 AssetClassId_t; +const AssetClassId_t k_ulAssetClassIdInvalid = 0x0; + +typedef uint32 PhysicalItemId_t; +const PhysicalItemId_t k_uPhysicalItemIdInvalid = 0x0; + + +// this is baked into client messages and interfaces as an int, +// make sure we never break this. AppIds and DepotIDs also presently +// share the same namespace, but since we'd like to change that in the future +// I've defined it seperately here. +typedef uint32 DepotId_t; +const DepotId_t k_uDepotIdInvalid = 0x0; + +// RTime32 +// We use this 32 bit time representing real world time. +// It offers 1 second resolution beginning on January 1, 1970 (Unix time) +typedef uint32 RTime32; + +typedef uint32 CellID_t; +const CellID_t k_uCellIDInvalid = 0xFFFFFFFF; + +// handle to a Steam API call +typedef uint64 SteamAPICall_t; +const SteamAPICall_t k_uAPICallInvalid = 0x0; + +typedef uint32 AccountID_t; + +typedef uint32 PartnerId_t; +const PartnerId_t k_uPartnerIdInvalid = 0; + +// ID for a depot content manifest +typedef uint64 ManifestId_t; +const ManifestId_t k_uManifestIdInvalid = 0; + +// ID for cafe sites +typedef uint64 SiteId_t; +const SiteId_t k_ulSiteIdInvalid = 0; + +// Party Beacon ID +typedef uint64 PartyBeaconID_t; +const PartyBeaconID_t k_ulPartyBeaconIdInvalid = 0; + + +// Custom additions +typedef int32 HSteamUser; +typedef int32 HSteamPipe; +typedef void ISteamApps; +typedef void CSteamID; // some struct, nobody cares + +// results from UserHasLicenseForApp +enum class EUserHasLicenseForAppResult +{ + k_EUserHasLicenseResultHasLicense = 0, // User has a license for specified app + k_EUserHasLicenseResultDoesNotHaveLicense = 1, // User does not have a license for the specified app + k_EUserHasLicenseResultNoAuth = 2, // User has not been authenticated +}; + + +#endif // STEAMTYPES_H diff --git a/Unlocker/src/platforms/ubisoft/Ubisoft.cpp b/Unlocker/src/platforms/ubisoft/Ubisoft.cpp new file mode 100644 index 0000000..b4ae50d --- /dev/null +++ b/Unlocker/src/platforms/ubisoft/Ubisoft.cpp @@ -0,0 +1,36 @@ +#include "pch.h" +#include "Ubisoft.h" +#include "ubisoft_hooks.h" + +#define HOOK(FUNC) installDetourHook(hooks, FUNC, #FUNC); + +void Ubisoft::init() +{ + if(initialized || handle == NULL) + return; + + logger->debug("Initializing Ubisoft platform"); + + HOOK(UPC_Init); + HOOK(UPC_ProductListGet); + HOOK(UPC_ProductListFree); + + logger->info("Ubisoft platform was initialized"); + initialized = true; +} + +void Ubisoft::shutdown() +{ + if(!initialized) + return; + + logger->debug("Shutting down Ubisoft platform"); + + for(auto& hook : hooks) + { + hook->unHook(); + } + hooks.clear(); + + logger->debug("Ubisoft platform was shut down"); +} diff --git a/Unlocker/src/platforms/ubisoft/Ubisoft.h b/Unlocker/src/platforms/ubisoft/Ubisoft.h new file mode 100644 index 0000000..db8c8e7 --- /dev/null +++ b/Unlocker/src/platforms/ubisoft/Ubisoft.h @@ -0,0 +1,13 @@ +#pragma once +#include "platforms/BasePlatform.h" +class Ubisoft : public BasePlatform +{ +public: + using BasePlatform::BasePlatform; + + inline static Hooks hooks; + + void init() override; + void shutdown() override; +}; + diff --git a/Unlocker/src/platforms/ubisoft/ubisoft_hooks.cpp b/Unlocker/src/platforms/ubisoft/ubisoft_hooks.cpp new file mode 100644 index 0000000..71a927e --- /dev/null +++ b/Unlocker/src/platforms/ubisoft/ubisoft_hooks.cpp @@ -0,0 +1,140 @@ +#include "pch.h" +#include "ubisoft_hooks.h" +#include "platforms/ubisoft/Ubisoft.h" + +#define GET_PROXY_FUNC(FUNC) \ + static auto proxyFunc = PLH::FnCast(BasePlatform::trampolineMap[#FUNC], FUNC); + +using namespace UPC; + +vector products; + +vector dlcs; +vector items; + + +string productTypeToString(ProductType type) +{ + switch(type) + { + case ProductType::App: + return "App"; + case ProductType::DLC: + return "DLC"; + case ProductType::Item: + return "Item"; + default: + return "Unexpected Type"; + } +} + +int UPC_Init(unsigned int version, unsigned int appID) +{ + logger->info("{} -> version: {}, appid: {}", __func__, version, appID); + + products.push_back(Product(appID, ProductType::App)); + for(auto& dlc : dlcs) + { + products.push_back(Product(dlc, ProductType::DLC)); + } + + for(auto& item : items) + { + products.push_back(Product(item, ProductType::Item)); + } + + GET_PROXY_FUNC(UPC_Init); + + return proxyFunc(version, appID); +} + +int UPC_ProductListFree(void* context, ProductList* inProductList) +{ + logger->debug(__func__); + if(inProductList) + { + for(unsigned i = 0; i < inProductList->length; ++i) + { + delete inProductList->data[i]; + } + + delete[] inProductList->data; + } + + delete inProductList; + return 0; +} + +void ProductListGetCallback(unsigned long arg1, void* data) +{ + logger->debug("{} -> arg1: {}, data: {}", arg1, data); + + auto callbackContainer = (CallbackContainer*) data; + + logger->debug("Legit product list:"); + + vector missingProducts; + auto list = callbackContainer->legitProductList; + for(uint32_t i = 0; i < list->length; i++) + { + auto product = list->data[i]; + + logger->debug( + "\tApp ID: {}, Type: {}, Mystery1: {}, Mystery2: {}, Always0: {}, Always3: {}", + product->appid, product->type, product->mystery1, product->mystery2, product->always_0, product->always_3 + ); + + if(!(vectorContains(dlcs, product->appid) || vectorContains(items, product->appid))) + if(product->type != ProductType::App) + missingProducts.push_back(product); + } + + if(missingProducts.size() != 0) + logger->warn("Some of the legitimately owned products are missing from the config: "); + + for(const auto& missingProduct : missingProducts) + { + logger->warn("\tApp ID: {}, Type: {}", missingProduct->appid, productTypeToString(missingProduct->type)); + } + + // Free the legit product list + GET_PROXY_FUNC(UPC_ProductListFree); + proxyFunc(callbackContainer->context, callbackContainer->legitProductList); + + callbackContainer->originalCallback(arg1, callbackContainer->callbackData); + logger->debug("Game callback was called"); + + delete callbackContainer; +} + +int UPC_ProductListGet( + void* context, + char* inOptUserIdUtf8, + unsigned int filter, + UPC::ProductList** outProductList, + UPC::UplayCallback callback, + void* callbackData +) +{ + logger->debug("{}", __func__); + + auto productList = new ProductList(); + productList->data = new Product * [products.size()]; + for(unsigned int i = 0; i < products.size(); i++) + { + productList->data[i] = new Product(products.at(i)); + } + + productList->length = (uint32_t) products.size(); + *outProductList = productList; + + auto callbackContainer = new CallbackContainer{ + context, + callback, + callbackData + }; + + GET_PROXY_FUNC(UPC_ProductListGet); + return proxyFunc(context, inOptUserIdUtf8, filter, &callbackContainer->legitProductList, ProductListGetCallback, callbackContainer); +} + diff --git a/Unlocker/src/platforms/ubisoft/ubisoft_hooks.h b/Unlocker/src/platforms/ubisoft/ubisoft_hooks.h new file mode 100644 index 0000000..c531dc5 --- /dev/null +++ b/Unlocker/src/platforms/ubisoft/ubisoft_hooks.h @@ -0,0 +1,65 @@ +#pragma once + +#include "hook_util.h" + +namespace UPC +{ + +enum class ProductType +{ + App = 1, + DLC = 2, + Item = 4 +}; + +struct Product +{ + Product(uint32_t appid, ProductType type) + { + this->appid = appid; + this->type = type; + this->mystery1 = type == ProductType::Item ? 4 : 1; + this->mystery2 = type == ProductType::Item ? 1 : 3; + } + + unsigned int appid; + ProductType type; + unsigned int mystery1; + unsigned int always_3 = 3; + unsigned int always_0 = 0; + unsigned int mystery2; +}; + +struct ProductList +{ + unsigned int length = 0; + unsigned int padding = 0; // What is this? offset? + Product** data = NULL; // Array of pointers +}; + +typedef void (*UplayCallback)(unsigned long, void*); + + +struct CallbackContainer +{ + void* context = NULL; + UplayCallback originalCallback = NULL; + void* callbackData = NULL; + ProductList* legitProductList = NULL; +}; + +} + +extern vector dlcs; +extern vector items; + +int UPC_Init(unsigned int version, unsigned int appid); +int UPC_ProductListFree(void* context, UPC::ProductList* inProductList); +int UPC_ProductListGet( + void* context, + char* inOptUserIdUtf8, + unsigned int filter, + UPC::ProductList** outProductList, + UPC::UplayCallback callback, + void* callbackData +); diff --git a/build_installer.bat b/build_installer.bat new file mode 100644 index 0000000..0d1d0ae --- /dev/null +++ b/build_installer.bat @@ -0,0 +1,8 @@ +cd /D "%~dp0" + +rem This script compiles Koalageddon installer using Inno Setup + +iscc inno_setup.iss + +pause +exit diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c2a8ae85eab4766e02bccdc896ddd7780da30e56 GIT binary patch literal 69313 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Ξ0va3~`(KK#WQry@EhI)%wrojlL z_tr}J$4YN7p}i++a~O zO1AIw<|11?`;VC~jZ^5AGcI(xOVt#q7~r&HWE5*$h-^wA@WL zW`UPo+2G5W@#F5+*sWO8P-_wEA2jWDzqp4L52v+_VduLKo99MIg?hRfrYW~v zW;Bco%&~MeJ9tj-o<*18_M<9Q8Tw#+)U#Jw{wsp~j+-&t?g~m46YR_6Hrg?+D@UPK zkWYNfjS<=^cVvUrS7jIoFI~+NQbRKvjt%Yc*UO17BEyVXKrdBwMYC~AY|~VG#~HJ- zN1cwdKJL0=eJKxF+IWkDf8Fb+xGUUr!fnODQ&*iz%lcflcE>*+&9@)cciM`vS{Bou z+qfi!HZx058WR%Qz|um+bylaETTW;!V%zRa*t(GGMpTk?syF)R}gC`;f7Bb)^RE z+T6R|<16pc*m~P(T2y1pQFbZep2J2ws@E{XLZ;Hxf8M6hX6(=o7Nb0%PK#|}YPyU3 zK_iQ%-RU0Zxw3~iGtE)r61l2kEb@UdPZVEl4XXOzS zr_Q|15B~MBL#J!E4&Sskoam9`k~N}I+cC>F5BKe;=5$=`q;+}Om})3N&&_p=&y>fl z`Y8Bc%DisyzRZ;j{Rw`~GrFiG>#8M3RPFLfFbV#Fx){21I zEas|GbgcK;*bPw|UHgY++oumW@Bg-~)`UBMeNfd^d!@3}-)#(jaIlfG+38i96FSMv zB0px9RaSUVwrW&-T!wF_?9VF6*81C{I;vj@PkBF4QLBvJ5oNtiQyQn0a&<}y4PZfL z3GdQYUsjzLdp3s%E*s zG~&dB%{uy4&Ni1O+_=!M>JIBB$(5$RjUS(CZ8*WtrSs>0inq3UIBnfGLr3rUvpz;K zihXwVJXQ6@qsQr;jOwf?-Jow$RM7N0F4L;p9LQbgnxmfeI(qNf_I<>iPyo-~`*sCvLj`%&5a?%LPU=lkINz?iRoQPP;SP{B;J--h9hIsm+C* z%Dw%Krk`%2q5eGhNapl;9V_J6AHLT}{b?oR+%>7;F78#++I}+*D2=w97h3Xt`!aMuWoE;Mq z4Qnj*lr1rtnf$os@3nyPFIzmPrRJnZA$og{|}8;wS4SWj%QXg_KiRrR>F&Sg={zlW(=f~KPDfLle^|V+NuMMa%V`51m8kP+S`JEZ)ayTK#D(K3 zf0Vu$ql|F3+}C5?=}t1d-TdR?XErg>%Qv@QdB*HXj1o_)dHA&_u+-V$5*obKsJU~- zOT~5S*K`+T-8_FK*GgXuv{0+_sGG;g;m&6M{=F5V_hm(_X!)jrMd#c#S)Qq#lw-!+ zJ+5gpskD)vm!p>dpYMlmt8pdOBlltT(>CXub{4nTi4VSDjkSOiel65j9*r-MoNp};qF&b+Xc&%y2eLVY8!9d?Z|9H zn^{JQBTg{?n|}tDHmY6e)QFke6OOOlx9FLxyUg!n_V5!57jOFe+rM4urufdZYfj4F zb*(=#-RoB(v=R>N3ClS%#OTlM4MY8z|4Ah!w@M01&nq3zu6Jovk84k#J`3$VD;V_m zVq?T6x&5iFtC8GD{o`^I8@(v~g)ydwSo7UCB7?7) zKdf9K_uQp7IcIiloL}os>Z4Cdec$|%)1XuL)-tVaSN7jCb||lUvVlg+%TE3QVQOlB zYmRL+wNZDam}^lzRNk4g-9d>}0hh)o57umPzR#1sTW+-IV3w1}qDm+H| zH>q8vGo7r?gxsvLS~vY;Y?9!7V06lnYnwZ4+&(|9UR;KXm+n7Gxzk-QrA9Ajw~d9{ zBSI^b>K_}Lpe}2vu5WVxeKVuKGgf8A?tXXjNkU4^<>=&=Z##@u$!?n@*q&b7A#RR) z^kJXOaAl`#`=oG_h)q3GBYLl|_?Hq3+LL9{-stiS71k%@%#E!mMh zv-fG%u*~k>MV53s^j@Vi?`Q7Z)xPDW?8O`387oBv9C>U!?N0Zd7cO2LlltuFi=p@W zdo?yvuGPfRNTch{)=k$Nt#3Z1t6SZwi&r;xY(A(_pjo!=7~=z)ISteHXFiJWlG4;@ z+R8^2?G)I+o;{~uRx~fIV76Xxj9l5R{&_3w1{b#O>(zQdnswa9&*e4uY;YVrbf{b9 z$^oNC`}r={jXgE~R=+zcY+t(>^&JA;7M3xYx_#fi6E|*{Z(M? z0oj6jPNlhphV?2fH@&fG`YS2fo))8azegE!tV`+7Q)?YpaKn$IzBZr(M=?SZM@x}oZOD|8m_ zjd8A$FnUnP2){$RG0XLDUmd4+^Tp7!OTCX-gdYg7(en?9c+}RTfqm?$*>%da>d@ig zyB9Z4pFKOLzJrru%;2?SlQI-?_wIeYF){h|n0wAE>MgOBEoZv5(&qLHay;+IwyA4b z2AO{Dpy;AG_?f?{^5T>`?XHv(YNYju=@_{6u~TcSnG;)f95vsm<(BgoFE&l|tC$oT z7B<C|nuHN>NLPYMGYAalQ3`eF1rfD9kWY<{O6untyT7N_C zz`e&>zqxBwZk);aKCi;IJWbWe^ov{jsp-R%(H~xQ+tO|yXkzoTbq7oVwZqZc}I z6wHZ8QW6VH!NeEX_n{H=b}P(VKO(`W6^j#U>=fBCZCgozUecv=2+ zDeU5t!4FOcG}rskoPWeywySj-5PNeM|fJ+QGrc4vs8St4ZcC9~c;&zj9^#O2;yD z>$)wh->s^v;y(s|RzA{DVTF>c);Q%bMa?>_+QHUn$`--*JT*oq_tAc98Mt-CP|x+~ z=!8FG5;aCnb-U0gBxFWu@07*i2kYW&L#_FVP5Q)b-aP%_LEkoQ+tyv(^hoFJCvPO} znDlV&;nKTX@2^=(P<96e{73a$Ka)r|QlOY)p@xMN~mPRewjkqw^SX;)>qFj^re zu#;=qjR^@$BW@3$*yY%a7j0f_Rki3Je`|0=#HBlTW+vYa9W`ocJC`1xSE}tlm{`U? zF7;wr^MIt|3SAv%tvA(Nr^_DSzH?&e-S9R!|1{irH4y`^qk`Y~gBNyBen0Bb(uf6% z79H+BBCFP48#biB{(w^qaVV^@V~|~w74GFHFS|Bk%eQ9{`}Y!h0*#6C(j(Wcjr2YHpI^tbN04R|ofY)z*5o3Gz>!xuyGZy_usFa=n%AkBAuXph}v1+Va?A zpQo%mf9uwnD;rx{EPJ`}*sI8d10SDm{Cm%y*ICD`cW&Djx<$L^<5FXm*RC|+VE3nU zroVQdsNAm7)Qz#VS8a<;k4umxy^9nAI^_iR(T0#E)ZXL~*l|AZdtYYkuQbpFjEx9N4hkKXaB(*=VKLFVuXm zBRl}W#-PE>$UW3;E)qWwL10eskSwB>Zu3mV15|b zuLpgw&D?Y0k!AgPt!Fgp;_Y(w-JG;x>l3!In^yhU^{2g^KUOQdz~B8v%X7wo)-|&l z)f$;IHGRTxm#Ek~!L$3V4biK8VA`@*ACUm|-hOQoWNEq_wiD!o9;aFe)Wl^4?dWL(RKT^;#IokeB#(Ae)t=XH< zUMJ@}E%A=fnV3AFjLXJLPyX7ub8b7AV^5!M7;YU%O^bAn0Wq+rGZ+nBh2O%!Uou`s_|$m3-H+;gRx28a)?H zSm}6Fi14yRPo=NA^u$@`A|^%j5LkQGuq8ou0o9i}#GZl>Fy+{ciaS#DY-ij~|D={3 z+^6rtu((|OG)B9910S3}`q{hv;@!Keb{O%Z3M7SyjN27;RW%;lJrl}|%(-9HrDF2S zp?CXCJij`0REH#&R?(wOJvR(EIC^Wg-e-@gQSVGV{Y>6uSgUW?dSz&reeGNhbf|=b zmJksxUb+<6w(ZT`3x_3D4cv9T&(T2hWe|W5SCSwlSJbU&Gc2IgVk4>Fk zIpkdGsVQyu+>h7N(RuamX1I0vd!Ab`O=0?~>1=ZE$)&Xq9&KPbqaV8~=Z@R9siKL^ z)UlCTyZ206_Ws1xlll?KY+C$cEwclOd+L6U*zWfDsLkkR?cNOTTParI(qzMOW>!{f zHg4SLrLl2Yy^t$*-kBdpzIpe~&DVFvnlQB3$_tJ_X49rk+ji_YoRXq4yUw`!4$W$K zhi<%);d=km`yC%YeH#AuO3TSdJA)$UFJDzAGjZiw<-~($Hk-d_v?HUUN#lD#jeDPCy{g*7-Vxh#w_Vyx}Pu9I-rAn8sIP_#Sbm{4YUeW#&%6Hgmns{e)&sVwe+Nv7k6{b#| z`fy>)w#;W4o%`5U?IkOfHBEbkYMG#!{$p91#nEetErZHkdfd(WmWSr(UQ^%4Mhg}- z-hWaKar)#EJC({`?sx9Nsn~@}mK=HXy0qE+c1?_yG%Sa22xw_*SN8I%)2NXy4zb)H zxUY0@udqe69gRBo?W-Mst0AmkAMF1ac%XU6YD_u(y-uGzIpxDBw}~!Wm*0HY=E0`u z0|8q9=!G_Jwl;J#OY^Vv&d0?kanpy`W>Jf8k6t^!cFSzRCG*^Q@8nnB7W2N0rn zJWW%V{#og@QjQ%{S5i>jP(J0kN_beOu86d&Vl&s}_E~dAaK>N~=rX=&my50m0+7oo&iLRK8q_4gMJJoU^cYqZV&8 zTAXh#M6|JZeAH)jgT}d`-Og32ab$J9*u9Z&EA%+GtlO|*3;b;wH-D?Ed22}ZrAi7i zbaN>yD~ILh3^m4AID56;+Dk_dFWj2(Hey-JKYO9oF#5r6GOuin znh$PX-P=2Bm=6YYU|S>mW;3G_m(QN9zkmOJwTa1P4Af6;vzdEh+y4E3ILs}7_M_jF zNdMlwolc!PHLj-ZAQL^+D8=say2j_rChJ9}w+het*nNn4*S7OLw>8PJFn*Ib_{O>1 zEe9qIo7U{IpUJ$)>6>p1yEkr^X(!K8S^ILM^>UiFa_H6d`eO{K;qPxnR2%Zt$ai^0 zV~bH`Z5}^)auIym&75Cgc^Za?A;g@DT zS1fgNjq-x=vS(w|k8Ctn-01#KxY5K>@6;}YFM6>v_g%(I=eQmC1y@4mHmOssQ&@N>XZ@-zRBG2!+cx7i z>oDJ}RYi6@eXQ-93Co-H(ayP+c)4T!VIPGilX_;X@N8U0*?v~vt>p)pzf`yrxcuO& zg>38+uNW)y*q!H(+!0dkZZBWoCkNxiuw29b4^A3GEb1TE{PSmn!?6DbH3r$~r)TXg zuV$p7+@fVm$hqJlR{Neu-|AOx`8jt_&z8Pnt#({*InFAsk0QH1rfhQbhNJ#%jXih% zB`kg1{`T4n&7wy=Z}}u#mQej=M}^VB!>;IEx#DB2)ZeFeM%*W}go(amZy$TP*?nZ# z;~F0Xi;ojb8f_Zy8xr#5>C^O_53k$B58doFD7Ra?q47(%Y;n19;|8cD$nK#9Gtp4K znwmObUpvT|){mX*Y_1m)(mbTwkReyvXnIZPVA=Ifib7X2J$=@Am)YLe*KeNu{9ZlT z&ptYC>E~cVlc1^afi&*fmzt^N7 zJ1orp>``aO;js3GGavkE_sDm%Z8?<}dMeEo+0cNuGu5B^m8}_bam&jTwVrpBb2TzE z#t*ns&;K9)@H!uYmTb|Ng^k!3n|}DUps@d9cuWEoOoJYs&HDIktGkEC;I6%IM_6d? z*}ePX?b~O;w(s`8_GY-a)QyPn1d^Y+XX{$|q@_%^_$OgR?1Km0`c$>>UB0!Sf82%& zSg?*CKmOH|!^)kuZ&fpGXQfi+=+cA=R)*f=WxLHECpH%vX;j`ezIKb;Zs$G6=DZ%W zd#(8qm#fOjdt*9Was9iVo?~2im$%w0>a{XUJoo4xCi?^LZuEw{Xgz#I_#T_I_n*Rg z_wLfS@AOvA-79FM+#6t2>$+)^#+N*NKioGw_O5F~!8!8f zi>JF?lUf-W8K)#p@pX#pY_`pinR$L(;&xuRI^lv)CbM4BawD(qZx8jY3cfe&!TgrP zh#ig2^?~d^ZRSk(8a2*bzH9_5aWBgmozF!cE!|#!*oRSBXV#2+wW4{aoaG9_YCTgY zGa)|X;E1&DH>@Z0PwKbF^cg$-CK|F&$Bx!fiHWvx8^X-}K3P_t)-AcXiy8nYieQUC$DQb#yMqb)~rcXKMIy85w z!NKi&I=%6myewJRIlspG`im1jJY2k_#t4(GsqrrkZ`{zP&)m`$7WcqBvqv86{H)i} z7mJ%dei9g;y?g!+rGcAXpIgzxOyg>baMnAc|I^Fs*Q9P8aAH(;*IIGccTGKZZ*Myn zOn2sUYMpp6aOA5K<@)sN*W#V^_E!33uo7~K?VXx>^KSploDYL)u4r6s>=`wc{`*gl z>1N>djB#=PrNF|PgI{xQ!|x4OH! zZd>IEIV}5gl!~fqDRc9~FNV%N(Y21l+^d03ogZJHV4@$stJQ%I8ku(#1(QoPp8GHF z{r=(NaJL#YZcTe1v9Ifg{;pl zw)GA^KR?GsHhOi~VXwA^Bb{r`2hBo$-DO`Twcpm}H@uqE3AwW299s6!-TvFKbh}X{ z_C(j?4+c(~G2?1`J!d10fA$SJ=2kJx<_bw7->O1#r?(1#r>9U%&K@6qE_Mw3!1 z2gcrBv}eBdtmGT_%65wG7Vck`&1yT!+B9Xf!Kj>Bd$zpkXgKdv_^P!z&JO929H6F1 zIXcz!{ohsQo#}3`r)JdqWb)l@LwfhBGihwOAib{cdYi4Mob&0@@N*3-T`WP;(~nL) z+HY&@5MvExj2f)!MrIu~{paAQ#S0gfT6v*oleOzx=+-G?0QR{^BmP3qd0_aMgr1&N zczGxBTuMsq26w8NPdB+T68g708Cxf7Uwbihna}XfiJ{;_UHfS#_U_tw?vcHj78F}&4uxrt za!d|Uyy0tlSx7B^Gr3`hz0KA(9Pr@e{Mds@<>sEq%)A>Im7Q!o@YB1#4<9{JR~dVB zzi{C~laan1uc+9Lcywlm!iSN|dU~9^e7Sl1Qn4!?p)p&e`(j=Cr-?OfaWGJAe9gcA z{+kqDAzqLBd$nray3_FC*W0NsQ0m}x@7|&S)mD|q$<{paUp)SO?4{stIez6whFZL} z%$co}s~vgw^!^Xca7t|0i(RREhh^DW&0M?1bM38uYj^H6+?smo;>9`D49{P?VOcI! zcZFmN@Y-|AXu&RWVfa5e-K!0KZkD>WWB8-Sm?27IX{aW*J#RQW9cbv8!I1B;ZqN>*6etQNzJY9<2 z9+6RF<%M86GsrB$QNIBpkY=nW0kp@dij|Svzt*kzQ68>XO1{C$))rfYWFQ#zUgT8-X43odk@>U{v?YovH^}$9z zz)U(cA%Ri{8LiB6cf|m~!!Z@5!uBtY)(eY^mt1Q%_V#RDTw)Uw6PMJ)M+S^YXU+%0 zua>DooGM!#=G>(FK~n(Dt<|Y|8T4^S$M*H3#iNV2c6QA>9@%iXyGC=U@v_kB9(m^J=4m&GMTa5!MhK#p&wGW+;4k)zk9;tow*TzG?L#Op7wF#`72;?3Fo z`cSqiC~p)h%%r$3x7XeRrVVP~)F5jOX8xen3@YIZd8%m3G4B%A0mggJ%CAW&uf@LW04M-TuWyIX6p39vv zd+!JxD<(HKHkjy$$qBLF{ji;%n(7%x+`5AbM*QXQcA*)g>1M~d;?JKy{Goj|6@XLU zBMzf}@L-~%)QF41z{qH+l|+7Jtmt(vdRoKp`uh4>{~PJ$-IW9vQloG@;5dD=m3EZdPJ3wJr&H97KK(m3vsEqT!=fVcHD^K8x9 zb0D9`sq`lr!bI(O8cksg!{KMlfT2wRp8;QK>m`8oafd+7^8tL$kD2 zTPx+iAI#_i9%^qq*dl-}xZYg4=jP^~7Aoe$p8<%%sg*8z@)uN#^?{6v%1YgPU10jw z`||iV_i`lI5~q{4vfhWkT&ZhcwfyD`l*hg%TF}bcJDtYW&Npy%{f%+%VYzRQg*M0u zWi}n7Fpj;?@2`IV+xdBtK%)>%5u68bCgY}$-=m|W;idorAi)kGQO_|08-K3R7YH8P zlEDEsHld(cZBHEy3=AC2?)fKyt4b!&b8tL6x$FPJ!Y`{S(mIzHSGwcCpYX%KIx30? zw8?&*h7ImIaN)0Cop+y|55@Rf5wVF_nEvB!V4*vIn&o~YfyDYJ;fa3CcJnzWbe*fr zLWjsX0F4R%`pvt4dnZ13ipm-PZnY5*IO90A0MKZBgOxP0xiXyd91}edWBG>lEABx0 zql#iz(L!aG<|H$JL-&M~_x<29K}{5vkg%OJ%YyR&yx4GaI-M%(U0$r`qs`7pO)cy< z?;zd_%7=lOnS_|>_ly#^bWx2vtSZ=a-a-fIHh zoP^DX8Y;zJ_YybH_)P;tmRRo;|G1ggbmN}$9|>r1@ZAl%b%ff>~_akoSKkjP|9VA*&e%J5vpr}Hl?N&Z`(u(PVDkoMD z4e7{%Ii-QAfCup}x#+}Pqj-D-{D$bdaj>x^{VtCKX)b2KCT(l`#IE61KbtD;{!hWt ziraS0aLAOG*}=mjvAesQeo{Z{g}H@ApxMo+8T$i5Be{W9S79-+_Sephqb?r*O;7jk z?+e{kl{r}e3IR^gOqYEMYW2NOO9ztRT$scl>yrSwM#Uzh>m+*dH%sUA4*fSttqo&s z1I)+&CL~|$1YUjW@?u!lRMa&;fy!k=U(9(b`20=KSsLPszPH9ddpp)$bj9{%~8 z@DuXk5&brxv{R87VpWg7bE=q3PEB1|Yz@0po6BLKc7pO+I=utRW%?uE4j_1Fd}kPO zfAwJ12@#Z9%TBx3snOORv{*a3=WIc7l*2#Wj#@8*6ev;a!VPctau5X@tPL3{zNp(< ze^7-rtH+_Akn!1p#b@^jRvh{G;c;J_XVc zsPvH!_u&>4uUgXhs@x^%fMGK=Jq^mZxxIbF?(VLMwa&JOOrBN-coTMqsHqviB%rKY zf%Ri9^tM0N2$MhN=iUm)h?!DN^q?3Ua^fXF=0m*G8ja0u1P2XDh~=U z&fnO>3t6Dy=H}!cevo)JF~y6K|KvsjLslweoDc%K%*C%MlWTe@Duut;xY)sc{rzPC znN2$*G=QR1{I_5xaDuPs`?H)H@1wQ9SwnaW2(swj@FpCU|Jt5Cfz5}>v6($8g_urc zVMD6$5=>zoX` z55OdFqwRXUfcpMb?bq7iPoo!VIofwv3m_B|6cGSnlNGia)Sviji)v<&j4Ja1du01m zFnMML#Y;`9OMym^LI|f1sR@Q4&0zj5pLAx*x0L8L)s5!WQ0$ZWZ@oVkRpHw>?<4cx zirPO#-T2D`$c5wDmb862?RZX#AJS!0VeRvUVxNZJytuG1c3|C;zc5|$eb62J8TOq< zF!}iGo`u&l97i-|VEI$k!RVFsugz2j5RBK4p5^3n8p3+b2c|rr!9UEQ&p$p2_pQf( zi5)X%ygy3b0jncd&sfk0(3QKDf%#yPt9<%L0CkE4kDJi0QANtQlY>L-vz#7?i4<2z zR@PYWK_(aek6g#A=+U=iuC|$tP{H)h8mR0IXLG(X519=GMu@Kcj~nU`Z6B{$#t=vg z;|MfpX6dE9Plz_QIVNnFbFU@h(>m(wNyv}nWXV`l(YS6>LTrt#sT~g)P%HJs2D`Z1 zTya4>Er~&oA?LU7BzY5UY;5C7y~$U#Hqo6;7s=%GbdEI5BY-!6XfcHRGOu{cS3miM zb#nC#R4yx#?~V`gQ#Eac-D{F6L4yYLaCm6Q;s+TfbNETNezz6@32H2m^5|aWzwIYq605Wf%xDWd>i=IfDCbvVo!uSKN> zVx3)&w&47Z39RY%H&-GmHtLFU-iMz+c?5jF=dKQxb)l!sqAQ4}u*Y?8H$E=LhYPfZ z*72IbpMg@)8BGtwhxmXb_v60*#)bZl$iSh@#EHrxU3O95^49I{(Rr1Pt8GIo6VQ9S z%~h>|iUmNo#r-Bi&Q1_++McXKLTfCbKB0Mwd+$**Yo1oe}!apA!IT zB(cZ;^e0tbf1@ZNa`<-&8q7p?sOcScsX70xRGKC@G>Uk;()jnMly(#xJ{DgE0uVr( zjbgkiV+6j2+W@WVnrrRP^cUf`9&T>ib%jR6M31&dzJPTLcI=uM2bRUPr#E$CrCt4Z zjg(Qei8j=<2N5*i?&^#HEu*f!o@9HnXVo>)#7P9>MWk?5WhD@exH~#JKwaY#6r`%I ziDF$OIrm97mg3D>`Ka1F@m2vJUVJ^*rjU+zES7!-j6hK<{G4ql7cPh8fE?BI>e6X z_~^~+P2O>Fy|w3e%Yusu#)>&JBoPr3(slL7*INDPaj2@Q8nrf6l*_OMTjnnD(zxe0 zAZ=!1c?|@|va&LI#jfQ8m+SKb6EsIAQsnXh9Uc^`WUArG(39<%CxD=kh7A>0mqAr6 zC3*`GhJW@=a&VyIH!!0iX? z&=#DjW|o{ezLf)RmqEO4E=C`oxVe0JM~QCvJKqq_ahvPR0nz#}yY-AOoy7JBF;>v4 ziep{9*^#~=(4qVK2=@>u|H)MUiqYZWmcc<%Z~=rQBqKnrA-@9ZCxArzpg5+>`5BEK zg@uN~i(UC1IS-HE1S>Mgee7y!L0@#KF*xKM_}y=_3}A(o2z72=9<3?xp>IMFCg4 zu#PZU`^Z?ZW}nc=B@)J9<5t{2Ksp`$a_%J(zY%iY#Tbipa@x9v}n2|@4$FC^AM=?lqE4`n(|sb_Oe1A>Q{9>Me82+ zi{06J{r4aLjBl+1<)Hy6*+e6%D-8|N(Zrx6wjLgWU==o4v=b8#Z2zVL36(<6M~cuI zB+~%%w&X)h-$DgVpMbyz_!Y1W?cToKaIK9n7oTJ<>lq&_dORxt#p%viXEx+ z0tlnsR;~l4bGh(yV*!VV78{YLS#9UTo1%bN{TkUc3DY0t3v0D7nW0dnLQ8>4`9(yg z<9@kbBVcwfL-@?Sho=4X2%uM?$-S!{RpGg#Ggk^sNFc7&x-33ca`u;kJ!FU>k9rcW z%mTJMXsuZ2WPiZK?EK9 z&`5yr2iCWaj)`gZx2u4<74chsb2h5>ye{N9{8{A4Q{+zW=uXGF z>*9M!m&u(8-72q`r?Dzr%OG4+W|l>q2TNYYC^LSrCBX7yRo0uwNiP>_ozHI#cb>F;^Uo`(cS$N zDyE|mO)o&){|(axD&!$i2JPXWwa<1KN8UX11(-Vg#NF<>(?|XFXRnBmC^oZYGSeq zL?g=e8D~(A@k2Itc8Z=8Zl3rYT-~%SudZ4EVCp4%`xs)nANfBa6LtLo7>S-cJ2_?3 zdX#znxt;68IQ*KN{z(KSBjLS^$g>_+vj5Qm{92&#Ob?B$y4G60 zeEtXmmg5KjLvVw2SE>6{R6w(xujYLa^~4)>b5`rV^=j1eJDf{p)x_uIei%j!_^X{d zxe1-*_bxtWy?BMoX1L_kTlh{YEOEV9zZ90Ofr=70$^V7+AS3m61R5JAMA6r{>4b>{ zZ9{HkcB$4iINS%<5e_%P4+MG;fXpJ_`8W$p+q&<`ErJ=&_3l4IxOBxv6uzwGI_=K z4EBgYt3SbMcxzPKjU2>0roF0%Xv;JbW)gd!>3%$131>eCoF_3U=?n`R8ylOLnBcnN zbE;3fRe%)(F+_k$v!33U$gb88K_FJdheDV^0K65UAq64i#W18$_X_IL<|cokpoM0% z;TEWnX#;O4D{C%#U2U%&vZ^T;^0-+vv2Xnd7*vNK1qhrR;5RO8PgZ~^Gi!QU(N z@Zp;}-dJN0DyXfkedKIuxKk^H@uwko(Or7;@S^lLj(i}$1g-ZyX4tlq=;*h)K(uHd zA_dI}*Jl$Rf9W6!(aN8=TxRB9sC$^DW9BeoMQkCkJjKFg%KgTMucrHhf#s$oNe6Oz z31}dTChL3p#o2G`HE=0@+R=b|3v3KfZ@qw08cC}i+G^o6-(+&{qiZc4P}xAw0_2az z=VV2o+1ivp!&dBmcISF_j-t`0<0&cO6{T_lLqVo395^x2tk{K^iWG;gN`Qy*s)s>}shX9_qZjth4*7 z38?Pr3axXHl}IYT9labTeeZ>kcuoyj66Bs91&6CFSb}X zLk~zQe>7_~^a5P!2pxb}cY;y7@}}Dl8P8qwib+Y|Q-{%(H?j=2@85q74-XG@as`ZE zQA;akQ>uH#wXogN&AV}8OF4_+#}mo#R&sPU{P$Vyw6pTK@WoZQUlCp!3rx*8f#x%J z<=kT68iGzu0J%t~5rMdmlY=yVKl4Vw^jBBQ>ls^hG`b=j1D;QNI_@4m!mIY3jL<2VKBr=^4QXe`m+x;?V9YaQj9 zA3@5cy}jKe3ctWQ>dO~{Q0;Dq#8ow`f!V?zxZDgSB^3-YaV1%O1|y;XN;P|>b~ZD4 zCM6k0wMVGP|H%E?W7C}s!QgP9ii8S7Qsm9L&^EKulk zd5>bJR`F#F*w&_$1O5F|YwlcCVnUCNH@HJW3=&0S1*cBEcTazL#>CV{^NakzI~wp8 zq=w&{+Bse7yM5AzN=Zo><14~RUf>dBO^JD3=3TXtE0IgIw4`RCM@qp@wXy;N<9Y@4 zcYMi0|Mp1>u4+5NthLhH{4e#__H_7UZcczjhM=lnZv2j}omR z#s7@iL>0|&^-!$He)R&wi^qW%{Thj^UrlHOHoe1bMP|%Qti>O&rS;zHwNjs-20kbt zA+aJ0pw}NBmU}-N$NOR!7N?2}iC~MH>EXhJJn*2h2x$e(5elYKeniso|0X?K#~&Ci8noezzCvDm_X4Hoal5d5}*RO3>v4y;(m~oeQHuW=aEc$>x*5)&QJI z&$+RRO4!BKbvP9ZI!Z}t^N`Yg!}`@+`~e^De`UNpa`kcIc`3yD1df^{{$T_${eenV zWjft2#WU6yFQrMw$37cS*m=vQKXR_Nsak2X&;bKt#jgL=Lpk)FgdWDPCTvS!bY##M z3#13~^J{Dzy-s(s-x?oK#0#lH3bn>9E(mrx>u#U@1vDx_Q!AiOOkf*vaCxBXVkRHKy5G%iF)fV zW%1tYH4e`D`gc5pJ!?m@re=aV8?U#CIgETIXXlwBZ-8_=(SVuv==O@A{$mMxu~ znBU^fRxxwMpl@?}75|@-T2q@vGrk|wal+w(@5gCLzPJvcCQy%J|Ch;`{vJZKLA4P; zc5ax(`;kR+oWkdiMRL*mmu48Zc)7G{z(fM=YHHANfI=m!zCPvP;J}fvQ*AjZ`=xY) zX`Npa{GXAiIX4-z!A}b>2V&Dr*^ZOBm*|=w?p0^nwYl0cs0Po>8(WCeuzk~bEh5Vc z-4Qu88Zn@j0y=qgoS}i+&1nW&LQ@=30p*}llVrqGtgrsLyuL;Du|99!WE*7ZhyE8F zw3{?Hq=XFMn}Br_oq=xxYN;yAAc(-zMT5kZ3Q^NA1awWgK!Rsuew~ts`Q}Bx^B>h1 z1~FMeFw6s}t&vESGDjiiJXL$rJE z+0@-(k2H1rZ7P6pma@J{A9Shpqn!%rOcok0Mo{CqXIlHrnF{)u2#eBbD4C=_faIg? z#1%$F0n{(BH<{}NUN6w(Kt@?Z?T|47iFX{jSbC_mHAE7I%I;j_YhnRS#l3e+3riIS zzoGzB9mtS(K2LD8v$L?Y3`$RD&`}gh>lLQLFQW)g&Yk@-@S58@dSG$M!B;+bPC+A` z4*AB@CATO>!dG)}_4E}M8W19h!co9u0(pNi1d4)r`;^Wb>cY?C-;oGZu`QuQeZwVF z88}uY1Uph%ueo0TI{s%&fVFk|HJ%BJcy(QhQ6Q@8QVJw}tV9fn1sIwkdz0CvJ?=Gg z&h=XfSMG=wBvPTBYy~{s)+({J$IQ=3aoNd~i70*HKV4?V|Nj1Cn)+|IC&ENrW`y3) zc#Ar#eRt|>|DB1;YytzcW$cEo5gE|)Qs7n?U7^ps-UnL&Y^GnNZ!uwqK9FEy?QxpT z`09~2K;1>OVt)zhUUlOaZV!vR^OEq6UdTQgIpes@hy~&oT|3re7a@U_VY8jbGL{ zzXwt>zjoI-?nfS6tY@Tj-tz2clqpxJWS2$DkLvO({a30H^=1 zBZjk2n{S-YnsMg7aF(6t0bfj~n!F*b5kCHA6}eZz$OqkJHY2G7?@uRd>;-{>1tK;^ z##XEhg5`k@a#}2D0_mRha95GR{(MB7&lmFd`QpWHB+H%#1rERyBbp>DvXrnzK+x4! zasM~x4EH^FRjwbOK$#BY=uyswMp0 z)??%Mh8;ceT{3)SWhJ_ULL<+cOZs$8T;eIU!81f&LSW)$`1>J;h=HJ^Uj%FF;MD#6 z-G2c9mx3vaG3&{jjo)*Ss$ci9nexQ8%}Yqv9tZl3(Ir8UkQGiVX>)5i8DY2j{Vq25 zmz3gIRD<8!NabU6hO*#xEt9F>&|IkL%X6K_#xqr0lmY7!$vWL2Qvh7gvk)w(^3R{c z4q9g+Ik_fQ+!YYNej|Lpb&&<);!>~lSbnj^+Odj{wHx}%i)OmC@FpWQb>>KODCcY# zOsfK;R>+yX8qFCk369Gh^W-{1L~=)CvF#|~qY=9_GcUSC8#4j^42+1%M|xL2Ir|V$ zd7uRgUYYPDSo+Ud#=^{uOJ`qH`7`62V##4U;djxX&8ZAL*iP`i3@m|AGW)+Ug;ntW zWvj-#1uWsbPXYG#ujZW37Qe9o)g7=e@qdW@8QbG$)&U>G&o>nzZ{|E2PG_~Kc;3=f zlrd#sudA)}MScZ|RFH2lFd29b=5N60*H|N4=U|1zjq2Qa)ISt9DaBtIrI6b6Q~Wti zC00Q$Wn91G^<0GqjOML)>ICuqNwlqC!EI)0_6qQGS8D7mik`#X1!1+dtsmmH7KK4p z}b|k|fND+WhKrs$Z&e{N!96-vy>ohLr*6;`3FxSn> zbzG;V61t5npN_WS9~uSPmXXez`L}4dG!zW(csvvn(;FAI2hvp-38O3O*6&DDTc+;q z6}GDPp)>cJ82n`kX$MCM!pwN>E{Ukfw>{DHA#sm!40!Wwz@2JaJ}7@R25$9hD*Fa& ze!f?Yx>;JH_H(`nbBetz=Sb7TMymVwfq%>asN}AFH6<=s1tK8+0EE(`qoW4Y?G+GY z_L392?jJhXeEU?vZSzLazw$J`IT6Z3Z%lo=^Q(Wex#RQ^w&+ddw-HhC;rC4w1`Ds= zhDW)z{CA%7R!Us_RrOqe#t29&-=TpqC9BRMoob8*!sqrL!-iq8v(i1gXvlnwF0=&^ z+J(pMPcrr=I3Ut+l4$2#dJ8xz&)U)1-&Ul5OMu&K<=tgQrMd_xW|s>t9pbQM?08mm-WD5Z6fBtm_7D2 zdOyAWR)HnMnw@%8(JRWfjEJn3???NiTrPYdv8T!hG&#}74cC4IamqFNfK1zbfAq$$ zy&fx3^ZB>V@uer%n$;pQpU@7Yg4vyC&?hz&1g9=uUSvGW(FW98L|WR&f%pdqXcwn2 zs%nkWg+41nJHCa*z`nbFOHk*yIA4w_o8D2^(i+!0huhq_VIcQ-r=i+r_;uW+x7#iC zoZE}CzIIvH)YtZ@dtoK@M?ZIep}-n9@Ks<1PQwI@vWSr9CqHk&VAow(3_Rl$`DW%8 z;^|8J^tv0!GvM62aD< z5cajyu@TuSPy`FjHwOlB*5yf??Fe&3F!Jw1y3E;?KRJi%+6Ocp@~vRB$(p!-Z9s~w z@-CVUVrg+&a?CVQKXm@cf0;uC7{IG!H=@|jVC!o}@4#FUv5X^_e4c>%p7k!e3{FmV zFDx(tQ~(5fT|M?RO^L!NfRsOgq~Kk!y3R3=A?iu(`(a811bnb#&KNP=Od6AgyGK#XW|((uUZ zm~sK3;QXJIdj2ebGLnd|S`FkDK^iSqjSYy^?OP|iR~=~)KY{%MvX?m}B{6}4XdpWX z2B}wErOGEAOz@N0rfWZ5hyed!Zgh9hhL?``XfcfO5VYpkdl+`)l_;$v5sI4!?T81L z>IKwj(15aJ>}pDanQ@wIc{{psi+-;V5@+IlR;)q>{EapS$dLj7UzPrI>#_6B4L@B| znvveI@dv@n38BcPj*oI$Ao~914Mt3-5)Z0!S>}je3{{ z+}uSFHvndyKEuZjzFxqQUjVQ@;morJ{knQ1sPYm>XNp|Tyhx>m;)SJV`we-quZ~1v znK$GcG+XW#<&7{}q7XBjTf)o2CVTqN1m_s&Rz4&bt7>Q{!F?($Vtu3;&?|oH*WJXbrGL5)M&M=JlQzex(R-G0gm%i;I`a zHE|yCxjC`Txgcx6R#IBWXlCO(A<4ePQ14g%2VluBf%FfAzOU{L2{7)Ww%xwM6wE~AcG=o^hY1V5ne zS?oW591>BXbfC4vjC}Qg$&6JeNQ7|E7sJZh+(4OilfI@C+V2dUZytK-u(P4W$$=#+ zV32`h$j83U0+LcQPG5`*C^0lYlZ#dJY0>j@E9Ws97FemPHJ_gq`siFL&!EC2 zePk-91}mstwTB9;ByoKb&ELN-oA^5g4hZR8ojOx*CTL7K@&;a0E$C*zBbIA6FD*X^ zpg*rbP7CH7JUPZOemw=Sz|Xb6jlxURqefdh#{0Cp^G zY;q-;axM9T$o&e(2M$Ek&wj7mjI^Z9V9lvgL3?;mmo6=8?>_X4k&!eM6$%B#C2Qct zTd_OhGSCSww)bY|Jw*xc50vuJ@L-{540M*g>68RC0(200p~s7+ucXt~z~%wtWz_~u zRd4QT6Zrhu$b*p!79G&OL9HY6Hbyxm1|tJ3s|Eu+{AjB4FiY@BB{^Aq2cG+Rd$jBd z`M~%0aBXx|*o)zCke>(PkGr4%0QVpP8_IKVVhUsokbMPU?)2Tz~t# z0;l6BrnHplMx zrL+9s#R4)%C>noHp5!A3dau@}7N9+>ef_V8p|mSJ9W7b36lT|qKBIB}MSBD~g6DA% z$x!<+P0xX&I4#I@+@cVTypxW+GC#vuR3hTv5+%-f^xB=lm=tj)PoQ6(iWqpYCSw<$ zKI;23M-_hKlg2AotJ|sMuNE^b7bBridHPGz>Sf+>gnqnL_(MZ1W7+WgR$uqhbn-%9 zpAE5sox0o{i~}NYRC<{FlkQ~4K5X@xe&D@c9haBV(J?Xejg1BcTxLpe-+aDo`pZv?pVkS=ws{j_tv5T<1d}d zYI!4fCvbpq4-jb-l9KW+yLjv1K-g^s;$`S%28GsHN>xTGzja#<4unt7&dMq(g25(m zaCBVU-{1FGXKUj=L%_C)F8F_qw3&qe>p*Wr1u%6Z0+h;8s-6e8{baCRq8F6=>{d9Z zf>HPn4z5n4J?^9^2)V3}bfa$6OFS_5l_>&Iz!3Vs!6^xPtSkVqr#3J7b zflCVlh#++;_?cOku*!%Cd`9h0EE5%e>pljbtQQIbNv(^E%%`WPrSgZBWMR6O`b?|MSF!dAGkQq=O|J-} ze>z>K8>q){?111EZ(h%<(Xrs^C$vO3Eji|Y^i57rWDo;}UB>P64?8l>`i6+g0vNst zn1;kdo>@xrDW%GBBo0Jk@zf~}2QevO$7kJbnTv&&5?YvNYT#s*%0>YZ(esBPW=-CD z=7F>KA|Fa#)3~ND=M5HFIgR_GbkO~yG*9_4+K%dwQyvbQdyz<@Hi;26-#2fA5DpTXkA|vKabyq4Txn zme?;cm#K*Q*&nZ&YGJ~MSAtB|&4SF`&4y~IjQQPH)7)a?h-5M!#jnZP9K4S&38YsR z1f#xOe0qOV!t7{C?%(UgebbAHB71Y|OifX|oS5Q2l?SmIwqsqp=spDN?4mE3a zitw+T6Zft{l(l^=`ryZzc{%jYJwcsM>iQIU(BbF}N~KiY&1iG)h6Bj8uT88-IX~RK zDJnW*o7xweEMK5ef?esN+1+L^;Mdfe@ZWecpWHHf&#rDIyh4SAi#OfVz!hNrL7%K5 zD&41qCQ;on91yi&rc6r~u)_)K#cS4QgV~W_&CC=h+(slNv7y@?MO5Xr7o=5r_r5K` zyQNmUH6Ra~BUxKpNScbg@0f(=PtQ7ie{9((&wm0 zYTeF(hXKJ63k~$c2y*<)mt2{n%f7Le|8f1auSl5|3YzyAY$E&>@Y`e<@eq@VEmJQL zL&P|dO(>9yIr;D-Y?`$5hW7YUFh71;;6_Fjvb4}s@cmC0OWj)z>G*m1mOThXIhA^{ zM~tSN1+$TQ$Ogkxd+hSx5t@?{$4%87f^hP`Mp*uA_0BM21RuHkpig+NTj5(EH97tl z)~TSSHSzof`G?F0AG&V)DK~~p=~Ms3ZVac)FVnsvQ+tRx7#H@4djtHNYLjvTOcaY! zV4UVt)GjIP6S_yXJ^m2(2nC9PUSR2|VznX$f+*PRvF33ZJ0dy=L5t5cBUDKpt1t{c zPmzua>T&*cPj^*=0sffItE+Eobh?I7Vu;r1rv`tW{^eC=M2ooW-K>(hCau+Fh;N-kB3C1 zp`jE7gAfvU3-)A#+59}ldx{SBD;D-l&}-Dut6)%1gB%N< z*L3M?31mQYzE71}4C;}UGs>r==k}Hf)**$4g_6>zMqcI12MiveJMDFdL~m^>X`(w7VF!c^7)6_!dY;^n3^6lEcUMo5IP(liFb4Ql zyhu1iP{bm`Vlm3A^pW5iiqHe7wbw?JWT;4Y{=K$fP?+{DxH;?~^K++VA(O;YTZrEx zM5@N(H)H->$t*ytwpsFXm#zP7bGGGwtJgGlU?JLO*km;c{o<_c!k!lT3o?i&f3Xk^kAkaR6Os*O!+FR+Rx`^yCm$St zDE?qse%)y4os^uXAKvXPJM1&=_Ad(}98ERI8=Q!hd=!h^p${xMxgl56LP@iYlw{Y7 z%?%u_!*B3!Z#iL~G5-pZ3AIdNsd2A;miWg?PWGj_0Mkj|_vYu<>w@@8K_+UH`?sKu zt5N@ek?_D8_ml4Ch?nT9x;Ro$hQcLQmSXOYgG`GB!|auh3U!gE38WwN~0`7swBjDFk90_$ZC?0CzJ zM<>LV941P<*{4`SK}fYP=KZn7){^|&YJf@!LY>_OY5SIm4Yfe7KtZM};FQ|+=M4|+ zk=pDjgw*;%qsB@j_fJ`h-(1R%QsqPwijV>&_AUh```wo+*9+;{J^W& zv=D9*0&Jt*B&MfJem>`VJ~t|Ap=NJx$a%V^GJ^t2@iI+`;`@mniLsUTp9otCHnXbb zn9~gzh7@CCEA77fCfMoQ^u!sVT#E|Tv{-1ob-9Newa-KfnTHi3o{H^15b@{>#nQyQ zp0`~!j42Nk(}LnLZE-IR79#A@p(jk3@kFml$Bf7A*x2E}_YvZzZgT#tpoEdacRWy0Az~Kj+}i+O8pFL5 z!?9ZH<$f8TCy(P?SNn$IlBIKQ7<^5*~#5#2k)5VkJK@LkY%z-yOI37O%li@4ft`Z#kJGM7E>dWi(9kC{P#rT|th#vI%9zZrJo^Jzr)7SVUDxQpjH^`_oXL)zFHVSC zp{N6Ta9R8$MBgyI5@Gs!`>|V($lBa8@hdZs-3vB@(^)02*7j%?ocHVc$3L)rW;ve0 z#c3i}p#7{67n04PC8Q$arGP#tX4?{s_f4@Uwz)J6uL?VJ+$8lMxVCxxF@eU$o@+AM z+I7r93zpJF#Zb6{(kCgh77QiKk=S2ZiDWvT;kd1t#M_h6l*SBu#hLO}goxHEN7)-R zMYH_43nkOPAJ8}ay+}EebvTrhIrjJBC~PiH2*WME$)^Zgk-~%hi+K!4oEsZDjy-&I zqBF;5f7G4cV?V~<-ARKk6)7i36%(4-@GPcckzcwgXC7LBLs0Pb?>!de4%uI^A@HLA ztIe|DZnOOL;1maY!}r=W&YO8-c3KAQn25R!!#Yz6<2kNQbDeFM!f12yOPuV_Jzoy6 zKaqY@DkOps#M{>7y%|~l+8@%9@}gTv(&l?V!~fb6`*6Y(b+M;B3cZ8h2i>H52Z}mAg4*OFka0Hd|=rONjc&=!cn^3g-K4QnDI<3?1A{SEjAVz-sx2 z=6~2+XrRUiut$gc>0KYtUEbxBK0!B>r{N0y#f<0c7|*MQnbo3!`2tV&$Q?iP!ACP< zm?`n+_D2omu-D|j%qf**_xCLrGKU6C@*oEt&B?Y)@PDqY1OL%&YeG&toY