Vtable slot 4 hook: direct request/response (no CProtoBufMsg wrapper). ClientBeginFileUpload / ClientCommitFileUpload / ClientFileDownload all call slot 4 directly, so handling here avoids the slot-5 deferred queue. Flags layout (int[68]): [0]=routing_appid, [1]=mode, [2]=error_code, [3]=eresult, [4..67]=error_message
| 1735 | // call slot 4 directly, so handling here avoids the slot-5 deferred queue. |
| 1736 | // Flags layout (int[68]): [0]=routing_appid, [1]=mode, [2]=error_code, [3]=eresult, [4..67]=error_message |
| 1737 | static bool __fastcall ServiceMethodDirectHook(void* thisptr, const char* methodName, |
| 1738 | void* requestBody, void* responseBody, int* flags) { |
| 1739 | HookGuard guard; |
| 1740 | if (g_shuttingDown.load(std::memory_order_acquire)) |
| 1741 | return g_originalSlot4(thisptr, methodName, requestBody, responseBody, flags); |
| 1742 | |
| 1743 | // cloudSaveOnly: drain playtime updates here (no RecvPkt/SendPkt hooks). |
| 1744 | if (g_cloudSaveOnly.load(std::memory_order_relaxed)) |
| 1745 | DrainPlaytimeUpdateQueueOnNetThread(); |
| 1746 | |
| 1747 | if (!methodName) { |
| 1748 | return g_originalSlot4(thisptr, methodName, requestBody, responseBody, flags); |
| 1749 | } |
| 1750 | |
| 1751 | if (g_parentalBypassPlaytime.load() && |
| 1752 | strcmp(methodName, "Parental.GetSignedParentalSettings#1") == 0) { |
| 1753 | bool result = g_originalSlot4(thisptr, methodName, requestBody, responseBody, flags); |
| 1754 | if (result && responseBody) { |
| 1755 | auto respBytes = SerializeBodyToBytes(responseBody); |
| 1756 | auto respFields = PB::Parse(respBytes.data(), respBytes.size()); |
| 1757 | const PB::Field* sf = PB::FindField(respFields, 1); |
| 1758 | if (sf && sf->wireType == PB::LengthDelimited && sf->data) { |
| 1759 | auto stripped = ParentalBypass::StripPlaytimeRestrictions(sf->data, sf->dataLen); |
| 1760 | PB::Writer newResp; |
| 1761 | newResp.WriteBytes(1, stripped.data(), stripped.size()); |
| 1762 | for (const auto& f : respFields) { |
| 1763 | if (f.fieldNum == 1) continue; |
| 1764 | if (f.wireType == PB::Varint) newResp.WriteVarint(f.fieldNum, f.varintVal); |
| 1765 | else if (f.wireType == PB::Fixed64) newResp.WriteFixed64(f.fieldNum, f.varintVal); |
| 1766 | else if (f.wireType == PB::LengthDelimited) newResp.WriteBytes(f.fieldNum, f.data, f.dataLen); |
| 1767 | } |
| 1768 | if (ParseBytesToBody(responseBody, newResp.Data().data(), newResp.Size())) |
| 1769 | LOG("[Parental] Stripped restrictions from GetSignedParentalSettings (slot4)"); |
| 1770 | } |
| 1771 | } |
| 1772 | return result; |
| 1773 | } |
| 1774 | |
| 1775 | // Player.GetUserStats#1 (slot 4, raw bodies). Third-party uses raw schemaFetch flag. |
| 1776 | if (strcmp(methodName, StatsHandlers::RPC_GET_USER_STATS) == 0 |
| 1777 | && (g_cloudSaveOnly.load(std::memory_order_relaxed) |
| 1778 | ? MetadataSync::schemaFetch.load(std::memory_order_relaxed) |
| 1779 | : MetadataSync::SchemaFetchEnabled())) { |
| 1780 | if (requestBody && responseBody && g_serializeToArray) { |
| 1781 | auto reqBytes = SerializeBodyToBytes(requestBody); |
| 1782 | auto reqFields = PB::Parse(reqBytes.data(), reqBytes.size()); |
| 1783 | uint32_t appId = 0; |
| 1784 | if (auto* f = PB::FindField(reqFields, 2)) appId = (uint32_t)f->varintVal; // appid #2 |
| 1785 | LOG("[Stats] slot4 GetUserStats seen: app=%u namespace=%d", appId, IsNamespaceApp(appId) ? 1 : 0); |
| 1786 | if (appId != 0 && IsNamespaceApp(appId)) { |
| 1787 | auto res = StatsHandlers::HandleGetUserStats(appId, reqFields); |
| 1788 | if (res.body.Size() > 0 && |
| 1789 | ParseBytesToBody(responseBody, res.body.Data().data(), res.body.Size())) { |
| 1790 | if (flags) { flags[2] = 1; flags[3] = res.eresult; } |
| 1791 | LOG("[Stats] GetUserStats app=%u handled locally via slot4 (%zu bytes)", |
| 1792 | appId, res.body.Size()); |
| 1793 | return true; |
| 1794 | } |
nothing calls this directly
no test coverage detected