From 6ecb228d9327b507d108429c5e8363cd85f4db98 Mon Sep 17 00:00:00 2001 From: JanitorialMess <65749353+JanitorialMess@users.noreply.github.com> Date: Sun, 24 May 2026 19:56:44 -0400 Subject: [PATCH 1/4] Add signatures for Steam stable build 1779486452 --- src/Hook/Patterns.h | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Hook/Patterns.h b/src/Hook/Patterns.h index fa25b37..89cfab0 100644 --- a/src/Hook/Patterns.h +++ b/src/Hook/Patterns.h @@ -22,56 +22,67 @@ inline const Signature BBuildAndAsyncSendFrameSigs[] = { {"1778803745", "48 8B C4 55 48 8D 68 A1 48 81 EC C0 00 00 00 48 89 70 18"}, // beta {"1778281814", "48 8B C4 55 48 8D 68 A1 48 81 EC C0 00 00 00"}, // stable + {"1779486452", "48 8B C4 55 48 8D 68 A1 48 81 EC C0 00 00 00"}, // stable }; inline const Signature BuildDepotDependencySigs[] = { {"1778803745", "48 8B C4 4C 89 48 20 89 50 10 48 89 48 08 55 ?? 48 8D"}, // beta {"1778281814", "48 8B C4 4C 89 48 20 89 50 10 48 89 48 08 55 ?? 48 8D"}, // stable + {"1779486452", "48 8B C4 4C 89 48 20 89 50 10 48 89 48 08 55 ?? 48 8D"}, // stable }; inline const Signature BuildSpawnEnvBlockSigs[] = { {"1779155395", "4C 89 4C 24 ?? 4C 89 44 24 ?? 48 89 54 24 ?? 48 89 4C 24 ?? 55 53 56 57 41 54 41 55 41 56 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05"}, // beta {"1778281814", "48 89 5C 24 ?? 4C 89 44 24 ?? 48 89 54 24 ?? 48 89 4C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05"}, // stable + {"1779486452", "48 89 5C 24 ?? 4C 89 44 24 ?? 48 89 54 24 ?? 48 89 4C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05"}, // stable }; inline const Signature CUtlBufferEnsureCapacitySigs[] = { {"1778803745", "48 89 5C 24 08 57 48 83 EC 30 48 8B D9 8D"}, // beta {"1778281814", "48 89 5C 24 ?? 57 48 83 EC ?? 0F B6 41 ?? 8D 7A"}, // stable + {"1779486452", "48 89 5C 24 ?? 57 48 83 EC ?? 0F B6 41 ?? 8D 7A"}, // stable }; inline const Signature CUtlMemoryGrowSigs[] = { {"1778803745", "48 89 5C 24 10 57 48 83 EC 30 8B FA 48 8B D9 8B 51 08"}, // beta {"1778281814", "48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 30"}, // stable + {"1779486452", "48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 30"}, // stable }; inline const Signature CheckAppOwnershipSigs[] = { {"1778803745", "48 8B C4 89 50 10 48 89 48 08 55 53"}, // beta {"1778281814", "48 8B C4 89 50 10 55 53 48 8D 68 D8"}, // stable + {"1779486452", "48 8B C4 89 50 10 55 53 48 8D 68 D8"}, // stable }; inline const Signature GetAppDataFromAppInfoSigs[] = { {"1778803745", "40 53 55 56 57 41 56 41 57 48 81 EC 78 01 00 00 41 C6 01 00"}, // beta {"1778281814", "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 41 56 41 57 48 81 EC 70 01 00 00"}, // stable + {"1779486452", "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 41 56 41 57 48 81 EC 70 01 00 00"}, // stable }; inline const Signature GetAppIDForCurrentPipeSigs[] = { {"1778803745", "8B 81 30 0D 00 00 83 F8"}, // beta {"1778281814", "48 83 EC 08 8B 81 30 0D 00 00 4C 8B D9 44 8B 91 D8 00 00 00 83 F8 FF"}, // stable + {"1779486452", "48 83 EC 08 8B 81 30 0D 00 00 4C 8B D9 44 8B 91 D8 00 00 00 83 F8 FF"}, // stable }; inline const Signature GetPackageInfoSigs[] = { {"1778803745", "48 89 5C 24 18 89 54 24 10 55 56 57 48 83 EC 20 44 8B 49 20"}, // beta {"1778281814", "48 89 6C 24 ?? 41 56 48 83 EC ?? 8B 41 ?? 49 8B E8"}, // stable + {"1779486452", "48 89 6C 24 ?? 41 56 48 83 EC ?? 8B 41 ?? 49 8B E8"}, // stable }; inline const Signature GetPipeClientSigs[] = { {"1778803745", "85 D2 74 ?? 44 0F B7 CA"}, // beta {"1778281814", "85 D2 74 ?? 44 0F B7 CA"}, // stable + {"1779486452", "85 D2 74 ?? 44 0F B7 CA"}, // stable }; inline const Signature IPCProcessMessageSigs[] = { {"1778803745", "48 89 5C 24 18 48 89 6C 24 20 57 41 54 41 55 41 56 41 57 48"}, // beta {"1778281814", "48 89 5C 24 ?? 48 89 6C 24 ?? 56 41 54 41 55 41 56 41 57 48 83 EC ?? 49 8B D9"}, // stable + {"1779486452", "48 89 5C 24 ?? 48 89 6C 24 ?? 56 41 54 41 55 41 56 41 57 48 83 EC ?? 49 8B D9"}, // stable }; inline const Signature KeyValues_FindOrCreateKeySigs[] = { @@ -87,46 +98,55 @@ inline const Signature KeyValues_ReadAsBinarySigs[] = { inline const Signature LoadDepotDecryptionKeySigs[] = { {"1778803745", "40 53 55 56 57 48 83 EC 38 48 63 FA 49 8B E9"}, // beta {"1778281814", "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 48 63 FA 49 8B E9 8B D7 49 8B D8 48 8B F1"}, // stable + {"1779486452", "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 30 48 63 FA 49 8B E9 8B D7 49 8B D8 48 8B F1"}, // stable }; inline const Signature LoadPackageSigs[] = { {"1778803745", "44 89 44 24 18 53 55 56 57 41 55"}, // beta {"1778281814", "48 89 5C 24 18 48 89 6C 24 20 56 57 41 54 41 55 41 57 48 81 EC 20 01"}, // stable + {"1779486452", "48 89 5C 24 18 48 89 6C 24 20 56 57 41 54 41 55 41 57 48 81 EC 20 01"}, // stable }; inline const Signature MarkLicenseAsChangedSigs[] = { {"1778803745", "48 89 5C 24 20 89 54 24 10 55 56 57 48 83 EC 20"}, // beta {"1778281814", "89 54 24 ?? 53 55 56 57 41 56 48 83 EC"}, // stable + {"1779486452", "89 54 24 ?? 53 55 56 57 41 56 48 83 EC"}, // stable }; inline const Signature OptedInMaskSigs[] = { {"1779155395", "89 54 24 ?? 55 53 56 57 41 54 41 55 48 8D AC 24"}, // beta {"1778281814", "89 54 24 ?? 55 53 56 41 55 41 56 41 57"}, // stable + {"1779486452", "89 54 24 ?? 55 53 56 41 55 41 56 41 57"}, // stable }; inline const Signature PchMsgNameFromEMsgSigs[] = { {"1778803745", "48 89 5C 24 08 57 48 83 EC 20 8B D9 E8"}, // beta {"1778281814", "48 89 5C 24 08 57 48 83 EC 20 8B D9 E8"}, // stable + {"1779486452", "48 89 5C 24 08 57 48 83 EC 20 8B D9 E8"}, // stable }; inline const Signature ProcessPendingLicenseUpdatesSigs[] = { {"1778803745", "41 56 41 57 48 83 EC 38 83"}, // beta {"1778281814", "4C 8B DC 49 89 4B 08 41 55 41 57 48 83 EC 48 4C 8B E9"}, // stable + {"1779486452", "4C 8B DC 49 89 4B 08 41 55 41 57 48 83 EC 48 4C 8B E9"}, // stable }; inline const Signature RecvPktSigs[] = { {"1778803745", "48 8B C4 55 48 8D A8 98 F6 FF FF 48 81 EC 60 0A"}, // beta {"1778281814", "48 8B C4 55 48 8D A8 98 F6 FF FF 48 81 EC 60 0A"}, // stable + {"1779486452", "48 8B C4 55 48 8D A8 98 F6 FF FF 48 81 EC 60 0A"}, // stable }; inline const Signature SendCallbackToPipeSigs[] = { {"1778803745", "48 89 5C 24 ?? 57 48 83 EC ?? 41 8B D9 41 8B F8 E8 ?? ?? ?? ?? 48 8B C8"}, // beta {"1778281814", "48 89 5C 24 ?? 57 48 83 EC ?? 41 8B D9 41 8B F8 E8 ?? ?? ?? ?? 48 8B C8"}, // stable + {"1779486452", "48 89 5C 24 ?? 57 48 83 EC ?? 41 8B D9 41 8B F8 E8 ?? ?? ?? ?? 48 8B C8"}, // stable }; inline const Signature SpawnProcessSigs[] = { {"1778803745", "48 89 5C 24 18 4C 89 4C 24 20 48 89 54 24 10 55 56 57 41 54 41 55 41 56 41 57 48 8D"}, // beta {"1778281814", "48 89 5C 24 18 4C 89 4C 24 20 48 89 54 24 10 55 56 57 41 54 41 55 41 56 41 57 48 8D"}, // stable + {"1779486452", "48 89 5C 24 18 4C 89 4C 24 20 48 89 54 24 10 55 56 57 41 54 41 55 41 56 41 57 48 8D"}, // stable }; /* -------------------------------------------------------------------------- */ @@ -136,19 +156,23 @@ inline const Signature SpawnProcessSigs[] = { inline const Signature GetAppByIDSigs[] = { {"1779155395", "89 54 24 ?? 53 48 83 EC ?? 48 8B 05 ?? ?? ?? ?? 41 0F B6 D8"}, // beta {"1778281814", "89 54 24 ?? 56 48 83 EC ?? 48 8B 05"}, // stable + {"1779486452", "89 54 24 ?? 56 48 83 EC ?? 48 8B 05"}, // stable }; inline const Signature LoadModuleWithPathSigs[] = { {"1778803745", "48 89 5C 24 18 55 56 41 57 48 83 EC 40"}, // beta {"1778281814", "48 89 5C 24 18 48 89 6C 24 20 56 41 54 41 57 48 83 EC 40"}, // stable + {"1779486452", "48 89 5C 24 18 48 89 6C 24 20 56 41 54 41 57 48 83 EC 40"}, // stable }; inline const Signature TopManagerCallSigs[] = { {"1779155395", "83 FE 07 0F 84 ?? ?? ?? ?? E8 ?? ?? ?? ?? 45 33 C0"}, // beta {"1778281814", "83 FF 07 0F 84 ?? ?? ?? ?? E8 ?? ?? ?? ?? 45 33 C0"}, // stable + {"1779486452", "83 FF 07 0F 84 ?? ?? ?? ?? E8 ?? ?? ?? ?? 45 33 C0"}, // stable }; inline const Signature AddProtobufAsBinarySigs[] = { {"1779155395", "40 53 55 56 57 48 83 EC ?? 48 8B 05 ?? ?? ?? ?? 48 8B F2"}, // beta {"1778281814", "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 05 ?? ?? ?? ?? 48 8B F2 48 8B D9 44 8B 00"}, // stable + {"1779486452", "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 05 ?? ?? ?? ?? 48 8B F2 48 8B D9 44 8B 00"}, // stable }; From 457a9f8a0a53d55e9cdf5023286002ca9c0909a5 Mon Sep 17 00:00:00 2001 From: JanitorialMess <65749353+JanitorialMess@users.noreply.github.com> Date: Sun, 24 May 2026 20:05:00 -0400 Subject: [PATCH 2/4] Fix OnlineFix achievements app identity --- src/Hook/Hooks_IPC.cpp | 6 ++++++ src/Hook/Hooks_Misc.cpp | 31 +++++++++++++++++++++++++++++-- src/Hook/Hooks_Misc.h | 5 ++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/Hook/Hooks_IPC.cpp b/src/Hook/Hooks_IPC.cpp index cb8af2c..f65ed8d 100644 --- a/src/Hook/Hooks_IPC.cpp +++ b/src/Hook/Hooks_IPC.cpp @@ -40,6 +40,7 @@ namespace { // ── Parse header, find handler ────────────────────────── const IpcHandlerEntry* entry = nullptr; + bool userStatsCall = false; if (pRead->TellPut() >= IPC_HEADER_SIZE) { const uint8* data = pRead->Base(); @@ -55,6 +56,7 @@ namespace { } const auto iface = static_cast(data[OFFSET_INTERFACE_ID]); const uint32 funcHash = *reinterpret_cast(data + OFFSET_FUNC_HASH); + userStatsCall = iface == EIPCInterface::IClientUserStats; entry = FindHandler(iface, funcHash); if (entry) { LOG_IPC_DEBUG("[InterfaceCall] {} {} realAppId={},AppId={}", @@ -76,7 +78,11 @@ namespace { } // ── Run original ──────────────────────────────────────── + if (userStatsCall) + Hooks_Misc::SetUserStatsContext(true); const bool result = oIPCProcessMessage(pServer, hSteamPipe, pRead, pWrite); + if (userStatsCall) + Hooks_Misc::SetUserStatsContext(false); if (!result || !entry) return result; // Only run handlers for apps with configured depots. diff --git a/src/Hook/Hooks_Misc.cpp b/src/Hook/Hooks_Misc.cpp index d0df592..ec20fd0 100644 --- a/src/Hook/Hooks_Misc.cpp +++ b/src/Hook/Hooks_Misc.cpp @@ -9,7 +9,6 @@ namespace { 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*); @@ -17,7 +16,6 @@ namespace { // ── 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) @@ -38,6 +36,8 @@ namespace { // 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; + void* g_steamEngine; + thread_local uint32 g_userStatsAppIdOverrideDepth; std::unordered_map g_GameNameCache; @@ -99,6 +99,20 @@ namespace { return EXCEPTION_CONTINUE_SEARCH; } + // ── GetAppIDForCurrentPipe ─────────────────────────────────────────────── + HOOK_FUNC(GetAppIDForCurrentPipe, AppId_t, void* pEngine) + { + g_steamEngine = pEngine; + const AppId_t appId = oGetAppIDForCurrentPipe(pEngine); + if (g_userStatsAppIdOverrideDepth && g_OnlineFixRealAppId + && appId == kOnlineFixAppId) { + LOG_MISC_TRACE("GetAppIDForCurrentPipe: user-stats OnlineFix AppId={} -> {}", + appId, g_OnlineFixRealAppId); + return g_OnlineFixRealAppId; + } + return appId; + } + // ── SteamController_OptedInMask ────────────────────────────────────────── // Called by CUser_BuildSpawnEnvBlock with pGameID's appid to // compute EnableConfiguratorSupport and the SDL_* env vars. @@ -155,6 +169,7 @@ namespace Hooks_Misc { g_vehHandle = AddVectoredExceptionHandler(1, VehHandler); HOOK_BEGIN(); + INSTALL_HOOK_D(GetAppIDForCurrentPipe); INSTALL_HOOK_D(BuildSpawnEnvBlock); INSTALL_HOOK_D(OptedInMask); HOOK_END(); @@ -162,6 +177,7 @@ namespace Hooks_Misc { void Uninstall() { UNHOOK_BEGIN(); + UNINSTALL_HOOK(GetAppIDForCurrentPipe); UNINSTALL_HOOK(BuildSpawnEnvBlock); UNINSTALL_HOOK(OptedInMask); UNHOOK_END(); @@ -179,6 +195,8 @@ namespace Hooks_Misc { LOCATE_LIST(VEH_ZERO_RESOLVE) g_OnlineFixRealAppId = 0; + g_steamEngine = nullptr; + g_userStatsAppIdOverrideDepth = 0; g_GameNameCache.clear(); } @@ -201,6 +219,15 @@ namespace Hooks_Misc { return GetAppIDForCurrentPipe(); } + void SetUserStatsContext(bool active) + { + if (active) { + ++g_userStatsAppIdOverrideDepth; + } else if (g_userStatsAppIdOverrideDepth) { + --g_userStatsAppIdOverrideDepth; + } + } + void EnsureBufferSize(CUtlBuffer* pWrite, int32 size) { if (oCUtlBufferEnsureCapacity) { diff --git a/src/Hook/Hooks_Misc.h b/src/Hook/Hooks_Misc.h index a6cfa23..46e612e 100644 --- a/src/Hook/Hooks_Misc.h +++ b/src/Hook/Hooks_Misc.h @@ -4,7 +4,7 @@ // Catch-all for the lightweight info-capture int3 traps that don't fit a // dedicated category — currently: -// * GetAppIDForCurrentPipe -> captures the SteamEngine pointer +// * GetAppIDForCurrentPipe -> detoured for scoped OnlineFix stats identity // * SpawnProcess -> OnlineFix detection + 480 rewrite // * GetAppDataFromAppInfo -> captures the CAppInfoCache pointer // * MarkLicenseAsChanged -> captures pCUser; resolved for NotifyLicenseChanged @@ -27,6 +27,9 @@ namespace Hooks_Misc { // otherwise fall back to GetAppIDForCurrentPipe(). AppId_t ResolveAppId(); + // Select real app identity while forwarding OnlineFix user-stats calls. + void SetUserStatsContext(bool active); + // Get localized game name via GetAppDataFromAppInfo (cached). std::string GetGameNameByAppID(AppId_t appId); From 6095c9f5d3253ef5396eb82e4d037a91311a80c7 Mon Sep 17 00:00:00 2001 From: JanitorialMess <65749353+JanitorialMess@users.noreply.github.com> Date: Sun, 24 May 2026 21:04:47 -0400 Subject: [PATCH 3/4] Fix OnlineFix SteamNetworkingSockets app identity --- src/Hook/Hooks_IPC.h | 1 - src/Hook/Hooks_IPC_ISteamUtils.cpp | 17 ----------------- 2 files changed, 18 deletions(-) diff --git a/src/Hook/Hooks_IPC.h b/src/Hook/Hooks_IPC.h index 8206b22..a1d58da 100644 --- a/src/Hook/Hooks_IPC.h +++ b/src/Hook/Hooks_IPC.h @@ -29,7 +29,6 @@ constexpr uint32 HASH_IClientUser_GetAppOwnershipTicketExtendedData = 0xC7E71245 constexpr uint32 HASH_IClientUser_RequestEncryptedAppTicket = 0x25D6BB1D; constexpr uint32 HASH_IClientUser_GetEncryptedAppTicket = 0xE0468CB4; -constexpr uint32 HASH_IClientUtils_GetAppID = 0x09607EC4; constexpr uint32 HASH_IClientUtils_GetAPICallResult = 0x2D3D3947; constexpr uint32 HASH_IClientUtils_SetAppIDForCurrentPipe = 0x3378803C; diff --git a/src/Hook/Hooks_IPC_ISteamUtils.cpp b/src/Hook/Hooks_IPC_ISteamUtils.cpp index f11e671..1adf88e 100644 --- a/src/Hook/Hooks_IPC_ISteamUtils.cpp +++ b/src/Hook/Hooks_IPC_ISteamUtils.cpp @@ -31,22 +31,6 @@ namespace { return true; } - // ── Handler: IClientUtils::GetAppID ────────────────────────── - // SpawnProcess rewrites pGameID to 480 for OnlineFix games, - // so steamclient returns 480. Restore the real app_id. - void Handler_IClientUtils_GetAppID( - CSteamPipeClient* pipe, CUtlBuffer*, CUtlBuffer* pWrite) - { - AppId_t realAppId = Hooks_Misc::ResolveAppId(); - if (!realAppId || pWrite->m_Put < 5) return; - - AppId_t current = *reinterpret_cast(pWrite->Base() + 1); - if (current == realAppId) return; - - *reinterpret_cast(pWrite->Base() + 1) = realAppId; - LOG_IPC_INFO("GetAppID: spoof response {} -> {}", current, realAppId); - } - // ════════════════════════════════════════════════════════════════ // GetAPICallResult per-callback handlers // ════════════════════════════════════════════════════════════════ @@ -98,7 +82,6 @@ namespace { } const Hooks_IPC::IpcHandlerEntry g_Entries[] = { - ADD_IPC_HANDLER(IClientUtils, GetAppID), ADD_IPC_HANDLER(IClientUtils, GetAPICallResult), }; From 53ce657ee127d4975574f919a8c25bad19d7cedf Mon Sep 17 00:00:00 2001 From: JanitorialMess <65749353+JanitorialMess@users.noreply.github.com> Date: Mon, 25 May 2026 04:10:41 -0400 Subject: [PATCH 4/4] Bridge OnlineFix user stats callback identity --- src/Hook/Hooks_IPC.cpp | 4 +- src/Hook/Hooks_IPC_ISteamUtils.cpp | 54 ++++++- src/Hook/Hooks_Misc.cpp | 26 +++- src/Hook/Hooks_Misc.h | 6 +- src/Hook/Hooks_Package.cpp | 222 ++++++++++++++++++----------- src/Hook/Patterns.h | 6 + src/Steam/Callback.h | 62 ++++++++ 7 files changed, 291 insertions(+), 89 deletions(-) diff --git a/src/Hook/Hooks_IPC.cpp b/src/Hook/Hooks_IPC.cpp index f65ed8d..958812c 100644 --- a/src/Hook/Hooks_IPC.cpp +++ b/src/Hook/Hooks_IPC.cpp @@ -79,10 +79,10 @@ namespace { // ── Run original ──────────────────────────────────────── if (userStatsCall) - Hooks_Misc::SetUserStatsContext(true); + Hooks_Misc::SetUserStatsContext(hSteamPipe, true); const bool result = oIPCProcessMessage(pServer, hSteamPipe, pRead, pWrite); if (userStatsCall) - Hooks_Misc::SetUserStatsContext(false); + Hooks_Misc::SetUserStatsContext(hSteamPipe, false); if (!result || !entry) return result; // Only run handlers for apps with configured depots. diff --git a/src/Hook/Hooks_IPC_ISteamUtils.cpp b/src/Hook/Hooks_IPC_ISteamUtils.cpp index 1adf88e..8deed46 100644 --- a/src/Hook/Hooks_IPC_ISteamUtils.cpp +++ b/src/Hook/Hooks_IPC_ISteamUtils.cpp @@ -36,7 +36,7 @@ namespace { // ════════════════════════════════════════════════════════════════ bool HandleCallback_EncryptedAppTicketResponse( - CUtlBuffer* pWrite, uint64 hAsyncCall, uint32 cubCallback) + CSteamPipeClient*, CUtlBuffer* pWrite, uint64 hAsyncCall, uint32 cubCallback) { AppId_t appId = Hooks_IPC_ISteamUser::LookupEticketAsyncCall(hAsyncCall); if (!appId) return false; @@ -52,18 +52,64 @@ namespace { return true; } + template + bool RewriteOnlineFixStatsResult( + CSteamPipeClient* pipe, CUtlBuffer* pWrite, const char* name, uint32 cubCallback) + { + constexpr int32 total = 1 + 1 + sizeof(CallbackT) + 1; + if (!pipe || cubCallback < sizeof(CallbackT) || pWrite->m_Put < total + || pWrite->Base()[1] == 0) { + return false; + } + + auto* cb = reinterpret_cast(pWrite->Base() + 2); + const uint64 previous = cb->m_nGameID; + if (!Hooks_Misc::RewriteOnlineFixUserStatsCallback(pipe->m_hSteamPipe, cb->m_nGameID)) + return false; + + LOG_IPC_TRACE("GetAPICallResult: OnlineFix {} CGameID {:#x} -> {:#x}", + name, previous, cb->m_nGameID); + return true; + } + + bool HandleCallback_UserStatsReceived( + CSteamPipeClient* pipe, CUtlBuffer* pWrite, uint64, uint32 cubCallback) + { + return RewriteOnlineFixStatsResult( + pipe, pWrite, "UserStatsReceived", cubCallback); + } + + bool HandleCallback_GlobalAchievementPercentagesReady( + CSteamPipeClient* pipe, CUtlBuffer* pWrite, uint64, uint32 cubCallback) + { + return RewriteOnlineFixStatsResult( + pipe, pWrite, "GlobalAchievementPercentagesReady", cubCallback); + } + + bool HandleCallback_GlobalStatsReceived( + CSteamPipeClient* pipe, CUtlBuffer* pWrite, uint64, uint32 cubCallback) + { + return RewriteOnlineFixStatsResult( + pipe, pWrite, "GlobalStatsReceived", cubCallback); + } + struct GacrDispatchEntry { uint32 callbackId; - bool (*handler)(CUtlBuffer* pWrite, uint64 hAsyncCall, uint32 cubCallback); + bool (*handler)(CSteamPipeClient* pipe, CUtlBuffer* pWrite, + uint64 hAsyncCall, uint32 cubCallback); }; constexpr GacrDispatchEntry g_GacrDispatch[] = { { EncryptedAppTicketResponse_t::k_iCallback, HandleCallback_EncryptedAppTicketResponse }, + { UserStatsReceived_t::k_iCallback, HandleCallback_UserStatsReceived }, + { GlobalAchievementPercentagesReady_t::k_iCallback, + HandleCallback_GlobalAchievementPercentagesReady }, + { GlobalStatsReceived_t::k_iCallback, HandleCallback_GlobalStatsReceived }, }; // ── Handler: IClientUtils::GetAPICallResult ────────────────── void Handler_IClientUtils_GetAPICallResult( - CSteamPipeClient*, CUtlBuffer* pRead, CUtlBuffer* pWrite) + CSteamPipeClient* pipe, CUtlBuffer* pRead, CUtlBuffer* pWrite) { if (pRead->m_Put < OFFSET_ARGS + sizeof(GetAPICallResultRequest)) return; @@ -75,7 +121,7 @@ namespace { req->hSteamAPICall, appId, req->iCallbackExpected, req->cubCallback); for (auto& entry : g_GacrDispatch) { if (entry.callbackId == req->iCallbackExpected) { - entry.handler(pWrite, req->hSteamAPICall, req->cubCallback); + entry.handler(pipe, pWrite, req->hSteamAPICall, req->cubCallback); return; } } diff --git a/src/Hook/Hooks_Misc.cpp b/src/Hook/Hooks_Misc.cpp index ec20fd0..bf8aab4 100644 --- a/src/Hook/Hooks_Misc.cpp +++ b/src/Hook/Hooks_Misc.cpp @@ -36,6 +36,7 @@ namespace { // 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; + HSteamPipe g_OnlineFixUserStatsPipe; void* g_steamEngine; thread_local uint32 g_userStatsAppIdOverrideDepth; @@ -78,11 +79,13 @@ namespace { if (LuaConfig::HasDepot(appId) && cmdLine && strstr(cmdLine, "-onlinefix")) { g_OnlineFixRealAppId = appId; + g_OnlineFixUserStatsPipe = 0; *pGameID = kOnlineFixAppId; LOG_MISC_INFO("SpawnProcess: appid {} -> {}, cmd=\"{}\"", appId, kOnlineFixAppId, cmdLine); } else { g_OnlineFixRealAppId = 0; + g_OnlineFixUserStatsPipe = 0; } return EXCEPTION_CONTINUE_EXECUTION; } @@ -195,6 +198,7 @@ namespace Hooks_Misc { LOCATE_LIST(VEH_ZERO_RESOLVE) g_OnlineFixRealAppId = 0; + g_OnlineFixUserStatsPipe = 0; g_steamEngine = nullptr; g_userStatsAppIdOverrideDepth = 0; g_GameNameCache.clear(); @@ -219,15 +223,35 @@ namespace Hooks_Misc { return GetAppIDForCurrentPipe(); } - void SetUserStatsContext(bool active) + void SetUserStatsContext(HSteamPipe hSteamPipe, bool active) { if (active) { + if (g_OnlineFixRealAppId) + g_OnlineFixUserStatsPipe = hSteamPipe; ++g_userStatsAppIdOverrideDepth; } else if (g_userStatsAppIdOverrideDepth) { --g_userStatsAppIdOverrideDepth; } } + bool RewriteOnlineFixUserStatsCallback(HSteamPipe hSteamPipe, uint64& gameId) + { + constexpr uint64 appIdMask = 0xFFFFFF; + if (!g_OnlineFixRealAppId || hSteamPipe != g_OnlineFixUserStatsPipe + || static_cast(gameId & appIdMask) != g_OnlineFixRealAppId) { + return false; + } + + gameId = (gameId & ~appIdMask) | kOnlineFixAppId; + return true; + } + + bool ShouldRouteOnlineFixUserStatsCallback(AppId_t routeAppId) + { + return g_OnlineFixRealAppId && g_OnlineFixUserStatsPipe + && routeAppId == g_OnlineFixRealAppId; + } + void EnsureBufferSize(CUtlBuffer* pWrite, int32 size) { if (oCUtlBufferEnsureCapacity) { diff --git a/src/Hook/Hooks_Misc.h b/src/Hook/Hooks_Misc.h index 46e612e..0f6e7f5 100644 --- a/src/Hook/Hooks_Misc.h +++ b/src/Hook/Hooks_Misc.h @@ -28,7 +28,11 @@ namespace Hooks_Misc { AppId_t ResolveAppId(); // Select real app identity while forwarding OnlineFix user-stats calls. - void SetUserStatsContext(bool active); + void SetUserStatsContext(HSteamPipe hSteamPipe, bool active); + + // Present real-app stats callbacks through the OnlineFix game identity. + bool RewriteOnlineFixUserStatsCallback(HSteamPipe hSteamPipe, uint64& gameId); + bool ShouldRouteOnlineFixUserStatsCallback(AppId_t routeAppId); // Get localized game name via GetAppDataFromAppInfo (cached). std::string GetGameNameByAppID(AppId_t appId); diff --git a/src/Hook/Hooks_Package.cpp b/src/Hook/Hooks_Package.cpp index b872c7d..3a76499 100644 --- a/src/Hook/Hooks_Package.cpp +++ b/src/Hook/Hooks_Package.cpp @@ -1,81 +1,141 @@ -#include "Hooks_Package.h" -#include "HookMacros.h" -#include "dllmain.h" - -namespace { - using CUtlMemoryGrow_t = void* (*)(CUtlVector* pVec, int grow_size); - CUtlMemoryGrow_t oCUtlMemoryGrow = nullptr; - - 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); - for (uint32 i = 0; i < numToAdd; i++) - pInfo->AppIdVec.m_Memory.m_pMemory[oldSize + i] = appIds[i]; - } - } - - return result; - } - - HOOK_FUNC(CheckAppOwnership, bool, void* pObj, AppId_t appId, AppOwnership* pOwn) { - bool result = oCheckAppOwnership(pObj, appId, pOwn); - // 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->ReleaseState = EAppReleaseState::Released; - // Setting this free flag to false will hide it from the library UI. - pOwn->bFreeLicense = false; - return true; - } - } - 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={} -> true", - p->m_bReloadAll); - p->m_bReloadAll = true; - } - - return oSendCallbackToPipe(pSteamEngine, hSteamPipe, iClientUser, - iCallback, pCallbackData, cubCallbackData); - } -} - -namespace Hooks_Package { - void Install() { - RESOLVE_D(CUtlMemoryGrow); - - HOOK_BEGIN(); - INSTALL_HOOK_D(LoadPackage); - INSTALL_HOOK_D(CheckAppOwnership); - INSTALL_HOOK_D(SendCallbackToPipe); - HOOK_END(); - } - - void Uninstall() { - UNHOOK_BEGIN(); - UNINSTALL_HOOK(LoadPackage); - UNINSTALL_HOOK(CheckAppOwnership); - UNINSTALL_HOOK(SendCallbackToPipe); - UNHOOK_END(); - oCUtlMemoryGrow = nullptr; - } -} +#include "Hooks_Package.h" +#include "HookMacros.h" +#include "Hooks_Misc.h" +#include "dllmain.h" + +namespace { + using CUtlMemoryGrow_t = void* (*)(CUtlVector* pVec, int grow_size); + CUtlMemoryGrow_t oCUtlMemoryGrow = nullptr; + + void RewriteOnlineFixStatsCallback(HSteamPipe hSteamPipe, const char* name, + uint64& gameId) { + const uint64 previous = gameId; + if (Hooks_Misc::RewriteOnlineFixUserStatsCallback(hSteamPipe, gameId)) { + LOG_ACHIEVEMENT_TRACE("SendCallbackToPipe: OnlineFix {} CGameID {:#x} -> {:#x}", + name, previous, gameId); + } + } + + 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); + for (uint32 i = 0; i < numToAdd; i++) + pInfo->AppIdVec.m_Memory.m_pMemory[oldSize + i] = appIds[i]; + } + } + + return result; + } + + HOOK_FUNC(CheckAppOwnership, bool, void* pObj, AppId_t appId, AppOwnership* pOwn) { + bool result = oCheckAppOwnership(pObj, appId, pOwn); + // 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->ReleaseState = EAppReleaseState::Released; + // Setting this free flag to false will hide it from the library UI. + pOwn->bFreeLicense = false; + return true; + } + } + return result; + } + + // Steamclient routes ordinary callbacks only to registrations for appId. + // OnlineFix games register under public app 480 while their stats execute + // under the real app ID, so route the result through their registration. + HOOK_FUNC(DispatchCallbackByAppId, bool, void* pBaseUser, AppId_t appId, + int iCallback, void* pCallbackData, int cubCallbackData) { + constexpr uint64 appIdMask = 0xFFFFFF; + const auto* stats = static_cast(pCallbackData); + const bool routeOnlineFixStats = + iCallback == UserStatsReceived_t::k_iCallback + && pCallbackData + && cubCallbackData >= sizeof(UserStatsReceived_t) + && Hooks_Misc::ShouldRouteOnlineFixUserStatsCallback(appId) + && static_cast(stats->m_nGameID & appIdMask) == appId; + + UserStatsReceived_t publicResult{}; + if (routeOnlineFixStats) + publicResult = *stats; + + const bool result = oDispatchCallbackByAppId( + pBaseUser, appId, iCallback, pCallbackData, cubCallbackData); + if (!routeOnlineFixStats) + return result; + + const bool publicResultSent = oDispatchCallbackByAppId( + pBaseUser, kOnlineFixAppId, iCallback, + &publicResult, sizeof(publicResult)); + LOG_ACHIEVEMENT_TRACE( + "DispatchCallbackByAppId: OnlineFix UserStatsReceived route AppId {} -> {} result={} delivered={}", + appId, kOnlineFixAppId, static_cast(publicResult.m_eResult), publicResultSent); + return result || publicResultSent; + } + + 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={} -> true", + p->m_bReloadAll); + p->m_bReloadAll = true; + } else if (iCallback == UserStatsReceived_t::k_iCallback + && cubCallbackData >= sizeof(uint64)) { + auto* p = static_cast(pCallbackData); + RewriteOnlineFixStatsCallback(hSteamPipe, "UserStatsReceived", p->m_nGameID); + } else if (iCallback == UserStatsStored_t::k_iCallback + && cubCallbackData >= sizeof(uint64)) { + auto* p = static_cast(pCallbackData); + RewriteOnlineFixStatsCallback(hSteamPipe, "UserStatsStored", p->m_nGameID); + } else if (iCallback == UserAchievementStored_t::k_iCallback + && cubCallbackData >= sizeof(uint64)) { + auto* p = static_cast(pCallbackData); + RewriteOnlineFixStatsCallback(hSteamPipe, "UserAchievementStored", p->m_nGameID); + } else if (iCallback == UserAchievementIconFetched_t::k_iCallback + && cubCallbackData >= sizeof(uint64)) { + auto* p = static_cast(pCallbackData); + RewriteOnlineFixStatsCallback(hSteamPipe, "UserAchievementIconFetched", p->m_nGameID); + } + + return oSendCallbackToPipe(pSteamEngine, hSteamPipe, iClientUser, + iCallback, pCallbackData, cubCallbackData); + } +} + +namespace Hooks_Package { + void Install() { + RESOLVE_D(CUtlMemoryGrow); + + HOOK_BEGIN(); + INSTALL_HOOK_D(LoadPackage); + INSTALL_HOOK_D(CheckAppOwnership); + INSTALL_HOOK_D(DispatchCallbackByAppId); + INSTALL_HOOK_D(SendCallbackToPipe); + HOOK_END(); + } + + void Uninstall() { + UNHOOK_BEGIN(); + UNINSTALL_HOOK(LoadPackage); + UNINSTALL_HOOK(CheckAppOwnership); + UNINSTALL_HOOK(DispatchCallbackByAppId); + UNINSTALL_HOOK(SendCallbackToPipe); + UNHOOK_END(); + oCUtlMemoryGrow = nullptr; + } +} diff --git a/src/Hook/Patterns.h b/src/Hook/Patterns.h index 89cfab0..59c6774 100644 --- a/src/Hook/Patterns.h +++ b/src/Hook/Patterns.h @@ -143,6 +143,12 @@ inline const Signature SendCallbackToPipeSigs[] = { {"1779486452", "48 89 5C 24 ?? 57 48 83 EC ?? 41 8B D9 41 8B F8 E8 ?? ?? ?? ?? 48 8B C8"}, // stable }; +inline const Signature DispatchCallbackByAppIdSigs[] = { + {"1779155395", "4C 89 4C 24 20 44 89 44 24 18 41 54 41 55 41 56 48 83 EC 50"}, // beta + {"1778281814", "4C 89 4C 24 20 44 89 44 24 18 41 54 41 55 41 56 48 83 EC 50"}, // stable + {"1779486452", "4C 89 4C 24 20 44 89 44 24 18 41 54 41 55 41 56 48 83 EC 50"}, // stable +}; + inline const Signature SpawnProcessSigs[] = { {"1778803745", "48 89 5C 24 18 4C 89 4C 24 20 48 89 54 24 10 55 56 57 41 54 41 55 41 56 41 57 48 8D"}, // beta {"1778281814", "48 89 5C 24 18 4C 89 4C 24 20 48 89 54 24 10 55 56 57 41 54 41 55 41 56 41 57 48 8D"}, // stable diff --git a/src/Steam/Callback.h b/src/Steam/Callback.h index ab854db..11de322 100644 --- a/src/Steam/Callback.h +++ b/src/Steam/Callback.h @@ -3,6 +3,8 @@ // ── ISteamUser callbacks (base = 100) ─────────────────────────────── constexpr int k_iSteamUserCallbacks = 100; +constexpr int k_iSteamUserStatsCallbacks = 1100; +constexpr int k_cchStatNameMax = 128; //----------------------------------------------------------------------------- // Purpose: Result from RequestEncryptedAppTicket (async) @@ -14,6 +16,66 @@ struct EncryptedAppTicketResponse_t EResult m_eResult; }; +//----------------------------------------------------------------------------- +// Purpose: User stats callbacks carry the app identity back to the game. +//----------------------------------------------------------------------------- +struct UserStatsReceived_t +{ + enum { k_iCallback = k_iSteamUserStatsCallbacks + 1 }; + + uint64 m_nGameID; + EResult m_eResult; + uint32 m_unPadding; + uint64 m_steamIDUser; +}; +static_assert(sizeof(UserStatsReceived_t) == 0x18, + "UserStatsReceived_t must be 0x18 bytes"); + +struct UserStatsStored_t +{ + enum { k_iCallback = k_iSteamUserStatsCallbacks + 2 }; + + uint64 m_nGameID; + EResult m_eResult; +}; + +struct UserAchievementStored_t +{ + enum { k_iCallback = k_iSteamUserStatsCallbacks + 3 }; + + uint64 m_nGameID; + bool m_bGroupAchievement; + char m_rgchAchievementName[k_cchStatNameMax]; + uint32 m_nCurProgress; + uint32 m_nMaxProgress; +}; + +struct UserAchievementIconFetched_t +{ + enum { k_iCallback = k_iSteamUserStatsCallbacks + 9 }; + + uint64 m_nGameID; + char m_rgchAchievementName[k_cchStatNameMax]; + bool m_bAchieved; + int32 m_nIconHandle; +}; + +struct GlobalAchievementPercentagesReady_t +{ + enum { k_iCallback = k_iSteamUserStatsCallbacks + 10 }; + + uint64 m_nGameID; + EResult m_eResult; +}; + +struct GlobalStatsReceived_t +{ + enum { k_iCallback = k_iSteamUserStatsCallbacks + 12 }; + + uint64 m_nGameID; + EResult m_eResult; +}; + //----------------------------------------------------------------------------- // Purpose: Broadcast when app licenses change (additions / removals / reload). // Sent by CClientAppManager after ProcessPendingLicenseUpdates.