Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Hook/Hooks_IPC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -55,6 +56,7 @@ namespace {
}
const auto iface = static_cast<EIPCInterface>(data[OFFSET_INTERFACE_ID]);
const uint32 funcHash = *reinterpret_cast<const uint32*>(data + OFFSET_FUNC_HASH);
userStatsCall = iface == EIPCInterface::IClientUserStats;
entry = FindHandler(iface, funcHash);
if (entry) {
LOG_IPC_DEBUG("[InterfaceCall] {} {} realAppId={},AppId={}",
Expand All @@ -76,7 +78,11 @@ namespace {
}

// ── Run original ────────────────────────────────────────
if (userStatsCall)
Hooks_Misc::SetUserStatsContext(hSteamPipe, true);
const bool result = oIPCProcessMessage(pServer, hSteamPipe, pRead, pWrite);
if (userStatsCall)
Hooks_Misc::SetUserStatsContext(hSteamPipe, false);
if (!result || !entry) return result;

// Only run handlers for apps with configured depots.
Expand Down
1 change: 0 additions & 1 deletion src/Hook/Hooks_IPC.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
71 changes: 50 additions & 21 deletions src/Hook/Hooks_IPC_ISteamUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,12 @@ 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<const AppId_t*>(pWrite->Base() + 1);
if (current == realAppId) return;

*reinterpret_cast<AppId_t*>(pWrite->Base() + 1) = realAppId;
LOG_IPC_INFO("GetAppID: spoof response {} -> {}", current, realAppId);
}

// ════════════════════════════════════════════════════════════════
// GetAPICallResult per-callback handlers
// ════════════════════════════════════════════════════════════════

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;
Expand All @@ -68,18 +52,64 @@ namespace {
return true;
}

template<typename CallbackT>
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<CallbackT*>(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<UserStatsReceived_t>(
pipe, pWrite, "UserStatsReceived", cubCallback);
}

bool HandleCallback_GlobalAchievementPercentagesReady(
CSteamPipeClient* pipe, CUtlBuffer* pWrite, uint64, uint32 cubCallback)
{
return RewriteOnlineFixStatsResult<GlobalAchievementPercentagesReady_t>(
pipe, pWrite, "GlobalAchievementPercentagesReady", cubCallback);
}

bool HandleCallback_GlobalStatsReceived(
CSteamPipeClient* pipe, CUtlBuffer* pWrite, uint64, uint32 cubCallback)
{
return RewriteOnlineFixStatsResult<GlobalStatsReceived_t>(
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;

Expand All @@ -91,14 +121,13 @@ 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;
}
}
}

const Hooks_IPC::IpcHandlerEntry g_Entries[] = {
ADD_IPC_HANDLER(IClientUtils, GetAppID),
ADD_IPC_HANDLER(IClientUtils, GetAPICallResult),
};

Expand Down
55 changes: 53 additions & 2 deletions src/Hook/Hooks_Misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ namespace {
using CUtlBufferEnsureCapacity_t = void*(*)(CUtlBuffer*, int);
using CUtlMemoryGrow_t = void*(*)(CUtlVector<AppId_t>*, 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*);

// ── 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)
Expand All @@ -38,6 +36,9 @@ 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;

std::unordered_map<AppId_t, std::string> g_GameNameCache;

Expand Down Expand Up @@ -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;
}
Expand All @@ -99,6 +102,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.
Expand Down Expand Up @@ -155,13 +172,15 @@ namespace Hooks_Misc {
g_vehHandle = AddVectoredExceptionHandler(1, VehHandler);

HOOK_BEGIN();
INSTALL_HOOK_D(GetAppIDForCurrentPipe);
INSTALL_HOOK_D(BuildSpawnEnvBlock);
INSTALL_HOOK_D(OptedInMask);
HOOK_END();
}

void Uninstall() {
UNHOOK_BEGIN();
UNINSTALL_HOOK(GetAppIDForCurrentPipe);
UNINSTALL_HOOK(BuildSpawnEnvBlock);
UNINSTALL_HOOK(OptedInMask);
UNHOOK_END();
Expand All @@ -179,6 +198,9 @@ namespace Hooks_Misc {

LOCATE_LIST(VEH_ZERO_RESOLVE)
g_OnlineFixRealAppId = 0;
g_OnlineFixUserStatsPipe = 0;
g_steamEngine = nullptr;
g_userStatsAppIdOverrideDepth = 0;
g_GameNameCache.clear();
}

Expand All @@ -201,6 +223,35 @@ namespace Hooks_Misc {
return GetAppIDForCurrentPipe();
}

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<AppId_t>(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) {
Expand Down
9 changes: 8 additions & 1 deletion src/Hook/Hooks_Misc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +27,13 @@ namespace Hooks_Misc {
// otherwise fall back to GetAppIDForCurrentPipe().
AppId_t ResolveAppId();

// Select real app identity while forwarding OnlineFix user-stats calls.
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);

Expand Down
Loading
Loading