summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:23:01 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:23:01 +0100
commit7352766582e140483ebcc1d8660a76e1bccbec0d (patch)
treeb06dabff70b294e19872ae195744532e3535dddd
parentHeaders: split the shared header by declaration group (diff)
downloadhl2h-7352766582e140483ebcc1d8660a76e1bccbec0d.tar.gz
hl2h-7352766582e140483ebcc1d8660a76e1bccbec0d.zip
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.
-rw-r--r--src/esp.cpp392
-rw-r--r--src/esp_internal.h34
-rw-r--r--src/projectiles.cpp364
3 files changed, 416 insertions, 374 deletions
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 @@
1// esp.cpp — ESP drawing + shared world_to_screen / entity gather (aimbot reuses these) 1// esp.cpp — ESP drawing + shared world_to_screen / entity gather (aimbot reuses these)
2#include "esp_internal.h"
2#include "hl2h.h" 3#include "hl2h.h"
3#include <cmath> 4#include <cmath>
4#include <cstring> 5#include <cstring>
@@ -31,7 +32,7 @@ static ArrowTarget g_arrows[32];
31static int g_narrows = 0; 32static int g_narrows = 0;
32static EspTarget g_targets[128]; 33static EspTarget g_targets[128];
33static int g_ntargets = 0; 34static int g_ntargets = 0;
34static int g_scr_w = 0, g_scr_h = 0; 35int g_scr_w = 0, g_scr_h = 0;
35static bool g_esp_logged = false; 36static bool g_esp_logged = false;
36 37
37// Listen-server sanity latch. Only ever set once per map (esp_reset_session), 38// Listen-server sanity latch. Only ever set once per map (esp_reset_session),
@@ -70,7 +71,7 @@ static float g_speed_samples[SPEED_SAMPLES] = {};
70static int g_speed_head = 0; 71static int g_speed_head = 0;
71static double g_speed_last_sample = 0.0; 72static double g_speed_last_sample = 0.0;
72 73
73static double esp_now() { 74double esp_now() {
74 timespec ts; 75 timespec ts;
75 clock_gettime(CLOCK_MONOTONIC, &ts); 76 clock_gettime(CLOCK_MONOTONIC, &ts);
76 return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; 77 return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
@@ -79,7 +80,7 @@ static double esp_now() {
79// Refresh the cached screen size every frame. It used to be read once, so a 80// Refresh the cached screen size every frame. It used to be read once, so a
80// resolution or window change left every projection, screen-edge clamp and 81// resolution or window change left every projection, screen-edge clamp and
81// centering calculation stale for the rest of the session. 82// centering calculation stale for the rest of the session.
82static bool esp_update_screen() { 83bool esp_update_screen() {
83 if (!g_surface) 84 if (!g_surface)
84 return false; 85 return false;
85 int w = 0, h = 0; 86 int w = 0, h = 0;
@@ -172,12 +173,12 @@ using GetModelNameFn = const char *(*)(void *, const void *);
172 173
173// entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the 174// entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the
174// IClientNetworkable subobject offset inside the entity (see projectz). 175// IClientNetworkable subobject offset inside the entity (see projectz).
175static void *entity_networkable(int idx) { 176void *entity_networkable(int idx) {
176 return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx); 177 return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx);
177} 178}
178 179
179// IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8. 180// IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8.
180static const char *entity_class_name(void *net, void **out_cc) { 181const char *entity_class_name(void *net, void **out_cc) {
181 if (!net) return nullptr; 182 if (!net) return nullptr;
182 void *cc = vfunc<GetCCFn>(net, 2)(net); 183 void *cc = vfunc<GetCCFn>(net, 2)(net);
183 if (out_cc) *out_cc = cc; 184 if (out_cc) *out_cc = cc;
@@ -220,7 +221,7 @@ static void dormant_selftest() {
220 logd("dormant check enabled"); 221 logd("dormant check enabled");
221} 222}
222 223
223static bool entity_is_dormant(void *net) { 224bool entity_is_dormant(void *net) {
224 if (!net) 225 if (!net)
225 return false; 226 return false;
226 if (g_dormant_state == 0) 227 if (g_dormant_state == 0)
@@ -276,7 +277,7 @@ static const char *model_name_from_index(int modelindex) {
276 return name; 277 return name;
277} 278}
278 279
279static const char *entity_model_name(void *ent) { 280const char *entity_model_name(void *ent) {
280 if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr; 281 if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr;
281 int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); 282 int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex);
282 return model_name_from_index(modelindex); 283 return model_name_from_index(modelindex);
@@ -285,7 +286,7 @@ static const char *entity_model_name(void *ent) {
285// Live origin for a cached entity index. Item/projectile discovery is throttled 286// Live origin for a cached entity index. Item/projectile discovery is throttled
286// to 10 Hz (it walks the whole entity list), but the markers must follow the 287// to 10 Hz (it walks the whole entity list), but the markers must follow the
287// world at render rate; refreshing one netvar read per cached target is cheap. 288// world at render rate; refreshing one netvar read per cached target is cheap.
288static bool entity_origin(void *ent, Vector &out) { 289bool entity_origin(void *ent, Vector &out) {
289 if (!ent || g_net.origin < 0) 290 if (!ent || g_net.origin < 0)
290 return false; 291 return false;
291 uintptr_t e = (uintptr_t)ent; 292 uintptr_t e = (uintptr_t)ent;
@@ -525,7 +526,7 @@ static int npc_damage_class(int idx, const char *model) {
525 return v; 526 return v;
526} 527}
527 528
528static bool active_weapon_contains(void *local_ent, const char *needle); 529bool active_weapon_contains(void *local_ent, const char *needle);
529 530
530// Armoured units need blast damage; the RPG is the only stock weapon that 531// Armoured units need blast damage; the RPG is the only stock weapon that
531// passes their OnTakeDamage filters. Immune units can never be damaged. 532// passes their OnTakeDamage filters. Immune units can never be damaged.
@@ -774,22 +775,22 @@ static void esp_collect(int local_idx) {
774 } 775 }
775} 776}
776 777
777static void surf_rect(int filled, int x0, int y0, int x1, int y1) { 778void surf_rect(int filled, int x0, int y0, int x1, int y1) {
778 if (filled) 779 if (filled)
779 overlay_fill(x0, y0, x1, y1); 780 overlay_fill(x0, y0, x1, y1);
780 else 781 else
781 overlay_outline_rect(x0, y0, x1, y1); 782 overlay_outline_rect(x0, y0, x1, y1);
782} 783}
783static void surf_color(int r, int g, int b, int a) { 784void surf_color(int r, int g, int b, int a) {
784 overlay_color(r, g, b, a); 785 overlay_color(r, g, b, a);
785} 786}
786static void surf_line(int x0, int y0, int x1, int y1) { 787void surf_line(int x0, int y0, int x1, int y1) {
787 overlay_line(x0, y0, x1, y1); 788 overlay_line(x0, y0, x1, y1);
788} 789}
789 790
790// Screen projection of the aimbot FOV cone boundary, using the engine's own 791// Screen projection of the aimbot FOV cone boundary, using the engine's own
791// world->screen transform so it follows the actual aspect/fov. 792// world->screen transform so it follows the actual aspect/fov.
792static QAngle engine_view_angles() { 793QAngle engine_view_angles() {
793 QAngle v{g_cam_view.x, g_cam_view.y, g_cam_view.z}; 794 QAngle v{g_cam_view.x, g_cam_view.y, g_cam_view.z};
794 if (g_iface.engine) { 795 if (g_iface.engine) {
795 using Fn = void (*)(void *, QAngle *); 796 using Fn = void (*)(void *, QAngle *);
@@ -1222,7 +1223,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
1222 return false; 1223 return false;
1223} 1224}
1224 1225
1225static bool active_weapon_contains(void *local_ent, const char *needle) { 1226bool active_weapon_contains(void *local_ent, const char *needle) {
1226 if (!local_ent || g_net.activeweapon < 0 || !needle) 1227 if (!local_ent || g_net.activeweapon < 0 || !needle)
1227 return false; 1228 return false;
1228 uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); 1229 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,
2277// Project a world-space segment, clipping it at the near plane first. Without 2278// Project a world-space segment, clipping it at the near plane first. Without
2278// this an edge with one corner behind the camera was dropped entirely, so a 2279// this an edge with one corner behind the camera was dropped entirely, so a
2279// partially visible transition box or radius ring disappeared. 2280// partially visible transition box or radius ring disappeared.
2280static bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0, 2281bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0,
2281 int &x1, int &y1) { 2282 int &x1, int &y1) {
2282 if (!g_scr_w || !g_scr_h) 2283 if (!g_scr_w || !g_scr_h)
2283 return false; 2284 return false;
2284 float pitch = g_cam_view.x * PI_F / 180.0f; 2285 float pitch = g_cam_view.x * PI_F / 180.0f;
@@ -2543,7 +2544,7 @@ static bool item_handle_valid(uint32_t h) {
2543} 2544}
2544 2545
2545// Weapons held by the player or an NPC are not world pickups. 2546// Weapons held by the player or an NPC are not world pickups.
2546static bool item_weapon_is_owned(void *ent) { 2547bool item_weapon_is_owned(void *ent) {
2547 if (!ent) 2548 if (!ent)
2548 return false; 2549 return false;
2549 if (g_net.owner >= 0) { 2550 if (g_net.owner >= 0) {
@@ -3320,360 +3321,3 @@ void esp_draw_items() {
3320// projectile ESP: grenades, rockets, helicopter bombs, AR2 balls 3321// projectile ESP: grenades, rockets, helicopter bombs, AR2 balls
3321// --------------------------------------------------------------------------- 3322// ---------------------------------------------------------------------------
3322 3323
3323enum ProjKind { PK_FRAG, PK_RPG, PK_BOMB, PK_BALL };
3324
3325struct ProjTarget {
3326 int idx;
3327 int kind;
3328 Vector origin;
3329 float fuse; // seconds; < 0 = impact / no fuse
3330 float radius;
3331 bool owned;
3332};
3333
3334static ProjTarget g_projs[32];
3335static int g_nprojs = 0;
3336
3337static float engine_curtime() {
3338 if (!g_iface.engine)
3339 return 0.0f;
3340 using TimeFn = float (*)(void *);
3341 return vfunc<TimeFn>(g_iface.engine, 14)(g_iface.engine);
3342}
3343
3344static const char *proj_name(int kind) {
3345 switch (kind) {
3346 case PK_FRAG: return "FRAG";
3347 case PK_RPG: return "RPG";
3348 case PK_BOMB: return "BOMB";
3349 case PK_BALL: return "AR2";
3350 default: return "PROJ";
3351 }
3352}
3353
3354// Fallback fuse clock when the server detonate time cannot be read. The stock
3355// frag fuse is 3 s from the first sighting.
3356static double proj_first_seen(int idx, double now) {
3357 static constexpr int N = 64;
3358 static int idxs[N];
3359 static double first[N];
3360 static int next = 0;
3361 for (int i = 0; i < N; i++) {
3362 if (idxs[i] != idx)
3363 continue;
3364 // The slot is stale if this index was last seen long ago: a new grenade
3365 // reusing the entity number must start its own fuse, not inherit an old
3366 // one (which produced permanently negative fuses).
3367 if (now - first[i] > 10.0)
3368 first[i] = now;
3369 return first[i];
3370 }
3371 idxs[next] = idx;
3372 first[next] = now;
3373 next = (next + 1) % N; // oldest entry is reused instead of dropped
3374 return now;
3375}
3376
3377static void projs_collect(int local_idx) {
3378 static double last_scan = 0.0;
3379 double now = esp_now();
3380 if (now - last_scan < 0.10)
3381 return;
3382 last_scan = now;
3383 g_nprojs = 0;
3384 if (!g_iface.entitylist || g_net.origin < 0)
3385 return;
3386 resolve_server_netvars();
3387 using Fn = int (*)(void *);
3388 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
3389 if (hi < 0 || hi > 4096)
3390 hi = 4096;
3391 float curtime = engine_curtime();
3392 for (int idx = 1; idx <= hi && g_nprojs < 32; idx++) {
3393 if (idx == local_idx)
3394 continue;
3395 void *ent = entitylist_get(idx);
3396 if (!ent)
3397 continue;
3398 void *net = entity_networkable(idx);
3399 if (!net || entity_is_dormant(net))
3400 continue;
3401 const char *cls = entity_class_name(net, nullptr);
3402 const char *model = entity_model_name(ent);
3403 if (!model)
3404 continue;
3405 bool owned = item_weapon_is_owned(ent);
3406 if (!owned && local_idx > 0 &&
3407 server_grenade_thrower(idx) == local_idx)
3408 owned = true;
3409 bool frag_cls = cls && strcasestr(cls, "grenade") &&
3410 !strcasestr(cls, "weapon");
3411 bool item_cls = cls && (strcasestr(cls, "item") ||
3412 strcasestr(cls, "weapon"));
3413 bool missile_model = strcasestr(model, "w_missile") &&
3414 !strcasestr(model, "missile_closed");
3415 int kind;
3416 if (strcasestr(model, "w_grenade") &&
3417 (frag_cls || (!cls && owned))) {
3418 kind = PK_FRAG;
3419 } else if ((missile_model || strcasestr(model, "missile_launch")) &&
3420 !item_cls) {
3421 // Launched RPG rocket; ammo pickups/weapons use the same model family.
3422 kind = PK_RPG;
3423 } else if (strcasestr(model, "helicopter_bomb")) {
3424 kind = PK_BOMB;
3425 } else if (strcasestr(model, "combineball")) {
3426 kind = PK_BALL;
3427 } else {
3428 continue;
3429 }
3430 uintptr_t e = (uintptr_t)ent;
3431 ProjTarget &p = g_projs[g_nprojs++];
3432 p.idx = idx;
3433 p.kind = kind;
3434 p.owned = owned;
3435 p.origin = Vector{*(float *)(e + g_net.origin),
3436 *(float *)(e + g_net.origin + 4),
3437 *(float *)(e + g_net.origin + 8)};
3438 // The client m_DmgRadius is networked; fall back to the server field and
3439 // then to the stock radius for this projectile type.
3440 p.radius = -1.0f;
3441 if (g_net.dmg_radius >= 0)
3442 p.radius = *(float *)(e + g_net.dmg_radius);
3443 if (!(p.radius > 1.0f && p.radius < 2048.0f))
3444 p.radius = server_grenade_radius(idx);
3445 if (!(p.radius > 1.0f && p.radius < 2048.0f))
3446 p.radius = kind == PK_RPG ? 200.0f : kind == PK_BALL ? 150.0f : 300.0f;
3447 // m_flDetonateTime is server-only (not in the send table).
3448 p.fuse = -1.0f;
3449 float det = -1.0f;
3450 if (kind != PK_RPG) {
3451 det = server_grenade_detonate(idx);
3452 if (det > 0.0f && curtime > 0.0f)
3453 p.fuse = det - curtime;
3454 if (!(p.fuse > -2.0f && p.fuse < 30.0f)) {
3455 p.fuse = 3.0f - (float)(now - proj_first_seen(idx, now));
3456 if (!(p.fuse > -2.0f && p.fuse < 30.0f))
3457 p.fuse = -1.0f;
3458 }
3459 }
3460 }
3461}
3462
3463// Frag-grenade path preview: throw velocity from weapon_frag.cpp (eye/right
3464// offset, forward*1200 + 0.1 z), gravity 600, and a simple bounce model.
3465// Shows the bounces and the first landing with the blast-radius ring.
3466static void draw_grenade_trajectory(int local_idx) {
3467 if (!g_proj_traj || !g_iface.entitylist)
3468 return;
3469 void *le = entitylist_get(local_idx);
3470 if (!le)
3471 return;
3472
3473 Vector src{}, vel{};
3474 bool have = false;
3475 bool actual = false;
3476 // Real blast radius of the grenade being previewed, not a hardcoded 300.
3477 float blast_radius = 300.0f;
3478 // Prefer the local player's thrown frag: the real position and velocity are
3479 // networked, so the preview starts from what is actually flying instead of
3480 // the pre-throw estimate. The bounce contact points still depend on the
3481 // grenade's spin/orientation, so this remains an approximation.
3482 for (int i = 0; i < g_nprojs; i++) {
3483 if (g_projs[i].kind != PK_FRAG || !g_projs[i].owned)
3484 continue;
3485 void *gent = entitylist_get(g_projs[i].idx);
3486 if (!gent)
3487 continue;
3488 src = g_projs[i].origin;
3489 // Live position: the cached origin is up to 100 ms old, and the velocity
3490 // read below is live, so mixing them desynced the arc from the grenade.
3491 Vector live;
3492 if (entity_origin(gent, live))
3493 src = live;
3494 if (g_projs[i].radius > 1.0f)
3495 blast_radius = g_projs[i].radius;
3496 if (g_net.velocity >= 0) {
3497 const float *pv = (const float *)((uintptr_t)gent + g_net.velocity);
3498 vel = Vector{pv[0], pv[1], pv[2]};
3499 }
3500 have = true;
3501 actual = true;
3502 break;
3503 }
3504 if (!have) {
3505 if (!active_weapon_contains(le, "frag") &&
3506 !active_weapon_contains(le, "w_grenade"))
3507 return;
3508 QAngle va = engine_view_angles();
3509 float pitch = va.x * PI_F / 180.0f;
3510 float yaw = va.y * PI_F / 180.0f;
3511 float cp = cosf(pitch), sp = sinf(pitch);
3512 float cy = cosf(yaw), sy = sinf(yaw);
3513 Vector fwd{cp * cy, cp * sy, -sp};
3514 Vector right{sy, -cy, 0.0f};
3515
3516 src = Vector{g_cam_eye.x + fwd.x * 18.0f + right.x * 8.0f,
3517 g_cam_eye.y + fwd.y * 18.0f + right.y * 8.0f,
3518 g_cam_eye.z + fwd.z * 18.0f};
3519 if (g_net.velocity >= 0) {
3520 const float *pv = (const float *)((uintptr_t)le + g_net.velocity);
3521 vel = Vector{pv[0], pv[1], pv[2]};
3522 }
3523 vel.x += fwd.x * 1200.0f;
3524 vel.y += fwd.y * 1200.0f;
3525 vel.z += fwd.z * 1200.0f + 120.0f; // vForward[2] += 0.1
3526 have = true;
3527 }
3528 if (actual && vel.x * vel.x + vel.y * vel.y + vel.z * vel.z < 100.0f)
3529 return; // resting grenade: no useful continuation
3530
3531 const float gravity = 600.0f;
3532 const float dt = 0.02f;
3533 const float restitution = 0.35f;
3534 const float friction = 0.35f;
3535
3536 Vector pts[512];
3537 int n = 0;
3538 Vector pos = src;
3539 pts[n++] = pos;
3540 bool landed = false;
3541 for (int step = 0; step < 1200 && n < 511; step++) {
3542 Vector nv = vel;
3543 nv.z -= gravity * dt;
3544 Vector next{pos.x + nv.x * dt, pos.y + nv.y * dt, pos.z + nv.z * dt};
3545 Vector hit_end{}, normal{};
3546 if (trace_solid_segment(pos, next, le, hit_end, normal)) {
3547 pts[n++] = hit_end;
3548 float vn = vel.x * normal.x + vel.y * normal.y + vel.z * normal.z;
3549 Vector vt{vel.x - vn * normal.x, vel.y - vn * normal.y,
3550 vel.z - vn * normal.z};
3551 float keep = 1.0f - friction;
3552 vel.x = vt.x * keep - normal.x * vn * restitution;
3553 vel.y = vt.y * keep - normal.y * vn * restitution;
3554 vel.z = vt.z * keep - normal.z * vn * restitution;
3555 float vlen2 = vel.x * vel.x + vel.y * vel.y + vel.z * vel.z;
3556 pos.x = hit_end.x + normal.x * 1.0f;
3557 pos.y = hit_end.y + normal.y * 1.0f;
3558 pos.z = hit_end.z + normal.z * 1.0f;
3559 if (normal.z > 0.6f && (vn > -80.0f || vlen2 < 2500.0f)) {
3560 landed = true;
3561 break;
3562 }
3563 } else {
3564 pos = next;
3565 vel = nv;
3566 // One point every other step covers the whole flight without the old
3567 // 127-point / 2.5-second cut-off.
3568 if ((step & 1) == 0 && n < 511)
3569 pts[n++] = pos;
3570 }
3571 }
3572
3573 surf_color(255, 210, 90, 200);
3574 for (int i = 0; i + 1 < n; i++) {
3575 int x0, y0, x1, y1;
3576 if (project_segment(pts[i], pts[i + 1], x0, y0, x1, y1))
3577 surf_line(x0, y0, x1, y1);
3578 }
3579
3580 Vector land = landed ? pos : pts[n - 1];
3581 float ssx, ssy;
3582 if (world_to_screen(land, ssx, ssy)) {
3583 int x = (int)ssx, y = (int)ssy;
3584 surf_color(255, 210, 90, 230);
3585 surf_rect(0, x - 3, y - 3, x + 3, y + 3);
3586 surf_line(x - 9, y, x - 4, y);
3587 surf_line(x + 4, y, x + 9, y);
3588 surf_line(x, y - 9, x, y - 4);
3589 surf_line(x, y + 4, x, y + 9);
3590 }
3591 // The landing ring belongs to the "Blast radius" toggle, and uses the
3592 // networked m_DmgRadius when available instead of a fixed 300.
3593 if (landed && g_proj_radius) {
3594 const int SEG = 36;
3595 const float radius = blast_radius;
3596 surf_color(255, 120, 40, 120);
3597 for (int k = 0; k < SEG; k++) {
3598 float a0 = (float)k * 2.0f * PI_F / (float)SEG;
3599 float a1 = (float)(k + 1) * 2.0f * PI_F / (float)SEG;
3600 Vector p0{land.x + cosf(a0) * radius, land.y + sinf(a0) * radius,
3601 land.z + 1.0f};
3602 Vector p1{land.x + cosf(a1) * radius, land.y + sinf(a1) * radius,
3603 land.z + 1.0f};
3604 int x0, y0, x1, y1;
3605 if (project_segment(p0, p1, x0, y0, x1, y1))
3606 surf_line(x0, y0, x1, y1);
3607 }
3608 }
3609}
3610
3611void esp_draw_projectiles() {
3612 if (!g_surface || !g_iface.entitylist || g_net.origin < 0)
3613 return;
3614 if (!esp_update_screen())
3615 return;
3616 projs_collect(engine_get_local_player());
3617 draw_grenade_trajectory(engine_get_local_player());
3618 for (int i = 0; i < g_nprojs; i++) {
3619 ProjTarget &p = g_projs[i];
3620 // Follow the entity between the 10 Hz discovery scans so the marker and
3621 // the blast-radius ring do not step in 100 ms increments.
3622 Vector live;
3623 if (entity_origin(entitylist_get(p.idx), live))
3624 p.origin = live;
3625 int r = 255, g = 170, b = 60;
3626 if (p.kind == PK_RPG) {
3627 r = 255; g = 120; b = 40;
3628 } else if (p.kind == PK_BOMB) {
3629 r = 255; g = 60; b = 60;
3630 } else if (p.kind == PK_BALL) {
3631 r = 120; g = 220; b = 255;
3632 } else if (p.fuse >= 0.0f && p.fuse < 0.8f) {
3633 r = 255; g = 60; b = 60;
3634 }
3635 float sx, sy;
3636 bool center_ok = world_to_screen(p.origin, sx, sy) &&
3637 sx >= -40.0f && sx <= (float)g_scr_w + 40.0f &&
3638 sy >= -40.0f && sy <= (float)g_scr_h + 40.0f;
3639 if (center_ok) {
3640 int x = (int)sx, y = (int)sy;
3641 surf_color(r, g, b, 255);
3642 surf_rect(0, x - 4, y - 4, x + 4, y + 4);
3643 surf_rect(1, x - 1, y - 1, x + 2, y + 2);
3644 char text[64];
3645 if (g_proj_fuse && p.fuse >= 0.0f)
3646 snprintf(text, sizeof(text), "%s %.1fs", proj_name(p.kind), p.fuse);
3647 else
3648 snprintf(text, sizeof(text), "%s", proj_name(p.kind));
3649 int tx = x + 9;
3650 if (tx > g_scr_w - 80)
3651 tx = x - 9 - (int)strlen(text) * 6;
3652 if (tx < 4)
3653 tx = 4;
3654 int ty = y - 14;
3655 if (ty < 4)
3656 ty = 4;
3657 draw_text_small_col(tx + 1, ty + 1, text, 0, 0, 0);
3658 draw_text_small_col(tx, ty, text, r, g, b);
3659 }
3660 // The ring is useful even when the grenade itself is behind the camera or
3661 // off-screen, so it is not gated on center_ok and each segment is clipped
3662 // at the near plane independently.
3663 if (g_proj_radius && p.radius > 1.0f) {
3664 const int SEG = 36;
3665 Vector ring[SEG + 1];
3666 for (int k = 0; k <= SEG; k++) {
3667 float a = (float)k * 2.0f * PI_F / (float)SEG;
3668 ring[k] = Vector{p.origin.x + cosf(a) * p.radius,
3669 p.origin.y + sinf(a) * p.radius, p.origin.z};
3670 }
3671 surf_color(r, g, b, 150);
3672 for (int k = 0; k < SEG; k++) {
3673 int x0, y0, x1, y1;
3674 if (project_segment(ring[k], ring[k + 1], x0, y0, x1, y1))
3675 surf_line(x0, y0, x1, y1);
3676 }
3677 }
3678 }
3679}
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 @@
1#ifndef HL2H_ESP_INTERNAL_H
2#define HL2H_ESP_INTERNAL_H
3
4// Helpers shared between the ESP translation units (esp.cpp, items.cpp,
5// projectiles.cpp). Everything here has internal linkage semantics but is
6// exported across those TUs, so it lives in the shared header instead of being
7// repeated.
8
9#include "hl2h.h"
10#include <cstdint>
11
12extern int g_scr_w, g_scr_h;
13
14double esp_now();
15bool esp_update_screen();
16
17// entity helpers
18void *entity_networkable(int idx);
19const char *entity_class_name(void *net, void **out_cc);
20const char *entity_model_name(void *ent);
21bool entity_origin(void *ent, Vector &out);
22bool entity_is_dormant(void *net);
23bool active_weapon_contains(void *local_ent, const char *needle);
24bool item_weapon_is_owned(void *ent);
25
26// drawing primitives
27void surf_rect(int filled, int x0, int y0, int x1, int y1);
28void surf_color(int r, int g, int b, int a);
29void surf_line(int x0, int y0, int x1, int y1);
30QAngle engine_view_angles();
31bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0,
32 int &x1, int &y1);
33
34#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 @@
1// projectiles.cpp — projectile ESP and grenade trajectory.
2#include "esp_internal.h"
3#include "hl2h.h"
4#include <cmath>
5#include <cstdio>
6#include <cstring>
7
8enum ProjKind { PK_FRAG, PK_RPG, PK_BOMB, PK_BALL };
9
10struct ProjTarget {
11 int idx;
12 int kind;
13 Vector origin;
14 float fuse; // seconds; < 0 = impact / no fuse
15 float radius;
16 bool owned;
17};
18
19static ProjTarget g_projs[32];
20static int g_nprojs = 0;
21
22static float engine_curtime() {
23 if (!g_iface.engine)
24 return 0.0f;
25 using TimeFn = float (*)(void *);
26 return vfunc<TimeFn>(g_iface.engine, 14)(g_iface.engine);
27}
28
29static const char *proj_name(int kind) {
30 switch (kind) {
31 case PK_FRAG: return "FRAG";
32 case PK_RPG: return "RPG";
33 case PK_BOMB: return "BOMB";
34 case PK_BALL: return "AR2";
35 default: return "PROJ";
36 }
37}
38
39// Fallback fuse clock when the server detonate time cannot be read. The stock
40// frag fuse is 3 s from the first sighting.
41static double proj_first_seen(int idx, double now) {
42 static constexpr int N = 64;
43 static int idxs[N];
44 static double first[N];
45 static int next = 0;
46 for (int i = 0; i < N; i++) {
47 if (idxs[i] != idx)
48 continue;
49 // The slot is stale if this index was last seen long ago: a new grenade
50 // reusing the entity number must start its own fuse, not inherit an old
51 // one (which produced permanently negative fuses).
52 if (now - first[i] > 10.0)
53 first[i] = now;
54 return first[i];
55 }
56 idxs[next] = idx;
57 first[next] = now;
58 next = (next + 1) % N; // oldest entry is reused instead of dropped
59 return now;
60}
61
62static void projs_collect(int local_idx) {
63 static double last_scan = 0.0;
64 double now = esp_now();
65 if (now - last_scan < 0.10)
66 return;
67 last_scan = now;
68 g_nprojs = 0;
69 if (!g_iface.entitylist || g_net.origin < 0)
70 return;
71 resolve_server_netvars();
72 using Fn = int (*)(void *);
73 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
74 if (hi < 0 || hi > 4096)
75 hi = 4096;
76 float curtime = engine_curtime();
77 for (int idx = 1; idx <= hi && g_nprojs < 32; idx++) {
78 if (idx == local_idx)
79 continue;
80 void *ent = entitylist_get(idx);
81 if (!ent)
82 continue;
83 void *net = entity_networkable(idx);
84 if (!net || entity_is_dormant(net))
85 continue;
86 const char *cls = entity_class_name(net, nullptr);
87 const char *model = entity_model_name(ent);
88 if (!model)
89 continue;
90 bool owned = item_weapon_is_owned(ent);
91 if (!owned && local_idx > 0 &&
92 server_grenade_thrower(idx) == local_idx)
93 owned = true;
94 bool frag_cls = cls && strcasestr(cls, "grenade") &&
95 !strcasestr(cls, "weapon");
96 bool item_cls = cls && (strcasestr(cls, "item") ||
97 strcasestr(cls, "weapon"));
98 bool missile_model = strcasestr(model, "w_missile") &&
99 !strcasestr(model, "missile_closed");
100 int kind;
101 if (strcasestr(model, "w_grenade") &&
102 (frag_cls || (!cls && owned))) {
103 kind = PK_FRAG;
104 } else if ((missile_model || strcasestr(model, "missile_launch")) &&
105 !item_cls) {
106 // Launched RPG rocket; ammo pickups/weapons use the same model family.
107 kind = PK_RPG;
108 } else if (strcasestr(model, "helicopter_bomb")) {
109 kind = PK_BOMB;
110 } else if (strcasestr(model, "combineball")) {
111 kind = PK_BALL;
112 } else {
113 continue;
114 }
115 uintptr_t e = (uintptr_t)ent;
116 ProjTarget &p = g_projs[g_nprojs++];
117 p.idx = idx;
118 p.kind = kind;
119 p.owned = owned;
120 p.origin = Vector{*(float *)(e + g_net.origin),
121 *(float *)(e + g_net.origin + 4),
122 *(float *)(e + g_net.origin + 8)};
123 // The client m_DmgRadius is networked; fall back to the server field and
124 // then to the stock radius for this projectile type.
125 p.radius = -1.0f;
126 if (g_net.dmg_radius >= 0)
127 p.radius = *(float *)(e + g_net.dmg_radius);
128 if (!(p.radius > 1.0f && p.radius < 2048.0f))
129 p.radius = server_grenade_radius(idx);
130 if (!(p.radius > 1.0f && p.radius < 2048.0f))
131 p.radius = kind == PK_RPG ? 200.0f : kind == PK_BALL ? 150.0f : 300.0f;
132 // m_flDetonateTime is server-only (not in the send table).
133 p.fuse = -1.0f;
134 float det = -1.0f;
135 if (kind != PK_RPG) {
136 det = server_grenade_detonate(idx);
137 if (det > 0.0f && curtime > 0.0f)
138 p.fuse = det - curtime;
139 if (!(p.fuse > -2.0f && p.fuse < 30.0f)) {
140 p.fuse = 3.0f - (float)(now - proj_first_seen(idx, now));
141 if (!(p.fuse > -2.0f && p.fuse < 30.0f))
142 p.fuse = -1.0f;
143 }
144 }
145 }
146}
147
148// Frag-grenade path preview: throw velocity from weapon_frag.cpp (eye/right
149// offset, forward*1200 + 0.1 z), gravity 600, and a simple bounce model.
150// Shows the bounces and the first landing with the blast-radius ring.
151static void draw_grenade_trajectory(int local_idx) {
152 if (!g_proj_traj || !g_iface.entitylist)
153 return;
154 void *le = entitylist_get(local_idx);
155 if (!le)
156 return;
157
158 Vector src{}, vel{};
159 bool have = false;
160 bool actual = false;
161 // Real blast radius of the grenade being previewed, not a hardcoded 300.
162 float blast_radius = 300.0f;
163 // Prefer the local player's thrown frag: the real position and velocity are
164 // networked, so the preview starts from what is actually flying instead of
165 // the pre-throw estimate. The bounce contact points still depend on the
166 // grenade's spin/orientation, so this remains an approximation.
167 for (int i = 0; i < g_nprojs; i++) {
168 if (g_projs[i].kind != PK_FRAG || !g_projs[i].owned)
169 continue;
170 void *gent = entitylist_get(g_projs[i].idx);
171 if (!gent)
172 continue;
173 src = g_projs[i].origin;
174 // Live position: the cached origin is up to 100 ms old, and the velocity
175 // read below is live, so mixing them desynced the arc from the grenade.
176 Vector live;
177 if (entity_origin(gent, live))
178 src = live;
179 if (g_projs[i].radius > 1.0f)
180 blast_radius = g_projs[i].radius;
181 if (g_net.velocity >= 0) {
182 const float *pv = (const float *)((uintptr_t)gent + g_net.velocity);
183 vel = Vector{pv[0], pv[1], pv[2]};
184 }
185 have = true;
186 actual = true;
187 break;
188 }
189 if (!have) {
190 if (!active_weapon_contains(le, "frag") &&
191 !active_weapon_contains(le, "w_grenade"))
192 return;
193 QAngle va = engine_view_angles();
194 float pitch = va.x * PI_F / 180.0f;
195 float yaw = va.y * PI_F / 180.0f;
196 float cp = cosf(pitch), sp = sinf(pitch);
197 float cy = cosf(yaw), sy = sinf(yaw);
198 Vector fwd{cp * cy, cp * sy, -sp};
199 Vector right{sy, -cy, 0.0f};
200
201 src = Vector{g_cam_eye.x + fwd.x * 18.0f + right.x * 8.0f,
202 g_cam_eye.y + fwd.y * 18.0f + right.y * 8.0f,
203 g_cam_eye.z + fwd.z * 18.0f};
204 if (g_net.velocity >= 0) {
205 const float *pv = (const float *)((uintptr_t)le + g_net.velocity);
206 vel = Vector{pv[0], pv[1], pv[2]};
207 }
208 vel.x += fwd.x * 1200.0f;
209 vel.y += fwd.y * 1200.0f;
210 vel.z += fwd.z * 1200.0f + 120.0f; // vForward[2] += 0.1
211 have = true;
212 }
213 if (actual && vel.x * vel.x + vel.y * vel.y + vel.z * vel.z < 100.0f)
214 return; // resting grenade: no useful continuation
215
216 const float gravity = 600.0f;
217 const float dt = 0.02f;
218 const float restitution = 0.35f;
219 const float friction = 0.35f;
220
221 Vector pts[512];
222 int n = 0;
223 Vector pos = src;
224 pts[n++] = pos;
225 bool landed = false;
226 for (int step = 0; step < 1200 && n < 511; step++) {
227 Vector nv = vel;
228 nv.z -= gravity * dt;
229 Vector next{pos.x + nv.x * dt, pos.y + nv.y * dt, pos.z + nv.z * dt};
230 Vector hit_end{}, normal{};
231 if (trace_solid_segment(pos, next, le, hit_end, normal)) {
232 pts[n++] = hit_end;
233 float vn = vel.x * normal.x + vel.y * normal.y + vel.z * normal.z;
234 Vector vt{vel.x - vn * normal.x, vel.y - vn * normal.y,
235 vel.z - vn * normal.z};
236 float keep = 1.0f - friction;
237 vel.x = vt.x * keep - normal.x * vn * restitution;
238 vel.y = vt.y * keep - normal.y * vn * restitution;
239 vel.z = vt.z * keep - normal.z * vn * restitution;
240 float vlen2 = vel.x * vel.x + vel.y * vel.y + vel.z * vel.z;
241 pos.x = hit_end.x + normal.x * 1.0f;
242 pos.y = hit_end.y + normal.y * 1.0f;
243 pos.z = hit_end.z + normal.z * 1.0f;
244 if (normal.z > 0.6f && (vn > -80.0f || vlen2 < 2500.0f)) {
245 landed = true;
246 break;
247 }
248 } else {
249 pos = next;
250 vel = nv;
251 // One point every other step covers the whole flight without the old
252 // 127-point / 2.5-second cut-off.
253 if ((step & 1) == 0 && n < 511)
254 pts[n++] = pos;
255 }
256 }
257
258 surf_color(255, 210, 90, 200);
259 for (int i = 0; i + 1 < n; i++) {
260 int x0, y0, x1, y1;
261 if (project_segment(pts[i], pts[i + 1], x0, y0, x1, y1))
262 surf_line(x0, y0, x1, y1);
263 }
264
265 Vector land = landed ? pos : pts[n - 1];
266 float ssx, ssy;
267 if (world_to_screen(land, ssx, ssy)) {
268 int x = (int)ssx, y = (int)ssy;
269 surf_color(255, 210, 90, 230);
270 surf_rect(0, x - 3, y - 3, x + 3, y + 3);
271 surf_line(x - 9, y, x - 4, y);
272 surf_line(x + 4, y, x + 9, y);
273 surf_line(x, y - 9, x, y - 4);
274 surf_line(x, y + 4, x, y + 9);
275 }
276 // The landing ring belongs to the "Blast radius" toggle, and uses the
277 // networked m_DmgRadius when available instead of a fixed 300.
278 if (landed && g_proj_radius) {
279 const int SEG = 36;
280 const float radius = blast_radius;
281 surf_color(255, 120, 40, 120);
282 for (int k = 0; k < SEG; k++) {
283 float a0 = (float)k * 2.0f * PI_F / (float)SEG;
284 float a1 = (float)(k + 1) * 2.0f * PI_F / (float)SEG;
285 Vector p0{land.x + cosf(a0) * radius, land.y + sinf(a0) * radius,
286 land.z + 1.0f};
287 Vector p1{land.x + cosf(a1) * radius, land.y + sinf(a1) * radius,
288 land.z + 1.0f};
289 int x0, y0, x1, y1;
290 if (project_segment(p0, p1, x0, y0, x1, y1))
291 surf_line(x0, y0, x1, y1);
292 }
293 }
294}
295
296void esp_draw_projectiles() {
297 if (!g_surface || !g_iface.entitylist || g_net.origin < 0)
298 return;
299 if (!esp_update_screen())
300 return;
301 projs_collect(engine_get_local_player());
302 draw_grenade_trajectory(engine_get_local_player());
303 for (int i = 0; i < g_nprojs; i++) {
304 ProjTarget &p = g_projs[i];
305 // Follow the entity between the 10 Hz discovery scans so the marker and
306 // the blast-radius ring do not step in 100 ms increments.
307 Vector live;
308 if (entity_origin(entitylist_get(p.idx), live))
309 p.origin = live;
310 int r = 255, g = 170, b = 60;
311 if (p.kind == PK_RPG) {
312 r = 255; g = 120; b = 40;
313 } else if (p.kind == PK_BOMB) {
314 r = 255; g = 60; b = 60;
315 } else if (p.kind == PK_BALL) {
316 r = 120; g = 220; b = 255;
317 } else if (p.fuse >= 0.0f && p.fuse < 0.8f) {
318 r = 255; g = 60; b = 60;
319 }
320 float sx, sy;
321 bool center_ok = world_to_screen(p.origin, sx, sy) &&
322 sx >= -40.0f && sx <= (float)g_scr_w + 40.0f &&
323 sy >= -40.0f && sy <= (float)g_scr_h + 40.0f;
324 if (center_ok) {
325 int x = (int)sx, y = (int)sy;
326 surf_color(r, g, b, 255);
327 surf_rect(0, x - 4, y - 4, x + 4, y + 4);
328 surf_rect(1, x - 1, y - 1, x + 2, y + 2);
329 char text[64];
330 if (g_proj_fuse && p.fuse >= 0.0f)
331 snprintf(text, sizeof(text), "%s %.1fs", proj_name(p.kind), p.fuse);
332 else
333 snprintf(text, sizeof(text), "%s", proj_name(p.kind));
334 int tx = x + 9;
335 if (tx > g_scr_w - 80)
336 tx = x - 9 - (int)strlen(text) * 6;
337 if (tx < 4)
338 tx = 4;
339 int ty = y - 14;
340 if (ty < 4)
341 ty = 4;
342 draw_text_small_col(tx + 1, ty + 1, text, 0, 0, 0);
343 draw_text_small_col(tx, ty, text, r, g, b);
344 }
345 // The ring is useful even when the grenade itself is behind the camera or
346 // off-screen, so it is not gated on center_ok and each segment is clipped
347 // at the near plane independently.
348 if (g_proj_radius && p.radius > 1.0f) {
349 const int SEG = 36;
350 Vector ring[SEG + 1];
351 for (int k = 0; k <= SEG; k++) {
352 float a = (float)k * 2.0f * PI_F / (float)SEG;
353 ring[k] = Vector{p.origin.x + cosf(a) * p.radius,
354 p.origin.y + sinf(a) * p.radius, p.origin.z};
355 }
356 surf_color(r, g, b, 150);
357 for (int k = 0; k < SEG; k++) {
358 int x0, y0, x1, y1;
359 if (project_segment(ring[k], ring[k + 1], x0, y0, x1, y1))
360 surf_line(x0, y0, x1, y1);
361 }
362 }
363 }
364}