From 714262bcf759a238daf906de16e7b204712a104f Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 14:28:21 +0100 Subject: Add usable item filter, projectile ESP and gravity-gun melee - Items: "Only if usable" hides health tags at full health and armor at full suit. Health ceiling is the server player's m_iMaxHealth; armor uses the server m_ArmorValue against HL2's 100 cap. Fails open when unreadable. - Projectiles: new Visuals feature/card for thrown grenades, RPG rockets, helicopter bombs and AR2 balls. Fuse from server m_flDetonateTime vs engine time; radius from m_DmgRadius with per-type fallback; optional 24-segment radius ring and FRAG timer label. - Melee: the killbot now handles the gravity gun as well; only VULNERABLE targets, 1200-unit range, primary attack to launch the held object, and no aim-punch compensation because the launch uses EyeVectors. - Server/netvars: m_ArmorValue, m_flDetonateTime, m_DmgRadius. --- src/esp.cpp | 283 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 273 insertions(+), 10 deletions(-) (limited to 'src/esp.cpp') diff --git a/src/esp.cpp b/src/esp.cpp index 7baf2c8..c956d1b 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -898,7 +898,8 @@ static bool active_weapon_contains(void *local_ent, const char *needle) { void *net = entity_networkable(idx); const char *cls = net ? entity_class_name(net, nullptr) : nullptr; const char *model = wep ? entity_model_name(wep) : nullptr; - return (cls && strstr(cls, needle)) || (model && strstr(model, needle)); + return (cls && strcasestr(cls, needle)) || + (model && strcasestr(model, needle)); } // Crossbow bolts and RPG rockets are not hitscan. Lead moving targets by the @@ -1175,7 +1176,35 @@ bool melee_weapon_active(void *local_ent) { return crowbar; } -bool melee_aimbot_apply(CUserCmd *cmd) { +bool gravity_gun_active(void *local_ent) { + if (!local_ent) + return false; + if (active_weapon_contains(local_ent, "physcannon")) + return true; + // Same server fallback as the crowbar: map transitions can leave the + // client activeweapon handle stale. + int local = engine_get_local_player(); + if (local <= 0 || !g_iface.engineserver) + return false; + resolve_server_netvars(); + int si = server_active_weapon(local); + if (si <= 0) + return false; + void *wep = entitylist_get(si); + void *net = entity_networkable(si); + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + const char *model = wep ? entity_model_name(wep) : nullptr; + if (!model) { + void *sbase = nullptr; + server_entity_health(si, nullptr, nullptr, &sbase); + if (sbase) + model = model_name_from_index(server_model_index(sbase)); + } + return (cls && strcasestr(cls, "physcannon")) || + (model && strcasestr(model, "physcannon")); +} + +bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { if (!g_iface.entitylist || g_net.origin < 0) return false; int local = engine_get_local_player(); @@ -1188,6 +1217,9 @@ bool melee_aimbot_apply(CUserCmd *cmd) { *(int *)((uintptr_t)local_ent + g_net.health) <= 0) return false; + // The punted object can travel much farther than the crowbar. + float max_range = gravity ? 1200.0f : MELEE_RANGE; + using Fn = int (*)(void *); int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); if (hi < 0 || hi > 4096) @@ -1226,6 +1258,9 @@ bool melee_aimbot_apply(CUserCmd *cmd) { continue; if (!npc_can_be_damaged(idx, model)) continue; + // The gravity gun is only the tool for physics-vulnerable units. + if (gravity && npc_damage_class(idx, model) != NPC_DMG_VULNERABLE) + continue; Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), @@ -1265,7 +1300,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) { if (closest.z < lo.z) closest.z = lo.z; if (closest.z > hiV.z) closest.z = hiV.z; float d = vec_dist(g_cam_eye, closest); - if (d > MELEE_RANGE) + if (d > max_range) continue; Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f, @@ -1298,7 +1333,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) { float bestd = 1e9f; for (int i = 0; i < np; i++) { float pd = vec_dist(g_cam_eye, pts[i]); - if (pd > MELEE_RANGE || pd >= bestd) + if (pd > max_range || pd >= bestd) continue; if (!is_visible(g_cam_eye, pts[i], local_ent, tent)) continue; @@ -1311,7 +1346,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) { if (!have_aim) { aim = c.center; float ad = vec_dist(g_cam_eye, aim); - if (ad > MELEE_RANGE) { + if (ad > max_range) { // The body is in range but its center is not: aim just inside the // closest point on the AABB. aim = c.closest; @@ -1319,7 +1354,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) { aim.y += (c.center.y - c.closest.y) * 0.25f; aim.z += (c.center.z - c.closest.z) * 0.25f; ad = vec_dist(g_cam_eye, aim); - if (ad > MELEE_RANGE) + if (ad > max_range) aim = c.closest; } if (!is_visible(g_cam_eye, aim, local_ent, tent)) @@ -1328,8 +1363,10 @@ bool melee_aimbot_apply(CUserCmd *cmd) { QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; QAngle desired = angle_to(g_cam_eye, aim); + // The crowbar uses GetAutoaimVector (adds punch); the gravity gun's + // launch uses EyeVectors (no punch), so only compensate for melee. float punch_x = 0.0f, punch_y = 0.0f; - if (punch_angles(punch_x, punch_y)) { + if (!gravity && punch_angles(punch_x, punch_y)) { desired.x -= punch_x; desired.y -= punch_y; } @@ -1350,13 +1387,15 @@ bool melee_aimbot_apply(CUserCmd *cmd) { using SetViewFn = void (*)(void *, QAngle *); vfunc(g_iface.engine, 20)(g_iface.engine, &restore); } + // Gravity gun primary attack launches the held object (EyeVectors); + // secondary would drop it instead. cmd->buttons |= IN_ATTACK; static int logs = 0; if (logs < 10) { logs++; - logd("melee: swing idx=%d dist=%.1f aim=(%.1f %.1f %.1f)", c.idx, - c.dist, aim.x, aim.y, aim.z); + logd("melee: %s idx=%d dist=%.1f aim=(%.1f %.1f %.1f)", + gravity ? "punt" : "swing", c.idx, c.dist, aim.x, aim.y, aim.z); } return true; } @@ -1365,7 +1404,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) { double now = esp_now(); if (now - last_none >= 2.0) { last_none = now; - logd("melee: no visible target within %.0f", MELEE_RANGE); + logd("melee: no visible target within %.0f", max_range); } return false; } @@ -2331,6 +2370,30 @@ static void items_collect(int local_idx) { } } +// Authoritative local health/armor for the "only if usable" item filter. +// The health ceiling comes from the server player's m_iMaxHealth, not a +// hardcoded 100; when it cannot be read the filter fails open. +static void local_vitals(int &hp, int &maxhp, int &armor) { + hp = maxhp = armor = -1; + int local = engine_get_local_player(); + if (local <= 0) + return; + void *le = entitylist_get(local); + if (le && g_net.health >= 0) + hp = *(int *)((uintptr_t)le + g_net.health); + if (g_iface.engineserver) { + int smax = -1; + int shp = server_entity_health(local, &smax, nullptr, nullptr); + if (shp >= 0) + hp = shp; + if (smax > 0) + maxhp = smax; + armor = server_player_armor(local); + } + if (maxhp < 0 && le && g_net.maxhealth >= 0) + maxhp = *(int *)((uintptr_t)le + g_net.maxhealth); +} + void esp_draw_items() { if (!g_surface || !g_iface.entitylist || g_net.origin < 0) return; @@ -2348,10 +2411,21 @@ void esp_draw_items() { } items_collect(local); + int vit_hp = -1, vit_max = -1, vit_armor = -1; + if (g_item_usable) + local_vitals(vit_hp, vit_max, vit_armor); + for (int i = 0; i < g_nitems; i++) { const ItemTarget &t = g_item_targets[i]; if (!item_kind_enabled(t.kind)) continue; + if (g_item_usable) { + if (t.kind == IK_HEALTH && vit_hp >= 0 && vit_max > 0 && + vit_hp >= vit_max) + continue; + if (t.kind == IK_ARMOR && vit_armor >= 0 && vit_armor >= 100) + continue; + } float sx, sy; if (!world_to_screen(t.origin, sx, sy)) continue; @@ -2402,3 +2476,192 @@ void esp_draw_items() { draw_text_col(tx, ty, text, r, g, b); } } + +// --------------------------------------------------------------------------- +// projectile ESP: grenades, rockets, helicopter bombs, AR2 balls +// --------------------------------------------------------------------------- + +enum ProjKind { PK_FRAG, PK_RPG, PK_BOMB, PK_BALL }; + +struct ProjTarget { + int idx; + int kind; + Vector origin; + float fuse; // seconds; < 0 = impact / no fuse + float radius; +}; + +static ProjTarget g_projs[32]; +static int g_nprojs = 0; + +static float engine_curtime() { + if (!g_iface.engine) + return 0.0f; + using TimeFn = float (*)(void *); + return vfunc(g_iface.engine, 14)(g_iface.engine); +} + +static const char *proj_name(int kind) { + switch (kind) { + case PK_FRAG: return "FRAG"; + case PK_RPG: return "RPG"; + case PK_BOMB: return "BOMB"; + case PK_BALL: return "AR2"; + default: return "PROJ"; + } +} + +static void projs_collect(int local_idx) { + static double last_scan = 0.0; + double now = esp_now(); + if (now - last_scan < 0.10) + return; + last_scan = now; + g_nprojs = 0; + if (!g_iface.entitylist || g_net.origin < 0) + return; + resolve_server_netvars(); + using Fn = int (*)(void *); + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); + if (hi < 0 || hi > 4096) + hi = 4096; + float curtime = engine_curtime(); + for (int idx = 1; idx <= hi && g_nprojs < 32; 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); + const char *model = entity_model_name(ent); + if (!model) + continue; + bool owned = item_weapon_is_owned(ent); + bool frag_cls = cls && strcasestr(cls, "grenade") && + !strcasestr(cls, "weapon"); + bool missile_cls = cls && strcasestr(cls, "missile") && + !strcasestr(cls, "weapon"); + int kind; + if (strcasestr(model, "w_grenade") && + (frag_cls || (!cls && owned))) { + kind = PK_FRAG; + } else if ((strcasestr(model, "w_missile") || + strcasestr(model, "missile_launch") || + strcasestr(model, "missile_closed")) && + (missile_cls || (!cls && owned))) { + kind = PK_RPG; + } else if (strcasestr(model, "helicopter_bomb")) { + kind = PK_BOMB; + } else if (strcasestr(model, "combineball")) { + kind = PK_BALL; + } else { + continue; + } + uintptr_t e = (uintptr_t)ent; + ProjTarget &p = g_projs[g_nprojs++]; + p.idx = idx; + p.kind = kind; + p.origin = Vector{*(float *)(e + g_net.origin), + *(float *)(e + g_net.origin + 4), + *(float *)(e + g_net.origin + 8)}; + // The client m_DmgRadius is networked; fall back to the server field and + // then to the stock radius for this projectile type. + p.radius = -1.0f; + if (g_net.dmg_radius >= 0) + p.radius = *(float *)(e + g_net.dmg_radius); + if (!(p.radius > 1.0f && p.radius < 2048.0f)) + p.radius = server_grenade_radius(idx); + if (!(p.radius > 1.0f && p.radius < 2048.0f)) + p.radius = kind == PK_RPG ? 200.0f : kind == PK_BALL ? 150.0f : 300.0f; + // m_flDetonateTime is server-only (not in the send table). + p.fuse = -1.0f; + if (kind != PK_RPG) { + float det = server_grenade_detonate(idx); + if (det > 0.0f && curtime > 0.0f) + p.fuse = det - curtime; + if (!(p.fuse > -2.0f && p.fuse < 30.0f)) + p.fuse = -1.0f; + } + static int logs = 0; + if (logs < 8) { + logs++; + logf("projectile: idx=%d cls=%s model=%s kind=%s fuse=%.2f " + "radius=%.0f", + idx, cls ? cls : "?", model, proj_name(kind), p.fuse, p.radius); + } + } +} + +void esp_draw_projectiles() { + if (!g_surface || !g_iface.entitylist || g_net.origin < 0) + return; + if (!g_scr_w) { + using Fn = void (*)(void *, int &, int &); + vfunc(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h); + if (!g_scr_w) + return; + } + projs_collect(engine_get_local_player()); + for (int i = 0; i < g_nprojs; i++) { + const ProjTarget &p = g_projs[i]; + float sx, sy; + if (!world_to_screen(p.origin, sx, sy)) + continue; + if (sx < -40 || sx > (float)g_scr_w + 40 || sy < -40 || + sy > (float)g_scr_h + 40) + continue; + int r = 255, g = 170, b = 60; + if (p.kind == PK_RPG) { + r = 255; g = 120; b = 40; + } else if (p.kind == PK_BOMB) { + r = 255; g = 60; b = 60; + } else if (p.kind == PK_BALL) { + r = 120; g = 220; b = 255; + } else if (p.fuse >= 0.0f && p.fuse < 0.8f) { + r = 255; g = 60; b = 60; + } + int x = (int)sx, y = (int)sy; + surf_color(r, g, b, 255); + surf_rect(0, x - 4, y - 4, x + 4, y + 4); + surf_rect(1, x - 1, y - 1, x + 2, y + 2); + char text[64]; + if (g_proj_fuse && p.fuse >= 0.0f) + snprintf(text, sizeof(text), "%s %.1fs", proj_name(p.kind), p.fuse); + else + snprintf(text, sizeof(text), "%s", proj_name(p.kind)); + int tx = x + 9; + if (tx > g_scr_w - 80) + tx = x - 9 - (int)strlen(text) * 6; + if (tx < 4) + tx = 4; + int ty = y - 14; + if (ty < 4) + ty = 4; + draw_text_small_col(tx + 1, ty + 1, text, 0, 0, 0); + draw_text_small_col(tx, ty, text, r, g, b); + if (g_proj_radius && p.radius > 1.0f) { + const int SEG = 24; + float lx = 0.0f, ly = 0.0f; + bool have = false; + surf_color(r, g, b, 150); + for (int k = 0; k <= SEG; k++) { + float a = (float)k * 2.0f * PI_F / (float)SEG; + Vector w{p.origin.x + cosf(a) * p.radius, + p.origin.y + sinf(a) * p.radius, p.origin.z}; + float rx, ry; + if (!world_to_screen(w, rx, ry)) { + have = false; + continue; + } + if (have) + surf_line((int)lx, (int)ly, (int)rx, (int)ry); + lx = rx; + ly = ry; + have = true; + } + } + } +} -- cgit v1.2.3