diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-06 16:23:01 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-06 16:23:01 +0100 |
| commit | 7352766582e140483ebcc1d8660a76e1bccbec0d (patch) | |
| tree | b06dabff70b294e19872ae195744532e3535dddd | |
| parent | Headers: split the shared header by declaration group (diff) | |
| download | hl2h-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.cpp | 392 | ||||
| -rw-r--r-- | src/esp_internal.h | 34 | ||||
| -rw-r--r-- | src/projectiles.cpp | 364 |
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]; | |||
| 31 | static int g_narrows = 0; | 32 | static int g_narrows = 0; |
| 32 | static EspTarget g_targets[128]; | 33 | static EspTarget g_targets[128]; |
| 33 | static int g_ntargets = 0; | 34 | static int g_ntargets = 0; |
| 34 | static int g_scr_w = 0, g_scr_h = 0; | 35 | int g_scr_w = 0, g_scr_h = 0; |
| 35 | static bool g_esp_logged = false; | 36 | static 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] = {}; | |||
| 70 | static int g_speed_head = 0; | 71 | static int g_speed_head = 0; |
| 71 | static double g_speed_last_sample = 0.0; | 72 | static double g_speed_last_sample = 0.0; |
| 72 | 73 | ||
| 73 | static double esp_now() { | 74 | double 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. |
| 82 | static bool esp_update_screen() { | 83 | bool 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). |
| 175 | static void *entity_networkable(int idx) { | 176 | void *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. |
| 180 | static const char *entity_class_name(void *net, void **out_cc) { | 181 | const 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 | ||
| 223 | static bool entity_is_dormant(void *net) { | 224 | bool 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 | ||
| 279 | static const char *entity_model_name(void *ent) { | 280 | const 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. |
| 288 | static bool entity_origin(void *ent, Vector &out) { | 289 | bool 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 | ||
| 528 | static bool active_weapon_contains(void *local_ent, const char *needle); | 529 | bool 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 | ||
| 777 | static void surf_rect(int filled, int x0, int y0, int x1, int y1) { | 778 | void 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 | } |
| 783 | static void surf_color(int r, int g, int b, int a) { | 784 | void 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 | } |
| 786 | static void surf_line(int x0, int y0, int x1, int y1) { | 787 | void 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. |
| 792 | static QAngle engine_view_angles() { | 793 | QAngle 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 | ||
| 1225 | static bool active_weapon_contains(void *local_ent, const char *needle) { | 1226 | bool 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. |
| 2280 | static bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0, | 2281 | bool 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. |
| 2546 | static bool item_weapon_is_owned(void *ent) { | 2547 | bool 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 | ||
| 3323 | enum ProjKind { PK_FRAG, PK_RPG, PK_BOMB, PK_BALL }; | ||
| 3324 | |||
| 3325 | struct 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 | |||
| 3334 | static ProjTarget g_projs[32]; | ||
| 3335 | static int g_nprojs = 0; | ||
| 3336 | |||
| 3337 | static 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 | |||
| 3344 | static 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. | ||
| 3356 | static 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 | |||
| 3377 | static 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. | ||
| 3466 | static 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 | |||
| 3611 | void 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 | |||
| 12 | extern int g_scr_w, g_scr_h; | ||
| 13 | |||
| 14 | double esp_now(); | ||
| 15 | bool esp_update_screen(); | ||
| 16 | |||
| 17 | // entity helpers | ||
| 18 | void *entity_networkable(int idx); | ||
| 19 | const char *entity_class_name(void *net, void **out_cc); | ||
| 20 | const char *entity_model_name(void *ent); | ||
| 21 | bool entity_origin(void *ent, Vector &out); | ||
| 22 | bool entity_is_dormant(void *net); | ||
| 23 | bool active_weapon_contains(void *local_ent, const char *needle); | ||
| 24 | bool item_weapon_is_owned(void *ent); | ||
| 25 | |||
| 26 | // drawing primitives | ||
| 27 | void surf_rect(int filled, int x0, int y0, int x1, int y1); | ||
| 28 | void surf_color(int r, int g, int b, int a); | ||
| 29 | void surf_line(int x0, int y0, int x1, int y1); | ||
| 30 | QAngle engine_view_angles(); | ||
| 31 | bool 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 | |||
| 8 | enum ProjKind { PK_FRAG, PK_RPG, PK_BOMB, PK_BALL }; | ||
| 9 | |||
| 10 | struct 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 | |||
| 19 | static ProjTarget g_projs[32]; | ||
| 20 | static int g_nprojs = 0; | ||
| 21 | |||
| 22 | static 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 | |||
| 29 | static 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. | ||
| 41 | static 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 | |||
| 62 | static 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. | ||
| 151 | static 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 | |||
| 296 | void 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 | } | ||
