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
64 changes: 55 additions & 9 deletions src/Hook/Hooks_NetPacket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
// ════════════════════════════════════════════════════════════════
namespace {

constexpr uint32 kMaxBodySize = 8092;
constexpr uint32 kMaxBodySize = 65536;
constexpr uint32 kMaxHdrSize = 1024;
constexpr uint32 kMaxPacketSize = 8 + kMaxHdrSize + kMaxBodySize;
constexpr int kPacketPoolSize = 8;
Expand Down Expand Up @@ -338,9 +338,9 @@ namespace Hooks_NetPacket_UserStats {
LOG_ACHIEVEMENT_WARN("ClientGetUserStats request: appid={} is not in addappid", appId);
return false;
}
if (!req.has_schema_local_version() || req.schema_local_version() != -1) {
LOG_ACHIEVEMENT_WARN("ClientGetUserStats request: schema_local_version is not -1");
return false;
if (req.schema_local_version() != -1) {
req.set_schema_local_version(-1);
LOG_ACHIEVEMENT_DEBUG("ClientGetUserStats request: forced schema_local_version to -1");
}

uint64_t newSteamId = LuaConfig::GetStatSteamId(appId);
Expand All @@ -357,7 +357,7 @@ namespace Hooks_NetPacket_UserStats {
}

// ── Recv: CMsgClientGetUserStatsResponse (eMsg 819) ────────
// Strip stats(5) + achievement_blocks(6), patch eresult->OK.
// Clear donor stats, overlay CR achievements, patch eresult->OK.
bool HandleRecv_ClientGetUserStatsResponse(const uint8* pBody, uint32 cbBody)
{
CMsgClientGetUserStatsResponse resp;
Expand All @@ -371,13 +371,39 @@ namespace Hooks_NetPacket_UserStats {
resp.clear_stats();
resp.clear_achievement_blocks();
resp.set_eresult(1); // k_EResultOK
LOG_ACHIEVEMENT_DEBUG("ClientGetUserStats response: clear stats and achievement_blocks, set eresult=OK");

g_NewBodySize = static_cast<uint32>(resp.ByteSizeLong());
if (!resp.SerializeToArray(const_cast<uint8*>(pBody), cbBody))
// Overlay CR's cloud-synced achievement state
const auto appId = static_cast<uint32_t>(resp.game_id());
CloudRedirectHost::AchievementBlock blocks[64];
uint32_t n = CloudRedirectHost::GetAchievements(appId, blocks, 64);
if (n > 0) {
uint32_t crc = 0;
for (uint32_t i = 0; i < n; i++) {
auto* s = resp.add_stats();
s->set_stat_id(blocks[i].statId);
s->set_stat_value(blocks[i].bits);
auto* ab = resp.add_achievement_blocks();
ab->set_achievement_id(blocks[i].statId);
for (uint32_t bit = 0; bit < 32; bit++)
ab->add_unlock_time(blocks[i].unlockTimes[bit]);
crc ^= blocks[i].bits ^ blocks[i].statId;
}
resp.set_crc_stats(crc);
LOG_ACHIEVEMENT_DEBUG("ClientGetUserStats response: injected {} CR achievement blocks for app {}", n, appId);
} else {
LOG_ACHIEVEMENT_DEBUG("ClientGetUserStats response: cleared stats/achievements (no CR data for app {})", appId);
}

auto newSize = resp.ByteSizeLong();
if (newSize > sizeof(g_NewBody)) {
LOG_ACHIEVEMENT_WARN("ClientGetUserStats response: modified message too large ({} bytes)", newSize);
return false;
}
if (!resp.SerializeToArray(g_NewBody, sizeof(g_NewBody)))
return false;

g_ResizedInPlace = true;
g_cbNewBody = static_cast<uint32>(newSize);
g_NeedReplaceBody = true;
LOG_ACHIEVEMENT_DEBUG("ClientGetUserStats response: modified body:\n{}", resp.DebugString());
return true;
}
Expand Down Expand Up @@ -746,6 +772,8 @@ namespace Hooks_NetPacket_RichPresence {
if (hdr.ParseFromArray(pHdr, cbHdr) && hdr.has_steamid() && hdr.steamid()) {
g_LocalSteamId = hdr.steamid();
LOG_RICHPRESENCE_DEBUG("Captured local SteamID 0x{:X}", g_LocalSteamId);
CloudRedirectHost::SetAccountId(
static_cast<uint32_t>(g_LocalSteamId & 0xFFFFFFFF));
}
}

Expand All @@ -766,8 +794,14 @@ namespace Hooks_NetPacket_RichPresence {
newTracked = topmost;

if (g_PlayingAppId == newTracked) return;
AppId_t oldTracked = g_PlayingAppId;
g_PlayingAppId = newTracked;

if (oldTracked != 0)
CloudRedirectHost::NotifyAppRunning(oldTracked, false);
if (newTracked != 0)
CloudRedirectHost::NotifyAppRunning(newTracked, true);

if (newTracked != 0) {
LOG_RICHPRESENCE_INFO("Tracking topmost appid {}", newTracked);
if (BuildInject(newTracked)) g_InjectPending = true;
Expand Down Expand Up @@ -1098,7 +1132,12 @@ namespace {
// Steam Cloud save redirection: hand every "Cloud.*" request to
// CloudRedirect. If it answers, suppress the outbound frame (the
// synthesized response is delivered from the RecvPkt hook).
// ExitSyncDone/ConflictResolution are notifications that must reach
// Steam's internal cloud state machine untouched.
if (std::strncmp(targetJobName, "Cloud.", 6) == 0) {
if (std::strcmp(targetJobName, "Cloud.SignalAppExitSyncDone#1") == 0 ||
std::strcmp(targetJobName, "Cloud.ClientConflictResolution#1") == 0)
return false;
if (Hooks_NetPacket_Cloud::HandleSend(targetJobName, pBody, cbBody, pHdr, cbHdr))
g_SuppressSend = true;
return false; // never body-replace a cloud frame
Expand Down Expand Up @@ -1153,6 +1192,13 @@ namespace {
g_NeedReplaceSend = Hooks_NetPacket_UserStats::HandleSend_ClientGetUserStats(pBody, cbBody);
return;

case k_EMsgClientStoreUserStats2: { // 5466
AppId_t appId = Hooks_NetPacket_RichPresence::g_PlayingAppId;
if (appId != 0)
CloudRedirectHost::NotifyStatsStored(appId);
return;
}

default:
return;
}
Expand Down
67 changes: 60 additions & 7 deletions src/Utils/CloudRedirect/CloudRedirectHost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,30 @@ namespace {
using CR_AddApp_t = void (*)(uint32_t appId);
using CR_RemoveApp_t = void (*)(uint32_t appId);
using CR_IsApp_t = bool (*)(uint32_t appId);
using CR_SetApps_t = void (*)(const uint32_t* appIds, uint32_t count);
using CR_Shutdown_t = void (*)();
using CR_SetApps_t = void (*)(const uint32_t* appIds, uint32_t count);
using CR_Shutdown_t = void (*)();
using CR_EnableStatsSync_t = void (*)(bool, bool);
using CR_SetAccountId_t = void (*)(uint32_t);
using CR_NotifyAppRunning_t = void (*)(uint32_t, bool);
using CR_NotifyStatsStored_t = void (*)(uint32_t);
using CR_GetAchievements_t = uint32_t (*)(uint32_t, CloudRedirectHost::AchievementBlock*, uint32_t);
using CR_InstallVtableHooks_t = bool (*)();

std::mutex g_mutex;
std::atomic<bool> g_active{false};
OSTPlatform::DynamicLibrary::ModuleHandle g_module = nullptr;

CR_InitCloudSave_t g_initCloudSave = nullptr;
CR_HandleCloudRpc_t g_handleCloudRpc = nullptr;
CR_SetApps_t g_setApps = nullptr;
CR_IsApp_t g_isApp = nullptr;
CR_Shutdown_t g_shutdownFn = nullptr;
CR_InitCloudSave_t g_initCloudSave = nullptr;
CR_HandleCloudRpc_t g_handleCloudRpc = nullptr;
CR_SetApps_t g_setApps = nullptr;
CR_IsApp_t g_isApp = nullptr;
CR_Shutdown_t g_shutdownFn = nullptr;
CR_EnableStatsSync_t g_enableStatsSync = nullptr;
CR_SetAccountId_t g_setAccountId = nullptr;
CR_NotifyAppRunning_t g_notifyAppRunning = nullptr;
CR_NotifyStatsStored_t g_notifyStatsStored = nullptr;
CR_GetAchievements_t g_getAchievements = nullptr;
CR_InstallVtableHooks_t g_installVtableHooks = nullptr;

// Routes CloudRedirect's notifications into OpenSteamTool's log instead of
// popping a MessageBox from inside Steam.
Expand Down Expand Up @@ -112,6 +124,14 @@ void Initialize(const char* steamInstallPath) {
return;
}

// Optional (CR 2.2.5+)
ResolveSymbol(g_module, "CR_EnableStatsSync", g_enableStatsSync);
ResolveSymbol(g_module, "CR_SetAccountId", g_setAccountId);
ResolveSymbol(g_module, "CR_NotifyAppRunning", g_notifyAppRunning);
ResolveSymbol(g_module, "CR_NotifyStatsStored", g_notifyStatsStored);
ResolveSymbol(g_module, "CR_GetAchievements", g_getAchievements);
ResolveSymbol(g_module, "CR_InstallVtableHooks", g_installVtableHooks);

if (!g_initCloudSave(steamInstallPath, &CloudNotify)) {
LOG_WARN("CloudRedirect: CR_InitCloudSave failed, disabling cloud save redirection");
g_module = nullptr;
Expand All @@ -122,12 +142,25 @@ void Initialize(const char* steamInstallPath) {
LOG_INFO("CloudRedirect: loaded {} and initialised cloud save redirection",
libPath.string());

if (g_enableStatsSync) {
g_enableStatsSync(true, true);
LOG_INFO("CloudRedirect: stats sync registered");
}

// Push the current unlocked-app set without re-locking g_mutex.
std::vector<AppId_t> depots = LuaConfig::GetAllDepotIds();
std::vector<uint32_t> appIds(depots.begin(), depots.end());
g_setApps(appIds.empty() ? nullptr : appIds.data(),
static_cast<uint32_t>(appIds.size()));
LOG_INFO("CloudRedirect: registered {} redirected app(s)", appIds.size());

// Vtable hooks let CR handle Cloud RPCs synchronously (slot4 semantics).
if (g_installVtableHooks) {
if (g_installVtableHooks())
LOG_INFO("CloudRedirect: vtable hooks installed");
else
LOG_WARN("CloudRedirect: vtable hook install failed, using packet-layer path");
}
}

void SyncAppSet() {
Expand Down Expand Up @@ -158,6 +191,26 @@ bool HandleCloudRpc(const char* method, uint32_t appId, uint32_t accountId,
respBuf, respMaxLen, respLen, eresult);
}

void SetAccountId(uint32_t accountId) {
if (!g_active.load(std::memory_order_acquire) || !g_setAccountId) return;
g_setAccountId(accountId);
}

void NotifyAppRunning(uint32_t appId, bool running) {
if (!g_active.load(std::memory_order_acquire) || !g_notifyAppRunning) return;
g_notifyAppRunning(appId, running);
}

void NotifyStatsStored(uint32_t appId) {
if (!g_active.load(std::memory_order_acquire) || !g_notifyStatsStored) return;
g_notifyStatsStored(appId);
}

uint32_t GetAchievements(uint32_t appId, AchievementBlock* out, uint32_t maxBlocks) {
if (!g_active.load(std::memory_order_acquire) || !g_getAchievements) return 0;
return g_getAchievements(appId, out, maxBlocks);
}

void Shutdown() {
std::lock_guard lock(g_mutex);
if (!g_active.exchange(false)) return;
Expand Down
12 changes: 12 additions & 0 deletions src/Utils/CloudRedirect/CloudRedirectHost.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ namespace CloudRedirectHost {
uint8_t* respBuf, uint32_t respMaxLen,
uint32_t* respLen, int32_t* eresult);

void SetAccountId(uint32_t accountId);
void NotifyAppRunning(uint32_t appId, bool running);
void NotifyStatsStored(uint32_t appId);

struct AchievementBlock {
uint32_t statId;
uint32_t bits;
uint32_t unlockTimes[32];
};
// Returns number of blocks written (0 if none/disabled).
uint32_t GetAchievements(uint32_t appId, AchievementBlock* out, uint32_t maxBlocks);

// Teardown. Called from DLL_PROCESS_DETACH.
void Shutdown();

Expand Down