diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-06 16:09:55 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-06 16:09:55 +0100 |
| commit | 33f5ed9ef58f26c6b3b52cf605dd4311d658a60d (patch) | |
| tree | a97254872e465c9107b7ef19695ab56bb076d7e2 /src/esp.cpp | |
| parent | Items: log the usable ammo table once (diff) | |
| download | hl2h-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.cpp | 582 |
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) | ||
| 11 | enum SurfSlot { | ||
| 12 | SURF_DrawSetColor = 10, SURF_FilledRect = 12, SURF_OutlinedRect = 14, | ||
| 13 | SURF_DrawLine = 15, SURF_GetScreenSize = 38, | ||
| 14 | }; | ||
| 15 | |||
| 16 | enum { LIFE_ALIVE = 0 }; | 10 | enum { LIFE_ALIVE = 0 }; |
| 17 | 11 | ||
| 18 | struct EspTarget { | 12 | struct EspTarget { |
| @@ -40,6 +34,36 @@ static int g_ntargets = 0; | |||
| 40 | static int g_scr_w = 0, g_scr_h = 0; | 34 | static int g_scr_w = 0, g_scr_h = 0; |
| 41 | static bool g_esp_logged = false; | 35 | static 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. | ||
| 40 | static bool g_srv_checked = false; | ||
| 41 | static 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. | ||
| 46 | static constexpr int MODEL_CACHE = 16384; | ||
| 47 | static const char *g_model_name_cache[MODEL_CACHE]; | ||
| 48 | static 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. | ||
| 53 | static constexpr int ENTPTR_CACHE = 4096; | ||
| 54 | static void *g_entptr_key[ENTPTR_CACHE]; | ||
| 55 | static int g_entptr_idx[ENTPTR_CACHE]; | ||
| 56 | |||
| 57 | void 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. |
| 44 | static constexpr int SPEED_SAMPLES = 100; | 68 | static constexpr int SPEED_SAMPLES = 100; |
| 45 | static float g_speed_samples[SPEED_SAMPLES] = {}; | 69 | static 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. | ||
| 82 | static 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. |
| 57 | static bool project_manual(const Vector &w, float &sx, float &sy) { | 96 | 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) { | |||
| 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. | ||
| 193 | static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled | ||
| 194 | |||
| 195 | static 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 | |||
| 223 | static 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. | ||
| 237 | static 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 | |||
| 244 | static 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. |
| 142 | static const char *model_name_from_index(int modelindex) { | 254 | static 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 | ||
| 152 | static const char *entity_model_name(void *ent) { | 279 | static 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. | ||
| 288 | static 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) { | |||
| 192 | static bool npc_is_friendly(int idx, const char *model, int local_idx, | 332 | static bool npc_is_friendly(int idx, const char *model, int local_idx, |
| 193 | bool &neutral); | 333 | bool &neutral); |
| 194 | int esp_npc_relation(int idx) { | 334 | int 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 | ||
| 211 | static bool model_is_friendly(const char *m) { | 366 | static 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 | ||
| 240 | static bool npc_is_friendly(int idx, const char *model, int local_idx, | 395 | static 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. | ||
| 463 | static 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 | ||
| 306 | enum NpcDamageClass { | 483 | enum 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. |
| 319 | static int npc_damage_class(int idx, const char *model) { | 496 | static 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. | ||
| 512 | static 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 | |||
| 333 | static bool active_weapon_contains(void *local_ent, const char *needle); | 528 | static 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 | ||
| 578 | static void surf_rect(int filled, int x0, int y0, int x1, int y1) { | 777 | static 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 | } |
| 582 | static void surf_color(int r, int g, int b, int a) { | 783 | static 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 | } |
| 586 | static void surf_line(int x0, int y0, int x1, int y1) { | 786 | static 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 | ||
| 602 | static void draw_fov_circle() { | 801 | void 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. | ||
| 1304 | static 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. | ||
| 1321 | static 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 | |||
| 1332 | static 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 | |||
| 1061 | bool aimbot_apply(CUserCmd *cmd) { | 1345 | bool 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 | ||
| 2076 | void esp_draw() { | 2377 | void 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. |
| 3060 | static double proj_first_seen(int idx, double now) { | 3356 | static 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) { | |||
| 3298 | void esp_draw_projectiles() { | 3611 | void 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; |
