// esp.cpp — ESP drawing + shared world_to_screen / entity gather (aimbot reuses these) #include "hl2h.h" #include #include #include #include static constexpr float PI_F = 3.14159265358979323846f; // ISurface slots (binary = SDK header + 4, disassembly-confirmed) enum SurfSlot { SURF_DrawSetColor = 10, SURF_FilledRect = 12, SURF_OutlinedRect = 14, SURF_GetScreenSize = 38, }; enum { LIFE_ALIVE = 0 }; struct EspTarget { float sx, sy; // screen pos of feet float h; // box height px float health, maxhealth; bool show_health; bool friendly; bool neutral; char name[48]; }; static EspTarget g_targets[128]; 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); float syy = sinf(yaw), cy = cosf(yaw); float fwd[3] = {cp * cy, cp * syy, -sp}; float right[3] = {syy, -cy, 0}; float up[3] = {sp * cy, sp * syy, cp}; float dx = w.x - g_cam_eye.x, dy = w.y - g_cam_eye.y, dz = w.z - g_cam_eye.z; float z = dx * fwd[0] + dy * fwd[1] + dz * fwd[2]; if (z < 1.0f) return false; float x = dx * right[0] + dy * right[1] + dz * right[2]; float y = dx * up[0] + dy * up[1] + dz * up[2]; float tanfov = tanf(75.0f * 0.5f * PI_F / 180.0f); sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov); sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov); 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 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; 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)}; 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(bottom, sx, sy) || !world_to_screen(top, tx, ty)) continue; float h = sy - ty; 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 = 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); } } static void surf_rect(int filled, int x0, int y0, int x1, int y1) { using Fn = void (*)(void *, int, int, int, int); vfunc(g_surface, filled ? SURF_FilledRect : SURF_OutlinedRect)(g_surface, x0, y0, x1, y1); } static void surf_color(int r, int g, int b, int a) { using Fn = void (*)(void *, int, int, int, int); vfunc(g_surface, SURF_DrawSetColor)(g_surface, r, g, b, a); } void esp_draw() { if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return; if (!g_scr_w) { using Fn = void (*)(void *, int &, int &); vfunc(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h); logf("ESP: screen %dx%d", g_scr_w, g_scr_h); if (!g_scr_w) return; } esp_collect(engine_get_local_player()); for (int i = 0; i < g_ntargets; i++) { const EspTarget &t = g_targets[i]; if (t.neutral && !esp_option(ESP_OPT_SHOW_NEUTRAL)) continue; 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 int left = (int)(t.sx - w / 2); int right = (int)(t.sx + w / 2); int top = (int)(t.sy - t.h); int bot = (int)t.sy; surf_rect(0, left, top, right, bot); bool draw_bar = t.show_health && esp_option(ESP_OPT_SHOW_HEALTHBAR); bool draw_num = t.show_health && esp_option(ESP_OPT_SHOW_HEALTHNUM); int num_x = right + 3; if (draw_bar) { float frac = t.health / (t.maxhealth > 1.0f ? t.maxhealth : 100.0f); if (frac > 1) frac = 1; if (frac < 0) frac = 0; // vertical bar just right of the box, HP number beside it int bx0 = right + 2, bx1 = right + 5; surf_color(0, 0, 0, 200); surf_rect(1, bx0, top, bx1, bot); int fill_top = bot - (int)((float)(bot - top) * frac); if (fill_top < top) fill_top = top; 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); if (fill_top < bot) surf_rect(1, bx0, fill_top, bx1, bot); num_x = bx1 + 3; } if (draw_num) { char hptext[16]; snprintf(hptext, sizeof(hptext), "%d", (int)t.health); draw_text_col(num_x, top, hptext, 255, 255, 255); } if (esp_option(ESP_OPT_SHOW_NAME)) draw_text_col(left, top - 14, t.name, 255, 255, 255); } }