diff --git a/README.md b/README.md index c79c77a..3373415 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ OpenSteamTool is a Windows DLL project built with CMake. - Unlock an unlimited number of unowned games. - Unlock all DLCs for unowned games. - Support auto load depot decryption keys from Lua config. -- Support auto manifest download via `steamrun` / `wudrm` upstream APIs(default is `wudrm`), or a custom Lua endpoint (see [Manifest via Lua](#manifest-via-lua)). +- Support auto manifest download via `opensteamtool` / `steamrun` / `wudrm` upstream APIs (default is `opensteamtool`), or a custom Lua endpoint (see [Manifest via Lua](#manifest-via-lua)). - Support downloading protected games or DLCs that require an access token. - Support binding manifest to prevent specific games from being updated. @@ -81,8 +81,8 @@ If no config file is found, built-in defaults are used — no auto-creation. level = "info" [manifest] -# Upstream API for depot manifest request codes. Options: "steamrun", "wudrm" -url = "steamrun" +# Upstream API for depot manifest request codes. Options: "opensteamtool", "steamrun", "wudrm" +url = "opensteamtool" # HTTP timeouts for manifest requests (milliseconds) timeout_resolve_ms = 5000 diff --git a/opensteamtool.example.toml b/opensteamtool.example.toml index 91ca70b..cf358f5 100644 --- a/opensteamtool.example.toml +++ b/opensteamtool.example.toml @@ -9,8 +9,9 @@ level = "info" [manifest] # Which upstream API to query for depot manifest request codes. -# "steamrun" → https://manifest.steam.run/api/manifest/{gid} -# "wudrm" → http://gmrc.wudrm.com/manifest/{gid} +# "opensteamtool" → https://manifest.opensteamtool.com/{gid} (default) +# "wudrm" → http://gmrc.wudrm.com/manifest/{gid} (recommended for China users) +# "steamrun" → https://manifest.steam.run/api/manifest/{gid} # If /config/lua/manifest.lua defines fetch_manifest_code(gid) or # fetch_manifest_code_ex(app_id, depot_id, gid), those Lua functions take # priority over the url setting below. @@ -43,7 +44,7 @@ level = "info" # end # return nil # end -url = "steamrun" +url = "opensteamtool" # HTTP timeouts for manifest requests (milliseconds). # timeout_resolve_ms — DNS resolution (default: 5000) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a27733d..708a685 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -84,6 +84,7 @@ add_library(OpenSteamTool SHARED Utils/VehCommon.cpp Utils/WinHttp.cpp Utils/FileWatcher.cpp + Utils/ManifestClient.cpp # Per-category hook modules Hook/HookManager.cpp diff --git a/src/Hook/Hooks_Manifest.cpp b/src/Hook/Hooks_Manifest.cpp index d52f54c..28977a6 100644 --- a/src/Hook/Hooks_Manifest.cpp +++ b/src/Hook/Hooks_Manifest.cpp @@ -1,243 +1,81 @@ -#include "Hooks_Manifest.h" -#include "HookMacros.h" -#include "dllmain.h" -#include "Utils/WinHttp.h" -#include -#include -#include - -// ═══════════════════════════════════════════════════════════════════ -// Manifest override hooks: -// BuildDepotDependency — patches depot entries' gid/size directly -// in the output vector (replaces the old KV-tree approach). -// -// GetManifestRequestCode — migrated to Hooks_NetPacket_Manifest -// (NetPacket layer, async HTTP via ContentServerDirectory#1). -// ═══════════════════════════════════════════════════════════════════ -namespace { - - // ── helper ───────────────────────────────────────────────────── - - std::string DepotEntryDebug(const DepotEntry& e) { - return std::format("DepotId={} AppId={} Gid={} Size={} Dlc={} Lcs={} Carry={} Shared={}", - e.DepotId, e.AppId, e.ManifestGid, e.ManifestSize, e.DlcAppId, - (int)e.LcsRequired, (int)e.bNotNewTarget, (int)e.SharedInstall); - } - - // ── BuildDepotDependency hook ────────────────────────────────── - // After Steam builds the depot list for an app, patch ManifestGid - // and ManifestSize for any depots we have overrides for. - - HOOK_FUNC(BuildDepotDependency, bool, void* pUserAppMgr, AppId_t AppId, - void* pUserConfig, CUtlVector* pDepotInfo, - CUtlVector* pSharedDepotInfo, void* pSteamApp, - uint32* pBuildId, bool* pbBetaFallback) - { - bool result = oBuildDepotDependency(pUserAppMgr, AppId, pUserConfig, - pDepotInfo, pSharedDepotInfo, pSteamApp, pBuildId, pbBetaFallback); - - LOG_MANIFEST_TRACE("BuildDepotDependency: AppId={} pUserConfig=0x{:X} result={} pSteamApp=0x{:X} pBuildId={} pbBetaFallback={}", - AppId, (uintptr_t)pUserConfig, result, (uintptr_t)pSteamApp, - pBuildId ? *pBuildId : 0, pbBetaFallback ? *pbBetaFallback : false); - if (pDepotInfo) { - LOG_MANIFEST_TRACE("pDepotInfo->nCount={}", pDepotInfo->m_Size); - for (uint32 i = 0; i < pDepotInfo->m_Size; ++i) { - LOG_MANIFEST_TRACE(" [{}] {}", i, DepotEntryDebug(pDepotInfo->m_Memory.m_pMemory[i])); - } - } - if (pSharedDepotInfo) { - LOG_MANIFEST_TRACE("pSharedDepotInfo->nCount={}", pSharedDepotInfo->m_Size); - for (uint32 i = 0; i < pSharedDepotInfo->m_Size; ++i) { - LOG_MANIFEST_TRACE(" shared[{}] {}", i, DepotEntryDebug(pSharedDepotInfo->m_Memory.m_pMemory[i])); - } - } - - if (!result) return result; - - const auto& overrides = LuaConfig::GetManifestOverrides(); - if (overrides.empty()) return result; - - if (pDepotInfo && pDepotInfo->m_Size) { - for (uint32 i = 0; i < pDepotInfo->m_Size; ++i) { - DepotEntry& e = pDepotInfo->m_Memory.m_pMemory[i]; - auto it = overrides.find(e.DepotId); - if (it != overrides.end()) { - // if size=0 in the override, keep the original size(affects download display but not the actual download) - uint64_t newSize = it->second.size ? it->second.size : e.ManifestSize; - LOG_MANIFEST_INFO("BuildDepotDependency: patching depot {} gid={}->{} size={}->{}", - e.DepotId, e.ManifestGid, it->second.gid, - e.ManifestSize, newSize); - e.ManifestGid = it->second.gid; - e.ManifestSize = newSize; - } - } - } - return result; - } - -} // anonymous namespace - -// ═══════════════════════════════════════════════════════════════════ -// Manifest HTTP providers (thread-safe via g_ConnectionMutex) -// ═══════════════════════════════════════════════════════════════════ -namespace Hooks_Manifest { - - std::mutex g_ConnectionMutex; - HINTERNET g_hSession = nullptr; - HINTERNET g_hConnect = nullptr; - bool g_tls = false; - - void EnsureConnection(const wchar_t* host, INTERNET_PORT port, bool tls) { - // Already connected to the right host — reuse - if (g_hSession && g_hConnect) - return; - - // Clean up stale handles - if (g_hConnect) { WinHttpCloseHandle(g_hConnect); g_hConnect = nullptr; } - if (g_hSession) { WinHttpCloseHandle(g_hSession); g_hSession = nullptr; } - - g_tls = tls; - g_hSession = WinHttpOpen(L"OpenSteamTool/1.0", - WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, - WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); - if (!g_hSession) return; - - WinHttpSetTimeouts(g_hSession, - Config::manifestTimeoutResolve, - Config::manifestTimeoutConnect, - Config::manifestTimeoutSend, - Config::manifestTimeoutRecv); - - g_hConnect = WinHttpConnect(g_hSession, host, port, 0); - if (!g_hConnect) { - WinHttpCloseHandle(g_hSession); - g_hSession = nullptr; - } - } - - void CloseConnection() { - if (g_hConnect) { WinHttpCloseHandle(g_hConnect); g_hConnect = nullptr; } - if (g_hSession) { WinHttpCloseHandle(g_hSession); g_hSession = nullptr; } - } - - // Try ExecuteEx on the persistent connection; on failure reset - // the connection so the next call reconnects. - WinHttp::Result DoGet(const wchar_t* path, const char* urlForLog) { - auto r = WinHttp::ExecuteEx(g_hSession, g_hConnect, g_tls, - L"GET", path, nullptr, 0, nullptr, - urlForLog); - if (!r.ok) - CloseConnection(); - return r; - } - - // ── HTTP providers ──────────────────────────────────────────── - - // GET https://manifest.steam.run/api/manifest/{gid} - // Response: {"content":"1666836470726104466"} - bool FetchSteamRun(uint64_t manifest_gid, uint64_t* outRequestCode) { - EnsureConnection(L"manifest.steam.run", INTERNET_DEFAULT_HTTPS_PORT, true); - if (!g_hConnect) return false; - - wchar_t path[80]; - swprintf_s(path, L"/api/manifest/%llu", manifest_gid); - - char urlForLog[128]; - snprintf(urlForLog, sizeof(urlForLog), "https://manifest.steam.run/api/manifest/%llu", manifest_gid); - - auto r = DoGet(path, urlForLog); - LOG_MANIFEST_INFO("Manifest steamrun status={} gid={}", r.status, manifest_gid); - - if (!r.ok || r.status != 200) return false; - - if (size_t key = r.body.find("\"content\""); key != std::string::npos) { - if (size_t q1 = r.body.find('"', key + 9); q1 != std::string::npos) { - if (size_t q2 = r.body.find('"', q1 + 1); q2 != std::string::npos) { - uint64_t code = 0; - auto [_, ec] = std::from_chars( - r.body.data() + q1 + 1, r.body.data() + q2, code); - if (ec == std::errc{}) { - *outRequestCode = code; - return true; - } - } - } - } - return false; - } - - // ── provider: gmrc.wudrm.com ─────────────────────────────────── - // GET http://gmrc.wudrm.com/manifest/{gid} - // Response: plain-text uint64_t, e.g. "10570517747114638659" - bool FetchWudrm(uint64_t manifest_gid, uint64_t* outRequestCode) { - EnsureConnection(L"gmrc.wudrm.com", INTERNET_DEFAULT_HTTP_PORT, false); - if (!g_hConnect) return false; - - wchar_t path[80]; - swprintf_s(path, L"/manifest/%llu", manifest_gid); - - char urlForLog[128]; - snprintf(urlForLog, sizeof(urlForLog), "http://gmrc.wudrm.com/manifest/%llu", manifest_gid); - - auto r = DoGet(path, urlForLog); - LOG_MANIFEST_INFO("Manifest wudrm status={} gid={}", r.status, manifest_gid); - - if (!r.ok || r.status != 200) return false; - - uint64_t code = 0; - auto [_, ec] = std::from_chars(r.body.data(), r.body.data() + r.body.size(), code); - if (ec == std::errc{}) { - *outRequestCode = code; - return true; - } - return false; - } - - // ── resolve (single-provider, no fallback) ──────────────────── - bool FetchManifestRequestCode(uint64_t manifestGid, uint64_t* outRequestCode, AppId_t AppId, AppId_t DepotId) { - std::lock_guard lock(g_ConnectionMutex); - - // Try extended Lua function first (receives app_id, depot_id, gid) - if (AppId && DepotId && LuaConfig::HasManifestCodeFuncEx()) { - if (LuaConfig::CallManifestFetchCodeEx(AppId, DepotId, manifestGid, outRequestCode)) { - LOG_MANIFEST_INFO("Manifest gid={} resolved via fetch_manifest_code_ex", manifestGid); - return true; - } - LOG_MANIFEST_WARN("Manifest gid={} fetch_manifest_code_ex returned nil, trying fetch_manifest_code", manifestGid); - } - - // Fall back to original Lua function (receives gid only) - if (LuaConfig::HasManifestCodeFunc()) { - if (LuaConfig::CallManifestFetchCode(manifestGid, outRequestCode)) { - LOG_MANIFEST_INFO("Manifest gid={} resolved via manifest.lua", manifestGid); - return true; - } - LOG_MANIFEST_WARN("Manifest gid={} lua returned nil, falling back to config", manifestGid); - } - - switch (Config::manifestUrl) { - case Config::ManifestUrl::Wudrm: - return FetchWudrm(manifestGid, outRequestCode); - case Config::ManifestUrl::SteamRun: - default: - return FetchSteamRun(manifestGid, outRequestCode); - } - } - - // ═══════════════════════════════════════════════════════════════ - // Install / Uninstall - // ═══════════════════════════════════════════════════════════════ - - void Install() { - HOOK_BEGIN(); - INSTALL_HOOK_C(BuildDepotDependency); - HOOK_END(); - } - - void Uninstall() { - UNHOOK_BEGIN(); - UNINSTALL_HOOK(BuildDepotDependency); - UNHOOK_END(); - CloseConnection(); - } -} +#include "Hooks_Manifest.h" +#include "HookMacros.h" +#include "dllmain.h" +#include + +// ═══════════════════════════════════════════════════════════════════ +// Manifest override hooks: +// BuildDepotDependency — patches depot entries' gid/size directly +// in the output vector (replaces the old KV-tree approach). +// ═══════════════════════════════════════════════════════════════════ +namespace { + + std::string DepotEntryDebug(const DepotEntry& e) { + return std::format("DepotId={} AppId={} Gid={} Size={} Dlc={} Lcs={} Carry={} Shared={}", + e.DepotId, e.AppId, e.ManifestGid, e.ManifestSize, e.DlcAppId, + (int)e.LcsRequired, (int)e.bNotNewTarget, (int)e.SharedInstall); + } + + HOOK_FUNC(BuildDepotDependency, bool, void* pUserAppMgr, AppId_t AppId, + void* pUserConfig, CUtlVector* pDepotInfo, + CUtlVector* pSharedDepotInfo, void* pSteamApp, + uint32* pBuildId, bool* pbBetaFallback) + { + bool result = oBuildDepotDependency(pUserAppMgr, AppId, pUserConfig, + pDepotInfo, pSharedDepotInfo, pSteamApp, pBuildId, pbBetaFallback); + + LOG_MANIFEST_TRACE("BuildDepotDependency: AppId={} pUserConfig=0x{:X} result={} pSteamApp=0x{:X} pBuildId={} pbBetaFallback={}", + AppId, (uintptr_t)pUserConfig, result, (uintptr_t)pSteamApp, + pBuildId ? *pBuildId : 0, pbBetaFallback ? *pbBetaFallback : false); + if (pDepotInfo) { + LOG_MANIFEST_TRACE("pDepotInfo->nCount={}", pDepotInfo->m_Size); + for (uint32 i = 0; i < pDepotInfo->m_Size; ++i) { + LOG_MANIFEST_TRACE(" [{}] {}", i, DepotEntryDebug(pDepotInfo->m_Memory.m_pMemory[i])); + } + } + if (pSharedDepotInfo) { + LOG_MANIFEST_TRACE("pSharedDepotInfo->nCount={}", pSharedDepotInfo->m_Size); + for (uint32 i = 0; i < pSharedDepotInfo->m_Size; ++i) { + LOG_MANIFEST_TRACE(" shared[{}] {}", i, DepotEntryDebug(pSharedDepotInfo->m_Memory.m_pMemory[i])); + } + } + + if (!result) return result; + + const auto& overrides = LuaConfig::GetManifestOverrides(); + if (overrides.empty()) return result; + + if (pDepotInfo && pDepotInfo->m_Size) { + for (uint32 i = 0; i < pDepotInfo->m_Size; ++i) { + DepotEntry& e = pDepotInfo->m_Memory.m_pMemory[i]; + auto it = overrides.find(e.DepotId); + if (it != overrides.end()) { + // if size=0 in the override, keep the original size(affects download display but not the actual download) + uint64_t newSize = it->second.size ? it->second.size : e.ManifestSize; + LOG_MANIFEST_INFO("BuildDepotDependency: patching depot {} gid={}->{} size={}->{}", + e.DepotId, e.ManifestGid, it->second.gid, + e.ManifestSize, newSize); + e.ManifestGid = it->second.gid; + e.ManifestSize = newSize; + } + } + } + return result; + } + +} // anonymous namespace + +namespace Hooks_Manifest { + + void Install() { + HOOK_BEGIN(); + INSTALL_HOOK_C(BuildDepotDependency); + HOOK_END(); + } + + void Uninstall() { + UNHOOK_BEGIN(); + UNINSTALL_HOOK(BuildDepotDependency); + UNHOOK_END(); + } +} diff --git a/src/Hook/Hooks_Manifest.h b/src/Hook/Hooks_Manifest.h index 21097f8..9635c1a 100644 --- a/src/Hook/Hooks_Manifest.h +++ b/src/Hook/Hooks_Manifest.h @@ -1,14 +1,6 @@ -#pragma once -#include "Steam/Types.h" - -namespace Hooks_Manifest { - void Install(); - void Uninstall(); - - // Fetch a manifest request code from online providers. - // Thread-safe — serialises access to the underlying WinHTTP connection. - // Returns true and sets *outRequestCode on success. - // AppId and DepotId are optional; when provided, enables fetch_manifest_code_ex. - bool FetchManifestRequestCode(uint64_t manifestGid, uint64_t* outRequestCode, - AppId_t AppId = 0, AppId_t DepotId = 0); -} +#pragma once + +namespace Hooks_Manifest { + void Install(); + void Uninstall(); +} diff --git a/src/Hook/Hooks_NetPacket.cpp b/src/Hook/Hooks_NetPacket.cpp index ca6af32..740022a 100644 --- a/src/Hook/Hooks_NetPacket.cpp +++ b/src/Hook/Hooks_NetPacket.cpp @@ -1,10 +1,10 @@ #include "Hooks_NetPacket.h" -#include "Hooks_Manifest.h" #include "Hooks_Misc.h" #include "HookMacros.h" #include "dllmain.h" #include "Utils/AppTicket.h" #include "Utils/Hash.h" +#include "Utils/ManifestClient.h" #include #include #include @@ -488,7 +488,7 @@ namespace Hooks_NetPacket_Manifest { auto task = std::async(std::launch::async, [manifestGid, depotId, appId]() -> uint64 { uint64 code = 0; - Hooks_Manifest::FetchManifestRequestCode(manifestGid, &code, appId, depotId); + ManifestClient::FetchManifestRequestCode(manifestGid, &code, appId, depotId); return code; }); diff --git a/src/Utils/Config.cpp b/src/Utils/Config.cpp index 05692bd..f566766 100644 --- a/src/Utils/Config.cpp +++ b/src/Utils/Config.cpp @@ -1,5 +1,6 @@ #include "Config.h" #include "Log.h" +#include "ManifestClient.h" #include #include @@ -20,8 +21,8 @@ namespace Config { // [manifest] if (auto manifest = tbl["manifest"].as_table()) { if (auto val = (*manifest)["url"].value()) { - if (*val == "wudrm") - manifestUrl = ManifestUrl::Wudrm; + if (!ManifestClient::SetProvider(*val)) + LOG_WARN("Unknown manifest.url \"{}\", keeping default", *val); } if (auto val = (*manifest)["timeout_resolve_ms"].value()) manifestTimeoutResolve = static_cast(*val); @@ -67,7 +68,7 @@ namespace Config { } LOG_INFO("Config loaded: manifest.url={} log.level={} lua.paths={} pattern.mirror={}", - manifestUrl == ManifestUrl::Wudrm ? "wudrm" : "steamrun", + ManifestClient::ActiveProviderName(), [&](){ switch (logLevel) { case LogLevel::Trace: return "trace"; diff --git a/src/Utils/Config.h b/src/Utils/Config.h index a0cb8f0..c1f02f9 100644 --- a/src/Utils/Config.h +++ b/src/Utils/Config.h @@ -6,14 +6,11 @@ namespace Config { - enum class ManifestUrl { SteamRun, Wudrm }; - enum class LogLevel { Trace, Debug, Info, Warn, Error }; void Load(const std::string& configPath); - // [manifest] - inline ManifestUrl manifestUrl = ManifestUrl::Wudrm; + // [manifest] — provider selection lives in ManifestClient (table-driven). inline DWORD manifestTimeoutResolve = 5000; inline DWORD manifestTimeoutConnect = 5000; inline DWORD manifestTimeoutSend = 10000; diff --git a/src/Utils/ManifestClient.cpp b/src/Utils/ManifestClient.cpp new file mode 100644 index 0000000..5df7d1b --- /dev/null +++ b/src/Utils/ManifestClient.cpp @@ -0,0 +1,202 @@ +#include "ManifestClient.h" +#include "Config.h" +#include "Log.h" +#include "LuaConfig.h" +#include "WinHttp.h" +#include +#include +#include +#include + +namespace ManifestClient { + + // ── parsers ──────────────────────────────────────────────────── + using Parser = bool (*)(std::string_view body, uint64_t* out); + + static bool ParsePlainUint(std::string_view body, uint64_t* out) { + uint64_t code = 0; + auto [_, ec] = std::from_chars(body.data(), body.data() + body.size(), code); + if (ec != std::errc{}) return false; + *out = code; + return true; + } + + static bool ParseSteamRunJson(std::string_view body, uint64_t* out) { + size_t key = body.find("\"content\""); + if (key == std::string_view::npos) return false; + size_t q1 = body.find('"', key + 9); + if (q1 == std::string_view::npos) return false; + size_t q2 = body.find('"', q1 + 1); + if (q2 == std::string_view::npos) return false; + return ParsePlainUint(body.substr(q1 + 1, q2 - q1 - 1), out); + } + + // ── provider table ──────────────────────────────────────────── + // + // Adding a new provider: add one row to kProviders below. + // host / port / tls / path are all derived from the URL template + // by Make() at compile time. + + struct Provider { + std::string_view name; // matches [manifest] url = "..." + const char* urlTemplate; // full literal with one %llu — for log & path + std::string_view host; // ASCII slice into urlTemplate + const char* pathFormat; // suffix of urlTemplate, null-terminated, e.g. "/path/%llu" + INTERNET_PORT port; + bool tls; + Parser parse; + }; + + consteval Provider Make(std::string_view name, const char* url, Parser parse) { + std::string_view sv{url}; + bool tls = false; + INTERNET_PORT port = INTERNET_DEFAULT_HTTP_PORT; + if (sv.starts_with("https://")) { + tls = true; + port = INTERNET_DEFAULT_HTTPS_PORT; + sv.remove_prefix(8); + } + else if (sv.starts_with("http://")) { + sv.remove_prefix(7); + } + + size_t slash = sv.find('/'); + std::string_view hostPart = sv.substr(0, slash); + // sv.data() points into url; the suffix at slash is null-terminated + // because url itself is a null-terminated literal. + const char* pathFormat = (slash == std::string_view::npos) ? "/" : (sv.data() + slash); + + std::string_view host = hostPart; + if (size_t colon = hostPart.find(':'); colon != std::string_view::npos) { + host = hostPart.substr(0, colon); + INTERNET_PORT custom = 0; + for (char c : hostPart.substr(colon + 1)) + custom = static_cast(custom * 10 + (c - '0')); + port = custom; + } + return {name, url, host, pathFormat, port, tls, parse}; + } + + static constexpr Provider kProviders[] = { + Make("opensteamtool", "https://manifest.opensteamtool.com/%llu", ParsePlainUint), + Make("wudrm", "http://gmrc.wudrm.com/manifest/%llu", ParsePlainUint), + Make("steamrun", "https://manifest.steam.run/api/manifest/%llu", ParseSteamRunJson), + }; + + static const Provider* g_active = &kProviders[0]; // opensteamtool + + bool SetProvider(std::string_view name) { + for (const auto& p : kProviders) + if (p.name == name) { + g_active = &p; + return true; + } + return false; + } + + const char* ActiveProviderName() { + return g_active->name.data(); + } + + // ── connection ──────────────────────────────────────────────── + + static std::mutex g_mutex; + static HINTERNET g_hSession = nullptr; + static HINTERNET g_hConnect = nullptr; + static const Provider* g_connected = nullptr; // provider g_hConnect is tied to + + static void CloseConnection() { + if (g_hConnect) { WinHttpCloseHandle(g_hConnect); g_hConnect = nullptr; } + if (g_hSession) { WinHttpCloseHandle(g_hSession); g_hSession = nullptr; } + g_connected = nullptr; + } + + // Widen an ASCII string_view into a wide stack buffer (null-terminated). + template + static void WidenAscii(std::string_view s, wchar_t (&dst)[N]) { + size_t n = (std::min)(s.size(), N - 1); // parens defeat the windows.h min macro + for (size_t i = 0; i < n; ++i) dst[i] = static_cast(s[i]); + dst[n] = 0; + } + + static void EnsureConnection() { + if (g_hConnect && g_connected == g_active) return; + CloseConnection(); + + g_hSession = WinHttpOpen(L"OpenSteamTool/1.0", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (!g_hSession) return; + + WinHttpSetTimeouts(g_hSession, + Config::manifestTimeoutResolve, Config::manifestTimeoutConnect, + Config::manifestTimeoutSend, Config::manifestTimeoutRecv); + + wchar_t hostW[256]; + WidenAscii(g_active->host, hostW); + + g_hConnect = WinHttpConnect(g_hSession, hostW, g_active->port, 0); + if (!g_hConnect) { + WinHttpCloseHandle(g_hSession); + g_hSession = nullptr; + return; + } + g_connected = g_active; + } + + void Shutdown() { + std::lock_guard lock(g_mutex); + CloseConnection(); + } + + // ── fetch ───────────────────────────────────────────────────── + + static bool FetchActive(uint64_t gid, uint64_t* outCode) { + EnsureConnection(); + if (!g_hConnect) return false; + const Provider& p = *g_active; + + char pathN[160]; + std::snprintf(pathN, sizeof(pathN), p.pathFormat, gid); + wchar_t pathW[160]; + WidenAscii(pathN, pathW); + + char urlLog[256]; + std::snprintf(urlLog, sizeof(urlLog), p.urlTemplate, gid); + + auto r = WinHttp::ExecuteEx(g_hSession, g_hConnect, p.tls, + L"GET", pathW, nullptr, 0, nullptr, urlLog); + if (!r.ok) CloseConnection(); + + LOG_MANIFEST_INFO("Manifest {} status={} gid={}", p.name, r.status, gid); + + if (!r.ok || r.status != 200) return false; + return p.parse(r.body, outCode); + } + + // ── public ──────────────────────────────────────────────────── + + bool FetchManifestRequestCode(uint64_t manifestGid, uint64_t* outRequestCode, + AppId_t appId, AppId_t depotId) + { + std::lock_guard lock(g_mutex); + + if (appId && depotId && LuaConfig::HasManifestCodeFuncEx()) { + if (LuaConfig::CallManifestFetchCodeEx(appId, depotId, manifestGid, outRequestCode)) { + LOG_MANIFEST_INFO("Manifest gid={} resolved via fetch_manifest_code_ex", manifestGid); + return true; + } + LOG_MANIFEST_WARN("Manifest gid={} fetch_manifest_code_ex returned nil, trying fetch_manifest_code", manifestGid); + } + + if (LuaConfig::HasManifestCodeFunc()) { + if (LuaConfig::CallManifestFetchCode(manifestGid, outRequestCode)) { + LOG_MANIFEST_INFO("Manifest gid={} resolved via manifest.lua", manifestGid); + return true; + } + LOG_MANIFEST_WARN("Manifest gid={} lua returned nil, falling back to config", manifestGid); + } + + return FetchActive(manifestGid, outRequestCode); + } +} diff --git a/src/Utils/ManifestClient.h b/src/Utils/ManifestClient.h new file mode 100644 index 0000000..2f19c30 --- /dev/null +++ b/src/Utils/ManifestClient.h @@ -0,0 +1,29 @@ +#pragma once +#include "Steam/Types.h" +#include + +// ───────────────────────────────────────────────────────────────── +// ManifestClient — HTTP client for depot manifest request codes. +// Provider table is internal (see kProviders in ManifestClient.cpp); +// adding a new provider only requires one row there. +// +// Thread-safe — serialises access to the underlying WinHTTP connection. +// ───────────────────────────────────────────────────────────────── +namespace ManifestClient { + + // Select the active provider by its string name (matches kProviders[i].name). + // Returns false if no provider matches; the previous selection is kept. + bool SetProvider(std::string_view name); + + // Name of the currently active provider (for logging / diagnostics). + const char* ActiveProviderName(); + + // Resolve a manifest GID to its request code. Tries Lua first + // (fetch_manifest_code_ex, then fetch_manifest_code), then the + // active provider. Returns true and sets *outRequestCode on success. + bool FetchManifestRequestCode(uint64_t manifestGid, uint64_t* outRequestCode, + AppId_t appId = 0, AppId_t depotId = 0); + + // Tear down the cached WinHTTP connection (call at unload). + void Shutdown(); +}