From d7edcc9d9e3d90e1c53885870461e046389dee96 Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 26 Oct 2026 15:33:02 +0100 Subject: ESP working: NPC detection + authoritative server-side health - entitylist slot 6 (GetHighestEntityIndex); class name via GetClientNetworkable -> GetClientClass (replaces the unsafe slot-2 call) - HL2 SP NPCs: class CAI_BaseNPC + m_lifeState, since NPC m_iHealth is not networked; model names from VModelInfoClient006 for labels/friend-foe - real HP/HP-max from the listen-server entity (IVEngineServer021 -> PEntityOfEntIndex -> edict->m_pUnk -> GetBaseEntity) using offsets scanned from server.so's datamap; validated against the client model index - world_to_screen via IVDebugOverlay003::ScreenPosition (fixes box drift) - maxs sanity clamp, NPC-only debug logging, neutral colour for wildlife - build.sh: restore -o libhl2hake.so and add server.cpp --- src/netvars.cpp | 67 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 16 deletions(-) (limited to 'src/netvars.cpp') diff --git a/src/netvars.cpp b/src/netvars.cpp index 5c8c296..f702c41 100644 --- a/src/netvars.cpp +++ b/src/netvars.cpp @@ -27,7 +27,24 @@ static bool module_string(uintptr_t p, char *out, size_t outn) { static int g_dumped_tables = 0; -static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { +// per-client-class health offset (m_iHealth lives at different offsets per +// class chain: CBasePlayer via DT_BasePlayer=128, NPCs via DT_BaseCombatCharacter) +static struct { + char name[32]; + int off; +} g_class_health[256]; +static int g_class_health_n = 0; + +int class_health_off(const char *name) { + for (int i = 0; i < g_class_health_n; i++) + if (strcmp(name, g_class_health[i].name) == 0) + return g_class_health[i].off; + return -1; +} + +static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth, + int *out_health) +{ if (depth > 8) return; uintptr_t props = *(uintptr_t *)(table + 0x00); @@ -67,13 +84,19 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { if (dump) logf(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2, ptype, poff, poff, ptable ? "yes" : "no", parray ? "yes" : "no"); - if (ptable) { - collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1); + collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1, + out_health); continue; } - if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { + if (out_health && *out_health < 0 && strcmp(pname2, "m_iHealth") == 0) + *out_health = (int)(base_off + (uint32_t)poff); + if (strcmp(pname2, "m_lifeState") == 0 && g_net.lifestate < 0) { + g_net.lifestate = (int)(base_off + (uint32_t)poff); + } else if (strcmp(pname2, "m_nModelIndex") == 0 && g_net.modelindex < 0) { + g_net.modelindex = (int)(base_off + (uint32_t)poff); + } else if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { g_net.flags = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) { if (parray && g_client.contains(parray)) { @@ -95,7 +118,8 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { } else { g_net.origin = (int)(base_off + (uint32_t)poff); } - } else if (strcmp(pname2, "m_vecViewOffset") == 0 && g_net.viewoffset < 0) { + } else if (strncmp(pname2, "m_vecViewOffset", 15) == 0 && + g_net.viewoffset < 0) { if (parray && g_client.contains(parray)) { int32_t eoff = *(int32_t *)(parray + 0x2C); g_net.viewoffset = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); @@ -117,9 +141,7 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { g_net.maxs = (int)(base_off + (uint32_t)poff); } } - if (g_net.esp_ok()) - return; -} + } } // prediction datamap scan for m_MoveType (FIELD_CHARACTER, fieldOffset in range) @@ -195,21 +217,34 @@ void resolve_netvars() { for (uintptr_t cc = head; cc && g_client.contains(cc) && classes < 1024; cc = *(uintptr_t *)(cc + 0x10)) { classes++; - collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0); - if (g_net.esp_ok() || (g_net.flags >= 0 && g_net.velocity >= 0 && - g_net.health < 0)) - break; + char cname[32]; + int hoff = -1; + // per-class health offset — NPCs send m_iHealth via their own table chain, + // not DT_BasePlayer's; g_net.health keeps the player's for aimbot use + if (module_string(*(uintptr_t *)(cc + 0x08), cname, sizeof(cname))) { + collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0, &hoff); + if (hoff >= 0 && g_class_health_n < 256) { + char *dst = g_class_health[g_class_health_n].name; + const char *src = cname; + size_t n = sizeof(g_class_health[g_class_health_n].name) - 1; + for (size_t i = 0; i < n && src[i]; i++) dst[i] = src[i]; + dst[n] = 0; + g_class_health[g_class_health_n].off = hoff; + g_class_health_n++; + } + } } - logf("client classes walked: %d", classes); + logf("client classes walked: %d, health-offset classes: %d", classes, + g_class_health_n); if (g_net.movetype < 0 && !resolve_movetype()) { // movetype unresolved — bhop still works, just no noclip/ladder guard logf("movetype guard disabled"); } - logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " - "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d", + "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d " + "modelindex=%d", g_net.flags, g_net.velocity, g_net.movetype, g_net.health, g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, - g_net.mins, g_net.maxs); + g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex); } -- cgit v1.2.3