diff options
Diffstat (limited to 'src/projectiles.cpp')
| -rw-r--r-- | src/projectiles.cpp | 364 |
1 files changed, 364 insertions, 0 deletions
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 | } | ||
