#include "detour.h" #include "common.h" #include "entity2/entitysystem.h" #include "entity2/entityinstance.h" #include "entityhandle.h" #include "gameconfig.h" #include "networksystem/inetworkserializer.h" #include "overrides.h" #include "playerslot.h" #include "schema.h" #include extern CGameConfig* g_gameConfig; extern CGameEntitySystem* g_entitySystem; extern bool g_enabled; extern bool g_armed; extern CGameEntitySystem* GameEntitySystem(); using SendSnapshotFn = void (*)(void*, void*); using PackEntitiesFn = void (*)(void*, void*, int, void*, void*, void*, void*, void*, void*); using PackEntityFn = void (*)(void*, void*, int, void*, void*, void*, void*, void*, void*, void*); static SendSnapshotFn g_sendSnapshot = nullptr; static PackEntitiesFn g_packEntities = nullptr; static PackEntityFn g_packEntity = nullptr; static funchook_t* g_snapshotHook = nullptr; static funchook_t* g_packHook = nullptr; static funchook_t* g_packEntityHook = nullptr; static uint64_t g_snapshotCalls = 0; static uint64_t g_snapshotApplied = 0; static uint64_t g_snapshotChanged = 0; static uint64_t g_packCalls = 0; static uint64_t g_packApplied = 0; static uint64_t g_packChanged = 0; static uint64_t g_packEntityCalls = 0; static uint64_t g_packEntitySpoofs = 0; static uint64_t g_packEntityScopedWrites = 0; static uint64_t g_packEntityGenericSpoofs = 0; static uint64_t g_packEntitySelfSkips = 0; static uint64_t g_packEntityNoRecipient = 0; static uint64_t g_fullUpdateWrites = 0; static int g_fullUpdateBudget = 0; static thread_local CPlayerSlot g_currentRecipient(-1); static CPlayerSlot SlotFromClient(void* client) { int offset = g_gameConfig ? g_gameConfig->GetOffset("CServerSideClient_Slot") : -1; if (offset < 0) return CPlayerSlot(-1); return *reinterpret_cast(reinterpret_cast(client) + offset); } struct ScopedRecipient { explicit ScopedRecipient(CPlayerSlot slot) : previous(g_currentRecipient) { g_currentRecipient = slot; } ~ScopedRecipient() { g_currentRecipient = previous; } CPlayerSlot previous; }; static void Detour_SendSnapshot(void* client, void* snapshot) { ++g_snapshotCalls; ScopedRecipient recipient(client ? SlotFromClient(client) : CPlayerSlot(-1)); if (client && g_fullUpdateBudget > 0) { int offset = g_gameConfig ? g_gameConfig->GetOffset("CServerSideClient_DeltaTick") : -1; if (offset >= 0) { *reinterpret_cast(reinterpret_cast(client) + offset) = -1; --g_fullUpdateBudget; ++g_fullUpdateWrites; } } if (!g_enabled || !g_armed || !client || g_overrides.Empty()) { g_sendSnapshot(client, snapshot); return; } CPlayerSlot slot = SlotFromClient(client); auto applied = g_overrides.ApplyForRecipient(slot); if (!applied.empty()) { ++g_snapshotApplied; g_snapshotChanged += applied.size(); } g_sendSnapshot(client, snapshot); g_overrides.Restore(applied); } static void Detour_PackEntities(void* self, void* arg1, int arg2, void* arg3, void* arg4, void* arg5, void* arg6, void* arg7, void* arg8) { ++g_packCalls; g_packEntities(self, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); } static CEntityIdentity* PlayerIdentityFromInstance(void* instance) { if (!instance) return nullptr; auto* entity = static_cast(instance); CEntityIdentity* identity = entity->m_pEntity; const char* designer = identity ? identity->m_designerName.String() : nullptr; return designer && !V_stricmp(designer, "player") ? identity : nullptr; } static bool DesignerContains(CEntityIdentity* identity, const char* needle) { const char* designer = identity ? identity->m_designerName.String() : nullptr; return designer && V_stristr(designer, needle); } static int EntityIndex(CEntityIdentity* identity) { return identity ? identity->m_EHandle.GetEntryIndex() : -1; } static int OwnerSlotForPawn(CEntityIdentity* pawnIdentity) { if (!pawnIdentity) return -1; if (!g_entitySystem) g_entitySystem = GameEntitySystem(); if (!g_entitySystem) return -1; auto pawnField = schema::FindField("CBasePlayerController", "m_hPawn"); auto slotField = schema::FindField("CBasePlayerController", "m_nSplitScreenSlot"); if (!pawnField.found || !slotField.found) return -1; int pawnIndex = EntityIndex(pawnIdentity); int visited = 0; for (auto* identity = g_entitySystem->m_EntityList.m_pFirstActiveEntity; identity && visited < 16384; identity = identity->m_pNext, ++visited) { if (!identity->m_pInstance || !DesignerContains(identity, "controller")) continue; auto* entity = reinterpret_cast(identity->m_pInstance); int controlledPawnIndex = reinterpret_cast(entity + pawnField.offset)->GetEntryIndex(); if (controlledPawnIndex != pawnIndex) continue; int slot = *reinterpret_cast(entity + slotField.offset); return slot >= 0 && slot < 64 ? slot : -1; } return -1; } static void Detour_PackEntity(void* self, void* arg1, int entityIndex, void* entityData, void* arg4, void* arg5, void* arg6, void* arg7, void* arg8, void* arg9) { ++g_packEntityCalls; if (!g_enabled || !g_armed || !entityData || !g_overrides.HasPackedOverrides()) { g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9); return; } CPlayerSlot recipient = g_currentRecipient; auto applied = g_overrides.ApplyForPackedEntity(recipient, entityIndex, entityData); g_packEntityGenericSpoofs += applied.size(); bool spoofedHealth = false; int savedHealth = 0; int* healthAddress = nullptr; int spoofHealth = 42; if (g_overrides.TryGetHealth42Value(recipient, spoofHealth)) { CEntityIdentity* identity = PlayerIdentityFromInstance(entityData); if (identity) { if (recipient.Get() < 0) ++g_packEntityNoRecipient; else if (OwnerSlotForPawn(identity) == recipient.Get()) { ++g_packEntitySelfSkips; } else { auto healthField = schema::FindField("CBaseEntity", "m_iHealth"); if (healthField.found && healthField.offset >= 0) { healthAddress = reinterpret_cast(reinterpret_cast(entityData) + healthField.offset); savedHealth = *healthAddress; *healthAddress = spoofHealth; spoofedHealth = true; ++g_packEntitySpoofs; ++g_packEntityScopedWrites; } } } } g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9); if (spoofedHealth) *healthAddress = savedHealth; g_overrides.Restore(applied); } static bool InitPackEntityDetour(CGameConfig* config) { void* packEntityTarget = config->ResolveSignature("CNetworkGameServer_PackEntity"); if (!packEntityTarget) { SPWarning("CNetworkGameServer_PackEntity signature missing; scoped health hook disabled\n"); return true; } g_packEntity = reinterpret_cast(packEntityTarget); g_packEntityHook = funchook_create(); if (!g_packEntityHook) return false; if (funchook_prepare(g_packEntityHook, reinterpret_cast(&g_packEntity), reinterpret_cast(Detour_PackEntity)) != 0) return false; if (funchook_install(g_packEntityHook, 0) != 0) return false; SPMessage("detoured CNetworkGameServer_PackEntity at %p\n", packEntityTarget); return true; } bool InitSnapshotDetour(CGameConfig* config) { void* target = config->ResolveSignature("CServerSideClient_SendSnapshot"); if (!target) { SPWarning("CServerSideClient_SendSnapshot signature missing; snapshot hook disabled\n"); return true; } g_sendSnapshot = reinterpret_cast(target); g_snapshotHook = funchook_create(); if (!g_snapshotHook) return false; if (funchook_prepare(g_snapshotHook, reinterpret_cast(&g_sendSnapshot), reinterpret_cast(Detour_SendSnapshot)) != 0) return false; if (funchook_install(g_snapshotHook, 0) != 0) return false; SPMessage("detoured CServerSideClient_SendSnapshot at %p\n", target); void* packTarget = config->ResolveSignature("CNetworkGameServer_PackEntities_Normal"); if (!packTarget) { SPWarning("CNetworkGameServer_PackEntities_Normal signature missing; pre-pack health_42 hook disabled\n"); return true; } g_packEntities = reinterpret_cast(packTarget); g_packHook = funchook_create(); if (!g_packHook) return false; if (funchook_prepare(g_packHook, reinterpret_cast(&g_packEntities), reinterpret_cast(Detour_PackEntities)) != 0) return false; if (funchook_install(g_packHook, 0) != 0) return false; SPMessage("detoured CNetworkGameServer_PackEntities_Normal at %p\n", packTarget); if (!InitPackEntityDetour(config)) return false; return true; } void ShutdownSnapshotDetour() { if (g_packEntityHook) { funchook_uninstall(g_packEntityHook, 0); funchook_destroy(g_packEntityHook); g_packEntityHook = nullptr; } if (g_packHook) { funchook_uninstall(g_packHook, 0); funchook_destroy(g_packHook); g_packHook = nullptr; } if (g_snapshotHook) { funchook_uninstall(g_snapshotHook, 0); funchook_destroy(g_snapshotHook); g_snapshotHook = nullptr; } g_sendSnapshot = nullptr; g_packEntities = nullptr; g_packEntity = nullptr; } void DumpDetourStats() { SPMessage("hooks: snapshot calls=%llu applied=%llu changed=%llu; pack calls=%llu applied=%llu changed=%llu; pack_entity calls=%llu health_spoofs=%llu generic_spoofs=%llu scoped_writes=%llu self_skips=%llu no_recipient=%llu; fullupdate writes=%llu budget=%d\n", static_cast(g_snapshotCalls), static_cast(g_snapshotApplied), static_cast(g_snapshotChanged), static_cast(g_packCalls), static_cast(g_packApplied), static_cast(g_packChanged), static_cast(g_packEntityCalls), static_cast(g_packEntitySpoofs), static_cast(g_packEntityGenericSpoofs), static_cast(g_packEntityScopedWrites), static_cast(g_packEntitySelfSkips), static_cast(g_packEntityNoRecipient), static_cast(g_fullUpdateWrites), g_fullUpdateBudget); } void RequestSnapshotFullUpdates(int budget) { if (budget > g_fullUpdateBudget) g_fullUpdateBudget = budget; }