summaryrefslogtreecommitdiffstats
path: root/src/combat.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/combat.cpp')
-rw-r--r--src/combat.cpp1066
1 files changed, 1066 insertions, 0 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}