From 33f5ed9ef58f26c6b3b52cf605dd4311d658a60d Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 6 Nov 2026 16:09:55 +0100 Subject: 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 --- src/esp.cpp | 582 ++++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 448 insertions(+), 134 deletions(-) (limited to 'src/esp.cpp') 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 @@ #include #include -// ISurface slots (binary = SDK header + 4, disassembly-confirmed) -enum SurfSlot { - SURF_DrawSetColor = 10, SURF_FilledRect = 12, SURF_OutlinedRect = 14, - SURF_DrawLine = 15, SURF_GetScreenSize = 38, -}; - enum { LIFE_ALIVE = 0 }; struct EspTarget { @@ -40,6 +34,36 @@ static int g_ntargets = 0; static int g_scr_w = 0, g_scr_h = 0; static bool g_esp_logged = false; +// Listen-server sanity latch. Only ever set once per map (esp_reset_session), +// and only after both model indices are valid: latching on the transient +// cli_model == 0 at map load disabled server health for the whole process. +static bool g_srv_checked = false; +static bool g_srv_trusted = false; + +// Per-map caches. The ESP resolves a model name for every entity on every scan +// and the chams hook asks for a relation on every model draw; both are virtual +// call chains into the engine plus server datamap reads, so memoise them. +static constexpr int MODEL_CACHE = 16384; +static const char *g_model_name_cache[MODEL_CACHE]; +static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail + +// Direct-mapped entity pointer -> index map, filled by esp_collect. The +// triggerbot traces a ray and gets a pointer back, and used to find its index +// by walking up to 4096 entity-list slots on every fire tick. +static constexpr int ENTPTR_CACHE = 4096; +static void *g_entptr_key[ENTPTR_CACHE]; +static int g_entptr_idx[ENTPTR_CACHE]; + +void esp_reset_session() { + g_srv_checked = false; + g_srv_trusted = false; + g_esp_logged = false; + memset(g_model_name_cache, 0, sizeof(g_model_name_cache)); + memset(g_model_state_cache, 0, sizeof(g_model_state_cache)); + memset(g_entptr_key, 0, sizeof(g_entptr_key)); + memset(g_entptr_idx, 0, sizeof(g_entptr_idx)); +} + // Speed graph: 100 samples at 0.1 s = the last 10 seconds. static constexpr int SPEED_SAMPLES = 100; static float g_speed_samples[SPEED_SAMPLES] = {}; @@ -52,6 +76,21 @@ static double esp_now() { return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; } +// Refresh the cached screen size every frame. It used to be read once, so a +// resolution or window change left every projection, screen-edge clamp and +// centering calculation stale for the rest of the session. +static bool esp_update_screen() { + if (!g_surface) + return false; + int w = 0, h = 0; + overlay_screen_size(w, h); + if (w > 0) + g_scr_w = w; + if (h > 0) + g_scr_h = h; + return g_scr_w > 0 && g_scr_h > 0; +} + // Local perspective projection used when the engine's debug-overlay // projection refuses a point that is actually in front of the camera. static 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) { return false; float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; + float roll = g_cam_view.z * PI_F / 180.0f; float sp = sinf(pitch), cp = cosf(pitch); float syy = sinf(yaw), cy = cosf(yaw); + float sr = sinf(roll), cr = cosf(roll); float fwd[3] = {cp * cy, cp * syy, -sp}; - float right[3] = {syy, -cy, 0}; - float up[3] = {sp * cy, sp * syy, cp}; + // Source's AngleVectors with roll; the old right/up vectors assumed roll 0, + // so view punch made the fallback projection rotate against the world. + float right[3] = {-sr * sp * cy + cr * syy, -sr * sp * syy - cr * cy, + -sr * cp}; + float up[3] = {cr * sp * cy + sr * syy, cr * sp * syy - sr * cy, cr * cp}; float dx = w.x - g_cam_eye.x, dy = w.y - g_cam_eye.y, dz = w.z - g_cam_eye.z; float z = dx * fwd[0] + dy * fwd[1] + dz * fwd[2]; if (z < 1.0f) @@ -71,7 +115,10 @@ static bool project_manual(const Vector &w, float &sx, float &sy) { float x = dx * right[0] + dy * right[1] + dz * right[2]; float y = dx * up[0] + dy * up[1] + dz * up[2]; float fov = g_fov_camera > 10 ? (float)g_fov_camera : 75.0f; - float tanfov = tanf(fov * 0.5f * PI_F / 180.0f); + // Source defines the FOV for a 4:3 reference and widens it on widescreen; + // using a raw tan(fov/2) made the fallback ~33% too narrow at 16:9. + float aspect = (float)g_scr_w / (float)g_scr_h; + float tanfov = tanf(fov * 0.5f * PI_F / 180.0f) * (aspect / (4.0f / 3.0f)); sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov); sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov); 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) { return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; } +// IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant +// 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK, +// so the slot is validated once against the local player, which can never be +// dormant: a wrong slot disables the check instead of misreporting entities. +// Dormant entities keep their last networked origin, so drawing or aiming at +// them produces ghosts at stale positions. +static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled + +static void dormant_selftest() { + // Leave the state untested (0) until a local player exists: the first call + // can come from the main menu, and latching "disabled" there would turn the + // check off for the whole session. + if (!g_iface.entitylist) + return; + int local = engine_get_local_player(); + if (local <= 0) + return; + void *net = entity_networkable(local); + if (!net) + return; + void **vt = *(void ***)net; + if (!vt || !mod_client().contains((uintptr_t)vt[8])) { + g_dormant_state = -1; + logf("dormant check disabled: slot 8 outside client module"); + return; + } + using DormantFn = unsigned char (*)(void *); + if (vfunc(net, 8)(net) != 0) { + g_dormant_state = -1; + logf("dormant check disabled: local player reported dormant"); + return; + } + g_dormant_state = 1; + logd("dormant check enabled"); +} + +static bool entity_is_dormant(void *net) { + if (!net) + return false; + if (g_dormant_state == 0) + dormant_selftest(); + if (g_dormant_state != 1) + return false; + using DormantFn = unsigned char (*)(void *); + return vfunc(net, 8)(net) != 0; +} + +// Entity pointer -> index, filled by the entity scans. The triggerbot gets a +// pointer back from the trace and needs the index; the alternative is walking +// up to 4096 entity-list slots on every fire tick. +static int entptr_lookup(void *ent) { + if (!ent) + return -1; + unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); + return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1; +} + +static void entptr_store(void *ent, int idx) { + if (!ent || idx <= 0) + return; + unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); + g_entptr_key[h] = ent; + g_entptr_idx[h] = idx; +} + // m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. // Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. static const char *model_name_from_index(int modelindex) { if (!g_iface.modelinfo || modelindex <= 0) return nullptr; + if (modelindex < MODEL_CACHE) { + if (g_model_state_cache[modelindex] == 1) + return g_model_name_cache[modelindex]; + if (g_model_state_cache[modelindex] == 2) + return nullptr; + } const void *model = vfunc(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); - if (!model) + if (!model) { + if (modelindex < MODEL_CACHE) + g_model_state_cache[modelindex] = 2; return nullptr; - return vfunc(g_iface.modelinfo, 4)(g_iface.modelinfo, model); + } + const char *name = + vfunc(g_iface.modelinfo, 4)(g_iface.modelinfo, model); + if (modelindex < MODEL_CACHE) { + g_model_name_cache[modelindex] = name; + g_model_state_cache[modelindex] = name ? 1 : 2; + } + return name; } static const char *entity_model_name(void *ent) { @@ -155,6 +282,19 @@ static const char *entity_model_name(void *ent) { return model_name_from_index(modelindex); } +// Live origin for a cached entity index. Item/projectile discovery is throttled +// to 10 Hz (it walks the whole entity list), but the markers must follow the +// world at render rate; refreshing one netvar read per cached target is cheap. +static bool entity_origin(void *ent, Vector &out) { + if (!ent || g_net.origin < 0) + return false; + uintptr_t e = (uintptr_t)ent; + out.x = *(float *)(e + g_net.origin); + out.y = *(float *)(e + g_net.origin + 4); + out.z = *(float *)(e + g_net.origin + 8); + return true; +} + // HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all // use the single client class "CAI_BaseNPC" — only special NPCs register their // own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). @@ -192,20 +332,35 @@ bool esp_is_viewmodel_index(int idx) { static bool npc_is_friendly(int idx, const char *model, int local_idx, bool &neutral); int esp_npc_relation(int idx) { - if (!esp_is_npc_index(idx)) - return -1; - int local = engine_get_local_player(); - if (local <= 0) - return 0; - void *ent = entitylist_get(idx); - const char *model = ent ? entity_model_name(ent) : nullptr; - bool neutral = false; - bool friendly = npc_is_friendly(idx, model, local, neutral); - if (neutral) - return 1; - if (friendly) - return 2; - return 0; + // Called from the chams hook up to three times per entity per frame (visible, + // through-wall, wireframe pass) and the work behind it is a chain of server + // edict/datamap reads. A short TTL cache keeps that out of the render path. + struct RelCache { + int idx; + double at; + int val; + }; + static RelCache cache[256]; + double now = esp_now(); + RelCache &c = cache[(unsigned)idx & 255]; + if (c.idx == idx && now - c.at < 0.05) + return c.val; + + int val = -1; + if (esp_is_npc_index(idx)) { + int local = engine_get_local_player(); + if (local <= 0) { + val = 0; + } else { + void *ent = entitylist_get(idx); + const char *model = ent ? entity_model_name(ent) : nullptr; + bool neutral = false; + bool friendly = npc_is_friendly(idx, model, local, neutral); + val = neutral ? 1 : friendly ? 2 : 0; + } + } + c = {idx, now, val}; + return val; } static bool model_is_friendly(const char *m) { @@ -237,8 +392,8 @@ static bool model_is_floor_turret(const char *m) { return m && strstr(m, "floor_turret"); } -static bool npc_is_friendly(int idx, const char *model, int local_idx, - bool &neutral) { +static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx, + bool &neutral) { int rel = server_npc_relation(idx, local_idx); int state = server_npc_state(idx); int turret = -1; @@ -302,6 +457,28 @@ static bool npc_is_friendly(int idx, const char *model, int local_idx, return friendly; } +// Friendly/neutral state changes slowly (hacked turrets, bugbait), while the +// entity scan and the chams hook ask for it constantly. Cache it briefly so the +// server lookups happen once per NPC per window instead of per call. +static bool npc_is_friendly(int idx, const char *model, int local_idx, + bool &neutral) { + struct Cache { + int idx; + double at; + bool friendly; + bool neutral; + }; + static Cache cache[256]; + double now = esp_now(); + Cache &c = cache[(unsigned)idx & 255]; + if (c.idx == idx && now - c.at < 0.10) { + neutral = c.neutral; + return c.friendly; + } + bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral); + c = {idx, now, f, neutral}; + return f; +} enum NpcDamageClass { NPC_DMG_NORMAL = 0, @@ -316,7 +493,7 @@ static bool model_has(const char *model, const char *needle) { // Server m_takedamage first (authoritative immunity), then the HL2 damage // rules for units whose OnTakeDamage filters specific damage types. -static int npc_damage_class(int idx, const char *model) { +static int npc_damage_class_uncached(int idx, const char *model) { int td = server_npc_takedamage(idx); if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY return NPC_DMG_IMMUNE; @@ -330,6 +507,24 @@ static int npc_damage_class(int idx, const char *model) { return NPC_DMG_NORMAL; } +// Damage class depends on m_takedamage (a server read) plus model string +// matching. It changes even more rarely than the relation, so cache it. +static int npc_damage_class(int idx, const char *model) { + struct Cache { + int idx; + double at; + int val; + }; + static Cache cache[256]; + double now = esp_now(); + Cache &c = cache[(unsigned)idx & 255]; + if (c.idx == idx && now - c.at < 0.25) + return c.val; + int v = npc_damage_class_uncached(idx, model); + c = {idx, now, v}; + return v; +} + static bool active_weapon_contains(void *local_ent, const char *needle); // Armoured units need blast damage; the RPG is the only stock weapon that @@ -430,15 +625,14 @@ static void esp_collect(int local_idx) { if (hi < 0 || hi > 4096) hi = 4096; int npc_alive = 0, dbg_lines = 0; bool srv = resolve_server_netvars(); - static bool srv_checked = false; - static bool srv_trusted = false; for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) { if (idx == local_idx) continue; void *ent = entitylist_get(idx); if (!ent) continue; + entptr_store(ent, idx); void *net = entity_networkable(idx); - if (!net) continue; + if (!net || entity_is_dormant(net)) continue; const char *cls = entity_class_name(net, nullptr); if (!cls) continue; @@ -456,19 +650,24 @@ static void esp_collect(int local_idx) { int slife = -1, smax = -1; void *sbase = nullptr; int shp = server_entity_health(idx, &smax, &slife, &sbase); - if (!srv_checked && sbase) { - srv_checked = true; + if (!g_srv_checked && sbase) { int cli_mi = g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1; int srv_mi = server_model_index(sbase); - srv_trusted = (cli_mi == srv_mi); - logd("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d " - "hp=%d maxhp=%d life=%d trusted=%d", - idx, sbase, cli_mi, srv_mi, shp, smax, slife, (int)srv_trusted); - if (!srv_trusted) - logf("ESP: server entity mismatch, ignoring server values"); + // Wait for both indices: the client index is often 0 for a tick right + // after a map load, which used to latch "untrusted" permanently. + if (cli_mi > 0 && srv_mi > 0) { + g_srv_checked = true; + g_srv_trusted = (cli_mi == srv_mi); + logd("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d " + "hp=%d maxhp=%d life=%d trusted=%d", + idx, sbase, cli_mi, srv_mi, shp, smax, slife, + (int)g_srv_trusted); + if (!g_srv_trusted) + logf("ESP: server entity mismatch, ignoring server values"); + } } - if (srv_trusted) { + if (g_srv_trusted) { if (shp >= 0) hp = shp; if (smax > 0) maxhp = smax; if (slife >= 0) life = slife; @@ -576,16 +775,16 @@ static void esp_collect(int local_idx) { } static void surf_rect(int filled, int x0, int y0, int x1, int y1) { - using Fn = void (*)(void *, int, int, int, int); - vfunc(g_surface, filled ? SURF_FilledRect : SURF_OutlinedRect)(g_surface, x0, y0, x1, y1); + if (filled) + overlay_fill(x0, y0, x1, y1); + else + overlay_outline_rect(x0, y0, x1, y1); } static void surf_color(int r, int g, int b, int a) { - using Fn = void (*)(void *, int, int, int, int); - vfunc(g_surface, SURF_DrawSetColor)(g_surface, r, g, b, a); + overlay_color(r, g, b, a); } static void surf_line(int x0, int y0, int x1, int y1) { - using Fn = void (*)(void *, int, int, int, int); - vfunc(g_surface, SURF_DrawLine)(g_surface, x0, y0, x1, y1); + overlay_line(x0, y0, x1, y1); } // Screen projection of the aimbot FOV cone boundary, using the engine's own @@ -599,8 +798,14 @@ static QAngle engine_view_angles() { return v; } -static void draw_fov_circle() { - if (!aimbot_fov_circle()) +void esp_draw_fov_circle() { + if (!g_surface || !aimbot_fov_circle()) + return; + // No local player means no game world (main menu); the cached camera is stale + // and the projected circle would land at a meaningless spot. + if (engine_get_local_player() <= 0) + return; + if (!esp_update_screen()) return; float fov = (float)aimbot_fov(); 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, out_idx = -1; out_point = {}; out_fov = 0.0f; - if (!g_iface.entitylist || g_net.origin < 0 || g_net.maxs < 0) + // maxs is optional: the per-target code falls back to 72 units when the + // netvar is missing, so it must not disable the whole aimbot here. + if (!g_iface.entitylist || g_net.origin < 0) return false; int local = engine_get_local_player(); if (local <= 0) @@ -830,7 +1037,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, bool rpg = active_weapon_contains(local_ent, "rocket_launcher") || active_weapon_contains(local_ent, "rpg"); - auto try_target = [&](int idx, bool enforce_fov, Vector &pt, + // fov_limit <= 0 means "no angular limit" (kept for unlimited acquisition). + auto try_target = [&](int idx, float fov_limit, Vector &pt, float &fov) -> bool { if (idx <= 0 || idx == local) return false; @@ -838,7 +1046,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, if (!ent) return false; void *net = entity_networkable(idx); - if (!net) + if (!net || entity_is_dormant(net)) return false; const char *cls = entity_class_name(net, nullptr); if (!cls || !is_npc_class(cls)) @@ -863,7 +1071,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, Vector best_pt{}; for (int i = 0; i < npoints; i++) { float f = getfov(view, angle_to(g_cam_eye, pts[i])); - if (enforce_fov && f > max_fov) + if (fov_limit > 0.0f && f > fov_limit) continue; if (f >= best) continue; @@ -880,11 +1088,13 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, }; // Keep the locked target while it is alive and has a visible selected point. - // No FOV limit on a locked target; only acquisition is FOV-limited. + // The lock uses a wider cone than acquisition (hysteresis), so a target that + // drifts slightly off the crosshair is not dropped, but one that swings + // behind the player is: an unbounded lock let silent aim fire backwards. if (lock_idx && *lock_idx >= 0) { Vector pt; float f; - if (try_target(*lock_idx, false, pt, f)) { + if (try_target(*lock_idx, max_fov * 1.5f, pt, f)) { out_idx = *lock_idx; out_point = pt; out_fov = f; @@ -920,7 +1130,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, if (!ent) continue; void *net = entity_networkable(idx); - if (!net) + if (!net || entity_is_dormant(net)) continue; const char *cls = entity_class_name(net, nullptr); if (!cls || !is_npc_class(cls)) @@ -949,7 +1159,17 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, Vector approx{origin.x, origin.y, origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)}; float fov = getfov(view, angle_to(g_cam_eye, approx)); - if (fov > max_fov) + // The prefilter samples a single point, but the selected hitbox can be a + // whole body-width away at close range. Reject only when the target's + // angular radius cannot reach the cone; try_target still enforces the + // exact per-hitbox FOV. + float ddx = approx.x - g_cam_eye.x; + float ddy = approx.y - g_cam_eye.y; + float ddz = approx.z - g_cam_eye.z; + float dist = sqrtf(ddx * ddx + ddy * ddy + ddz * ddz); + float margin = + dist > 1.0f ? atan2f(maxz, dist) * 180.0f / PI_F : 90.0f; + if (fov - margin > max_fov) continue; float key = (rpg && armored) ? fov - 1000.0f : fov; @@ -977,7 +1197,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, for (int ci = 0; ci < ncands; ci++) { Vector pt; float f; - if (!try_target(cands[ci].idx, true, pt, f)) + if (!try_target(cands[ci].idx, max_fov, pt, f)) continue; out_idx = cands[ci].idx; out_point = pt; @@ -1043,9 +1263,28 @@ static bool projectile_lead_point(int target_idx, const Vector &point, return false; } void *target = entitylist_get(target_idx); - if (!target || g_net.velocity < 0) + if (!target) + return false; + // NPCs do not network C_BaseEntity::m_vecVelocity (only the player does), so + // the client field is stale for exactly the targets the aimbot uses. Read + // the listen-server entity when available and fall back to the client value. + float v[3] = {0.0f, 0.0f, 0.0f}; + Vector srv_vel; + bool have_vel = false; + if (server_entity_velocity(target_idx, &srv_vel)) { + v[0] = srv_vel.x; + v[1] = srv_vel.y; + v[2] = srv_vel.z; + have_vel = true; + } else if (g_net.velocity >= 0) { + const float *cv = (const float *)((uintptr_t)target + g_net.velocity); + v[0] = cv[0]; + v[1] = cv[1]; + v[2] = cv[2]; + have_vel = true; + } + if (!have_vel) return false; - const float *v = (const float *)((uintptr_t)target + g_net.velocity); float dx = point.x - g_cam_eye.x; float dy = point.y - g_cam_eye.y; float dz = point.z - g_cam_eye.z; @@ -1058,6 +1297,51 @@ static bool projectile_lead_point(int target_idx, const Vector &point, return true; } +// A led point is a prediction, not a verified aim point: acquisition checked +// the un-led position, so the shift has to pass the same FOV and visibility +// tests. On failure the caller keeps the original point instead of firing into +// geometry or outside the configured cone. +static bool lead_point_ok(int target_idx, const Vector &lead, + const QAngle &view, float max_fov) { + if (getfov(view, angle_to(g_cam_eye, lead)) > max_fov) + return false; + if (!aimbot_vischeck_enabled()) + return true; + void *local_ent = entitylist_get(engine_get_local_player()); + void *target_ent = entitylist_get(target_idx); + if (!local_ent || !target_ent) + return false; + return is_visible(g_cam_eye, lead, local_ent, target_ent); +} + +// Semi-auto weapons (pistol, .357, crossbow, shotgun, RPG, gravity gun) only +// fire on the IN_ATTACK press edge, so a synthetic button held every tick fires +// exactly once per buy/equip. Alternate press/release for those; automatic +// weapons keep the button held. +static bool weapon_is_semi_auto(void *local_ent) { + static const char *names[] = {"pistol", "357", + "crossbow", "shotgun", + "rocket_launcher", "physcannon", + "crowbar", nullptr}; + for (int i = 0; names[i]; i++) + if (active_weapon_contains(local_ent, names[i])) + return true; + return false; +} + +static bool fire_edge_ok(CUserCmd *cmd) { + void *local_ent = entitylist_get(engine_get_local_player()); + if (!local_ent || !weapon_is_semi_auto(local_ent)) + return true; + static int last_tick = -1; + static bool phase = false; + if (cmd->tick_count != last_tick) { + last_tick = cmd->tick_count; + phase = !phase; + } + return phase; +} + bool aimbot_apply(CUserCmd *cmd) { unsigned bone_mask = aimbot_bone_mask(); if (!bone_mask) @@ -1080,7 +1364,8 @@ bool aimbot_apply(CUserCmd *cmd) { "aimbot", lock, target, point, fov)) return false; Vector lead; - if (projectile_lead_point(target, point, lead)) + if (projectile_lead_point(target, point, lead) && + lead_point_ok(target, lead, view, max_fov)) point = lead; QAngle desired = angle_to(g_cam_eye, point); if (punch) { @@ -1168,7 +1453,8 @@ bool silent_aim_apply(CUserCmd *cmd) { "silentaim", lock, target, point, fov)) return false; Vector lead; - if (projectile_lead_point(target, point, lead)) + if (projectile_lead_point(target, point, lead) && + lead_point_ok(target, lead, view, max_fov)) point = lead; QAngle desired = angle_to(g_cam_eye, point); if (punch) { @@ -1196,7 +1482,8 @@ bool silent_aim_apply(CUserCmd *cmd) { using SetViewFn = void (*)(void *, QAngle *); vfunc(g_iface.engine, 20)(g_iface.engine, &restore); } - cmd->buttons |= IN_ATTACK; + if (fire_edge_ok(cmd)) + cmd->buttons |= IN_ATTACK; static int logs = 0; if (logs < 5) { logs++; @@ -1379,7 +1666,7 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { if (!ent) continue; void *net = entity_networkable(idx); - if (!net) + if (!net || entity_is_dormant(net)) continue; const char *cls = entity_class_name(net, nullptr); if (!cls || !is_npc_class(cls)) @@ -1417,15 +1704,17 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { maxy = *(float *)(e + g_net.maxs + 4); maxz = *(float *)(e + g_net.maxs + 8); } - if (!(maxz > minz + 4.0f && maxz < 512.0f)) { + // Reject absurd boxes in both directions; a garbage mins value that is + // hugely negative would otherwise produce an AABB spanning the map. + if (!(minz > -512.0f && maxz > minz + 4.0f && maxz < 512.0f)) { minz = 0.0f; maxz = 72.0f; } - if (!(maxx > minx + 1.0f && maxx < 256.0f)) { + if (!(minx > -512.0f && maxx > minx + 1.0f && maxx < 256.0f)) { minx = -16.0f; maxx = 16.0f; } - if (!(maxy > miny + 1.0f && maxy < 256.0f)) { + if (!(miny > -512.0f && maxy > miny + 1.0f && maxy < 256.0f)) { miny = -16.0f; maxy = 16.0f; } @@ -1529,7 +1818,8 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { } // Gravity gun primary attack launches the held object (EyeVectors); // secondary would drop it instead. - cmd->buttons |= IN_ATTACK; + if (fire_edge_ok(cmd)) + cmd->buttons |= IN_ATTACK; static int logs = 0; if (logs < 10) { @@ -1572,9 +1862,9 @@ static void spread_compute() { SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed"); FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat"); if (!seed || !rf) { - size_t slash = g_engine.path.find_last_of('/'); + size_t slash = mod_engine().path.find_last_of('/'); if (slash != std::string::npos) { - std::string path = g_engine.path.substr(0, slash + 1) + "libvstdlib.so"; + std::string path = mod_engine().path.substr(0, slash + 1) + "libvstdlib.so"; void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); if (!h) h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); @@ -1725,38 +2015,49 @@ bool triggerbot_apply(CUserCmd *cmd) { if (!hit) return false; - using Fn = int (*)(void *); - int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); - if (hi < 0 || hi > 4096) - hi = 4096; - for (int idx = 1; idx <= hi; idx++) { - if (entitylist_get(idx) != hit) - continue; - void *net = entity_networkable(idx); - const char *cls = net ? entity_class_name(net, nullptr) : nullptr; - if (!cls || !is_npc_class(cls)) - return false; - uintptr_t e = (uintptr_t)hit; - int life = g_net.lifestate >= 0 - ? (int)*(unsigned char *)(e + g_net.lifestate) - : -1; - if (life != LIFE_ALIVE) - return false; - const char *model = entity_model_name(hit); - bool neutral = false; - if (npc_is_friendly(idx, model, local, neutral) || neutral) - return false; - if (!npc_can_be_damaged(idx, model)) + int idx = entptr_lookup(hit); + if (idx <= 0) { + // Cache miss: ESP is off, or this entity appeared since the last scan. + using Fn = int (*)(void *); + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); + if (hi < 0 || hi > 4096) + hi = 4096; + for (int i = 1; i <= hi; i++) { + if (entitylist_get(i) != hit) + continue; + idx = i; + entptr_store(hit, i); + break; + } + if (idx <= 0) return false; + } + void *net = entity_networkable(idx); + if (entity_is_dormant(net)) + return false; + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + if (!cls || !is_npc_class(cls)) + return false; + uintptr_t e = (uintptr_t)hit; + int life = g_net.lifestate >= 0 + ? (int)*(unsigned char *)(e + g_net.lifestate) + : -1; + if (life != LIFE_ALIVE) + return false; + const char *model = entity_model_name(hit); + bool neutral = false; + if (npc_is_friendly(idx, model, local, neutral) || neutral) + return false; + if (!npc_can_be_damaged(idx, model)) + return false; + if (fire_edge_ok(cmd)) cmd->buttons |= IN_ATTACK; - static int logs = 0; - if (logs < 5) { - logs++; - logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?"); - } - return true; + static int logs = 0; + if (logs < 5) { + logs++; + logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?"); } - return false; + return true; } // Dynamic props are tinted through their client render colour. StudioRender @@ -2075,13 +2376,8 @@ static void draw_transitions() { void esp_draw() { if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return; - if (!g_scr_w) { - using Fn = void (*)(void *, int &, int &); - vfunc(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h); - logd("ESP: screen %dx%d", g_scr_w, g_scr_h); - if (!g_scr_w) return; - } - draw_fov_circle(); + if (!esp_update_screen()) + return; esp_collect(engine_get_local_player()); static double last_trans = 0.0; double trans_now = esp_now(); @@ -2805,7 +3101,7 @@ static void items_collect(int local_idx) { if (!ent) continue; void *net = entity_networkable(idx); - if (!net) + if (!net || entity_is_dormant(net)) continue; const char *cls = entity_class_name(net, nullptr); const char *scls = nullptr; @@ -2922,12 +3218,8 @@ void esp_draw_items() { int local = engine_get_local_player(); if (local <= 0) return; - if (!g_scr_w) { - using Fn = void (*)(void *, int &, int &); - vfunc(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h); - if (!g_scr_w) - return; - } + if (!esp_update_screen()) + return; items_collect(local); int vit_hp = -1, vit_max = -1, vit_armor = -1; @@ -2949,7 +3241,11 @@ void esp_draw_items() { } for (int i = 0; i < g_nitems; i++) { - const ItemTarget &t = g_item_targets[i]; + ItemTarget &t = g_item_targets[i]; + // Follow the entity between the 10 Hz discovery scans. + Vector live; + if (entity_origin(entitylist_get(t.idx), live)) + t.origin = live; if (!item_kind_enabled(t.kind)) continue; // Dispensers (chargers, ammo crates, supply crates) stay useful after a @@ -3058,17 +3354,23 @@ static const char *proj_name(int kind) { // Fallback fuse clock when the server detonate time cannot be read. The stock // frag fuse is 3 s from the first sighting. static double proj_first_seen(int idx, double now) { - static int idxs[32]; - static double first[32]; - static int n = 0; - for (int i = 0; i < n; i++) - if (idxs[i] == idx) - return first[i]; - if (n < 32) { - idxs[n] = idx; - first[n] = now; - n++; - } + static constexpr int N = 64; + static int idxs[N]; + static double first[N]; + static int next = 0; + for (int i = 0; i < N; i++) { + if (idxs[i] != idx) + continue; + // The slot is stale if this index was last seen long ago: a new grenade + // reusing the entity number must start its own fuse, not inherit an old + // one (which produced permanently negative fuses). + if (now - first[i] > 10.0) + first[i] = now; + return first[i]; + } + idxs[next] = idx; + first[next] = now; + next = (next + 1) % N; // oldest entry is reused instead of dropped return now; } @@ -3094,7 +3396,7 @@ static void projs_collect(int local_idx) { if (!ent) continue; void *net = entity_networkable(idx); - if (!net) + if (!net || entity_is_dormant(net)) continue; const char *cls = entity_class_name(net, nullptr); const char *model = entity_model_name(ent); @@ -3171,6 +3473,8 @@ static void draw_grenade_trajectory(int local_idx) { Vector src{}, vel{}; bool have = false; bool actual = false; + // Real blast radius of the grenade being previewed, not a hardcoded 300. + float blast_radius = 300.0f; // Prefer the local player's thrown frag: the real position and velocity are // networked, so the preview starts from what is actually flying instead of // the pre-throw estimate. The bounce contact points still depend on the @@ -3182,6 +3486,13 @@ static void draw_grenade_trajectory(int local_idx) { if (!gent) continue; src = g_projs[i].origin; + // Live position: the cached origin is up to 100 ms old, and the velocity + // read below is live, so mixing them desynced the arc from the grenade. + Vector live; + if (entity_origin(gent, live)) + src = live; + if (g_projs[i].radius > 1.0f) + blast_radius = g_projs[i].radius; if (g_net.velocity >= 0) { const float *pv = (const float *)((uintptr_t)gent + g_net.velocity); vel = Vector{pv[0], pv[1], pv[2]}; @@ -3277,9 +3588,11 @@ static void draw_grenade_trajectory(int local_idx) { surf_line(x, y - 9, x, y - 4); surf_line(x, y + 4, x, y + 9); } - if (landed) { + // The landing ring belongs to the "Blast radius" toggle, and uses the + // networked m_DmgRadius when available instead of a fixed 300. + if (landed && g_proj_radius) { const int SEG = 36; - const float radius = 300.0f; // CBaseGrenade m_DmgRadius + const float radius = blast_radius; surf_color(255, 120, 40, 120); for (int k = 0; k < SEG; k++) { float a0 = (float)k * 2.0f * PI_F / (float)SEG; @@ -3298,16 +3611,17 @@ static void draw_grenade_trajectory(int local_idx) { void esp_draw_projectiles() { if (!g_surface || !g_iface.entitylist || g_net.origin < 0) return; - if (!g_scr_w) { - using Fn = void (*)(void *, int &, int &); - vfunc(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h); - if (!g_scr_w) - return; - } + if (!esp_update_screen()) + return; projs_collect(engine_get_local_player()); draw_grenade_trajectory(engine_get_local_player()); for (int i = 0; i < g_nprojs; i++) { - const ProjTarget &p = g_projs[i]; + ProjTarget &p = g_projs[i]; + // Follow the entity between the 10 Hz discovery scans so the marker and + // the blast-radius ring do not step in 100 ms increments. + Vector live; + if (entity_origin(entitylist_get(p.idx), live)) + p.origin = live; int r = 255, g = 170, b = 60; if (p.kind == PK_RPG) { r = 255; g = 120; b = 40; -- cgit v1.2.3