From 7352766582e140483ebcc1d8660a76e1bccbec0d Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 6 Nov 2026 16:23:01 +0100 Subject: ESP: extract projectile ESP into its own translation unit First slice of the esp.cpp split. Shared helpers move to esp_internal.h (entity lookups, screen size, surf primitives, segment projection); esp.cpp keeps the definitions and drops their static qualifiers. --- src/esp.cpp | 392 +++------------------------------------------------- src/esp_internal.h | 34 +++++ src/projectiles.cpp | 364 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 416 insertions(+), 374 deletions(-) create mode 100644 src/esp_internal.h create mode 100644 src/projectiles.cpp diff --git a/src/esp.cpp b/src/esp.cpp index 81ec81c..fef3e7c 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -1,4 +1,5 @@ // esp.cpp — ESP drawing + shared world_to_screen / entity gather (aimbot reuses these) +#include "esp_internal.h" #include "hl2h.h" #include #include @@ -31,7 +32,7 @@ static ArrowTarget g_arrows[32]; static int g_narrows = 0; static EspTarget g_targets[128]; static int g_ntargets = 0; -static int g_scr_w = 0, g_scr_h = 0; +int g_scr_w = 0, g_scr_h = 0; static bool g_esp_logged = false; // Listen-server sanity latch. Only ever set once per map (esp_reset_session), @@ -70,7 +71,7 @@ static float g_speed_samples[SPEED_SAMPLES] = {}; static int g_speed_head = 0; static double g_speed_last_sample = 0.0; -static double esp_now() { +double esp_now() { timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; @@ -79,7 +80,7 @@ static double esp_now() { // Refresh the cached screen size every frame. It used to be read once, so a // resolution or window change left every projection, screen-edge clamp and // centering calculation stale for the rest of the session. -static bool esp_update_screen() { +bool esp_update_screen() { if (!g_surface) return false; int w = 0, h = 0; @@ -172,12 +173,12 @@ 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) { +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) { +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; @@ -220,7 +221,7 @@ static void dormant_selftest() { logd("dormant check enabled"); } -static bool entity_is_dormant(void *net) { +bool entity_is_dormant(void *net) { if (!net) return false; if (g_dormant_state == 0) @@ -276,7 +277,7 @@ static const char *model_name_from_index(int modelindex) { return name; } -static const char *entity_model_name(void *ent) { +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); return model_name_from_index(modelindex); @@ -285,7 +286,7 @@ static const char *entity_model_name(void *ent) { // Live origin for a cached entity index. Item/projectile discovery is throttled // to 10 Hz (it walks the whole entity list), but the markers must follow the // world at render rate; refreshing one netvar read per cached target is cheap. -static bool entity_origin(void *ent, Vector &out) { +bool entity_origin(void *ent, Vector &out) { if (!ent || g_net.origin < 0) return false; uintptr_t e = (uintptr_t)ent; @@ -525,7 +526,7 @@ static int npc_damage_class(int idx, const char *model) { return v; } -static bool active_weapon_contains(void *local_ent, const char *needle); +bool active_weapon_contains(void *local_ent, const char *needle); // Armoured units need blast damage; the RPG is the only stock weapon that // passes their OnTakeDamage filters. Immune units can never be damaged. @@ -774,22 +775,22 @@ static void esp_collect(int local_idx) { } } -static void surf_rect(int filled, int x0, int y0, int x1, int y1) { +void surf_rect(int filled, int x0, int y0, int x1, int y1) { if (filled) overlay_fill(x0, y0, x1, y1); else overlay_outline_rect(x0, y0, x1, y1); } -static void surf_color(int r, int g, int b, int a) { +void surf_color(int r, int g, int b, int a) { overlay_color(r, g, b, a); } -static void surf_line(int x0, int y0, int x1, int y1) { +void surf_line(int x0, int y0, int x1, int y1) { overlay_line(x0, y0, x1, y1); } // Screen projection of the aimbot FOV cone boundary, using the engine's own // world->screen transform so it follows the actual aspect/fov. -static QAngle engine_view_angles() { +QAngle engine_view_angles() { QAngle v{g_cam_view.x, g_cam_view.y, g_cam_view.z}; if (g_iface.engine) { using Fn = void (*)(void *, QAngle *); @@ -1222,7 +1223,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, return false; } -static bool active_weapon_contains(void *local_ent, const char *needle) { +bool active_weapon_contains(void *local_ent, const char *needle) { if (!local_ent || g_net.activeweapon < 0 || !needle) return false; uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); @@ -2277,8 +2278,8 @@ static bool clip_line2d(float &x0, float &y0, float &x1, float &y1, // Project a world-space segment, clipping it at the near plane first. Without // this an edge with one corner behind the camera was dropped entirely, so a // partially visible transition box or radius ring disappeared. -static bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0, - int &x1, int &y1) { +bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0, + int &x1, int &y1) { if (!g_scr_w || !g_scr_h) return false; float pitch = g_cam_view.x * PI_F / 180.0f; @@ -2543,7 +2544,7 @@ static bool item_handle_valid(uint32_t h) { } // Weapons held by the player or an NPC are not world pickups. -static bool item_weapon_is_owned(void *ent) { +bool item_weapon_is_owned(void *ent) { if (!ent) return false; if (g_net.owner >= 0) { @@ -3320,360 +3321,3 @@ void esp_draw_items() { // 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; - bool owned; -}; - -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"; - } -} - -// Fallback fuse clock when the server detonate time cannot be read. The stock -// frag fuse is 3 s from the first sighting. -static double proj_first_seen(int idx, double now) { - static constexpr int N = 64; - static int idxs[N]; - static double first[N]; - static int next = 0; - for (int i = 0; i < N; i++) { - if (idxs[i] != idx) - continue; - // The slot is stale if this index was last seen long ago: a new grenade - // reusing the entity number must start its own fuse, not inherit an old - // one (which produced permanently negative fuses). - if (now - first[i] > 10.0) - first[i] = now; - return first[i]; - } - idxs[next] = idx; - first[next] = now; - next = (next + 1) % N; // oldest entry is reused instead of dropped - return now; -} - -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 || entity_is_dormant(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); - if (!owned && local_idx > 0 && - server_grenade_thrower(idx) == local_idx) - owned = true; - bool frag_cls = cls && strcasestr(cls, "grenade") && - !strcasestr(cls, "weapon"); - bool item_cls = cls && (strcasestr(cls, "item") || - strcasestr(cls, "weapon")); - bool missile_model = strcasestr(model, "w_missile") && - !strcasestr(model, "missile_closed"); - int kind; - if (strcasestr(model, "w_grenade") && - (frag_cls || (!cls && owned))) { - kind = PK_FRAG; - } else if ((missile_model || strcasestr(model, "missile_launch")) && - !item_cls) { - // Launched RPG rocket; ammo pickups/weapons use the same model family. - 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.owned = owned; - 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; - float det = -1.0f; - if (kind != PK_RPG) { - 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 = 3.0f - (float)(now - proj_first_seen(idx, now)); - if (!(p.fuse > -2.0f && p.fuse < 30.0f)) - p.fuse = -1.0f; - } - } - } -} - -// Frag-grenade path preview: throw velocity from weapon_frag.cpp (eye/right -// offset, forward*1200 + 0.1 z), gravity 600, and a simple bounce model. -// Shows the bounces and the first landing with the blast-radius ring. -static void draw_grenade_trajectory(int local_idx) { - if (!g_proj_traj || !g_iface.entitylist) - return; - void *le = entitylist_get(local_idx); - if (!le) - return; - - Vector src{}, vel{}; - bool have = false; - bool actual = false; - // Real blast radius of the grenade being previewed, not a hardcoded 300. - float blast_radius = 300.0f; - // Prefer the local player's thrown frag: the real position and velocity are - // networked, so the preview starts from what is actually flying instead of - // the pre-throw estimate. The bounce contact points still depend on the - // grenade's spin/orientation, so this remains an approximation. - for (int i = 0; i < g_nprojs; i++) { - if (g_projs[i].kind != PK_FRAG || !g_projs[i].owned) - continue; - void *gent = entitylist_get(g_projs[i].idx); - if (!gent) - continue; - src = g_projs[i].origin; - // Live position: the cached origin is up to 100 ms old, and the velocity - // read below is live, so mixing them desynced the arc from the grenade. - Vector live; - if (entity_origin(gent, live)) - src = live; - if (g_projs[i].radius > 1.0f) - blast_radius = g_projs[i].radius; - if (g_net.velocity >= 0) { - const float *pv = (const float *)((uintptr_t)gent + g_net.velocity); - vel = Vector{pv[0], pv[1], pv[2]}; - } - have = true; - actual = true; - break; - } - if (!have) { - if (!active_weapon_contains(le, "frag") && - !active_weapon_contains(le, "w_grenade")) - return; - QAngle va = engine_view_angles(); - float pitch = va.x * PI_F / 180.0f; - float yaw = va.y * PI_F / 180.0f; - float cp = cosf(pitch), sp = sinf(pitch); - float cy = cosf(yaw), sy = sinf(yaw); - Vector fwd{cp * cy, cp * sy, -sp}; - Vector right{sy, -cy, 0.0f}; - - src = Vector{g_cam_eye.x + fwd.x * 18.0f + right.x * 8.0f, - g_cam_eye.y + fwd.y * 18.0f + right.y * 8.0f, - g_cam_eye.z + fwd.z * 18.0f}; - if (g_net.velocity >= 0) { - const float *pv = (const float *)((uintptr_t)le + g_net.velocity); - vel = Vector{pv[0], pv[1], pv[2]}; - } - vel.x += fwd.x * 1200.0f; - vel.y += fwd.y * 1200.0f; - vel.z += fwd.z * 1200.0f + 120.0f; // vForward[2] += 0.1 - have = true; - } - if (actual && vel.x * vel.x + vel.y * vel.y + vel.z * vel.z < 100.0f) - return; // resting grenade: no useful continuation - - const float gravity = 600.0f; - const float dt = 0.02f; - const float restitution = 0.35f; - const float friction = 0.35f; - - Vector pts[512]; - int n = 0; - Vector pos = src; - pts[n++] = pos; - bool landed = false; - for (int step = 0; step < 1200 && n < 511; step++) { - Vector nv = vel; - nv.z -= gravity * dt; - Vector next{pos.x + nv.x * dt, pos.y + nv.y * dt, pos.z + nv.z * dt}; - Vector hit_end{}, normal{}; - if (trace_solid_segment(pos, next, le, hit_end, normal)) { - pts[n++] = hit_end; - float vn = vel.x * normal.x + vel.y * normal.y + vel.z * normal.z; - Vector vt{vel.x - vn * normal.x, vel.y - vn * normal.y, - vel.z - vn * normal.z}; - float keep = 1.0f - friction; - vel.x = vt.x * keep - normal.x * vn * restitution; - vel.y = vt.y * keep - normal.y * vn * restitution; - vel.z = vt.z * keep - normal.z * vn * restitution; - float vlen2 = vel.x * vel.x + vel.y * vel.y + vel.z * vel.z; - pos.x = hit_end.x + normal.x * 1.0f; - pos.y = hit_end.y + normal.y * 1.0f; - pos.z = hit_end.z + normal.z * 1.0f; - if (normal.z > 0.6f && (vn > -80.0f || vlen2 < 2500.0f)) { - landed = true; - break; - } - } else { - pos = next; - vel = nv; - // One point every other step covers the whole flight without the old - // 127-point / 2.5-second cut-off. - if ((step & 1) == 0 && n < 511) - pts[n++] = pos; - } - } - - surf_color(255, 210, 90, 200); - for (int i = 0; i + 1 < n; i++) { - int x0, y0, x1, y1; - if (project_segment(pts[i], pts[i + 1], x0, y0, x1, y1)) - surf_line(x0, y0, x1, y1); - } - - Vector land = landed ? pos : pts[n - 1]; - float ssx, ssy; - if (world_to_screen(land, ssx, ssy)) { - int x = (int)ssx, y = (int)ssy; - surf_color(255, 210, 90, 230); - surf_rect(0, x - 3, y - 3, x + 3, y + 3); - surf_line(x - 9, y, x - 4, y); - surf_line(x + 4, y, x + 9, y); - surf_line(x, y - 9, x, y - 4); - surf_line(x, y + 4, x, y + 9); - } - // The landing ring belongs to the "Blast radius" toggle, and uses the - // networked m_DmgRadius when available instead of a fixed 300. - if (landed && g_proj_radius) { - const int SEG = 36; - const float radius = blast_radius; - surf_color(255, 120, 40, 120); - for (int k = 0; k < SEG; k++) { - float a0 = (float)k * 2.0f * PI_F / (float)SEG; - float a1 = (float)(k + 1) * 2.0f * PI_F / (float)SEG; - Vector p0{land.x + cosf(a0) * radius, land.y + sinf(a0) * radius, - land.z + 1.0f}; - Vector p1{land.x + cosf(a1) * radius, land.y + sinf(a1) * radius, - land.z + 1.0f}; - int x0, y0, x1, y1; - if (project_segment(p0, p1, x0, y0, x1, y1)) - surf_line(x0, y0, x1, y1); - } - } -} - -void esp_draw_projectiles() { - if (!g_surface || !g_iface.entitylist || g_net.origin < 0) - return; - if (!esp_update_screen()) - return; - projs_collect(engine_get_local_player()); - draw_grenade_trajectory(engine_get_local_player()); - for (int i = 0; i < g_nprojs; i++) { - ProjTarget &p = g_projs[i]; - // Follow the entity between the 10 Hz discovery scans so the marker and - // the blast-radius ring do not step in 100 ms increments. - Vector live; - if (entity_origin(entitylist_get(p.idx), live)) - p.origin = live; - 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; - } - float sx, sy; - bool center_ok = world_to_screen(p.origin, sx, sy) && - sx >= -40.0f && sx <= (float)g_scr_w + 40.0f && - sy >= -40.0f && sy <= (float)g_scr_h + 40.0f; - if (center_ok) { - 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); - } - // The ring is useful even when the grenade itself is behind the camera or - // off-screen, so it is not gated on center_ok and each segment is clipped - // at the near plane independently. - if (g_proj_radius && p.radius > 1.0f) { - const int SEG = 36; - Vector ring[SEG + 1]; - for (int k = 0; k <= SEG; k++) { - float a = (float)k * 2.0f * PI_F / (float)SEG; - ring[k] = Vector{p.origin.x + cosf(a) * p.radius, - p.origin.y + sinf(a) * p.radius, p.origin.z}; - } - surf_color(r, g, b, 150); - for (int k = 0; k < SEG; k++) { - int x0, y0, x1, y1; - if (project_segment(ring[k], ring[k + 1], x0, y0, x1, y1)) - surf_line(x0, y0, x1, y1); - } - } - } -} diff --git a/src/esp_internal.h b/src/esp_internal.h new file mode 100644 index 0000000..15178a7 --- /dev/null +++ b/src/esp_internal.h @@ -0,0 +1,34 @@ +#ifndef HL2H_ESP_INTERNAL_H +#define HL2H_ESP_INTERNAL_H + +// Helpers shared between the ESP translation units (esp.cpp, items.cpp, +// projectiles.cpp). Everything here has internal linkage semantics but is +// exported across those TUs, so it lives in the shared header instead of being +// repeated. + +#include "hl2h.h" +#include + +extern int g_scr_w, g_scr_h; + +double esp_now(); +bool esp_update_screen(); + +// entity helpers +void *entity_networkable(int idx); +const char *entity_class_name(void *net, void **out_cc); +const char *entity_model_name(void *ent); +bool entity_origin(void *ent, Vector &out); +bool entity_is_dormant(void *net); +bool active_weapon_contains(void *local_ent, const char *needle); +bool item_weapon_is_owned(void *ent); + +// drawing primitives +void surf_rect(int filled, int x0, int y0, int x1, int y1); +void surf_color(int r, int g, int b, int a); +void surf_line(int x0, int y0, int x1, int y1); +QAngle engine_view_angles(); +bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0, + int &x1, int &y1); + +#endif diff --git a/src/projectiles.cpp b/src/projectiles.cpp new file mode 100644 index 0000000..58b7745 --- /dev/null +++ b/src/projectiles.cpp @@ -0,0 +1,364 @@ +// projectiles.cpp — projectile ESP and grenade trajectory. +#include "esp_internal.h" +#include "hl2h.h" +#include +#include +#include + +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; + bool owned; +}; + +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"; + } +} + +// Fallback fuse clock when the server detonate time cannot be read. The stock +// frag fuse is 3 s from the first sighting. +static double proj_first_seen(int idx, double now) { + static constexpr int N = 64; + static int idxs[N]; + static double first[N]; + static int next = 0; + for (int i = 0; i < N; i++) { + if (idxs[i] != idx) + continue; + // The slot is stale if this index was last seen long ago: a new grenade + // reusing the entity number must start its own fuse, not inherit an old + // one (which produced permanently negative fuses). + if (now - first[i] > 10.0) + first[i] = now; + return first[i]; + } + idxs[next] = idx; + first[next] = now; + next = (next + 1) % N; // oldest entry is reused instead of dropped + return now; +} + +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 || entity_is_dormant(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); + if (!owned && local_idx > 0 && + server_grenade_thrower(idx) == local_idx) + owned = true; + bool frag_cls = cls && strcasestr(cls, "grenade") && + !strcasestr(cls, "weapon"); + bool item_cls = cls && (strcasestr(cls, "item") || + strcasestr(cls, "weapon")); + bool missile_model = strcasestr(model, "w_missile") && + !strcasestr(model, "missile_closed"); + int kind; + if (strcasestr(model, "w_grenade") && + (frag_cls || (!cls && owned))) { + kind = PK_FRAG; + } else if ((missile_model || strcasestr(model, "missile_launch")) && + !item_cls) { + // Launched RPG rocket; ammo pickups/weapons use the same model family. + 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.owned = owned; + 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; + float det = -1.0f; + if (kind != PK_RPG) { + 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 = 3.0f - (float)(now - proj_first_seen(idx, now)); + if (!(p.fuse > -2.0f && p.fuse < 30.0f)) + p.fuse = -1.0f; + } + } + } +} + +// Frag-grenade path preview: throw velocity from weapon_frag.cpp (eye/right +// offset, forward*1200 + 0.1 z), gravity 600, and a simple bounce model. +// Shows the bounces and the first landing with the blast-radius ring. +static void draw_grenade_trajectory(int local_idx) { + if (!g_proj_traj || !g_iface.entitylist) + return; + void *le = entitylist_get(local_idx); + if (!le) + return; + + Vector src{}, vel{}; + bool have = false; + bool actual = false; + // Real blast radius of the grenade being previewed, not a hardcoded 300. + float blast_radius = 300.0f; + // Prefer the local player's thrown frag: the real position and velocity are + // networked, so the preview starts from what is actually flying instead of + // the pre-throw estimate. The bounce contact points still depend on the + // grenade's spin/orientation, so this remains an approximation. + for (int i = 0; i < g_nprojs; i++) { + if (g_projs[i].kind != PK_FRAG || !g_projs[i].owned) + continue; + void *gent = entitylist_get(g_projs[i].idx); + if (!gent) + continue; + src = g_projs[i].origin; + // Live position: the cached origin is up to 100 ms old, and the velocity + // read below is live, so mixing them desynced the arc from the grenade. + Vector live; + if (entity_origin(gent, live)) + src = live; + if (g_projs[i].radius > 1.0f) + blast_radius = g_projs[i].radius; + if (g_net.velocity >= 0) { + const float *pv = (const float *)((uintptr_t)gent + g_net.velocity); + vel = Vector{pv[0], pv[1], pv[2]}; + } + have = true; + actual = true; + break; + } + if (!have) { + if (!active_weapon_contains(le, "frag") && + !active_weapon_contains(le, "w_grenade")) + return; + QAngle va = engine_view_angles(); + float pitch = va.x * PI_F / 180.0f; + float yaw = va.y * PI_F / 180.0f; + float cp = cosf(pitch), sp = sinf(pitch); + float cy = cosf(yaw), sy = sinf(yaw); + Vector fwd{cp * cy, cp * sy, -sp}; + Vector right{sy, -cy, 0.0f}; + + src = Vector{g_cam_eye.x + fwd.x * 18.0f + right.x * 8.0f, + g_cam_eye.y + fwd.y * 18.0f + right.y * 8.0f, + g_cam_eye.z + fwd.z * 18.0f}; + if (g_net.velocity >= 0) { + const float *pv = (const float *)((uintptr_t)le + g_net.velocity); + vel = Vector{pv[0], pv[1], pv[2]}; + } + vel.x += fwd.x * 1200.0f; + vel.y += fwd.y * 1200.0f; + vel.z += fwd.z * 1200.0f + 120.0f; // vForward[2] += 0.1 + have = true; + } + if (actual && vel.x * vel.x + vel.y * vel.y + vel.z * vel.z < 100.0f) + return; // resting grenade: no useful continuation + + const float gravity = 600.0f; + const float dt = 0.02f; + const float restitution = 0.35f; + const float friction = 0.35f; + + Vector pts[512]; + int n = 0; + Vector pos = src; + pts[n++] = pos; + bool landed = false; + for (int step = 0; step < 1200 && n < 511; step++) { + Vector nv = vel; + nv.z -= gravity * dt; + Vector next{pos.x + nv.x * dt, pos.y + nv.y * dt, pos.z + nv.z * dt}; + Vector hit_end{}, normal{}; + if (trace_solid_segment(pos, next, le, hit_end, normal)) { + pts[n++] = hit_end; + float vn = vel.x * normal.x + vel.y * normal.y + vel.z * normal.z; + Vector vt{vel.x - vn * normal.x, vel.y - vn * normal.y, + vel.z - vn * normal.z}; + float keep = 1.0f - friction; + vel.x = vt.x * keep - normal.x * vn * restitution; + vel.y = vt.y * keep - normal.y * vn * restitution; + vel.z = vt.z * keep - normal.z * vn * restitution; + float vlen2 = vel.x * vel.x + vel.y * vel.y + vel.z * vel.z; + pos.x = hit_end.x + normal.x * 1.0f; + pos.y = hit_end.y + normal.y * 1.0f; + pos.z = hit_end.z + normal.z * 1.0f; + if (normal.z > 0.6f && (vn > -80.0f || vlen2 < 2500.0f)) { + landed = true; + break; + } + } else { + pos = next; + vel = nv; + // One point every other step covers the whole flight without the old + // 127-point / 2.5-second cut-off. + if ((step & 1) == 0 && n < 511) + pts[n++] = pos; + } + } + + surf_color(255, 210, 90, 200); + for (int i = 0; i + 1 < n; i++) { + int x0, y0, x1, y1; + if (project_segment(pts[i], pts[i + 1], x0, y0, x1, y1)) + surf_line(x0, y0, x1, y1); + } + + Vector land = landed ? pos : pts[n - 1]; + float ssx, ssy; + if (world_to_screen(land, ssx, ssy)) { + int x = (int)ssx, y = (int)ssy; + surf_color(255, 210, 90, 230); + surf_rect(0, x - 3, y - 3, x + 3, y + 3); + surf_line(x - 9, y, x - 4, y); + surf_line(x + 4, y, x + 9, y); + surf_line(x, y - 9, x, y - 4); + surf_line(x, y + 4, x, y + 9); + } + // The landing ring belongs to the "Blast radius" toggle, and uses the + // networked m_DmgRadius when available instead of a fixed 300. + if (landed && g_proj_radius) { + const int SEG = 36; + const float radius = blast_radius; + surf_color(255, 120, 40, 120); + for (int k = 0; k < SEG; k++) { + float a0 = (float)k * 2.0f * PI_F / (float)SEG; + float a1 = (float)(k + 1) * 2.0f * PI_F / (float)SEG; + Vector p0{land.x + cosf(a0) * radius, land.y + sinf(a0) * radius, + land.z + 1.0f}; + Vector p1{land.x + cosf(a1) * radius, land.y + sinf(a1) * radius, + land.z + 1.0f}; + int x0, y0, x1, y1; + if (project_segment(p0, p1, x0, y0, x1, y1)) + surf_line(x0, y0, x1, y1); + } + } +} + +void esp_draw_projectiles() { + if (!g_surface || !g_iface.entitylist || g_net.origin < 0) + return; + if (!esp_update_screen()) + return; + projs_collect(engine_get_local_player()); + draw_grenade_trajectory(engine_get_local_player()); + for (int i = 0; i < g_nprojs; i++) { + ProjTarget &p = g_projs[i]; + // Follow the entity between the 10 Hz discovery scans so the marker and + // the blast-radius ring do not step in 100 ms increments. + Vector live; + if (entity_origin(entitylist_get(p.idx), live)) + p.origin = live; + 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; + } + float sx, sy; + bool center_ok = world_to_screen(p.origin, sx, sy) && + sx >= -40.0f && sx <= (float)g_scr_w + 40.0f && + sy >= -40.0f && sy <= (float)g_scr_h + 40.0f; + if (center_ok) { + 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); + } + // The ring is useful even when the grenade itself is behind the camera or + // off-screen, so it is not gated on center_ok and each segment is clipped + // at the near plane independently. + if (g_proj_radius && p.radius > 1.0f) { + const int SEG = 36; + Vector ring[SEG + 1]; + for (int k = 0; k <= SEG; k++) { + float a = (float)k * 2.0f * PI_F / (float)SEG; + ring[k] = Vector{p.origin.x + cosf(a) * p.radius, + p.origin.y + sinf(a) * p.radius, p.origin.z}; + } + surf_color(r, g, b, 150); + for (int k = 0; k < SEG; k++) { + int x0, y0, x1, y1; + if (project_segment(ring[k], ring[k + 1], x0, y0, x1, y1)) + surf_line(x0, y0, x1, y1); + } + } + } +} -- cgit v1.2.3