// 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); } } } }