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/projectiles.cpp | 364 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 src/projectiles.cpp (limited to 'src/projectiles.cpp') 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