summaryrefslogtreecommitdiffstats
path: root/src/projectiles.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/projectiles.cpp')
-rw-r--r--src/projectiles.cpp364
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
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}