summaryrefslogtreecommitdiffstats
path: root/src/esp.cpp
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 /src/esp.cpp
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.
Diffstat (limited to '')
-rw-r--r--src/esp.cpp1519
1 files changed, 3 insertions, 1516 deletions
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