summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:25:48 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:25:48 +0100
commitc43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1 (patch)
tree2da18f9d243d898fb218f36a4508e0d309782d5e
parentESP: extract item ESP into its own translation unit (diff)
downloadhl2h-c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1.tar.gz
hl2h-c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1.zip
ESP: finish the split into entity, combat, item and projectile units
esp.cpp keeps the scan and drawing (~1000 lines, down from 3665). Entity lookups and NPC classification move to entity_query.cpp together with the per-map caches and the session reset; combat (aimbot, silent aim, melee, spread, triggerbot) moves to combat.cpp. Shared helpers are declared in esp_internal.h.
-rw-r--r--src/combat.cpp1066
-rw-r--r--src/entity_query.cpp460
-rw-r--r--src/esp.cpp1519
-rw-r--r--src/esp_internal.h23
4 files changed, 1552 insertions, 1516 deletions
diff --git a/src/combat.cpp b/src/combat.cpp
new file mode 100644
index 0000000..cfc8190
--- /dev/null
+++ b/src/combat.cpp
@@ -0,0 +1,1066 @@
1// combat.cpp — aimbot, silent aim, melee, spread control, triggerbot.
2#include "esp_internal.h"
3#include "hl2h.h"
4#include <cmath>
5#include <cstdio>
6#include <cstring>
7
8static int g_aim_lock = -1;
9static int g_silent_lock = -1;
10
11void aimbot_reset_lock() { g_aim_lock = -1; }
12void silentaim_reset_lock() { g_silent_lock = -1; }
13
14// max_fov is the angular radius around the crosshair. If lock_idx is non-null
15// and valid, that target is kept while it is alive and visible (the "do not
16// switch while the bind is held" rule); a fresh acquisition is ranked by angle
17// and the nearest visible selected hitbox point.
18static bool select_aim_target(const QAngle &view, unsigned bone_mask,
19 bool vischeck, float max_fov, const char *tag,
20 int *lock_idx, int &out_idx, Vector &out_point,
21 float &out_fov) {
22 out_idx = -1;
23 out_point = {};
24 out_fov = 0.0f;
25 // maxs is optional: the per-target code falls back to 72 units when the
26 // netvar is missing, so it must not disable the whole aimbot here.
27 if (!g_iface.entitylist || g_net.origin < 0)
28 return false;
29 int local = engine_get_local_player();
30 if (local <= 0)
31 return false;
32 void *local_ent = entitylist_get(local);
33 if (!local_ent)
34 return false;
35 if (g_net.health >= 0 &&
36 *(int *)((uintptr_t)local_ent + g_net.health) <= 0)
37 return false;
38
39 // With the RPG equipped, armoured units (striders, gunships, dropships,
40 // hunter-choppers) are the ones ordinary shots cannot hurt, so prefer them
41 // over normal NPCs when acquiring.
42 bool rpg = active_weapon_contains(local_ent, "rocket_launcher") ||
43 active_weapon_contains(local_ent, "rpg");
44
45 // fov_limit <= 0 means "no angular limit" (kept for unlimited acquisition).
46 auto try_target = [&](int idx, float fov_limit, Vector &pt,
47 float &fov) -> bool {
48 if (idx <= 0 || idx == local)
49 return false;
50 void *ent = entitylist_get(idx);
51 if (!ent)
52 return false;
53 void *net = entity_networkable(idx);
54 if (!net || entity_is_dormant(net))
55 return false;
56 const char *cls = entity_class_name(net, nullptr);
57 if (!cls || !is_npc_class(cls))
58 return false;
59 uintptr_t e = (uintptr_t)ent;
60 int life = g_net.lifestate >= 0
61 ? (int)*(unsigned char *)(e + g_net.lifestate)
62 : -1;
63 if (life != LIFE_ALIVE)
64 return false;
65 const char *model = entity_model_name(ent);
66 bool neutral = false;
67 if (npc_is_friendly(idx, model, local, neutral) || neutral)
68 return false;
69 if (!npc_can_be_damaged(idx, model))
70 return false;
71 if (!bones_update(idx, ent))
72 return false;
73 Vector pts[32];
74 int npoints = aim_points(idx, bone_mask, pts, 32);
75 float best = 1e9f;
76 Vector best_pt{};
77 for (int i = 0; i < npoints; i++) {
78 float f = getfov(view, angle_to(g_cam_eye, pts[i]));
79 if (fov_limit > 0.0f && f > fov_limit)
80 continue;
81 if (f >= best)
82 continue;
83 if (vischeck && !is_visible(g_cam_eye, pts[i], local_ent, ent))
84 continue;
85 best = f;
86 best_pt = pts[i];
87 }
88 if (best > 1e8f)
89 return false;
90 pt = best_pt;
91 fov = best;
92 return true;
93 };
94
95 // Keep the locked target while it is alive and has a visible selected point.
96 // The lock uses a wider cone than acquisition (hysteresis), so a target that
97 // drifts slightly off the crosshair is not dropped, but one that swings
98 // behind the player is: an unbounded lock let silent aim fire backwards.
99 if (lock_idx && *lock_idx >= 0) {
100 Vector pt;
101 float f;
102 if (try_target(*lock_idx, max_fov * 1.5f, pt, f)) {
103 out_idx = *lock_idx;
104 out_point = pt;
105 out_fov = f;
106 static int lock_logs = 0;
107 if (lock_logs < 5) {
108 lock_logs++;
109 logd("%s: lock idx=%d fov=%.2f", tag, out_idx, f);
110 }
111 return true;
112 }
113 *lock_idx = -1;
114 }
115
116 using Fn = int (*)(void *);
117 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
118 if (hi < 0 || hi > 4096)
119 hi = 4096;
120
121 struct Candidate {
122 int idx;
123 float fov;
124 Vector approx;
125 bool armored;
126 float key;
127 };
128 Candidate cands[16];
129 int ncands = 0;
130
131 for (int idx = 1; idx <= hi; idx++) {
132 if (idx == local)
133 continue;
134 void *ent = entitylist_get(idx);
135 if (!ent)
136 continue;
137 void *net = entity_networkable(idx);
138 if (!net || entity_is_dormant(net))
139 continue;
140 const char *cls = entity_class_name(net, nullptr);
141 if (!cls || !is_npc_class(cls))
142 continue;
143
144 uintptr_t e = (uintptr_t)ent;
145 int life = g_net.lifestate >= 0
146 ? (int)*(unsigned char *)(e + g_net.lifestate)
147 : -1;
148 if (life != LIFE_ALIVE)
149 continue;
150 const char *model = entity_model_name(ent);
151 bool neutral = false;
152 if (npc_is_friendly(idx, model, local, neutral) || neutral)
153 continue;
154 if (!npc_can_be_damaged(idx, model))
155 continue;
156 bool armored = npc_damage_class(idx, model) == NPC_DMG_ARMORED;
157
158 float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f;
159 if (!(maxz > 4.0f && maxz < 512.0f))
160 maxz = 72.0f;
161 Vector origin{*(float *)(e + g_net.origin),
162 *(float *)(e + g_net.origin + 4),
163 *(float *)(e + g_net.origin + 8)};
164 Vector approx{origin.x, origin.y,
165 origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)};
166 float fov = getfov(view, angle_to(g_cam_eye, approx));
167 // The prefilter samples a single point, but the selected hitbox can be a
168 // whole body-width away at close range. Reject only when the target's
169 // angular radius cannot reach the cone; try_target still enforces the
170 // exact per-hitbox FOV.
171 float ddx = approx.x - g_cam_eye.x;
172 float ddy = approx.y - g_cam_eye.y;
173 float ddz = approx.z - g_cam_eye.z;
174 float dist = sqrtf(ddx * ddx + ddy * ddy + ddz * ddz);
175 float margin =
176 dist > 1.0f ? atan2f(maxz, dist) * 180.0f / PI_F : 90.0f;
177 if (fov - margin > max_fov)
178 continue;
179 float key = (rpg && armored) ? fov - 1000.0f : fov;
180
181 int pos = ncands;
182 while (pos > 0 && cands[pos - 1].key > key)
183 pos--;
184 if (ncands < 16)
185 ncands++;
186 for (int j = ncands - 1; j > pos; j--)
187 cands[j] = cands[j - 1];
188 if (pos < 16)
189 cands[pos] = {idx, fov, approx, armored, key};
190 }
191
192 if (ncands == 0) {
193 static double last_none = 0.0;
194 double now = esp_now();
195 if (now - last_none >= 2.0) {
196 last_none = now;
197 logd("%s: no hostile target within %.0f deg", tag, max_fov);
198 }
199 return false;
200 }
201
202 for (int ci = 0; ci < ncands; ci++) {
203 Vector pt;
204 float f;
205 if (!try_target(cands[ci].idx, max_fov, pt, f))
206 continue;
207 out_idx = cands[ci].idx;
208 out_point = pt;
209 out_fov = f;
210 if (lock_idx)
211 *lock_idx = out_idx;
212 static int sel_logs = 0;
213 if (sel_logs < 5) {
214 sel_logs++;
215 logd("%s: target idx=%d fov=%.2f vischeck=%d", tag, out_idx, f,
216 (int)vischeck);
217 }
218 return true;
219 }
220
221 static double last_vis = 0.0;
222 double now = esp_now();
223 if (now - last_vis >= 2.0) {
224 last_vis = now;
225 logd("%s: no visible aim point (vischeck=%d)", tag, (int)vischeck);
226 }
227 return false;
228}
229
230bool active_weapon_contains(void *local_ent, const char *needle) {
231 if (!local_ent || g_net.activeweapon < 0 || !needle)
232 return false;
233 uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon);
234 if (h == 0xFFFFFFFFu)
235 return false;
236 int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index
237 if (idx <= 0 || idx > 4096)
238 return false;
239 void *wep = entitylist_get(idx);
240 void *net = entity_networkable(idx);
241 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
242 const char *model = wep ? entity_model_name(wep) : nullptr;
243 return (cls && strcasestr(cls, needle)) ||
244 (model && strcasestr(model, needle));
245}
246
247// Crossbow bolts and RPG rockets are not hitscan. Lead moving targets by the
248// projectile travel time; the crossbow bolt also has gravity 0.05 (30 u/s^2).
249static bool projectile_lead_point(int target_idx, const Vector &point,
250 Vector &out) {
251 int local = engine_get_local_player();
252 if (local <= 0)
253 return false;
254 void *local_ent = entitylist_get(local);
255 if (!local_ent)
256 return false;
257 float speed = 0.0f, gravity = 0.0f;
258 if (active_weapon_contains(local_ent, "crossbow") ||
259 active_weapon_contains(local_ent, "Crossbow")) {
260 speed = 2500.0f;
261 gravity = 30.0f;
262 } else if (active_weapon_contains(local_ent, "rocket_launcher") ||
263 active_weapon_contains(local_ent, "w_rpg") ||
264 active_weapon_contains(local_ent, "RPG")) {
265 speed = 1500.0f;
266 gravity = 0.0f;
267 } else {
268 return false;
269 }
270 void *target = entitylist_get(target_idx);
271 if (!target)
272 return false;
273 // NPCs do not network C_BaseEntity::m_vecVelocity (only the player does), so
274 // the client field is stale for exactly the targets the aimbot uses. Read
275 // the listen-server entity when available and fall back to the client value.
276 float v[3] = {0.0f, 0.0f, 0.0f};
277 Vector srv_vel;
278 bool have_vel = false;
279 if (server_entity_velocity(target_idx, &srv_vel)) {
280 v[0] = srv_vel.x;
281 v[1] = srv_vel.y;
282 v[2] = srv_vel.z;
283 have_vel = true;
284 } else if (g_net.velocity >= 0) {
285 const float *cv = (const float *)((uintptr_t)target + g_net.velocity);
286 v[0] = cv[0];
287 v[1] = cv[1];
288 v[2] = cv[2];
289 have_vel = true;
290 }
291 if (!have_vel)
292 return false;
293 float dx = point.x - g_cam_eye.x;
294 float dy = point.y - g_cam_eye.y;
295 float dz = point.z - g_cam_eye.z;
296 float dist = sqrtf(dx * dx + dy * dy + dz * dz);
297 float t = dist / speed;
298 out = point;
299 out.x += v[0] * t;
300 out.y += v[1] * t;
301 out.z += v[2] * t + 0.5f * gravity * t * t;
302 return true;
303}
304
305// A led point is a prediction, not a verified aim point: acquisition checked
306// the un-led position, so the shift has to pass the same FOV and visibility
307// tests. On failure the caller keeps the original point instead of firing into
308// geometry or outside the configured cone.
309static bool lead_point_ok(int target_idx, const Vector &lead,
310 const QAngle &view, float max_fov) {
311 if (getfov(view, angle_to(g_cam_eye, lead)) > max_fov)
312 return false;
313 if (!aimbot_vischeck_enabled())
314 return true;
315 void *local_ent = entitylist_get(engine_get_local_player());
316 void *target_ent = entitylist_get(target_idx);
317 if (!local_ent || !target_ent)
318 return false;
319 return is_visible(g_cam_eye, lead, local_ent, target_ent);
320}
321
322// Semi-auto weapons (pistol, .357, crossbow, shotgun, RPG, gravity gun) only
323// fire on the IN_ATTACK press edge, so a synthetic button held every tick fires
324// exactly once per buy/equip. Alternate press/release for those; automatic
325// weapons keep the button held.
326static bool weapon_is_semi_auto(void *local_ent) {
327 static const char *names[] = {"pistol", "357",
328 "crossbow", "shotgun",
329 "rocket_launcher", "physcannon",
330 "crowbar", nullptr};
331 for (int i = 0; names[i]; i++)
332 if (active_weapon_contains(local_ent, names[i]))
333 return true;
334 return false;
335}
336
337static bool fire_edge_ok(CUserCmd *cmd) {
338 void *local_ent = entitylist_get(engine_get_local_player());
339 if (!local_ent || !weapon_is_semi_auto(local_ent))
340 return true;
341 static int last_tick = -1;
342 static bool phase = false;
343 if (cmd->tick_count != last_tick) {
344 last_tick = cmd->tick_count;
345 phase = !phase;
346 }
347 return phase;
348}
349
350bool aimbot_apply(CUserCmd *cmd) {
351 unsigned bone_mask = aimbot_bone_mask();
352 if (!bone_mask)
353 return false;
354 QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f};
355 float punch_x = 0.0f, punch_y = 0.0f;
356 bool punch = punch_angles(punch_x, punch_y);
357 if (punch) {
358 view.x += punch_x;
359 view.y += punch_y;
360 }
361 int target = -1;
362 Vector point{};
363 float fov = 0.0f;
364 float max_fov = (float)aimbot_fov();
365 if (max_fov < 1.0f) max_fov = 1.0f;
366 if (max_fov > 180.0f) max_fov = 180.0f;
367 int *lock = feature_has_bind(FEAT_AIMBOT) ? &g_aim_lock : nullptr;
368 if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov,
369 "aimbot", lock, target, point, fov))
370 return false;
371 Vector lead;
372 if (projectile_lead_point(target, point, lead) &&
373 lead_point_ok(target, lead, view, max_fov))
374 point = lead;
375 QAngle desired = angle_to(g_cam_eye, point);
376 if (punch) {
377 desired.x -= punch_x;
378 desired.y -= punch_y;
379 }
380 static int want_logs = 0;
381 if (want_logs < 10) {
382 want_logs++;
383 logd("aimbot: target=%d aim=(%.1f %.1f %.1f) fov=%.2f want=%.2f/%.2f",
384 target, point.x, point.y, point.z, fov, desired.x, desired.y);
385 }
386 float dp = desired.x - cmd->viewangles[0];
387 float dy = desired.y - cmd->viewangles[1];
388 while (dy > 180.0f) dy -= 360.0f;
389 while (dy < -180.0f) dy += 360.0f;
390
391 constexpr float DEADZONE = 0.05f;
392 float max_step = (float)aimbot_smooth_step();
393 if (max_step < 1.0f) max_step = 1.0f;
394 float maxd = fmaxf(fabsf(dp), fabsf(dy));
395 if (maxd <= DEADZONE)
396 return false;
397
398 float scale = (maxd > max_step) ? (max_step / maxd) : 1.0f;
399 float old_pitch = cmd->viewangles[0];
400 float old_yaw = cmd->viewangles[1];
401 float np = cmd->viewangles[0] + dp * scale;
402 float ny = cmd->viewangles[1] + dy * scale;
403 if (np > 89.0f) np = 89.0f;
404 if (np < -89.0f) np = -89.0f;
405 while (ny > 180.0f) ny -= 360.0f;
406 while (ny < -180.0f) ny += 360.0f;
407 cmd->viewangles[0] = np;
408 cmd->viewangles[1] = ny;
409
410 if (g_iface.engine) {
411 QAngle out{np, ny, 0.0f};
412 using SetViewFn = void (*)(void *, QAngle *);
413 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &out);
414 }
415 static int mod_logs = 0;
416 if (mod_logs < 10) {
417 mod_logs++;
418 logd("aimbot: view %.2f/%.2f -> %.2f/%.2f", old_pitch, old_yaw, np, ny);
419 }
420 return true;
421}
422
423// Keep the world-space movement wish direction unchanged when the usercmd
424// view angles are temporarily replaced by the silent-aim angles.
425static void preserve_movement(CUserCmd *cmd, float old_yaw, float new_yaw) {
426 float oy = old_yaw * PI_F / 180.0f;
427 float ay = new_yaw * PI_F / 180.0f;
428 float ofx = cosf(oy), ofy = sinf(oy);
429 float orx = sinf(oy), ory = -cosf(oy);
430 float afx = cosf(ay), afy = sinf(ay);
431 float arx = sinf(ay), ary = -cosf(ay);
432 float wx = ofx * cmd->forwardmove + orx * cmd->sidemove;
433 float wy = ofy * cmd->forwardmove + ory * cmd->sidemove;
434 cmd->forwardmove = wx * afx + wy * afy;
435 cmd->sidemove = wx * arx + wy * ary;
436}
437
438bool silent_aim_apply(CUserCmd *cmd) {
439 unsigned bone_mask = aimbot_bone_mask();
440 if (!bone_mask)
441 return false;
442 QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z};
443 QAngle view{orig.x, orig.y, 0.0f};
444 float punch_x = 0.0f, punch_y = 0.0f;
445 bool punch = punch_angles(punch_x, punch_y);
446 if (punch) {
447 view.x += punch_x;
448 view.y += punch_y;
449 }
450 int target = -1;
451 Vector point{};
452 float fov = 0.0f;
453 float max_fov = (float)aimbot_fov();
454 if (max_fov < 1.0f) max_fov = 1.0f;
455 if (max_fov > 180.0f) max_fov = 180.0f;
456 int *lock = feature_has_bind(FEAT_SILENTAIM) ? &g_silent_lock : nullptr;
457 if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov,
458 "silentaim", lock, target, point, fov))
459 return false;
460 Vector lead;
461 if (projectile_lead_point(target, point, lead) &&
462 lead_point_ok(target, lead, view, max_fov))
463 point = lead;
464 QAngle desired = angle_to(g_cam_eye, point);
465 if (punch) {
466 desired.x -= punch_x;
467 desired.y -= punch_y;
468 }
469 float np = desired.x;
470 if (np > 89.0f) np = 89.0f;
471 if (np < -89.0f) np = -89.0f;
472 float ny = desired.y;
473 while (ny > 180.0f) ny -= 360.0f;
474 while (ny < -180.0f) ny += 360.0f;
475
476 preserve_movement(cmd, orig.y, ny);
477
478 cmd->viewangles[0] = np;
479 cmd->viewangles[1] = ny;
480 cmd->viewangles[2] = orig.z;
481
482 // Restore now and again at FRAME_RENDER_START (after the engine consumed
483 // the aimed usercmd); the frame-stage restore is the one that sticks.
484 set_silent_restore(orig);
485 if (g_iface.engine) {
486 QAngle restore{orig.x, orig.y, orig.z};
487 using SetViewFn = void (*)(void *, QAngle *);
488 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore);
489 }
490 if (fire_edge_ok(cmd))
491 cmd->buttons |= IN_ATTACK;
492 static int logs = 0;
493 if (logs < 5) {
494 logs++;
495 logd("silentaim: fire idx=%d fov=%.2f aim=%.2f/%.2f", target, fov, np, ny);
496 }
497 return true;
498}
499
500// ---------------------------------------------------------------------------
501// melee killbot
502// ---------------------------------------------------------------------------
503//
504// The crowbar's CBaseHLBludgeonWeapon::Swing traces 75 units from
505// Weapon_ShootPosition along the view forward (CROWBAR_RANGE in
506// weapon_crowbar.h). It first tries a line, then a +/-16 hull backed off by
507// ~28 units, and ignores targets more than ~45 degrees off the aim direction.
508// We therefore only need to point the usercmd at the target for one command;
509// the rendered view is restored exactly like silent aim.
510
511static constexpr float MELEE_RANGE = 75.0f;
512
513static float vec_dist(const Vector &a, const Vector &b) {
514 float dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
515 return sqrtf(dx * dx + dy * dy + dz * dz);
516}
517
518bool melee_weapon_active(void *local_ent) {
519 if (!local_ent)
520 return false;
521 int local = engine_get_local_player();
522 if (g_iface.engineserver)
523 resolve_server_netvars();
524
525 auto check = [&](int idx, const char **cls_out,
526 const char **model_out) -> bool {
527 if (idx <= 0 || idx > 4096)
528 return false;
529 void *wep = entitylist_get(idx);
530 void *net = entity_networkable(idx);
531 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
532 const char *model = wep ? entity_model_name(wep) : nullptr;
533 // Client weapon models can be missing/0 after a map change; recover the
534 // server model index so the crowbar is still recognized.
535 if ((!model || !*model) && g_iface.engineserver) {
536 void *sbase = nullptr;
537 server_entity_health(idx, nullptr, nullptr, &sbase);
538 if (sbase)
539 model = model_name_from_index(server_model_index(sbase));
540 }
541 *cls_out = cls;
542 *model_out = model;
543 return (cls && strstr(cls, "Crowbar")) ||
544 (model && strstr(model, "crowbar"));
545 };
546
547 int idx = -1;
548 if (g_net.activeweapon >= 0) {
549 uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon);
550 if (h != 0xFFFFFFFFu)
551 idx = (int)(h & 0x7FF); // 11-bit entity index; upper bits are serial
552 }
553 const char *cls = nullptr, *model = nullptr;
554 bool crowbar = check(idx, &cls, &model);
555
556 if (!crowbar) {
557 // Fallback: the authoritative server player's m_hActiveWeapon handle.
558 // Also covers a client netvar offset that resolves but does not point at
559 // the weapon the server is actually using.
560 if (local > 0) {
561 resolve_server_netvars();
562 int si = server_active_weapon(local);
563 if (si > 0 && si != idx) {
564 idx = si;
565 crowbar = check(idx, &cls, &model);
566 }
567 }
568 }
569
570 static int last_idx = -2;
571 static bool last_crowbar = false;
572 if (idx != last_idx || crowbar != last_crowbar) {
573 last_idx = idx;
574 last_crowbar = crowbar;
575 logf("melee: active idx=%d cls=%s model=%s crowbar=%d", idx,
576 cls ? cls : "?", model ? model : "?", (int)crowbar);
577 if (!crowbar) {
578 static bool warned = false;
579 if (!warned) {
580 warned = true;
581 toastf(TOAST_WARN, "Melee killbot: equip the crowbar");
582 }
583 }
584 }
585 return crowbar;
586}
587
588bool gravity_gun_active(void *local_ent) {
589 if (!local_ent)
590 return false;
591 if (active_weapon_contains(local_ent, "physcannon"))
592 return true;
593 // Same server fallback as the crowbar: map transitions can leave the
594 // client activeweapon handle stale.
595 int local = engine_get_local_player();
596 if (local <= 0 || !g_iface.engineserver)
597 return false;
598 resolve_server_netvars();
599 int si = server_active_weapon(local);
600 if (si <= 0)
601 return false;
602 void *wep = entitylist_get(si);
603 void *net = entity_networkable(si);
604 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
605 const char *model = wep ? entity_model_name(wep) : nullptr;
606 if (!model) {
607 void *sbase = nullptr;
608 server_entity_health(si, nullptr, nullptr, &sbase);
609 if (sbase)
610 model = model_name_from_index(server_model_index(sbase));
611 }
612 return (cls && strcasestr(cls, "physcannon")) ||
613 (model && strcasestr(model, "physcannon"));
614}
615
616// The citadel supercharges the gravity gun (d3_citadel_03 onward). It can then
617// punt any NPC, so the melee aimbot should not restrict itself to
618// physics-vulnerable units.
619static bool supercharged_gravity_gun() {
620 if (!g_iface.engine)
621 return false;
622 using Fn = const char *(*)(void *);
623 const char *m = vfunc<Fn>(g_iface.engine, 51)(g_iface.engine);
624 if (!m)
625 return false;
626 bool supe = strstr(m, "d3_citadel_03") || strstr(m, "d3_citadel_04") ||
627 strstr(m, "d3_breen_01");
628 static const char *logged_map = nullptr;
629 if (logged_map != m) {
630 logged_map = m;
631 logf("melee: gravity gun map=%s supercharged=%d", m, (int)supe);
632 }
633 return supe;
634}
635
636bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) {
637 if (!g_iface.entitylist || g_net.origin < 0)
638 return false;
639 int local = engine_get_local_player();
640 if (local <= 0)
641 return false;
642 void *local_ent = entitylist_get(local);
643 if (!local_ent)
644 return false;
645 if (g_net.health >= 0 &&
646 *(int *)((uintptr_t)local_ent + g_net.health) <= 0)
647 return false;
648
649 // The punted object can travel much farther than the crowbar.
650 float max_range = gravity ? 1200.0f : MELEE_RANGE;
651 bool supercharged = gravity && supercharged_gravity_gun();
652
653 using Fn = int (*)(void *);
654 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
655 if (hi < 0 || hi > 4096)
656 hi = 4096;
657
658 struct Cand {
659 int idx;
660 float dist;
661 Vector closest;
662 Vector center;
663 };
664 Cand cands[16];
665 int n = 0;
666
667 for (int idx = 1; idx <= hi; idx++) {
668 if (idx == local)
669 continue;
670 void *ent = entitylist_get(idx);
671 if (!ent)
672 continue;
673 void *net = entity_networkable(idx);
674 if (!net || entity_is_dormant(net))
675 continue;
676 const char *cls = entity_class_name(net, nullptr);
677 if (!cls || !is_npc_class(cls))
678 continue;
679 uintptr_t e = (uintptr_t)ent;
680 int life = g_net.lifestate >= 0
681 ? (int)*(unsigned char *)(e + g_net.lifestate)
682 : -1;
683 if (life != LIFE_ALIVE)
684 continue;
685 const char *model = entity_model_name(ent);
686 bool neutral = false;
687 if (npc_is_friendly(idx, model, local, neutral) || neutral)
688 continue;
689 if (!npc_can_be_damaged(idx, model))
690 continue;
691 // The normal gravity gun is only the tool for physics-vulnerable units;
692 // the citadel version behaves like a normal weapon.
693 if (gravity && !supercharged &&
694 npc_damage_class(idx, model) != NPC_DMG_VULNERABLE)
695 continue;
696
697 Vector origin{*(float *)(e + g_net.origin),
698 *(float *)(e + g_net.origin + 4),
699 *(float *)(e + g_net.origin + 8)};
700 float minx = -16.0f, miny = -16.0f, minz = 0.0f;
701 float maxx = 16.0f, maxy = 16.0f, maxz = 72.0f;
702 if (g_net.mins >= 0) {
703 minx = *(float *)(e + g_net.mins);
704 miny = *(float *)(e + g_net.mins + 4);
705 minz = *(float *)(e + g_net.mins + 8);
706 }
707 if (g_net.maxs >= 0) {
708 maxx = *(float *)(e + g_net.maxs);
709 maxy = *(float *)(e + g_net.maxs + 4);
710 maxz = *(float *)(e + g_net.maxs + 8);
711 }
712 // Reject absurd boxes in both directions; a garbage mins value that is
713 // hugely negative would otherwise produce an AABB spanning the map.
714 if (!(minz > -512.0f && maxz > minz + 4.0f && maxz < 512.0f)) {
715 minz = 0.0f;
716 maxz = 72.0f;
717 }
718 if (!(minx > -512.0f && maxx > minx + 1.0f && maxx < 256.0f)) {
719 minx = -16.0f;
720 maxx = 16.0f;
721 }
722 if (!(miny > -512.0f && maxy > miny + 1.0f && maxy < 256.0f)) {
723 miny = -16.0f;
724 maxy = 16.0f;
725 }
726
727 Vector lo{origin.x + minx, origin.y + miny, origin.z + minz};
728 Vector hiV{origin.x + maxx, origin.y + maxy, origin.z + maxz};
729 Vector closest{g_cam_eye.x, g_cam_eye.y, g_cam_eye.z};
730 if (closest.x < lo.x) closest.x = lo.x;
731 if (closest.x > hiV.x) closest.x = hiV.x;
732 if (closest.y < lo.y) closest.y = lo.y;
733 if (closest.y > hiV.y) closest.y = hiV.y;
734 if (closest.z < lo.z) closest.z = lo.z;
735 if (closest.z > hiV.z) closest.z = hiV.z;
736 float d = vec_dist(g_cam_eye, closest);
737 if (d > max_range)
738 continue;
739
740 Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f,
741 (lo.z + hiV.z) * 0.5f};
742
743 int pos = n;
744 while (pos > 0 && cands[pos - 1].dist > d)
745 pos--;
746 if (n < 16)
747 n++;
748 else if (pos >= 16)
749 continue;
750 for (int j = n - 1; j > pos; j--)
751 cands[j] = cands[j - 1];
752 cands[pos] = {idx, d, closest, center};
753 }
754
755 for (int ci = 0; ci < n; ci++) {
756 const Cand &c = cands[ci];
757 Vector aim{};
758 bool have_aim = false;
759
760 // Prefer the nearest visible hitbox center. This handles small NPCs
761 // (headcrabs, antlions) whose AABB center can be less useful than the
762 // actual body hitboxes.
763 void *tent = entitylist_get(c.idx);
764 if (tent && bones_update(c.idx, tent)) {
765 Vector pts[32];
766 int np = aim_points(c.idx, 0xFE, pts, 32);
767 float bestd = 1e9f;
768 for (int i = 0; i < np; i++) {
769 float pd = vec_dist(g_cam_eye, pts[i]);
770 if (pd > max_range || pd >= bestd)
771 continue;
772 if (!is_visible(g_cam_eye, pts[i], local_ent, tent))
773 continue;
774 bestd = pd;
775 aim = pts[i];
776 have_aim = true;
777 }
778 }
779
780 if (!have_aim) {
781 aim = c.center;
782 float ad = vec_dist(g_cam_eye, aim);
783 if (ad > max_range) {
784 // The body is in range but its center is not: aim just inside the
785 // closest point on the AABB.
786 aim = c.closest;
787 aim.x += (c.center.x - c.closest.x) * 0.25f;
788 aim.y += (c.center.y - c.closest.y) * 0.25f;
789 aim.z += (c.center.z - c.closest.z) * 0.25f;
790 ad = vec_dist(g_cam_eye, aim);
791 if (ad > max_range)
792 aim = c.closest;
793 }
794 if (!is_visible(g_cam_eye, aim, local_ent, tent))
795 continue;
796 }
797
798 QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z};
799 QAngle desired = angle_to(g_cam_eye, aim);
800 // The crowbar uses GetAutoaimVector (adds punch); the gravity gun's
801 // launch uses EyeVectors (no punch), so only compensate for melee.
802 float punch_x = 0.0f, punch_y = 0.0f;
803 if (!gravity && punch_angles(punch_x, punch_y)) {
804 desired.x -= punch_x;
805 desired.y -= punch_y;
806 }
807 float np = desired.x;
808 if (np > 89.0f) np = 89.0f;
809 if (np < -89.0f) np = -89.0f;
810 float ny = desired.y;
811 while (ny > 180.0f) ny -= 360.0f;
812 while (ny < -180.0f) ny += 360.0f;
813
814 preserve_movement(cmd, orig.y, ny);
815 cmd->viewangles[0] = np;
816 cmd->viewangles[1] = ny;
817 cmd->viewangles[2] = orig.z;
818 set_silent_restore(orig);
819 if (g_iface.engine) {
820 QAngle restore{orig.x, orig.y, orig.z};
821 using SetViewFn = void (*)(void *, QAngle *);
822 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore);
823 }
824 // Gravity gun primary attack launches the held object (EyeVectors);
825 // secondary would drop it instead.
826 if (fire_edge_ok(cmd))
827 cmd->buttons |= IN_ATTACK;
828
829 static int logs = 0;
830 if (logs < 10) {
831 logs++;
832 logd("melee: %s idx=%d dist=%.1f aim=(%.1f %.1f %.1f)",
833 gravity ? "punt" : "swing", c.idx, c.dist, aim.x, aim.y, aim.z);
834 }
835 return true;
836 }
837
838 static double last_none = 0.0;
839 double now = esp_now();
840 if (now - last_none >= 2.0) {
841 last_none = now;
842 logd("melee: no visible target within %.0f", max_range);
843 }
844 return false;
845}
846
847// ---------------------------------------------------------------------------
848// weapon spread control
849// ---------------------------------------------------------------------------
850//
851// HL2's CBaseEntity::FireBullets uses CBaseEntity::GetPredictionRandomSeed()
852// (the client usercmd's random_seed) unless a weapon opts into
853// m_bUseServerRandomSeed. HL2 weapons do not, so the client can choose the
854// 8-bit seed and with it the random cone offset. CShotManipulator::ApplySpread
855// draws four uniform floats per attempt; we precompute the best seed for one
856// shot and for the shotgun's pellet windows with the engine's own RNG.
857
858static bool g_spread_ready = false;
859static int g_spread_seed1 = 0;
860static int g_spread_seed8 = 0; // first pellet exact, 7 spread
861static int g_spread_seed12 = 0; // all 12 spread
862int g_spread_pellets = 1;
863
864static void spread_compute() {
865 using SeedFn = void (*)(int);
866 using FloatFn = float (*)(float, float);
867 SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed");
868 FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat");
869 if (!seed || !rf) {
870 size_t slash = mod_engine().path.find_last_of('/');
871 if (slash != std::string::npos) {
872 std::string path = mod_engine().path.substr(0, slash + 1) + "libvstdlib.so";
873 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
874 if (!h)
875 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
876 if (h) {
877 seed = (SeedFn)dlsym(h, "RandomSeed");
878 rf = (FloatFn)dlsym(h, "RandomFloat");
879 }
880 }
881 }
882 if (!seed || !rf) {
883 logf("spread: RandomSeed/RandomFloat unavailable");
884 return;
885 }
886
887 auto draw = [&](int s, float &x, float &y) {
888 seed(s);
889 int it = 0;
890 do {
891 // CShotManipulator::ApplySpread with the default bias of 1.0:
892 // shotBias = 1.0, flatness = 0.5.
893 x = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f;
894 y = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f;
895 it++;
896 } while (x * x + y * y > 1.0f && it < 64);
897 };
898
899 float best1 = 1e9f;
900 for (int s = 0; s < 256; s++) {
901 float x, y;
902 draw(s, x, y);
903 float r2 = x * x + y * y;
904 if (r2 < best1) {
905 best1 = r2;
906 g_spread_seed1 = s;
907 }
908 }
909
910 auto window = [&](int n, int first) {
911 int best_s = 0;
912 float best = 1e9f;
913 for (int s = 0; s < 256; s++) {
914 float score = 0.0f;
915 for (int i = first; i < n; i++) {
916 float x, y;
917 draw(s + i, x, y);
918 score += x * x + y * y;
919 }
920 if (score < best) {
921 best = score;
922 best_s = s;
923 }
924 }
925 return best_s;
926 };
927 g_spread_seed8 = window(8, 1);
928 g_spread_seed12 = window(12, 0);
929 seed(0);
930
931 logd("spread: seeds single=%d shotgun8=%d shotgun12=%d", g_spread_seed1,
932 g_spread_seed8, g_spread_seed12);
933 g_spread_ready = true;
934}
935
936static bool active_weapon_is_shotgun(void *local_ent) {
937 if (!local_ent || g_net.activeweapon < 0)
938 return false;
939 uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon);
940 if (h == 0xFFFFFFFFu)
941 return false;
942 int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index
943 if (idx <= 0 || idx > 4096)
944 return false;
945 void *wep = entitylist_get(idx);
946 void *net = entity_networkable(idx);
947 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
948 const char *model = wep ? entity_model_name(wep) : nullptr;
949 return (cls && strstr(cls, "Shotgun")) ||
950 (model && strstr(model, "shotgun"));
951}
952
953bool spread_control_apply(CUserCmd *cmd, void *local_ent) {
954 if (!feature_active(FEAT_SPREAD) || !spread_no_spread_enabled())
955 return false;
956 if (!(cmd->buttons & (IN_ATTACK | IN_ATTACK2)))
957 return false;
958 if (!g_spread_ready) {
959 static bool tried = false;
960 if (!tried) {
961 tried = true;
962 spread_compute();
963 }
964 }
965 if (!g_spread_ready)
966 return false;
967
968 bool shotgun = active_weapon_is_shotgun(local_ent);
969 int pellets = 1;
970 if (shotgun)
971 pellets = (cmd->buttons & IN_ATTACK2) ? 12 : 8;
972 g_spread_pellets = pellets;
973 int seed = spread_best_seed(pellets);
974 if (cmd->random_seed == seed)
975 return false;
976 cmd->random_seed = seed;
977
978 static int logs = 0;
979 if (logs < 10) {
980 logs++;
981 logd("spread: seed=%d pellets=%d", seed, pellets);
982 }
983 return true;
984}
985
986int spread_best_seed(int pellets) {
987 if (!g_spread_ready)
988 return 33;
989 if (pellets >= 12)
990 return g_spread_seed12;
991 if (pellets >= 8)
992 return g_spread_seed8;
993 return g_spread_seed1;
994}
995
996bool triggerbot_apply(CUserCmd *cmd) {
997 if (!g_iface.enginetrace || !g_iface.entitylist)
998 return false;
999 int local = engine_get_local_player();
1000 if (local <= 0)
1001 return false;
1002 void *local_ent = entitylist_get(local);
1003 if (!local_ent)
1004 return false;
1005 if (g_net.health >= 0 &&
1006 *(int *)((uintptr_t)local_ent + g_net.health) <= 0)
1007 return false;
1008
1009 QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f};
1010 float punch_x = 0.0f, punch_y = 0.0f;
1011 if (punch_angles(punch_x, punch_y)) {
1012 view.x += punch_x;
1013 view.y += punch_y;
1014 }
1015 Vector fwd = angle_forward_vector(view);
1016 Vector start = g_cam_eye;
1017 Vector end{start.x + fwd.x * 8192.0f, start.y + fwd.y * 8192.0f,
1018 start.z + fwd.z * 8192.0f};
1019 void *hit = trace_entity(start, end, local_ent);
1020 if (!hit)
1021 return false;
1022
1023 int idx = entptr_lookup(hit);
1024 if (idx <= 0) {
1025 // Cache miss: ESP is off, or this entity appeared since the last scan.
1026 using Fn = int (*)(void *);
1027 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
1028 if (hi < 0 || hi > 4096)
1029 hi = 4096;
1030 for (int i = 1; i <= hi; i++) {
1031 if (entitylist_get(i) != hit)
1032 continue;
1033 idx = i;
1034 entptr_store(hit, i);
1035 break;
1036 }
1037 if (idx <= 0)
1038 return false;
1039 }
1040 void *net = entity_networkable(idx);
1041 if (entity_is_dormant(net))
1042 return false;
1043 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
1044 if (!cls || !is_npc_class(cls))
1045 return false;
1046 uintptr_t e = (uintptr_t)hit;
1047 int life = g_net.lifestate >= 0
1048 ? (int)*(unsigned char *)(e + g_net.lifestate)
1049 : -1;
1050 if (life != LIFE_ALIVE)
1051 return false;
1052 const char *model = entity_model_name(hit);
1053 bool neutral = false;
1054 if (npc_is_friendly(idx, model, local, neutral) || neutral)
1055 return false;
1056 if (!npc_can_be_damaged(idx, model))
1057 return false;
1058 if (fire_edge_ok(cmd))
1059 cmd->buttons |= IN_ATTACK;
1060 static int logs = 0;
1061 if (logs < 5) {
1062 logs++;
1063 logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?");
1064 }
1065 return true;
1066}
diff --git a/src/entity_query.cpp b/src/entity_query.cpp
new file mode 100644
index 0000000..4697bf2
--- /dev/null
+++ b/src/entity_query.cpp
@@ -0,0 +1,460 @@
1// entity_query.cpp — client entity lookups, NPC classification, per-map caches.
2#include "esp_internal.h"
3#include "hl2h.h"
4#include <cmath>
5#include <cstring>
6
7using GetNetFn = void *(*)(void *, int);
8using GetCCFn = void *(*)(void *);
9using GetModelFn = const void *(*)(void *, int);
10using GetModelNameFn = const char *(*)(void *, const void *);
11
12// Listen-server sanity latch. Only ever set once per map (esp_reset_session),
13// and only after both model indices are valid: latching on the transient
14// cli_model == 0 at map load disabled server health for the whole process.
15bool g_srv_checked = false;
16bool g_srv_trusted = false;
17
18// Per-map caches. The ESP resolves a model name for every entity on every scan
19// and the chams hook asks for a relation on every model draw; both are virtual
20// call chains into the engine plus server datamap reads, so memoise them.
21static constexpr int MODEL_CACHE = 16384;
22static const char *g_model_name_cache[MODEL_CACHE];
23static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail
24
25// Direct-mapped entity pointer -> index map, filled by esp_collect. The
26// triggerbot traces a ray and gets a pointer back, and used to find its index
27// by walking up to 4096 entity-list slots on every fire tick.
28static constexpr int ENTPTR_CACHE = 4096;
29static void *g_entptr_key[ENTPTR_CACHE];
30static int g_entptr_idx[ENTPTR_CACHE];
31
32void esp_reset_session() {
33 g_srv_checked = false;
34 g_srv_trusted = false;
35 memset(g_model_name_cache, 0, sizeof(g_model_name_cache));
36 memset(g_model_state_cache, 0, sizeof(g_model_state_cache));
37 memset(g_entptr_key, 0, sizeof(g_entptr_key));
38 memset(g_entptr_idx, 0, sizeof(g_entptr_idx));
39}
40
41void *entity_networkable(int idx) {
42 return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx);
43}
44
45// IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8.
46const char *entity_class_name(void *net, void **out_cc) {
47 if (!net) return nullptr;
48 void *cc = vfunc<GetCCFn>(net, 2)(net);
49 if (out_cc) *out_cc = cc;
50 return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr;
51}
52
53// IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant
54// 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK,
55// so the slot is validated once against the local player, which can never be
56// dormant: a wrong slot disables the check instead of misreporting entities.
57// Dormant entities keep their last networked origin, so drawing or aiming at
58// them produces ghosts at stale positions.
59static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled
60
61static void dormant_selftest() {
62 // Leave the state untested (0) until a local player exists: the first call
63 // can come from the main menu, and latching "disabled" there would turn the
64 // check off for the whole session.
65 if (!g_iface.entitylist)
66 return;
67 int local = engine_get_local_player();
68 if (local <= 0)
69 return;
70 void *net = entity_networkable(local);
71 if (!net)
72 return;
73 void **vt = *(void ***)net;
74 if (!vt || !mod_client().contains((uintptr_t)vt[8])) {
75 g_dormant_state = -1;
76 logf("dormant check disabled: slot 8 outside client module");
77 return;
78 }
79 using DormantFn = unsigned char (*)(void *);
80 if (vfunc<DormantFn>(net, 8)(net) != 0) {
81 g_dormant_state = -1;
82 logf("dormant check disabled: local player reported dormant");
83 return;
84 }
85 g_dormant_state = 1;
86 logd("dormant check enabled");
87}
88
89bool entity_is_dormant(void *net) {
90 if (!net)
91 return false;
92 if (g_dormant_state == 0)
93 dormant_selftest();
94 if (g_dormant_state != 1)
95 return false;
96 using DormantFn = unsigned char (*)(void *);
97 return vfunc<DormantFn>(net, 8)(net) != 0;
98}
99
100// Entity pointer -> index, filled by the entity scans. The triggerbot gets a
101// pointer back from the trace and needs the index; the alternative is walking
102// up to 4096 entity-list slots on every fire tick.
103int entptr_lookup(void *ent) {
104 if (!ent)
105 return -1;
106 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
107 return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1;
108}
109
110void entptr_store(void *ent, int idx) {
111 if (!ent || idx <= 0)
112 return;
113 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
114 g_entptr_key[h] = ent;
115 g_entptr_idx[h] = idx;
116}
117
118// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName.
119// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4.
120const char *model_name_from_index(int modelindex) {
121 if (!g_iface.modelinfo || modelindex <= 0)
122 return nullptr;
123 if (modelindex < MODEL_CACHE) {
124 if (g_model_state_cache[modelindex] == 1)
125 return g_model_name_cache[modelindex];
126 if (g_model_state_cache[modelindex] == 2)
127 return nullptr;
128 }
129 const void *model =
130 vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex);
131 if (!model) {
132 if (modelindex < MODEL_CACHE)
133 g_model_state_cache[modelindex] = 2;
134 return nullptr;
135 }
136 const char *name =
137 vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model);
138 if (modelindex < MODEL_CACHE) {
139 g_model_name_cache[modelindex] = name;
140 g_model_state_cache[modelindex] = name ? 1 : 2;
141 }
142 return name;
143}
144
145const char *entity_model_name(void *ent) {
146 if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr;
147 int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex);
148 return model_name_from_index(modelindex);
149}
150
151// Live origin for a cached entity index. Item/projectile discovery is throttled
152// to 10 Hz (it walks the whole entity list), but the markers must follow the
153// world at render rate; refreshing one netvar read per cached target is cheap.
154bool entity_origin(void *ent, Vector &out) {
155 if (!ent || g_net.origin < 0)
156 return false;
157 uintptr_t e = (uintptr_t)ent;
158 out.x = *(float *)(e + g_net.origin);
159 out.y = *(float *)(e + g_net.origin + 4);
160 out.z = *(float *)(e + g_net.origin + 8);
161 return true;
162}
163
164// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all
165// use the single client class "CAI_BaseNPC" — only special NPCs register their
166// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...).
167bool is_npc_class(const char *name) {
168 if (!name) return false;
169 return strstr(name, "NPC") != nullptr ||
170 strstr(name, "BaseHelicopter") != nullptr;
171}
172
173// Used by the chams hook to decide whether a model-render entity is an NPC.
174bool esp_is_npc_index(int idx) {
175 if (!g_iface.entitylist || idx <= 0 || idx > 8192)
176 return false;
177 void *net = entity_networkable(idx);
178 if (!net)
179 return false;
180 return is_npc_class(entity_class_name(net, nullptr));
181}
182
183bool esp_is_viewmodel_index(int idx) {
184 if (!g_iface.entitylist || idx <= 0 || idx > 8192)
185 return false;
186 void *ent = entitylist_get(idx);
187 if (!ent)
188 return false;
189 void *net = entity_networkable(idx);
190 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
191 if (cls && strcasestr(cls, "viewmodel"))
192 return true;
193 const char *model = entity_model_name(ent);
194 return model && (strstr(model, "/v_") || strstr(model, "\\v_"));
195}
196
197// 0 = hostile, 1 = neutral, 2 = friendly, -1 = not an NPC.
198bool npc_is_friendly(int idx, const char *model, int local_idx,
199 bool &neutral);
200int esp_npc_relation(int idx) {
201 // Called from the chams hook up to three times per entity per frame (visible,
202 // through-wall, wireframe pass) and the work behind it is a chain of server
203 // edict/datamap reads. A short TTL cache keeps that out of the render path.
204 struct RelCache {
205 int idx;
206 double at;
207 int val;
208 };
209 static RelCache cache[256];
210 double now = esp_now();
211 RelCache &c = cache[(unsigned)idx & 255];
212 if (c.idx == idx && now - c.at < 0.05)
213 return c.val;
214
215 int val = -1;
216 if (esp_is_npc_index(idx)) {
217 int local = engine_get_local_player();
218 if (local <= 0) {
219 val = 0;
220 } else {
221 void *ent = entitylist_get(idx);
222 const char *model = ent ? entity_model_name(ent) : nullptr;
223 bool neutral = false;
224 bool friendly = npc_is_friendly(idx, model, local, neutral);
225 val = neutral ? 1 : friendly ? 2 : 0;
226 }
227 }
228 c = {idx, now, val};
229 return val;
230}
231
232static bool model_is_friendly(const char *m) {
233 static const char *patterns[] = {
234 "citizen", "humans/", "male_", "female_", "barney", "alyx",
235 "alex", "scientist", "kleiner", "eli", "mossman", "vortigaunt",
236 "dog", "grigori", "monk", "odessa", "gman", nullptr};
237 if (!m) return false;
238 for (int i = 0; patterns[i]; i++)
239 if (strstr(m, patterns[i]))
240 return true;
241 return false;
242}
243
244// harmless wildlife that also happens to be CAI_BaseNPC
245static bool model_is_neutral(const char *m) {
246 // Lamarr is Kleiner's pet headcrab: scripted and harmless, and the user
247 // wants her shown neutral rather than hostile.
248 static const char *patterns[] = {"crow", "gull", "pigeon", "seagull",
249 "bird", "Lamarr", nullptr};
250 if (!m) return false;
251 for (int i = 0; patterns[i]; i++)
252 if (strstr(m, patterns[i]))
253 return true;
254 return false;
255}
256
257static bool model_is_floor_turret(const char *m) {
258 return m && strstr(m, "floor_turret");
259}
260
261static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx,
262 bool &neutral) {
263 int rel = server_npc_relation(idx, local_idx);
264 int state = server_npc_state(idx);
265 int turret = -1;
266 // Citizen-built and Alyx-hacked turrets never add an m_Relationship row;
267 // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state.
268 if (model_is_floor_turret(model))
269 turret = server_npc_turret_flags(idx);
270 // Debug-only: relation/state changes are the authoritative signals and the
271 // first thing to check when a friendly NPC still looks hostile.
272 static int rel_cache[2048];
273 static int state_cache[2048];
274 static bool cache_init = false;
275 if (!cache_init) {
276 for (int i = 0; i < 2048; i++)
277 rel_cache[i] = state_cache[i] = -999;
278 cache_init = true;
279 }
280 if (idx > 0 && idx < 2048 &&
281 (rel_cache[idx] != rel || state_cache[idx] != state)) {
282 rel_cache[idx] = rel;
283 state_cache[idx] = state;
284 logd("npc %d (%s): relation=%d state=%d followtarget=%d turret=%d "
285 "local=%d", idx, model ? model : "?", rel, state,
286 (int)server_npc_has_follow_target(idx), turret, local_idx);
287 }
288 if (rel == 3) { // D_LI
289 neutral = false;
290 return true;
291 }
292 if (turret > 0) { // citizen-built / Alyx-hacked
293 neutral = false;
294 return true;
295 }
296 if (rel == 4) { // D_NU
297 neutral = true;
298 return false;
299 }
300 // NPC_STATE_SCRIPT = 4: cutscene actors and scripted pets are passive even
301 // when their class relationship says hate (Lamarr, scripted Combine, ...).
302 if (state == 4) {
303 neutral = true;
304 return false;
305 }
306 if (rel == 1 || rel == 2) { // D_HT / D_FR
307 neutral = false;
308 return false;
309 }
310 if (turret >= 0) { // ordinary floor turret
311 neutral = false;
312 return false;
313 }
314 // Bugbait-controlled antlions follow a target even when their default
315 // relationship was never rewritten.
316 if (model && strstr(model, "antlion") &&
317 server_npc_has_follow_target(idx)) {
318 neutral = false;
319 return true;
320 }
321 bool friendly = model_is_friendly(model);
322 neutral = model_is_neutral(model);
323 return friendly;
324}
325
326// Friendly/neutral state changes slowly (hacked turrets, bugbait), while the
327// entity scan and the chams hook ask for it constantly. Cache it briefly so the
328// server lookups happen once per NPC per window instead of per call.
329bool npc_is_friendly(int idx, const char *model, int local_idx,
330 bool &neutral) {
331 struct Cache {
332 int idx;
333 double at;
334 bool friendly;
335 bool neutral;
336 };
337 static Cache cache[256];
338 double now = esp_now();
339 Cache &c = cache[(unsigned)idx & 255];
340 if (c.idx == idx && now - c.at < 0.10) {
341 neutral = c.neutral;
342 return c.friendly;
343 }
344 bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral);
345 c = {idx, now, f, neutral};
346 return f;
347}
348
349
350static bool model_has(const char *model, const char *needle) {
351 return model && strcasestr(model, needle) != nullptr;
352}
353
354// Server m_takedamage first (authoritative immunity), then the HL2 damage
355// rules for units whose OnTakeDamage filters specific damage types.
356static int npc_damage_class_uncached(int idx, const char *model) {
357 int td = server_npc_takedamage(idx);
358 if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY
359 return NPC_DMG_IMMUNE;
360 if (model_has(model, "gunship") || model_has(model, "combine_strider") ||
361 model_has(model, "dropship") || model_has(model, "helicopter") ||
362 model_has(model, "chopper"))
363 return NPC_DMG_ARMORED;
364 if (model_has(model, "headcrab") || model_has(model, "antlion") ||
365 model_has(model, "manhack"))
366 return NPC_DMG_VULNERABLE;
367 return NPC_DMG_NORMAL;
368}
369
370// Damage class depends on m_takedamage (a server read) plus model string
371// matching. It changes even more rarely than the relation, so cache it.
372int npc_damage_class(int idx, const char *model) {
373 struct Cache {
374 int idx;
375 double at;
376 int val;
377 };
378 static Cache cache[256];
379 double now = esp_now();
380 Cache &c = cache[(unsigned)idx & 255];
381 if (c.idx == idx && now - c.at < 0.25)
382 return c.val;
383 int v = npc_damage_class_uncached(idx, model);
384 c = {idx, now, v};
385 return v;
386}
387
388bool active_weapon_contains(void *local_ent, const char *needle);
389
390// Armoured units need blast damage; the RPG is the only stock weapon that
391// passes their OnTakeDamage filters. Immune units can never be damaged.
392bool npc_can_be_damaged(int idx, const char *model) {
393 int dc = npc_damage_class(idx, model);
394 if (dc == NPC_DMG_IMMUNE)
395 return false;
396 if (dc != NPC_DMG_ARMORED)
397 return true;
398 int local = engine_get_local_player();
399 void *le = local > 0 ? entitylist_get(local) : nullptr;
400 return active_weapon_contains(le, "rocket_launcher") ||
401 active_weapon_contains(le, "rpg");
402}
403
404void short_name(const char *model, char *out, size_t outn) {
405 if (!model) { snprintf(out, outn, "NPC"); return; }
406 const char *base = strrchr(model, '/');
407 base = base ? base + 1 : model;
408 size_t n = strcspn(base, ".");
409 if (n >= outn) n = outn - 1;
410 memcpy(out, base, n);
411 out[n] = 0;
412}
413
414// Enemy weapon label. NPC weapons are server entities, so read the server
415// model index first and fall back to the client class name.
416void weapon_short_name(int idx, char *out, size_t outn) {
417 out[0] = 0;
418 int wi = server_active_weapon(idx);
419 if (wi <= 0)
420 return;
421 const char *name = nullptr;
422 void *base = server_entity_base(wi);
423 int mi = base ? server_model_index(base) : -1;
424 if (mi > 0)
425 name = model_name_from_index(mi);
426 if (!name) {
427 void *wep = entitylist_get(wi);
428 if (wep) {
429 void *net = entity_networkable(wi);
430 name = entity_class_name(net, nullptr);
431 }
432 }
433 if (!name)
434 name = server_entity_classname(wi);
435 if (!name)
436 return;
437 static const struct {
438 const char *needle;
439 const char *label;
440 } map[] = {
441 {"smg1", "SMG1"}, {"irifle", "AR2"}, {"ar2", "AR2"},
442 {"shotgun", "Shotgun"}, {"pistol", "Pistol"}, {"357", "357"},
443 {"crossbow", "Crossbow"}, {"rocket", "RPG"}, {"rpg", "RPG"},
444 {"grenade", "Grenade"}, {"frag", "Grenade"}, {"crowbar", "Crowbar"},
445 {"physcannon", "Gravity"}, {"slam", "SLAM"}, {"bugbait", "Bugbait"},
446 {"manhack", "Manhack"}, {"turret", "Turret"}, {"annabelle", "Rifle"},
447 {"alyxgun", "Alyx gun"}, {"sniper", "Sniper"}, {"smg", "SMG"},
448 };
449 for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) {
450 if (strcasestr(name, map[i].needle)) {
451 snprintf(out, outn, "%s", map[i].label);
452 return;
453 }
454 }
455 // Last resort: the short class/model name.
456 short_name(name, out, outn);
457}
458
459// Hostile / neutral / friendly colours come from the ESP colour pickers.
460
diff --git a/src/esp.cpp b/src/esp.cpp
index 3f21dce..9046d22 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -8,7 +8,7 @@
8#include <cstdlib> 8#include <cstdlib>
9#include <ctime> 9#include <ctime>
10 10
11enum { LIFE_ALIVE = 0 }; 11
12 12
13struct EspTarget { 13struct EspTarget {
14 int idx; 14 int idx;
@@ -35,35 +35,6 @@ static int g_ntargets = 0;
35int g_scr_w = 0, g_scr_h = 0; 35int g_scr_w = 0, g_scr_h = 0;
36static bool g_esp_logged = false; 36static bool g_esp_logged = false;
37 37
38// Listen-server sanity latch. Only ever set once per map (esp_reset_session),
39// and only after both model indices are valid: latching on the transient
40// cli_model == 0 at map load disabled server health for the whole process.
41static bool g_srv_checked = false;
42static bool g_srv_trusted = false;
43
44// Per-map caches. The ESP resolves a model name for every entity on every scan
45// and the chams hook asks for a relation on every model draw; both are virtual
46// call chains into the engine plus server datamap reads, so memoise them.
47static constexpr int MODEL_CACHE = 16384;
48static const char *g_model_name_cache[MODEL_CACHE];
49static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail
50
51// Direct-mapped entity pointer -> index map, filled by esp_collect. The
52// triggerbot traces a ray and gets a pointer back, and used to find its index
53// by walking up to 4096 entity-list slots on every fire tick.
54static constexpr int ENTPTR_CACHE = 4096;
55static void *g_entptr_key[ENTPTR_CACHE];
56static int g_entptr_idx[ENTPTR_CACHE];
57
58void esp_reset_session() {
59 g_srv_checked = false;
60 g_srv_trusted = false;
61 g_esp_logged = false;
62 memset(g_model_name_cache, 0, sizeof(g_model_name_cache));
63 memset(g_model_state_cache, 0, sizeof(g_model_state_cache));
64 memset(g_entptr_key, 0, sizeof(g_entptr_key));
65 memset(g_entptr_idx, 0, sizeof(g_entptr_idx));
66}
67 38
68// Speed graph: 100 samples at 0.1 s = the last 10 seconds. 39// Speed graph: 100 samples at 0.1 s = the last 10 seconds.
69static constexpr int SPEED_SAMPLES = 100; 40static constexpr int SPEED_SAMPLES = 100;
@@ -173,431 +144,6 @@ using GetModelNameFn = const char *(*)(void *, const void *);
173 144
174// entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the 145// entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the
175// IClientNetworkable subobject offset inside the entity (see projectz). 146// IClientNetworkable subobject offset inside the entity (see projectz).
176void *entity_networkable(int idx) {
177 return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx);
178}
179
180// IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8.
181const char *entity_class_name(void *net, void **out_cc) {
182 if (!net) return nullptr;
183 void *cc = vfunc<GetCCFn>(net, 2)(net);
184 if (out_cc) *out_cc = cc;
185 return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr;
186}
187
188// IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant
189// 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK,
190// so the slot is validated once against the local player, which can never be
191// dormant: a wrong slot disables the check instead of misreporting entities.
192// Dormant entities keep their last networked origin, so drawing or aiming at
193// them produces ghosts at stale positions.
194static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled
195
196static void dormant_selftest() {
197 // Leave the state untested (0) until a local player exists: the first call
198 // can come from the main menu, and latching "disabled" there would turn the
199 // check off for the whole session.
200 if (!g_iface.entitylist)
201 return;
202 int local = engine_get_local_player();
203 if (local <= 0)
204 return;
205 void *net = entity_networkable(local);
206 if (!net)
207 return;
208 void **vt = *(void ***)net;
209 if (!vt || !mod_client().contains((uintptr_t)vt[8])) {
210 g_dormant_state = -1;
211 logf("dormant check disabled: slot 8 outside client module");
212 return;
213 }
214 using DormantFn = unsigned char (*)(void *);
215 if (vfunc<DormantFn>(net, 8)(net) != 0) {
216 g_dormant_state = -1;
217 logf("dormant check disabled: local player reported dormant");
218 return;
219 }
220 g_dormant_state = 1;
221 logd("dormant check enabled");
222}
223
224bool entity_is_dormant(void *net) {
225 if (!net)
226 return false;
227 if (g_dormant_state == 0)
228 dormant_selftest();
229 if (g_dormant_state != 1)
230 return false;
231 using DormantFn = unsigned char (*)(void *);
232 return vfunc<DormantFn>(net, 8)(net) != 0;
233}
234
235// Entity pointer -> index, filled by the entity scans. The triggerbot gets a
236// pointer back from the trace and needs the index; the alternative is walking
237// up to 4096 entity-list slots on every fire tick.
238static int entptr_lookup(void *ent) {
239 if (!ent)
240 return -1;
241 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
242 return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1;
243}
244
245static void entptr_store(void *ent, int idx) {
246 if (!ent || idx <= 0)
247 return;
248 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
249 g_entptr_key[h] = ent;
250 g_entptr_idx[h] = idx;
251}
252
253// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName.
254// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4.
255const char *model_name_from_index(int modelindex) {
256 if (!g_iface.modelinfo || modelindex <= 0)
257 return nullptr;
258 if (modelindex < MODEL_CACHE) {
259 if (g_model_state_cache[modelindex] == 1)
260 return g_model_name_cache[modelindex];
261 if (g_model_state_cache[modelindex] == 2)
262 return nullptr;
263 }
264 const void *model =
265 vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex);
266 if (!model) {
267 if (modelindex < MODEL_CACHE)
268 g_model_state_cache[modelindex] = 2;
269 return nullptr;
270 }
271 const char *name =
272 vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model);
273 if (modelindex < MODEL_CACHE) {
274 g_model_name_cache[modelindex] = name;
275 g_model_state_cache[modelindex] = name ? 1 : 2;
276 }
277 return name;
278}
279
280const char *entity_model_name(void *ent) {
281 if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr;
282 int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex);
283 return model_name_from_index(modelindex);
284}
285
286// Live origin for a cached entity index. Item/projectile discovery is throttled
287// to 10 Hz (it walks the whole entity list), but the markers must follow the
288// world at render rate; refreshing one netvar read per cached target is cheap.
289bool entity_origin(void *ent, Vector &out) {
290 if (!ent || g_net.origin < 0)
291 return false;
292 uintptr_t e = (uintptr_t)ent;
293 out.x = *(float *)(e + g_net.origin);
294 out.y = *(float *)(e + g_net.origin + 4);
295 out.z = *(float *)(e + g_net.origin + 8);
296 return true;
297}
298
299// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all
300// use the single client class "CAI_BaseNPC" — only special NPCs register their
301// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...).
302static bool is_npc_class(const char *name) {
303 if (!name) return false;
304 return strstr(name, "NPC") != nullptr ||
305 strstr(name, "BaseHelicopter") != nullptr;
306}
307
308// Used by the chams hook to decide whether a model-render entity is an NPC.
309bool esp_is_npc_index(int idx) {
310 if (!g_iface.entitylist || idx <= 0 || idx > 8192)
311 return false;
312 void *net = entity_networkable(idx);
313 if (!net)
314 return false;
315 return is_npc_class(entity_class_name(net, nullptr));
316}
317
318bool esp_is_viewmodel_index(int idx) {
319 if (!g_iface.entitylist || idx <= 0 || idx > 8192)
320 return false;
321 void *ent = entitylist_get(idx);
322 if (!ent)
323 return false;
324 void *net = entity_networkable(idx);
325 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
326 if (cls && strcasestr(cls, "viewmodel"))
327 return true;
328 const char *model = entity_model_name(ent);
329 return model && (strstr(model, "/v_") || strstr(model, "\\v_"));
330}
331
332// 0 = hostile, 1 = neutral, 2 = friendly, -1 = not an NPC.
333static bool npc_is_friendly(int idx, const char *model, int local_idx,
334 bool &neutral);
335int esp_npc_relation(int idx) {
336 // Called from the chams hook up to three times per entity per frame (visible,
337 // through-wall, wireframe pass) and the work behind it is a chain of server
338 // edict/datamap reads. A short TTL cache keeps that out of the render path.
339 struct RelCache {
340 int idx;
341 double at;
342 int val;
343 };
344 static RelCache cache[256];
345 double now = esp_now();
346 RelCache &c = cache[(unsigned)idx & 255];
347 if (c.idx == idx && now - c.at < 0.05)
348 return c.val;
349
350 int val = -1;
351 if (esp_is_npc_index(idx)) {
352 int local = engine_get_local_player();
353 if (local <= 0) {
354 val = 0;
355 } else {
356 void *ent = entitylist_get(idx);
357 const char *model = ent ? entity_model_name(ent) : nullptr;
358 bool neutral = false;
359 bool friendly = npc_is_friendly(idx, model, local, neutral);
360 val = neutral ? 1 : friendly ? 2 : 0;
361 }
362 }
363 c = {idx, now, val};
364 return val;
365}
366
367static bool model_is_friendly(const char *m) {
368 static const char *patterns[] = {
369 "citizen", "humans/", "male_", "female_", "barney", "alyx",
370 "alex", "scientist", "kleiner", "eli", "mossman", "vortigaunt",
371 "dog", "grigori", "monk", "odessa", "gman", nullptr};
372 if (!m) return false;
373 for (int i = 0; patterns[i]; i++)
374 if (strstr(m, patterns[i]))
375 return true;
376 return false;
377}
378
379// harmless wildlife that also happens to be CAI_BaseNPC
380static bool model_is_neutral(const char *m) {
381 // Lamarr is Kleiner's pet headcrab: scripted and harmless, and the user
382 // wants her shown neutral rather than hostile.
383 static const char *patterns[] = {"crow", "gull", "pigeon", "seagull",
384 "bird", "Lamarr", nullptr};
385 if (!m) return false;
386 for (int i = 0; patterns[i]; i++)
387 if (strstr(m, patterns[i]))
388 return true;
389 return false;
390}
391
392static bool model_is_floor_turret(const char *m) {
393 return m && strstr(m, "floor_turret");
394}
395
396static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx,
397 bool &neutral) {
398 int rel = server_npc_relation(idx, local_idx);
399 int state = server_npc_state(idx);
400 int turret = -1;
401 // Citizen-built and Alyx-hacked turrets never add an m_Relationship row;
402 // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state.
403 if (model_is_floor_turret(model))
404 turret = server_npc_turret_flags(idx);
405 // Debug-only: relation/state changes are the authoritative signals and the
406 // first thing to check when a friendly NPC still looks hostile.
407 static int rel_cache[2048];
408 static int state_cache[2048];
409 static bool cache_init = false;
410 if (!cache_init) {
411 for (int i = 0; i < 2048; i++)
412 rel_cache[i] = state_cache[i] = -999;
413 cache_init = true;
414 }
415 if (idx > 0 && idx < 2048 &&
416 (rel_cache[idx] != rel || state_cache[idx] != state)) {
417 rel_cache[idx] = rel;
418 state_cache[idx] = state;
419 logd("npc %d (%s): relation=%d state=%d followtarget=%d turret=%d "
420 "local=%d", idx, model ? model : "?", rel, state,
421 (int)server_npc_has_follow_target(idx), turret, local_idx);
422 }
423 if (rel == 3) { // D_LI
424 neutral = false;
425 return true;
426 }
427 if (turret > 0) { // citizen-built / Alyx-hacked
428 neutral = false;
429 return true;
430 }
431 if (rel == 4) { // D_NU
432 neutral = true;
433 return false;
434 }
435 // NPC_STATE_SCRIPT = 4: cutscene actors and scripted pets are passive even
436 // when their class relationship says hate (Lamarr, scripted Combine, ...).
437 if (state == 4) {
438 neutral = true;
439 return false;
440 }
441 if (rel == 1 || rel == 2) { // D_HT / D_FR
442 neutral = false;
443 return false;
444 }
445 if (turret >= 0) { // ordinary floor turret
446 neutral = false;
447 return false;
448 }
449 // Bugbait-controlled antlions follow a target even when their default
450 // relationship was never rewritten.
451 if (model && strstr(model, "antlion") &&
452 server_npc_has_follow_target(idx)) {
453 neutral = false;
454 return true;
455 }
456 bool friendly = model_is_friendly(model);
457 neutral = model_is_neutral(model);
458 return friendly;
459}
460
461// Friendly/neutral state changes slowly (hacked turrets, bugbait), while the
462// entity scan and the chams hook ask for it constantly. Cache it briefly so the
463// server lookups happen once per NPC per window instead of per call.
464static bool npc_is_friendly(int idx, const char *model, int local_idx,
465 bool &neutral) {
466 struct Cache {
467 int idx;
468 double at;
469 bool friendly;
470 bool neutral;
471 };
472 static Cache cache[256];
473 double now = esp_now();
474 Cache &c = cache[(unsigned)idx & 255];
475 if (c.idx == idx && now - c.at < 0.10) {
476 neutral = c.neutral;
477 return c.friendly;
478 }
479 bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral);
480 c = {idx, now, f, neutral};
481 return f;
482}
483
484enum NpcDamageClass {
485 NPC_DMG_NORMAL = 0,
486 NPC_DMG_ARMORED, // only blast damage (RPG) hurts it
487 NPC_DMG_VULNERABLE, // gravity gun / physics damage works well
488 NPC_DMG_IMMUNE // m_takedamage says no damage at all
489};
490
491static bool model_has(const char *model, const char *needle) {
492 return model && strcasestr(model, needle) != nullptr;
493}
494
495// Server m_takedamage first (authoritative immunity), then the HL2 damage
496// rules for units whose OnTakeDamage filters specific damage types.
497static int npc_damage_class_uncached(int idx, const char *model) {
498 int td = server_npc_takedamage(idx);
499 if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY
500 return NPC_DMG_IMMUNE;
501 if (model_has(model, "gunship") || model_has(model, "combine_strider") ||
502 model_has(model, "dropship") || model_has(model, "helicopter") ||
503 model_has(model, "chopper"))
504 return NPC_DMG_ARMORED;
505 if (model_has(model, "headcrab") || model_has(model, "antlion") ||
506 model_has(model, "manhack"))
507 return NPC_DMG_VULNERABLE;
508 return NPC_DMG_NORMAL;
509}
510
511// Damage class depends on m_takedamage (a server read) plus model string
512// matching. It changes even more rarely than the relation, so cache it.
513static int npc_damage_class(int idx, const char *model) {
514 struct Cache {
515 int idx;
516 double at;
517 int val;
518 };
519 static Cache cache[256];
520 double now = esp_now();
521 Cache &c = cache[(unsigned)idx & 255];
522 if (c.idx == idx && now - c.at < 0.25)
523 return c.val;
524 int v = npc_damage_class_uncached(idx, model);
525 c = {idx, now, v};
526 return v;
527}
528
529bool active_weapon_contains(void *local_ent, const char *needle);
530
531// Armoured units need blast damage; the RPG is the only stock weapon that
532// passes their OnTakeDamage filters. Immune units can never be damaged.
533static bool npc_can_be_damaged(int idx, const char *model) {
534 int dc = npc_damage_class(idx, model);
535 if (dc == NPC_DMG_IMMUNE)
536 return false;
537 if (dc != NPC_DMG_ARMORED)
538 return true;
539 int local = engine_get_local_player();
540 void *le = local > 0 ? entitylist_get(local) : nullptr;
541 return active_weapon_contains(le, "rocket_launcher") ||
542 active_weapon_contains(le, "rpg");
543}
544
545static void short_name(const char *model, char *out, size_t outn) {
546 if (!model) { snprintf(out, outn, "NPC"); return; }
547 const char *base = strrchr(model, '/');
548 base = base ? base + 1 : model;
549 size_t n = strcspn(base, ".");
550 if (n >= outn) n = outn - 1;
551 memcpy(out, base, n);
552 out[n] = 0;
553}
554
555// Enemy weapon label. NPC weapons are server entities, so read the server
556// model index first and fall back to the client class name.
557static void weapon_short_name(int idx, char *out, size_t outn) {
558 out[0] = 0;
559 int wi = server_active_weapon(idx);
560 if (wi <= 0)
561 return;
562 const char *name = nullptr;
563 void *base = server_entity_base(wi);
564 int mi = base ? server_model_index(base) : -1;
565 if (mi > 0)
566 name = model_name_from_index(mi);
567 if (!name) {
568 void *wep = entitylist_get(wi);
569 if (wep) {
570 void *net = entity_networkable(wi);
571 name = entity_class_name(net, nullptr);
572 }
573 }
574 if (!name)
575 name = server_entity_classname(wi);
576 if (!name)
577 return;
578 static const struct {
579 const char *needle;
580 const char *label;
581 } map[] = {
582 {"smg1", "SMG1"}, {"irifle", "AR2"}, {"ar2", "AR2"},
583 {"shotgun", "Shotgun"}, {"pistol", "Pistol"}, {"357", "357"},
584 {"crossbow", "Crossbow"}, {"rocket", "RPG"}, {"rpg", "RPG"},
585 {"grenade", "Grenade"}, {"frag", "Grenade"}, {"crowbar", "Crowbar"},
586 {"physcannon", "Gravity"}, {"slam", "SLAM"}, {"bugbait", "Bugbait"},
587 {"manhack", "Manhack"}, {"turret", "Turret"}, {"annabelle", "Rifle"},
588 {"alyxgun", "Alyx gun"}, {"sniper", "Sniper"}, {"smg", "SMG"},
589 };
590 for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) {
591 if (strcasestr(name, map[i].needle)) {
592 snprintf(out, outn, "%s", map[i].label);
593 return;
594 }
595 }
596 // Last resort: the short class/model name.
597 short_name(name, out, outn);
598}
599
600// Hostile / neutral / friendly colours come from the ESP colour pickers.
601static void relation_rgb(const EspTarget &t, int &r, int &g, int &b) { 147static void relation_rgb(const EspTarget &t, int &r, int &g, int &b) {
602 const int *c = t.friendly ? g_esp_col_friendly 148 const int *c = t.friendly ? g_esp_col_friendly
603 : t.neutral ? g_esp_col_neutral 149 : t.neutral ? g_esp_col_neutral
@@ -971,7 +517,7 @@ static void draw_skeleton(const EspTarget &t) {
971// MAX_STEP degrees toward the target and stops inside DEADZONE. 517// MAX_STEP degrees toward the target and stops inside DEADZONE.
972// --------------------------------------------------------------------------- 518// ---------------------------------------------------------------------------
973 519
974static QAngle angle_to(const Vector &from, const Vector &to) { 520QAngle angle_to(const Vector &from, const Vector &to) {
975 float dx = to.x - from.x, dy = to.y - from.y, dz = to.z - from.z; 521 float dx = to.x - from.x, dy = to.y - from.y, dz = to.z - from.z;
976 float horiz = sqrtf(dx * dx + dy * dy); 522 float horiz = sqrtf(dx * dx + dy * dy);
977 if (horiz < 0.001f) 523 if (horiz < 0.001f)
@@ -985,7 +531,7 @@ static QAngle angle_to(const Vector &from, const Vector &to) {
985 531
986// Server bullet direction adds m_Local.m_vecPunchAngle to the usercmd view 532// Server bullet direction adds m_Local.m_vecPunchAngle to the usercmd view
987// (GetAutoaimVector), so aim angles have to be pre-subtracted by it. 533// (GetAutoaimVector), so aim angles have to be pre-subtracted by it.
988static bool punch_angles(float &px, float &py) { 534bool punch_angles(float &px, float &py) {
989 px = py = 0.0f; 535 px = py = 0.0f;
990 if (!aim_punch_enabled() || g_net.punch < 0) 536 if (!aim_punch_enabled() || g_net.punch < 0)
991 return false; 537 return false;
@@ -1001,1065 +547,6 @@ static bool punch_angles(float &px, float &py) {
1001 return std::isfinite(px) && std::isfinite(py); 547 return std::isfinite(px) && std::isfinite(py);
1002} 548}
1003 549
1004static int g_aim_lock = -1;
1005static int g_silent_lock = -1;
1006
1007void aimbot_reset_lock() { g_aim_lock = -1; }
1008void silentaim_reset_lock() { g_silent_lock = -1; }
1009
1010// max_fov is the angular radius around the crosshair. If lock_idx is non-null
1011// and valid, that target is kept while it is alive and visible (the "do not
1012// switch while the bind is held" rule); a fresh acquisition is ranked by angle
1013// and the nearest visible selected hitbox point.
1014static bool select_aim_target(const QAngle &view, unsigned bone_mask,
1015 bool vischeck, float max_fov, const char *tag,
1016 int *lock_idx, int &out_idx, Vector &out_point,
1017 float &out_fov) {
1018 out_idx = -1;
1019 out_point = {};
1020 out_fov = 0.0f;
1021 // maxs is optional: the per-target code falls back to 72 units when the
1022 // netvar is missing, so it must not disable the whole aimbot here.
1023 if (!g_iface.entitylist || g_net.origin < 0)
1024 return false;
1025 int local = engine_get_local_player();
1026 if (local <= 0)
1027 return false;
1028 void *local_ent = entitylist_get(local);
1029 if (!local_ent)
1030 return false;
1031 if (g_net.health >= 0 &&
1032 *(int *)((uintptr_t)local_ent + g_net.health) <= 0)
1033 return false;
1034
1035 // With the RPG equipped, armoured units (striders, gunships, dropships,
1036 // hunter-choppers) are the ones ordinary shots cannot hurt, so prefer them
1037 // over normal NPCs when acquiring.
1038 bool rpg = active_weapon_contains(local_ent, "rocket_launcher") ||
1039 active_weapon_contains(local_ent, "rpg");
1040
1041 // fov_limit <= 0 means "no angular limit" (kept for unlimited acquisition).
1042 auto try_target = [&](int idx, float fov_limit, Vector &pt,
1043 float &fov) -> bool {
1044 if (idx <= 0 || idx == local)
1045 return false;
1046 void *ent = entitylist_get(idx);
1047 if (!ent)
1048 return false;
1049 void *net = entity_networkable(idx);
1050 if (!net || entity_is_dormant(net))
1051 return false;
1052 const char *cls = entity_class_name(net, nullptr);
1053 if (!cls || !is_npc_class(cls))
1054 return false;
1055 uintptr_t e = (uintptr_t)ent;
1056 int life = g_net.lifestate >= 0
1057 ? (int)*(unsigned char *)(e + g_net.lifestate)
1058 : -1;
1059 if (life != LIFE_ALIVE)
1060 return false;
1061 const char *model = entity_model_name(ent);
1062 bool neutral = false;
1063 if (npc_is_friendly(idx, model, local, neutral) || neutral)
1064 return false;
1065 if (!npc_can_be_damaged(idx, model))
1066 return false;
1067 if (!bones_update(idx, ent))
1068 return false;
1069 Vector pts[32];
1070 int npoints = aim_points(idx, bone_mask, pts, 32);
1071 float best = 1e9f;
1072 Vector best_pt{};
1073 for (int i = 0; i < npoints; i++) {
1074 float f = getfov(view, angle_to(g_cam_eye, pts[i]));
1075 if (fov_limit > 0.0f && f > fov_limit)
1076 continue;
1077 if (f >= best)
1078 continue;
1079 if (vischeck && !is_visible(g_cam_eye, pts[i], local_ent, ent))
1080 continue;
1081 best = f;
1082 best_pt = pts[i];
1083 }
1084 if (best > 1e8f)
1085 return false;
1086 pt = best_pt;
1087 fov = best;
1088 return true;
1089 };
1090
1091 // Keep the locked target while it is alive and has a visible selected point.
1092 // The lock uses a wider cone than acquisition (hysteresis), so a target that
1093 // drifts slightly off the crosshair is not dropped, but one that swings
1094 // behind the player is: an unbounded lock let silent aim fire backwards.
1095 if (lock_idx && *lock_idx >= 0) {
1096 Vector pt;
1097 float f;
1098 if (try_target(*lock_idx, max_fov * 1.5f, pt, f)) {
1099 out_idx = *lock_idx;
1100 out_point = pt;
1101 out_fov = f;
1102 static int lock_logs = 0;
1103 if (lock_logs < 5) {
1104 lock_logs++;
1105 logd("%s: lock idx=%d fov=%.2f", tag, out_idx, f);
1106 }
1107 return true;
1108 }
1109 *lock_idx = -1;
1110 }
1111
1112 using Fn = int (*)(void *);
1113 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
1114 if (hi < 0 || hi > 4096)
1115 hi = 4096;
1116
1117 struct Candidate {
1118 int idx;
1119 float fov;
1120 Vector approx;
1121 bool armored;
1122 float key;
1123 };
1124 Candidate cands[16];
1125 int ncands = 0;
1126
1127 for (int idx = 1; idx <= hi; idx++) {
1128 if (idx == local)
1129 continue;
1130 void *ent = entitylist_get(idx);
1131 if (!ent)
1132 continue;
1133 void *net = entity_networkable(idx);
1134 if (!net || entity_is_dormant(net))
1135 continue;
1136 const char *cls = entity_class_name(net, nullptr);
1137 if (!cls || !is_npc_class(cls))
1138 continue;
1139
1140 uintptr_t e = (uintptr_t)ent;
1141 int life = g_net.lifestate >= 0
1142 ? (int)*(unsigned char *)(e + g_net.lifestate)
1143 : -1;
1144 if (life != LIFE_ALIVE)
1145 continue;
1146 const char *model = entity_model_name(ent);
1147 bool neutral = false;
1148 if (npc_is_friendly(idx, model, local, neutral) || neutral)
1149 continue;
1150 if (!npc_can_be_damaged(idx, model))
1151 continue;
1152 bool armored = npc_damage_class(idx, model) == NPC_DMG_ARMORED;
1153
1154 float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f;
1155 if (!(maxz > 4.0f && maxz < 512.0f))
1156 maxz = 72.0f;
1157 Vector origin{*(float *)(e + g_net.origin),
1158 *(float *)(e + g_net.origin + 4),
1159 *(float *)(e + g_net.origin + 8)};
1160 Vector approx{origin.x, origin.y,
1161 origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)};
1162 float fov = getfov(view, angle_to(g_cam_eye, approx));
1163 // The prefilter samples a single point, but the selected hitbox can be a
1164 // whole body-width away at close range. Reject only when the target's
1165 // angular radius cannot reach the cone; try_target still enforces the
1166 // exact per-hitbox FOV.
1167 float ddx = approx.x - g_cam_eye.x;
1168 float ddy = approx.y - g_cam_eye.y;
1169 float ddz = approx.z - g_cam_eye.z;
1170 float dist = sqrtf(ddx * ddx + ddy * ddy + ddz * ddz);
1171 float margin =
1172 dist > 1.0f ? atan2f(maxz, dist) * 180.0f / PI_F : 90.0f;
1173 if (fov - margin > max_fov)
1174 continue;
1175 float key = (rpg && armored) ? fov - 1000.0f : fov;
1176
1177 int pos = ncands;
1178 while (pos > 0 && cands[pos - 1].key > key)
1179 pos--;
1180 if (ncands < 16)
1181 ncands++;
1182 for (int j = ncands - 1; j > pos; j--)
1183 cands[j] = cands[j - 1];
1184 if (pos < 16)
1185 cands[pos] = {idx, fov, approx, armored, key};
1186 }
1187
1188 if (ncands == 0) {
1189 static double last_none = 0.0;
1190 double now = esp_now();
1191 if (now - last_none >= 2.0) {
1192 last_none = now;
1193 logd("%s: no hostile target within %.0f deg", tag, max_fov);
1194 }
1195 return false;
1196 }
1197
1198 for (int ci = 0; ci < ncands; ci++) {
1199 Vector pt;
1200 float f;
1201 if (!try_target(cands[ci].idx, max_fov, pt, f))
1202 continue;
1203 out_idx = cands[ci].idx;
1204 out_point = pt;
1205 out_fov = f;
1206 if (lock_idx)
1207 *lock_idx = out_idx;
1208 static int sel_logs = 0;
1209 if (sel_logs < 5) {
1210 sel_logs++;
1211 logd("%s: target idx=%d fov=%.2f vischeck=%d", tag, out_idx, f,
1212 (int)vischeck);
1213 }
1214 return true;
1215 }
1216
1217 static double last_vis = 0.0;
1218 double now = esp_now();
1219 if (now - last_vis >= 2.0) {
1220 last_vis = now;
1221 logd("%s: no visible aim point (vischeck=%d)", tag, (int)vischeck);
1222 }
1223 return false;
1224}
1225
1226bool active_weapon_contains(void *local_ent, const char *needle) {
1227 if (!local_ent || g_net.activeweapon < 0 || !needle)
1228 return false;
1229 uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon);
1230 if (h == 0xFFFFFFFFu)
1231 return false;
1232 int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index
1233 if (idx <= 0 || idx > 4096)
1234 return false;
1235 void *wep = entitylist_get(idx);
1236 void *net = entity_networkable(idx);
1237 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
1238 const char *model = wep ? entity_model_name(wep) : nullptr;
1239 return (cls && strcasestr(cls, needle)) ||
1240 (model && strcasestr(model, needle));
1241}
1242
1243// Crossbow bolts and RPG rockets are not hitscan. Lead moving targets by the
1244// projectile travel time; the crossbow bolt also has gravity 0.05 (30 u/s^2).
1245static bool projectile_lead_point(int target_idx, const Vector &point,
1246 Vector &out) {
1247 int local = engine_get_local_player();
1248 if (local <= 0)
1249 return false;
1250 void *local_ent = entitylist_get(local);
1251 if (!local_ent)
1252 return false;
1253 float speed = 0.0f, gravity = 0.0f;
1254 if (active_weapon_contains(local_ent, "crossbow") ||
1255 active_weapon_contains(local_ent, "Crossbow")) {
1256 speed = 2500.0f;
1257 gravity = 30.0f;
1258 } else if (active_weapon_contains(local_ent, "rocket_launcher") ||
1259 active_weapon_contains(local_ent, "w_rpg") ||
1260 active_weapon_contains(local_ent, "RPG")) {
1261 speed = 1500.0f;
1262 gravity = 0.0f;
1263 } else {
1264 return false;
1265 }
1266 void *target = entitylist_get(target_idx);
1267 if (!target)
1268 return false;
1269 // NPCs do not network C_BaseEntity::m_vecVelocity (only the player does), so
1270 // the client field is stale for exactly the targets the aimbot uses. Read
1271 // the listen-server entity when available and fall back to the client value.
1272 float v[3] = {0.0f, 0.0f, 0.0f};
1273 Vector srv_vel;
1274 bool have_vel = false;
1275 if (server_entity_velocity(target_idx, &srv_vel)) {
1276 v[0] = srv_vel.x;
1277 v[1] = srv_vel.y;
1278 v[2] = srv_vel.z;
1279 have_vel = true;
1280 } else if (g_net.velocity >= 0) {
1281 const float *cv = (const float *)((uintptr_t)target + g_net.velocity);
1282 v[0] = cv[0];
1283 v[1] = cv[1];
1284 v[2] = cv[2];
1285 have_vel = true;
1286 }
1287 if (!have_vel)
1288 return false;
1289 float dx = point.x - g_cam_eye.x;
1290 float dy = point.y - g_cam_eye.y;
1291 float dz = point.z - g_cam_eye.z;
1292 float dist = sqrtf(dx * dx + dy * dy + dz * dz);
1293 float t = dist / speed;
1294 out = point;
1295 out.x += v[0] * t;
1296 out.y += v[1] * t;
1297 out.z += v[2] * t + 0.5f * gravity * t * t;
1298 return true;
1299}
1300
1301// A led point is a prediction, not a verified aim point: acquisition checked
1302// the un-led position, so the shift has to pass the same FOV and visibility
1303// tests. On failure the caller keeps the original point instead of firing into
1304// geometry or outside the configured cone.
1305static bool lead_point_ok(int target_idx, const Vector &lead,
1306 const QAngle &view, float max_fov) {
1307 if (getfov(view, angle_to(g_cam_eye, lead)) > max_fov)
1308 return false;
1309 if (!aimbot_vischeck_enabled())
1310 return true;
1311 void *local_ent = entitylist_get(engine_get_local_player());
1312 void *target_ent = entitylist_get(target_idx);
1313 if (!local_ent || !target_ent)
1314 return false;
1315 return is_visible(g_cam_eye, lead, local_ent, target_ent);
1316}
1317
1318// Semi-auto weapons (pistol, .357, crossbow, shotgun, RPG, gravity gun) only
1319// fire on the IN_ATTACK press edge, so a synthetic button held every tick fires
1320// exactly once per buy/equip. Alternate press/release for those; automatic
1321// weapons keep the button held.
1322static bool weapon_is_semi_auto(void *local_ent) {
1323 static const char *names[] = {"pistol", "357",
1324 "crossbow", "shotgun",
1325 "rocket_launcher", "physcannon",
1326 "crowbar", nullptr};
1327 for (int i = 0; names[i]; i++)
1328 if (active_weapon_contains(local_ent, names[i]))
1329 return true;
1330 return false;
1331}
1332
1333static bool fire_edge_ok(CUserCmd *cmd) {
1334 void *local_ent = entitylist_get(engine_get_local_player());
1335 if (!local_ent || !weapon_is_semi_auto(local_ent))
1336 return true;
1337 static int last_tick = -1;
1338 static bool phase = false;
1339 if (cmd->tick_count != last_tick) {
1340 last_tick = cmd->tick_count;
1341 phase = !phase;
1342 }
1343 return phase;
1344}
1345
1346bool aimbot_apply(CUserCmd *cmd) {
1347 unsigned bone_mask = aimbot_bone_mask();
1348 if (!bone_mask)
1349 return false;
1350 QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f};
1351 float punch_x = 0.0f, punch_y = 0.0f;
1352 bool punch = punch_angles(punch_x, punch_y);
1353 if (punch) {
1354 view.x += punch_x;
1355 view.y += punch_y;
1356 }
1357 int target = -1;
1358 Vector point{};
1359 float fov = 0.0f;
1360 float max_fov = (float)aimbot_fov();
1361 if (max_fov < 1.0f) max_fov = 1.0f;
1362 if (max_fov > 180.0f) max_fov = 180.0f;
1363 int *lock = feature_has_bind(FEAT_AIMBOT) ? &g_aim_lock : nullptr;
1364 if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov,
1365 "aimbot", lock, target, point, fov))
1366 return false;
1367 Vector lead;
1368 if (projectile_lead_point(target, point, lead) &&
1369 lead_point_ok(target, lead, view, max_fov))
1370 point = lead;
1371 QAngle desired = angle_to(g_cam_eye, point);
1372 if (punch) {
1373 desired.x -= punch_x;
1374 desired.y -= punch_y;
1375 }
1376 static int want_logs = 0;
1377 if (want_logs < 10) {
1378 want_logs++;
1379 logd("aimbot: target=%d aim=(%.1f %.1f %.1f) fov=%.2f want=%.2f/%.2f",
1380 target, point.x, point.y, point.z, fov, desired.x, desired.y);
1381 }
1382 float dp = desired.x - cmd->viewangles[0];
1383 float dy = desired.y - cmd->viewangles[1];
1384 while (dy > 180.0f) dy -= 360.0f;
1385 while (dy < -180.0f) dy += 360.0f;
1386
1387 constexpr float DEADZONE = 0.05f;
1388 float max_step = (float)aimbot_smooth_step();
1389 if (max_step < 1.0f) max_step = 1.0f;
1390 float maxd = fmaxf(fabsf(dp), fabsf(dy));
1391 if (maxd <= DEADZONE)
1392 return false;
1393
1394 float scale = (maxd > max_step) ? (max_step / maxd) : 1.0f;
1395 float old_pitch = cmd->viewangles[0];
1396 float old_yaw = cmd->viewangles[1];
1397 float np = cmd->viewangles[0] + dp * scale;
1398 float ny = cmd->viewangles[1] + dy * scale;
1399 if (np > 89.0f) np = 89.0f;
1400 if (np < -89.0f) np = -89.0f;
1401 while (ny > 180.0f) ny -= 360.0f;
1402 while (ny < -180.0f) ny += 360.0f;
1403 cmd->viewangles[0] = np;
1404 cmd->viewangles[1] = ny;
1405
1406 if (g_iface.engine) {
1407 QAngle out{np, ny, 0.0f};
1408 using SetViewFn = void (*)(void *, QAngle *);
1409 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &out);
1410 }
1411 static int mod_logs = 0;
1412 if (mod_logs < 10) {
1413 mod_logs++;
1414 logd("aimbot: view %.2f/%.2f -> %.2f/%.2f", old_pitch, old_yaw, np, ny);
1415 }
1416 return true;
1417}
1418
1419// Keep the world-space movement wish direction unchanged when the usercmd
1420// view angles are temporarily replaced by the silent-aim angles.
1421static void preserve_movement(CUserCmd *cmd, float old_yaw, float new_yaw) {
1422 float oy = old_yaw * PI_F / 180.0f;
1423 float ay = new_yaw * PI_F / 180.0f;
1424 float ofx = cosf(oy), ofy = sinf(oy);
1425 float orx = sinf(oy), ory = -cosf(oy);
1426 float afx = cosf(ay), afy = sinf(ay);
1427 float arx = sinf(ay), ary = -cosf(ay);
1428 float wx = ofx * cmd->forwardmove + orx * cmd->sidemove;
1429 float wy = ofy * cmd->forwardmove + ory * cmd->sidemove;
1430 cmd->forwardmove = wx * afx + wy * afy;
1431 cmd->sidemove = wx * arx + wy * ary;
1432}
1433
1434bool silent_aim_apply(CUserCmd *cmd) {
1435 unsigned bone_mask = aimbot_bone_mask();
1436 if (!bone_mask)
1437 return false;
1438 QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z};
1439 QAngle view{orig.x, orig.y, 0.0f};
1440 float punch_x = 0.0f, punch_y = 0.0f;
1441 bool punch = punch_angles(punch_x, punch_y);
1442 if (punch) {
1443 view.x += punch_x;
1444 view.y += punch_y;
1445 }
1446 int target = -1;
1447 Vector point{};
1448 float fov = 0.0f;
1449 float max_fov = (float)aimbot_fov();
1450 if (max_fov < 1.0f) max_fov = 1.0f;
1451 if (max_fov > 180.0f) max_fov = 180.0f;
1452 int *lock = feature_has_bind(FEAT_SILENTAIM) ? &g_silent_lock : nullptr;
1453 if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov,
1454 "silentaim", lock, target, point, fov))
1455 return false;
1456 Vector lead;
1457 if (projectile_lead_point(target, point, lead) &&
1458 lead_point_ok(target, lead, view, max_fov))
1459 point = lead;
1460 QAngle desired = angle_to(g_cam_eye, point);
1461 if (punch) {
1462 desired.x -= punch_x;
1463 desired.y -= punch_y;
1464 }
1465 float np = desired.x;
1466 if (np > 89.0f) np = 89.0f;
1467 if (np < -89.0f) np = -89.0f;
1468 float ny = desired.y;
1469 while (ny > 180.0f) ny -= 360.0f;
1470 while (ny < -180.0f) ny += 360.0f;
1471
1472 preserve_movement(cmd, orig.y, ny);
1473
1474 cmd->viewangles[0] = np;
1475 cmd->viewangles[1] = ny;
1476 cmd->viewangles[2] = orig.z;
1477
1478 // Restore now and again at FRAME_RENDER_START (after the engine consumed
1479 // the aimed usercmd); the frame-stage restore is the one that sticks.
1480 set_silent_restore(orig);
1481 if (g_iface.engine) {
1482 QAngle restore{orig.x, orig.y, orig.z};
1483 using SetViewFn = void (*)(void *, QAngle *);
1484 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore);
1485 }
1486 if (fire_edge_ok(cmd))
1487 cmd->buttons |= IN_ATTACK;
1488 static int logs = 0;
1489 if (logs < 5) {
1490 logs++;
1491 logd("silentaim: fire idx=%d fov=%.2f aim=%.2f/%.2f", target, fov, np, ny);
1492 }
1493 return true;
1494}
1495
1496// ---------------------------------------------------------------------------
1497// melee killbot
1498// ---------------------------------------------------------------------------
1499//
1500// The crowbar's CBaseHLBludgeonWeapon::Swing traces 75 units from
1501// Weapon_ShootPosition along the view forward (CROWBAR_RANGE in
1502// weapon_crowbar.h). It first tries a line, then a +/-16 hull backed off by
1503// ~28 units, and ignores targets more than ~45 degrees off the aim direction.
1504// We therefore only need to point the usercmd at the target for one command;
1505// the rendered view is restored exactly like silent aim.
1506
1507static constexpr float MELEE_RANGE = 75.0f;
1508
1509static float vec_dist(const Vector &a, const Vector &b) {
1510 float dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
1511 return sqrtf(dx * dx + dy * dy + dz * dz);
1512}
1513
1514bool melee_weapon_active(void *local_ent) {
1515 if (!local_ent)
1516 return false;
1517 int local = engine_get_local_player();
1518 if (g_iface.engineserver)
1519 resolve_server_netvars();
1520
1521 auto check = [&](int idx, const char **cls_out,
1522 const char **model_out) -> bool {
1523 if (idx <= 0 || idx > 4096)
1524 return false;
1525 void *wep = entitylist_get(idx);
1526 void *net = entity_networkable(idx);
1527 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
1528 const char *model = wep ? entity_model_name(wep) : nullptr;
1529 // Client weapon models can be missing/0 after a map change; recover the
1530 // server model index so the crowbar is still recognized.
1531 if ((!model || !*model) && g_iface.engineserver) {
1532 void *sbase = nullptr;
1533 server_entity_health(idx, nullptr, nullptr, &sbase);
1534 if (sbase)
1535 model = model_name_from_index(server_model_index(sbase));
1536 }
1537 *cls_out = cls;
1538 *model_out = model;
1539 return (cls && strstr(cls, "Crowbar")) ||
1540 (model && strstr(model, "crowbar"));
1541 };
1542
1543 int idx = -1;
1544 if (g_net.activeweapon >= 0) {
1545 uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon);
1546 if (h != 0xFFFFFFFFu)
1547 idx = (int)(h & 0x7FF); // 11-bit entity index; upper bits are serial
1548 }
1549 const char *cls = nullptr, *model = nullptr;
1550 bool crowbar = check(idx, &cls, &model);
1551
1552 if (!crowbar) {
1553 // Fallback: the authoritative server player's m_hActiveWeapon handle.
1554 // Also covers a client netvar offset that resolves but does not point at
1555 // the weapon the server is actually using.
1556 if (local > 0) {
1557 resolve_server_netvars();
1558 int si = server_active_weapon(local);
1559 if (si > 0 && si != idx) {
1560 idx = si;
1561 crowbar = check(idx, &cls, &model);
1562 }
1563 }
1564 }
1565
1566 static int last_idx = -2;
1567 static bool last_crowbar = false;
1568 if (idx != last_idx || crowbar != last_crowbar) {
1569 last_idx = idx;
1570 last_crowbar = crowbar;
1571 logf("melee: active idx=%d cls=%s model=%s crowbar=%d", idx,
1572 cls ? cls : "?", model ? model : "?", (int)crowbar);
1573 if (!crowbar) {
1574 static bool warned = false;
1575 if (!warned) {
1576 warned = true;
1577 toastf(TOAST_WARN, "Melee killbot: equip the crowbar");
1578 }
1579 }
1580 }
1581 return crowbar;
1582}
1583
1584bool gravity_gun_active(void *local_ent) {
1585 if (!local_ent)
1586 return false;
1587 if (active_weapon_contains(local_ent, "physcannon"))
1588 return true;
1589 // Same server fallback as the crowbar: map transitions can leave the
1590 // client activeweapon handle stale.
1591 int local = engine_get_local_player();
1592 if (local <= 0 || !g_iface.engineserver)
1593 return false;
1594 resolve_server_netvars();
1595 int si = server_active_weapon(local);
1596 if (si <= 0)
1597 return false;
1598 void *wep = entitylist_get(si);
1599 void *net = entity_networkable(si);
1600 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
1601 const char *model = wep ? entity_model_name(wep) : nullptr;
1602 if (!model) {
1603 void *sbase = nullptr;
1604 server_entity_health(si, nullptr, nullptr, &sbase);
1605 if (sbase)
1606 model = model_name_from_index(server_model_index(sbase));
1607 }
1608 return (cls && strcasestr(cls, "physcannon")) ||
1609 (model && strcasestr(model, "physcannon"));
1610}
1611
1612// The citadel supercharges the gravity gun (d3_citadel_03 onward). It can then
1613// punt any NPC, so the melee aimbot should not restrict itself to
1614// physics-vulnerable units.
1615static bool supercharged_gravity_gun() {
1616 if (!g_iface.engine)
1617 return false;
1618 using Fn = const char *(*)(void *);
1619 const char *m = vfunc<Fn>(g_iface.engine, 51)(g_iface.engine);
1620 if (!m)
1621 return false;
1622 bool supe = strstr(m, "d3_citadel_03") || strstr(m, "d3_citadel_04") ||
1623 strstr(m, "d3_breen_01");
1624 static const char *logged_map = nullptr;
1625 if (logged_map != m) {
1626 logged_map = m;
1627 logf("melee: gravity gun map=%s supercharged=%d", m, (int)supe);
1628 }
1629 return supe;
1630}
1631
1632bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) {
1633 if (!g_iface.entitylist || g_net.origin < 0)
1634 return false;
1635 int local = engine_get_local_player();
1636 if (local <= 0)
1637 return false;
1638 void *local_ent = entitylist_get(local);
1639 if (!local_ent)
1640 return false;
1641 if (g_net.health >= 0 &&
1642 *(int *)((uintptr_t)local_ent + g_net.health) <= 0)
1643 return false;
1644
1645 // The punted object can travel much farther than the crowbar.
1646 float max_range = gravity ? 1200.0f : MELEE_RANGE;
1647 bool supercharged = gravity && supercharged_gravity_gun();
1648
1649 using Fn = int (*)(void *);
1650 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
1651 if (hi < 0 || hi > 4096)
1652 hi = 4096;
1653
1654 struct Cand {
1655 int idx;
1656 float dist;
1657 Vector closest;
1658 Vector center;
1659 };
1660 Cand cands[16];
1661 int n = 0;
1662
1663 for (int idx = 1; idx <= hi; idx++) {
1664 if (idx == local)
1665 continue;
1666 void *ent = entitylist_get(idx);
1667 if (!ent)
1668 continue;
1669 void *net = entity_networkable(idx);
1670 if (!net || entity_is_dormant(net))
1671 continue;
1672 const char *cls = entity_class_name(net, nullptr);
1673 if (!cls || !is_npc_class(cls))
1674 continue;
1675 uintptr_t e = (uintptr_t)ent;
1676 int life = g_net.lifestate >= 0
1677 ? (int)*(unsigned char *)(e + g_net.lifestate)
1678 : -1;
1679 if (life != LIFE_ALIVE)
1680 continue;
1681 const char *model = entity_model_name(ent);
1682 bool neutral = false;
1683 if (npc_is_friendly(idx, model, local, neutral) || neutral)
1684 continue;
1685 if (!npc_can_be_damaged(idx, model))
1686 continue;
1687 // The normal gravity gun is only the tool for physics-vulnerable units;
1688 // the citadel version behaves like a normal weapon.
1689 if (gravity && !supercharged &&
1690 npc_damage_class(idx, model) != NPC_DMG_VULNERABLE)
1691 continue;
1692
1693 Vector origin{*(float *)(e + g_net.origin),
1694 *(float *)(e + g_net.origin + 4),
1695 *(float *)(e + g_net.origin + 8)};
1696 float minx = -16.0f, miny = -16.0f, minz = 0.0f;
1697 float maxx = 16.0f, maxy = 16.0f, maxz = 72.0f;
1698 if (g_net.mins >= 0) {
1699 minx = *(float *)(e + g_net.mins);
1700 miny = *(float *)(e + g_net.mins + 4);
1701 minz = *(float *)(e + g_net.mins + 8);
1702 }
1703 if (g_net.maxs >= 0) {
1704 maxx = *(float *)(e + g_net.maxs);
1705 maxy = *(float *)(e + g_net.maxs + 4);
1706 maxz = *(float *)(e + g_net.maxs + 8);
1707 }
1708 // Reject absurd boxes in both directions; a garbage mins value that is
1709 // hugely negative would otherwise produce an AABB spanning the map.
1710 if (!(minz > -512.0f && maxz > minz + 4.0f && maxz < 512.0f)) {
1711 minz = 0.0f;
1712 maxz = 72.0f;
1713 }
1714 if (!(minx > -512.0f && maxx > minx + 1.0f && maxx < 256.0f)) {
1715 minx = -16.0f;
1716 maxx = 16.0f;
1717 }
1718 if (!(miny > -512.0f && maxy > miny + 1.0f && maxy < 256.0f)) {
1719 miny = -16.0f;
1720 maxy = 16.0f;
1721 }
1722
1723 Vector lo{origin.x + minx, origin.y + miny, origin.z + minz};
1724 Vector hiV{origin.x + maxx, origin.y + maxy, origin.z + maxz};
1725 Vector closest{g_cam_eye.x, g_cam_eye.y, g_cam_eye.z};
1726 if (closest.x < lo.x) closest.x = lo.x;
1727 if (closest.x > hiV.x) closest.x = hiV.x;
1728 if (closest.y < lo.y) closest.y = lo.y;
1729 if (closest.y > hiV.y) closest.y = hiV.y;
1730 if (closest.z < lo.z) closest.z = lo.z;
1731 if (closest.z > hiV.z) closest.z = hiV.z;
1732 float d = vec_dist(g_cam_eye, closest);
1733 if (d > max_range)
1734 continue;
1735
1736 Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f,
1737 (lo.z + hiV.z) * 0.5f};
1738
1739 int pos = n;
1740 while (pos > 0 && cands[pos - 1].dist > d)
1741 pos--;
1742 if (n < 16)
1743 n++;
1744 else if (pos >= 16)
1745 continue;
1746 for (int j = n - 1; j > pos; j--)
1747 cands[j] = cands[j - 1];
1748 cands[pos] = {idx, d, closest, center};
1749 }
1750
1751 for (int ci = 0; ci < n; ci++) {
1752 const Cand &c = cands[ci];
1753 Vector aim{};
1754 bool have_aim = false;
1755
1756 // Prefer the nearest visible hitbox center. This handles small NPCs
1757 // (headcrabs, antlions) whose AABB center can be less useful than the
1758 // actual body hitboxes.
1759 void *tent = entitylist_get(c.idx);
1760 if (tent && bones_update(c.idx, tent)) {
1761 Vector pts[32];
1762 int np = aim_points(c.idx, 0xFE, pts, 32);
1763 float bestd = 1e9f;
1764 for (int i = 0; i < np; i++) {
1765 float pd = vec_dist(g_cam_eye, pts[i]);
1766 if (pd > max_range || pd >= bestd)
1767 continue;
1768 if (!is_visible(g_cam_eye, pts[i], local_ent, tent))
1769 continue;
1770 bestd = pd;
1771 aim = pts[i];
1772 have_aim = true;
1773 }
1774 }
1775
1776 if (!have_aim) {
1777 aim = c.center;
1778 float ad = vec_dist(g_cam_eye, aim);
1779 if (ad > max_range) {
1780 // The body is in range but its center is not: aim just inside the
1781 // closest point on the AABB.
1782 aim = c.closest;
1783 aim.x += (c.center.x - c.closest.x) * 0.25f;
1784 aim.y += (c.center.y - c.closest.y) * 0.25f;
1785 aim.z += (c.center.z - c.closest.z) * 0.25f;
1786 ad = vec_dist(g_cam_eye, aim);
1787 if (ad > max_range)
1788 aim = c.closest;
1789 }
1790 if (!is_visible(g_cam_eye, aim, local_ent, tent))
1791 continue;
1792 }
1793
1794 QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z};
1795 QAngle desired = angle_to(g_cam_eye, aim);
1796 // The crowbar uses GetAutoaimVector (adds punch); the gravity gun's
1797 // launch uses EyeVectors (no punch), so only compensate for melee.
1798 float punch_x = 0.0f, punch_y = 0.0f;
1799 if (!gravity && punch_angles(punch_x, punch_y)) {
1800 desired.x -= punch_x;
1801 desired.y -= punch_y;
1802 }
1803 float np = desired.x;
1804 if (np > 89.0f) np = 89.0f;
1805 if (np < -89.0f) np = -89.0f;
1806 float ny = desired.y;
1807 while (ny > 180.0f) ny -= 360.0f;
1808 while (ny < -180.0f) ny += 360.0f;
1809
1810 preserve_movement(cmd, orig.y, ny);
1811 cmd->viewangles[0] = np;
1812 cmd->viewangles[1] = ny;
1813 cmd->viewangles[2] = orig.z;
1814 set_silent_restore(orig);
1815 if (g_iface.engine) {
1816 QAngle restore{orig.x, orig.y, orig.z};
1817 using SetViewFn = void (*)(void *, QAngle *);
1818 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore);
1819 }
1820 // Gravity gun primary attack launches the held object (EyeVectors);
1821 // secondary would drop it instead.
1822 if (fire_edge_ok(cmd))
1823 cmd->buttons |= IN_ATTACK;
1824
1825 static int logs = 0;
1826 if (logs < 10) {
1827 logs++;
1828 logd("melee: %s idx=%d dist=%.1f aim=(%.1f %.1f %.1f)",
1829 gravity ? "punt" : "swing", c.idx, c.dist, aim.x, aim.y, aim.z);
1830 }
1831 return true;
1832 }
1833
1834 static double last_none = 0.0;
1835 double now = esp_now();
1836 if (now - last_none >= 2.0) {
1837 last_none = now;
1838 logd("melee: no visible target within %.0f", max_range);
1839 }
1840 return false;
1841}
1842
1843// ---------------------------------------------------------------------------
1844// weapon spread control
1845// ---------------------------------------------------------------------------
1846//
1847// HL2's CBaseEntity::FireBullets uses CBaseEntity::GetPredictionRandomSeed()
1848// (the client usercmd's random_seed) unless a weapon opts into
1849// m_bUseServerRandomSeed. HL2 weapons do not, so the client can choose the
1850// 8-bit seed and with it the random cone offset. CShotManipulator::ApplySpread
1851// draws four uniform floats per attempt; we precompute the best seed for one
1852// shot and for the shotgun's pellet windows with the engine's own RNG.
1853
1854static bool g_spread_ready = false;
1855static int g_spread_seed1 = 0;
1856static int g_spread_seed8 = 0; // first pellet exact, 7 spread
1857static int g_spread_seed12 = 0; // all 12 spread
1858int g_spread_pellets = 1;
1859
1860static void spread_compute() {
1861 using SeedFn = void (*)(int);
1862 using FloatFn = float (*)(float, float);
1863 SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed");
1864 FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat");
1865 if (!seed || !rf) {
1866 size_t slash = mod_engine().path.find_last_of('/');
1867 if (slash != std::string::npos) {
1868 std::string path = mod_engine().path.substr(0, slash + 1) + "libvstdlib.so";
1869 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1870 if (!h)
1871 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
1872 if (h) {
1873 seed = (SeedFn)dlsym(h, "RandomSeed");
1874 rf = (FloatFn)dlsym(h, "RandomFloat");
1875 }
1876 }
1877 }
1878 if (!seed || !rf) {
1879 logf("spread: RandomSeed/RandomFloat unavailable");
1880 return;
1881 }
1882
1883 auto draw = [&](int s, float &x, float &y) {
1884 seed(s);
1885 int it = 0;
1886 do {
1887 // CShotManipulator::ApplySpread with the default bias of 1.0:
1888 // shotBias = 1.0, flatness = 0.5.
1889 x = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f;
1890 y = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f;
1891 it++;
1892 } while (x * x + y * y > 1.0f && it < 64);
1893 };
1894
1895 float best1 = 1e9f;
1896 for (int s = 0; s < 256; s++) {
1897 float x, y;
1898 draw(s, x, y);
1899 float r2 = x * x + y * y;
1900 if (r2 < best1) {
1901 best1 = r2;
1902 g_spread_seed1 = s;
1903 }
1904 }
1905
1906 auto window = [&](int n, int first) {
1907 int best_s = 0;
1908 float best = 1e9f;
1909 for (int s = 0; s < 256; s++) {
1910 float score = 0.0f;
1911 for (int i = first; i < n; i++) {
1912 float x, y;
1913 draw(s + i, x, y);
1914 score += x * x + y * y;
1915 }
1916 if (score < best) {
1917 best = score;
1918 best_s = s;
1919 }
1920 }
1921 return best_s;
1922 };
1923 g_spread_seed8 = window(8, 1);
1924 g_spread_seed12 = window(12, 0);
1925 seed(0);
1926
1927 logd("spread: seeds single=%d shotgun8=%d shotgun12=%d", g_spread_seed1,
1928 g_spread_seed8, g_spread_seed12);
1929 g_spread_ready = true;
1930}
1931
1932static bool active_weapon_is_shotgun(void *local_ent) {
1933 if (!local_ent || g_net.activeweapon < 0)
1934 return false;
1935 uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon);
1936 if (h == 0xFFFFFFFFu)
1937 return false;
1938 int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index
1939 if (idx <= 0 || idx > 4096)
1940 return false;
1941 void *wep = entitylist_get(idx);
1942 void *net = entity_networkable(idx);
1943 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
1944 const char *model = wep ? entity_model_name(wep) : nullptr;
1945 return (cls && strstr(cls, "Shotgun")) ||
1946 (model && strstr(model, "shotgun"));
1947}
1948
1949bool spread_control_apply(CUserCmd *cmd, void *local_ent) {
1950 if (!feature_active(FEAT_SPREAD) || !spread_no_spread_enabled())
1951 return false;
1952 if (!(cmd->buttons & (IN_ATTACK | IN_ATTACK2)))
1953 return false;
1954 if (!g_spread_ready) {
1955 static bool tried = false;
1956 if (!tried) {
1957 tried = true;
1958 spread_compute();
1959 }
1960 }
1961 if (!g_spread_ready)
1962 return false;
1963
1964 bool shotgun = active_weapon_is_shotgun(local_ent);
1965 int pellets = 1;
1966 if (shotgun)
1967 pellets = (cmd->buttons & IN_ATTACK2) ? 12 : 8;
1968 g_spread_pellets = pellets;
1969 int seed = spread_best_seed(pellets);
1970 if (cmd->random_seed == seed)
1971 return false;
1972 cmd->random_seed = seed;
1973
1974 static int logs = 0;
1975 if (logs < 10) {
1976 logs++;
1977 logd("spread: seed=%d pellets=%d", seed, pellets);
1978 }
1979 return true;
1980}
1981
1982int spread_best_seed(int pellets) {
1983 if (!g_spread_ready)
1984 return 33;
1985 if (pellets >= 12)
1986 return g_spread_seed12;
1987 if (pellets >= 8)
1988 return g_spread_seed8;
1989 return g_spread_seed1;
1990}
1991
1992bool triggerbot_apply(CUserCmd *cmd) {
1993 if (!g_iface.enginetrace || !g_iface.entitylist)
1994 return false;
1995 int local = engine_get_local_player();
1996 if (local <= 0)
1997 return false;
1998 void *local_ent = entitylist_get(local);
1999 if (!local_ent)
2000 return false;
2001 if (g_net.health >= 0 &&
2002 *(int *)((uintptr_t)local_ent + g_net.health) <= 0)
2003 return false;
2004
2005 QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f};
2006 float punch_x = 0.0f, punch_y = 0.0f;
2007 if (punch_angles(punch_x, punch_y)) {
2008 view.x += punch_x;
2009 view.y += punch_y;
2010 }
2011 Vector fwd = angle_forward_vector(view);
2012 Vector start = g_cam_eye;
2013 Vector end{start.x + fwd.x * 8192.0f, start.y + fwd.y * 8192.0f,
2014 start.z + fwd.z * 8192.0f};
2015 void *hit = trace_entity(start, end, local_ent);
2016 if (!hit)
2017 return false;
2018
2019 int idx = entptr_lookup(hit);
2020 if (idx <= 0) {
2021 // Cache miss: ESP is off, or this entity appeared since the last scan.
2022 using Fn = int (*)(void *);
2023 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
2024 if (hi < 0 || hi > 4096)
2025 hi = 4096;
2026 for (int i = 1; i <= hi; i++) {
2027 if (entitylist_get(i) != hit)
2028 continue;
2029 idx = i;
2030 entptr_store(hit, i);
2031 break;
2032 }
2033 if (idx <= 0)
2034 return false;
2035 }
2036 void *net = entity_networkable(idx);
2037 if (entity_is_dormant(net))
2038 return false;
2039 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
2040 if (!cls || !is_npc_class(cls))
2041 return false;
2042 uintptr_t e = (uintptr_t)hit;
2043 int life = g_net.lifestate >= 0
2044 ? (int)*(unsigned char *)(e + g_net.lifestate)
2045 : -1;
2046 if (life != LIFE_ALIVE)
2047 return false;
2048 const char *model = entity_model_name(hit);
2049 bool neutral = false;
2050 if (npc_is_friendly(idx, model, local, neutral) || neutral)
2051 return false;
2052 if (!npc_can_be_damaged(idx, model))
2053 return false;
2054 if (fire_edge_ok(cmd))
2055 cmd->buttons |= IN_ATTACK;
2056 static int logs = 0;
2057 if (logs < 5) {
2058 logs++;
2059 logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?");
2060 }
2061 return true;
2062}
2063 550
2064// Dynamic props are tinted through their client render colour. StudioRender 551// Dynamic props are tinted through their client render colour. StudioRender
2065// feeds m_clrRender into the per-model colour modulation each draw, which is 552// feeds m_clrRender into the per-model colour modulation each draw, which is
diff --git a/src/esp_internal.h b/src/esp_internal.h
index 6e66f42..1ffdd7a 100644
--- a/src/esp_internal.h
+++ b/src/esp_internal.h
@@ -11,6 +11,29 @@
11 11
12extern int g_scr_w, g_scr_h; 12extern int g_scr_w, g_scr_h;
13 13
14enum { LIFE_ALIVE = 0 };
15enum NpcDamageClass {
16 NPC_DMG_NORMAL = 0,
17 NPC_DMG_ARMORED, // only blast damage (RPG) hurts it
18 NPC_DMG_VULNERABLE, // gravity gun / physics damage works well
19 NPC_DMG_IMMUNE // m_takedamage says no damage at all
20};
21
22bool is_npc_class(const char *name);
23bool npc_is_friendly(int idx, const char *model, int local_idx, bool &neutral);
24bool npc_can_be_damaged(int idx, const char *model);
25int npc_damage_class(int idx, const char *model);
26QAngle angle_to(const Vector &from, const Vector &to);
27bool punch_angles(float &px, float &py);
28int entptr_lookup(void *ent);
29void entptr_store(void *ent, int idx);
30void weapon_short_name(int idx, char *out, size_t outn);
31void short_name(const char *model, char *out, size_t outn);
32
33// Listen-server sanity latch, shared with esp_collect().
34extern bool g_srv_checked;
35extern bool g_srv_trusted;
36
14double esp_now(); 37double esp_now();
15bool esp_update_screen(); 38bool esp_update_screen();
16 39