diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a27733d..5cd969e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,144 +1,145 @@ -cmake_minimum_required(VERSION 3.20) -project(OpenSteamTool VERSION 1.0.0 LANGUAGES C CXX) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_C_STANDARD 11) - -# Allow CMAKE_MSVC_RUNTIME_LIBRARY to control runtime selection for all targets, -# including dependencies pulled in via FetchContent. -if(POLICY CMP0091) - cmake_policy(SET CMP0091 NEW) -endif() - -# Static MSVC runtime everywhere, so the resulting DLL has no extra runtime -# dependencies. Must be set BEFORE FetchContent_MakeAvailable so the fetched -# deps (Lua, Detours, spdlog) inherit it. -set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) -set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") -set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) - -# --------------------------------------------------------------------------- -# Dependency recipes (FetchContent-backed, cached at /.deps). -# --------------------------------------------------------------------------- -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -include(Lua) -include(Detours) -include(Spdlog) -include(Protobuf) -include(Tomlplusplus) -include(LogMacros) - -# --------------------------------------------------------------------------- -# Protobuf code generation — two variants from the same .proto: -# -# Debug → full Message (protoc --cpp_out) → links libprotobuf -# Release → lite MessageLite (protoc --cpp_out=lite) → links libprotobuf-lite -# -# Both land in separate subdirectories of the build tree so the source -# directory stays clean and the right set is picked per configuration. -# --------------------------------------------------------------------------- -set(PROTO_SRC "${CMAKE_CURRENT_SOURCE_DIR}/proto/steam_messages.proto") -set(PROTO_GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/proto") -set(PROTO_GEN_LITE_DIR "${CMAKE_CURRENT_BINARY_DIR}/proto_lite") - -# Full Message (Debug) -add_custom_command( - OUTPUT "${PROTO_GEN_DIR}/steam_messages.pb.cc" - "${PROTO_GEN_DIR}/steam_messages.pb.h" - COMMAND ${CMAKE_COMMAND} -E make_directory "${PROTO_GEN_DIR}" - COMMAND $ - "--cpp_out=${PROTO_GEN_DIR}" - "-I${CMAKE_CURRENT_SOURCE_DIR}/proto" - "${PROTO_SRC}" - DEPENDS "${PROTO_SRC}" protoc - COMMENT "Generating protobuf full-Message sources (Debug)" -) - -# Lite MessageLite (Release) -add_custom_command( - OUTPUT "${PROTO_GEN_LITE_DIR}/steam_messages.pb.cc" - "${PROTO_GEN_LITE_DIR}/steam_messages.pb.h" - COMMAND ${CMAKE_COMMAND} -E make_directory "${PROTO_GEN_LITE_DIR}" - COMMAND $ - "--cpp_out=lite:${PROTO_GEN_LITE_DIR}" - "-I${CMAKE_CURRENT_SOURCE_DIR}/proto" - "${PROTO_SRC}" - DEPENDS "${PROTO_SRC}" protoc - COMMENT "Generating protobuf lite-MessageLite sources (Release)" -) - -# --------------------------------------------------------------------------- -# OpenSteamTool — the hook DLL injected into Steam (always 64-bit). -# --------------------------------------------------------------------------- -add_library(OpenSteamTool SHARED - dllmain.cpp - - # Shared utilities - Utils/AppTicket.cpp - Utils/ByteSearch.cpp - Utils/PatternLoader.cpp - Utils/Log.cpp - Utils/Config.cpp - Utils/LuaConfig.cpp - Utils/VehCommon.cpp - Utils/WinHttp.cpp - Utils/FileWatcher.cpp - - # Per-category hook modules - Hook/HookManager.cpp - Hook/Hooks_CallBack.cpp - Hook/Hooks_Decryption.cpp - Hook/Hooks_IPC.cpp - Hook/Hooks_IPC_ISteamUser.cpp - Hook/Hooks_IPC_ISteamUtils.cpp - Hook/Hooks_KeyValues.cpp - Hook/Hooks_Manifest.cpp - Hook/Hooks_Misc.cpp - Hook/Hooks_NetPacket.cpp - Hook/Hooks_SteamUI.cpp - Hook/Hooks_Package.cpp - - # protobuf generated sources — per-config variant - $<$:${PROTO_GEN_DIR}/steam_messages.pb.cc> - $<$:${PROTO_GEN_LITE_DIR}/steam_messages.pb.cc> -) - -# Header search path — per-config include directory -target_include_directories(OpenSteamTool PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_BINARY_DIR}/generated - $<$:${PROTO_GEN_DIR}> - $<$:${PROTO_GEN_LITE_DIR}> -) - -target_link_libraries(OpenSteamTool PRIVATE - lua_static - detours - winhttp - Bcrypt - $<$:libprotobuf> - $<$:libprotobuf-lite> - tomlplusplus::tomlplusplus - $<$:spdlog::spdlog> -) - -# Logging is compiled in only for Debug; Release reduces LOG_* to no-ops. -target_compile_definitions(OpenSteamTool PRIVATE - $<$:OPENSTEAMTOOL_LOGGING_ENABLED> -) - -# --------------------------------------------------------------------------- -# dwmapi.dll hijack — small loader DLL placed alongside Steam. -# --------------------------------------------------------------------------- -add_library(dwmapi SHARED - dwmapi/dwmapi.cpp -) - -# --------------------------------------------------------------------------- -# xinput1_4.dll hijack — secondary loader DLL placed alongside Steam. -# --------------------------------------------------------------------------- -add_library(xinput1_4 SHARED - xinput1_4/xinput1_4.cpp - xinput1_4/xinput1_4.def -) +cmake_minimum_required(VERSION 3.20) +project(OpenSteamTool VERSION 1.0.0 LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_C_STANDARD 11) + +# Allow CMAKE_MSVC_RUNTIME_LIBRARY to control runtime selection for all targets, +# including dependencies pulled in via FetchContent. +if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) +endif() + +# Static MSVC runtime everywhere, so the resulting DLL has no extra runtime +# dependencies. Must be set BEFORE FetchContent_MakeAvailable so the fetched +# deps (Lua, Detours, spdlog) inherit it. +set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + +# --------------------------------------------------------------------------- +# Dependency recipes (FetchContent-backed, cached at /.deps). +# --------------------------------------------------------------------------- +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +include(Lua) +include(Detours) +include(Spdlog) +include(Protobuf) +include(Tomlplusplus) +include(LogMacros) + +# --------------------------------------------------------------------------- +# Protobuf code generation — two variants from the same .proto: +# +# Debug → full Message (protoc --cpp_out) → links libprotobuf +# Release → lite MessageLite (protoc --cpp_out=lite) → links libprotobuf-lite +# +# Both land in separate subdirectories of the build tree so the source +# directory stays clean and the right set is picked per configuration. +# --------------------------------------------------------------------------- +set(PROTO_SRC "${CMAKE_CURRENT_SOURCE_DIR}/proto/steam_messages.proto") +set(PROTO_GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/proto") +set(PROTO_GEN_LITE_DIR "${CMAKE_CURRENT_BINARY_DIR}/proto_lite") + +# Full Message (Debug) +add_custom_command( + OUTPUT "${PROTO_GEN_DIR}/steam_messages.pb.cc" + "${PROTO_GEN_DIR}/steam_messages.pb.h" + COMMAND ${CMAKE_COMMAND} -E make_directory "${PROTO_GEN_DIR}" + COMMAND $ + "--cpp_out=${PROTO_GEN_DIR}" + "-I${CMAKE_CURRENT_SOURCE_DIR}/proto" + "${PROTO_SRC}" + DEPENDS "${PROTO_SRC}" protoc + COMMENT "Generating protobuf full-Message sources (Debug)" +) + +# Lite MessageLite (Release) +add_custom_command( + OUTPUT "${PROTO_GEN_LITE_DIR}/steam_messages.pb.cc" + "${PROTO_GEN_LITE_DIR}/steam_messages.pb.h" + COMMAND ${CMAKE_COMMAND} -E make_directory "${PROTO_GEN_LITE_DIR}" + COMMAND $ + "--cpp_out=lite:${PROTO_GEN_LITE_DIR}" + "-I${CMAKE_CURRENT_SOURCE_DIR}/proto" + "${PROTO_SRC}" + DEPENDS "${PROTO_SRC}" protoc + COMMENT "Generating protobuf lite-MessageLite sources (Release)" +) + +# --------------------------------------------------------------------------- +# OpenSteamTool — the hook DLL injected into Steam (always 64-bit). +# --------------------------------------------------------------------------- +add_library(OpenSteamTool SHARED + dllmain.cpp + + # Shared utilities + Utils/AppTicket.cpp + Utils/ByteSearch.cpp + Utils/PatternLoader.cpp + Utils/Log.cpp + Utils/Config.cpp + Utils/LuaConfig.cpp + Utils/VehCommon.cpp + Utils/WinHttp.cpp + Utils/FileWatcher.cpp + Utils/DllDirectory.cpp + + # Per-category hook modules + Hook/HookManager.cpp + Hook/Hooks_CallBack.cpp + Hook/Hooks_Decryption.cpp + Hook/Hooks_IPC.cpp + Hook/Hooks_IPC_ISteamUser.cpp + Hook/Hooks_IPC_ISteamUtils.cpp + Hook/Hooks_KeyValues.cpp + Hook/Hooks_Manifest.cpp + Hook/Hooks_Misc.cpp + Hook/Hooks_NetPacket.cpp + Hook/Hooks_SteamUI.cpp + Hook/Hooks_Package.cpp + + # protobuf generated sources — per-config variant + $<$:${PROTO_GEN_DIR}/steam_messages.pb.cc> + $<$:${PROTO_GEN_LITE_DIR}/steam_messages.pb.cc> +) + +# Header search path — per-config include directory +target_include_directories(OpenSteamTool PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR}/generated + $<$:${PROTO_GEN_DIR}> + $<$:${PROTO_GEN_LITE_DIR}> +) + +target_link_libraries(OpenSteamTool PRIVATE + lua_static + detours + winhttp + Bcrypt + $<$:libprotobuf> + $<$:libprotobuf-lite> + tomlplusplus::tomlplusplus + $<$:spdlog::spdlog> +) + +# Logging is compiled in only for Debug; Release reduces LOG_* to no-ops. +target_compile_definitions(OpenSteamTool PRIVATE + $<$:OPENSTEAMTOOL_LOGGING_ENABLED> +) + +# --------------------------------------------------------------------------- +# dwmapi.dll hijack — small loader DLL placed alongside Steam. +# --------------------------------------------------------------------------- +add_library(dwmapi SHARED + dwmapi/dwmapi.cpp +) + +# --------------------------------------------------------------------------- +# xinput1_4.dll hijack — secondary loader DLL placed alongside Steam. +# --------------------------------------------------------------------------- +add_library(xinput1_4 SHARED + xinput1_4/xinput1_4.cpp + xinput1_4/xinput1_4.def +) diff --git a/src/Utils/DllDirectory.cpp b/src/Utils/DllDirectory.cpp new file mode 100644 index 0000000..1958431 --- /dev/null +++ b/src/Utils/DllDirectory.cpp @@ -0,0 +1,19 @@ +#include "DllDirectory.h" +#include + +namespace Utils { + + std::filesystem::path GetDllDirectory() { + HMODULE hSelf = nullptr; + GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&GetDllDirectory), + &hSelf + ); + char dllPath[MAX_PATH] = { 0 }; + GetModuleFileNameA(hSelf, dllPath, MAX_PATH); + return std::filesystem::path(dllPath).parent_path(); + } + +} \ No newline at end of file diff --git a/src/Utils/DllDirectory.h b/src/Utils/DllDirectory.h new file mode 100644 index 0000000..24cbe9c --- /dev/null +++ b/src/Utils/DllDirectory.h @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace Utils { + // Returns the directory where the current DLL is located. + std::filesystem::path GetDllDirectory(); +} \ No newline at end of file diff --git a/src/Utils/PatternLoader.cpp b/src/Utils/PatternLoader.cpp index c3ed8fc..771ebf6 100644 --- a/src/Utils/PatternLoader.cpp +++ b/src/Utils/PatternLoader.cpp @@ -1,429 +1,430 @@ -#include "PatternLoader.h" -#include "Config.h" -#include "Hash.h" -#include "Log.h" -#include "WinHttp.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -// ---- compile-time sanity checks for FNV-1a table keys ---- -// If the steam-monitor bot uses the same algorithm these must hold. -static_assert(Fnv1aHash("BBuildAndAsyncSendFrame") == 0x82428E37u, - "FNV-1a mismatch for BBuildAndAsyncSendFrame"); -static_assert(Fnv1aHash("BuildDepotDependency") == 0xC37F2D8Eu, - "FNV-1a mismatch for BuildDepotDependency"); - -namespace { - -// ---- per-function pattern record ---- -struct PatternEntry { - std::string name; - uintptr_t rva = 0; // 0 = not present in file - std::string sig; // empty = not present in file -}; - -// key = Fnv1aHash(funcName) -using PatternMap = std::unordered_map; - -// module → its pattern map -static std::unordered_map g_moduleMaps; - -// Modules whose Load() call failed (popup already shown). FindPattern -// silently returns nullptr for these — without re-logging or adding the -// function to g_missingFunctions — so we don't follow one "TOML missing" -// popup with a second popup listing every dependent hook. -static std::unordered_set g_failedModules; - -// functions whose names were not found during FindPattern -static std::vector g_missingFunctions; - -// Built-in fallback mirrors. Tried in this fixed order when [pattern] -// mirror is not configured: GitHub raw first (canonical source), jsDelivr -// (global CDN) on connection failure. -static constexpr const char* kGithubMirror = - "https://raw.githubusercontent.com/OpenSteam001/steam-monitor/pattern"; -static constexpr const char* kJsdelivrMirror = - "https://cdn.jsdelivr.net/gh/OpenSteam001/steam-monitor@pattern"; - -// ---- byte-pattern scanner (independent of old ByteSearch) ---- - -static bool ParseSig(const std::string& str, - std::vector& bytes, - std::vector& mask) -{ - bytes.clear(); - mask.clear(); - for (const char* p = str.c_str(); *p; ) { - if (*p == ' ' || *p == '\t' || *p == ',') { ++p; continue; } - if (p[0] == '?' && p[1] == '?') { - bytes.push_back(0); mask.push_back(0); p += 2; continue; - } - char hi = p[0], lo = p[1]; - if (!hi || !lo) return false; - auto nib = [](char c) -> int { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; - }; - int h = nib(hi), l = nib(lo); - if (h < 0 || l < 0) return false; - bytes.push_back(static_cast((h << 4) | l)); - mask.push_back(1); - p += 2; - } - return !bytes.empty(); -} - -static void* ScanModule(HMODULE module, - const std::vector& bytes, - const std::vector& mask) -{ - MODULEINFO mi{}; - if (!GetModuleInformation(GetCurrentProcess(), module, &mi, sizeof(mi))) - return nullptr; - - auto* base = static_cast(mi.lpBaseOfDll); - SIZE_T size = mi.SizeOfImage; - SIZE_T patLen = bytes.size(); - if (size < patLen) return nullptr; - - for (SIZE_T i = 0; i <= size - patLen; ++i) { - bool found = true; - for (SIZE_T j = 0; j < patLen; ++j) { - if (mask[j] && base[i + j] != bytes[j]) { found = false; break; } - } - if (found) return base + i; - } - return nullptr; -} - -// ---- TOML pattern parser ---- - -// Section keys are hex literals like "0x82428E37"; each section is a table -// with optional `name`, `rva` (hex string), and `sig` (IDA-style bytes). -static PatternMap TableToPatternMap(const toml::table& tbl) -{ - PatternMap map; - map.reserve(tbl.size()); - for (auto& [rawKey, val] : tbl) { - if (!val.is_table()) continue; - auto& sub = *val.as_table(); - - uint32_t hashKey = 0; - try { - hashKey = static_cast( - std::stoull(std::string(rawKey), nullptr, 16)); - } catch (...) { continue; } - - PatternEntry entry; - if (auto v = sub["name"].value()) entry.name = *v; - if (auto v = sub["rva"].value()) { - try { entry.rva = static_cast(std::stoull(*v, nullptr, 16)); } - catch (...) {} - } - if (auto v = sub["sig"].value()) entry.sig = *v; - - map[hashKey] = std::move(entry); - } - return map; -} - -static PatternMap ParsePatternFile(const std::filesystem::path& filePath) -{ - try { - return TableToPatternMap(toml::parse_file(filePath.string())); - } catch (const toml::parse_error& e) { - LOG_WARN("PatternLoader: TOML parse error in {}: {}", - filePath.string(), e.description()); - return {}; - } -} - -static PatternMap ParsePatternString(std::string_view body, - std::string* outError = nullptr) -{ - try { - return TableToPatternMap(toml::parse(body)); - } catch (const toml::parse_error& e) { - if (outError) *outError = e.description(); - return {}; - } -} - -// ---- popup helpers (detached threads so we never block Steam) ---- - -// Surface a missing pattern file to the user, with enough detail to either -// (a) drop a file in manually, (b) check the upstream repo, or (c) file -// an actionable bug report. We deliberately only disable hooks for the -// failing module — the rest of OpenSteamTool keeps working. -static void ShowDownloadFailedPopup(const std::string& dllName, - const std::string& sha256, - const std::string& ghSubdir) -{ - std::thread([dllName, sha256, ghSubdir]() { - std::string msg = - "OpenSteamTool: signature file not found for " + dllName + ".\n\n" - " Steam DLL: " + dllName + "\n" - " SHA-256: " + sha256 + "\n\n" - "Steam was likely just updated and the matching pattern file is " - "not yet published on the steam-monitor server. Hooks that depend " - "on " + dllName + " are disabled for this session; other modules " - "are unaffected.\n\n" - "You can:\n" - " 1. Wait for the next signature update (usually within hours of " - "a new Steam build), then restart Steam.\n" - " 2. Drop a matching TOML at:\n" - " \\opensteamtool\\pattern\\" + ghSubdir + "\\" + sha256 + ".toml\n" - " 3. Check upstream:\n" - " https://github.com/OpenSteam001/steam-monitor/tree/pattern/" + ghSubdir + "\n" - " 4. Report this hash so it gets prioritized:\n" - " https://github.com/OpenSteam001/OpenSteamTool/issues"; - MessageBoxA(nullptr, msg.c_str(), - "OpenSteamTool - Unsupported Steam Version", - MB_OK | MB_ICONWARNING | MB_TOPMOST); - }).detach(); -} - -} // namespace - -// ---- public API ---- - -namespace PatternLoader { - -bool Load(HMODULE module, const std::string& dllPath, const std::string& ghSubdir) -{ - namespace fs = std::filesystem; - - // 1. Compute SHA-256 of the DLL file on disk. - // Timed so we can see the cost in main.log — useful when triaging - // "Steam takes ages to start" reports from HDD users. - const auto hashStart = std::chrono::steady_clock::now(); - const std::string sha256 = Sha256OfFile(dllPath); - const auto hashMs = std::chrono::duration_cast( - std::chrono::steady_clock::now() - hashStart).count(); - - if (sha256.empty()) { - LOG_WARN("PatternLoader: Sha256OfFile failed for {} ({} ms)", dllPath, hashMs); - ShowDownloadFailedPopup(fs::path(dllPath).filename().string(), - "(hash failed)", ghSubdir); - g_failedModules.insert(module); - return false; - } - LOG_INFO("PatternLoader: {} sha256 = {} ({} ms)", ghSubdir, sha256, hashMs); - - // 2. Build local cache path and make sure the directory exists. - // Cache lives at: /opensteamtool/pattern//.toml - // dllPath is always inside the Steam root directory. - fs::path steamRoot = fs::path(dllPath).parent_path(); - fs::path cacheDir = steamRoot / "opensteamtool" / "pattern" / ghSubdir; - fs::path cachePath = cacheDir / (sha256 + ".toml"); - - std::error_code mkdirEc; - fs::create_directories(cacheDir, mkdirEc); - if (mkdirEc) { - // Non-fatal: we can still try to read an existing file or hold the - // downloaded TOML in memory. Log it so disk-permission issues surface. - LOG_WARN("PatternLoader: could not create cache dir {} ({})", - cacheDir.string(), mkdirEc.message()); - } - - // 3. Try remote first. Rationale: the upstream bot can re-publish the - // TOML for the same SHA-256 (adding new function signatures, fixing - // stale ones, etc.). Reading the local cache first would silently - // pin users to whatever version they downloaded on day 1. The cache - // is kept purely as an offline fallback below. - // - // Mirror selection: - // - If [pattern] mirror is configured, use only that URL. Explicit - // user choice wins — no automatic fallback. - // - Otherwise try GitHub raw, then jsDelivr on connection failure - // (helps users where raw.githubusercontent.com is blocked). - // - HTTP 404 stops the loop early: all mirrors serve the same data, - // so 404 means the upstream bot hasn't published this SHA yet. - std::vector mirrors; - if (!Config::patternMirror.empty()) { - mirrors.push_back(Config::patternMirror); - } else { - mirrors.emplace_back(kGithubMirror); - mirrors.emplace_back(kJsdelivrMirror); - } - - WinHttp::Result result; - std::string url; - for (size_t i = 0; i < mirrors.size(); ++i) { - url = mirrors[i] + "/" + ghSubdir + "/" + sha256 + ".toml"; - LOG_INFO("PatternLoader: downloading {}", url); - - result = WinHttp::Execute(L"GET", url.c_str(), - nullptr, 0, nullptr, - /*timeoutResolve=*/5000, - /*timeoutConnect=*/5000, - /*timeoutSend=*/10000, - /*timeoutRecv=*/15000); - - if (result.ok && result.status == 200) break; - - if (result.ok && result.status == 404) { - LOG_WARN("PatternLoader: mirror has no such file (HTTP 404): {}", url); - break; // all mirrors serve the same content — no point trying others - } - - // Connection error or 5xx — try next mirror if any - if (i + 1 < mirrors.size()) { - LOG_WARN("PatternLoader: mirror failed ({} ok={} HTTP={}), falling back", - mirrors[i], result.ok, result.status); - } - } - - // 4. Remote succeeded → parse, then update cache on disk so the next - // launch has an up-to-date offline fallback. - if (result.ok && result.status == 200) { - std::string parseErr; - PatternMap map = ParsePatternString(result.body, &parseErr); - if (!map.empty()) { - std::ofstream ofs(cachePath, std::ios::binary); - if (ofs) { - ofs.write(result.body.data(), - static_cast(result.body.size())); - LOG_INFO("PatternLoader: cached to {}", cachePath.string()); - } else { - LOG_WARN("PatternLoader: could not open {} for writing", - cachePath.string()); - } - LOG_INFO("PatternLoader: loaded {} patterns for {} (remote)", - map.size(), ghSubdir); - g_moduleMaps[module] = std::move(map); - return true; - } - LOG_WARN("PatternLoader: downloaded body unparseable ({}); " - "trying local cache", - parseErr.empty() ? "empty or no entries" : parseErr); - } - - // 5. Remote unreachable (or returned garbage) → fall back to whatever - // we previously cached for this exact SHA-256. Better stale-but- - // working than nothing at all. - if (fs::exists(cachePath)) { - LOG_WARN("PatternLoader: remote failed (last: {} HTTP {}); " - "falling back to local cache {}", - url, result.status, cachePath.string()); - PatternMap map = ParsePatternFile(cachePath); - if (!map.empty()) { - LOG_INFO("PatternLoader: loaded {} patterns for {} (cache fallback)", - map.size(), ghSubdir); - g_moduleMaps[module] = std::move(map); - return true; - } - LOG_WARN("PatternLoader: cache fallback also failed (file empty/invalid)"); - } - - // 6. Remote failed and no usable cache — give up. - LOG_WARN("PatternLoader: no source available for {} (last URL: {} HTTP {})", - ghSubdir, url, result.status); - std::string dllName = fs::path(dllPath).filename().string(); - ShowDownloadFailedPopup(dllName, sha256, ghSubdir); - g_failedModules.insert(module); - return false; -} - -void* FindPattern(HMODULE module, const char* funcName) -{ - // If the whole module's pattern file failed to load, stay quiet — the - // user already saw one popup and the main.log already has the warning. - // No point amplifying that into one log line per hook plus a second - // "missing functions" popup later. - if (g_failedModules.count(module)) { - return nullptr; - } - - uint32_t key = Fnv1aHash(funcName); - - auto mapIt = g_moduleMaps.find(module); - if (mapIt == g_moduleMaps.end()) { - // Load() was never called for this module. - LOG_WARN("PatternLoader: FindPattern called for module that was never loaded " - "('{}')", funcName); - g_missingFunctions.emplace_back(funcName); - return nullptr; - } - - auto& map = mapIt->second; - auto entryIt = map.find(key); - if (entryIt == map.end()) { - LOG_WARN("PatternLoader: no entry for '{}' (key=0x{:08X})", funcName, key); - g_missingFunctions.emplace_back(funcName); - return nullptr; - } - - const PatternEntry& entry = entryIt->second; - - // Priority 1: RVA direct offset - if (entry.rva != 0) { - void* addr = reinterpret_cast( - reinterpret_cast(module) + entry.rva); - LOG_DEBUG("PatternLoader: {} resolved via RVA 0x{:X}", funcName, entry.rva); - return addr; - } - - // Priority 2: byte-signature scan - if (!entry.sig.empty()) { - std::vector bytes, mask; - if (ParseSig(entry.sig, bytes, mask)) { - void* addr = ScanModule(module, bytes, mask); - if (addr) { - uintptr_t rva = reinterpret_cast(addr) - - reinterpret_cast(module); - LOG_DEBUG("PatternLoader: {} resolved via sig @ RVA 0x{:X}", - funcName, rva); - return addr; - } - LOG_WARN("PatternLoader: sig scan miss for '{}' (pattern parsed OK, " - "no match in module image)", funcName); - } else { - LOG_WARN("PatternLoader: malformed sig for '{}': '{}'", - funcName, entry.sig); - } - } else { - LOG_WARN("PatternLoader: entry for '{}' has neither rva nor sig", funcName); - } - - g_missingFunctions.emplace_back(funcName); - return nullptr; -} - -void ReportMissingFunctions() -{ - if (g_missingFunctions.empty()) return; - - // Build the list - std::string list; - for (const auto& name : g_missingFunctions) - list += " - " + name + "\n"; - g_missingFunctions.clear(); - - std::thread([list]() { - std::string msg = - "OpenSteamTool: some functions could not be located.\n\n" - "The following functions were not found in the signature file:\n" + - list + - "\nHooks for these functions are disabled for this session.\n\n" - "Please report this at:\n" - "https://github.com/OpenSteam001/OpenSteamTool/issues"; - MessageBoxA(nullptr, msg.c_str(), - "OpenSteamTool - Missing Signatures", - MB_OK | MB_ICONWARNING | MB_TOPMOST); - }).detach(); -} - -} // namespace PatternLoader +#include "PatternLoader.h" +#include "Config.h" +#include "Hash.h" +#include "Log.h" +#include "WinHttp.h" +#include "DllDirectory.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// ---- compile-time sanity checks for FNV-1a table keys ---- +// If the steam-monitor bot uses the same algorithm these must hold. +static_assert(Fnv1aHash("BBuildAndAsyncSendFrame") == 0x82428E37u, + "FNV-1a mismatch for BBuildAndAsyncSendFrame"); +static_assert(Fnv1aHash("BuildDepotDependency") == 0xC37F2D8Eu, + "FNV-1a mismatch for BuildDepotDependency"); + +namespace { + +// ---- per-function pattern record ---- +struct PatternEntry { + std::string name; + uintptr_t rva = 0; // 0 = not present in file + std::string sig; // empty = not present in file +}; + +// key = Fnv1aHash(funcName) +using PatternMap = std::unordered_map; + +// module → its pattern map +static std::unordered_map g_moduleMaps; + +// Modules whose Load() call failed (popup already shown). FindPattern +// silently returns nullptr for these — without re-logging or adding the +// function to g_missingFunctions — so we don't follow one "TOML missing" +// popup with a second popup listing every dependent hook. +static std::unordered_set g_failedModules; + +// functions whose names were not found during FindPattern +static std::vector g_missingFunctions; + +// Built-in fallback mirrors. Tried in this fixed order when [pattern] +// mirror is not configured: GitHub raw first (canonical source), jsDelivr +// (global CDN) on connection failure. +static constexpr const char* kGithubMirror = + "https://raw.githubusercontent.com/OpenSteam001/steam-monitor/pattern"; +static constexpr const char* kJsdelivrMirror = + "https://cdn.jsdelivr.net/gh/OpenSteam001/steam-monitor@pattern"; + +// ---- byte-pattern scanner (independent of old ByteSearch) ---- + +static bool ParseSig(const std::string& str, + std::vector& bytes, + std::vector& mask) +{ + bytes.clear(); + mask.clear(); + for (const char* p = str.c_str(); *p; ) { + if (*p == ' ' || *p == '\t' || *p == ',') { ++p; continue; } + if (p[0] == '?' && p[1] == '?') { + bytes.push_back(0); mask.push_back(0); p += 2; continue; + } + char hi = p[0], lo = p[1]; + if (!hi || !lo) return false; + auto nib = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + }; + int h = nib(hi), l = nib(lo); + if (h < 0 || l < 0) return false; + bytes.push_back(static_cast((h << 4) | l)); + mask.push_back(1); + p += 2; + } + return !bytes.empty(); +} + +static void* ScanModule(HMODULE module, + const std::vector& bytes, + const std::vector& mask) +{ + MODULEINFO mi{}; + if (!GetModuleInformation(GetCurrentProcess(), module, &mi, sizeof(mi))) + return nullptr; + + auto* base = static_cast(mi.lpBaseOfDll); + SIZE_T size = mi.SizeOfImage; + SIZE_T patLen = bytes.size(); + if (size < patLen) return nullptr; + + for (SIZE_T i = 0; i <= size - patLen; ++i) { + bool found = true; + for (SIZE_T j = 0; j < patLen; ++j) { + if (mask[j] && base[i + j] != bytes[j]) { found = false; break; } + } + if (found) return base + i; + } + return nullptr; +} + +// ---- TOML pattern parser ---- + +// Section keys are hex literals like "0x82428E37"; each section is a table +// with optional `name`, `rva` (hex string), and `sig` (IDA-style bytes). +static PatternMap TableToPatternMap(const toml::table& tbl) +{ + PatternMap map; + map.reserve(tbl.size()); + for (auto& [rawKey, val] : tbl) { + if (!val.is_table()) continue; + auto& sub = *val.as_table(); + + uint32_t hashKey = 0; + try { + hashKey = static_cast( + std::stoull(std::string(rawKey), nullptr, 16)); + } catch (...) { continue; } + + PatternEntry entry; + if (auto v = sub["name"].value()) entry.name = *v; + if (auto v = sub["rva"].value()) { + try { entry.rva = static_cast(std::stoull(*v, nullptr, 16)); } + catch (...) {} + } + if (auto v = sub["sig"].value()) entry.sig = *v; + + map[hashKey] = std::move(entry); + } + return map; +} + +static PatternMap ParsePatternFile(const std::filesystem::path& filePath) +{ + try { + return TableToPatternMap(toml::parse_file(filePath.string())); + } catch (const toml::parse_error& e) { + LOG_WARN("PatternLoader: TOML parse error in {}: {}", + filePath.string(), e.description()); + return {}; + } +} + +static PatternMap ParsePatternString(std::string_view body, + std::string* outError = nullptr) +{ + try { + return TableToPatternMap(toml::parse(body)); + } catch (const toml::parse_error& e) { + if (outError) *outError = e.description(); + return {}; + } +} + +// ---- popup helpers (detached threads so we never block Steam) ---- + +// Surface a missing pattern file to the user, with enough detail to either +// (a) drop a file in manually, (b) check the upstream repo, or (c) file +// an actionable bug report. We deliberately only disable hooks for the +// failing module — the rest of OpenSteamTool keeps working. +static void ShowDownloadFailedPopup(const std::string& dllName, + const std::string& sha256, + const std::string& ghSubdir) +{ + std::thread([dllName, sha256, ghSubdir]() { + std::string msg = + "OpenSteamTool: signature file not found for " + dllName + ".\n\n" + " Steam DLL: " + dllName + "\n" + " SHA-256: " + sha256 + "\n\n" + "Steam was likely just updated and the matching pattern file is " + "not yet published on the steam-monitor server. Hooks that depend " + "on " + dllName + " are disabled for this session; other modules " + "are unaffected.\n\n" + "You can:\n" + " 1. Wait for the next signature update (usually within hours of " + "a new Steam build), then restart Steam.\n" + " 2. Drop a matching TOML at:\n" + " \\opensteamtool\\pattern\\" + ghSubdir + "\\" + sha256 + ".toml\n" + " 3. Check upstream:\n" + " https://github.com/OpenSteam001/steam-monitor/tree/pattern/" + ghSubdir + "\n" + " 4. Report this hash so it gets prioritized:\n" + " https://github.com/OpenSteam001/OpenSteamTool/issues"; + MessageBoxA(nullptr, msg.c_str(), + "OpenSteamTool - Unsupported Steam Version", + MB_OK | MB_ICONWARNING | MB_TOPMOST); + }).detach(); +} + +} // namespace + +// ---- public API ---- + +namespace PatternLoader { + +bool Load(HMODULE module, const std::string& dllPath, const std::string& ghSubdir) +{ + namespace fs = std::filesystem; + + // 1. Compute SHA-256 of the DLL file on disk. + // Timed so we can see the cost in main.log — useful when triaging + // "Steam takes ages to start" reports from HDD users. + const auto hashStart = std::chrono::steady_clock::now(); + const std::string sha256 = Sha256OfFile(dllPath); + const auto hashMs = std::chrono::duration_cast( + std::chrono::steady_clock::now() - hashStart).count(); + + if (sha256.empty()) { + LOG_WARN("PatternLoader: Sha256OfFile failed for {} ({} ms)", dllPath, hashMs); + ShowDownloadFailedPopup(fs::path(dllPath).filename().string(), + "(hash failed)", ghSubdir); + g_failedModules.insert(module); + return false; + } + LOG_INFO("PatternLoader: {} sha256 = {} ({} ms)", ghSubdir, sha256, hashMs); + + // 2. Build local cache path and make sure the directory exists. + // Cache lives at: /opensteamtool/pattern//.toml + // dllPath is always inside the Steam root directory. + fs::path dllRoot = Utils::GetDllDirectory(); + fs::path cacheDir = dllRoot / "opensteamtool" / "pattern" / ghSubdir; + fs::path cachePath = cacheDir / (sha256 + ".toml"); + + std::error_code mkdirEc; + fs::create_directories(cacheDir, mkdirEc); + if (mkdirEc) { + // Non-fatal: we can still try to read an existing file or hold the + // downloaded TOML in memory. Log it so disk-permission issues surface. + LOG_WARN("PatternLoader: could not create cache dir {} ({})", + cacheDir.string(), mkdirEc.message()); + } + + // 3. Try remote first. Rationale: the upstream bot can re-publish the + // TOML for the same SHA-256 (adding new function signatures, fixing + // stale ones, etc.). Reading the local cache first would silently + // pin users to whatever version they downloaded on day 1. The cache + // is kept purely as an offline fallback below. + // + // Mirror selection: + // - If [pattern] mirror is configured, use only that URL. Explicit + // user choice wins — no automatic fallback. + // - Otherwise try GitHub raw, then jsDelivr on connection failure + // (helps users where raw.githubusercontent.com is blocked). + // - HTTP 404 stops the loop early: all mirrors serve the same data, + // so 404 means the upstream bot hasn't published this SHA yet. + std::vector mirrors; + if (!Config::patternMirror.empty()) { + mirrors.push_back(Config::patternMirror); + } else { + mirrors.emplace_back(kGithubMirror); + mirrors.emplace_back(kJsdelivrMirror); + } + + WinHttp::Result result; + std::string url; + for (size_t i = 0; i < mirrors.size(); ++i) { + url = mirrors[i] + "/" + ghSubdir + "/" + sha256 + ".toml"; + LOG_INFO("PatternLoader: downloading {}", url); + + result = WinHttp::Execute(L"GET", url.c_str(), + nullptr, 0, nullptr, + /*timeoutResolve=*/5000, + /*timeoutConnect=*/5000, + /*timeoutSend=*/10000, + /*timeoutRecv=*/15000); + + if (result.ok && result.status == 200) break; + + if (result.ok && result.status == 404) { + LOG_WARN("PatternLoader: mirror has no such file (HTTP 404): {}", url); + break; // all mirrors serve the same content — no point trying others + } + + // Connection error or 5xx — try next mirror if any + if (i + 1 < mirrors.size()) { + LOG_WARN("PatternLoader: mirror failed ({} ok={} HTTP={}), falling back", + mirrors[i], result.ok, result.status); + } + } + + // 4. Remote succeeded → parse, then update cache on disk so the next + // launch has an up-to-date offline fallback. + if (result.ok && result.status == 200) { + std::string parseErr; + PatternMap map = ParsePatternString(result.body, &parseErr); + if (!map.empty()) { + std::ofstream ofs(cachePath, std::ios::binary); + if (ofs) { + ofs.write(result.body.data(), + static_cast(result.body.size())); + LOG_INFO("PatternLoader: cached to {}", cachePath.string()); + } else { + LOG_WARN("PatternLoader: could not open {} for writing", + cachePath.string()); + } + LOG_INFO("PatternLoader: loaded {} patterns for {} (remote)", + map.size(), ghSubdir); + g_moduleMaps[module] = std::move(map); + return true; + } + LOG_WARN("PatternLoader: downloaded body unparseable ({}); " + "trying local cache", + parseErr.empty() ? "empty or no entries" : parseErr); + } + + // 5. Remote unreachable (or returned garbage) → fall back to whatever + // we previously cached for this exact SHA-256. Better stale-but- + // working than nothing at all. + if (fs::exists(cachePath)) { + LOG_WARN("PatternLoader: remote failed (last: {} HTTP {}); " + "falling back to local cache {}", + url, result.status, cachePath.string()); + PatternMap map = ParsePatternFile(cachePath); + if (!map.empty()) { + LOG_INFO("PatternLoader: loaded {} patterns for {} (cache fallback)", + map.size(), ghSubdir); + g_moduleMaps[module] = std::move(map); + return true; + } + LOG_WARN("PatternLoader: cache fallback also failed (file empty/invalid)"); + } + + // 6. Remote failed and no usable cache — give up. + LOG_WARN("PatternLoader: no source available for {} (last URL: {} HTTP {})", + ghSubdir, url, result.status); + std::string dllName = fs::path(dllPath).filename().string(); + ShowDownloadFailedPopup(dllName, sha256, ghSubdir); + g_failedModules.insert(module); + return false; +} + +void* FindPattern(HMODULE module, const char* funcName) +{ + // If the whole module's pattern file failed to load, stay quiet — the + // user already saw one popup and the main.log already has the warning. + // No point amplifying that into one log line per hook plus a second + // "missing functions" popup later. + if (g_failedModules.count(module)) { + return nullptr; + } + + uint32_t key = Fnv1aHash(funcName); + + auto mapIt = g_moduleMaps.find(module); + if (mapIt == g_moduleMaps.end()) { + // Load() was never called for this module. + LOG_WARN("PatternLoader: FindPattern called for module that was never loaded " + "('{}')", funcName); + g_missingFunctions.emplace_back(funcName); + return nullptr; + } + + auto& map = mapIt->second; + auto entryIt = map.find(key); + if (entryIt == map.end()) { + LOG_WARN("PatternLoader: no entry for '{}' (key=0x{:08X})", funcName, key); + g_missingFunctions.emplace_back(funcName); + return nullptr; + } + + const PatternEntry& entry = entryIt->second; + + // Priority 1: RVA direct offset + if (entry.rva != 0) { + void* addr = reinterpret_cast( + reinterpret_cast(module) + entry.rva); + LOG_DEBUG("PatternLoader: {} resolved via RVA 0x{:X}", funcName, entry.rva); + return addr; + } + + // Priority 2: byte-signature scan + if (!entry.sig.empty()) { + std::vector bytes, mask; + if (ParseSig(entry.sig, bytes, mask)) { + void* addr = ScanModule(module, bytes, mask); + if (addr) { + uintptr_t rva = reinterpret_cast(addr) - + reinterpret_cast(module); + LOG_DEBUG("PatternLoader: {} resolved via sig @ RVA 0x{:X}", + funcName, rva); + return addr; + } + LOG_WARN("PatternLoader: sig scan miss for '{}' (pattern parsed OK, " + "no match in module image)", funcName); + } else { + LOG_WARN("PatternLoader: malformed sig for '{}': '{}'", + funcName, entry.sig); + } + } else { + LOG_WARN("PatternLoader: entry for '{}' has neither rva nor sig", funcName); + } + + g_missingFunctions.emplace_back(funcName); + return nullptr; +} + +void ReportMissingFunctions() +{ + if (g_missingFunctions.empty()) return; + + // Build the list + std::string list; + for (const auto& name : g_missingFunctions) + list += " - " + name + "\n"; + g_missingFunctions.clear(); + + std::thread([list]() { + std::string msg = + "OpenSteamTool: some functions could not be located.\n\n" + "The following functions were not found in the signature file:\n" + + list + + "\nHooks for these functions are disabled for this session.\n\n" + "Please report this at:\n" + "https://github.com/OpenSteam001/OpenSteamTool/issues"; + MessageBoxA(nullptr, msg.c_str(), + "OpenSteamTool - Missing Signatures", + MB_OK | MB_ICONWARNING | MB_TOPMOST); + }).detach(); +} + +} // namespace PatternLoader diff --git a/src/Utils/Utils.h b/src/Utils/Utils.h new file mode 100644 index 0000000..4d4d0e5 --- /dev/null +++ b/src/Utils/Utils.h @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include + +namespace Utils { + inline std::filesystem::path GetDllDirectory() { + char dllPath[MAX_PATH]; + GetModuleFileNameA(g_hSelfModule, dllPath, MAX_PATH); + return std::filesystem::path(dllPath).parent_path(); + } +} \ No newline at end of file diff --git a/src/dllmain.cpp b/src/dllmain.cpp index 9db12a9..cbcbf2d 100644 --- a/src/dllmain.cpp +++ b/src/dllmain.cpp @@ -1,92 +1,94 @@ -#include "dllmain.h" -#include "Hook/HookManager.h" -#include "Utils/FileWatcher.h" -#include "Utils/PatternLoader.h" - -// prepare key runtime paths. -bool InitializeSteamComponents() -{ - if (!GetCurrentDirectoryA(MAX_PATH, SteamInstallPath)) { - return false; - } - sprintf_s(SteamclientPath, MAX_PATH, "%s\\steamclient64.dll", SteamInstallPath); - sprintf_s(SteamUIPath, MAX_PATH, "%s\\steamui.dll", SteamInstallPath); - sprintf_s(DiversionPath, MAX_PATH, "%s\\bin\\diversion.dll", SteamInstallPath); - sprintf_s(LuaDir, MAX_PATH, "%s\\config\\lua", SteamInstallPath); - sprintf_s(ConfigPath, MAX_PATH, "%s\\opensteamtool.toml", SteamInstallPath); - - client_hModule = LoadLibraryA(SteamclientPath); - if (!client_hModule) { - LOG_ERROR("LoadLibraryA failed: {} (err={})", SteamclientPath, GetLastError()); - return false; - } - LOG_INFO("Loaded diversion.dll from {}", SteamclientPath); - - ui_hModule = GetModuleHandleA("steamui.dll"); - if(!ui_hModule) { - LOG_ERROR("GetModuleHandleA failed for steamui.dll: err={}", GetLastError()); - return false; - } - return true; -} - -// All initialisation that touches the filesystem, calls LoadLibrary, scans -// memory, or installs detours runs here on a worker thread — we MUST NOT do -// any of that from inside DllMain (loader lock). -static DWORD WINAPI InitThread(LPVOID param) { - HMODULE selfModule = static_cast(param); - Log::Init(selfModule); - LOG_INFO("OpenSteamTool init thread started"); - - if (!InitializeSteamComponents()) { - LOG_ERROR("InitializeSteamComponents failed"); - return 1; - } - - Config::Load(ConfigPath); - Log::InitModules(); - - // Load pattern files for steamclient64.dll and steamui.dll. - // Each call computes the SHA-256 of the DLL on disk, checks the local - // cache, and downloads from GitHub if needed. Both calls are synchronous - // but run on this worker thread, never under the loader lock. - PatternLoader::Load(ui_hModule, SteamUIPath, "steamui"); - PatternLoader::Load(client_hModule, SteamclientPath, "steamclient"); - - std::vector watchDirs = Config::luaPaths; - watchDirs.push_back(std::string(LuaDir)); - for (const auto& dir : watchDirs) - LuaConfig::ParseDirectory(dir); - - FileWatcher::Start(watchDirs); - - SteamUI::CoreHook(); - SteamClient::CoreHook(); - - // Surface any functions that FindPattern() could not locate. - PatternLoader::ReportMissingFunctions(); - - g_HooksInstalled.store(true); - LOG_INFO("OpenSteamTool init complete"); - return 0; -} - -BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved) -{ - if (dwReason == DLL_PROCESS_ATTACH) - { - DisableThreadLibraryCalls(hModule); - // Hand off all real work to a worker thread to avoid running file I/O, - // LoadLibrary, and detour transactions under the loader lock. - HANDLE h = CreateThread(nullptr, 0, InitThread, hModule, 0, nullptr); - if (h) CloseHandle(h); - } - else if (dwReason == DLL_PROCESS_DETACH) - { - FileWatcher::Stop(); - SteamUI::CoreUnhook(); - SteamClient::CoreUnhook(); - } - - return TRUE; -} +#include "dllmain.h" +#include "Hook/HookManager.h" +#include "Utils/FileWatcher.h" +#include "Utils/PatternLoader.h" +#include "Utils/DllDirectory.h" + +// prepare key runtime paths. +bool InitializeSteamComponents() +{ + if (!GetCurrentDirectoryA(MAX_PATH, SteamInstallPath)) { + return false; + } + sprintf_s(SteamclientPath, MAX_PATH, "%s\\steamclient64.dll", SteamInstallPath); + sprintf_s(SteamUIPath, MAX_PATH, "%s\\steamui.dll", SteamInstallPath); + sprintf_s(DiversionPath, MAX_PATH, "%s\\bin\\diversion.dll", SteamInstallPath); + sprintf_s(DllDir, MAX_PATH, "%s", Utils::GetDllDirectory().string().c_str()); + sprintf_s(LuaDir, MAX_PATH, "%s\\config\\lua", DllDir); + sprintf_s(ConfigPath, MAX_PATH, "%s\\opensteamtool.toml", DllDir); + + client_hModule = LoadLibraryA(SteamclientPath); + if (!client_hModule) { + LOG_ERROR("LoadLibraryA failed: {} (err={})", SteamclientPath, GetLastError()); + return false; + } + LOG_INFO("Loaded diversion.dll from {}", SteamclientPath); + + ui_hModule = GetModuleHandleA("steamui.dll"); + if(!ui_hModule) { + LOG_ERROR("GetModuleHandleA failed for steamui.dll: err={}", GetLastError()); + return false; + } + return true; +} + +// All initialisation that touches the filesystem, calls LoadLibrary, scans +// memory, or installs detours runs here on a worker thread — we MUST NOT do +// any of that from inside DllMain (loader lock). +static DWORD WINAPI InitThread(LPVOID param) { + HMODULE selfModule = static_cast(param); + Log::Init(selfModule); + LOG_INFO("OpenSteamTool init thread started"); + + if (!InitializeSteamComponents()) { + LOG_ERROR("InitializeSteamComponents failed"); + return 1; + } + + Config::Load(ConfigPath); + Log::InitModules(); + + // Load pattern files for steamclient64.dll and steamui.dll. + // Each call computes the SHA-256 of the DLL on disk, checks the local + // cache, and downloads from GitHub if needed. Both calls are synchronous + // but run on this worker thread, never under the loader lock. + PatternLoader::Load(ui_hModule, SteamUIPath, "steamui"); + PatternLoader::Load(client_hModule, SteamclientPath, "steamclient"); + + std::vector watchDirs = Config::luaPaths; + watchDirs.push_back(std::string(LuaDir)); + for (const auto& dir : watchDirs) + LuaConfig::ParseDirectory(dir); + + FileWatcher::Start(watchDirs); + + SteamUI::CoreHook(); + SteamClient::CoreHook(); + + // Surface any functions that FindPattern() could not locate. + PatternLoader::ReportMissingFunctions(); + + g_HooksInstalled.store(true); + LOG_INFO("OpenSteamTool init complete"); + return 0; +} + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved) +{ + if (dwReason == DLL_PROCESS_ATTACH) + { + DisableThreadLibraryCalls(hModule); + // Hand off all real work to a worker thread to avoid running file I/O, + // LoadLibrary, and detour transactions under the loader lock. + HANDLE h = CreateThread(nullptr, 0, InitThread, hModule, 0, nullptr); + if (h) CloseHandle(h); + } + else if (dwReason == DLL_PROCESS_DETACH) + { + FileWatcher::Stop(); + SteamUI::CoreUnhook(); + SteamClient::CoreUnhook(); + } + + return TRUE; +} diff --git a/src/dllmain.h b/src/dllmain.h index ec81dd6..528aea5 100644 --- a/src/dllmain.h +++ b/src/dllmain.h @@ -1,40 +1,41 @@ -#ifndef DLLMAIN_H -#define DLLMAIN_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Steam/Types.h" -#include "Steam/Enums.h" -#include "Steam/Structs.h" -#include "Steam/Callback.h" -#include "Utils/LuaConfig.h" -#include "Utils/Log.h" -#include "Utils/Config.h" - - -inline HMODULE client_hModule = nullptr; -inline HMODULE ui_hModule = nullptr; - -inline std::atomic g_HooksInstalled{false}; -inline char SteamInstallPath[MAX_PATH] = {}; -inline char SteamclientPath[MAX_PATH] = {}; -inline char SteamUIPath[MAX_PATH] = {}; -inline char DiversionPath[MAX_PATH] = {}; -inline char LuaDir[MAX_PATH] = {}; -inline char ConfigPath[MAX_PATH] = {}; - -// The fake AppId used by -onlinefix (SpaceWar). -constexpr AppId_t kOnlineFixAppId = 480; - -#endif // DLLMAIN_H +#ifndef DLLMAIN_H +#define DLLMAIN_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Steam/Types.h" +#include "Steam/Enums.h" +#include "Steam/Structs.h" +#include "Steam/Callback.h" +#include "Utils/LuaConfig.h" +#include "Utils/Log.h" +#include "Utils/Config.h" + + +inline HMODULE client_hModule = nullptr; +inline HMODULE ui_hModule = nullptr; + +inline std::atomic g_HooksInstalled{false}; +inline char SteamInstallPath[MAX_PATH] = {}; +inline char SteamclientPath[MAX_PATH] = {}; +inline char SteamUIPath[MAX_PATH] = {}; +inline char DiversionPath[MAX_PATH] = {}; +inline char LuaDir[MAX_PATH] = {}; +inline char ConfigPath[MAX_PATH] = {}; +inline char DllDir[MAX_PATH] = {}; + +// The fake AppId used by -onlinefix (SpaceWar). +constexpr AppId_t kOnlineFixAppId = 480; + +#endif // DLLMAIN_H