From 28176959fec58abaf320af0f8c7b0d1c10a7e311 Mon Sep 17 00:00:00 2001 From: OpenSteam001 <248184252+OpenSteam001@users.noreply.github.com> Date: Thu, 28 May 2026 17:14:36 +0800 Subject: [PATCH] refactor:inject fake licenses into original steamclient module replacing the copied diversion module --- src/CMakeLists.txt | 1 + src/Hook/HookMacros.h | 55 ++++-- src/Hook/HookManager.cpp | 14 +- src/Hook/Hooks_CallBack.cpp | 29 ++++ src/Hook/Hooks_CallBack.h | 7 + src/Hook/Hooks_Decryption.cpp | 4 +- src/Hook/Hooks_IPC.cpp | 17 +- src/Hook/Hooks_IPC_ISteamUser.cpp | 6 +- src/Hook/Hooks_IPC_ISteamUtils.cpp | 2 +- src/Hook/Hooks_KeyValues.cpp | 34 ++-- src/Hook/Hooks_Manifest.cpp | 2 +- src/Hook/Hooks_Misc.cpp | 270 ++++++++--------------------- src/Hook/Hooks_Misc.h | 15 +- src/Hook/Hooks_NetPacket.cpp | 11 +- src/Hook/Hooks_Package.cpp | 188 ++++++++++++++++---- src/Hook/Hooks_Package.h | 6 + src/Hook/Hooks_SteamUI.cpp | 138 ++------------- src/Hook/Hooks_SteamUI.h | 11 +- src/Steam/Enums.h | 44 ++++- src/Steam/Structs.h | 89 +++++++++- src/Utils/FileWatcher.cpp | 4 +- src/Utils/VehCommon.cpp | 97 ++++++++++- src/Utils/VehCommon.h | 216 ++++++++++++++++++----- src/dllmain.cpp | 33 ++-- src/dllmain.h | 4 +- 25 files changed, 789 insertions(+), 508 deletions(-) create mode 100644 src/Hook/Hooks_CallBack.cpp create mode 100644 src/Hook/Hooks_CallBack.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7180198..a27733d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -87,6 +87,7 @@ add_library(OpenSteamTool SHARED # Per-category hook modules Hook/HookManager.cpp + Hook/Hooks_CallBack.cpp Hook/Hooks_Decryption.cpp Hook/Hooks_IPC.cpp Hook/Hooks_IPC_ISteamUser.cpp diff --git a/src/Hook/HookMacros.h b/src/Hook/HookMacros.h index 608b768..b8f8618 100644 --- a/src/Hook/HookMacros.h +++ b/src/Hook/HookMacros.h @@ -3,7 +3,7 @@ // ───────────────────────────────────────────────────────────────── // Hook boilerplate elimination macros. // -// Convention: macros ending in _D use diversion_hMdoule as module. +// Convention: macros ending in _D use client_hModule as module. // Standard macros (no _D suffix) take an explicit module argument. // ───────────────────────────────────────────────────────────────── @@ -44,26 +44,21 @@ inline name##_t o##name = nullptr; \ ret __fastcall hk##name(__VA_ARGS__) + // ── install ───────────────────────────────────────────────────── // Call between HOOK_BEGIN / HOOK_END. #define INSTALL_HOOK(module, name) \ - do { \ - void* _p_ = FIND_SIG(module, name); \ - if (_p_) { \ - o##name = (name##_t)_p_; \ - DetourAttach(reinterpret_cast(&o##name), \ - reinterpret_cast(hk##name)); \ - } \ - } while (0) +do { \ + void* _p_ = FIND_SIG(module, name); \ + if (_p_) { \ + o##name = (name##_t)_p_; \ + DetourAttach(reinterpret_cast(&o##name), \ + reinterpret_cast(hk##name)); \ + } \ +} while (0) -#define INSTALL_HOOK_D(name) INSTALL_HOOK(diversion_hMdoule, name) - -// ── resolve ───────────────────────────────────────────────────── -// Find signature → cast to name##_t → assign to o##name. No Detours. -#define RESOLVE(module, name) \ - o##name = reinterpret_cast(FIND_SIG(module, name)) - -#define RESOLVE_D(name) RESOLVE(diversion_hMdoule, name) +#define INSTALL_HOOK_C(name) INSTALL_HOOK(client_hModule, name) +#define INSTALL_HOOK_U(name) INSTALL_HOOK(ui_hModule, name) // ── uninstall ─────────────────────────────────────────────────── // Call between UNHOOK_BEGIN / UNHOOK_END. @@ -75,3 +70,29 @@ o##name = nullptr; \ } \ } while (0) +#define UNINSTALL_HOOK_C(name) UNINSTALL_HOOK(name) +#define UNINSTALL_HOOK_U(name) UNINSTALL_HOOK(name) + +// ── resolve function definition ──────────────────────────────────── +// RESOLVE_FUNC(CUtlMemoryGrow, void*, CUtlVector*, int); +// generates: +// using CUtlMemoryGrow_t = void*(__fastcall*)(CUtlVector*, int); +// inline CUtlMemoryGrow_t oCUtlMemoryGrow = nullptr; +#define RESOLVE_FUNC(name, ret, ...) \ + using name##_t = ret(__fastcall*)(__VA_ARGS__); \ + inline name##_t o##name = nullptr + +// ── resolve ───────────────────────────────────────────────────── +// Find signature → cast to name##_t → assign to o##name. +#define RESOLVE(module, name) \ +do { \ + void* _p_ = FIND_SIG(module, name); \ + if (_p_) { \ + o##name = reinterpret_cast(_p_); \ + } \ +} while (0) + +#define RESOLVE_C(name) RESOLVE(client_hModule, name) +#define RESOLVE_U(name) RESOLVE(ui_hModule, name) + + diff --git a/src/Hook/HookManager.cpp b/src/Hook/HookManager.cpp index c4ca51b..0d6bc63 100644 --- a/src/Hook/HookManager.cpp +++ b/src/Hook/HookManager.cpp @@ -1,4 +1,5 @@ #include "HookManager.h" +#include "Hooks_CallBack.h" #include "Hooks_Decryption.h" #include "Hooks_IPC.h" #include "Hooks_KeyValues.h" @@ -7,17 +8,23 @@ #include "Hooks_NetPacket.h" #include "Hooks_Package.h" #include "Hooks_SteamUI.h" +#include "Utils/VehCommon.h" namespace SteamUI { - void CoreHook() { Hooks_SteamUI::Install(); } - void CoreUnhook() { Hooks_SteamUI::Uninstall(); } + void CoreHook() { + Hooks_SteamUI::Install(); + } + void CoreUnhook() { + Hooks_SteamUI::Uninstall(); + } } namespace SteamClient { void CoreHook() { + Hooks_CallBack::Install(); Hooks_Decryption::Install(); Hooks_IPC::Install(); // Hooks_KeyValues::Install(); @@ -28,6 +35,7 @@ namespace SteamClient { } void CoreUnhook() { + Hooks_CallBack::Uninstall(); Hooks_Decryption::Uninstall(); Hooks_IPC::Uninstall(); // Hooks_KeyValues::Uninstall(); @@ -35,5 +43,7 @@ namespace SteamClient { Hooks_Misc::Uninstall(); Hooks_NetPacket::Uninstall(); Hooks_Package::Uninstall(); + VehCommon::DisarmAll(); + VehCommon::RemoveHandler(); } } diff --git a/src/Hook/Hooks_CallBack.cpp b/src/Hook/Hooks_CallBack.cpp new file mode 100644 index 0000000..46f4f11 --- /dev/null +++ b/src/Hook/Hooks_CallBack.cpp @@ -0,0 +1,29 @@ +#include "Hooks_CallBack.h" +#include "HookMacros.h" +#include "dllmain.h" +namespace { + + HOOK_FUNC(SendCallbackToPipe, bool, void* pSteamEngine, HSteamPipe hSteamPipe, + HSteamUser iClientUser, int iCallback, void* pCallbackData, int cubCallbackData) { + // ── Callback modifier dispatch ───────────────────────────────────────── + // Intercept callbacks before they reach the pipe and modify data in-place. + // To add a new callback: add an else-if branch here. + + return oSendCallbackToPipe(pSteamEngine, hSteamPipe, iClientUser, + iCallback, pCallbackData, cubCallbackData); + } +} + +namespace Hooks_CallBack { + void Install() { + HOOK_BEGIN(); + INSTALL_HOOK_C(SendCallbackToPipe); + HOOK_END(); + } + + void Uninstall() { + UNHOOK_BEGIN(); + UNINSTALL_HOOK_C(SendCallbackToPipe); + UNHOOK_END(); + } +} diff --git a/src/Hook/Hooks_CallBack.h b/src/Hook/Hooks_CallBack.h new file mode 100644 index 0000000..c1d3cc9 --- /dev/null +++ b/src/Hook/Hooks_CallBack.h @@ -0,0 +1,7 @@ +#pragma once + +namespace Hooks_CallBack { + // CallBack hook: handles various callback events. + void Install(); + void Uninstall(); +} \ No newline at end of file diff --git a/src/Hook/Hooks_Decryption.cpp b/src/Hook/Hooks_Decryption.cpp index 79a628e..bff22b6 100644 --- a/src/Hook/Hooks_Decryption.cpp +++ b/src/Hook/Hooks_Decryption.cpp @@ -30,13 +30,13 @@ namespace { namespace Hooks_Decryption { void Install() { HOOK_BEGIN(); - INSTALL_HOOK_D(LoadDepotDecryptionKey); + INSTALL_HOOK_C(LoadDepotDecryptionKey); HOOK_END(); } void Uninstall() { UNHOOK_BEGIN(); - UNINSTALL_HOOK(LoadDepotDecryptionKey); + UNINSTALL_HOOK_C(LoadDepotDecryptionKey); UNHOOK_END(); } } diff --git a/src/Hook/Hooks_IPC.cpp b/src/Hook/Hooks_IPC.cpp index cb8af2c..dded03f 100644 --- a/src/Hook/Hooks_IPC.cpp +++ b/src/Hook/Hooks_IPC.cpp @@ -8,8 +8,7 @@ namespace { - using GetPipeClient_t = CSteamPipeClient*(*)(void* pEngine, HSteamPipe hSteamPipe); - GetPipeClient_t oGetPipeClient = nullptr; + RESOLVE_FUNC(GetPipeClient, CSteamPipeClient*, void* pEngine, HSteamPipe hSteamPipe); static CSteamPipeClient* GetPipe(void* pServer, HSteamPipe hSteamPipe) { return oGetPipeClient ? oGetPipeClient(pServer, hSteamPipe) : nullptr; @@ -36,7 +35,7 @@ namespace { void* pServer, HSteamPipe hSteamPipe, CUtlBuffer* pRead, CUtlBuffer* pWrite) { - auto* pipe = GetPipe(pServer, hSteamPipe); + CSteamPipeClient* pipe = GetPipe(pServer, hSteamPipe); // ── Parse header, find handler ────────────────────────── const IpcHandlerEntry* entry = nullptr; @@ -60,14 +59,14 @@ namespace { LOG_IPC_DEBUG("[InterfaceCall] {} {} realAppId={},AppId={}", entry->name, pipe->DebugString(), Hooks_Misc::ResolveAppId(), - Hooks_Misc::GetAppIDForCurrentPipe() + Hooks_Misc::GetAppIDForCurrentPipeWrap() ); } else { LOG_IPC_TRACE("[InterfaceCall(unhandled)]{}::0x{:08X} {} realAppId={},AppId={}", EIPCInterfaceName(iface), funcHash, pipe->DebugString(), Hooks_Misc::ResolveAppId(), - Hooks_Misc::GetAppIDForCurrentPipe() + Hooks_Misc::GetAppIDForCurrentPipeWrap() ); } } else { @@ -101,23 +100,21 @@ namespace Hooks_IPC { } void Install() { - RESOLVE_D(GetPipeClient); + RESOLVE_C(GetPipeClient); // Interface modules register their handlers here. Hooks_IPC_ISteamUser::Register(); Hooks_IPC_ISteamUtils::Register(); HOOK_BEGIN(); - INSTALL_HOOK_D(IPCProcessMessage); + INSTALL_HOOK_C(IPCProcessMessage); HOOK_END(); } void Uninstall() { UNHOOK_BEGIN(); - UNINSTALL_HOOK(IPCProcessMessage); + UNINSTALL_HOOK_C(IPCProcessMessage); UNHOOK_END(); - oGetPipeClient = nullptr; - g_Handlers.clear(); } } diff --git a/src/Hook/Hooks_IPC_ISteamUser.cpp b/src/Hook/Hooks_IPC_ISteamUser.cpp index ccd9aae..4a82629 100644 --- a/src/Hook/Hooks_IPC_ISteamUser.cpp +++ b/src/Hook/Hooks_IPC_ISteamUser.cpp @@ -108,7 +108,11 @@ namespace { const uint32 ticketSize = static_cast(ticket.size()); const int32 totalSize = 1 + 1 + 4 + ticketSize; - Hooks_Misc::EnsureBufferSize(pWrite, totalSize); + if (!Hooks_Misc::EnsureBufferSize(pWrite, totalSize)) { + LOG_IPC_DEBUG("GetEncryptedAppTicket: AppId={} - failed to ensure buffer size", appId); + return; + } + pWrite->m_Put = totalSize; uint8* base = pWrite->Base(); base[0] = RESPONSE_PREFIX; diff --git a/src/Hook/Hooks_IPC_ISteamUtils.cpp b/src/Hook/Hooks_IPC_ISteamUtils.cpp index f11e671..704ec01 100644 --- a/src/Hook/Hooks_IPC_ISteamUtils.cpp +++ b/src/Hook/Hooks_IPC_ISteamUtils.cpp @@ -86,7 +86,7 @@ namespace { const auto* req = reinterpret_cast( pRead->Base() + OFFSET_ARGS); - AppId_t appId = Hooks_Misc::GetAppIDForCurrentPipe(); + AppId_t appId = Hooks_Misc::GetAppIDForCurrentPipeWrap(); LOG_IPC_DEBUG("GetAPICallResult: hAsyncCall=0x{:016X} AppId={} iCallback={} cubCallback={}", req->hSteamAPICall, appId, req->iCallbackExpected, req->cubCallback); for (auto& entry : g_GacrDispatch) { diff --git a/src/Hook/Hooks_KeyValues.cpp b/src/Hook/Hooks_KeyValues.cpp index b759fe5..b715a1f 100644 --- a/src/Hook/Hooks_KeyValues.cpp +++ b/src/Hook/Hooks_KeyValues.cpp @@ -7,17 +7,12 @@ namespace { - HOOK_FUNC(ReadAsBinary, bool, KeyValues* root, void* buf, int depth, - bool textMode, void* symTable) { - bool ok = oReadAsBinary(root, buf, depth, textMode, symTable); - return ok; + RESOLVE_FUNC(FindOrCreateKey, KeyValues*, KeyValues* parent, const char* name, bool create, KeyValues** out); + KeyValues* KV_FindKey(KeyValues* parent, const char* name) { + return oFindOrCreateKey ? oFindOrCreateKey(parent, name, false, nullptr) : nullptr; } - - using FindOrCreateKey_t = KeyValues*(*)(KeyValues*, const char*, bool, KeyValues**); - FindOrCreateKey_t oFindOrCreateKey = nullptr; - + // ── KeyValuesSystem — symbol ↔ string (from vstdlib_s64.dll) ─── - IKeyValuesSystem* GetKeyValuesSystem() { static IKeyValuesSystem* sys = []() -> IKeyValuesSystem* { HMODULE vstdlib = GetModuleHandleW(L"vstdlib_s64.dll"); @@ -27,16 +22,18 @@ namespace { }(); return sys; } - + const char* GetKeyName(int symbol) { auto* sys = GetKeyValuesSystem(); auto name = sys->GetStringForSymbol(symbol); LOG_KEYVALUE_TRACE("GetKeyName: symbol={} -> name={}", symbol, name); return name ? name : nullptr; } - - KeyValues* KV_FindKey(KeyValues* parent, const char* name) { - return oFindOrCreateKey ? oFindOrCreateKey(parent, name, false, nullptr) : nullptr; + + HOOK_FUNC(ReadAsBinary, bool, KeyValues* root, void* buf, int depth, + bool textMode, void* symTable) { + bool ok = oReadAsBinary(root, buf, depth, textMode, symTable); + return ok; } } // anonymous namespace @@ -44,20 +41,17 @@ namespace { namespace Hooks_KeyValues { void Install() { - RESOLVE_D(FindOrCreateKey); - if (!oFindOrCreateKey) return; - + RESOLVE_C(FindOrCreateKey); + HOOK_BEGIN(); - INSTALL_HOOK_D(ReadAsBinary); + INSTALL_HOOK_C(ReadAsBinary); HOOK_END(); } void Uninstall() { - if (!oReadAsBinary) return; UNHOOK_BEGIN(); - UNINSTALL_HOOK(ReadAsBinary); + UNINSTALL_HOOK_C(ReadAsBinary); UNHOOK_END(); - oFindOrCreateKey = nullptr; } } // namespace Hooks_KeyValues diff --git a/src/Hook/Hooks_Manifest.cpp b/src/Hook/Hooks_Manifest.cpp index 44c9788..d52f54c 100644 --- a/src/Hook/Hooks_Manifest.cpp +++ b/src/Hook/Hooks_Manifest.cpp @@ -230,7 +230,7 @@ namespace Hooks_Manifest { void Install() { HOOK_BEGIN(); - INSTALL_HOOK_D(BuildDepotDependency); + INSTALL_HOOK_C(BuildDepotDependency); HOOK_END(); } diff --git a/src/Hook/Hooks_Misc.cpp b/src/Hook/Hooks_Misc.cpp index d0df592..7f5b7e6 100644 --- a/src/Hook/Hooks_Misc.cpp +++ b/src/Hook/Hooks_Misc.cpp @@ -1,102 +1,41 @@ #include "Hooks_Misc.h" #include "HookMacros.h" -#include "Hooks_SteamUI.h" #include "Utils/VehCommon.h" #include "dllmain.h" namespace { - // ── function type aliases (alphabetical) ───────────────────────────────── - using CUtlBufferEnsureCapacity_t = void*(*)(CUtlBuffer*, int); - using CUtlMemoryGrow_t = void*(*)(CUtlVector*, int); - using GetAppDataFromAppInfo_t = int64(*)(void*, AppId_t, const char*, uint8*, int32); - using GetAppIDForCurrentPipe_t = AppId_t(*)(void*); - using GetPackageInfo_t = PackageInfo*(*)(void*, uint32, int64); - using MarkLicenseAsChanged_t = int64(*)(void*, uint32, bool); - using ProcessPendingLicenseUpdates_t = bool(*)(void*); + // ── Resolve-only functions ───────────────────────────────────── + RESOLVE_FUNC(CUtlBufferEnsureCapacity, void*, CUtlBuffer*, int); - // ── X-macro lists ──────────────────────────────────────────────────────── - // One-shot int3: on hit, ctx->Rcx stored to the named output variable. - #define CAPTURE_LIST(X) \ - X(GetAppIDForCurrentPipe, g_steamEngine) \ - X(GetAppDataFromAppInfo, g_pCAppInfoCache) \ - X(MarkLicenseAsChanged, g_pCUser) \ - X(GetPackageInfo, g_pCPackageInfo) - - // Resolve-only (no int3). - #define LOCATE_LIST(X) \ - X(CUtlBufferEnsureCapacity) \ - X(CUtlMemoryGrow) \ - X(ProcessPendingLicenseUpdates) - - // ── generated declarations ─────────────────────────────────────────────── - CAPTURE_LIST(VEH_DECL_CAPTURE) - LOCATE_LIST(VEH_DECL_RESOLVE) - - uint8_t* g_spawnProcessTarget; - PVOID g_vehHandle; + // ── VEH-captured functions (one-shot int3) ─────────────────────────────── + // On int3 hit, ctx->Rcx is stored to the named output variable. + CAPTURE_THIS_FUNC(GetAppIDForCurrentPipe, AppId_t, g_steamEngine, void*); + CAPTURE_THIS_FUNC(GetAppDataFromAppInfo, int64, g_pCAppInfoCache, void*, AppId_t, const char*, uint8*, int32); // Assumes one game at a time. Set by SpawnProcess VEH when -onlinefix // is detected; cleared when a non-onlinefix game launches. AppId_t g_OnlineFixRealAppId; - std::unordered_map g_GameNameCache; - static std::vector g_captures; - - // ── VEH handler ────────────────────────────────────────────────────────── - // Scoped to this module's int3 sites only. Foreign RIP -> - // EXCEPTION_CONTINUE_SEARCH so other VEH handlers still get their turn. - LONG CALLBACK VehHandler(PEXCEPTION_POINTERS pExInfo) { - PCONTEXT ctx = pExInfo->ContextRecord; - - if (pExInfo->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT) { - for (auto& cap : g_captures) { - if (*cap.funcPtr && ctx->Rip == reinterpret_cast(*cap.funcPtr)) { - *cap.outPtr = reinterpret_cast(ctx->Rcx); - *reinterpret_cast(*cap.funcPtr) = cap.restoreByte; - LOG_MISC_INFO("Captured {}: 0x{:X}", cap.label, - reinterpret_cast(*cap.outPtr)); - return EXCEPTION_CONTINUE_EXECUTION; - } - } - - // CUser_SpawnProcess(pCUser, pExePath, pCommandLine, pWorkingDir, - // pGameID, ...) - // RCX=pCUser, RDX=pExePath, R8=pCommandLine, R9=pWorkingDir - // [RSP+0x28]=pGameID (5th arg, pointer to CGameID, low 24 bits = AppId) - if (g_spawnProcessTarget - && ctx->Rip == reinterpret_cast(g_spawnProcessTarget)) { - auto* pGameID = reinterpret_cast( - *reinterpret_cast(ctx->Rsp + 0x28)); - AppId_t appId = static_cast(*pGameID & 0xFFFFFF); - - *g_spawnProcessTarget = 0x48; - ctx->EFlags |= 0x100; - - const char* cmdLine = reinterpret_cast(ctx->R8); - - if (LuaConfig::HasDepot(appId) && cmdLine - && strstr(cmdLine, "-onlinefix")) { - g_OnlineFixRealAppId = appId; - *pGameID = kOnlineFixAppId; - LOG_MISC_INFO("SpawnProcess: appid {} -> {}, cmd=\"{}\"", - appId, kOnlineFixAppId, cmdLine); - } else { - g_OnlineFixRealAppId = 0; - } - return EXCEPTION_CONTINUE_EXECUTION; - } - } - if (pExInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) { - if (g_spawnProcessTarget - && ctx->Rip == reinterpret_cast(g_spawnProcessTarget + 5)) { - *g_spawnProcessTarget = 0xCC; - return EXCEPTION_CONTINUE_EXECUTION; - } + // ── SpawnProcess interception ──────────────────────────────────────────── + // CUser_SpawnProcess(pCUser, pExePath, pCommandLine, pWorkingDir, + // pGameID, ...) + // arg1=pCUser, arg2=pExePath, arg3=pCommandLine, arg4=pWorkingDir + // arg5=pGameID (CGameID*; low 24 bits = AppId) + static void OnSpawnProcessHit(PCONTEXT ctx, const VehCommon::Int3Site& /*site*/) { + CGameID* pGameID = VehCommon::GetArg(ctx, 5); + AppId_t appId = static_cast(pGameID->AppID(true)); + const char* cmdLine = VehCommon::GetArg(ctx, 3); + + if (LuaConfig::HasDepot(appId) && cmdLine && strstr(cmdLine, "-onlinefix")) + { + g_OnlineFixRealAppId = appId; + pGameID->SetAppID(kOnlineFixAppId); + LOG_MISC_INFO("SpawnProcess: appid {} -> {}, cmd=\"{}\"",appId, kOnlineFixAppId, cmdLine); + } else { + g_OnlineFixRealAppId = 0; } - - return EXCEPTION_CONTINUE_SEARCH; } // ── SteamController_OptedInMask ────────────────────────────────────────── @@ -104,13 +43,11 @@ namespace { // compute EnableConfiguratorSupport and the SDL_* env vars. // With 480 the spawned game inherits Spacewar's Steam Input // opt-in and gameoverlayrenderer hijacks the XInput stream. - HOOK_FUNC(OptedInMask, __int64, - void* pThis, unsigned int appId) + HOOK_FUNC(OptedInMask, int64,void* pThis, AppId_t appId) { if (appId == kOnlineFixAppId && g_OnlineFixRealAppId) { - LOG_MISC_INFO("OptedInMask: appid {} -> {}", - appId, g_OnlineFixRealAppId); - return oOptedInMask(pThis, g_OnlineFixRealAppId); + LOG_MISC_INFO("OptedInMask: appid {} -> {}",appId, g_OnlineFixRealAppId); + appId = g_OnlineFixRealAppId; } return oOptedInMask(pThis, appId); } @@ -120,43 +57,58 @@ namespace { // overlay reads for screenshot tags, community URLs, and asset // selection. pCGameID drives SteamGameId / SteamAppId; leave it // at 480 so the in-game ownership bypass holds. - HOOK_FUNC(BuildSpawnEnvBlock, __int64, - void* pThis, uint64_t* pCGameID, void* a3, void* env, - uint64_t* pOverlayCGameID, void* a6, int a7, + HOOK_FUNC(BuildSpawnEnvBlock, int64, + void* pThis, CGameID* pCGameID, void* a3, void* env, + CGameID* pOverlayCGameID, void* a6, int a7, void* a8, void* a9, unsigned int a10, char a11) { if (g_OnlineFixRealAppId && pOverlayCGameID - && (*pOverlayCGameID & 0xFFFFFF) == kOnlineFixAppId) { - uint64_t prev = *pOverlayCGameID; - *pOverlayCGameID = (prev & ~static_cast(0xFFFFFF)) - | g_OnlineFixRealAppId; - LOG_MISC_INFO("BuildSpawnEnvBlock: overlay CGameID {:#x} -> {:#x}", - prev, *pOverlayCGameID); + && pOverlayCGameID->AppID(true) == kOnlineFixAppId) + { + LOG_MISC_INFO("BuildSpawnEnvBlock: SetAppID in OverlayCGameID {} -> {}", + pOverlayCGameID->AppID(true), g_OnlineFixRealAppId); + pOverlayCGameID->SetAppID(g_OnlineFixRealAppId); } return oBuildSpawnEnvBlock(pThis, pCGameID, a3, env, pOverlayCGameID, a6, a7, a8, a9, a10, a11); } + + // CAppInfoCache::GetOrAddAppData + // The injected package keeps Lua-provided ids in PackageInfo::AppIdVec. + // Some of those ids can actually be depot ids, but we cannot trust the + // Lua config to classify app ids and depot ids for us. In offline mode, + // depot ids usually have only placeholder appinfo data. That blocks + // CClientAppManager_ProcessPendingLicenseUpdates, because it waits for + // every AppIdVec entry to have resolved appinfo unless the entry has been + // marked as a known-unknown id by the PICS path. For injected ids that + // still have placeholder appinfo, set skip_flag so Steam treats them like + // PICS unknown_appids instead of keeping the license update pending. + HOOK_FUNC(GetOrAddAppData,CAppData*,void* pCache, AppId_t appId,bool bCreate) + { + CAppData* pData = oGetOrAddAppData(pCache, appId, bCreate); + // LOG_MISC_TRACE("GetOrAddAppData: appId={} bCreate={} -> pData={}", appId, bCreate, pData ? pData->DebugString() : "null"); + if (LuaConfig::HasDepot(appId) && pData && !bCreate && pData->IsUnresolvedAppInfo()) { + LOG_MISC_DEBUG("GetOrAddAppData: Marking appId {} as skip_flag=true to bypass license update blocking", appId); + pData->bSkipFlag = true; + } + return pData; + } } namespace Hooks_Misc { void Install() { - if (g_vehHandle) return; + RESOLVE_C(CUtlBufferEnsureCapacity); - LOCATE_LIST(VEH_LOCATE) - CAPTURE_LIST(VEH_ARM) + ARM_CAPTURE_C(GetAppIDForCurrentPipe); + ARM_CAPTURE_C(GetAppDataFromAppInfo); - if (auto* p = FIND_SIG(diversion_hMdoule, SpawnProcess)) { - g_spawnProcessTarget = static_cast(p); - VehCommon::ArmInt3(p); - } - - if (!g_captures.empty() || g_spawnProcessTarget) - g_vehHandle = AddVectoredExceptionHandler(1, VehHandler); + ARM_INT3_C(SpawnProcess, true, &OnSpawnProcessHit, nullptr); HOOK_BEGIN(); - INSTALL_HOOK_D(BuildSpawnEnvBlock); - INSTALL_HOOK_D(OptedInMask); + INSTALL_HOOK_C(BuildSpawnEnvBlock); + INSTALL_HOOK_C(OptedInMask); + INSTALL_HOOK_C(GetOrAddAppData); HOOK_END(); } @@ -164,51 +116,40 @@ namespace Hooks_Misc { UNHOOK_BEGIN(); UNINSTALL_HOOK(BuildSpawnEnvBlock); UNINSTALL_HOOK(OptedInMask); + UNINSTALL_HOOK(GetOrAddAppData); UNHOOK_END(); - - if (g_vehHandle) { - RemoveVectoredExceptionHandler(g_vehHandle); - g_vehHandle = nullptr; - } - - VEH_CLEANUP_CAPTURES(g_captures); - - if (g_spawnProcessTarget && *g_spawnProcessTarget == 0xCC) - VehCommon::RestoreByte(g_spawnProcessTarget, 0x48); - g_spawnProcessTarget = nullptr; - - LOCATE_LIST(VEH_ZERO_RESOLVE) - g_OnlineFixRealAppId = 0; - g_GameNameCache.clear(); } - AppId_t GetAppIDForCurrentPipe() { - if (!g_steamEngine || !oGetAppIDForCurrentPipe) { - LOG_MISC_WARN("GetAppIDForCurrentPipe called before capture — returning 0"); + AppId_t GetAppIDForCurrentPipeWrap() { + if (!CAPTURE_READY(GetAppIDForCurrentPipe)) { + LOG_MISC_WARN("GetAppIDForCurrentPipeWrap called before capture — returning 0"); return 0; } auto appid = oGetAppIDForCurrentPipe(g_steamEngine); if (!appid) { - LOG_MISC_TRACE("GetAppIDForCurrentPipe: AppId=0(Not GamePipe)"); + LOG_MISC_TRACE("GetAppIDForCurrentPipeWrap: AppId=0(Not GamePipe)"); } else { - LOG_MISC_DEBUG("GetAppIDForCurrentPipe: AppId={}", appid); + LOG_MISC_DEBUG("GetAppIDForCurrentPipeWrap: AppId={}", appid); } return appid; } + AppId_t ResolveAppId() { if (g_OnlineFixRealAppId) return g_OnlineFixRealAppId; - return GetAppIDForCurrentPipe(); + return GetAppIDForCurrentPipeWrap(); } - - void EnsureBufferSize(CUtlBuffer* pWrite, int32 size) + + bool EnsureBufferSize(CUtlBuffer* pWrite, int32 size) { if (oCUtlBufferEnsureCapacity) { LOG_MISC_DEBUG("Before ensuring CUtlBuffer capacity: {}", pWrite->DebugString()); oCUtlBufferEnsureCapacity(pWrite, size); LOG_MISC_DEBUG("After ensuring CUtlBuffer capacity: {}", pWrite->DebugString()); + return true; } - pWrite->m_Put = size; + LOG_MISC_WARN("EnsureBufferSize: oCUtlBufferEnsureCapacity not resolved"); + return false; } // ── Game name ──────────────────────────────────────────────── @@ -219,14 +160,13 @@ namespace Hooks_Misc { std::string name; - if (g_pCAppInfoCache && oGetAppDataFromAppInfo) { + if (CAPTURE_READY(GetAppDataFromAppInfo)) { char buf[256] = {}; // "common/name" triggers auto-localization: the function detects // prefix "common" (keyType=2) + key "name", then tries // "name_localized/" before falling back to "name". // Returns strlen+1 on success, -1 on failure. - int64 len = oGetAppDataFromAppInfo( - g_pCAppInfoCache, appId, "common/name", + int64 len = oGetAppDataFromAppInfo(g_pCAppInfoCache, appId, "common/name", reinterpret_cast(buf), sizeof(buf)); if (len > 1) name.assign(buf, static_cast(len - 1)); @@ -237,60 +177,4 @@ namespace Hooks_Misc { return name; } - // ── License refresh (no-restart) ──────────────────────────────── - void NotifyLicenseChanged() { - if (!g_pCUser || !g_pCPackageInfo) { - LOG_PACKAGE_WARN("NotifyLicenseChanged: pCUser or pCPackageInfo not captured yet, skipping"); - return; - } - if (!oGetPackageInfo || !oMarkLicenseAsChanged - || !oProcessPendingLicenseUpdates || !oCUtlMemoryGrow) { - LOG_PACKAGE_WARN("NotifyLicenseChanged: functions not resolved, skipping"); - return; - } - - PackageInfo* pPkg = oGetPackageInfo(g_pCPackageInfo, 0, 0); - if (!pPkg) { - LOG_PACKAGE_WARN("NotifyLicenseChanged: GetPackageInfo returned null"); - return; - } - - // ── Remove depots that were unloaded ── - std::vector removals = LuaConfig::TakePendingRemovals(); - uint32_t removedCount = 0; - for (AppId_t id : removals) { - if (pPkg->AppIdVec.FindAndFastRemove(id)) { - ++removedCount; - LOG_PACKAGE_DEBUG("NotifyLicenseChanged: removed AppId {}", id); - } - } - - // ── Add depots that are newly loaded ── - std::vector additions = LuaConfig::TakePendingAdditions(); - if (!additions.empty()) { - uint32_t oldSize = pPkg->AppIdVec.m_Size; - oCUtlMemoryGrow(&pPkg->AppIdVec, static_cast(additions.size())); - for (size_t i = 0; i < additions.size(); ++i) { - pPkg->AppIdVec.m_Memory.m_pMemory[oldSize + i] = additions[i]; - LOG_PACKAGE_DEBUG("NotifyLicenseChanged: inserted AppId {} at [{}]", additions[i], oldSize + i); - } - } - - if (additions.empty() && removedCount == 0) { - LOG_PACKAGE_DEBUG("NotifyLicenseChanged: no changes"); - return; - } - - // Mark package 0 as changed and trigger library refresh. - oMarkLicenseAsChanged(g_pCUser, 0, true); - oProcessPendingLicenseUpdates(g_pCUser); - LOG_PACKAGE_INFO("NotifyLicenseChanged: {} added, {} removed", additions.size(), removedCount); - - // CSteamUIAppController caches its own AppOverview entries and doesn't - // re-query subscription state on package mutation; remove them - // explicitly so the library UI reflects the dropped license. - for (AppId_t id : removals) { - Hooks_SteamUI::RemoveAppOverview(id); - } - } } diff --git a/src/Hook/Hooks_Misc.h b/src/Hook/Hooks_Misc.h index a6cfa23..f7fbafc 100644 --- a/src/Hook/Hooks_Misc.h +++ b/src/Hook/Hooks_Misc.h @@ -2,14 +2,11 @@ #include "dllmain.h" -// Catch-all for the lightweight info-capture int3 traps that don't fit a -// dedicated category — currently: +// Catch-all for lightweight info-capture int3 traps and launch-time rewrites +// that don't fit a dedicated category — currently: // * GetAppIDForCurrentPipe -> captures the SteamEngine pointer // * SpawnProcess -> OnlineFix detection + 480 rewrite // * GetAppDataFromAppInfo -> captures the CAppInfoCache pointer -// * MarkLicenseAsChanged -> captures pCUser; resolved for NotifyLicenseChanged -// * GetPackageInfo -> captures pCPackageInfo; used by NotifyLicenseChanged to append AppIds -// * ProcessPendingLicenseUpdates -> resolved for NotifyLicenseChanged namespace Hooks_Misc { void Install(); void Uninstall(); @@ -17,11 +14,11 @@ namespace Hooks_Misc { // Returns the AppId for the current Steam pipe via the captured engine // pointer, or 0 if we haven't yet observed the host calling // GetAppIDForCurrentPipe. - AppId_t GetAppIDForCurrentPipe(); + AppId_t GetAppIDForCurrentPipeWrap(); // Grow a CUtlBuffer to at least 'size' bytes and set m_Put = size. // Uses CUtlBuffer::EnsureCapacity from steamclient, resolved on first call. - void EnsureBufferSize(CUtlBuffer* pWrite, int32 size); + bool EnsureBufferSize(CUtlBuffer* pWrite, int32 size); // Resolve the real appid: if OnlineFix is active return real appid, // otherwise fall back to GetAppIDForCurrentPipe(). @@ -30,8 +27,4 @@ namespace Hooks_Misc { // Get localized game name via GetAppDataFromAppInfo (cached). std::string GetGameNameByAppID(AppId_t appId); - // Mark package 0 as changed and trigger CClientAppManager_ProcessPendingLicenseUpdates - // Requires pCUser to have been captured (happens on first natural call to - // MarkLicenseAsChanged, which Steam makes during license load on startup). - void NotifyLicenseChanged(); } diff --git a/src/Hook/Hooks_NetPacket.cpp b/src/Hook/Hooks_NetPacket.cpp index 5941daa..ca6af32 100644 --- a/src/Hook/Hooks_NetPacket.cpp +++ b/src/Hook/Hooks_NetPacket.cpp @@ -41,9 +41,7 @@ namespace { int g_SendPacketPoolIdx = 0; // ── EMsg -> name lookup ───────────────────────── - using PchMsgNameFromEMsg_t = char*(*)(EMsg); - PchMsgNameFromEMsg_t oPchMsgNameFromEMsg = nullptr; - + RESOLVE_FUNC(PchMsgNameFromEMsg, char*, EMsg eMsg); inline const char* MsgName(EMsg eMsg) { if (oPchMsgNameFromEMsg) return oPchMsgNameFromEMsg(eMsg); return "?"; @@ -1116,10 +1114,10 @@ namespace { namespace Hooks_NetPacket { void Install() { - RESOLVE_D(PchMsgNameFromEMsg); + RESOLVE_C(PchMsgNameFromEMsg); HOOK_BEGIN(); - INSTALL_HOOK_D(BBuildAndAsyncSendFrame); - INSTALL_HOOK_D(RecvPkt); + INSTALL_HOOK_C(BBuildAndAsyncSendFrame); + INSTALL_HOOK_C(RecvPkt); HOOK_END(); } @@ -1128,6 +1126,5 @@ namespace Hooks_NetPacket { UNINSTALL_HOOK(BBuildAndAsyncSendFrame); UNINSTALL_HOOK(RecvPkt); UNHOOK_END(); - oPchMsgNameFromEMsg = nullptr; } } diff --git a/src/Hook/Hooks_Package.cpp b/src/Hook/Hooks_Package.cpp index 08a7922..2cd33f2 100644 --- a/src/Hook/Hooks_Package.cpp +++ b/src/Hook/Hooks_Package.cpp @@ -1,38 +1,128 @@ #include "Hooks_Package.h" #include "HookMacros.h" +#include "Hooks_SteamUI.h" #include "dllmain.h" namespace { - using CUtlMemoryGrow_t = void* (*)(CUtlVector* pVec, int grow_size); - CUtlMemoryGrow_t oCUtlMemoryGrow = nullptr; + RESOLVE_FUNC(CUtlMemoryGrow, void*, CUtlVector*, int); + RESOLVE_FUNC(MarkLicenseAsChanged, int64, void*, uint32, bool); + RESOLVE_FUNC(ProcessPendingLicenseUpdates, bool, void*); + + void* g_pCUser = nullptr; + void* g_pCPackageInfo = nullptr; + PackageInfo* g_pInjectedPackageInfo = nullptr; + bool g_licenseInitialized = false; + bool g_licenseRefreshPending = false; + + constexpr PackageId_t kInjectedPackageId = 0; + + bool MarkLicenseAsChangedAndProcessUpdates() { + if (!g_pCUser || !oMarkLicenseAsChanged || !oProcessPendingLicenseUpdates) { + LOG_PACKAGE_WARN("MarkLicenseAsChangedAndProcessUpdates: dependencies not ready, skipping"); + return false; + } + oMarkLicenseAsChanged(g_pCUser, kInjectedPackageId, true); + oProcessPendingLicenseUpdates(g_pCUser); + LOG_PACKAGE_DEBUG("MarkLicenseAsChangedAndProcessUpdates: marked package {} as changed and processed updates", kInjectedPackageId); + return true; + } + + void TryProcessPendingLicenseRefresh() { + if (!g_licenseRefreshPending) + return; + if (MarkLicenseAsChangedAndProcessUpdates()) + g_licenseRefreshPending = false; + } + + bool CUtlMemoryGrowWrap(CUtlVector* pVec, int grow_size) { + if (!oCUtlMemoryGrow) { + LOG_PACKAGE_WARN("CUtlMemoryGrow: oCUtlMemoryGrow not ready, cannot grow"); + return false; + } + return oCUtlMemoryGrow(pVec, grow_size); + } + + bool InitFakeLicenseOnce(PackageInfo* pPkg) { + if (!pPkg) { + LOG_PACKAGE_WARN("InitFakeLicense: null PackageInfo pointer"); + return false; + } + // Inject all depots from config into the fake license. + std::vector appIds = LuaConfig::GetAllDepotIds(); + if (!appIds.empty()) { + uint32 oldSize = pPkg->AppIdVec.m_Size; + uint32 numToAdd = static_cast(appIds.size()); + LOG_PACKAGE_INFO("InitFakeLicense(PackageId={}): adding {} apps, oldSize={}", kInjectedPackageId, numToAdd, oldSize); + if (!CUtlMemoryGrowWrap(&pPkg->AppIdVec, numToAdd)) { + LOG_PACKAGE_WARN("InitFakeLicense(PackageId={}): failed to grow AppId vector", kInjectedPackageId); + return false; + } + for (uint32 i = 0; i < numToAdd; i++) + pPkg->AppIdVec.m_Memory.m_pMemory[oldSize + i] = appIds[i]; + } + + g_licenseInitialized = true; + g_licenseRefreshPending = true; + TryProcessPendingLicenseRefresh(); + return true; + } HOOK_FUNC(LoadPackage, bool, PackageInfo* pInfo, uint8* sha1, int32 cn, void* p4) { bool result = oLoadPackage(pInfo, sha1, cn, p4); - + if (pInfo->PackageId == 0) { std::vector appIds = LuaConfig::GetAllDepotIds(); if (!appIds.empty()) { uint32 oldSize = pInfo->AppIdVec.m_Size; uint32 numToAdd = static_cast(appIds.size()); - LOG_PACKAGE_INFO("LoadPackage(PackageId=0): adding {} apps, oldSize={}", numToAdd, oldSize); - oCUtlMemoryGrow(&pInfo->AppIdVec, numToAdd); + LOG_PACKAGE_INFO("LoadPackage(PackageId={}): adding {} apps, oldSize={}", kInjectedPackageId, numToAdd, oldSize); + if (!CUtlMemoryGrowWrap(&pInfo->AppIdVec, numToAdd)) { + LOG_PACKAGE_WARN("LoadPackage(PackageId={}): failed to grow AppId vector", kInjectedPackageId); + return result; + } for (uint32 i = 0; i < numToAdd; i++) - pInfo->AppIdVec.m_Memory.m_pMemory[oldSize + i] = appIds[i]; + pInfo->AppIdVec.m_Memory.m_pMemory[oldSize + i] = appIds[i]; } } - + return result; } + + HOOK_FUNC(GetPackageInfo, PackageInfo*, void* pPackageInfoCache, uint32 packageId, uint64 accessToken) { + if (!g_pCPackageInfo) { + g_pCPackageInfo = pPackageInfoCache; + LOG_PACKAGE_DEBUG("GetPackageInfo: captured CPackageInfo {}", g_pCPackageInfo); + } + // LOG_PACKAGE_TRACE("GetPackageInfo: package cache {}, packageId {}, accessToken {}", pPackageInfoCache, packageId, accessToken); + PackageInfo* pPkg = oGetPackageInfo(pPackageInfoCache, packageId, accessToken); + if (packageId == 0 && pPkg) { + if(!g_pInjectedPackageInfo) { + g_pInjectedPackageInfo = pPkg; + LOG_PACKAGE_INFO("GetPackageInfo: g_pInjectedPackageInfo set to {}", (void*)g_pInjectedPackageInfo); + } + if(!g_licenseInitialized) { + InitFakeLicenseOnce(pPkg); + } + } + return pPkg; + } HOOK_FUNC(CheckAppOwnership, bool, void* pObj, AppId_t appId, AppOwnership* pOwn) { + if (!g_pCUser) { + g_pCUser = pObj; + LOG_PACKAGE_DEBUG("CheckAppOwnership: captured CUser {}", g_pCUser); + } + bool result = oCheckAppOwnership(pObj, appId, pOwn); + TryProcessPendingLicenseRefresh(); + // LOG_PACKAGE_TRACE("CheckAppOwnership: AppId={} result={} {}", appId, result, pOwn->DebugString()); if (LuaConfig::HasDepot(appId)) { if (result && pOwn->ExistInPackageNums > 1) { // Actually owned — record so HasDepot excludes it going forward LuaConfig::MarkOwned(appId); } else { - pOwn->PackageId = 0; + pOwn->PackageId = kInjectedPackageId; pOwn->ReleaseState = EAppReleaseState::Released; // Setting this free flag to false will hide it from the library UI. pOwn->bFreeLicense = false; @@ -41,41 +131,75 @@ namespace { } return result; } - - HOOK_FUNC(SendCallbackToPipe, bool, void* pSteamEngine, HSteamPipe hSteamPipe, - HSteamUser iClientUser, int iCallback, void* pCallbackData, int cubCallbackData) { - // ── Callback modifier dispatch ───────────────────────────────────────── - // Intercept callbacks before they reach the pipe and modify data in-place. - // To add a new callback: add an else-if branch here. - if (iCallback == AppLicensesChanged_t::k_iCallback) { - auto* p = static_cast(pCallbackData); - LOG_PACKAGE_DEBUG("SendCallbackToPipe: AppLicensesChanged m_bReloadAll={}", - p->m_bReloadAll); - // p->m_bReloadAll = true; we don't need it anymore - } - - return oSendCallbackToPipe(pSteamEngine, hSteamPipe, iClientUser, - iCallback, pCallbackData, cubCallbackData); - } } namespace Hooks_Package { void Install() { - RESOLVE_D(CUtlMemoryGrow); + RESOLVE_C(CUtlMemoryGrow); + RESOLVE_C(MarkLicenseAsChanged); + RESOLVE_C(ProcessPendingLicenseUpdates); HOOK_BEGIN(); - INSTALL_HOOK_D(LoadPackage); - INSTALL_HOOK_D(CheckAppOwnership); - INSTALL_HOOK_D(SendCallbackToPipe); + // INSTALL_HOOK_C(LoadPackage); + INSTALL_HOOK_C(GetPackageInfo); + INSTALL_HOOK_C(CheckAppOwnership); HOOK_END(); } void Uninstall() { UNHOOK_BEGIN(); - UNINSTALL_HOOK(LoadPackage); - UNINSTALL_HOOK(CheckAppOwnership); - UNINSTALL_HOOK(SendCallbackToPipe); + // UNINSTALL_HOOK_C(LoadPackage); + UNINSTALL_HOOK_C(GetPackageInfo); + UNINSTALL_HOOK_C(CheckAppOwnership); UNHOOK_END(); - oCUtlMemoryGrow = nullptr; + } + + void NotifyLicenseChanged() { + PackageInfo* pPkg = g_pInjectedPackageInfo; + if (!pPkg) { + LOG_PACKAGE_WARN("NotifyLicenseChanged: injected PackageInfo not ready, cannot notify"); + return; + } + + // ── Remove depots that were unloaded ── + std::vector removals = LuaConfig::TakePendingRemovals(); + uint32_t removedCount = 0; + for (AppId_t id : removals) { + if (pPkg->AppIdVec.FindAndFastRemove(id)) { + ++removedCount; + LOG_PACKAGE_DEBUG("NotifyLicenseChanged: removed AppId {}", id); + } + } + + // ── Add depots that are newly loaded ── + std::vector additions = LuaConfig::TakePendingAdditions(); + if (!additions.empty()) { + uint32_t oldSize = pPkg->AppIdVec.m_Size; + if (CUtlMemoryGrowWrap(&pPkg->AppIdVec, additions.size())) { + for (size_t i = 0; i < additions.size(); ++i) { + pPkg->AppIdVec.m_Memory.m_pMemory[oldSize + i] = additions[i]; + LOG_PACKAGE_DEBUG("NotifyLicenseChanged: inserted AppId {} at [{}]", additions[i], oldSize + i); + } + } + } + + if (additions.empty() && removedCount == 0) { + LOG_PACKAGE_DEBUG("NotifyLicenseChanged: no changes"); + return; + } + + // Mark package 0 as changed and trigger library refresh. + if (!MarkLicenseAsChangedAndProcessUpdates()) { + LOG_PACKAGE_WARN("NotifyLicenseChanged: failed to mark license as changed"); + return; + } + LOG_PACKAGE_INFO("NotifyLicenseChanged: {} added, {} removed", additions.size(), removedCount); + + // CSteamUIAppController caches its own AppOverview entries and doesn't + // re-query subscription state on package mutation; remove them + // explicitly so the library UI reflects the dropped license. + for (AppId_t id : removals) { + Hooks_SteamUI::RemoveAppAndSendChange(id); + } } } diff --git a/src/Hook/Hooks_Package.h b/src/Hook/Hooks_Package.h index 94ef5a1..d86de21 100644 --- a/src/Hook/Hooks_Package.h +++ b/src/Hook/Hooks_Package.h @@ -1,8 +1,14 @@ #pragma once +#include "dllmain.h" + namespace Hooks_Package { // LoadPackage + CheckAppOwnership — patches the package store so that // user-supplied depots appear owned and accessible. void Install(); void Uninstall(); + + // Mark package 0 as changed and trigger CClientAppManager_ProcessPendingLicenseUpdates. + void NotifyLicenseChanged(); + } diff --git a/src/Hook/Hooks_SteamUI.cpp b/src/Hook/Hooks_SteamUI.cpp index 2cb71a4..691c2da 100644 --- a/src/Hook/Hooks_SteamUI.cpp +++ b/src/Hook/Hooks_SteamUI.cpp @@ -3,145 +3,45 @@ #include "HookMacros.h" #include "dllmain.h" #include "steam_messages.pb.h" -#include +#include "Utils/VehCommon.h" + #include +#include namespace { - using namespace std::chrono_literals; - constexpr int MAX_RETRY = 20; - constexpr auto RETRY_INTERVAL = 300ms; - - // ── function type aliases (alphabetical) ───────────────────────────────── - using AddProtobufAsBinary_t = void*(__fastcall*)(void* /*args*/, void* /*proto*/); - using GetAppByID_t = void*(__fastcall*)(void* /*controller*/, AppId_t, bool /*create*/); - using GetTopManager_t = void*(__fastcall*)(); - - // ── resolved function pointers ─────────────────────────────────────────── - AddProtobufAsBinary_t oAddProtobufAsBinary = nullptr; - GetAppByID_t oGetAppByID = nullptr; - GetTopManager_t oGetTopManager = nullptr; - - // CSteamUIAppController offsets (see its Validate() method): - // +0xAB8 from top-manager -> CSteamUIAppController* - // +848 m_mapAppIdToCApp - // +1744 m_vecAppOverviewChanged - constexpr size_t kControllerInTopManager = 0xAB8; - constexpr size_t kSubscriberVecOffset = 1744; - constexpr size_t kSubscriberVecSizeOffset = 1760; - - constexpr size_t kArgsSize = 64; - constexpr size_t kSubscriberInvokeVtableSlot = 4; - - // Cleared so BuildCompleteAppOverviewChange's filter (BIsOwned via - // vtable[22]) also excludes the app on the next full snapshot. - constexpr size_t kCSteamAppOwnedFlagOffset = 28; - HOOK_FUNC(LoadModuleWithPath, HMODULE, const char* path, bool flags) { - LOG_INFO("LoadModuleWithPath called with path: {} , flags: {}", path, flags); - // wait for hooks to be installed - for (int i = 0; i < MAX_RETRY && !g_HooksInstalled.load(); ++i){ - LOG_DEBUG("LoadModuleWithPath: waiting for hooks to be installed... (attempt {}/{},interval: {})", i + 1, MAX_RETRY, RETRY_INTERVAL.count()); - std::this_thread::sleep_for(RETRY_INTERVAL); - } - HMODULE h = oLoadModuleWithPath(path, flags); - if (!strcmp(path, "steamclient64.dll")) - h = diversion_hMdoule; - return h; - } - - // Fetch the CSteamUIAppController via the captured getter. Returns null - // if the singleton chain isn't ready yet. - void* ResolveController() { - if (!oGetTopManager) return nullptr; - void* topMgr = oGetTopManager(); - if (!topMgr) return nullptr; - return *reinterpret_cast(static_cast(topMgr) + kControllerInTopManager); - } + CAPTURE_THIS_FUNC(GetAppByID, CSteamApp*, g_pController,void* pThis, AppId_t appId, bool bCreate); + CAPTURE_THIS_FUNC(MarkAppChange,void*,g_pAppChangeSource,void* pThis,AppId_t appId, EAppChangeFlags changeFlags); - // Synthesize a CAppOverview_Change proto with removed_appid=[appId] and - // dispatch to every registered webhelper subscriber. Leaves the host-side - // CSteamApp alive so async holders' cached pointers stay valid. - bool EmitRemovedAppId(void* pController, AppId_t appId) { - alignas(8) uint8_t argsBuf[kArgsSize] = {}; - - ::CAppOverview_Change msg; - msg.add_removed_appid(appId); - msg.set_update_complete(true); - oAddProtobufAsBinary(argsBuf, &msg); - - void** vecData = *reinterpret_cast( - static_cast(pController) + kSubscriberVecOffset); - uint32_t subCount = *reinterpret_cast( - static_cast(pController) + kSubscriberVecSizeOffset); - - if (!vecData || subCount == 0) { - LOG_STEAMUI_WARN("EmitRemovedAppId: no subscribers; appId={}", appId); - return false; - } - - for (uint32_t i = 0; i < subCount; ++i) { - void* subscriber = vecData[i]; - if (!subscriber) continue; - void** vtable = *reinterpret_cast(subscriber); - auto invoke = reinterpret_cast( - vtable[kSubscriberInvokeVtableSlot]); - invoke(subscriber, argsBuf); - } - - return true; - } } namespace Hooks_SteamUI { void Install() { - HMODULE hSteamUI = GetModuleHandleA("steamui.dll"); - if (!hSteamUI) { - LOG_STEAMUI_WARN("steamui.dll not loaded; SteamUI hooks disabled"); - return; - } + + ARM_CAPTURE_U(GetAppByID); + ARM_CAPTURE_U(MarkAppChange); HOOK_BEGIN(); - INSTALL_HOOK(hSteamUI, LoadModuleWithPath); HOOK_END(); - RESOLVE(hSteamUI, GetAppByID); - RESOLVE(hSteamUI, AddProtobufAsBinary); - RESOLVE(hSteamUI, GetTopManager); - - LOG_STEAMUI_INFO("Install: GetAppByID={}, AddProtobufAsBinary={}, GetTopManager={}", - reinterpret_cast(oGetAppByID), - reinterpret_cast(oAddProtobufAsBinary), - reinterpret_cast(oGetTopManager)); } void Uninstall() { UNHOOK_BEGIN(); - UNINSTALL_HOOK(LoadModuleWithPath); UNHOOK_END(); - - oAddProtobufAsBinary = nullptr; - oGetAppByID = nullptr; - oGetTopManager = nullptr; } - void RemoveAppOverview(AppId_t appId) { - if (!oAddProtobufAsBinary || !oGetTopManager || !oGetAppByID) { - LOG_STEAMUI_WARN("RemoveAppOverview: primitives unresolved; appId={}", appId); - return; + void RemoveAppAndSendChange(AppId_t appId) { + if(CAPTURE_READY(GetAppByID) && CAPTURE_READY(MarkAppChange)) { + CSteamApp* pApp = oGetAppByID(g_pController, appId, false); + if(pApp) { + pApp->OwnershipFlags = k_EAppOwnershipFlags_None; + LOG_STEAMUI_DEBUG("RemoveAppAndSendChange: cleared owned flag for appId={}", appId); + oMarkAppChange(g_pAppChangeSource, appId, EAppChangeFlags::AddedOrCreated); + } else { + LOG_STEAMUI_WARN("RemoveAppAndSendChange: appId={} not found in GetAppByID", appId); + } } - - void* pController = ResolveController(); - if (!pController) { - LOG_STEAMUI_WARN("RemoveAppOverview: controller singleton not initialized; appId={}", appId); - return; - } - - if (void* pApp = oGetAppByID(pController, appId, /*create=*/false)) { - *reinterpret_cast(static_cast(pApp) + kCSteamAppOwnedFlagOffset) &= ~1u; - } - - if (!EmitRemovedAppId(pController, appId)) return; - - LOG_STEAMUI_INFO("RemoveAppOverview: appId={} done", appId); } + } diff --git a/src/Hook/Hooks_SteamUI.h b/src/Hook/Hooks_SteamUI.h index d68421e..26f1025 100644 --- a/src/Hook/Hooks_SteamUI.h +++ b/src/Hook/Hooks_SteamUI.h @@ -3,15 +3,12 @@ #include "dllmain.h" // Hooks targeting steamui.dll: -// * LoadModuleWithPath -> redirect steamclient64.dll loads to the diversion copy -// * RemoveAppOverview -> evict a card from the live library UI by -// emitting a synthesized CAppOverview_Change to -// every registered webhelper subscriber. + namespace Hooks_SteamUI { void Install(); void Uninstall(); - // Drop appId from the webhelper's m_mapApps and clear the host-side - // CSteamApp owned flag so the next full snapshot also excludes it. - void RemoveAppOverview(AppId_t appId); + // Clears ownership flag for the given appId and + // sends an app change notification to update the library UI. + void RemoveAppAndSendChange(AppId_t appId); } diff --git a/src/Steam/Enums.h b/src/Steam/Enums.h index 19ec8f3..5fc23c3 100644 --- a/src/Steam/Enums.h +++ b/src/Steam/Enums.h @@ -58,6 +58,33 @@ enum class EAppReleaseState Released = 4 }; +enum EAppOwnershipFlags +{ + k_EAppOwnershipFlags_None = 0x0000, // unknown + k_EAppOwnershipFlags_OwnsLicense = 0x0001, // owns license for this game + k_EAppOwnershipFlags_FreeLicense = 0x0002, // not paid for game + k_EAppOwnershipFlags_RegionRestricted = 0x0004, // owns app, but not allowed to play in current region + k_EAppOwnershipFlags_LowViolence = 0x0008, // only low violence version + k_EAppOwnershipFlags_InvalidPlatform = 0x0010, // app not supported on current platform + k_EAppOwnershipFlags_SharedLicense = 0x0020, // license was granted by authorized local device + k_EAppOwnershipFlags_FreeWeekend = 0x0040, // owned by a free weekend licenses + k_EAppOwnershipFlags_RetailLicense = 0x0080, // has a retail license for game, (CD-Key etc) + k_EAppOwnershipFlags_LicenseLocked = 0x0100, // shared license is locked (in use) by other user + k_EAppOwnershipFlags_LicensePending = 0x0200, // owns app, but transaction is still pending. Can't install or play + k_EAppOwnershipFlags_LicenseExpired = 0x0400, // doesn't own app anymore since license expired + k_EAppOwnershipFlags_LicensePermanent = 0x0800, // permanent license, not borrowed, or guest or freeweekend etc + k_EAppOwnershipFlags_LicenseRecurring = 0x1000, // Recurring license, user is charged periodically + k_EAppOwnershipFlags_LicenseCanceled = 0x2000, // Mark as canceled, but might be still active if recurring + k_EAppOwnershipFlags_AutoGrant = 0x4000, // Ownership is based on any kind of autogrant license + k_EAppOwnershipFlags_PendingGift = 0x8000, // user has pending gift to redeem + k_EAppOwnershipFlags_RentalNotActivated = 0x10000, // Rental hasn't been activated yet + k_EAppOwnershipFlags_Rental = 0x20000, // Is a rental + k_EAppOwnershipFlags_SiteLicense = 0x40000, // Is from a site license + k_EAppOwnershipFlags_LegacyFreeSub = 0x80000, // App only owned through Steam's legacy free sub + k_EAppOwnershipFlags_InvalidOSType = 0x100000, // app not supported on current OS version, used to indicate a game is 32-bit on post-catalina. Currently it's own flag so the library will display a notice. + k_EAppOwnershipFlags_TimedTrial = 0x200000, // App is playable only for limited time +}; + enum class EGameIDType { k_EGameIDTypeApp = 0, @@ -1881,4 +1908,19 @@ inline const char* EIPCInterfaceName(EIPCInterface iface) { return buf; } } -} \ No newline at end of file +} + +enum class EAppChangeFlags { + AddedOrCreated = 0x0001, + AppInfoOrConfig = 0x0002, + ClientShutdown = 0x0004, + UpdateProgress = 0x0010, + InternalRunState = 0x0040, + CloudState = 0x0100, + LogonState = 0x0200, + RemoteAppState = 0x0800, + RemoteLibraryAssetState = 0x1000, + GameAction = 0x2000, + LibraryAssetCleanup = 0x4000, + MRURegenerated = 0x8000, +}; \ No newline at end of file diff --git a/src/Steam/Structs.h b/src/Steam/Structs.h index 2c1ae0e..7a61473 100644 --- a/src/Steam/Structs.h +++ b/src/Steam/Structs.h @@ -7,6 +7,7 @@ #include "Types.h" #include "Enums.h" + #include #include @@ -125,13 +126,6 @@ struct AppOwnership } }; -struct CSteamApp{ - void** vfptr; - int32 StateFlags; - AppId_t AppID; - // ... -}; - // Single depot manifest entry (0x20 bytes) produced by BuildDepotDependency. struct DepotEntry { @@ -271,3 +265,84 @@ struct CSteamPipeClient { m_hSteamPipe, m_clientPID, m_szProcessName ? m_szProcessName : "?"); } }; + +struct CGameID{ + enum EGameIDType + { + k_EGameIDTypeApp = 0, + k_EGameIDTypeGameMod = 1, + k_EGameIDTypeShortcut = 2, + k_EGameIDTypeP2P = 3, + }; + + bool IsSteamApp() const + { + return ( m_gameID.m_nType == k_EGameIDTypeApp ); + } + + AppId_t AppID(bool checkType = false) const + { + if(checkType && !IsSteamApp()) + return 0; + return m_gameID.m_nAppID; + } + + void SetAppID(AppId_t nAppID) + { + m_gameID.m_nAppID = nAppID; + } + + struct GameID_t + { + unsigned int m_nAppID : 24; + unsigned int m_nType : 8; + unsigned int m_nModID : 32; + }; + + union + { + uint64 m_ulGameID; + GameID_t m_gameID; + }; +}; + +struct CAppData +{ + void** fptr; + AppId_t nAppID; + uint32 ChangeNumber; + uint32 LastChangeTimeStamp; + bool bSkipFlag; + bool bDeniedToken; + bool bMissingToken; + uint64 accessToken; + uint8 sha1Hash[20]; + + bool HasEmptyAppInfoSha() const { + static constexpr uint8 kEmptySha1[sizeof(sha1Hash)] = {}; + return memcmp(sha1Hash, kEmptySha1, sizeof(sha1Hash)) == 0; + } + + bool IsUnresolvedAppInfo() const { + return HasEmptyAppInfoSha() && !bSkipFlag; + } + + std::string DebugString() const { + return std::format("AppID={} ChangeNumber={} LastChangeTimeStamp={} SkipFlag={} DeniedToken={} MissingToken={} AccessToken={}", + nAppID, ChangeNumber, LastChangeTimeStamp, bSkipFlag, bDeniedToken, bMissingToken, accessToken); + } +}; + +struct CSteamApp +{ + void** fptr; + CGameID GameID; + AppId_t nAppID; + uint16 _unknown1; + uint16 _unknown2; + EAppReleaseState ReleaseState; + EAppOwnershipFlags OwnershipFlags; + uint32 _unknown3; + uint64 SteamID; + uint32 PurchasedTime; +}; \ No newline at end of file diff --git a/src/Utils/FileWatcher.cpp b/src/Utils/FileWatcher.cpp index 1b5b061..4fb7768 100644 --- a/src/Utils/FileWatcher.cpp +++ b/src/Utils/FileWatcher.cpp @@ -1,7 +1,7 @@ #include "dllmain.h" #include "FileWatcher.h" #include "LuaConfig.h" -#include "Hook/Hooks_Misc.h" +#include "Hook/Hooks_Package.h" #include "Log.h" #include #include @@ -49,7 +49,7 @@ namespace FileWatcher { } } - Hooks_Misc::NotifyLicenseChanged(); + Hooks_Package::NotifyLicenseChanged(); LOG_PACKAGE_INFO("Refresh completed"); } diff --git a/src/Utils/VehCommon.cpp b/src/Utils/VehCommon.cpp index 59f07d2..ff16847 100644 --- a/src/Utils/VehCommon.cpp +++ b/src/Utils/VehCommon.cpp @@ -1,15 +1,96 @@ #include "VehCommon.h" +namespace { + std::vector g_sites; + PVOID g_vehHandle = nullptr; +} + namespace VehCommon { - void ArmInt3(void* target) { - DWORD oldProtect = 0; - VirtualProtect(target, 1, PAGE_EXECUTE_READWRITE, &oldProtect); - *static_cast(target) = 0xCC; + +static LONG CALLBACK VehHandler(PEXCEPTION_POINTERS pExInfo) { + auto code = pExInfo->ExceptionRecord->ExceptionCode; + PCONTEXT ctx = pExInfo->ContextRecord; + + if (code == EXCEPTION_BREAKPOINT && OnBreakpoint(ctx)) + return EXCEPTION_CONTINUE_EXECUTION; + if (code == EXCEPTION_SINGLE_STEP && OnSingleStep(ctx)) + return EXCEPTION_CONTINUE_EXECUTION; + return EXCEPTION_CONTINUE_SEARCH; +} + +static void EnsureHandlerInstalled() { + if (!g_vehHandle) + g_vehHandle = AddVectoredExceptionHandler(1, VehHandler); +} + +static void ArmInt3(void* target) { + DWORD oldProtect = 0; + VirtualProtect(target, 1, PAGE_EXECUTE_READWRITE, &oldProtect); + *static_cast(target) = 0xCC; +} + +static void RestoreByte(void* target, uint8_t original) { + DWORD oldProtect = 0; + VirtualProtect(target, 1, PAGE_EXECUTE_READWRITE, &oldProtect); + *static_cast(target) = original; +} + +void Arm(Int3Site site) { + EnsureHandlerInstalled(); + g_sites.push_back(site); + ArmInt3(site.target); +} + +bool HasSites() { + return !g_sites.empty(); +} + +bool OnBreakpoint(PCONTEXT ctx) { + for (auto& site : g_sites) { + if (!site.target || !IsAt(ctx->Rip, site.target)) continue; + + // Restore the original byte so the CPU can execute the real + // first instruction when we resume. + *site.target = site.originalByte; + + if (site.onHit) site.onHit(ctx, site); + + if (site.persistent) { + // Set TF: CPU executes one instruction then raises SINGLE_STEP, + // where we re-arm the int3. + ctx->EFlags |= 0x100; + } + // For one-shot sites, leaving the byte restored permanently is the + // desired behavior -- no further action needed. + return true; } + return false; +} - void RestoreByte(void* target, uint8_t original) { - DWORD oldProtect = 0; - VirtualProtect(target, 1, PAGE_EXECUTE_READWRITE, &oldProtect); - *static_cast(target) = original; +bool OnSingleStep(PCONTEXT ctx) { + for (auto& site : g_sites) { + if (!site.persistent || !site.target) continue; + if (!IsPostInt3Step(ctx->Rip, site.target)) continue; + *site.target = 0xCC; // re-arm + return true; } + return false; } + +void DisarmAll() { + for (auto& site : g_sites) { + if (site.target && *site.target == 0xCC) { + RestoreByte(site.target, site.originalByte); + } + } + g_sites.clear(); +} + +void RemoveHandler() { + if (g_vehHandle) { + RemoveVectoredExceptionHandler(g_vehHandle); + g_vehHandle = nullptr; + } +} + +} // namespace VehCommon diff --git a/src/Utils/VehCommon.h b/src/Utils/VehCommon.h index 50ad48f..b9c5984 100644 --- a/src/Utils/VehCommon.h +++ b/src/Utils/VehCommon.h @@ -1,58 +1,178 @@ #pragma once +#include "Log.h" + #include #include +#include #include -// ── VEH one-shot capture entry ─────────────────────────────────────────────── -struct CaptureEntry { - void** funcPtr; // &o##Name - void** outPtr; // capture target (e.g. &g_pCUser) - uint8_t restoreByte; // original first byte, saved before arm - const char* label; -}; - -// ── X-macro helpers (all include trailing semicolons for list expansion) ───── -// CAPTURE_LIST(X): X(FuncName, CaptureVar) -#define VEH_DECL_CAPTURE(name, out) name##_t o##name; void* out; -#define VEH_ARM(name, out) ARM_CAPTURE_D(name, out); -// LOCATE_LIST(X): X(FuncName) -#define VEH_DECL_RESOLVE(name) name##_t o##name; -#define VEH_LOCATE(name) RESOLVE_D(name); -#define VEH_ZERO_RESOLVE(name) o##name = nullptr; - -// ── ARM_CAPTURE_D ──────────────────────────────────────────────────────────── -// Find signature, save original byte, push to g_captures, arm int3. -// Requires g_captures (std::vector) in scope. -#define ARM_CAPTURE_D(name, outVar) \ - do { \ - if (auto* _p_ = FIND_SIG(diversion_hMdoule, name)) { \ - o##name = reinterpret_cast(_p_); \ - g_captures.push_back({ \ - reinterpret_cast(&o##name), \ - reinterpret_cast(&(outVar)), \ - *reinterpret_cast(_p_), \ - #name \ - }); \ - VehCommon::ArmInt3(_p_); \ - } \ +namespace VehCommon { + + // ── x86-64 instruction length cap ──────────────────────────────────────── + constexpr uint64_t kMaxX64InsnLen = 15; + + // ── RIP comparisons ────────────────────────────────────────────────────── + inline bool IsAt(uint64_t rip, const void* target) { + return rip == reinterpret_cast(target); + } + + // True if rip is in (target, target + kMaxX64InsnLen], i.e. one + // instruction past `target`. Used by the SINGLE_STEP branch to recognize + // that the trap was caused by us setting TF after restoring `target`'s + // first byte. Independent of the prologue's actual instruction length. + inline bool IsPostInt3Step(uint64_t rip, const void* target) { + auto base = reinterpret_cast(target); + return rip > base && rip <= base + kMaxX64InsnLen; + } + + // ── x64 fastcall argument access ───────────────────────────────────────── + // Read the Nth argument (1-based) of the function whose prologue was just + // entered. Args 1-4 are in RCX/RDX/R8/R9; args 5+ live at [RSP + 8*N] + // (return address at RSP+0, shadow space at +8..+0x20, arg5 at +0x28). + // Only valid before RSP moves -- i.e. on an int3 hit at the first + // instruction of the function. + template + inline T GetArg(PCONTEXT ctx, int index) { + static_assert(sizeof(T) <= sizeof(uint64_t), + "GetArg: T must fit in a 64-bit register slot"); + uint64_t raw; + switch (index) { + case 1: raw = ctx->Rcx; break; + case 2: raw = ctx->Rdx; break; + case 3: raw = ctx->R8; break; + case 4: raw = ctx->R9; break; + default: raw = *reinterpret_cast(ctx->Rsp + 8 * index); + break; + } + if constexpr (std::is_pointer_v) { + return reinterpret_cast(raw); + } else { + return static_cast(raw); + } + } + + // ── Int3Site: managed soft-breakpoint site ─────────────────────────────── + // One soft-breakpoint location handled by the shared VEH dispatcher. + // + // persistent = false one-shot: on hit, restore byte and leave disarmed. + // Typical use: capture a `this` pointer once on the + // first call, then disappear. + // + // persistent = true on hit, restore byte and set TF. The next + // SINGLE_STEP exception re-arms the int3. Typical + // use: continuous interception that mutates args. + // + // onHit fires after the byte is restored but before control returns to the + // original code. It may read/mutate the CPU context (ctx) freely and inspect + // the site metadata for logging or site-specific callback data. + struct Int3Site { + uint8_t* target; // first byte of the hooked function + uint8_t originalByte; // saved before arming + bool persistent; + void (*onHit)(PCONTEXT ctx, const Int3Site& site); + void* callbackData; // site-specific data consumed by onHit + const char* label; // logging tag + }; + + // Append a site to the registry and write 0xCC to its first byte. + // Caller must populate `originalByte` from `*target` before calling. + void Arm(Int3Site site); + + // True if at least one site is currently registered. + bool HasSites(); + + // Drive these from a Vectored Exception Handler: + // - returns true if the exception matched a registered site and was + // handled (caller should return EXCEPTION_CONTINUE_EXECUTION) + // - returns false if nothing matched (caller should fall through to + // EXCEPTION_CONTINUE_SEARCH) + bool OnBreakpoint(PCONTEXT ctx); + bool OnSingleStep(PCONTEXT ctx); + + // Restore any still-armed bytes and clear the registry. Call during global + // hook shutdown before removing the VEH handler. + void DisarmAll(); + + // Remove the shared VEH handler. Call only after all sites are disarmed. + void RemoveHandler(); + + // Convenience onHit for the "capture this" pattern. + // callbackData must point to a `void*` slot that will receive RCX (the + // implicit `this` argument in the Windows x64 fastcall convention). + inline void CaptureRcxTo(PCONTEXT ctx, const Int3Site& site) { + *static_cast(site.callbackData) = GetArg(ctx, 1); + LOG_MISC_DEBUG("CaptureRcxTo {}: captured {}", site.label, GetArg(ctx, 1)); + } +} + +// ── CAPTURE_THIS_FUNC ──────────────────────────────────────────────────────── +// CAPTURE_THIS_FUNC(GetPackageInfo, PackageInfo*, g_pCPackageInfo, +// void*, uint32, int64); +// generates: +// using GetPackageInfo_t = PackageInfo*(__fastcall*)(void*, uint32, int64); +// inline GetPackageInfo_t oGetPackageInfo = nullptr; +// inline void* g_pCPackageInfo = nullptr; +// inline void** const _capture_out_GetPackageInfo = &g_pCPackageInfo; +// +// The trailing `_capture_out_*` slot lets ARM_CAPTURE_C(name) pick up the +// outVar binding without the caller having to repeat it. +#define CAPTURE_THIS_FUNC(name, ret, outVar, ...) \ + using name##_t = ret(__fastcall*)(__VA_ARGS__); \ + inline name##_t o##name = nullptr; \ + inline void* outVar = nullptr; \ + inline void** const _capture_out_##name = &outVar + +// ── ARM_INT3 ───────────────────────────────────────────────────────────────── +// Generic int3 site registration: resolve `name` in `module`, save its first +// byte, register an Int3Site with the given persistence, onHit, and callbackData. +// Use this when you need full control (e.g. persistent intercepts whose +// callback mutates args). For the common "capture this" pattern see +// ARM_CAPTURE_C below. +#define ARM_INT3(module, name, persistent_, onHit_, callbackData_) \ + do { \ + if (auto* _p_ = FIND_SIG(module, name)) { \ + auto* _t_ = static_cast(_p_); \ + VehCommon::Arm(VehCommon::Int3Site{ \ + _t_, *_t_, persistent_, onHit_, callbackData_, #name, \ + }); \ + } \ } while (0) -// ── VEH_CLEANUP_CAPTURES ───────────────────────────────────────────────────── -// Restore unarmed int3 sites, zero all pointers, clear the table. -#define VEH_CLEANUP_CAPTURES(captures) \ - do { \ - for (auto& _cap_ : (captures)) { \ - if (*_cap_.funcPtr \ - && *reinterpret_cast(*_cap_.funcPtr) == 0xCC) \ - VehCommon::RestoreByte(*_cap_.funcPtr, _cap_.restoreByte); \ - *_cap_.funcPtr = nullptr; \ - *_cap_.outPtr = nullptr; \ - } \ - (captures).clear(); \ +#define ARM_INT3_C(name, persistent_, onHit_, callbackData_) \ + ARM_INT3(client_hModule, name, persistent_, onHit_, callbackData_) + +#define ARM_INT3_U(name, persistent_, onHit_, callbackData_) \ + ARM_INT3(ui_hModule, name, persistent_, onHit_, callbackData_) + +// ── CAPTURE_READY ──────────────────────────────────────────────────────────── +// True when both the captured `this` pointer and the resolved function pointer +// are populated. Use to guard call sites where either may not be set yet +// (capture hasn't fired, or symbol resolution failed). +// +// if (CAPTURE_READY(GetAppDataFromAppInfo)) { ... } +// is equivalent to +// if (g_pCAppInfoCache && oGetAppDataFromAppInfo) { ... } +#define CAPTURE_READY(name) (*_capture_out_##name && o##name) + +// ── ARM_CAPTURE ──────────────────────────────────────────────────────────── +// Pair to CAPTURE_THIS_FUNC. Resolves the symbol, saves its first byte, +// registers a one-shot Int3Site that writes RCX into the outVar bound by +// CAPTURE_THIS_FUNC (via `_capture_out_##name`), and stashes the resolved +// address into `o##name` so callers can invoke the original after capture. +#define ARM_CAPTURE(module,name) \ + do { \ + if (auto* _p_ = FIND_SIG(module, name)) { \ + o##name = reinterpret_cast(_p_); \ + auto* _t_ = static_cast(_p_); \ + VehCommon::Arm(VehCommon::Int3Site{ \ + _t_, *_t_, /*persistent=*/false, \ + &VehCommon::CaptureRcxTo, \ + static_cast(_capture_out_##name), \ + #name, \ + }); \ + } \ } while (0) -namespace VehCommon { - void ArmInt3(void* target); - void RestoreByte(void* target, uint8_t original); -} +#define ARM_CAPTURE_C(name) ARM_CAPTURE(client_hModule, name) +#define ARM_CAPTURE_U(name) ARM_CAPTURE(ui_hModule, name) diff --git a/src/dllmain.cpp b/src/dllmain.cpp index a887468..9db12a9 100644 --- a/src/dllmain.cpp +++ b/src/dllmain.cpp @@ -3,8 +3,8 @@ #include "Utils/FileWatcher.h" #include "Utils/PatternLoader.h" -// Load diversion.dll and prepare key runtime paths. -bool LoadDiversion() +// prepare key runtime paths. +bool InitializeSteamComponents() { if (!GetCurrentDirectoryA(MAX_PATH, SteamInstallPath)) { return false; @@ -14,21 +14,19 @@ bool LoadDiversion() 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); - // ensure bin\ directory exists before copying - char binDir[MAX_PATH]; - sprintf_s(binDir, MAX_PATH, "%s\\bin", SteamInstallPath); - CreateDirectoryA(binDir, nullptr); // no-op if already exists - if (!CopyFileA(SteamclientPath, DiversionPath, FALSE)) { - LOG_ERROR("CopyFileA failed: {} -> {} (err={})", - SteamclientPath, DiversionPath, GetLastError()); + + client_hModule = LoadLibraryA(SteamclientPath); + if (!client_hModule) { + LOG_ERROR("LoadLibraryA failed: {} (err={})", SteamclientPath, GetLastError()); return false; } - diversion_hMdoule = LoadLibraryA(DiversionPath); - if (!diversion_hMdoule) { - LOG_ERROR("LoadLibraryA failed: {} (err={})", DiversionPath, GetLastError()); + 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; } - LOG_INFO("Loaded diversion.dll from {}", DiversionPath); return true; } @@ -40,8 +38,8 @@ static DWORD WINAPI InitThread(LPVOID param) { Log::Init(selfModule); LOG_INFO("OpenSteamTool init thread started"); - if (!LoadDiversion()) { - LOG_ERROR("LoadDiversion failed"); + if (!InitializeSteamComponents()) { + LOG_ERROR("InitializeSteamComponents failed"); return 1; } @@ -52,9 +50,8 @@ static DWORD WINAPI InitThread(LPVOID param) { // 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. - HMODULE hSteamUI = GetModuleHandleA("steamui.dll"); - PatternLoader::Load(hSteamUI, SteamUIPath, "steamui"); - PatternLoader::Load(diversion_hMdoule, SteamclientPath, "steamclient"); + PatternLoader::Load(ui_hModule, SteamUIPath, "steamui"); + PatternLoader::Load(client_hModule, SteamclientPath, "steamclient"); std::vector watchDirs = Config::luaPaths; watchDirs.push_back(std::string(LuaDir)); diff --git a/src/dllmain.h b/src/dllmain.h index 7dee563..ec81dd6 100644 --- a/src/dllmain.h +++ b/src/dllmain.h @@ -23,7 +23,9 @@ #include "Utils/Config.h" -inline HMODULE diversion_hMdoule = nullptr; +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] = {};