summaryrefslogtreecommitdiffstats
path: root/src/esp.cpp
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:09:55 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:09:55 +0100
commit33f5ed9ef58f26c6b3b52cf605dd4311d658a60d (patch)
treea97254872e465c9107b7ef19695ab56bb076d7e2 /src/esp.cpp
parentItems: log the usable ammo table once (diff)
downloadhl2h-33f5ed9ef58f26c6b3b52cf605dd4311d658a60d.tar.gz
hl2h-33f5ed9ef58f26c6b3b52cf605dd4311d658a60d.zip
Fix audit findings: crash safety, correctness, rendering, performance
Crash/UB: - gate every hook behind an init-stage flag until config/surface/input are ready - publish module maps as immutable atomic snapshots instead of reallocating shared vectors under unlocked readers - validate the %ebp walk in the RandomSeed/RandomFloat hooks against the stack - bounds-check every RecvTable/RecvProp/string dereference with module_range() - preserve the Itanium vtable header when cloning (client, CInput, model render, studio render) - reject negative usercmd sequence numbers; guard config_load against a null section target; cap the bone parent walk; check mprotect; drop PROT_EXEC on the GOT patch; absorb server.so .bss tail Correctness: - filter dormant entities across ESP, aimbot, triggerbot, melee, items and projectiles (slot self-tested against the local player) - reset the server-health latch per map and only latch on valid model indices - bone cache sized for the 128-target scan; set valid=false on every failure; head_position keeps searching past an invalid bone - vischeck steps past glass by probing until clear, and ignores entities owned by the target (held weapons, bone followers) - aimbot: maxs no longer disables it, AABB-aware FOV prefilter, lock hysteresis, lead point re-checked for FOV and visibility - spread: seed hook arms on the no-spread toggle alone; engine toggle only controls the RandomFloat zeroing - resolve m_hOwnerEntity/m_hOwner; NPC lead velocity from the server entity - vector_angles pitch range, point_ground normal test, underwater jump+duck no longer skips combat, semi-auto press/release edge for synthetic fire - item/projectile markers and blast rings refresh origin per frame while discovery stays at 10 Hz; trajectory uses the real m_DmgRadius and honours the blast-radius toggle Rendering/GUI: - one overlay draw per engine frame (FRAME_RENDER_START boundary), fixing alpha stacking and the speed-graph flicker - clipped lines/outlines and ellipsised text instead of drawing past the content pane or shifting over neighbours - dropdown dismissal consumes the click; hover ignores hidden controls - chams: reset AlphaModulate, reference shared materials once, restore prop tint colour; fonts created once per handle - projection uses Source's 4:3 aspect scaling and roll; screen size refreshed each frame; FOV circle gated on the aim toggles, not the bind Performance: - TTL caches for NPC relation, friendly/neutral and damage class - model names memoised per model index - entity pointer -> index cache for the triggerbot (was a 4096-slot walk/tick) Build: - real Makefile with -MMD dependency tracking, debug/release objects and an always-relink rule; build.sh kept as a thin wrapper
Diffstat (limited to '')
-rw-r--r--src/esp.cpp582
1 files changed, 448 insertions, 134 deletions
diff --git a/src/esp.cpp b/src/esp.cpp
index 72110cc..81ec81c 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -7,12 +7,6 @@
7#include <cstdlib> 7#include <cstdlib>
8#include <ctime> 8#include <ctime>
9 9
10// ISurface slots (binary = SDK header + 4, disassembly-confirmed)
11enum SurfSlot {
12 SURF_DrawSetColor = 10, SURF_FilledRect = 12, SURF_OutlinedRect = 14,
13 SURF_DrawLine = 15, SURF_GetScreenSize = 38,
14};
15
16enum { LIFE_ALIVE = 0 }; 10enum { LIFE_ALIVE = 0 };
17 11
18struct EspTarget { 12struct EspTarget {
@@ -40,6 +34,36 @@ static int g_ntargets = 0;
40static int g_scr_w = 0, g_scr_h = 0; 34static int g_scr_w = 0, g_scr_h = 0;
41static bool g_esp_logged = false; 35static bool g_esp_logged = false;
42 36
37// Listen-server sanity latch. Only ever set once per map (esp_reset_session),
38// and only after both model indices are valid: latching on the transient
39// cli_model == 0 at map load disabled server health for the whole process.
40static bool g_srv_checked = false;
41static bool g_srv_trusted = false;
42
43// Per-map caches. The ESP resolves a model name for every entity on every scan
44// and the chams hook asks for a relation on every model draw; both are virtual
45// call chains into the engine plus server datamap reads, so memoise them.
46static constexpr int MODEL_CACHE = 16384;
47static const char *g_model_name_cache[MODEL_CACHE];
48static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail
49
50// Direct-mapped entity pointer -> index map, filled by esp_collect. The
51// triggerbot traces a ray and gets a pointer back, and used to find its index
52// by walking up to 4096 entity-list slots on every fire tick.
53static constexpr int ENTPTR_CACHE = 4096;
54static void *g_entptr_key[ENTPTR_CACHE];
55static int g_entptr_idx[ENTPTR_CACHE];
56
57void esp_reset_session() {
58 g_srv_checked = false;
59 g_srv_trusted = false;
60 g_esp_logged = false;
61 memset(g_model_name_cache, 0, sizeof(g_model_name_cache));
62 memset(g_model_state_cache, 0, sizeof(g_model_state_cache));
63 memset(g_entptr_key, 0, sizeof(g_entptr_key));
64 memset(g_entptr_idx, 0, sizeof(g_entptr_idx));
65}
66
43// Speed graph: 100 samples at 0.1 s = the last 10 seconds. 67// Speed graph: 100 samples at 0.1 s = the last 10 seconds.
44static constexpr int SPEED_SAMPLES = 100; 68static constexpr int SPEED_SAMPLES = 100;
45static float g_speed_samples[SPEED_SAMPLES] = {}; 69static float g_speed_samples[SPEED_SAMPLES] = {};
@@ -52,6 +76,21 @@ static double esp_now() {
52 return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; 76 return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
53} 77}
54 78
79// Refresh the cached screen size every frame. It used to be read once, so a
80// resolution or window change left every projection, screen-edge clamp and
81// centering calculation stale for the rest of the session.
82static bool esp_update_screen() {
83 if (!g_surface)
84 return false;
85 int w = 0, h = 0;
86 overlay_screen_size(w, h);
87 if (w > 0)
88 g_scr_w = w;
89 if (h > 0)
90 g_scr_h = h;
91 return g_scr_w > 0 && g_scr_h > 0;
92}
93
55// Local perspective projection used when the engine's debug-overlay 94// Local perspective projection used when the engine's debug-overlay
56// projection refuses a point that is actually in front of the camera. 95// projection refuses a point that is actually in front of the camera.
57static bool project_manual(const Vector &w, float &sx, float &sy) { 96static bool project_manual(const Vector &w, float &sx, float &sy) {
@@ -59,11 +98,16 @@ static bool project_manual(const Vector &w, float &sx, float &sy) {
59 return false; 98 return false;
60 float pitch = g_cam_view.x * PI_F / 180.0f; 99 float pitch = g_cam_view.x * PI_F / 180.0f;
61 float yaw = g_cam_view.y * PI_F / 180.0f; 100 float yaw = g_cam_view.y * PI_F / 180.0f;
101 float roll = g_cam_view.z * PI_F / 180.0f;
62 float sp = sinf(pitch), cp = cosf(pitch); 102 float sp = sinf(pitch), cp = cosf(pitch);
63 float syy = sinf(yaw), cy = cosf(yaw); 103 float syy = sinf(yaw), cy = cosf(yaw);
104 float sr = sinf(roll), cr = cosf(roll);
64 float fwd[3] = {cp * cy, cp * syy, -sp}; 105 float fwd[3] = {cp * cy, cp * syy, -sp};
65 float right[3] = {syy, -cy, 0}; 106 // Source's AngleVectors with roll; the old right/up vectors assumed roll 0,
66 float up[3] = {sp * cy, sp * syy, cp}; 107 // so view punch made the fallback projection rotate against the world.
108 float right[3] = {-sr * sp * cy + cr * syy, -sr * sp * syy - cr * cy,
109 -sr * cp};
110 float up[3] = {cr * sp * cy + sr * syy, cr * sp * syy - sr * cy, cr * cp};
67 float dx = w.x - g_cam_eye.x, dy = w.y - g_cam_eye.y, dz = w.z - g_cam_eye.z; 111 float dx = w.x - g_cam_eye.x, dy = w.y - g_cam_eye.y, dz = w.z - g_cam_eye.z;
68 float z = dx * fwd[0] + dy * fwd[1] + dz * fwd[2]; 112 float z = dx * fwd[0] + dy * fwd[1] + dz * fwd[2];
69 if (z < 1.0f) 113 if (z < 1.0f)
@@ -71,7 +115,10 @@ static bool project_manual(const Vector &w, float &sx, float &sy) {
71 float x = dx * right[0] + dy * right[1] + dz * right[2]; 115 float x = dx * right[0] + dy * right[1] + dz * right[2];
72 float y = dx * up[0] + dy * up[1] + dz * up[2]; 116 float y = dx * up[0] + dy * up[1] + dz * up[2];
73 float fov = g_fov_camera > 10 ? (float)g_fov_camera : 75.0f; 117 float fov = g_fov_camera > 10 ? (float)g_fov_camera : 75.0f;
74 float tanfov = tanf(fov * 0.5f * PI_F / 180.0f); 118 // Source defines the FOV for a 4:3 reference and widens it on widescreen;
119 // using a raw tan(fov/2) made the fallback ~33% too narrow at 16:9.
120 float aspect = (float)g_scr_w / (float)g_scr_h;
121 float tanfov = tanf(fov * 0.5f * PI_F / 180.0f) * (aspect / (4.0f / 3.0f));
75 sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov); 122 sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov);
76 sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov); 123 sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov);
77 return std::isfinite(sx) && std::isfinite(sy) && fabsf(sx) < 1e7f && 124 return std::isfinite(sx) && std::isfinite(sy) && fabsf(sx) < 1e7f &&
@@ -137,16 +184,96 @@ static const char *entity_class_name(void *net, void **out_cc) {
137 return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; 184 return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr;
138} 185}
139 186
187// IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant
188// 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK,
189// so the slot is validated once against the local player, which can never be
190// dormant: a wrong slot disables the check instead of misreporting entities.
191// Dormant entities keep their last networked origin, so drawing or aiming at
192// them produces ghosts at stale positions.
193static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled
194
195static void dormant_selftest() {
196 // Leave the state untested (0) until a local player exists: the first call
197 // can come from the main menu, and latching "disabled" there would turn the
198 // check off for the whole session.
199 if (!g_iface.entitylist)
200 return;
201 int local = engine_get_local_player();
202 if (local <= 0)
203 return;
204 void *net = entity_networkable(local);
205 if (!net)
206 return;
207 void **vt = *(void ***)net;
208 if (!vt || !mod_client().contains((uintptr_t)vt[8])) {
209 g_dormant_state = -1;
210 logf("dormant check disabled: slot 8 outside client module");
211 return;
212 }
213 using DormantFn = unsigned char (*)(void *);
214 if (vfunc<DormantFn>(net, 8)(net) != 0) {
215 g_dormant_state = -1;
216 logf("dormant check disabled: local player reported dormant");
217 return;
218 }
219 g_dormant_state = 1;
220 logd("dormant check enabled");
221}
222
223static bool entity_is_dormant(void *net) {
224 if (!net)
225 return false;
226 if (g_dormant_state == 0)
227 dormant_selftest();
228 if (g_dormant_state != 1)
229 return false;
230 using DormantFn = unsigned char (*)(void *);
231 return vfunc<DormantFn>(net, 8)(net) != 0;
232}
233
234// Entity pointer -> index, filled by the entity scans. The triggerbot gets a
235// pointer back from the trace and needs the index; the alternative is walking
236// up to 4096 entity-list slots on every fire tick.
237static int entptr_lookup(void *ent) {
238 if (!ent)
239 return -1;
240 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
241 return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1;
242}
243
244static void entptr_store(void *ent, int idx) {
245 if (!ent || idx <= 0)
246 return;
247 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
248 g_entptr_key[h] = ent;
249 g_entptr_idx[h] = idx;
250}
251
140// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. 252// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName.
141// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. 253// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4.
142static const char *model_name_from_index(int modelindex) { 254static const char *model_name_from_index(int modelindex) {
143 if (!g_iface.modelinfo || modelindex <= 0) 255 if (!g_iface.modelinfo || modelindex <= 0)
144 return nullptr; 256 return nullptr;
257 if (modelindex < MODEL_CACHE) {
258 if (g_model_state_cache[modelindex] == 1)
259 return g_model_name_cache[modelindex];
260 if (g_model_state_cache[modelindex] == 2)
261 return nullptr;
262 }
145 const void *model = 263 const void *model =
146 vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); 264 vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex);
147 if (!model) 265 if (!model) {
266 if (modelindex < MODEL_CACHE)
267 g_model_state_cache[modelindex] = 2;
148 return nullptr; 268 return nullptr;
149 return vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model); 269 }
270 const char *name =
271 vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model);
272 if (modelindex < MODEL_CACHE) {
273 g_model_name_cache[modelindex] = name;
274 g_model_state_cache[modelindex] = name ? 1 : 2;
275 }
276 return name;
150} 277}
151 278
152static const char *entity_model_name(void *ent) { 279static const char *entity_model_name(void *ent) {
@@ -155,6 +282,19 @@ static const char *entity_model_name(void *ent) {
155 return model_name_from_index(modelindex); 282 return model_name_from_index(modelindex);
156} 283}
157 284
285// Live origin for a cached entity index. Item/projectile discovery is throttled
286// to 10 Hz (it walks the whole entity list), but the markers must follow the
287// world at render rate; refreshing one netvar read per cached target is cheap.
288static bool entity_origin(void *ent, Vector &out) {
289 if (!ent || g_net.origin < 0)
290 return false;
291 uintptr_t e = (uintptr_t)ent;
292 out.x = *(float *)(e + g_net.origin);
293 out.y = *(float *)(e + g_net.origin + 4);
294 out.z = *(float *)(e + g_net.origin + 8);
295 return true;
296}
297
158// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all 298// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all
159// use the single client class "CAI_BaseNPC" — only special NPCs register their 299// use the single client class "CAI_BaseNPC" — only special NPCs register their
160// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). 300// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...).
@@ -192,20 +332,35 @@ bool esp_is_viewmodel_index(int idx) {
192static bool npc_is_friendly(int idx, const char *model, int local_idx, 332static bool npc_is_friendly(int idx, const char *model, int local_idx,
193 bool &neutral); 333 bool &neutral);
194int esp_npc_relation(int idx) { 334int esp_npc_relation(int idx) {
195 if (!esp_is_npc_index(idx)) 335 // Called from the chams hook up to three times per entity per frame (visible,
196 return -1; 336 // through-wall, wireframe pass) and the work behind it is a chain of server
197 int local = engine_get_local_player(); 337 // edict/datamap reads. A short TTL cache keeps that out of the render path.
198 if (local <= 0) 338 struct RelCache {
199 return 0; 339 int idx;
200 void *ent = entitylist_get(idx); 340 double at;
201 const char *model = ent ? entity_model_name(ent) : nullptr; 341 int val;
202 bool neutral = false; 342 };
203 bool friendly = npc_is_friendly(idx, model, local, neutral); 343 static RelCache cache[256];
204 if (neutral) 344 double now = esp_now();
205 return 1; 345 RelCache &c = cache[(unsigned)idx & 255];
206 if (friendly) 346 if (c.idx == idx && now - c.at < 0.05)
207 return 2; 347 return c.val;
208 return 0; 348
349 int val = -1;
350 if (esp_is_npc_index(idx)) {
351 int local = engine_get_local_player();
352 if (local <= 0) {
353 val = 0;
354 } else {
355 void *ent = entitylist_get(idx);
356 const char *model = ent ? entity_model_name(ent) : nullptr;
357 bool neutral = false;
358 bool friendly = npc_is_friendly(idx, model, local, neutral);
359 val = neutral ? 1 : friendly ? 2 : 0;
360 }
361 }
362 c = {idx, now, val};
363 return val;
209} 364}
210 365
211static bool model_is_friendly(const char *m) { 366static bool model_is_friendly(const char *m) {
@@ -237,8 +392,8 @@ static bool model_is_floor_turret(const char *m) {
237 return m && strstr(m, "floor_turret"); 392 return m && strstr(m, "floor_turret");
238} 393}
239 394
240static bool npc_is_friendly(int idx, const char *model, int local_idx, 395static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx,
241 bool &neutral) { 396 bool &neutral) {
242 int rel = server_npc_relation(idx, local_idx); 397 int rel = server_npc_relation(idx, local_idx);
243 int state = server_npc_state(idx); 398 int state = server_npc_state(idx);
244 int turret = -1; 399 int turret = -1;
@@ -302,6 +457,28 @@ static bool npc_is_friendly(int idx, const char *model, int local_idx,
302 return friendly; 457 return friendly;
303} 458}
304 459
460// Friendly/neutral state changes slowly (hacked turrets, bugbait), while the
461// entity scan and the chams hook ask for it constantly. Cache it briefly so the
462// server lookups happen once per NPC per window instead of per call.
463static bool npc_is_friendly(int idx, const char *model, int local_idx,
464 bool &neutral) {
465 struct Cache {
466 int idx;
467 double at;
468 bool friendly;
469 bool neutral;
470 };
471 static Cache cache[256];
472 double now = esp_now();
473 Cache &c = cache[(unsigned)idx & 255];
474 if (c.idx == idx && now - c.at < 0.10) {
475 neutral = c.neutral;
476 return c.friendly;
477 }
478 bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral);
479 c = {idx, now, f, neutral};
480 return f;
481}
305 482
306enum NpcDamageClass { 483enum NpcDamageClass {
307 NPC_DMG_NORMAL = 0, 484 NPC_DMG_NORMAL = 0,
@@ -316,7 +493,7 @@ static bool model_has(const char *model, const char *needle) {
316 493
317// Server m_takedamage first (authoritative immunity), then the HL2 damage 494// Server m_takedamage first (authoritative immunity), then the HL2 damage
318// rules for units whose OnTakeDamage filters specific damage types. 495// rules for units whose OnTakeDamage filters specific damage types.
319static int npc_damage_class(int idx, const char *model) { 496static int npc_damage_class_uncached(int idx, const char *model) {
320 int td = server_npc_takedamage(idx); 497 int td = server_npc_takedamage(idx);
321 if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY 498 if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY
322 return NPC_DMG_IMMUNE; 499 return NPC_DMG_IMMUNE;
@@ -330,6 +507,24 @@ static int npc_damage_class(int idx, const char *model) {
330 return NPC_DMG_NORMAL; 507 return NPC_DMG_NORMAL;
331} 508}
332 509
510// Damage class depends on m_takedamage (a server read) plus model string
511// matching. It changes even more rarely than the relation, so cache it.
512static int npc_damage_class(int idx, const char *model) {
513 struct Cache {
514 int idx;
515 double at;
516 int val;
517 };
518 static Cache cache[256];
519 double now = esp_now();
520 Cache &c = cache[(unsigned)idx & 255];
521 if (c.idx == idx && now - c.at < 0.25)
522 return c.val;
523 int v = npc_damage_class_uncached(idx, model);
524 c = {idx, now, v};
525 return v;
526}
527
333static bool active_weapon_contains(void *local_ent, const char *needle); 528static bool active_weapon_contains(void *local_ent, const char *needle);
334 529
335// Armoured units need blast damage; the RPG is the only stock weapon that 530// Armoured units need blast damage; the RPG is the only stock weapon that
@@ -430,15 +625,14 @@ static void esp_collect(int local_idx) {
430 if (hi < 0 || hi > 4096) hi = 4096; 625 if (hi < 0 || hi > 4096) hi = 4096;
431 int npc_alive = 0, dbg_lines = 0; 626 int npc_alive = 0, dbg_lines = 0;
432 bool srv = resolve_server_netvars(); 627 bool srv = resolve_server_netvars();
433 static bool srv_checked = false;
434 static bool srv_trusted = false;
435 628
436 for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) { 629 for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) {
437 if (idx == local_idx) continue; 630 if (idx == local_idx) continue;
438 void *ent = entitylist_get(idx); 631 void *ent = entitylist_get(idx);
439 if (!ent) continue; 632 if (!ent) continue;
633 entptr_store(ent, idx);
440 void *net = entity_networkable(idx); 634 void *net = entity_networkable(idx);
441 if (!net) continue; 635 if (!net || entity_is_dormant(net)) continue;
442 const char *cls = entity_class_name(net, nullptr); 636 const char *cls = entity_class_name(net, nullptr);
443 if (!cls) continue; 637 if (!cls) continue;
444 638
@@ -456,19 +650,24 @@ static void esp_collect(int local_idx) {
456 int slife = -1, smax = -1; 650 int slife = -1, smax = -1;
457 void *sbase = nullptr; 651 void *sbase = nullptr;
458 int shp = server_entity_health(idx, &smax, &slife, &sbase); 652 int shp = server_entity_health(idx, &smax, &slife, &sbase);
459 if (!srv_checked && sbase) { 653 if (!g_srv_checked && sbase) {
460 srv_checked = true;
461 int cli_mi = 654 int cli_mi =
462 g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1; 655 g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1;
463 int srv_mi = server_model_index(sbase); 656 int srv_mi = server_model_index(sbase);
464 srv_trusted = (cli_mi == srv_mi); 657 // Wait for both indices: the client index is often 0 for a tick right
465 logd("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d " 658 // after a map load, which used to latch "untrusted" permanently.
466 "hp=%d maxhp=%d life=%d trusted=%d", 659 if (cli_mi > 0 && srv_mi > 0) {
467 idx, sbase, cli_mi, srv_mi, shp, smax, slife, (int)srv_trusted); 660 g_srv_checked = true;
468 if (!srv_trusted) 661 g_srv_trusted = (cli_mi == srv_mi);
469 logf("ESP: server entity mismatch, ignoring server values"); 662 logd("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d "
663 "hp=%d maxhp=%d life=%d trusted=%d",
664 idx, sbase, cli_mi, srv_mi, shp, smax, slife,
665 (int)g_srv_trusted);
666 if (!g_srv_trusted)
667 logf("ESP: server entity mismatch, ignoring server values");
668 }
470 } 669 }
471 if (srv_trusted) { 670 if (g_srv_trusted) {
472 if (shp >= 0) hp = shp; 671 if (shp >= 0) hp = shp;
473 if (smax > 0) maxhp = smax; 672 if (smax > 0) maxhp = smax;
474 if (slife >= 0) life = slife; 673 if (slife >= 0) life = slife;
@@ -576,16 +775,16 @@ static void esp_collect(int local_idx) {
576} 775}
577 776
578static void surf_rect(int filled, int x0, int y0, int x1, int y1) { 777static void surf_rect(int filled, int x0, int y0, int x1, int y1) {
579 using Fn = void (*)(void *, int, int, int, int); 778 if (filled)
580 vfunc<Fn>(g_surface, filled ? SURF_FilledRect : SURF_OutlinedRect)(g_surface, x0, y0, x1, y1); 779 overlay_fill(x0, y0, x1, y1);
780 else
781 overlay_outline_rect(x0, y0, x1, y1);
581} 782}
582static void surf_color(int r, int g, int b, int a) { 783static void surf_color(int r, int g, int b, int a) {
583 using Fn = void (*)(void *, int, int, int, int); 784 overlay_color(r, g, b, a);
584 vfunc<Fn>(g_surface, SURF_DrawSetColor)(g_surface, r, g, b, a);
585} 785}
586static void surf_line(int x0, int y0, int x1, int y1) { 786static void surf_line(int x0, int y0, int x1, int y1) {
587 using Fn = void (*)(void *, int, int, int, int); 787 overlay_line(x0, y0, x1, y1);
588 vfunc<Fn>(g_surface, SURF_DrawLine)(g_surface, x0, y0, x1, y1);
589} 788}
590 789
591// Screen projection of the aimbot FOV cone boundary, using the engine's own 790// Screen projection of the aimbot FOV cone boundary, using the engine's own
@@ -599,8 +798,14 @@ static QAngle engine_view_angles() {
599 return v; 798 return v;
600} 799}
601 800
602static void draw_fov_circle() { 801void esp_draw_fov_circle() {
603 if (!aimbot_fov_circle()) 802 if (!g_surface || !aimbot_fov_circle())
803 return;
804 // No local player means no game world (main menu); the cached camera is stale
805 // and the projected circle would land at a meaningless spot.
806 if (engine_get_local_player() <= 0)
807 return;
808 if (!esp_update_screen())
604 return; 809 return;
605 float fov = (float)aimbot_fov(); 810 float fov = (float)aimbot_fov();
606 if (fov < 1.0f || fov >= 89.0f) // beyond this the "circle" is behind the eye 811 if (fov < 1.0f || fov >= 89.0f) // beyond this the "circle" is behind the eye
@@ -812,7 +1017,9 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
812 out_idx = -1; 1017 out_idx = -1;
813 out_point = {}; 1018 out_point = {};
814 out_fov = 0.0f; 1019 out_fov = 0.0f;
815 if (!g_iface.entitylist || g_net.origin < 0 || g_net.maxs < 0) 1020 // maxs is optional: the per-target code falls back to 72 units when the
1021 // netvar is missing, so it must not disable the whole aimbot here.
1022 if (!g_iface.entitylist || g_net.origin < 0)
816 return false; 1023 return false;
817 int local = engine_get_local_player(); 1024 int local = engine_get_local_player();
818 if (local <= 0) 1025 if (local <= 0)
@@ -830,7 +1037,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
830 bool rpg = active_weapon_contains(local_ent, "rocket_launcher") || 1037 bool rpg = active_weapon_contains(local_ent, "rocket_launcher") ||
831 active_weapon_contains(local_ent, "rpg"); 1038 active_weapon_contains(local_ent, "rpg");
832 1039
833 auto try_target = [&](int idx, bool enforce_fov, Vector &pt, 1040 // fov_limit <= 0 means "no angular limit" (kept for unlimited acquisition).
1041 auto try_target = [&](int idx, float fov_limit, Vector &pt,
834 float &fov) -> bool { 1042 float &fov) -> bool {
835 if (idx <= 0 || idx == local) 1043 if (idx <= 0 || idx == local)
836 return false; 1044 return false;
@@ -838,7 +1046,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
838 if (!ent) 1046 if (!ent)
839 return false; 1047 return false;
840 void *net = entity_networkable(idx); 1048 void *net = entity_networkable(idx);
841 if (!net) 1049 if (!net || entity_is_dormant(net))
842 return false; 1050 return false;
843 const char *cls = entity_class_name(net, nullptr); 1051 const char *cls = entity_class_name(net, nullptr);
844 if (!cls || !is_npc_class(cls)) 1052 if (!cls || !is_npc_class(cls))
@@ -863,7 +1071,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
863 Vector best_pt{}; 1071 Vector best_pt{};
864 for (int i = 0; i < npoints; i++) { 1072 for (int i = 0; i < npoints; i++) {
865 float f = getfov(view, angle_to(g_cam_eye, pts[i])); 1073 float f = getfov(view, angle_to(g_cam_eye, pts[i]));
866 if (enforce_fov && f > max_fov) 1074 if (fov_limit > 0.0f && f > fov_limit)
867 continue; 1075 continue;
868 if (f >= best) 1076 if (f >= best)
869 continue; 1077 continue;
@@ -880,11 +1088,13 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
880 }; 1088 };
881 1089
882 // Keep the locked target while it is alive and has a visible selected point. 1090 // Keep the locked target while it is alive and has a visible selected point.
883 // No FOV limit on a locked target; only acquisition is FOV-limited. 1091 // The lock uses a wider cone than acquisition (hysteresis), so a target that
1092 // drifts slightly off the crosshair is not dropped, but one that swings
1093 // behind the player is: an unbounded lock let silent aim fire backwards.
884 if (lock_idx && *lock_idx >= 0) { 1094 if (lock_idx && *lock_idx >= 0) {
885 Vector pt; 1095 Vector pt;
886 float f; 1096 float f;
887 if (try_target(*lock_idx, false, pt, f)) { 1097 if (try_target(*lock_idx, max_fov * 1.5f, pt, f)) {
888 out_idx = *lock_idx; 1098 out_idx = *lock_idx;
889 out_point = pt; 1099 out_point = pt;
890 out_fov = f; 1100 out_fov = f;
@@ -920,7 +1130,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
920 if (!ent) 1130 if (!ent)
921 continue; 1131 continue;
922 void *net = entity_networkable(idx); 1132 void *net = entity_networkable(idx);
923 if (!net) 1133 if (!net || entity_is_dormant(net))
924 continue; 1134 continue;
925 const char *cls = entity_class_name(net, nullptr); 1135 const char *cls = entity_class_name(net, nullptr);
926 if (!cls || !is_npc_class(cls)) 1136 if (!cls || !is_npc_class(cls))
@@ -949,7 +1159,17 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
949 Vector approx{origin.x, origin.y, 1159 Vector approx{origin.x, origin.y,
950 origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)}; 1160 origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)};
951 float fov = getfov(view, angle_to(g_cam_eye, approx)); 1161 float fov = getfov(view, angle_to(g_cam_eye, approx));
952 if (fov > max_fov) 1162 // The prefilter samples a single point, but the selected hitbox can be a
1163 // whole body-width away at close range. Reject only when the target's
1164 // angular radius cannot reach the cone; try_target still enforces the
1165 // exact per-hitbox FOV.
1166 float ddx = approx.x - g_cam_eye.x;
1167 float ddy = approx.y - g_cam_eye.y;
1168 float ddz = approx.z - g_cam_eye.z;
1169 float dist = sqrtf(ddx * ddx + ddy * ddy + ddz * ddz);
1170 float margin =
1171 dist > 1.0f ? atan2f(maxz, dist) * 180.0f / PI_F : 90.0f;
1172 if (fov - margin > max_fov)
953 continue; 1173 continue;
954 float key = (rpg && armored) ? fov - 1000.0f : fov; 1174 float key = (rpg && armored) ? fov - 1000.0f : fov;
955 1175
@@ -977,7 +1197,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
977 for (int ci = 0; ci < ncands; ci++) { 1197 for (int ci = 0; ci < ncands; ci++) {
978 Vector pt; 1198 Vector pt;
979 float f; 1199 float f;
980 if (!try_target(cands[ci].idx, true, pt, f)) 1200 if (!try_target(cands[ci].idx, max_fov, pt, f))
981 continue; 1201 continue;
982 out_idx = cands[ci].idx; 1202 out_idx = cands[ci].idx;
983 out_point = pt; 1203 out_point = pt;
@@ -1043,9 +1263,28 @@ static bool projectile_lead_point(int target_idx, const Vector &point,
1043 return false; 1263 return false;
1044 } 1264 }
1045 void *target = entitylist_get(target_idx); 1265 void *target = entitylist_get(target_idx);
1046 if (!target || g_net.velocity < 0) 1266 if (!target)
1267 return false;
1268 // NPCs do not network C_BaseEntity::m_vecVelocity (only the player does), so
1269 // the client field is stale for exactly the targets the aimbot uses. Read
1270 // the listen-server entity when available and fall back to the client value.
1271 float v[3] = {0.0f, 0.0f, 0.0f};
1272 Vector srv_vel;
1273 bool have_vel = false;
1274 if (server_entity_velocity(target_idx, &srv_vel)) {
1275 v[0] = srv_vel.x;
1276 v[1] = srv_vel.y;
1277 v[2] = srv_vel.z;
1278 have_vel = true;
1279 } else if (g_net.velocity >= 0) {
1280 const float *cv = (const float *)((uintptr_t)target + g_net.velocity);
1281 v[0] = cv[0];
1282 v[1] = cv[1];
1283 v[2] = cv[2];
1284 have_vel = true;
1285 }
1286 if (!have_vel)
1047 return false; 1287 return false;
1048 const float *v = (const float *)((uintptr_t)target + g_net.velocity);
1049 float dx = point.x - g_cam_eye.x; 1288 float dx = point.x - g_cam_eye.x;
1050 float dy = point.y - g_cam_eye.y; 1289 float dy = point.y - g_cam_eye.y;
1051 float dz = point.z - g_cam_eye.z; 1290 float dz = point.z - g_cam_eye.z;
@@ -1058,6 +1297,51 @@ static bool projectile_lead_point(int target_idx, const Vector &point,
1058 return true; 1297 return true;
1059} 1298}
1060 1299
1300// A led point is a prediction, not a verified aim point: acquisition checked
1301// the un-led position, so the shift has to pass the same FOV and visibility
1302// tests. On failure the caller keeps the original point instead of firing into
1303// geometry or outside the configured cone.
1304static bool lead_point_ok(int target_idx, const Vector &lead,
1305 const QAngle &view, float max_fov) {
1306 if (getfov(view, angle_to(g_cam_eye, lead)) > max_fov)
1307 return false;
1308 if (!aimbot_vischeck_enabled())
1309 return true;
1310 void *local_ent = entitylist_get(engine_get_local_player());
1311 void *target_ent = entitylist_get(target_idx);
1312 if (!local_ent || !target_ent)
1313 return false;
1314 return is_visible(g_cam_eye, lead, local_ent, target_ent);
1315}
1316
1317// Semi-auto weapons (pistol, .357, crossbow, shotgun, RPG, gravity gun) only
1318// fire on the IN_ATTACK press edge, so a synthetic button held every tick fires
1319// exactly once per buy/equip. Alternate press/release for those; automatic
1320// weapons keep the button held.
1321static bool weapon_is_semi_auto(void *local_ent) {
1322 static const char *names[] = {"pistol", "357",
1323 "crossbow", "shotgun",
1324 "rocket_launcher", "physcannon",
1325 "crowbar", nullptr};
1326 for (int i = 0; names[i]; i++)
1327 if (active_weapon_contains(local_ent, names[i]))
1328 return true;
1329 return false;
1330}
1331
1332static bool fire_edge_ok(CUserCmd *cmd) {
1333 void *local_ent = entitylist_get(engine_get_local_player());
1334 if (!local_ent || !weapon_is_semi_auto(local_ent))
1335 return true;
1336 static int last_tick = -1;
1337 static bool phase = false;
1338 if (cmd->tick_count != last_tick) {
1339 last_tick = cmd->tick_count;
1340 phase = !phase;
1341 }
1342 return phase;
1343}
1344
1061bool aimbot_apply(CUserCmd *cmd) { 1345bool aimbot_apply(CUserCmd *cmd) {
1062 unsigned bone_mask = aimbot_bone_mask(); 1346 unsigned bone_mask = aimbot_bone_mask();
1063 if (!bone_mask) 1347 if (!bone_mask)
@@ -1080,7 +1364,8 @@ bool aimbot_apply(CUserCmd *cmd) {
1080 "aimbot", lock, target, point, fov)) 1364 "aimbot", lock, target, point, fov))
1081 return false; 1365 return false;
1082 Vector lead; 1366 Vector lead;
1083 if (projectile_lead_point(target, point, lead)) 1367 if (projectile_lead_point(target, point, lead) &&
1368 lead_point_ok(target, lead, view, max_fov))
1084 point = lead; 1369 point = lead;
1085 QAngle desired = angle_to(g_cam_eye, point); 1370 QAngle desired = angle_to(g_cam_eye, point);
1086 if (punch) { 1371 if (punch) {
@@ -1168,7 +1453,8 @@ bool silent_aim_apply(CUserCmd *cmd) {
1168 "silentaim", lock, target, point, fov)) 1453 "silentaim", lock, target, point, fov))
1169 return false; 1454 return false;
1170 Vector lead; 1455 Vector lead;
1171 if (projectile_lead_point(target, point, lead)) 1456 if (projectile_lead_point(target, point, lead) &&
1457 lead_point_ok(target, lead, view, max_fov))
1172 point = lead; 1458 point = lead;
1173 QAngle desired = angle_to(g_cam_eye, point); 1459 QAngle desired = angle_to(g_cam_eye, point);
1174 if (punch) { 1460 if (punch) {
@@ -1196,7 +1482,8 @@ bool silent_aim_apply(CUserCmd *cmd) {
1196 using SetViewFn = void (*)(void *, QAngle *); 1482 using SetViewFn = void (*)(void *, QAngle *);
1197 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore); 1483 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore);
1198 } 1484 }
1199 cmd->buttons |= IN_ATTACK; 1485 if (fire_edge_ok(cmd))
1486 cmd->buttons |= IN_ATTACK;
1200 static int logs = 0; 1487 static int logs = 0;
1201 if (logs < 5) { 1488 if (logs < 5) {
1202 logs++; 1489 logs++;
@@ -1379,7 +1666,7 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) {
1379 if (!ent) 1666 if (!ent)
1380 continue; 1667 continue;
1381 void *net = entity_networkable(idx); 1668 void *net = entity_networkable(idx);
1382 if (!net) 1669 if (!net || entity_is_dormant(net))
1383 continue; 1670 continue;
1384 const char *cls = entity_class_name(net, nullptr); 1671 const char *cls = entity_class_name(net, nullptr);
1385 if (!cls || !is_npc_class(cls)) 1672 if (!cls || !is_npc_class(cls))
@@ -1417,15 +1704,17 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) {
1417 maxy = *(float *)(e + g_net.maxs + 4); 1704 maxy = *(float *)(e + g_net.maxs + 4);
1418 maxz = *(float *)(e + g_net.maxs + 8); 1705 maxz = *(float *)(e + g_net.maxs + 8);
1419 } 1706 }
1420 if (!(maxz > minz + 4.0f && maxz < 512.0f)) { 1707 // Reject absurd boxes in both directions; a garbage mins value that is
1708 // hugely negative would otherwise produce an AABB spanning the map.
1709 if (!(minz > -512.0f && maxz > minz + 4.0f && maxz < 512.0f)) {
1421 minz = 0.0f; 1710 minz = 0.0f;
1422 maxz = 72.0f; 1711 maxz = 72.0f;
1423 } 1712 }
1424 if (!(maxx > minx + 1.0f && maxx < 256.0f)) { 1713 if (!(minx > -512.0f && maxx > minx + 1.0f && maxx < 256.0f)) {
1425 minx = -16.0f; 1714 minx = -16.0f;
1426 maxx = 16.0f; 1715 maxx = 16.0f;
1427 } 1716 }
1428 if (!(maxy > miny + 1.0f && maxy < 256.0f)) { 1717 if (!(miny > -512.0f && maxy > miny + 1.0f && maxy < 256.0f)) {
1429 miny = -16.0f; 1718 miny = -16.0f;
1430 maxy = 16.0f; 1719 maxy = 16.0f;
1431 } 1720 }
@@ -1529,7 +1818,8 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) {
1529 } 1818 }
1530 // Gravity gun primary attack launches the held object (EyeVectors); 1819 // Gravity gun primary attack launches the held object (EyeVectors);
1531 // secondary would drop it instead. 1820 // secondary would drop it instead.
1532 cmd->buttons |= IN_ATTACK; 1821 if (fire_edge_ok(cmd))
1822 cmd->buttons |= IN_ATTACK;
1533 1823
1534 static int logs = 0; 1824 static int logs = 0;
1535 if (logs < 10) { 1825 if (logs < 10) {
@@ -1572,9 +1862,9 @@ static void spread_compute() {
1572 SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed"); 1862 SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed");
1573 FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat"); 1863 FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat");
1574 if (!seed || !rf) { 1864 if (!seed || !rf) {
1575 size_t slash = g_engine.path.find_last_of('/'); 1865 size_t slash = mod_engine().path.find_last_of('/');
1576 if (slash != std::string::npos) { 1866 if (slash != std::string::npos) {
1577 std::string path = g_engine.path.substr(0, slash + 1) + "libvstdlib.so"; 1867 std::string path = mod_engine().path.substr(0, slash + 1) + "libvstdlib.so";
1578 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); 1868 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1579 if (!h) 1869 if (!h)
1580 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); 1870 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
@@ -1725,38 +2015,49 @@ bool triggerbot_apply(CUserCmd *cmd) {
1725 if (!hit) 2015 if (!hit)
1726 return false; 2016 return false;
1727 2017
1728 using Fn = int (*)(void *); 2018 int idx = entptr_lookup(hit);
1729 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); 2019 if (idx <= 0) {
1730 if (hi < 0 || hi > 4096) 2020 // Cache miss: ESP is off, or this entity appeared since the last scan.
1731 hi = 4096; 2021 using Fn = int (*)(void *);
1732 for (int idx = 1; idx <= hi; idx++) { 2022 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
1733 if (entitylist_get(idx) != hit) 2023 if (hi < 0 || hi > 4096)
1734 continue; 2024 hi = 4096;
1735 void *net = entity_networkable(idx); 2025 for (int i = 1; i <= hi; i++) {
1736 const char *cls = net ? entity_class_name(net, nullptr) : nullptr; 2026 if (entitylist_get(i) != hit)
1737 if (!cls || !is_npc_class(cls)) 2027 continue;
1738 return false; 2028 idx = i;
1739 uintptr_t e = (uintptr_t)hit; 2029 entptr_store(hit, i);
1740 int life = g_net.lifestate >= 0 2030 break;
1741 ? (int)*(unsigned char *)(e + g_net.lifestate) 2031 }
1742 : -1; 2032 if (idx <= 0)
1743 if (life != LIFE_ALIVE)
1744 return false;
1745 const char *model = entity_model_name(hit);
1746 bool neutral = false;
1747 if (npc_is_friendly(idx, model, local, neutral) || neutral)
1748 return false;
1749 if (!npc_can_be_damaged(idx, model))
1750 return false; 2033 return false;
2034 }
2035 void *net = entity_networkable(idx);
2036 if (entity_is_dormant(net))
2037 return false;
2038 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
2039 if (!cls || !is_npc_class(cls))
2040 return false;
2041 uintptr_t e = (uintptr_t)hit;
2042 int life = g_net.lifestate >= 0
2043 ? (int)*(unsigned char *)(e + g_net.lifestate)
2044 : -1;
2045 if (life != LIFE_ALIVE)
2046 return false;
2047 const char *model = entity_model_name(hit);
2048 bool neutral = false;
2049 if (npc_is_friendly(idx, model, local, neutral) || neutral)
2050 return false;
2051 if (!npc_can_be_damaged(idx, model))
2052 return false;
2053 if (fire_edge_ok(cmd))
1751 cmd->buttons |= IN_ATTACK; 2054 cmd->buttons |= IN_ATTACK;
1752 static int logs = 0; 2055 static int logs = 0;
1753 if (logs < 5) { 2056 if (logs < 5) {
1754 logs++; 2057 logs++;
1755 logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?"); 2058 logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?");
1756 }
1757 return true;
1758 } 2059 }
1759 return false; 2060 return true;
1760} 2061}
1761 2062
1762// Dynamic props are tinted through their client render colour. StudioRender 2063// Dynamic props are tinted through their client render colour. StudioRender
@@ -2075,13 +2376,8 @@ static void draw_transitions() {
2075 2376
2076void esp_draw() { 2377void esp_draw() {
2077 if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return; 2378 if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return;
2078 if (!g_scr_w) { 2379 if (!esp_update_screen())
2079 using Fn = void (*)(void *, int &, int &); 2380 return;
2080 vfunc<Fn>(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h);
2081 logd("ESP: screen %dx%d", g_scr_w, g_scr_h);
2082 if (!g_scr_w) return;
2083 }
2084 draw_fov_circle();
2085 esp_collect(engine_get_local_player()); 2381 esp_collect(engine_get_local_player());
2086 static double last_trans = 0.0; 2382 static double last_trans = 0.0;
2087 double trans_now = esp_now(); 2383 double trans_now = esp_now();
@@ -2805,7 +3101,7 @@ static void items_collect(int local_idx) {
2805 if (!ent) 3101 if (!ent)
2806 continue; 3102 continue;
2807 void *net = entity_networkable(idx); 3103 void *net = entity_networkable(idx);
2808 if (!net) 3104 if (!net || entity_is_dormant(net))
2809 continue; 3105 continue;
2810 const char *cls = entity_class_name(net, nullptr); 3106 const char *cls = entity_class_name(net, nullptr);
2811 const char *scls = nullptr; 3107 const char *scls = nullptr;
@@ -2922,12 +3218,8 @@ void esp_draw_items() {
2922 int local = engine_get_local_player(); 3218 int local = engine_get_local_player();
2923 if (local <= 0) 3219 if (local <= 0)
2924 return; 3220 return;
2925 if (!g_scr_w) { 3221 if (!esp_update_screen())
2926 using Fn = void (*)(void *, int &, int &); 3222 return;
2927 vfunc<Fn>(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h);
2928 if (!g_scr_w)
2929 return;
2930 }
2931 items_collect(local); 3223 items_collect(local);
2932 3224
2933 int vit_hp = -1, vit_max = -1, vit_armor = -1; 3225 int vit_hp = -1, vit_max = -1, vit_armor = -1;
@@ -2949,7 +3241,11 @@ void esp_draw_items() {
2949 } 3241 }
2950 3242
2951 for (int i = 0; i < g_nitems; i++) { 3243 for (int i = 0; i < g_nitems; i++) {
2952 const ItemTarget &t = g_item_targets[i]; 3244 ItemTarget &t = g_item_targets[i];
3245 // Follow the entity between the 10 Hz discovery scans.
3246 Vector live;
3247 if (entity_origin(entitylist_get(t.idx), live))
3248 t.origin = live;
2953 if (!item_kind_enabled(t.kind)) 3249 if (!item_kind_enabled(t.kind))
2954 continue; 3250 continue;
2955 // Dispensers (chargers, ammo crates, supply crates) stay useful after a 3251 // Dispensers (chargers, ammo crates, supply crates) stay useful after a
@@ -3058,17 +3354,23 @@ static const char *proj_name(int kind) {
3058// Fallback fuse clock when the server detonate time cannot be read. The stock 3354// Fallback fuse clock when the server detonate time cannot be read. The stock
3059// frag fuse is 3 s from the first sighting. 3355// frag fuse is 3 s from the first sighting.
3060static double proj_first_seen(int idx, double now) { 3356static double proj_first_seen(int idx, double now) {
3061 static int idxs[32]; 3357 static constexpr int N = 64;
3062 static double first[32]; 3358 static int idxs[N];
3063 static int n = 0; 3359 static double first[N];
3064 for (int i = 0; i < n; i++) 3360 static int next = 0;
3065 if (idxs[i] == idx) 3361 for (int i = 0; i < N; i++) {
3066 return first[i]; 3362 if (idxs[i] != idx)
3067 if (n < 32) { 3363 continue;
3068 idxs[n] = idx; 3364 // The slot is stale if this index was last seen long ago: a new grenade
3069 first[n] = now; 3365 // reusing the entity number must start its own fuse, not inherit an old
3070 n++; 3366 // one (which produced permanently negative fuses).
3071 } 3367 if (now - first[i] > 10.0)
3368 first[i] = now;
3369 return first[i];
3370 }
3371 idxs[next] = idx;
3372 first[next] = now;
3373 next = (next + 1) % N; // oldest entry is reused instead of dropped
3072 return now; 3374 return now;
3073} 3375}
3074 3376
@@ -3094,7 +3396,7 @@ static void projs_collect(int local_idx) {
3094 if (!ent) 3396 if (!ent)
3095 continue; 3397 continue;
3096 void *net = entity_networkable(idx); 3398 void *net = entity_networkable(idx);
3097 if (!net) 3399 if (!net || entity_is_dormant(net))
3098 continue; 3400 continue;
3099 const char *cls = entity_class_name(net, nullptr); 3401 const char *cls = entity_class_name(net, nullptr);
3100 const char *model = entity_model_name(ent); 3402 const char *model = entity_model_name(ent);
@@ -3171,6 +3473,8 @@ static void draw_grenade_trajectory(int local_idx) {
3171 Vector src{}, vel{}; 3473 Vector src{}, vel{};
3172 bool have = false; 3474 bool have = false;
3173 bool actual = false; 3475 bool actual = false;
3476 // Real blast radius of the grenade being previewed, not a hardcoded 300.
3477 float blast_radius = 300.0f;
3174 // Prefer the local player's thrown frag: the real position and velocity are 3478 // Prefer the local player's thrown frag: the real position and velocity are
3175 // networked, so the preview starts from what is actually flying instead of 3479 // networked, so the preview starts from what is actually flying instead of
3176 // the pre-throw estimate. The bounce contact points still depend on the 3480 // the pre-throw estimate. The bounce contact points still depend on the
@@ -3182,6 +3486,13 @@ static void draw_grenade_trajectory(int local_idx) {
3182 if (!gent) 3486 if (!gent)
3183 continue; 3487 continue;
3184 src = g_projs[i].origin; 3488 src = g_projs[i].origin;
3489 // Live position: the cached origin is up to 100 ms old, and the velocity
3490 // read below is live, so mixing them desynced the arc from the grenade.
3491 Vector live;
3492 if (entity_origin(gent, live))
3493 src = live;
3494 if (g_projs[i].radius > 1.0f)
3495 blast_radius = g_projs[i].radius;
3185 if (g_net.velocity >= 0) { 3496 if (g_net.velocity >= 0) {
3186 const float *pv = (const float *)((uintptr_t)gent + g_net.velocity); 3497 const float *pv = (const float *)((uintptr_t)gent + g_net.velocity);
3187 vel = Vector{pv[0], pv[1], pv[2]}; 3498 vel = Vector{pv[0], pv[1], pv[2]};
@@ -3277,9 +3588,11 @@ static void draw_grenade_trajectory(int local_idx) {
3277 surf_line(x, y - 9, x, y - 4); 3588 surf_line(x, y - 9, x, y - 4);
3278 surf_line(x, y + 4, x, y + 9); 3589 surf_line(x, y + 4, x, y + 9);
3279 } 3590 }
3280 if (landed) { 3591 // The landing ring belongs to the "Blast radius" toggle, and uses the
3592 // networked m_DmgRadius when available instead of a fixed 300.
3593 if (landed && g_proj_radius) {
3281 const int SEG = 36; 3594 const int SEG = 36;
3282 const float radius = 300.0f; // CBaseGrenade m_DmgRadius 3595 const float radius = blast_radius;
3283 surf_color(255, 120, 40, 120); 3596 surf_color(255, 120, 40, 120);
3284 for (int k = 0; k < SEG; k++) { 3597 for (int k = 0; k < SEG; k++) {
3285 float a0 = (float)k * 2.0f * PI_F / (float)SEG; 3598 float a0 = (float)k * 2.0f * PI_F / (float)SEG;
@@ -3298,16 +3611,17 @@ static void draw_grenade_trajectory(int local_idx) {
3298void esp_draw_projectiles() { 3611void esp_draw_projectiles() {
3299 if (!g_surface || !g_iface.entitylist || g_net.origin < 0) 3612 if (!g_surface || !g_iface.entitylist || g_net.origin < 0)
3300 return; 3613 return;
3301 if (!g_scr_w) { 3614 if (!esp_update_screen())
3302 using Fn = void (*)(void *, int &, int &); 3615 return;
3303 vfunc<Fn>(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h);
3304 if (!g_scr_w)
3305 return;
3306 }
3307 projs_collect(engine_get_local_player()); 3616 projs_collect(engine_get_local_player());
3308 draw_grenade_trajectory(engine_get_local_player()); 3617 draw_grenade_trajectory(engine_get_local_player());
3309 for (int i = 0; i < g_nprojs; i++) { 3618 for (int i = 0; i < g_nprojs; i++) {
3310 const ProjTarget &p = g_projs[i]; 3619 ProjTarget &p = g_projs[i];
3620 // Follow the entity between the 10 Hz discovery scans so the marker and
3621 // the blast-radius ring do not step in 100 ms increments.
3622 Vector live;
3623 if (entity_origin(entitylist_get(p.idx), live))
3624 p.origin = live;
3311 int r = 255, g = 170, b = 60; 3625 int r = 255, g = 170, b = 60;
3312 if (p.kind == PK_RPG) { 3626 if (p.kind == PK_RPG) {
3313 r = 255; g = 120; b = 40; 3627 r = 255; g = 120; b = 40;