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/esp.cpp | 262 ++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 222 insertions(+), 40 deletions(-) (limited to 'src/esp.cpp') diff --git a/src/esp.cpp b/src/esp.cpp index 87d0b80..d12c718 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -3,6 +3,7 @@ #include #include #include +#include static constexpr float PI_F = 3.14159265358979323846f; @@ -12,11 +13,15 @@ enum SurfSlot { SURF_GetScreenSize = 38, }; +enum { LIFE_ALIVE = 0 }; + struct EspTarget { float sx, sy; // screen pos of feet float h; // box height px float health, maxhealth; - int teamnum; + bool show_health; + bool friendly; + bool neutral; char name[48]; }; static EspTarget g_targets[128]; @@ -24,7 +29,27 @@ static int g_ntargets = 0; static int g_scr_w = 0, g_scr_h = 0; static bool g_esp_logged = false; +static double esp_now() { + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + bool world_to_screen(const Vector &w, float &sx, float &sy) { + // Preferred: the engine's own transform. Uses the real view setup (FOV, + // aspect, roll), so boxes do not drift at the screen edges. + if (g_iface.debugoverlay) { + using Fn = int (*)(void *, const Vector *, Vector *); + Vector s; + int behind = + vfunc(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s); + if (behind) + return false; + sx = s.x; + sy = s.y; + return true; + } + // Fallback: local math with the default HL2 FOV. float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; float sp = sinf(pitch), cp = cosf(pitch); @@ -43,39 +68,195 @@ bool world_to_screen(const Vector &w, float &sx, float &sy) { return true; } +// --------------------------------------------------------------------------- +// entity helpers (IClientEntityList / IClientNetworkable, header-documented) +// --------------------------------------------------------------------------- + +using GetNetFn = void *(*)(void *, int); +using GetCCFn = void *(*)(void *); +using GetModelFn = const void *(*)(void *, int); +using GetModelNameFn = const char *(*)(void *, const void *); + +// entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the +// IClientNetworkable subobject offset inside the entity (see projectz). +static void *entity_networkable(int idx) { + return vfunc(g_iface.entitylist, 0)(g_iface.entitylist, idx); +} + +// IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8. +static const char *entity_class_name(void *net, void **out_cc) { + if (!net) return nullptr; + void *cc = vfunc(net, 2)(net); + if (out_cc) *out_cc = cc; + return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; +} + +// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. +// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. +static const char *entity_model_name(void *ent) { + if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr; + int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); + if (modelindex <= 0) return nullptr; + const void *model = + vfunc(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); + if (!model) + return nullptr; + return vfunc(g_iface.modelinfo, 4)(g_iface.modelinfo, model); +} + +// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all +// use the single client class "CAI_BaseNPC" — only special NPCs register their +// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). +static bool is_npc_class(const char *name) { + if (!name) return false; + return strstr(name, "NPC") != nullptr || + strstr(name, "BaseHelicopter") != nullptr; +} + +static bool model_is_friendly(const char *m) { + static const char *patterns[] = { + "citizen", "humans/", "barney", "alyx", "alex", "scientist", + "kleiner", "eli", "mossman", "vortigaunt", "dog", "grigori", + "monk", "gman", nullptr}; + if (!m) return false; + for (int i = 0; patterns[i]; i++) + if (strstr(m, patterns[i])) + return true; + return false; +} + +// harmless wildlife that also happens to be CAI_BaseNPC +static bool model_is_neutral(const char *m) { + static const char *patterns[] = {"crow", "gull", "pigeon", "seagull", + "bird", nullptr}; + if (!m) return false; + for (int i = 0; patterns[i]; i++) + if (strstr(m, patterns[i])) + return true; + return false; +} + +static void short_name(const char *model, char *out, size_t outn) { + if (!model) { snprintf(out, outn, "NPC"); return; } + const char *base = strrchr(model, '/'); + base = base ? base + 1 : model; + size_t n = strcspn(base, "."); + if (n >= outn) n = outn - 1; + memcpy(out, base, n); + out[n] = 0; +} + static void esp_collect(int local_idx) { + // outer_draw() runs once per PaintTraverseEx call (several per frame); cache + // the scan briefly so we do not walk the entity list dozens of times a frame. + static double last_scan = 0.0; + static double last_dbg = 0.0; + double now = esp_now(); + if (now - last_scan < 0.01) + return; + last_scan = now; + bool dbg = (now - last_dbg) >= 2.0; + if (dbg) last_dbg = now; + g_ntargets = 0; using Fn = int (*)(void *); - int maxent = vfunc(g_iface.entitylist, 5)(g_iface.entitylist); // GetHighestEntityIndex - if (maxent < 0 || maxent > 512) maxent = 512; - for (int idx = 0; idx <= maxent && g_ntargets < 128; idx++) { + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); // GetHighestEntityIndex + if (hi < 0 || hi > 4096) hi = 4096; + int npc_alive = 0, dbg_lines = 0; + bool srv = resolve_server_netvars(); + static bool srv_checked = false; + static bool srv_trusted = false; + + for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) { if (idx == local_idx) continue; void *ent = entitylist_get(idx); if (!ent) continue; + void *net = entity_networkable(idx); + if (!net) continue; + const char *cls = entity_class_name(net, nullptr); + if (!cls) continue; + uintptr_t e = (uintptr_t)ent; - float hpv = (float)*(int *)(e + g_net.health); // m_iHealth is int - if (!(hpv > 0)) continue; + int cli_life = g_net.lifestate >= 0 + ? (int)*(unsigned char *)(e + g_net.lifestate) + : -1; + int cli_hp = g_net.health >= 0 ? *(int *)(e + g_net.health) : 0; + bool npc = is_npc_class(cls); + const char *model = entity_model_name(ent); + + // NPC health is not networked; read it from the listen-server entity. + int hp = cli_hp, maxhp = -1, life = cli_life; + if (npc && srv) { + int slife = -1, smax = -1; + void *sbase = nullptr; + int shp = server_entity_health(idx, &smax, &slife, &sbase); + if (!srv_checked && sbase) { + srv_checked = true; + int cli_mi = + g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1; + int srv_mi = server_model_index(sbase); + srv_trusted = (cli_mi == srv_mi); + logf("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d " + "hp=%d maxhp=%d life=%d trusted=%d", + idx, sbase, cli_mi, srv_mi, shp, smax, slife, (int)srv_trusted); + if (!srv_trusted) + logf("ESP: server entity mismatch, ignoring server values"); + } + if (srv_trusted) { + if (shp >= 0) hp = shp; + if (smax > 0) maxhp = smax; + if (slife >= 0) life = slife; + } + } + + if (dbg && npc && dbg_lines < 40) { + dbg_lines++; + logf("ESP npc %d cls=%s life=%d(cli %d) hp=%d(cli %d) maxhp=%d model=%s", + idx, cls, life, cli_life, hp, cli_hp, maxhp, + model ? model : "?"); + } + + if (npc) { + if (life != LIFE_ALIVE) continue; + npc_alive++; + } else { + // players (HL2DM) are the only entities with a live networked m_iHealth + if (!strstr(cls, "Player") || !(hp > 0)) continue; + } + + float minz = g_net.mins >= 0 ? *(float *)(e + g_net.mins + 8) : 0.0f; + float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; + if (!(maxz > minz + 4.0f && maxz - minz < 512.0f)) { + minz = 0.0f; + maxz = 72.0f; + } Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), *(float *)(e + g_net.origin + 8)}; - // ponytail: maxs netvar may be unresolved; fixed 72-unit torso fallback - float maxsz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs) : 72.0f; - Vector top{origin.x, origin.y, origin.z + maxsz}; + Vector bottom{origin.x, origin.y, origin.z + minz}; + Vector top{origin.x, origin.y, origin.z + maxz}; float sx, sy, tx, ty; - if (!world_to_screen(origin, sx, sy) || !world_to_screen(top, tx, ty)) + if (!world_to_screen(bottom, sx, sy) || !world_to_screen(top, tx, ty)) continue; float h = sy - ty; - if (h < 4.0f || sy < 0 || ty > (float)g_scr_h) continue; // behind / offscreen + if (h < 3.0f || sy < 0 || ty > (float)g_scr_h) continue; // behind / offscreen + if (sx < -(float)g_scr_w || sx > 2.0f * (float)g_scr_w) continue; + EspTarget &t = g_targets[g_ntargets++]; t.sx = sx; t.sy = sy; t.h = h; - t.health = hpv; - t.maxhealth = (float)*(int *)(e + g_net.maxhealth); - if (t.maxhealth <= 0) t.maxhealth = hpv; - // classname via IClientNetworkable::GetClientClass (slot 2) -> m_pNetworkName @ +8 - using CCFn = void *(*)(void *); - void *cc = vfunc(ent, 2)(ent); - const char *nn = cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; - strncpy(t.name, nn ? nn : "?", sizeof(t.name) - 1); - t.name[sizeof(t.name) - 1] = 0; + t.health = hp > 0 ? (float)hp : 0.0f; + t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; + t.show_health = hp > 0; + t.friendly = model_is_friendly(model); + t.neutral = model_is_neutral(model); + short_name(model, t.name, sizeof(t.name)); + if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); + } + + if (dbg) + logf("ESP scan: hi=%d npc_alive=%d targets=%d", hi, npc_alive, g_ntargets); + if (g_ntargets && !g_esp_logged) { + g_esp_logged = true; + logf("ESP: first targets, %d", g_ntargets); } } @@ -97,28 +278,29 @@ void esp_draw() { if (!g_scr_w) return; } esp_collect(engine_get_local_player()); - if (g_ntargets && !g_esp_logged) { - g_esp_logged = true; - logf("ESP: first draw, %d targets", g_ntargets); - } for (int i = 0; i < g_ntargets; i++) { const EspTarget &t = g_targets[i]; - // team color: 3 combine red, 2 rebels green, else white - if (t.teamnum == 3) surf_color(255, 60, 60, 255); - else if (t.teamnum == 2) surf_color(60, 255, 60, 255); - else surf_color(255, 255, 255, 255); - float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width — swap for m_vecMins/maxs span later + if (t.friendly) surf_color(60, 255, 60, 255); + else if (t.neutral) surf_color(220, 220, 220, 255); + else surf_color(255, 60, 60, 255); + float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width surf_rect(0, (int)(t.sx - w / 2), (int)(t.sy - t.h), (int)(t.sx + w / 2), (int)t.sy); - // health bar under box - float frac = t.health / t.maxhealth; - if (frac > 1) frac = 1; - surf_color(0, 0, 0, 200); - surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 1, (int)(t.sx + w / 2), (int)t.sy + 4); - if (frac > 0.5f) surf_color(40, 255, 40, 255); - else if (frac > 0.25f) surf_color(255, 255, 0, 255); - else surf_color(255, 40, 40, 255); - surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 2, - (int)(t.sx - w / 2 + w * frac), (int)t.sy + 3); - draw_text_col((int)(t.sx - w / 2), (int)(t.sy - t.h) - 14, t.name, 255, 255, 255); + if (t.show_health) { + float frac = t.health / (t.maxhealth > 1.0f ? t.maxhealth : 100.0f); + if (frac > 1) frac = 1; + surf_color(0, 0, 0, 200); + surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 1, (int)(t.sx + w / 2), (int)t.sy + 4); + if (frac > 0.5f) surf_color(40, 255, 40, 255); + else if (frac > 0.25f) surf_color(255, 255, 0, 255); + else surf_color(255, 40, 40, 255); + surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 2, + (int)(t.sx - w / 2 + w * frac), (int)t.sy + 3); + } + char label[64]; + if (t.show_health) + snprintf(label, sizeof(label), "%s %d", t.name, (int)t.health); + else + snprintf(label, sizeof(label), "%s", t.name); + draw_text_col((int)(t.sx - w / 2), (int)(t.sy - t.h) - 14, label, 255, 255, 255); } } -- cgit v1.2.3