diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-06 16:25:48 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-06 16:25:48 +0100 |
| commit | c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1 (patch) | |
| tree | 2da18f9d243d898fb218f36a4508e0d309782d5e | |
| parent | ESP: extract item ESP into its own translation unit (diff) | |
| download | hl2h-c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1.tar.gz hl2h-c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1.zip | |
ESP: finish the split into entity, combat, item and projectile units
esp.cpp keeps the scan and drawing (~1000 lines, down from 3665). Entity
lookups and NPC classification move to entity_query.cpp together with the
per-map caches and the session reset; combat (aimbot, silent aim, melee,
spread, triggerbot) moves to combat.cpp. Shared helpers are declared in
esp_internal.h.
| -rw-r--r-- | src/combat.cpp | 1066 | ||||
| -rw-r--r-- | src/entity_query.cpp | 460 | ||||
| -rw-r--r-- | src/esp.cpp | 1519 | ||||
| -rw-r--r-- | src/esp_internal.h | 23 |
4 files changed, 1552 insertions, 1516 deletions
diff --git a/src/combat.cpp b/src/combat.cpp new file mode 100644 index 0000000..cfc8190 --- /dev/null +++ b/src/combat.cpp | |||
| @@ -0,0 +1,1066 @@ | |||
| 1 | // combat.cpp — aimbot, silent aim, melee, spread control, triggerbot. | ||
| 2 | #include "esp_internal.h" | ||
| 3 | #include "hl2h.h" | ||
| 4 | #include <cmath> | ||
| 5 | #include <cstdio> | ||
| 6 | #include <cstring> | ||
| 7 | |||
| 8 | static int g_aim_lock = -1; | ||
| 9 | static int g_silent_lock = -1; | ||
| 10 | |||
| 11 | void aimbot_reset_lock() { g_aim_lock = -1; } | ||
| 12 | void silentaim_reset_lock() { g_silent_lock = -1; } | ||
| 13 | |||
| 14 | // max_fov is the angular radius around the crosshair. If lock_idx is non-null | ||
| 15 | // and valid, that target is kept while it is alive and visible (the "do not | ||
| 16 | // switch while the bind is held" rule); a fresh acquisition is ranked by angle | ||
| 17 | // and the nearest visible selected hitbox point. | ||
| 18 | static bool select_aim_target(const QAngle &view, unsigned bone_mask, | ||
| 19 | bool vischeck, float max_fov, const char *tag, | ||
| 20 | int *lock_idx, int &out_idx, Vector &out_point, | ||
| 21 | float &out_fov) { | ||
| 22 | out_idx = -1; | ||
| 23 | out_point = {}; | ||
| 24 | out_fov = 0.0f; | ||
| 25 | // maxs is optional: the per-target code falls back to 72 units when the | ||
| 26 | // netvar is missing, so it must not disable the whole aimbot here. | ||
| 27 | if (!g_iface.entitylist || g_net.origin < 0) | ||
| 28 | return false; | ||
| 29 | int local = engine_get_local_player(); | ||
| 30 | if (local <= 0) | ||
| 31 | return false; | ||
| 32 | void *local_ent = entitylist_get(local); | ||
| 33 | if (!local_ent) | ||
| 34 | return false; | ||
| 35 | if (g_net.health >= 0 && | ||
| 36 | *(int *)((uintptr_t)local_ent + g_net.health) <= 0) | ||
| 37 | return false; | ||
| 38 | |||
| 39 | // With the RPG equipped, armoured units (striders, gunships, dropships, | ||
| 40 | // hunter-choppers) are the ones ordinary shots cannot hurt, so prefer them | ||
| 41 | // over normal NPCs when acquiring. | ||
| 42 | bool rpg = active_weapon_contains(local_ent, "rocket_launcher") || | ||
| 43 | active_weapon_contains(local_ent, "rpg"); | ||
| 44 | |||
| 45 | // fov_limit <= 0 means "no angular limit" (kept for unlimited acquisition). | ||
| 46 | auto try_target = [&](int idx, float fov_limit, Vector &pt, | ||
| 47 | float &fov) -> bool { | ||
| 48 | if (idx <= 0 || idx == local) | ||
| 49 | return false; | ||
| 50 | void *ent = entitylist_get(idx); | ||
| 51 | if (!ent) | ||
| 52 | return false; | ||
| 53 | void *net = entity_networkable(idx); | ||
| 54 | if (!net || entity_is_dormant(net)) | ||
| 55 | return false; | ||
| 56 | const char *cls = entity_class_name(net, nullptr); | ||
| 57 | if (!cls || !is_npc_class(cls)) | ||
| 58 | return false; | ||
| 59 | uintptr_t e = (uintptr_t)ent; | ||
| 60 | int life = g_net.lifestate >= 0 | ||
| 61 | ? (int)*(unsigned char *)(e + g_net.lifestate) | ||
| 62 | : -1; | ||
| 63 | if (life != LIFE_ALIVE) | ||
| 64 | return false; | ||
| 65 | const char *model = entity_model_name(ent); | ||
| 66 | bool neutral = false; | ||
| 67 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 68 | return false; | ||
| 69 | if (!npc_can_be_damaged(idx, model)) | ||
| 70 | return false; | ||
| 71 | if (!bones_update(idx, ent)) | ||
| 72 | return false; | ||
| 73 | Vector pts[32]; | ||
| 74 | int npoints = aim_points(idx, bone_mask, pts, 32); | ||
| 75 | float best = 1e9f; | ||
| 76 | Vector best_pt{}; | ||
| 77 | for (int i = 0; i < npoints; i++) { | ||
| 78 | float f = getfov(view, angle_to(g_cam_eye, pts[i])); | ||
| 79 | if (fov_limit > 0.0f && f > fov_limit) | ||
| 80 | continue; | ||
| 81 | if (f >= best) | ||
| 82 | continue; | ||
| 83 | if (vischeck && !is_visible(g_cam_eye, pts[i], local_ent, ent)) | ||
| 84 | continue; | ||
| 85 | best = f; | ||
| 86 | best_pt = pts[i]; | ||
| 87 | } | ||
| 88 | if (best > 1e8f) | ||
| 89 | return false; | ||
| 90 | pt = best_pt; | ||
| 91 | fov = best; | ||
| 92 | return true; | ||
| 93 | }; | ||
| 94 | |||
| 95 | // Keep the locked target while it is alive and has a visible selected point. | ||
| 96 | // The lock uses a wider cone than acquisition (hysteresis), so a target that | ||
| 97 | // drifts slightly off the crosshair is not dropped, but one that swings | ||
| 98 | // behind the player is: an unbounded lock let silent aim fire backwards. | ||
| 99 | if (lock_idx && *lock_idx >= 0) { | ||
| 100 | Vector pt; | ||
| 101 | float f; | ||
| 102 | if (try_target(*lock_idx, max_fov * 1.5f, pt, f)) { | ||
| 103 | out_idx = *lock_idx; | ||
| 104 | out_point = pt; | ||
| 105 | out_fov = f; | ||
| 106 | static int lock_logs = 0; | ||
| 107 | if (lock_logs < 5) { | ||
| 108 | lock_logs++; | ||
| 109 | logd("%s: lock idx=%d fov=%.2f", tag, out_idx, f); | ||
| 110 | } | ||
| 111 | return true; | ||
| 112 | } | ||
| 113 | *lock_idx = -1; | ||
| 114 | } | ||
| 115 | |||
| 116 | using Fn = int (*)(void *); | ||
| 117 | int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); | ||
| 118 | if (hi < 0 || hi > 4096) | ||
| 119 | hi = 4096; | ||
| 120 | |||
| 121 | struct Candidate { | ||
| 122 | int idx; | ||
| 123 | float fov; | ||
| 124 | Vector approx; | ||
| 125 | bool armored; | ||
| 126 | float key; | ||
| 127 | }; | ||
| 128 | Candidate cands[16]; | ||
| 129 | int ncands = 0; | ||
| 130 | |||
| 131 | for (int idx = 1; idx <= hi; idx++) { | ||
| 132 | if (idx == local) | ||
| 133 | continue; | ||
| 134 | void *ent = entitylist_get(idx); | ||
| 135 | if (!ent) | ||
| 136 | continue; | ||
| 137 | void *net = entity_networkable(idx); | ||
| 138 | if (!net || entity_is_dormant(net)) | ||
| 139 | continue; | ||
| 140 | const char *cls = entity_class_name(net, nullptr); | ||
| 141 | if (!cls || !is_npc_class(cls)) | ||
| 142 | continue; | ||
| 143 | |||
| 144 | uintptr_t e = (uintptr_t)ent; | ||
| 145 | int life = g_net.lifestate >= 0 | ||
| 146 | ? (int)*(unsigned char *)(e + g_net.lifestate) | ||
| 147 | : -1; | ||
| 148 | if (life != LIFE_ALIVE) | ||
| 149 | continue; | ||
| 150 | const char *model = entity_model_name(ent); | ||
| 151 | bool neutral = false; | ||
| 152 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 153 | continue; | ||
| 154 | if (!npc_can_be_damaged(idx, model)) | ||
| 155 | continue; | ||
| 156 | bool armored = npc_damage_class(idx, model) == NPC_DMG_ARMORED; | ||
| 157 | |||
| 158 | float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; | ||
| 159 | if (!(maxz > 4.0f && maxz < 512.0f)) | ||
| 160 | maxz = 72.0f; | ||
| 161 | Vector origin{*(float *)(e + g_net.origin), | ||
| 162 | *(float *)(e + g_net.origin + 4), | ||
| 163 | *(float *)(e + g_net.origin + 8)}; | ||
| 164 | Vector approx{origin.x, origin.y, | ||
| 165 | origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)}; | ||
| 166 | float fov = getfov(view, angle_to(g_cam_eye, approx)); | ||
| 167 | // The prefilter samples a single point, but the selected hitbox can be a | ||
| 168 | // whole body-width away at close range. Reject only when the target's | ||
| 169 | // angular radius cannot reach the cone; try_target still enforces the | ||
| 170 | // exact per-hitbox FOV. | ||
| 171 | float ddx = approx.x - g_cam_eye.x; | ||
| 172 | float ddy = approx.y - g_cam_eye.y; | ||
| 173 | float ddz = approx.z - g_cam_eye.z; | ||
| 174 | float dist = sqrtf(ddx * ddx + ddy * ddy + ddz * ddz); | ||
| 175 | float margin = | ||
| 176 | dist > 1.0f ? atan2f(maxz, dist) * 180.0f / PI_F : 90.0f; | ||
| 177 | if (fov - margin > max_fov) | ||
| 178 | continue; | ||
| 179 | float key = (rpg && armored) ? fov - 1000.0f : fov; | ||
| 180 | |||
| 181 | int pos = ncands; | ||
| 182 | while (pos > 0 && cands[pos - 1].key > key) | ||
| 183 | pos--; | ||
| 184 | if (ncands < 16) | ||
| 185 | ncands++; | ||
| 186 | for (int j = ncands - 1; j > pos; j--) | ||
| 187 | cands[j] = cands[j - 1]; | ||
| 188 | if (pos < 16) | ||
| 189 | cands[pos] = {idx, fov, approx, armored, key}; | ||
| 190 | } | ||
| 191 | |||
| 192 | if (ncands == 0) { | ||
| 193 | static double last_none = 0.0; | ||
| 194 | double now = esp_now(); | ||
| 195 | if (now - last_none >= 2.0) { | ||
| 196 | last_none = now; | ||
| 197 | logd("%s: no hostile target within %.0f deg", tag, max_fov); | ||
| 198 | } | ||
| 199 | return false; | ||
| 200 | } | ||
| 201 | |||
| 202 | for (int ci = 0; ci < ncands; ci++) { | ||
| 203 | Vector pt; | ||
| 204 | float f; | ||
| 205 | if (!try_target(cands[ci].idx, max_fov, pt, f)) | ||
| 206 | continue; | ||
| 207 | out_idx = cands[ci].idx; | ||
| 208 | out_point = pt; | ||
| 209 | out_fov = f; | ||
| 210 | if (lock_idx) | ||
| 211 | *lock_idx = out_idx; | ||
| 212 | static int sel_logs = 0; | ||
| 213 | if (sel_logs < 5) { | ||
| 214 | sel_logs++; | ||
| 215 | logd("%s: target idx=%d fov=%.2f vischeck=%d", tag, out_idx, f, | ||
| 216 | (int)vischeck); | ||
| 217 | } | ||
| 218 | return true; | ||
| 219 | } | ||
| 220 | |||
| 221 | static double last_vis = 0.0; | ||
| 222 | double now = esp_now(); | ||
| 223 | if (now - last_vis >= 2.0) { | ||
| 224 | last_vis = now; | ||
| 225 | logd("%s: no visible aim point (vischeck=%d)", tag, (int)vischeck); | ||
| 226 | } | ||
| 227 | return false; | ||
| 228 | } | ||
| 229 | |||
| 230 | bool active_weapon_contains(void *local_ent, const char *needle) { | ||
| 231 | if (!local_ent || g_net.activeweapon < 0 || !needle) | ||
| 232 | return false; | ||
| 233 | uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); | ||
| 234 | if (h == 0xFFFFFFFFu) | ||
| 235 | return false; | ||
| 236 | int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index | ||
| 237 | if (idx <= 0 || idx > 4096) | ||
| 238 | return false; | ||
| 239 | void *wep = entitylist_get(idx); | ||
| 240 | void *net = entity_networkable(idx); | ||
| 241 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 242 | const char *model = wep ? entity_model_name(wep) : nullptr; | ||
| 243 | return (cls && strcasestr(cls, needle)) || | ||
| 244 | (model && strcasestr(model, needle)); | ||
| 245 | } | ||
| 246 | |||
| 247 | // Crossbow bolts and RPG rockets are not hitscan. Lead moving targets by the | ||
| 248 | // projectile travel time; the crossbow bolt also has gravity 0.05 (30 u/s^2). | ||
| 249 | static bool projectile_lead_point(int target_idx, const Vector &point, | ||
| 250 | Vector &out) { | ||
| 251 | int local = engine_get_local_player(); | ||
| 252 | if (local <= 0) | ||
| 253 | return false; | ||
| 254 | void *local_ent = entitylist_get(local); | ||
| 255 | if (!local_ent) | ||
| 256 | return false; | ||
| 257 | float speed = 0.0f, gravity = 0.0f; | ||
| 258 | if (active_weapon_contains(local_ent, "crossbow") || | ||
| 259 | active_weapon_contains(local_ent, "Crossbow")) { | ||
| 260 | speed = 2500.0f; | ||
| 261 | gravity = 30.0f; | ||
| 262 | } else if (active_weapon_contains(local_ent, "rocket_launcher") || | ||
| 263 | active_weapon_contains(local_ent, "w_rpg") || | ||
| 264 | active_weapon_contains(local_ent, "RPG")) { | ||
| 265 | speed = 1500.0f; | ||
| 266 | gravity = 0.0f; | ||
| 267 | } else { | ||
| 268 | return false; | ||
| 269 | } | ||
| 270 | void *target = entitylist_get(target_idx); | ||
| 271 | if (!target) | ||
| 272 | return false; | ||
| 273 | // NPCs do not network C_BaseEntity::m_vecVelocity (only the player does), so | ||
| 274 | // the client field is stale for exactly the targets the aimbot uses. Read | ||
| 275 | // the listen-server entity when available and fall back to the client value. | ||
| 276 | float v[3] = {0.0f, 0.0f, 0.0f}; | ||
| 277 | Vector srv_vel; | ||
| 278 | bool have_vel = false; | ||
| 279 | if (server_entity_velocity(target_idx, &srv_vel)) { | ||
| 280 | v[0] = srv_vel.x; | ||
| 281 | v[1] = srv_vel.y; | ||
| 282 | v[2] = srv_vel.z; | ||
| 283 | have_vel = true; | ||
| 284 | } else if (g_net.velocity >= 0) { | ||
| 285 | const float *cv = (const float *)((uintptr_t)target + g_net.velocity); | ||
| 286 | v[0] = cv[0]; | ||
| 287 | v[1] = cv[1]; | ||
| 288 | v[2] = cv[2]; | ||
| 289 | have_vel = true; | ||
| 290 | } | ||
| 291 | if (!have_vel) | ||
| 292 | return false; | ||
| 293 | float dx = point.x - g_cam_eye.x; | ||
| 294 | float dy = point.y - g_cam_eye.y; | ||
| 295 | float dz = point.z - g_cam_eye.z; | ||
| 296 | float dist = sqrtf(dx * dx + dy * dy + dz * dz); | ||
| 297 | float t = dist / speed; | ||
| 298 | out = point; | ||
| 299 | out.x += v[0] * t; | ||
| 300 | out.y += v[1] * t; | ||
| 301 | out.z += v[2] * t + 0.5f * gravity * t * t; | ||
| 302 | return true; | ||
| 303 | } | ||
| 304 | |||
| 305 | // A led point is a prediction, not a verified aim point: acquisition checked | ||
| 306 | // the un-led position, so the shift has to pass the same FOV and visibility | ||
| 307 | // tests. On failure the caller keeps the original point instead of firing into | ||
| 308 | // geometry or outside the configured cone. | ||
| 309 | static bool lead_point_ok(int target_idx, const Vector &lead, | ||
| 310 | const QAngle &view, float max_fov) { | ||
| 311 | if (getfov(view, angle_to(g_cam_eye, lead)) > max_fov) | ||
| 312 | return false; | ||
| 313 | if (!aimbot_vischeck_enabled()) | ||
| 314 | return true; | ||
| 315 | void *local_ent = entitylist_get(engine_get_local_player()); | ||
| 316 | void *target_ent = entitylist_get(target_idx); | ||
| 317 | if (!local_ent || !target_ent) | ||
| 318 | return false; | ||
| 319 | return is_visible(g_cam_eye, lead, local_ent, target_ent); | ||
| 320 | } | ||
| 321 | |||
| 322 | // Semi-auto weapons (pistol, .357, crossbow, shotgun, RPG, gravity gun) only | ||
| 323 | // fire on the IN_ATTACK press edge, so a synthetic button held every tick fires | ||
| 324 | // exactly once per buy/equip. Alternate press/release for those; automatic | ||
| 325 | // weapons keep the button held. | ||
| 326 | static bool weapon_is_semi_auto(void *local_ent) { | ||
| 327 | static const char *names[] = {"pistol", "357", | ||
| 328 | "crossbow", "shotgun", | ||
| 329 | "rocket_launcher", "physcannon", | ||
| 330 | "crowbar", nullptr}; | ||
| 331 | for (int i = 0; names[i]; i++) | ||
| 332 | if (active_weapon_contains(local_ent, names[i])) | ||
| 333 | return true; | ||
| 334 | return false; | ||
| 335 | } | ||
| 336 | |||
| 337 | static bool fire_edge_ok(CUserCmd *cmd) { | ||
| 338 | void *local_ent = entitylist_get(engine_get_local_player()); | ||
| 339 | if (!local_ent || !weapon_is_semi_auto(local_ent)) | ||
| 340 | return true; | ||
| 341 | static int last_tick = -1; | ||
| 342 | static bool phase = false; | ||
| 343 | if (cmd->tick_count != last_tick) { | ||
| 344 | last_tick = cmd->tick_count; | ||
| 345 | phase = !phase; | ||
| 346 | } | ||
| 347 | return phase; | ||
| 348 | } | ||
| 349 | |||
| 350 | bool aimbot_apply(CUserCmd *cmd) { | ||
| 351 | unsigned bone_mask = aimbot_bone_mask(); | ||
| 352 | if (!bone_mask) | ||
| 353 | return false; | ||
| 354 | QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; | ||
| 355 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 356 | bool punch = punch_angles(punch_x, punch_y); | ||
| 357 | if (punch) { | ||
| 358 | view.x += punch_x; | ||
| 359 | view.y += punch_y; | ||
| 360 | } | ||
| 361 | int target = -1; | ||
| 362 | Vector point{}; | ||
| 363 | float fov = 0.0f; | ||
| 364 | float max_fov = (float)aimbot_fov(); | ||
| 365 | if (max_fov < 1.0f) max_fov = 1.0f; | ||
| 366 | if (max_fov > 180.0f) max_fov = 180.0f; | ||
| 367 | int *lock = feature_has_bind(FEAT_AIMBOT) ? &g_aim_lock : nullptr; | ||
| 368 | if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov, | ||
| 369 | "aimbot", lock, target, point, fov)) | ||
| 370 | return false; | ||
| 371 | Vector lead; | ||
| 372 | if (projectile_lead_point(target, point, lead) && | ||
| 373 | lead_point_ok(target, lead, view, max_fov)) | ||
| 374 | point = lead; | ||
| 375 | QAngle desired = angle_to(g_cam_eye, point); | ||
| 376 | if (punch) { | ||
| 377 | desired.x -= punch_x; | ||
| 378 | desired.y -= punch_y; | ||
| 379 | } | ||
| 380 | static int want_logs = 0; | ||
| 381 | if (want_logs < 10) { | ||
| 382 | want_logs++; | ||
| 383 | logd("aimbot: target=%d aim=(%.1f %.1f %.1f) fov=%.2f want=%.2f/%.2f", | ||
| 384 | target, point.x, point.y, point.z, fov, desired.x, desired.y); | ||
| 385 | } | ||
| 386 | float dp = desired.x - cmd->viewangles[0]; | ||
| 387 | float dy = desired.y - cmd->viewangles[1]; | ||
| 388 | while (dy > 180.0f) dy -= 360.0f; | ||
| 389 | while (dy < -180.0f) dy += 360.0f; | ||
| 390 | |||
| 391 | constexpr float DEADZONE = 0.05f; | ||
| 392 | float max_step = (float)aimbot_smooth_step(); | ||
| 393 | if (max_step < 1.0f) max_step = 1.0f; | ||
| 394 | float maxd = fmaxf(fabsf(dp), fabsf(dy)); | ||
| 395 | if (maxd <= DEADZONE) | ||
| 396 | return false; | ||
| 397 | |||
| 398 | float scale = (maxd > max_step) ? (max_step / maxd) : 1.0f; | ||
| 399 | float old_pitch = cmd->viewangles[0]; | ||
| 400 | float old_yaw = cmd->viewangles[1]; | ||
| 401 | float np = cmd->viewangles[0] + dp * scale; | ||
| 402 | float ny = cmd->viewangles[1] + dy * scale; | ||
| 403 | if (np > 89.0f) np = 89.0f; | ||
| 404 | if (np < -89.0f) np = -89.0f; | ||
| 405 | while (ny > 180.0f) ny -= 360.0f; | ||
| 406 | while (ny < -180.0f) ny += 360.0f; | ||
| 407 | cmd->viewangles[0] = np; | ||
| 408 | cmd->viewangles[1] = ny; | ||
| 409 | |||
| 410 | if (g_iface.engine) { | ||
| 411 | QAngle out{np, ny, 0.0f}; | ||
| 412 | using SetViewFn = void (*)(void *, QAngle *); | ||
| 413 | vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &out); | ||
| 414 | } | ||
| 415 | static int mod_logs = 0; | ||
| 416 | if (mod_logs < 10) { | ||
| 417 | mod_logs++; | ||
| 418 | logd("aimbot: view %.2f/%.2f -> %.2f/%.2f", old_pitch, old_yaw, np, ny); | ||
| 419 | } | ||
| 420 | return true; | ||
| 421 | } | ||
| 422 | |||
| 423 | // Keep the world-space movement wish direction unchanged when the usercmd | ||
| 424 | // view angles are temporarily replaced by the silent-aim angles. | ||
| 425 | static void preserve_movement(CUserCmd *cmd, float old_yaw, float new_yaw) { | ||
| 426 | float oy = old_yaw * PI_F / 180.0f; | ||
| 427 | float ay = new_yaw * PI_F / 180.0f; | ||
| 428 | float ofx = cosf(oy), ofy = sinf(oy); | ||
| 429 | float orx = sinf(oy), ory = -cosf(oy); | ||
| 430 | float afx = cosf(ay), afy = sinf(ay); | ||
| 431 | float arx = sinf(ay), ary = -cosf(ay); | ||
| 432 | float wx = ofx * cmd->forwardmove + orx * cmd->sidemove; | ||
| 433 | float wy = ofy * cmd->forwardmove + ory * cmd->sidemove; | ||
| 434 | cmd->forwardmove = wx * afx + wy * afy; | ||
| 435 | cmd->sidemove = wx * arx + wy * ary; | ||
| 436 | } | ||
| 437 | |||
| 438 | bool silent_aim_apply(CUserCmd *cmd) { | ||
| 439 | unsigned bone_mask = aimbot_bone_mask(); | ||
| 440 | if (!bone_mask) | ||
| 441 | return false; | ||
| 442 | QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; | ||
| 443 | QAngle view{orig.x, orig.y, 0.0f}; | ||
| 444 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 445 | bool punch = punch_angles(punch_x, punch_y); | ||
| 446 | if (punch) { | ||
| 447 | view.x += punch_x; | ||
| 448 | view.y += punch_y; | ||
| 449 | } | ||
| 450 | int target = -1; | ||
| 451 | Vector point{}; | ||
| 452 | float fov = 0.0f; | ||
| 453 | float max_fov = (float)aimbot_fov(); | ||
| 454 | if (max_fov < 1.0f) max_fov = 1.0f; | ||
| 455 | if (max_fov > 180.0f) max_fov = 180.0f; | ||
| 456 | int *lock = feature_has_bind(FEAT_SILENTAIM) ? &g_silent_lock : nullptr; | ||
| 457 | if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov, | ||
| 458 | "silentaim", lock, target, point, fov)) | ||
| 459 | return false; | ||
| 460 | Vector lead; | ||
| 461 | if (projectile_lead_point(target, point, lead) && | ||
| 462 | lead_point_ok(target, lead, view, max_fov)) | ||
| 463 | point = lead; | ||
| 464 | QAngle desired = angle_to(g_cam_eye, point); | ||
| 465 | if (punch) { | ||
| 466 | desired.x -= punch_x; | ||
| 467 | desired.y -= punch_y; | ||
| 468 | } | ||
| 469 | float np = desired.x; | ||
| 470 | if (np > 89.0f) np = 89.0f; | ||
| 471 | if (np < -89.0f) np = -89.0f; | ||
| 472 | float ny = desired.y; | ||
| 473 | while (ny > 180.0f) ny -= 360.0f; | ||
| 474 | while (ny < -180.0f) ny += 360.0f; | ||
| 475 | |||
| 476 | preserve_movement(cmd, orig.y, ny); | ||
| 477 | |||
| 478 | cmd->viewangles[0] = np; | ||
| 479 | cmd->viewangles[1] = ny; | ||
| 480 | cmd->viewangles[2] = orig.z; | ||
| 481 | |||
| 482 | // Restore now and again at FRAME_RENDER_START (after the engine consumed | ||
| 483 | // the aimed usercmd); the frame-stage restore is the one that sticks. | ||
| 484 | set_silent_restore(orig); | ||
| 485 | if (g_iface.engine) { | ||
| 486 | QAngle restore{orig.x, orig.y, orig.z}; | ||
| 487 | using SetViewFn = void (*)(void *, QAngle *); | ||
| 488 | vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore); | ||
| 489 | } | ||
| 490 | if (fire_edge_ok(cmd)) | ||
| 491 | cmd->buttons |= IN_ATTACK; | ||
| 492 | static int logs = 0; | ||
| 493 | if (logs < 5) { | ||
| 494 | logs++; | ||
| 495 | logd("silentaim: fire idx=%d fov=%.2f aim=%.2f/%.2f", target, fov, np, ny); | ||
| 496 | } | ||
| 497 | return true; | ||
| 498 | } | ||
| 499 | |||
| 500 | // --------------------------------------------------------------------------- | ||
| 501 | // melee killbot | ||
| 502 | // --------------------------------------------------------------------------- | ||
| 503 | // | ||
| 504 | // The crowbar's CBaseHLBludgeonWeapon::Swing traces 75 units from | ||
| 505 | // Weapon_ShootPosition along the view forward (CROWBAR_RANGE in | ||
| 506 | // weapon_crowbar.h). It first tries a line, then a +/-16 hull backed off by | ||
| 507 | // ~28 units, and ignores targets more than ~45 degrees off the aim direction. | ||
| 508 | // We therefore only need to point the usercmd at the target for one command; | ||
| 509 | // the rendered view is restored exactly like silent aim. | ||
| 510 | |||
| 511 | static constexpr float MELEE_RANGE = 75.0f; | ||
| 512 | |||
| 513 | static float vec_dist(const Vector &a, const Vector &b) { | ||
| 514 | float dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z; | ||
| 515 | return sqrtf(dx * dx + dy * dy + dz * dz); | ||
| 516 | } | ||
| 517 | |||
| 518 | bool melee_weapon_active(void *local_ent) { | ||
| 519 | if (!local_ent) | ||
| 520 | return false; | ||
| 521 | int local = engine_get_local_player(); | ||
| 522 | if (g_iface.engineserver) | ||
| 523 | resolve_server_netvars(); | ||
| 524 | |||
| 525 | auto check = [&](int idx, const char **cls_out, | ||
| 526 | const char **model_out) -> bool { | ||
| 527 | if (idx <= 0 || idx > 4096) | ||
| 528 | return false; | ||
| 529 | void *wep = entitylist_get(idx); | ||
| 530 | void *net = entity_networkable(idx); | ||
| 531 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 532 | const char *model = wep ? entity_model_name(wep) : nullptr; | ||
| 533 | // Client weapon models can be missing/0 after a map change; recover the | ||
| 534 | // server model index so the crowbar is still recognized. | ||
| 535 | if ((!model || !*model) && g_iface.engineserver) { | ||
| 536 | void *sbase = nullptr; | ||
| 537 | server_entity_health(idx, nullptr, nullptr, &sbase); | ||
| 538 | if (sbase) | ||
| 539 | model = model_name_from_index(server_model_index(sbase)); | ||
| 540 | } | ||
| 541 | *cls_out = cls; | ||
| 542 | *model_out = model; | ||
| 543 | return (cls && strstr(cls, "Crowbar")) || | ||
| 544 | (model && strstr(model, "crowbar")); | ||
| 545 | }; | ||
| 546 | |||
| 547 | int idx = -1; | ||
| 548 | if (g_net.activeweapon >= 0) { | ||
| 549 | uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); | ||
| 550 | if (h != 0xFFFFFFFFu) | ||
| 551 | idx = (int)(h & 0x7FF); // 11-bit entity index; upper bits are serial | ||
| 552 | } | ||
| 553 | const char *cls = nullptr, *model = nullptr; | ||
| 554 | bool crowbar = check(idx, &cls, &model); | ||
| 555 | |||
| 556 | if (!crowbar) { | ||
| 557 | // Fallback: the authoritative server player's m_hActiveWeapon handle. | ||
| 558 | // Also covers a client netvar offset that resolves but does not point at | ||
| 559 | // the weapon the server is actually using. | ||
| 560 | if (local > 0) { | ||
| 561 | resolve_server_netvars(); | ||
| 562 | int si = server_active_weapon(local); | ||
| 563 | if (si > 0 && si != idx) { | ||
| 564 | idx = si; | ||
| 565 | crowbar = check(idx, &cls, &model); | ||
| 566 | } | ||
| 567 | } | ||
| 568 | } | ||
| 569 | |||
| 570 | static int last_idx = -2; | ||
| 571 | static bool last_crowbar = false; | ||
| 572 | if (idx != last_idx || crowbar != last_crowbar) { | ||
| 573 | last_idx = idx; | ||
| 574 | last_crowbar = crowbar; | ||
| 575 | logf("melee: active idx=%d cls=%s model=%s crowbar=%d", idx, | ||
| 576 | cls ? cls : "?", model ? model : "?", (int)crowbar); | ||
| 577 | if (!crowbar) { | ||
| 578 | static bool warned = false; | ||
| 579 | if (!warned) { | ||
| 580 | warned = true; | ||
| 581 | toastf(TOAST_WARN, "Melee killbot: equip the crowbar"); | ||
| 582 | } | ||
| 583 | } | ||
| 584 | } | ||
| 585 | return crowbar; | ||
| 586 | } | ||
| 587 | |||
| 588 | bool gravity_gun_active(void *local_ent) { | ||
| 589 | if (!local_ent) | ||
| 590 | return false; | ||
| 591 | if (active_weapon_contains(local_ent, "physcannon")) | ||
| 592 | return true; | ||
| 593 | // Same server fallback as the crowbar: map transitions can leave the | ||
| 594 | // client activeweapon handle stale. | ||
| 595 | int local = engine_get_local_player(); | ||
| 596 | if (local <= 0 || !g_iface.engineserver) | ||
| 597 | return false; | ||
| 598 | resolve_server_netvars(); | ||
| 599 | int si = server_active_weapon(local); | ||
| 600 | if (si <= 0) | ||
| 601 | return false; | ||
| 602 | void *wep = entitylist_get(si); | ||
| 603 | void *net = entity_networkable(si); | ||
| 604 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 605 | const char *model = wep ? entity_model_name(wep) : nullptr; | ||
| 606 | if (!model) { | ||
| 607 | void *sbase = nullptr; | ||
| 608 | server_entity_health(si, nullptr, nullptr, &sbase); | ||
| 609 | if (sbase) | ||
| 610 | model = model_name_from_index(server_model_index(sbase)); | ||
| 611 | } | ||
| 612 | return (cls && strcasestr(cls, "physcannon")) || | ||
| 613 | (model && strcasestr(model, "physcannon")); | ||
| 614 | } | ||
| 615 | |||
| 616 | // The citadel supercharges the gravity gun (d3_citadel_03 onward). It can then | ||
| 617 | // punt any NPC, so the melee aimbot should not restrict itself to | ||
| 618 | // physics-vulnerable units. | ||
| 619 | static bool supercharged_gravity_gun() { | ||
| 620 | if (!g_iface.engine) | ||
| 621 | return false; | ||
| 622 | using Fn = const char *(*)(void *); | ||
| 623 | const char *m = vfunc<Fn>(g_iface.engine, 51)(g_iface.engine); | ||
| 624 | if (!m) | ||
| 625 | return false; | ||
| 626 | bool supe = strstr(m, "d3_citadel_03") || strstr(m, "d3_citadel_04") || | ||
| 627 | strstr(m, "d3_breen_01"); | ||
| 628 | static const char *logged_map = nullptr; | ||
| 629 | if (logged_map != m) { | ||
| 630 | logged_map = m; | ||
| 631 | logf("melee: gravity gun map=%s supercharged=%d", m, (int)supe); | ||
| 632 | } | ||
| 633 | return supe; | ||
| 634 | } | ||
| 635 | |||
| 636 | bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { | ||
| 637 | if (!g_iface.entitylist || g_net.origin < 0) | ||
| 638 | return false; | ||
| 639 | int local = engine_get_local_player(); | ||
| 640 | if (local <= 0) | ||
| 641 | return false; | ||
| 642 | void *local_ent = entitylist_get(local); | ||
| 643 | if (!local_ent) | ||
| 644 | return false; | ||
| 645 | if (g_net.health >= 0 && | ||
| 646 | *(int *)((uintptr_t)local_ent + g_net.health) <= 0) | ||
| 647 | return false; | ||
| 648 | |||
| 649 | // The punted object can travel much farther than the crowbar. | ||
| 650 | float max_range = gravity ? 1200.0f : MELEE_RANGE; | ||
| 651 | bool supercharged = gravity && supercharged_gravity_gun(); | ||
| 652 | |||
| 653 | using Fn = int (*)(void *); | ||
| 654 | int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); | ||
| 655 | if (hi < 0 || hi > 4096) | ||
| 656 | hi = 4096; | ||
| 657 | |||
| 658 | struct Cand { | ||
| 659 | int idx; | ||
| 660 | float dist; | ||
| 661 | Vector closest; | ||
| 662 | Vector center; | ||
| 663 | }; | ||
| 664 | Cand cands[16]; | ||
| 665 | int n = 0; | ||
| 666 | |||
| 667 | for (int idx = 1; idx <= hi; idx++) { | ||
| 668 | if (idx == local) | ||
| 669 | continue; | ||
| 670 | void *ent = entitylist_get(idx); | ||
| 671 | if (!ent) | ||
| 672 | continue; | ||
| 673 | void *net = entity_networkable(idx); | ||
| 674 | if (!net || entity_is_dormant(net)) | ||
| 675 | continue; | ||
| 676 | const char *cls = entity_class_name(net, nullptr); | ||
| 677 | if (!cls || !is_npc_class(cls)) | ||
| 678 | continue; | ||
| 679 | uintptr_t e = (uintptr_t)ent; | ||
| 680 | int life = g_net.lifestate >= 0 | ||
| 681 | ? (int)*(unsigned char *)(e + g_net.lifestate) | ||
| 682 | : -1; | ||
| 683 | if (life != LIFE_ALIVE) | ||
| 684 | continue; | ||
| 685 | const char *model = entity_model_name(ent); | ||
| 686 | bool neutral = false; | ||
| 687 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 688 | continue; | ||
| 689 | if (!npc_can_be_damaged(idx, model)) | ||
| 690 | continue; | ||
| 691 | // The normal gravity gun is only the tool for physics-vulnerable units; | ||
| 692 | // the citadel version behaves like a normal weapon. | ||
| 693 | if (gravity && !supercharged && | ||
| 694 | npc_damage_class(idx, model) != NPC_DMG_VULNERABLE) | ||
| 695 | continue; | ||
| 696 | |||
| 697 | Vector origin{*(float *)(e + g_net.origin), | ||
| 698 | *(float *)(e + g_net.origin + 4), | ||
| 699 | *(float *)(e + g_net.origin + 8)}; | ||
| 700 | float minx = -16.0f, miny = -16.0f, minz = 0.0f; | ||
| 701 | float maxx = 16.0f, maxy = 16.0f, maxz = 72.0f; | ||
| 702 | if (g_net.mins >= 0) { | ||
| 703 | minx = *(float *)(e + g_net.mins); | ||
| 704 | miny = *(float *)(e + g_net.mins + 4); | ||
| 705 | minz = *(float *)(e + g_net.mins + 8); | ||
| 706 | } | ||
| 707 | if (g_net.maxs >= 0) { | ||
| 708 | maxx = *(float *)(e + g_net.maxs); | ||
| 709 | maxy = *(float *)(e + g_net.maxs + 4); | ||
| 710 | maxz = *(float *)(e + g_net.maxs + 8); | ||
| 711 | } | ||
| 712 | // Reject absurd boxes in both directions; a garbage mins value that is | ||
| 713 | // hugely negative would otherwise produce an AABB spanning the map. | ||
| 714 | if (!(minz > -512.0f && maxz > minz + 4.0f && maxz < 512.0f)) { | ||
| 715 | minz = 0.0f; | ||
| 716 | maxz = 72.0f; | ||
| 717 | } | ||
| 718 | if (!(minx > -512.0f && maxx > minx + 1.0f && maxx < 256.0f)) { | ||
| 719 | minx = -16.0f; | ||
| 720 | maxx = 16.0f; | ||
| 721 | } | ||
| 722 | if (!(miny > -512.0f && maxy > miny + 1.0f && maxy < 256.0f)) { | ||
| 723 | miny = -16.0f; | ||
| 724 | maxy = 16.0f; | ||
| 725 | } | ||
| 726 | |||
| 727 | Vector lo{origin.x + minx, origin.y + miny, origin.z + minz}; | ||
| 728 | Vector hiV{origin.x + maxx, origin.y + maxy, origin.z + maxz}; | ||
| 729 | Vector closest{g_cam_eye.x, g_cam_eye.y, g_cam_eye.z}; | ||
| 730 | if (closest.x < lo.x) closest.x = lo.x; | ||
| 731 | if (closest.x > hiV.x) closest.x = hiV.x; | ||
| 732 | if (closest.y < lo.y) closest.y = lo.y; | ||
| 733 | if (closest.y > hiV.y) closest.y = hiV.y; | ||
| 734 | if (closest.z < lo.z) closest.z = lo.z; | ||
| 735 | if (closest.z > hiV.z) closest.z = hiV.z; | ||
| 736 | float d = vec_dist(g_cam_eye, closest); | ||
| 737 | if (d > max_range) | ||
| 738 | continue; | ||
| 739 | |||
| 740 | Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f, | ||
| 741 | (lo.z + hiV.z) * 0.5f}; | ||
| 742 | |||
| 743 | int pos = n; | ||
| 744 | while (pos > 0 && cands[pos - 1].dist > d) | ||
| 745 | pos--; | ||
| 746 | if (n < 16) | ||
| 747 | n++; | ||
| 748 | else if (pos >= 16) | ||
| 749 | continue; | ||
| 750 | for (int j = n - 1; j > pos; j--) | ||
| 751 | cands[j] = cands[j - 1]; | ||
| 752 | cands[pos] = {idx, d, closest, center}; | ||
| 753 | } | ||
| 754 | |||
| 755 | for (int ci = 0; ci < n; ci++) { | ||
| 756 | const Cand &c = cands[ci]; | ||
| 757 | Vector aim{}; | ||
| 758 | bool have_aim = false; | ||
| 759 | |||
| 760 | // Prefer the nearest visible hitbox center. This handles small NPCs | ||
| 761 | // (headcrabs, antlions) whose AABB center can be less useful than the | ||
| 762 | // actual body hitboxes. | ||
| 763 | void *tent = entitylist_get(c.idx); | ||
| 764 | if (tent && bones_update(c.idx, tent)) { | ||
| 765 | Vector pts[32]; | ||
| 766 | int np = aim_points(c.idx, 0xFE, pts, 32); | ||
| 767 | float bestd = 1e9f; | ||
| 768 | for (int i = 0; i < np; i++) { | ||
| 769 | float pd = vec_dist(g_cam_eye, pts[i]); | ||
| 770 | if (pd > max_range || pd >= bestd) | ||
| 771 | continue; | ||
| 772 | if (!is_visible(g_cam_eye, pts[i], local_ent, tent)) | ||
| 773 | continue; | ||
| 774 | bestd = pd; | ||
| 775 | aim = pts[i]; | ||
| 776 | have_aim = true; | ||
| 777 | } | ||
| 778 | } | ||
| 779 | |||
| 780 | if (!have_aim) { | ||
| 781 | aim = c.center; | ||
| 782 | float ad = vec_dist(g_cam_eye, aim); | ||
| 783 | if (ad > max_range) { | ||
| 784 | // The body is in range but its center is not: aim just inside the | ||
| 785 | // closest point on the AABB. | ||
| 786 | aim = c.closest; | ||
| 787 | aim.x += (c.center.x - c.closest.x) * 0.25f; | ||
| 788 | aim.y += (c.center.y - c.closest.y) * 0.25f; | ||
| 789 | aim.z += (c.center.z - c.closest.z) * 0.25f; | ||
| 790 | ad = vec_dist(g_cam_eye, aim); | ||
| 791 | if (ad > max_range) | ||
| 792 | aim = c.closest; | ||
| 793 | } | ||
| 794 | if (!is_visible(g_cam_eye, aim, local_ent, tent)) | ||
| 795 | continue; | ||
| 796 | } | ||
| 797 | |||
| 798 | QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; | ||
| 799 | QAngle desired = angle_to(g_cam_eye, aim); | ||
| 800 | // The crowbar uses GetAutoaimVector (adds punch); the gravity gun's | ||
| 801 | // launch uses EyeVectors (no punch), so only compensate for melee. | ||
| 802 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 803 | if (!gravity && punch_angles(punch_x, punch_y)) { | ||
| 804 | desired.x -= punch_x; | ||
| 805 | desired.y -= punch_y; | ||
| 806 | } | ||
| 807 | float np = desired.x; | ||
| 808 | if (np > 89.0f) np = 89.0f; | ||
| 809 | if (np < -89.0f) np = -89.0f; | ||
| 810 | float ny = desired.y; | ||
| 811 | while (ny > 180.0f) ny -= 360.0f; | ||
| 812 | while (ny < -180.0f) ny += 360.0f; | ||
| 813 | |||
| 814 | preserve_movement(cmd, orig.y, ny); | ||
| 815 | cmd->viewangles[0] = np; | ||
| 816 | cmd->viewangles[1] = ny; | ||
| 817 | cmd->viewangles[2] = orig.z; | ||
| 818 | set_silent_restore(orig); | ||
| 819 | if (g_iface.engine) { | ||
| 820 | QAngle restore{orig.x, orig.y, orig.z}; | ||
| 821 | using SetViewFn = void (*)(void *, QAngle *); | ||
| 822 | vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore); | ||
| 823 | } | ||
| 824 | // Gravity gun primary attack launches the held object (EyeVectors); | ||
| 825 | // secondary would drop it instead. | ||
| 826 | if (fire_edge_ok(cmd)) | ||
| 827 | cmd->buttons |= IN_ATTACK; | ||
| 828 | |||
| 829 | static int logs = 0; | ||
| 830 | if (logs < 10) { | ||
| 831 | logs++; | ||
| 832 | logd("melee: %s idx=%d dist=%.1f aim=(%.1f %.1f %.1f)", | ||
| 833 | gravity ? "punt" : "swing", c.idx, c.dist, aim.x, aim.y, aim.z); | ||
| 834 | } | ||
| 835 | return true; | ||
| 836 | } | ||
| 837 | |||
| 838 | static double last_none = 0.0; | ||
| 839 | double now = esp_now(); | ||
| 840 | if (now - last_none >= 2.0) { | ||
| 841 | last_none = now; | ||
| 842 | logd("melee: no visible target within %.0f", max_range); | ||
| 843 | } | ||
| 844 | return false; | ||
| 845 | } | ||
| 846 | |||
| 847 | // --------------------------------------------------------------------------- | ||
| 848 | // weapon spread control | ||
| 849 | // --------------------------------------------------------------------------- | ||
| 850 | // | ||
| 851 | // HL2's CBaseEntity::FireBullets uses CBaseEntity::GetPredictionRandomSeed() | ||
| 852 | // (the client usercmd's random_seed) unless a weapon opts into | ||
| 853 | // m_bUseServerRandomSeed. HL2 weapons do not, so the client can choose the | ||
| 854 | // 8-bit seed and with it the random cone offset. CShotManipulator::ApplySpread | ||
| 855 | // draws four uniform floats per attempt; we precompute the best seed for one | ||
| 856 | // shot and for the shotgun's pellet windows with the engine's own RNG. | ||
| 857 | |||
| 858 | static bool g_spread_ready = false; | ||
| 859 | static int g_spread_seed1 = 0; | ||
| 860 | static int g_spread_seed8 = 0; // first pellet exact, 7 spread | ||
| 861 | static int g_spread_seed12 = 0; // all 12 spread | ||
| 862 | int g_spread_pellets = 1; | ||
| 863 | |||
| 864 | static void spread_compute() { | ||
| 865 | using SeedFn = void (*)(int); | ||
| 866 | using FloatFn = float (*)(float, float); | ||
| 867 | SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed"); | ||
| 868 | FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat"); | ||
| 869 | if (!seed || !rf) { | ||
| 870 | size_t slash = mod_engine().path.find_last_of('/'); | ||
| 871 | if (slash != std::string::npos) { | ||
| 872 | std::string path = mod_engine().path.substr(0, slash + 1) + "libvstdlib.so"; | ||
| 873 | void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | ||
| 874 | if (!h) | ||
| 875 | h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); | ||
| 876 | if (h) { | ||
| 877 | seed = (SeedFn)dlsym(h, "RandomSeed"); | ||
| 878 | rf = (FloatFn)dlsym(h, "RandomFloat"); | ||
| 879 | } | ||
| 880 | } | ||
| 881 | } | ||
| 882 | if (!seed || !rf) { | ||
| 883 | logf("spread: RandomSeed/RandomFloat unavailable"); | ||
| 884 | return; | ||
| 885 | } | ||
| 886 | |||
| 887 | auto draw = [&](int s, float &x, float &y) { | ||
| 888 | seed(s); | ||
| 889 | int it = 0; | ||
| 890 | do { | ||
| 891 | // CShotManipulator::ApplySpread with the default bias of 1.0: | ||
| 892 | // shotBias = 1.0, flatness = 0.5. | ||
| 893 | x = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f; | ||
| 894 | y = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f; | ||
| 895 | it++; | ||
| 896 | } while (x * x + y * y > 1.0f && it < 64); | ||
| 897 | }; | ||
| 898 | |||
| 899 | float best1 = 1e9f; | ||
| 900 | for (int s = 0; s < 256; s++) { | ||
| 901 | float x, y; | ||
| 902 | draw(s, x, y); | ||
| 903 | float r2 = x * x + y * y; | ||
| 904 | if (r2 < best1) { | ||
| 905 | best1 = r2; | ||
| 906 | g_spread_seed1 = s; | ||
| 907 | } | ||
| 908 | } | ||
| 909 | |||
| 910 | auto window = [&](int n, int first) { | ||
| 911 | int best_s = 0; | ||
| 912 | float best = 1e9f; | ||
| 913 | for (int s = 0; s < 256; s++) { | ||
| 914 | float score = 0.0f; | ||
| 915 | for (int i = first; i < n; i++) { | ||
| 916 | float x, y; | ||
| 917 | draw(s + i, x, y); | ||
| 918 | score += x * x + y * y; | ||
| 919 | } | ||
| 920 | if (score < best) { | ||
| 921 | best = score; | ||
| 922 | best_s = s; | ||
| 923 | } | ||
| 924 | } | ||
| 925 | return best_s; | ||
| 926 | }; | ||
| 927 | g_spread_seed8 = window(8, 1); | ||
| 928 | g_spread_seed12 = window(12, 0); | ||
| 929 | seed(0); | ||
| 930 | |||
| 931 | logd("spread: seeds single=%d shotgun8=%d shotgun12=%d", g_spread_seed1, | ||
| 932 | g_spread_seed8, g_spread_seed12); | ||
| 933 | g_spread_ready = true; | ||
| 934 | } | ||
| 935 | |||
| 936 | static bool active_weapon_is_shotgun(void *local_ent) { | ||
| 937 | if (!local_ent || g_net.activeweapon < 0) | ||
| 938 | return false; | ||
| 939 | uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); | ||
| 940 | if (h == 0xFFFFFFFFu) | ||
| 941 | return false; | ||
| 942 | int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index | ||
| 943 | if (idx <= 0 || idx > 4096) | ||
| 944 | return false; | ||
| 945 | void *wep = entitylist_get(idx); | ||
| 946 | void *net = entity_networkable(idx); | ||
| 947 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 948 | const char *model = wep ? entity_model_name(wep) : nullptr; | ||
| 949 | return (cls && strstr(cls, "Shotgun")) || | ||
| 950 | (model && strstr(model, "shotgun")); | ||
| 951 | } | ||
| 952 | |||
| 953 | bool spread_control_apply(CUserCmd *cmd, void *local_ent) { | ||
| 954 | if (!feature_active(FEAT_SPREAD) || !spread_no_spread_enabled()) | ||
| 955 | return false; | ||
| 956 | if (!(cmd->buttons & (IN_ATTACK | IN_ATTACK2))) | ||
| 957 | return false; | ||
| 958 | if (!g_spread_ready) { | ||
| 959 | static bool tried = false; | ||
| 960 | if (!tried) { | ||
| 961 | tried = true; | ||
| 962 | spread_compute(); | ||
| 963 | } | ||
| 964 | } | ||
| 965 | if (!g_spread_ready) | ||
| 966 | return false; | ||
| 967 | |||
| 968 | bool shotgun = active_weapon_is_shotgun(local_ent); | ||
| 969 | int pellets = 1; | ||
| 970 | if (shotgun) | ||
| 971 | pellets = (cmd->buttons & IN_ATTACK2) ? 12 : 8; | ||
| 972 | g_spread_pellets = pellets; | ||
| 973 | int seed = spread_best_seed(pellets); | ||
| 974 | if (cmd->random_seed == seed) | ||
| 975 | return false; | ||
| 976 | cmd->random_seed = seed; | ||
| 977 | |||
| 978 | static int logs = 0; | ||
| 979 | if (logs < 10) { | ||
| 980 | logs++; | ||
| 981 | logd("spread: seed=%d pellets=%d", seed, pellets); | ||
| 982 | } | ||
| 983 | return true; | ||
| 984 | } | ||
| 985 | |||
| 986 | int spread_best_seed(int pellets) { | ||
| 987 | if (!g_spread_ready) | ||
| 988 | return 33; | ||
| 989 | if (pellets >= 12) | ||
| 990 | return g_spread_seed12; | ||
| 991 | if (pellets >= 8) | ||
| 992 | return g_spread_seed8; | ||
| 993 | return g_spread_seed1; | ||
| 994 | } | ||
| 995 | |||
| 996 | bool triggerbot_apply(CUserCmd *cmd) { | ||
| 997 | if (!g_iface.enginetrace || !g_iface.entitylist) | ||
| 998 | return false; | ||
| 999 | int local = engine_get_local_player(); | ||
| 1000 | if (local <= 0) | ||
| 1001 | return false; | ||
| 1002 | void *local_ent = entitylist_get(local); | ||
| 1003 | if (!local_ent) | ||
| 1004 | return false; | ||
| 1005 | if (g_net.health >= 0 && | ||
| 1006 | *(int *)((uintptr_t)local_ent + g_net.health) <= 0) | ||
| 1007 | return false; | ||
| 1008 | |||
| 1009 | QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; | ||
| 1010 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 1011 | if (punch_angles(punch_x, punch_y)) { | ||
| 1012 | view.x += punch_x; | ||
| 1013 | view.y += punch_y; | ||
| 1014 | } | ||
| 1015 | Vector fwd = angle_forward_vector(view); | ||
| 1016 | Vector start = g_cam_eye; | ||
| 1017 | Vector end{start.x + fwd.x * 8192.0f, start.y + fwd.y * 8192.0f, | ||
| 1018 | start.z + fwd.z * 8192.0f}; | ||
| 1019 | void *hit = trace_entity(start, end, local_ent); | ||
| 1020 | if (!hit) | ||
| 1021 | return false; | ||
| 1022 | |||
| 1023 | int idx = entptr_lookup(hit); | ||
| 1024 | if (idx <= 0) { | ||
| 1025 | // Cache miss: ESP is off, or this entity appeared since the last scan. | ||
| 1026 | using Fn = int (*)(void *); | ||
| 1027 | int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); | ||
| 1028 | if (hi < 0 || hi > 4096) | ||
| 1029 | hi = 4096; | ||
| 1030 | for (int i = 1; i <= hi; i++) { | ||
| 1031 | if (entitylist_get(i) != hit) | ||
| 1032 | continue; | ||
| 1033 | idx = i; | ||
| 1034 | entptr_store(hit, i); | ||
| 1035 | break; | ||
| 1036 | } | ||
| 1037 | if (idx <= 0) | ||
| 1038 | return false; | ||
| 1039 | } | ||
| 1040 | void *net = entity_networkable(idx); | ||
| 1041 | if (entity_is_dormant(net)) | ||
| 1042 | return false; | ||
| 1043 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 1044 | if (!cls || !is_npc_class(cls)) | ||
| 1045 | return false; | ||
| 1046 | uintptr_t e = (uintptr_t)hit; | ||
| 1047 | int life = g_net.lifestate >= 0 | ||
| 1048 | ? (int)*(unsigned char *)(e + g_net.lifestate) | ||
| 1049 | : -1; | ||
| 1050 | if (life != LIFE_ALIVE) | ||
| 1051 | return false; | ||
| 1052 | const char *model = entity_model_name(hit); | ||
| 1053 | bool neutral = false; | ||
| 1054 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 1055 | return false; | ||
| 1056 | if (!npc_can_be_damaged(idx, model)) | ||
| 1057 | return false; | ||
| 1058 | if (fire_edge_ok(cmd)) | ||
| 1059 | cmd->buttons |= IN_ATTACK; | ||
| 1060 | static int logs = 0; | ||
| 1061 | if (logs < 5) { | ||
| 1062 | logs++; | ||
| 1063 | logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?"); | ||
| 1064 | } | ||
| 1065 | return true; | ||
| 1066 | } | ||
diff --git a/src/entity_query.cpp b/src/entity_query.cpp new file mode 100644 index 0000000..4697bf2 --- /dev/null +++ b/src/entity_query.cpp | |||
| @@ -0,0 +1,460 @@ | |||
| 1 | // entity_query.cpp — client entity lookups, NPC classification, per-map caches. | ||
| 2 | #include "esp_internal.h" | ||
| 3 | #include "hl2h.h" | ||
| 4 | #include <cmath> | ||
| 5 | #include <cstring> | ||
| 6 | |||
| 7 | using GetNetFn = void *(*)(void *, int); | ||
| 8 | using GetCCFn = void *(*)(void *); | ||
| 9 | using GetModelFn = const void *(*)(void *, int); | ||
| 10 | using GetModelNameFn = const char *(*)(void *, const void *); | ||
| 11 | |||
| 12 | // Listen-server sanity latch. Only ever set once per map (esp_reset_session), | ||
| 13 | // and only after both model indices are valid: latching on the transient | ||
| 14 | // cli_model == 0 at map load disabled server health for the whole process. | ||
| 15 | bool g_srv_checked = false; | ||
| 16 | bool g_srv_trusted = false; | ||
| 17 | |||
| 18 | // Per-map caches. The ESP resolves a model name for every entity on every scan | ||
| 19 | // and the chams hook asks for a relation on every model draw; both are virtual | ||
| 20 | // call chains into the engine plus server datamap reads, so memoise them. | ||
| 21 | static constexpr int MODEL_CACHE = 16384; | ||
| 22 | static const char *g_model_name_cache[MODEL_CACHE]; | ||
| 23 | static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail | ||
| 24 | |||
| 25 | // Direct-mapped entity pointer -> index map, filled by esp_collect. The | ||
| 26 | // triggerbot traces a ray and gets a pointer back, and used to find its index | ||
| 27 | // by walking up to 4096 entity-list slots on every fire tick. | ||
| 28 | static constexpr int ENTPTR_CACHE = 4096; | ||
| 29 | static void *g_entptr_key[ENTPTR_CACHE]; | ||
| 30 | static int g_entptr_idx[ENTPTR_CACHE]; | ||
| 31 | |||
| 32 | void esp_reset_session() { | ||
| 33 | g_srv_checked = false; | ||
| 34 | g_srv_trusted = false; | ||
| 35 | memset(g_model_name_cache, 0, sizeof(g_model_name_cache)); | ||
| 36 | memset(g_model_state_cache, 0, sizeof(g_model_state_cache)); | ||
| 37 | memset(g_entptr_key, 0, sizeof(g_entptr_key)); | ||
| 38 | memset(g_entptr_idx, 0, sizeof(g_entptr_idx)); | ||
| 39 | } | ||
| 40 | |||
| 41 | void *entity_networkable(int idx) { | ||
| 42 | return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx); | ||
| 43 | } | ||
| 44 | |||
| 45 | // IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8. | ||
| 46 | const char *entity_class_name(void *net, void **out_cc) { | ||
| 47 | if (!net) return nullptr; | ||
| 48 | void *cc = vfunc<GetCCFn>(net, 2)(net); | ||
| 49 | if (out_cc) *out_cc = cc; | ||
| 50 | return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; | ||
| 51 | } | ||
| 52 | |||
| 53 | // IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant | ||
| 54 | // 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK, | ||
| 55 | // so the slot is validated once against the local player, which can never be | ||
| 56 | // dormant: a wrong slot disables the check instead of misreporting entities. | ||
| 57 | // Dormant entities keep their last networked origin, so drawing or aiming at | ||
| 58 | // them produces ghosts at stale positions. | ||
| 59 | static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled | ||
| 60 | |||
| 61 | static void dormant_selftest() { | ||
| 62 | // Leave the state untested (0) until a local player exists: the first call | ||
| 63 | // can come from the main menu, and latching "disabled" there would turn the | ||
| 64 | // check off for the whole session. | ||
| 65 | if (!g_iface.entitylist) | ||
| 66 | return; | ||
| 67 | int local = engine_get_local_player(); | ||
| 68 | if (local <= 0) | ||
| 69 | return; | ||
| 70 | void *net = entity_networkable(local); | ||
| 71 | if (!net) | ||
| 72 | return; | ||
| 73 | void **vt = *(void ***)net; | ||
| 74 | if (!vt || !mod_client().contains((uintptr_t)vt[8])) { | ||
| 75 | g_dormant_state = -1; | ||
| 76 | logf("dormant check disabled: slot 8 outside client module"); | ||
| 77 | return; | ||
| 78 | } | ||
| 79 | using DormantFn = unsigned char (*)(void *); | ||
| 80 | if (vfunc<DormantFn>(net, 8)(net) != 0) { | ||
| 81 | g_dormant_state = -1; | ||
| 82 | logf("dormant check disabled: local player reported dormant"); | ||
| 83 | return; | ||
| 84 | } | ||
| 85 | g_dormant_state = 1; | ||
| 86 | logd("dormant check enabled"); | ||
| 87 | } | ||
| 88 | |||
| 89 | bool entity_is_dormant(void *net) { | ||
| 90 | if (!net) | ||
| 91 | return false; | ||
| 92 | if (g_dormant_state == 0) | ||
| 93 | dormant_selftest(); | ||
| 94 | if (g_dormant_state != 1) | ||
| 95 | return false; | ||
| 96 | using DormantFn = unsigned char (*)(void *); | ||
| 97 | return vfunc<DormantFn>(net, 8)(net) != 0; | ||
| 98 | } | ||
| 99 | |||
| 100 | // Entity pointer -> index, filled by the entity scans. The triggerbot gets a | ||
| 101 | // pointer back from the trace and needs the index; the alternative is walking | ||
| 102 | // up to 4096 entity-list slots on every fire tick. | ||
| 103 | int entptr_lookup(void *ent) { | ||
| 104 | if (!ent) | ||
| 105 | return -1; | ||
| 106 | unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); | ||
| 107 | return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1; | ||
| 108 | } | ||
| 109 | |||
| 110 | void entptr_store(void *ent, int idx) { | ||
| 111 | if (!ent || idx <= 0) | ||
| 112 | return; | ||
| 113 | unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); | ||
| 114 | g_entptr_key[h] = ent; | ||
| 115 | g_entptr_idx[h] = idx; | ||
| 116 | } | ||
| 117 | |||
| 118 | // m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. | ||
| 119 | // Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. | ||
| 120 | const char *model_name_from_index(int modelindex) { | ||
| 121 | if (!g_iface.modelinfo || modelindex <= 0) | ||
| 122 | return nullptr; | ||
| 123 | if (modelindex < MODEL_CACHE) { | ||
| 124 | if (g_model_state_cache[modelindex] == 1) | ||
| 125 | return g_model_name_cache[modelindex]; | ||
| 126 | if (g_model_state_cache[modelindex] == 2) | ||
| 127 | return nullptr; | ||
| 128 | } | ||
| 129 | const void *model = | ||
| 130 | vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); | ||
| 131 | if (!model) { | ||
| 132 | if (modelindex < MODEL_CACHE) | ||
| 133 | g_model_state_cache[modelindex] = 2; | ||
| 134 | return nullptr; | ||
| 135 | } | ||
| 136 | const char *name = | ||
| 137 | vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model); | ||
| 138 | if (modelindex < MODEL_CACHE) { | ||
| 139 | g_model_name_cache[modelindex] = name; | ||
| 140 | g_model_state_cache[modelindex] = name ? 1 : 2; | ||
| 141 | } | ||
| 142 | return name; | ||
| 143 | } | ||
| 144 | |||
| 145 | const char *entity_model_name(void *ent) { | ||
| 146 | if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr; | ||
| 147 | int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); | ||
| 148 | return model_name_from_index(modelindex); | ||
| 149 | } | ||
| 150 | |||
| 151 | // Live origin for a cached entity index. Item/projectile discovery is throttled | ||
| 152 | // to 10 Hz (it walks the whole entity list), but the markers must follow the | ||
| 153 | // world at render rate; refreshing one netvar read per cached target is cheap. | ||
| 154 | bool entity_origin(void *ent, Vector &out) { | ||
| 155 | if (!ent || g_net.origin < 0) | ||
| 156 | return false; | ||
| 157 | uintptr_t e = (uintptr_t)ent; | ||
| 158 | out.x = *(float *)(e + g_net.origin); | ||
| 159 | out.y = *(float *)(e + g_net.origin + 4); | ||
| 160 | out.z = *(float *)(e + g_net.origin + 8); | ||
| 161 | return true; | ||
| 162 | } | ||
| 163 | |||
| 164 | // HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all | ||
| 165 | // use the single client class "CAI_BaseNPC" — only special NPCs register their | ||
| 166 | // own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). | ||
| 167 | bool is_npc_class(const char *name) { | ||
| 168 | if (!name) return false; | ||
| 169 | return strstr(name, "NPC") != nullptr || | ||
| 170 | strstr(name, "BaseHelicopter") != nullptr; | ||
| 171 | } | ||
| 172 | |||
| 173 | // Used by the chams hook to decide whether a model-render entity is an NPC. | ||
| 174 | bool esp_is_npc_index(int idx) { | ||
| 175 | if (!g_iface.entitylist || idx <= 0 || idx > 8192) | ||
| 176 | return false; | ||
| 177 | void *net = entity_networkable(idx); | ||
| 178 | if (!net) | ||
| 179 | return false; | ||
| 180 | return is_npc_class(entity_class_name(net, nullptr)); | ||
| 181 | } | ||
| 182 | |||
| 183 | bool esp_is_viewmodel_index(int idx) { | ||
| 184 | if (!g_iface.entitylist || idx <= 0 || idx > 8192) | ||
| 185 | return false; | ||
| 186 | void *ent = entitylist_get(idx); | ||
| 187 | if (!ent) | ||
| 188 | return false; | ||
| 189 | void *net = entity_networkable(idx); | ||
| 190 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 191 | if (cls && strcasestr(cls, "viewmodel")) | ||
| 192 | return true; | ||
| 193 | const char *model = entity_model_name(ent); | ||
| 194 | return model && (strstr(model, "/v_") || strstr(model, "\\v_")); | ||
| 195 | } | ||
| 196 | |||
| 197 | // 0 = hostile, 1 = neutral, 2 = friendly, -1 = not an NPC. | ||
| 198 | bool npc_is_friendly(int idx, const char *model, int local_idx, | ||
| 199 | bool &neutral); | ||
| 200 | int esp_npc_relation(int idx) { | ||
| 201 | // Called from the chams hook up to three times per entity per frame (visible, | ||
| 202 | // through-wall, wireframe pass) and the work behind it is a chain of server | ||
| 203 | // edict/datamap reads. A short TTL cache keeps that out of the render path. | ||
| 204 | struct RelCache { | ||
| 205 | int idx; | ||
| 206 | double at; | ||
| 207 | int val; | ||
| 208 | }; | ||
| 209 | static RelCache cache[256]; | ||
| 210 | double now = esp_now(); | ||
| 211 | RelCache &c = cache[(unsigned)idx & 255]; | ||
| 212 | if (c.idx == idx && now - c.at < 0.05) | ||
| 213 | return c.val; | ||
| 214 | |||
| 215 | int val = -1; | ||
| 216 | if (esp_is_npc_index(idx)) { | ||
| 217 | int local = engine_get_local_player(); | ||
| 218 | if (local <= 0) { | ||
| 219 | val = 0; | ||
| 220 | } else { | ||
| 221 | void *ent = entitylist_get(idx); | ||
| 222 | const char *model = ent ? entity_model_name(ent) : nullptr; | ||
| 223 | bool neutral = false; | ||
| 224 | bool friendly = npc_is_friendly(idx, model, local, neutral); | ||
| 225 | val = neutral ? 1 : friendly ? 2 : 0; | ||
| 226 | } | ||
| 227 | } | ||
| 228 | c = {idx, now, val}; | ||
| 229 | return val; | ||
| 230 | } | ||
| 231 | |||
| 232 | static bool model_is_friendly(const char *m) { | ||
| 233 | static const char *patterns[] = { | ||
| 234 | "citizen", "humans/", "male_", "female_", "barney", "alyx", | ||
| 235 | "alex", "scientist", "kleiner", "eli", "mossman", "vortigaunt", | ||
| 236 | "dog", "grigori", "monk", "odessa", "gman", nullptr}; | ||
| 237 | if (!m) return false; | ||
| 238 | for (int i = 0; patterns[i]; i++) | ||
| 239 | if (strstr(m, patterns[i])) | ||
| 240 | return true; | ||
| 241 | return false; | ||
| 242 | } | ||
| 243 | |||
| 244 | // harmless wildlife that also happens to be CAI_BaseNPC | ||
| 245 | static bool model_is_neutral(const char *m) { | ||
| 246 | // Lamarr is Kleiner's pet headcrab: scripted and harmless, and the user | ||
| 247 | // wants her shown neutral rather than hostile. | ||
| 248 | static const char *patterns[] = {"crow", "gull", "pigeon", "seagull", | ||
| 249 | "bird", "Lamarr", nullptr}; | ||
| 250 | if (!m) return false; | ||
| 251 | for (int i = 0; patterns[i]; i++) | ||
| 252 | if (strstr(m, patterns[i])) | ||
| 253 | return true; | ||
| 254 | return false; | ||
| 255 | } | ||
| 256 | |||
| 257 | static bool model_is_floor_turret(const char *m) { | ||
| 258 | return m && strstr(m, "floor_turret"); | ||
| 259 | } | ||
| 260 | |||
| 261 | static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx, | ||
| 262 | bool &neutral) { | ||
| 263 | int rel = server_npc_relation(idx, local_idx); | ||
| 264 | int state = server_npc_state(idx); | ||
| 265 | int turret = -1; | ||
| 266 | // Citizen-built and Alyx-hacked turrets never add an m_Relationship row; | ||
| 267 | // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state. | ||
| 268 | if (model_is_floor_turret(model)) | ||
| 269 | turret = server_npc_turret_flags(idx); | ||
| 270 | // Debug-only: relation/state changes are the authoritative signals and the | ||
| 271 | // first thing to check when a friendly NPC still looks hostile. | ||
| 272 | static int rel_cache[2048]; | ||
| 273 | static int state_cache[2048]; | ||
| 274 | static bool cache_init = false; | ||
| 275 | if (!cache_init) { | ||
| 276 | for (int i = 0; i < 2048; i++) | ||
| 277 | rel_cache[i] = state_cache[i] = -999; | ||
| 278 | cache_init = true; | ||
| 279 | } | ||
| 280 | if (idx > 0 && idx < 2048 && | ||
| 281 | (rel_cache[idx] != rel || state_cache[idx] != state)) { | ||
| 282 | rel_cache[idx] = rel; | ||
| 283 | state_cache[idx] = state; | ||
| 284 | logd("npc %d (%s): relation=%d state=%d followtarget=%d turret=%d " | ||
| 285 | "local=%d", idx, model ? model : "?", rel, state, | ||
| 286 | (int)server_npc_has_follow_target(idx), turret, local_idx); | ||
| 287 | } | ||
| 288 | if (rel == 3) { // D_LI | ||
| 289 | neutral = false; | ||
| 290 | return true; | ||
| 291 | } | ||
| 292 | if (turret > 0) { // citizen-built / Alyx-hacked | ||
| 293 | neutral = false; | ||
| 294 | return true; | ||
| 295 | } | ||
| 296 | if (rel == 4) { // D_NU | ||
| 297 | neutral = true; | ||
| 298 | return false; | ||
| 299 | } | ||
| 300 | // NPC_STATE_SCRIPT = 4: cutscene actors and scripted pets are passive even | ||
| 301 | // when their class relationship says hate (Lamarr, scripted Combine, ...). | ||
| 302 | if (state == 4) { | ||
| 303 | neutral = true; | ||
| 304 | return false; | ||
| 305 | } | ||
| 306 | if (rel == 1 || rel == 2) { // D_HT / D_FR | ||
| 307 | neutral = false; | ||
| 308 | return false; | ||
| 309 | } | ||
| 310 | if (turret >= 0) { // ordinary floor turret | ||
| 311 | neutral = false; | ||
| 312 | return false; | ||
| 313 | } | ||
| 314 | // Bugbait-controlled antlions follow a target even when their default | ||
| 315 | // relationship was never rewritten. | ||
| 316 | if (model && strstr(model, "antlion") && | ||
| 317 | server_npc_has_follow_target(idx)) { | ||
| 318 | neutral = false; | ||
| 319 | return true; | ||
| 320 | } | ||
| 321 | bool friendly = model_is_friendly(model); | ||
| 322 | neutral = model_is_neutral(model); | ||
| 323 | return friendly; | ||
| 324 | } | ||
| 325 | |||
| 326 | // Friendly/neutral state changes slowly (hacked turrets, bugbait), while the | ||
| 327 | // entity scan and the chams hook ask for it constantly. Cache it briefly so the | ||
| 328 | // server lookups happen once per NPC per window instead of per call. | ||
| 329 | bool npc_is_friendly(int idx, const char *model, int local_idx, | ||
| 330 | bool &neutral) { | ||
| 331 | struct Cache { | ||
| 332 | int idx; | ||
| 333 | double at; | ||
| 334 | bool friendly; | ||
| 335 | bool neutral; | ||
| 336 | }; | ||
| 337 | static Cache cache[256]; | ||
| 338 | double now = esp_now(); | ||
| 339 | Cache &c = cache[(unsigned)idx & 255]; | ||
| 340 | if (c.idx == idx && now - c.at < 0.10) { | ||
| 341 | neutral = c.neutral; | ||
| 342 | return c.friendly; | ||
| 343 | } | ||
| 344 | bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral); | ||
| 345 | c = {idx, now, f, neutral}; | ||
| 346 | return f; | ||
| 347 | } | ||
| 348 | |||
| 349 | |||
| 350 | static bool model_has(const char *model, const char *needle) { | ||
| 351 | return model && strcasestr(model, needle) != nullptr; | ||
| 352 | } | ||
| 353 | |||
| 354 | // Server m_takedamage first (authoritative immunity), then the HL2 damage | ||
| 355 | // rules for units whose OnTakeDamage filters specific damage types. | ||
| 356 | static int npc_damage_class_uncached(int idx, const char *model) { | ||
| 357 | int td = server_npc_takedamage(idx); | ||
| 358 | if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY | ||
| 359 | return NPC_DMG_IMMUNE; | ||
| 360 | if (model_has(model, "gunship") || model_has(model, "combine_strider") || | ||
| 361 | model_has(model, "dropship") || model_has(model, "helicopter") || | ||
| 362 | model_has(model, "chopper")) | ||
| 363 | return NPC_DMG_ARMORED; | ||
| 364 | if (model_has(model, "headcrab") || model_has(model, "antlion") || | ||
| 365 | model_has(model, "manhack")) | ||
| 366 | return NPC_DMG_VULNERABLE; | ||
| 367 | return NPC_DMG_NORMAL; | ||
| 368 | } | ||
| 369 | |||
| 370 | // Damage class depends on m_takedamage (a server read) plus model string | ||
| 371 | // matching. It changes even more rarely than the relation, so cache it. | ||
| 372 | int npc_damage_class(int idx, const char *model) { | ||
| 373 | struct Cache { | ||
| 374 | int idx; | ||
| 375 | double at; | ||
| 376 | int val; | ||
| 377 | }; | ||
| 378 | static Cache cache[256]; | ||
| 379 | double now = esp_now(); | ||
| 380 | Cache &c = cache[(unsigned)idx & 255]; | ||
| 381 | if (c.idx == idx && now - c.at < 0.25) | ||
| 382 | return c.val; | ||
| 383 | int v = npc_damage_class_uncached(idx, model); | ||
| 384 | c = {idx, now, v}; | ||
| 385 | return v; | ||
| 386 | } | ||
| 387 | |||
| 388 | bool active_weapon_contains(void *local_ent, const char *needle); | ||
| 389 | |||
| 390 | // Armoured units need blast damage; the RPG is the only stock weapon that | ||
| 391 | // passes their OnTakeDamage filters. Immune units can never be damaged. | ||
| 392 | bool npc_can_be_damaged(int idx, const char *model) { | ||
| 393 | int dc = npc_damage_class(idx, model); | ||
| 394 | if (dc == NPC_DMG_IMMUNE) | ||
| 395 | return false; | ||
| 396 | if (dc != NPC_DMG_ARMORED) | ||
| 397 | return true; | ||
| 398 | int local = engine_get_local_player(); | ||
| 399 | void *le = local > 0 ? entitylist_get(local) : nullptr; | ||
| 400 | return active_weapon_contains(le, "rocket_launcher") || | ||
| 401 | active_weapon_contains(le, "rpg"); | ||
| 402 | } | ||
| 403 | |||
| 404 | void short_name(const char *model, char *out, size_t outn) { | ||
| 405 | if (!model) { snprintf(out, outn, "NPC"); return; } | ||
| 406 | const char *base = strrchr(model, '/'); | ||
| 407 | base = base ? base + 1 : model; | ||
| 408 | size_t n = strcspn(base, "."); | ||
| 409 | if (n >= outn) n = outn - 1; | ||
| 410 | memcpy(out, base, n); | ||
| 411 | out[n] = 0; | ||
| 412 | } | ||
| 413 | |||
| 414 | // Enemy weapon label. NPC weapons are server entities, so read the server | ||
| 415 | // model index first and fall back to the client class name. | ||
| 416 | void weapon_short_name(int idx, char *out, size_t outn) { | ||
| 417 | out[0] = 0; | ||
| 418 | int wi = server_active_weapon(idx); | ||
| 419 | if (wi <= 0) | ||
| 420 | return; | ||
| 421 | const char *name = nullptr; | ||
| 422 | void *base = server_entity_base(wi); | ||
| 423 | int mi = base ? server_model_index(base) : -1; | ||
| 424 | if (mi > 0) | ||
| 425 | name = model_name_from_index(mi); | ||
| 426 | if (!name) { | ||
| 427 | void *wep = entitylist_get(wi); | ||
| 428 | if (wep) { | ||
| 429 | void *net = entity_networkable(wi); | ||
| 430 | name = entity_class_name(net, nullptr); | ||
| 431 | } | ||
| 432 | } | ||
| 433 | if (!name) | ||
| 434 | name = server_entity_classname(wi); | ||
| 435 | if (!name) | ||
| 436 | return; | ||
| 437 | static const struct { | ||
| 438 | const char *needle; | ||
| 439 | const char *label; | ||
| 440 | } map[] = { | ||
| 441 | {"smg1", "SMG1"}, {"irifle", "AR2"}, {"ar2", "AR2"}, | ||
| 442 | {"shotgun", "Shotgun"}, {"pistol", "Pistol"}, {"357", "357"}, | ||
| 443 | {"crossbow", "Crossbow"}, {"rocket", "RPG"}, {"rpg", "RPG"}, | ||
| 444 | {"grenade", "Grenade"}, {"frag", "Grenade"}, {"crowbar", "Crowbar"}, | ||
| 445 | {"physcannon", "Gravity"}, {"slam", "SLAM"}, {"bugbait", "Bugbait"}, | ||
| 446 | {"manhack", "Manhack"}, {"turret", "Turret"}, {"annabelle", "Rifle"}, | ||
| 447 | {"alyxgun", "Alyx gun"}, {"sniper", "Sniper"}, {"smg", "SMG"}, | ||
| 448 | }; | ||
| 449 | for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { | ||
| 450 | if (strcasestr(name, map[i].needle)) { | ||
| 451 | snprintf(out, outn, "%s", map[i].label); | ||
| 452 | return; | ||
| 453 | } | ||
| 454 | } | ||
| 455 | // Last resort: the short class/model name. | ||
| 456 | short_name(name, out, outn); | ||
| 457 | } | ||
| 458 | |||
| 459 | // Hostile / neutral / friendly colours come from the ESP colour pickers. | ||
| 460 | |||
diff --git a/src/esp.cpp b/src/esp.cpp index 3f21dce..9046d22 100644 --- a/src/esp.cpp +++ b/src/esp.cpp | |||
| @@ -8,7 +8,7 @@ | |||
| 8 | #include <cstdlib> | 8 | #include <cstdlib> |
| 9 | #include <ctime> | 9 | #include <ctime> |
| 10 | 10 | ||
| 11 | enum { LIFE_ALIVE = 0 }; | 11 | |
| 12 | 12 | ||
| 13 | struct EspTarget { | 13 | struct EspTarget { |
| 14 | int idx; | 14 | int idx; |
| @@ -35,35 +35,6 @@ static int g_ntargets = 0; | |||
| 35 | int g_scr_w = 0, g_scr_h = 0; | 35 | int g_scr_w = 0, g_scr_h = 0; |
| 36 | static bool g_esp_logged = false; | 36 | static bool g_esp_logged = false; |
| 37 | 37 | ||
| 38 | // Listen-server sanity latch. Only ever set once per map (esp_reset_session), | ||
| 39 | // and only after both model indices are valid: latching on the transient | ||
| 40 | // cli_model == 0 at map load disabled server health for the whole process. | ||
| 41 | static bool g_srv_checked = false; | ||
| 42 | static bool g_srv_trusted = false; | ||
| 43 | |||
| 44 | // Per-map caches. The ESP resolves a model name for every entity on every scan | ||
| 45 | // and the chams hook asks for a relation on every model draw; both are virtual | ||
| 46 | // call chains into the engine plus server datamap reads, so memoise them. | ||
| 47 | static constexpr int MODEL_CACHE = 16384; | ||
| 48 | static const char *g_model_name_cache[MODEL_CACHE]; | ||
| 49 | static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail | ||
| 50 | |||
| 51 | // Direct-mapped entity pointer -> index map, filled by esp_collect. The | ||
| 52 | // triggerbot traces a ray and gets a pointer back, and used to find its index | ||
| 53 | // by walking up to 4096 entity-list slots on every fire tick. | ||
| 54 | static constexpr int ENTPTR_CACHE = 4096; | ||
| 55 | static void *g_entptr_key[ENTPTR_CACHE]; | ||
| 56 | static int g_entptr_idx[ENTPTR_CACHE]; | ||
| 57 | |||
| 58 | void esp_reset_session() { | ||
| 59 | g_srv_checked = false; | ||
| 60 | g_srv_trusted = false; | ||
| 61 | g_esp_logged = false; | ||
| 62 | memset(g_model_name_cache, 0, sizeof(g_model_name_cache)); | ||
| 63 | memset(g_model_state_cache, 0, sizeof(g_model_state_cache)); | ||
| 64 | memset(g_entptr_key, 0, sizeof(g_entptr_key)); | ||
| 65 | memset(g_entptr_idx, 0, sizeof(g_entptr_idx)); | ||
| 66 | } | ||
| 67 | 38 | ||
| 68 | // Speed graph: 100 samples at 0.1 s = the last 10 seconds. | 39 | // Speed graph: 100 samples at 0.1 s = the last 10 seconds. |
| 69 | static constexpr int SPEED_SAMPLES = 100; | 40 | static constexpr int SPEED_SAMPLES = 100; |
| @@ -173,431 +144,6 @@ using GetModelNameFn = const char *(*)(void *, const void *); | |||
| 173 | 144 | ||
| 174 | // entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the | 145 | // entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the |
| 175 | // IClientNetworkable subobject offset inside the entity (see projectz). | 146 | // IClientNetworkable subobject offset inside the entity (see projectz). |
| 176 | void *entity_networkable(int idx) { | ||
| 177 | return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx); | ||
| 178 | } | ||
| 179 | |||
| 180 | // IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8. | ||
| 181 | const char *entity_class_name(void *net, void **out_cc) { | ||
| 182 | if (!net) return nullptr; | ||
| 183 | void *cc = vfunc<GetCCFn>(net, 2)(net); | ||
| 184 | if (out_cc) *out_cc = cc; | ||
| 185 | return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; | ||
| 186 | } | ||
| 187 | |||
| 188 | // IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant | ||
| 189 | // 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK, | ||
| 190 | // so the slot is validated once against the local player, which can never be | ||
| 191 | // dormant: a wrong slot disables the check instead of misreporting entities. | ||
| 192 | // Dormant entities keep their last networked origin, so drawing or aiming at | ||
| 193 | // them produces ghosts at stale positions. | ||
| 194 | static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled | ||
| 195 | |||
| 196 | static void dormant_selftest() { | ||
| 197 | // Leave the state untested (0) until a local player exists: the first call | ||
| 198 | // can come from the main menu, and latching "disabled" there would turn the | ||
| 199 | // check off for the whole session. | ||
| 200 | if (!g_iface.entitylist) | ||
| 201 | return; | ||
| 202 | int local = engine_get_local_player(); | ||
| 203 | if (local <= 0) | ||
| 204 | return; | ||
| 205 | void *net = entity_networkable(local); | ||
| 206 | if (!net) | ||
| 207 | return; | ||
| 208 | void **vt = *(void ***)net; | ||
| 209 | if (!vt || !mod_client().contains((uintptr_t)vt[8])) { | ||
| 210 | g_dormant_state = -1; | ||
| 211 | logf("dormant check disabled: slot 8 outside client module"); | ||
| 212 | return; | ||
| 213 | } | ||
| 214 | using DormantFn = unsigned char (*)(void *); | ||
| 215 | if (vfunc<DormantFn>(net, 8)(net) != 0) { | ||
| 216 | g_dormant_state = -1; | ||
| 217 | logf("dormant check disabled: local player reported dormant"); | ||
| 218 | return; | ||
| 219 | } | ||
| 220 | g_dormant_state = 1; | ||
| 221 | logd("dormant check enabled"); | ||
| 222 | } | ||
| 223 | |||
| 224 | bool entity_is_dormant(void *net) { | ||
| 225 | if (!net) | ||
| 226 | return false; | ||
| 227 | if (g_dormant_state == 0) | ||
| 228 | dormant_selftest(); | ||
| 229 | if (g_dormant_state != 1) | ||
| 230 | return false; | ||
| 231 | using DormantFn = unsigned char (*)(void *); | ||
| 232 | return vfunc<DormantFn>(net, 8)(net) != 0; | ||
| 233 | } | ||
| 234 | |||
| 235 | // Entity pointer -> index, filled by the entity scans. The triggerbot gets a | ||
| 236 | // pointer back from the trace and needs the index; the alternative is walking | ||
| 237 | // up to 4096 entity-list slots on every fire tick. | ||
| 238 | static int entptr_lookup(void *ent) { | ||
| 239 | if (!ent) | ||
| 240 | return -1; | ||
| 241 | unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); | ||
| 242 | return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1; | ||
| 243 | } | ||
| 244 | |||
| 245 | static void entptr_store(void *ent, int idx) { | ||
| 246 | if (!ent || idx <= 0) | ||
| 247 | return; | ||
| 248 | unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); | ||
| 249 | g_entptr_key[h] = ent; | ||
| 250 | g_entptr_idx[h] = idx; | ||
| 251 | } | ||
| 252 | |||
| 253 | // m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. | ||
| 254 | // Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. | ||
| 255 | const char *model_name_from_index(int modelindex) { | ||
| 256 | if (!g_iface.modelinfo || modelindex <= 0) | ||
| 257 | return nullptr; | ||
| 258 | if (modelindex < MODEL_CACHE) { | ||
| 259 | if (g_model_state_cache[modelindex] == 1) | ||
| 260 | return g_model_name_cache[modelindex]; | ||
| 261 | if (g_model_state_cache[modelindex] == 2) | ||
| 262 | return nullptr; | ||
| 263 | } | ||
| 264 | const void *model = | ||
| 265 | vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); | ||
| 266 | if (!model) { | ||
| 267 | if (modelindex < MODEL_CACHE) | ||
| 268 | g_model_state_cache[modelindex] = 2; | ||
| 269 | return nullptr; | ||
| 270 | } | ||
| 271 | const char *name = | ||
| 272 | vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model); | ||
| 273 | if (modelindex < MODEL_CACHE) { | ||
| 274 | g_model_name_cache[modelindex] = name; | ||
| 275 | g_model_state_cache[modelindex] = name ? 1 : 2; | ||
| 276 | } | ||
| 277 | return name; | ||
| 278 | } | ||
| 279 | |||
| 280 | const char *entity_model_name(void *ent) { | ||
| 281 | if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr; | ||
| 282 | int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); | ||
| 283 | return model_name_from_index(modelindex); | ||
| 284 | } | ||
| 285 | |||
| 286 | // Live origin for a cached entity index. Item/projectile discovery is throttled | ||
| 287 | // to 10 Hz (it walks the whole entity list), but the markers must follow the | ||
| 288 | // world at render rate; refreshing one netvar read per cached target is cheap. | ||
| 289 | bool entity_origin(void *ent, Vector &out) { | ||
| 290 | if (!ent || g_net.origin < 0) | ||
| 291 | return false; | ||
| 292 | uintptr_t e = (uintptr_t)ent; | ||
| 293 | out.x = *(float *)(e + g_net.origin); | ||
| 294 | out.y = *(float *)(e + g_net.origin + 4); | ||
| 295 | out.z = *(float *)(e + g_net.origin + 8); | ||
| 296 | return true; | ||
| 297 | } | ||
| 298 | |||
| 299 | // HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all | ||
| 300 | // use the single client class "CAI_BaseNPC" — only special NPCs register their | ||
| 301 | // own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). | ||
| 302 | static bool is_npc_class(const char *name) { | ||
| 303 | if (!name) return false; | ||
| 304 | return strstr(name, "NPC") != nullptr || | ||
| 305 | strstr(name, "BaseHelicopter") != nullptr; | ||
| 306 | } | ||
| 307 | |||
| 308 | // Used by the chams hook to decide whether a model-render entity is an NPC. | ||
| 309 | bool esp_is_npc_index(int idx) { | ||
| 310 | if (!g_iface.entitylist || idx <= 0 || idx > 8192) | ||
| 311 | return false; | ||
| 312 | void *net = entity_networkable(idx); | ||
| 313 | if (!net) | ||
| 314 | return false; | ||
| 315 | return is_npc_class(entity_class_name(net, nullptr)); | ||
| 316 | } | ||
| 317 | |||
| 318 | bool esp_is_viewmodel_index(int idx) { | ||
| 319 | if (!g_iface.entitylist || idx <= 0 || idx > 8192) | ||
| 320 | return false; | ||
| 321 | void *ent = entitylist_get(idx); | ||
| 322 | if (!ent) | ||
| 323 | return false; | ||
| 324 | void *net = entity_networkable(idx); | ||
| 325 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 326 | if (cls && strcasestr(cls, "viewmodel")) | ||
| 327 | return true; | ||
| 328 | const char *model = entity_model_name(ent); | ||
| 329 | return model && (strstr(model, "/v_") || strstr(model, "\\v_")); | ||
| 330 | } | ||
| 331 | |||
| 332 | // 0 = hostile, 1 = neutral, 2 = friendly, -1 = not an NPC. | ||
| 333 | static bool npc_is_friendly(int idx, const char *model, int local_idx, | ||
| 334 | bool &neutral); | ||
| 335 | int esp_npc_relation(int idx) { | ||
| 336 | // Called from the chams hook up to three times per entity per frame (visible, | ||
| 337 | // through-wall, wireframe pass) and the work behind it is a chain of server | ||
| 338 | // edict/datamap reads. A short TTL cache keeps that out of the render path. | ||
| 339 | struct RelCache { | ||
| 340 | int idx; | ||
| 341 | double at; | ||
| 342 | int val; | ||
| 343 | }; | ||
| 344 | static RelCache cache[256]; | ||
| 345 | double now = esp_now(); | ||
| 346 | RelCache &c = cache[(unsigned)idx & 255]; | ||
| 347 | if (c.idx == idx && now - c.at < 0.05) | ||
| 348 | return c.val; | ||
| 349 | |||
| 350 | int val = -1; | ||
| 351 | if (esp_is_npc_index(idx)) { | ||
| 352 | int local = engine_get_local_player(); | ||
| 353 | if (local <= 0) { | ||
| 354 | val = 0; | ||
| 355 | } else { | ||
| 356 | void *ent = entitylist_get(idx); | ||
| 357 | const char *model = ent ? entity_model_name(ent) : nullptr; | ||
| 358 | bool neutral = false; | ||
| 359 | bool friendly = npc_is_friendly(idx, model, local, neutral); | ||
| 360 | val = neutral ? 1 : friendly ? 2 : 0; | ||
| 361 | } | ||
| 362 | } | ||
| 363 | c = {idx, now, val}; | ||
| 364 | return val; | ||
| 365 | } | ||
| 366 | |||
| 367 | static bool model_is_friendly(const char *m) { | ||
| 368 | static const char *patterns[] = { | ||
| 369 | "citizen", "humans/", "male_", "female_", "barney", "alyx", | ||
| 370 | "alex", "scientist", "kleiner", "eli", "mossman", "vortigaunt", | ||
| 371 | "dog", "grigori", "monk", "odessa", "gman", nullptr}; | ||
| 372 | if (!m) return false; | ||
| 373 | for (int i = 0; patterns[i]; i++) | ||
| 374 | if (strstr(m, patterns[i])) | ||
| 375 | return true; | ||
| 376 | return false; | ||
| 377 | } | ||
| 378 | |||
| 379 | // harmless wildlife that also happens to be CAI_BaseNPC | ||
| 380 | static bool model_is_neutral(const char *m) { | ||
| 381 | // Lamarr is Kleiner's pet headcrab: scripted and harmless, and the user | ||
| 382 | // wants her shown neutral rather than hostile. | ||
| 383 | static const char *patterns[] = {"crow", "gull", "pigeon", "seagull", | ||
| 384 | "bird", "Lamarr", nullptr}; | ||
| 385 | if (!m) return false; | ||
| 386 | for (int i = 0; patterns[i]; i++) | ||
| 387 | if (strstr(m, patterns[i])) | ||
| 388 | return true; | ||
| 389 | return false; | ||
| 390 | } | ||
| 391 | |||
| 392 | static bool model_is_floor_turret(const char *m) { | ||
| 393 | return m && strstr(m, "floor_turret"); | ||
| 394 | } | ||
| 395 | |||
| 396 | static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx, | ||
| 397 | bool &neutral) { | ||
| 398 | int rel = server_npc_relation(idx, local_idx); | ||
| 399 | int state = server_npc_state(idx); | ||
| 400 | int turret = -1; | ||
| 401 | // Citizen-built and Alyx-hacked turrets never add an m_Relationship row; | ||
| 402 | // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state. | ||
| 403 | if (model_is_floor_turret(model)) | ||
| 404 | turret = server_npc_turret_flags(idx); | ||
| 405 | // Debug-only: relation/state changes are the authoritative signals and the | ||
| 406 | // first thing to check when a friendly NPC still looks hostile. | ||
| 407 | static int rel_cache[2048]; | ||
| 408 | static int state_cache[2048]; | ||
| 409 | static bool cache_init = false; | ||
| 410 | if (!cache_init) { | ||
| 411 | for (int i = 0; i < 2048; i++) | ||
| 412 | rel_cache[i] = state_cache[i] = -999; | ||
| 413 | cache_init = true; | ||
| 414 | } | ||
| 415 | if (idx > 0 && idx < 2048 && | ||
| 416 | (rel_cache[idx] != rel || state_cache[idx] != state)) { | ||
| 417 | rel_cache[idx] = rel; | ||
| 418 | state_cache[idx] = state; | ||
| 419 | logd("npc %d (%s): relation=%d state=%d followtarget=%d turret=%d " | ||
| 420 | "local=%d", idx, model ? model : "?", rel, state, | ||
| 421 | (int)server_npc_has_follow_target(idx), turret, local_idx); | ||
| 422 | } | ||
| 423 | if (rel == 3) { // D_LI | ||
| 424 | neutral = false; | ||
| 425 | return true; | ||
| 426 | } | ||
| 427 | if (turret > 0) { // citizen-built / Alyx-hacked | ||
| 428 | neutral = false; | ||
| 429 | return true; | ||
| 430 | } | ||
| 431 | if (rel == 4) { // D_NU | ||
| 432 | neutral = true; | ||
| 433 | return false; | ||
| 434 | } | ||
| 435 | // NPC_STATE_SCRIPT = 4: cutscene actors and scripted pets are passive even | ||
| 436 | // when their class relationship says hate (Lamarr, scripted Combine, ...). | ||
| 437 | if (state == 4) { | ||
| 438 | neutral = true; | ||
| 439 | return false; | ||
| 440 | } | ||
| 441 | if (rel == 1 || rel == 2) { // D_HT / D_FR | ||
| 442 | neutral = false; | ||
| 443 | return false; | ||
| 444 | } | ||
| 445 | if (turret >= 0) { // ordinary floor turret | ||
| 446 | neutral = false; | ||
| 447 | return false; | ||
| 448 | } | ||
| 449 | // Bugbait-controlled antlions follow a target even when their default | ||
| 450 | // relationship was never rewritten. | ||
| 451 | if (model && strstr(model, "antlion") && | ||
| 452 | server_npc_has_follow_target(idx)) { | ||
| 453 | neutral = false; | ||
| 454 | return true; | ||
| 455 | } | ||
| 456 | bool friendly = model_is_friendly(model); | ||
| 457 | neutral = model_is_neutral(model); | ||
| 458 | return friendly; | ||
| 459 | } | ||
| 460 | |||
| 461 | // Friendly/neutral state changes slowly (hacked turrets, bugbait), while the | ||
| 462 | // entity scan and the chams hook ask for it constantly. Cache it briefly so the | ||
| 463 | // server lookups happen once per NPC per window instead of per call. | ||
| 464 | static bool npc_is_friendly(int idx, const char *model, int local_idx, | ||
| 465 | bool &neutral) { | ||
| 466 | struct Cache { | ||
| 467 | int idx; | ||
| 468 | double at; | ||
| 469 | bool friendly; | ||
| 470 | bool neutral; | ||
| 471 | }; | ||
| 472 | static Cache cache[256]; | ||
| 473 | double now = esp_now(); | ||
| 474 | Cache &c = cache[(unsigned)idx & 255]; | ||
| 475 | if (c.idx == idx && now - c.at < 0.10) { | ||
| 476 | neutral = c.neutral; | ||
| 477 | return c.friendly; | ||
| 478 | } | ||
| 479 | bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral); | ||
| 480 | c = {idx, now, f, neutral}; | ||
| 481 | return f; | ||
| 482 | } | ||
| 483 | |||
| 484 | enum NpcDamageClass { | ||
| 485 | NPC_DMG_NORMAL = 0, | ||
| 486 | NPC_DMG_ARMORED, // only blast damage (RPG) hurts it | ||
| 487 | NPC_DMG_VULNERABLE, // gravity gun / physics damage works well | ||
| 488 | NPC_DMG_IMMUNE // m_takedamage says no damage at all | ||
| 489 | }; | ||
| 490 | |||
| 491 | static bool model_has(const char *model, const char *needle) { | ||
| 492 | return model && strcasestr(model, needle) != nullptr; | ||
| 493 | } | ||
| 494 | |||
| 495 | // Server m_takedamage first (authoritative immunity), then the HL2 damage | ||
| 496 | // rules for units whose OnTakeDamage filters specific damage types. | ||
| 497 | static int npc_damage_class_uncached(int idx, const char *model) { | ||
| 498 | int td = server_npc_takedamage(idx); | ||
| 499 | if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY | ||
| 500 | return NPC_DMG_IMMUNE; | ||
| 501 | if (model_has(model, "gunship") || model_has(model, "combine_strider") || | ||
| 502 | model_has(model, "dropship") || model_has(model, "helicopter") || | ||
| 503 | model_has(model, "chopper")) | ||
| 504 | return NPC_DMG_ARMORED; | ||
| 505 | if (model_has(model, "headcrab") || model_has(model, "antlion") || | ||
| 506 | model_has(model, "manhack")) | ||
| 507 | return NPC_DMG_VULNERABLE; | ||
| 508 | return NPC_DMG_NORMAL; | ||
| 509 | } | ||
| 510 | |||
| 511 | // Damage class depends on m_takedamage (a server read) plus model string | ||
| 512 | // matching. It changes even more rarely than the relation, so cache it. | ||
| 513 | static int npc_damage_class(int idx, const char *model) { | ||
| 514 | struct Cache { | ||
| 515 | int idx; | ||
| 516 | double at; | ||
| 517 | int val; | ||
| 518 | }; | ||
| 519 | static Cache cache[256]; | ||
| 520 | double now = esp_now(); | ||
| 521 | Cache &c = cache[(unsigned)idx & 255]; | ||
| 522 | if (c.idx == idx && now - c.at < 0.25) | ||
| 523 | return c.val; | ||
| 524 | int v = npc_damage_class_uncached(idx, model); | ||
| 525 | c = {idx, now, v}; | ||
| 526 | return v; | ||
| 527 | } | ||
| 528 | |||
| 529 | bool active_weapon_contains(void *local_ent, const char *needle); | ||
| 530 | |||
| 531 | // Armoured units need blast damage; the RPG is the only stock weapon that | ||
| 532 | // passes their OnTakeDamage filters. Immune units can never be damaged. | ||
| 533 | static bool npc_can_be_damaged(int idx, const char *model) { | ||
| 534 | int dc = npc_damage_class(idx, model); | ||
| 535 | if (dc == NPC_DMG_IMMUNE) | ||
| 536 | return false; | ||
| 537 | if (dc != NPC_DMG_ARMORED) | ||
| 538 | return true; | ||
| 539 | int local = engine_get_local_player(); | ||
| 540 | void *le = local > 0 ? entitylist_get(local) : nullptr; | ||
| 541 | return active_weapon_contains(le, "rocket_launcher") || | ||
| 542 | active_weapon_contains(le, "rpg"); | ||
| 543 | } | ||
| 544 | |||
| 545 | static void short_name(const char *model, char *out, size_t outn) { | ||
| 546 | if (!model) { snprintf(out, outn, "NPC"); return; } | ||
| 547 | const char *base = strrchr(model, '/'); | ||
| 548 | base = base ? base + 1 : model; | ||
| 549 | size_t n = strcspn(base, "."); | ||
| 550 | if (n >= outn) n = outn - 1; | ||
| 551 | memcpy(out, base, n); | ||
| 552 | out[n] = 0; | ||
| 553 | } | ||
| 554 | |||
| 555 | // Enemy weapon label. NPC weapons are server entities, so read the server | ||
| 556 | // model index first and fall back to the client class name. | ||
| 557 | static void weapon_short_name(int idx, char *out, size_t outn) { | ||
| 558 | out[0] = 0; | ||
| 559 | int wi = server_active_weapon(idx); | ||
| 560 | if (wi <= 0) | ||
| 561 | return; | ||
| 562 | const char *name = nullptr; | ||
| 563 | void *base = server_entity_base(wi); | ||
| 564 | int mi = base ? server_model_index(base) : -1; | ||
| 565 | if (mi > 0) | ||
| 566 | name = model_name_from_index(mi); | ||
| 567 | if (!name) { | ||
| 568 | void *wep = entitylist_get(wi); | ||
| 569 | if (wep) { | ||
| 570 | void *net = entity_networkable(wi); | ||
| 571 | name = entity_class_name(net, nullptr); | ||
| 572 | } | ||
| 573 | } | ||
| 574 | if (!name) | ||
| 575 | name = server_entity_classname(wi); | ||
| 576 | if (!name) | ||
| 577 | return; | ||
| 578 | static const struct { | ||
| 579 | const char *needle; | ||
| 580 | const char *label; | ||
| 581 | } map[] = { | ||
| 582 | {"smg1", "SMG1"}, {"irifle", "AR2"}, {"ar2", "AR2"}, | ||
| 583 | {"shotgun", "Shotgun"}, {"pistol", "Pistol"}, {"357", "357"}, | ||
| 584 | {"crossbow", "Crossbow"}, {"rocket", "RPG"}, {"rpg", "RPG"}, | ||
| 585 | {"grenade", "Grenade"}, {"frag", "Grenade"}, {"crowbar", "Crowbar"}, | ||
| 586 | {"physcannon", "Gravity"}, {"slam", "SLAM"}, {"bugbait", "Bugbait"}, | ||
| 587 | {"manhack", "Manhack"}, {"turret", "Turret"}, {"annabelle", "Rifle"}, | ||
| 588 | {"alyxgun", "Alyx gun"}, {"sniper", "Sniper"}, {"smg", "SMG"}, | ||
| 589 | }; | ||
| 590 | for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { | ||
| 591 | if (strcasestr(name, map[i].needle)) { | ||
| 592 | snprintf(out, outn, "%s", map[i].label); | ||
| 593 | return; | ||
| 594 | } | ||
| 595 | } | ||
| 596 | // Last resort: the short class/model name. | ||
| 597 | short_name(name, out, outn); | ||
| 598 | } | ||
| 599 | |||
| 600 | // Hostile / neutral / friendly colours come from the ESP colour pickers. | ||
| 601 | static void relation_rgb(const EspTarget &t, int &r, int &g, int &b) { | 147 | static void relation_rgb(const EspTarget &t, int &r, int &g, int &b) { |
| 602 | const int *c = t.friendly ? g_esp_col_friendly | 148 | const int *c = t.friendly ? g_esp_col_friendly |
| 603 | : t.neutral ? g_esp_col_neutral | 149 | : t.neutral ? g_esp_col_neutral |
| @@ -971,7 +517,7 @@ static void draw_skeleton(const EspTarget &t) { | |||
| 971 | // MAX_STEP degrees toward the target and stops inside DEADZONE. | 517 | // MAX_STEP degrees toward the target and stops inside DEADZONE. |
| 972 | // --------------------------------------------------------------------------- | 518 | // --------------------------------------------------------------------------- |
| 973 | 519 | ||
| 974 | static QAngle angle_to(const Vector &from, const Vector &to) { | 520 | QAngle angle_to(const Vector &from, const Vector &to) { |
| 975 | float dx = to.x - from.x, dy = to.y - from.y, dz = to.z - from.z; | 521 | float dx = to.x - from.x, dy = to.y - from.y, dz = to.z - from.z; |
| 976 | float horiz = sqrtf(dx * dx + dy * dy); | 522 | float horiz = sqrtf(dx * dx + dy * dy); |
| 977 | if (horiz < 0.001f) | 523 | if (horiz < 0.001f) |
| @@ -985,7 +531,7 @@ static QAngle angle_to(const Vector &from, const Vector &to) { | |||
| 985 | 531 | ||
| 986 | // Server bullet direction adds m_Local.m_vecPunchAngle to the usercmd view | 532 | // Server bullet direction adds m_Local.m_vecPunchAngle to the usercmd view |
| 987 | // (GetAutoaimVector), so aim angles have to be pre-subtracted by it. | 533 | // (GetAutoaimVector), so aim angles have to be pre-subtracted by it. |
| 988 | static bool punch_angles(float &px, float &py) { | 534 | bool punch_angles(float &px, float &py) { |
| 989 | px = py = 0.0f; | 535 | px = py = 0.0f; |
| 990 | if (!aim_punch_enabled() || g_net.punch < 0) | 536 | if (!aim_punch_enabled() || g_net.punch < 0) |
| 991 | return false; | 537 | return false; |
| @@ -1001,1065 +547,6 @@ static bool punch_angles(float &px, float &py) { | |||
| 1001 | return std::isfinite(px) && std::isfinite(py); | 547 | return std::isfinite(px) && std::isfinite(py); |
| 1002 | } | 548 | } |
| 1003 | 549 | ||
| 1004 | static int g_aim_lock = -1; | ||
| 1005 | static int g_silent_lock = -1; | ||
| 1006 | |||
| 1007 | void aimbot_reset_lock() { g_aim_lock = -1; } | ||
| 1008 | void silentaim_reset_lock() { g_silent_lock = -1; } | ||
| 1009 | |||
| 1010 | // max_fov is the angular radius around the crosshair. If lock_idx is non-null | ||
| 1011 | // and valid, that target is kept while it is alive and visible (the "do not | ||
| 1012 | // switch while the bind is held" rule); a fresh acquisition is ranked by angle | ||
| 1013 | // and the nearest visible selected hitbox point. | ||
| 1014 | static bool select_aim_target(const QAngle &view, unsigned bone_mask, | ||
| 1015 | bool vischeck, float max_fov, const char *tag, | ||
| 1016 | int *lock_idx, int &out_idx, Vector &out_point, | ||
| 1017 | float &out_fov) { | ||
| 1018 | out_idx = -1; | ||
| 1019 | out_point = {}; | ||
| 1020 | out_fov = 0.0f; | ||
| 1021 | // maxs is optional: the per-target code falls back to 72 units when the | ||
| 1022 | // netvar is missing, so it must not disable the whole aimbot here. | ||
| 1023 | if (!g_iface.entitylist || g_net.origin < 0) | ||
| 1024 | return false; | ||
| 1025 | int local = engine_get_local_player(); | ||
| 1026 | if (local <= 0) | ||
| 1027 | return false; | ||
| 1028 | void *local_ent = entitylist_get(local); | ||
| 1029 | if (!local_ent) | ||
| 1030 | return false; | ||
| 1031 | if (g_net.health >= 0 && | ||
| 1032 | *(int *)((uintptr_t)local_ent + g_net.health) <= 0) | ||
| 1033 | return false; | ||
| 1034 | |||
| 1035 | // With the RPG equipped, armoured units (striders, gunships, dropships, | ||
| 1036 | // hunter-choppers) are the ones ordinary shots cannot hurt, so prefer them | ||
| 1037 | // over normal NPCs when acquiring. | ||
| 1038 | bool rpg = active_weapon_contains(local_ent, "rocket_launcher") || | ||
| 1039 | active_weapon_contains(local_ent, "rpg"); | ||
| 1040 | |||
| 1041 | // fov_limit <= 0 means "no angular limit" (kept for unlimited acquisition). | ||
| 1042 | auto try_target = [&](int idx, float fov_limit, Vector &pt, | ||
| 1043 | float &fov) -> bool { | ||
| 1044 | if (idx <= 0 || idx == local) | ||
| 1045 | return false; | ||
| 1046 | void *ent = entitylist_get(idx); | ||
| 1047 | if (!ent) | ||
| 1048 | return false; | ||
| 1049 | void *net = entity_networkable(idx); | ||
| 1050 | if (!net || entity_is_dormant(net)) | ||
| 1051 | return false; | ||
| 1052 | const char *cls = entity_class_name(net, nullptr); | ||
| 1053 | if (!cls || !is_npc_class(cls)) | ||
| 1054 | return false; | ||
| 1055 | uintptr_t e = (uintptr_t)ent; | ||
| 1056 | int life = g_net.lifestate >= 0 | ||
| 1057 | ? (int)*(unsigned char *)(e + g_net.lifestate) | ||
| 1058 | : -1; | ||
| 1059 | if (life != LIFE_ALIVE) | ||
| 1060 | return false; | ||
| 1061 | const char *model = entity_model_name(ent); | ||
| 1062 | bool neutral = false; | ||
| 1063 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 1064 | return false; | ||
| 1065 | if (!npc_can_be_damaged(idx, model)) | ||
| 1066 | return false; | ||
| 1067 | if (!bones_update(idx, ent)) | ||
| 1068 | return false; | ||
| 1069 | Vector pts[32]; | ||
| 1070 | int npoints = aim_points(idx, bone_mask, pts, 32); | ||
| 1071 | float best = 1e9f; | ||
| 1072 | Vector best_pt{}; | ||
| 1073 | for (int i = 0; i < npoints; i++) { | ||
| 1074 | float f = getfov(view, angle_to(g_cam_eye, pts[i])); | ||
| 1075 | if (fov_limit > 0.0f && f > fov_limit) | ||
| 1076 | continue; | ||
| 1077 | if (f >= best) | ||
| 1078 | continue; | ||
| 1079 | if (vischeck && !is_visible(g_cam_eye, pts[i], local_ent, ent)) | ||
| 1080 | continue; | ||
| 1081 | best = f; | ||
| 1082 | best_pt = pts[i]; | ||
| 1083 | } | ||
| 1084 | if (best > 1e8f) | ||
| 1085 | return false; | ||
| 1086 | pt = best_pt; | ||
| 1087 | fov = best; | ||
| 1088 | return true; | ||
| 1089 | }; | ||
| 1090 | |||
| 1091 | // Keep the locked target while it is alive and has a visible selected point. | ||
| 1092 | // The lock uses a wider cone than acquisition (hysteresis), so a target that | ||
| 1093 | // drifts slightly off the crosshair is not dropped, but one that swings | ||
| 1094 | // behind the player is: an unbounded lock let silent aim fire backwards. | ||
| 1095 | if (lock_idx && *lock_idx >= 0) { | ||
| 1096 | Vector pt; | ||
| 1097 | float f; | ||
| 1098 | if (try_target(*lock_idx, max_fov * 1.5f, pt, f)) { | ||
| 1099 | out_idx = *lock_idx; | ||
| 1100 | out_point = pt; | ||
| 1101 | out_fov = f; | ||
| 1102 | static int lock_logs = 0; | ||
| 1103 | if (lock_logs < 5) { | ||
| 1104 | lock_logs++; | ||
| 1105 | logd("%s: lock idx=%d fov=%.2f", tag, out_idx, f); | ||
| 1106 | } | ||
| 1107 | return true; | ||
| 1108 | } | ||
| 1109 | *lock_idx = -1; | ||
| 1110 | } | ||
| 1111 | |||
| 1112 | using Fn = int (*)(void *); | ||
| 1113 | int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); | ||
| 1114 | if (hi < 0 || hi > 4096) | ||
| 1115 | hi = 4096; | ||
| 1116 | |||
| 1117 | struct Candidate { | ||
| 1118 | int idx; | ||
| 1119 | float fov; | ||
| 1120 | Vector approx; | ||
| 1121 | bool armored; | ||
| 1122 | float key; | ||
| 1123 | }; | ||
| 1124 | Candidate cands[16]; | ||
| 1125 | int ncands = 0; | ||
| 1126 | |||
| 1127 | for (int idx = 1; idx <= hi; idx++) { | ||
| 1128 | if (idx == local) | ||
| 1129 | continue; | ||
| 1130 | void *ent = entitylist_get(idx); | ||
| 1131 | if (!ent) | ||
| 1132 | continue; | ||
| 1133 | void *net = entity_networkable(idx); | ||
| 1134 | if (!net || entity_is_dormant(net)) | ||
| 1135 | continue; | ||
| 1136 | const char *cls = entity_class_name(net, nullptr); | ||
| 1137 | if (!cls || !is_npc_class(cls)) | ||
| 1138 | continue; | ||
| 1139 | |||
| 1140 | uintptr_t e = (uintptr_t)ent; | ||
| 1141 | int life = g_net.lifestate >= 0 | ||
| 1142 | ? (int)*(unsigned char *)(e + g_net.lifestate) | ||
| 1143 | : -1; | ||
| 1144 | if (life != LIFE_ALIVE) | ||
| 1145 | continue; | ||
| 1146 | const char *model = entity_model_name(ent); | ||
| 1147 | bool neutral = false; | ||
| 1148 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 1149 | continue; | ||
| 1150 | if (!npc_can_be_damaged(idx, model)) | ||
| 1151 | continue; | ||
| 1152 | bool armored = npc_damage_class(idx, model) == NPC_DMG_ARMORED; | ||
| 1153 | |||
| 1154 | float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; | ||
| 1155 | if (!(maxz > 4.0f && maxz < 512.0f)) | ||
| 1156 | maxz = 72.0f; | ||
| 1157 | Vector origin{*(float *)(e + g_net.origin), | ||
| 1158 | *(float *)(e + g_net.origin + 4), | ||
| 1159 | *(float *)(e + g_net.origin + 8)}; | ||
| 1160 | Vector approx{origin.x, origin.y, | ||
| 1161 | origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)}; | ||
| 1162 | float fov = getfov(view, angle_to(g_cam_eye, approx)); | ||
| 1163 | // The prefilter samples a single point, but the selected hitbox can be a | ||
| 1164 | // whole body-width away at close range. Reject only when the target's | ||
| 1165 | // angular radius cannot reach the cone; try_target still enforces the | ||
| 1166 | // exact per-hitbox FOV. | ||
| 1167 | float ddx = approx.x - g_cam_eye.x; | ||
| 1168 | float ddy = approx.y - g_cam_eye.y; | ||
| 1169 | float ddz = approx.z - g_cam_eye.z; | ||
| 1170 | float dist = sqrtf(ddx * ddx + ddy * ddy + ddz * ddz); | ||
| 1171 | float margin = | ||
| 1172 | dist > 1.0f ? atan2f(maxz, dist) * 180.0f / PI_F : 90.0f; | ||
| 1173 | if (fov - margin > max_fov) | ||
| 1174 | continue; | ||
| 1175 | float key = (rpg && armored) ? fov - 1000.0f : fov; | ||
| 1176 | |||
| 1177 | int pos = ncands; | ||
| 1178 | while (pos > 0 && cands[pos - 1].key > key) | ||
| 1179 | pos--; | ||
| 1180 | if (ncands < 16) | ||
| 1181 | ncands++; | ||
| 1182 | for (int j = ncands - 1; j > pos; j--) | ||
| 1183 | cands[j] = cands[j - 1]; | ||
| 1184 | if (pos < 16) | ||
| 1185 | cands[pos] = {idx, fov, approx, armored, key}; | ||
| 1186 | } | ||
| 1187 | |||
| 1188 | if (ncands == 0) { | ||
| 1189 | static double last_none = 0.0; | ||
| 1190 | double now = esp_now(); | ||
| 1191 | if (now - last_none >= 2.0) { | ||
| 1192 | last_none = now; | ||
| 1193 | logd("%s: no hostile target within %.0f deg", tag, max_fov); | ||
| 1194 | } | ||
| 1195 | return false; | ||
| 1196 | } | ||
| 1197 | |||
| 1198 | for (int ci = 0; ci < ncands; ci++) { | ||
| 1199 | Vector pt; | ||
| 1200 | float f; | ||
| 1201 | if (!try_target(cands[ci].idx, max_fov, pt, f)) | ||
| 1202 | continue; | ||
| 1203 | out_idx = cands[ci].idx; | ||
| 1204 | out_point = pt; | ||
| 1205 | out_fov = f; | ||
| 1206 | if (lock_idx) | ||
| 1207 | *lock_idx = out_idx; | ||
| 1208 | static int sel_logs = 0; | ||
| 1209 | if (sel_logs < 5) { | ||
| 1210 | sel_logs++; | ||
| 1211 | logd("%s: target idx=%d fov=%.2f vischeck=%d", tag, out_idx, f, | ||
| 1212 | (int)vischeck); | ||
| 1213 | } | ||
| 1214 | return true; | ||
| 1215 | } | ||
| 1216 | |||
| 1217 | static double last_vis = 0.0; | ||
| 1218 | double now = esp_now(); | ||
| 1219 | if (now - last_vis >= 2.0) { | ||
| 1220 | last_vis = now; | ||
| 1221 | logd("%s: no visible aim point (vischeck=%d)", tag, (int)vischeck); | ||
| 1222 | } | ||
| 1223 | return false; | ||
| 1224 | } | ||
| 1225 | |||
| 1226 | bool active_weapon_contains(void *local_ent, const char *needle) { | ||
| 1227 | if (!local_ent || g_net.activeweapon < 0 || !needle) | ||
| 1228 | return false; | ||
| 1229 | uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); | ||
| 1230 | if (h == 0xFFFFFFFFu) | ||
| 1231 | return false; | ||
| 1232 | int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index | ||
| 1233 | if (idx <= 0 || idx > 4096) | ||
| 1234 | return false; | ||
| 1235 | void *wep = entitylist_get(idx); | ||
| 1236 | void *net = entity_networkable(idx); | ||
| 1237 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 1238 | const char *model = wep ? entity_model_name(wep) : nullptr; | ||
| 1239 | return (cls && strcasestr(cls, needle)) || | ||
| 1240 | (model && strcasestr(model, needle)); | ||
| 1241 | } | ||
| 1242 | |||
| 1243 | // Crossbow bolts and RPG rockets are not hitscan. Lead moving targets by the | ||
| 1244 | // projectile travel time; the crossbow bolt also has gravity 0.05 (30 u/s^2). | ||
| 1245 | static bool projectile_lead_point(int target_idx, const Vector &point, | ||
| 1246 | Vector &out) { | ||
| 1247 | int local = engine_get_local_player(); | ||
| 1248 | if (local <= 0) | ||
| 1249 | return false; | ||
| 1250 | void *local_ent = entitylist_get(local); | ||
| 1251 | if (!local_ent) | ||
| 1252 | return false; | ||
| 1253 | float speed = 0.0f, gravity = 0.0f; | ||
| 1254 | if (active_weapon_contains(local_ent, "crossbow") || | ||
| 1255 | active_weapon_contains(local_ent, "Crossbow")) { | ||
| 1256 | speed = 2500.0f; | ||
| 1257 | gravity = 30.0f; | ||
| 1258 | } else if (active_weapon_contains(local_ent, "rocket_launcher") || | ||
| 1259 | active_weapon_contains(local_ent, "w_rpg") || | ||
| 1260 | active_weapon_contains(local_ent, "RPG")) { | ||
| 1261 | speed = 1500.0f; | ||
| 1262 | gravity = 0.0f; | ||
| 1263 | } else { | ||
| 1264 | return false; | ||
| 1265 | } | ||
| 1266 | void *target = entitylist_get(target_idx); | ||
| 1267 | if (!target) | ||
| 1268 | return false; | ||
| 1269 | // NPCs do not network C_BaseEntity::m_vecVelocity (only the player does), so | ||
| 1270 | // the client field is stale for exactly the targets the aimbot uses. Read | ||
| 1271 | // the listen-server entity when available and fall back to the client value. | ||
| 1272 | float v[3] = {0.0f, 0.0f, 0.0f}; | ||
| 1273 | Vector srv_vel; | ||
| 1274 | bool have_vel = false; | ||
| 1275 | if (server_entity_velocity(target_idx, &srv_vel)) { | ||
| 1276 | v[0] = srv_vel.x; | ||
| 1277 | v[1] = srv_vel.y; | ||
| 1278 | v[2] = srv_vel.z; | ||
| 1279 | have_vel = true; | ||
| 1280 | } else if (g_net.velocity >= 0) { | ||
| 1281 | const float *cv = (const float *)((uintptr_t)target + g_net.velocity); | ||
| 1282 | v[0] = cv[0]; | ||
| 1283 | v[1] = cv[1]; | ||
| 1284 | v[2] = cv[2]; | ||
| 1285 | have_vel = true; | ||
| 1286 | } | ||
| 1287 | if (!have_vel) | ||
| 1288 | return false; | ||
| 1289 | float dx = point.x - g_cam_eye.x; | ||
| 1290 | float dy = point.y - g_cam_eye.y; | ||
| 1291 | float dz = point.z - g_cam_eye.z; | ||
| 1292 | float dist = sqrtf(dx * dx + dy * dy + dz * dz); | ||
| 1293 | float t = dist / speed; | ||
| 1294 | out = point; | ||
| 1295 | out.x += v[0] * t; | ||
| 1296 | out.y += v[1] * t; | ||
| 1297 | out.z += v[2] * t + 0.5f * gravity * t * t; | ||
| 1298 | return true; | ||
| 1299 | } | ||
| 1300 | |||
| 1301 | // A led point is a prediction, not a verified aim point: acquisition checked | ||
| 1302 | // the un-led position, so the shift has to pass the same FOV and visibility | ||
| 1303 | // tests. On failure the caller keeps the original point instead of firing into | ||
| 1304 | // geometry or outside the configured cone. | ||
| 1305 | static bool lead_point_ok(int target_idx, const Vector &lead, | ||
| 1306 | const QAngle &view, float max_fov) { | ||
| 1307 | if (getfov(view, angle_to(g_cam_eye, lead)) > max_fov) | ||
| 1308 | return false; | ||
| 1309 | if (!aimbot_vischeck_enabled()) | ||
| 1310 | return true; | ||
| 1311 | void *local_ent = entitylist_get(engine_get_local_player()); | ||
| 1312 | void *target_ent = entitylist_get(target_idx); | ||
| 1313 | if (!local_ent || !target_ent) | ||
| 1314 | return false; | ||
| 1315 | return is_visible(g_cam_eye, lead, local_ent, target_ent); | ||
| 1316 | } | ||
| 1317 | |||
| 1318 | // Semi-auto weapons (pistol, .357, crossbow, shotgun, RPG, gravity gun) only | ||
| 1319 | // fire on the IN_ATTACK press edge, so a synthetic button held every tick fires | ||
| 1320 | // exactly once per buy/equip. Alternate press/release for those; automatic | ||
| 1321 | // weapons keep the button held. | ||
| 1322 | static bool weapon_is_semi_auto(void *local_ent) { | ||
| 1323 | static const char *names[] = {"pistol", "357", | ||
| 1324 | "crossbow", "shotgun", | ||
| 1325 | "rocket_launcher", "physcannon", | ||
| 1326 | "crowbar", nullptr}; | ||
| 1327 | for (int i = 0; names[i]; i++) | ||
| 1328 | if (active_weapon_contains(local_ent, names[i])) | ||
| 1329 | return true; | ||
| 1330 | return false; | ||
| 1331 | } | ||
| 1332 | |||
| 1333 | static bool fire_edge_ok(CUserCmd *cmd) { | ||
| 1334 | void *local_ent = entitylist_get(engine_get_local_player()); | ||
| 1335 | if (!local_ent || !weapon_is_semi_auto(local_ent)) | ||
| 1336 | return true; | ||
| 1337 | static int last_tick = -1; | ||
| 1338 | static bool phase = false; | ||
| 1339 | if (cmd->tick_count != last_tick) { | ||
| 1340 | last_tick = cmd->tick_count; | ||
| 1341 | phase = !phase; | ||
| 1342 | } | ||
| 1343 | return phase; | ||
| 1344 | } | ||
| 1345 | |||
| 1346 | bool aimbot_apply(CUserCmd *cmd) { | ||
| 1347 | unsigned bone_mask = aimbot_bone_mask(); | ||
| 1348 | if (!bone_mask) | ||
| 1349 | return false; | ||
| 1350 | QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; | ||
| 1351 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 1352 | bool punch = punch_angles(punch_x, punch_y); | ||
| 1353 | if (punch) { | ||
| 1354 | view.x += punch_x; | ||
| 1355 | view.y += punch_y; | ||
| 1356 | } | ||
| 1357 | int target = -1; | ||
| 1358 | Vector point{}; | ||
| 1359 | float fov = 0.0f; | ||
| 1360 | float max_fov = (float)aimbot_fov(); | ||
| 1361 | if (max_fov < 1.0f) max_fov = 1.0f; | ||
| 1362 | if (max_fov > 180.0f) max_fov = 180.0f; | ||
| 1363 | int *lock = feature_has_bind(FEAT_AIMBOT) ? &g_aim_lock : nullptr; | ||
| 1364 | if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov, | ||
| 1365 | "aimbot", lock, target, point, fov)) | ||
| 1366 | return false; | ||
| 1367 | Vector lead; | ||
| 1368 | if (projectile_lead_point(target, point, lead) && | ||
| 1369 | lead_point_ok(target, lead, view, max_fov)) | ||
| 1370 | point = lead; | ||
| 1371 | QAngle desired = angle_to(g_cam_eye, point); | ||
| 1372 | if (punch) { | ||
| 1373 | desired.x -= punch_x; | ||
| 1374 | desired.y -= punch_y; | ||
| 1375 | } | ||
| 1376 | static int want_logs = 0; | ||
| 1377 | if (want_logs < 10) { | ||
| 1378 | want_logs++; | ||
| 1379 | logd("aimbot: target=%d aim=(%.1f %.1f %.1f) fov=%.2f want=%.2f/%.2f", | ||
| 1380 | target, point.x, point.y, point.z, fov, desired.x, desired.y); | ||
| 1381 | } | ||
| 1382 | float dp = desired.x - cmd->viewangles[0]; | ||
| 1383 | float dy = desired.y - cmd->viewangles[1]; | ||
| 1384 | while (dy > 180.0f) dy -= 360.0f; | ||
| 1385 | while (dy < -180.0f) dy += 360.0f; | ||
| 1386 | |||
| 1387 | constexpr float DEADZONE = 0.05f; | ||
| 1388 | float max_step = (float)aimbot_smooth_step(); | ||
| 1389 | if (max_step < 1.0f) max_step = 1.0f; | ||
| 1390 | float maxd = fmaxf(fabsf(dp), fabsf(dy)); | ||
| 1391 | if (maxd <= DEADZONE) | ||
| 1392 | return false; | ||
| 1393 | |||
| 1394 | float scale = (maxd > max_step) ? (max_step / maxd) : 1.0f; | ||
| 1395 | float old_pitch = cmd->viewangles[0]; | ||
| 1396 | float old_yaw = cmd->viewangles[1]; | ||
| 1397 | float np = cmd->viewangles[0] + dp * scale; | ||
| 1398 | float ny = cmd->viewangles[1] + dy * scale; | ||
| 1399 | if (np > 89.0f) np = 89.0f; | ||
| 1400 | if (np < -89.0f) np = -89.0f; | ||
| 1401 | while (ny > 180.0f) ny -= 360.0f; | ||
| 1402 | while (ny < -180.0f) ny += 360.0f; | ||
| 1403 | cmd->viewangles[0] = np; | ||
| 1404 | cmd->viewangles[1] = ny; | ||
| 1405 | |||
| 1406 | if (g_iface.engine) { | ||
| 1407 | QAngle out{np, ny, 0.0f}; | ||
| 1408 | using SetViewFn = void (*)(void *, QAngle *); | ||
| 1409 | vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &out); | ||
| 1410 | } | ||
| 1411 | static int mod_logs = 0; | ||
| 1412 | if (mod_logs < 10) { | ||
| 1413 | mod_logs++; | ||
| 1414 | logd("aimbot: view %.2f/%.2f -> %.2f/%.2f", old_pitch, old_yaw, np, ny); | ||
| 1415 | } | ||
| 1416 | return true; | ||
| 1417 | } | ||
| 1418 | |||
| 1419 | // Keep the world-space movement wish direction unchanged when the usercmd | ||
| 1420 | // view angles are temporarily replaced by the silent-aim angles. | ||
| 1421 | static void preserve_movement(CUserCmd *cmd, float old_yaw, float new_yaw) { | ||
| 1422 | float oy = old_yaw * PI_F / 180.0f; | ||
| 1423 | float ay = new_yaw * PI_F / 180.0f; | ||
| 1424 | float ofx = cosf(oy), ofy = sinf(oy); | ||
| 1425 | float orx = sinf(oy), ory = -cosf(oy); | ||
| 1426 | float afx = cosf(ay), afy = sinf(ay); | ||
| 1427 | float arx = sinf(ay), ary = -cosf(ay); | ||
| 1428 | float wx = ofx * cmd->forwardmove + orx * cmd->sidemove; | ||
| 1429 | float wy = ofy * cmd->forwardmove + ory * cmd->sidemove; | ||
| 1430 | cmd->forwardmove = wx * afx + wy * afy; | ||
| 1431 | cmd->sidemove = wx * arx + wy * ary; | ||
| 1432 | } | ||
| 1433 | |||
| 1434 | bool silent_aim_apply(CUserCmd *cmd) { | ||
| 1435 | unsigned bone_mask = aimbot_bone_mask(); | ||
| 1436 | if (!bone_mask) | ||
| 1437 | return false; | ||
| 1438 | QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; | ||
| 1439 | QAngle view{orig.x, orig.y, 0.0f}; | ||
| 1440 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 1441 | bool punch = punch_angles(punch_x, punch_y); | ||
| 1442 | if (punch) { | ||
| 1443 | view.x += punch_x; | ||
| 1444 | view.y += punch_y; | ||
| 1445 | } | ||
| 1446 | int target = -1; | ||
| 1447 | Vector point{}; | ||
| 1448 | float fov = 0.0f; | ||
| 1449 | float max_fov = (float)aimbot_fov(); | ||
| 1450 | if (max_fov < 1.0f) max_fov = 1.0f; | ||
| 1451 | if (max_fov > 180.0f) max_fov = 180.0f; | ||
| 1452 | int *lock = feature_has_bind(FEAT_SILENTAIM) ? &g_silent_lock : nullptr; | ||
| 1453 | if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov, | ||
| 1454 | "silentaim", lock, target, point, fov)) | ||
| 1455 | return false; | ||
| 1456 | Vector lead; | ||
| 1457 | if (projectile_lead_point(target, point, lead) && | ||
| 1458 | lead_point_ok(target, lead, view, max_fov)) | ||
| 1459 | point = lead; | ||
| 1460 | QAngle desired = angle_to(g_cam_eye, point); | ||
| 1461 | if (punch) { | ||
| 1462 | desired.x -= punch_x; | ||
| 1463 | desired.y -= punch_y; | ||
| 1464 | } | ||
| 1465 | float np = desired.x; | ||
| 1466 | if (np > 89.0f) np = 89.0f; | ||
| 1467 | if (np < -89.0f) np = -89.0f; | ||
| 1468 | float ny = desired.y; | ||
| 1469 | while (ny > 180.0f) ny -= 360.0f; | ||
| 1470 | while (ny < -180.0f) ny += 360.0f; | ||
| 1471 | |||
| 1472 | preserve_movement(cmd, orig.y, ny); | ||
| 1473 | |||
| 1474 | cmd->viewangles[0] = np; | ||
| 1475 | cmd->viewangles[1] = ny; | ||
| 1476 | cmd->viewangles[2] = orig.z; | ||
| 1477 | |||
| 1478 | // Restore now and again at FRAME_RENDER_START (after the engine consumed | ||
| 1479 | // the aimed usercmd); the frame-stage restore is the one that sticks. | ||
| 1480 | set_silent_restore(orig); | ||
| 1481 | if (g_iface.engine) { | ||
| 1482 | QAngle restore{orig.x, orig.y, orig.z}; | ||
| 1483 | using SetViewFn = void (*)(void *, QAngle *); | ||
| 1484 | vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore); | ||
| 1485 | } | ||
| 1486 | if (fire_edge_ok(cmd)) | ||
| 1487 | cmd->buttons |= IN_ATTACK; | ||
| 1488 | static int logs = 0; | ||
| 1489 | if (logs < 5) { | ||
| 1490 | logs++; | ||
| 1491 | logd("silentaim: fire idx=%d fov=%.2f aim=%.2f/%.2f", target, fov, np, ny); | ||
| 1492 | } | ||
| 1493 | return true; | ||
| 1494 | } | ||
| 1495 | |||
| 1496 | // --------------------------------------------------------------------------- | ||
| 1497 | // melee killbot | ||
| 1498 | // --------------------------------------------------------------------------- | ||
| 1499 | // | ||
| 1500 | // The crowbar's CBaseHLBludgeonWeapon::Swing traces 75 units from | ||
| 1501 | // Weapon_ShootPosition along the view forward (CROWBAR_RANGE in | ||
| 1502 | // weapon_crowbar.h). It first tries a line, then a +/-16 hull backed off by | ||
| 1503 | // ~28 units, and ignores targets more than ~45 degrees off the aim direction. | ||
| 1504 | // We therefore only need to point the usercmd at the target for one command; | ||
| 1505 | // the rendered view is restored exactly like silent aim. | ||
| 1506 | |||
| 1507 | static constexpr float MELEE_RANGE = 75.0f; | ||
| 1508 | |||
| 1509 | static float vec_dist(const Vector &a, const Vector &b) { | ||
| 1510 | float dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z; | ||
| 1511 | return sqrtf(dx * dx + dy * dy + dz * dz); | ||
| 1512 | } | ||
| 1513 | |||
| 1514 | bool melee_weapon_active(void *local_ent) { | ||
| 1515 | if (!local_ent) | ||
| 1516 | return false; | ||
| 1517 | int local = engine_get_local_player(); | ||
| 1518 | if (g_iface.engineserver) | ||
| 1519 | resolve_server_netvars(); | ||
| 1520 | |||
| 1521 | auto check = [&](int idx, const char **cls_out, | ||
| 1522 | const char **model_out) -> bool { | ||
| 1523 | if (idx <= 0 || idx > 4096) | ||
| 1524 | return false; | ||
| 1525 | void *wep = entitylist_get(idx); | ||
| 1526 | void *net = entity_networkable(idx); | ||
| 1527 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 1528 | const char *model = wep ? entity_model_name(wep) : nullptr; | ||
| 1529 | // Client weapon models can be missing/0 after a map change; recover the | ||
| 1530 | // server model index so the crowbar is still recognized. | ||
| 1531 | if ((!model || !*model) && g_iface.engineserver) { | ||
| 1532 | void *sbase = nullptr; | ||
| 1533 | server_entity_health(idx, nullptr, nullptr, &sbase); | ||
| 1534 | if (sbase) | ||
| 1535 | model = model_name_from_index(server_model_index(sbase)); | ||
| 1536 | } | ||
| 1537 | *cls_out = cls; | ||
| 1538 | *model_out = model; | ||
| 1539 | return (cls && strstr(cls, "Crowbar")) || | ||
| 1540 | (model && strstr(model, "crowbar")); | ||
| 1541 | }; | ||
| 1542 | |||
| 1543 | int idx = -1; | ||
| 1544 | if (g_net.activeweapon >= 0) { | ||
| 1545 | uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); | ||
| 1546 | if (h != 0xFFFFFFFFu) | ||
| 1547 | idx = (int)(h & 0x7FF); // 11-bit entity index; upper bits are serial | ||
| 1548 | } | ||
| 1549 | const char *cls = nullptr, *model = nullptr; | ||
| 1550 | bool crowbar = check(idx, &cls, &model); | ||
| 1551 | |||
| 1552 | if (!crowbar) { | ||
| 1553 | // Fallback: the authoritative server player's m_hActiveWeapon handle. | ||
| 1554 | // Also covers a client netvar offset that resolves but does not point at | ||
| 1555 | // the weapon the server is actually using. | ||
| 1556 | if (local > 0) { | ||
| 1557 | resolve_server_netvars(); | ||
| 1558 | int si = server_active_weapon(local); | ||
| 1559 | if (si > 0 && si != idx) { | ||
| 1560 | idx = si; | ||
| 1561 | crowbar = check(idx, &cls, &model); | ||
| 1562 | } | ||
| 1563 | } | ||
| 1564 | } | ||
| 1565 | |||
| 1566 | static int last_idx = -2; | ||
| 1567 | static bool last_crowbar = false; | ||
| 1568 | if (idx != last_idx || crowbar != last_crowbar) { | ||
| 1569 | last_idx = idx; | ||
| 1570 | last_crowbar = crowbar; | ||
| 1571 | logf("melee: active idx=%d cls=%s model=%s crowbar=%d", idx, | ||
| 1572 | cls ? cls : "?", model ? model : "?", (int)crowbar); | ||
| 1573 | if (!crowbar) { | ||
| 1574 | static bool warned = false; | ||
| 1575 | if (!warned) { | ||
| 1576 | warned = true; | ||
| 1577 | toastf(TOAST_WARN, "Melee killbot: equip the crowbar"); | ||
| 1578 | } | ||
| 1579 | } | ||
| 1580 | } | ||
| 1581 | return crowbar; | ||
| 1582 | } | ||
| 1583 | |||
| 1584 | bool gravity_gun_active(void *local_ent) { | ||
| 1585 | if (!local_ent) | ||
| 1586 | return false; | ||
| 1587 | if (active_weapon_contains(local_ent, "physcannon")) | ||
| 1588 | return true; | ||
| 1589 | // Same server fallback as the crowbar: map transitions can leave the | ||
| 1590 | // client activeweapon handle stale. | ||
| 1591 | int local = engine_get_local_player(); | ||
| 1592 | if (local <= 0 || !g_iface.engineserver) | ||
| 1593 | return false; | ||
| 1594 | resolve_server_netvars(); | ||
| 1595 | int si = server_active_weapon(local); | ||
| 1596 | if (si <= 0) | ||
| 1597 | return false; | ||
| 1598 | void *wep = entitylist_get(si); | ||
| 1599 | void *net = entity_networkable(si); | ||
| 1600 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 1601 | const char *model = wep ? entity_model_name(wep) : nullptr; | ||
| 1602 | if (!model) { | ||
| 1603 | void *sbase = nullptr; | ||
| 1604 | server_entity_health(si, nullptr, nullptr, &sbase); | ||
| 1605 | if (sbase) | ||
| 1606 | model = model_name_from_index(server_model_index(sbase)); | ||
| 1607 | } | ||
| 1608 | return (cls && strcasestr(cls, "physcannon")) || | ||
| 1609 | (model && strcasestr(model, "physcannon")); | ||
| 1610 | } | ||
| 1611 | |||
| 1612 | // The citadel supercharges the gravity gun (d3_citadel_03 onward). It can then | ||
| 1613 | // punt any NPC, so the melee aimbot should not restrict itself to | ||
| 1614 | // physics-vulnerable units. | ||
| 1615 | static bool supercharged_gravity_gun() { | ||
| 1616 | if (!g_iface.engine) | ||
| 1617 | return false; | ||
| 1618 | using Fn = const char *(*)(void *); | ||
| 1619 | const char *m = vfunc<Fn>(g_iface.engine, 51)(g_iface.engine); | ||
| 1620 | if (!m) | ||
| 1621 | return false; | ||
| 1622 | bool supe = strstr(m, "d3_citadel_03") || strstr(m, "d3_citadel_04") || | ||
| 1623 | strstr(m, "d3_breen_01"); | ||
| 1624 | static const char *logged_map = nullptr; | ||
| 1625 | if (logged_map != m) { | ||
| 1626 | logged_map = m; | ||
| 1627 | logf("melee: gravity gun map=%s supercharged=%d", m, (int)supe); | ||
| 1628 | } | ||
| 1629 | return supe; | ||
| 1630 | } | ||
| 1631 | |||
| 1632 | bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { | ||
| 1633 | if (!g_iface.entitylist || g_net.origin < 0) | ||
| 1634 | return false; | ||
| 1635 | int local = engine_get_local_player(); | ||
| 1636 | if (local <= 0) | ||
| 1637 | return false; | ||
| 1638 | void *local_ent = entitylist_get(local); | ||
| 1639 | if (!local_ent) | ||
| 1640 | return false; | ||
| 1641 | if (g_net.health >= 0 && | ||
| 1642 | *(int *)((uintptr_t)local_ent + g_net.health) <= 0) | ||
| 1643 | return false; | ||
| 1644 | |||
| 1645 | // The punted object can travel much farther than the crowbar. | ||
| 1646 | float max_range = gravity ? 1200.0f : MELEE_RANGE; | ||
| 1647 | bool supercharged = gravity && supercharged_gravity_gun(); | ||
| 1648 | |||
| 1649 | using Fn = int (*)(void *); | ||
| 1650 | int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); | ||
| 1651 | if (hi < 0 || hi > 4096) | ||
| 1652 | hi = 4096; | ||
| 1653 | |||
| 1654 | struct Cand { | ||
| 1655 | int idx; | ||
| 1656 | float dist; | ||
| 1657 | Vector closest; | ||
| 1658 | Vector center; | ||
| 1659 | }; | ||
| 1660 | Cand cands[16]; | ||
| 1661 | int n = 0; | ||
| 1662 | |||
| 1663 | for (int idx = 1; idx <= hi; idx++) { | ||
| 1664 | if (idx == local) | ||
| 1665 | continue; | ||
| 1666 | void *ent = entitylist_get(idx); | ||
| 1667 | if (!ent) | ||
| 1668 | continue; | ||
| 1669 | void *net = entity_networkable(idx); | ||
| 1670 | if (!net || entity_is_dormant(net)) | ||
| 1671 | continue; | ||
| 1672 | const char *cls = entity_class_name(net, nullptr); | ||
| 1673 | if (!cls || !is_npc_class(cls)) | ||
| 1674 | continue; | ||
| 1675 | uintptr_t e = (uintptr_t)ent; | ||
| 1676 | int life = g_net.lifestate >= 0 | ||
| 1677 | ? (int)*(unsigned char *)(e + g_net.lifestate) | ||
| 1678 | : -1; | ||
| 1679 | if (life != LIFE_ALIVE) | ||
| 1680 | continue; | ||
| 1681 | const char *model = entity_model_name(ent); | ||
| 1682 | bool neutral = false; | ||
| 1683 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 1684 | continue; | ||
| 1685 | if (!npc_can_be_damaged(idx, model)) | ||
| 1686 | continue; | ||
| 1687 | // The normal gravity gun is only the tool for physics-vulnerable units; | ||
| 1688 | // the citadel version behaves like a normal weapon. | ||
| 1689 | if (gravity && !supercharged && | ||
| 1690 | npc_damage_class(idx, model) != NPC_DMG_VULNERABLE) | ||
| 1691 | continue; | ||
| 1692 | |||
| 1693 | Vector origin{*(float *)(e + g_net.origin), | ||
| 1694 | *(float *)(e + g_net.origin + 4), | ||
| 1695 | *(float *)(e + g_net.origin + 8)}; | ||
| 1696 | float minx = -16.0f, miny = -16.0f, minz = 0.0f; | ||
| 1697 | float maxx = 16.0f, maxy = 16.0f, maxz = 72.0f; | ||
| 1698 | if (g_net.mins >= 0) { | ||
| 1699 | minx = *(float *)(e + g_net.mins); | ||
| 1700 | miny = *(float *)(e + g_net.mins + 4); | ||
| 1701 | minz = *(float *)(e + g_net.mins + 8); | ||
| 1702 | } | ||
| 1703 | if (g_net.maxs >= 0) { | ||
| 1704 | maxx = *(float *)(e + g_net.maxs); | ||
| 1705 | maxy = *(float *)(e + g_net.maxs + 4); | ||
| 1706 | maxz = *(float *)(e + g_net.maxs + 8); | ||
| 1707 | } | ||
| 1708 | // Reject absurd boxes in both directions; a garbage mins value that is | ||
| 1709 | // hugely negative would otherwise produce an AABB spanning the map. | ||
| 1710 | if (!(minz > -512.0f && maxz > minz + 4.0f && maxz < 512.0f)) { | ||
| 1711 | minz = 0.0f; | ||
| 1712 | maxz = 72.0f; | ||
| 1713 | } | ||
| 1714 | if (!(minx > -512.0f && maxx > minx + 1.0f && maxx < 256.0f)) { | ||
| 1715 | minx = -16.0f; | ||
| 1716 | maxx = 16.0f; | ||
| 1717 | } | ||
| 1718 | if (!(miny > -512.0f && maxy > miny + 1.0f && maxy < 256.0f)) { | ||
| 1719 | miny = -16.0f; | ||
| 1720 | maxy = 16.0f; | ||
| 1721 | } | ||
| 1722 | |||
| 1723 | Vector lo{origin.x + minx, origin.y + miny, origin.z + minz}; | ||
| 1724 | Vector hiV{origin.x + maxx, origin.y + maxy, origin.z + maxz}; | ||
| 1725 | Vector closest{g_cam_eye.x, g_cam_eye.y, g_cam_eye.z}; | ||
| 1726 | if (closest.x < lo.x) closest.x = lo.x; | ||
| 1727 | if (closest.x > hiV.x) closest.x = hiV.x; | ||
| 1728 | if (closest.y < lo.y) closest.y = lo.y; | ||
| 1729 | if (closest.y > hiV.y) closest.y = hiV.y; | ||
| 1730 | if (closest.z < lo.z) closest.z = lo.z; | ||
| 1731 | if (closest.z > hiV.z) closest.z = hiV.z; | ||
| 1732 | float d = vec_dist(g_cam_eye, closest); | ||
| 1733 | if (d > max_range) | ||
| 1734 | continue; | ||
| 1735 | |||
| 1736 | Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f, | ||
| 1737 | (lo.z + hiV.z) * 0.5f}; | ||
| 1738 | |||
| 1739 | int pos = n; | ||
| 1740 | while (pos > 0 && cands[pos - 1].dist > d) | ||
| 1741 | pos--; | ||
| 1742 | if (n < 16) | ||
| 1743 | n++; | ||
| 1744 | else if (pos >= 16) | ||
| 1745 | continue; | ||
| 1746 | for (int j = n - 1; j > pos; j--) | ||
| 1747 | cands[j] = cands[j - 1]; | ||
| 1748 | cands[pos] = {idx, d, closest, center}; | ||
| 1749 | } | ||
| 1750 | |||
| 1751 | for (int ci = 0; ci < n; ci++) { | ||
| 1752 | const Cand &c = cands[ci]; | ||
| 1753 | Vector aim{}; | ||
| 1754 | bool have_aim = false; | ||
| 1755 | |||
| 1756 | // Prefer the nearest visible hitbox center. This handles small NPCs | ||
| 1757 | // (headcrabs, antlions) whose AABB center can be less useful than the | ||
| 1758 | // actual body hitboxes. | ||
| 1759 | void *tent = entitylist_get(c.idx); | ||
| 1760 | if (tent && bones_update(c.idx, tent)) { | ||
| 1761 | Vector pts[32]; | ||
| 1762 | int np = aim_points(c.idx, 0xFE, pts, 32); | ||
| 1763 | float bestd = 1e9f; | ||
| 1764 | for (int i = 0; i < np; i++) { | ||
| 1765 | float pd = vec_dist(g_cam_eye, pts[i]); | ||
| 1766 | if (pd > max_range || pd >= bestd) | ||
| 1767 | continue; | ||
| 1768 | if (!is_visible(g_cam_eye, pts[i], local_ent, tent)) | ||
| 1769 | continue; | ||
| 1770 | bestd = pd; | ||
| 1771 | aim = pts[i]; | ||
| 1772 | have_aim = true; | ||
| 1773 | } | ||
| 1774 | } | ||
| 1775 | |||
| 1776 | if (!have_aim) { | ||
| 1777 | aim = c.center; | ||
| 1778 | float ad = vec_dist(g_cam_eye, aim); | ||
| 1779 | if (ad > max_range) { | ||
| 1780 | // The body is in range but its center is not: aim just inside the | ||
| 1781 | // closest point on the AABB. | ||
| 1782 | aim = c.closest; | ||
| 1783 | aim.x += (c.center.x - c.closest.x) * 0.25f; | ||
| 1784 | aim.y += (c.center.y - c.closest.y) * 0.25f; | ||
| 1785 | aim.z += (c.center.z - c.closest.z) * 0.25f; | ||
| 1786 | ad = vec_dist(g_cam_eye, aim); | ||
| 1787 | if (ad > max_range) | ||
| 1788 | aim = c.closest; | ||
| 1789 | } | ||
| 1790 | if (!is_visible(g_cam_eye, aim, local_ent, tent)) | ||
| 1791 | continue; | ||
| 1792 | } | ||
| 1793 | |||
| 1794 | QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; | ||
| 1795 | QAngle desired = angle_to(g_cam_eye, aim); | ||
| 1796 | // The crowbar uses GetAutoaimVector (adds punch); the gravity gun's | ||
| 1797 | // launch uses EyeVectors (no punch), so only compensate for melee. | ||
| 1798 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 1799 | if (!gravity && punch_angles(punch_x, punch_y)) { | ||
| 1800 | desired.x -= punch_x; | ||
| 1801 | desired.y -= punch_y; | ||
| 1802 | } | ||
| 1803 | float np = desired.x; | ||
| 1804 | if (np > 89.0f) np = 89.0f; | ||
| 1805 | if (np < -89.0f) np = -89.0f; | ||
| 1806 | float ny = desired.y; | ||
| 1807 | while (ny > 180.0f) ny -= 360.0f; | ||
| 1808 | while (ny < -180.0f) ny += 360.0f; | ||
| 1809 | |||
| 1810 | preserve_movement(cmd, orig.y, ny); | ||
| 1811 | cmd->viewangles[0] = np; | ||
| 1812 | cmd->viewangles[1] = ny; | ||
| 1813 | cmd->viewangles[2] = orig.z; | ||
| 1814 | set_silent_restore(orig); | ||
| 1815 | if (g_iface.engine) { | ||
| 1816 | QAngle restore{orig.x, orig.y, orig.z}; | ||
| 1817 | using SetViewFn = void (*)(void *, QAngle *); | ||
| 1818 | vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore); | ||
| 1819 | } | ||
| 1820 | // Gravity gun primary attack launches the held object (EyeVectors); | ||
| 1821 | // secondary would drop it instead. | ||
| 1822 | if (fire_edge_ok(cmd)) | ||
| 1823 | cmd->buttons |= IN_ATTACK; | ||
| 1824 | |||
| 1825 | static int logs = 0; | ||
| 1826 | if (logs < 10) { | ||
| 1827 | logs++; | ||
| 1828 | logd("melee: %s idx=%d dist=%.1f aim=(%.1f %.1f %.1f)", | ||
| 1829 | gravity ? "punt" : "swing", c.idx, c.dist, aim.x, aim.y, aim.z); | ||
| 1830 | } | ||
| 1831 | return true; | ||
| 1832 | } | ||
| 1833 | |||
| 1834 | static double last_none = 0.0; | ||
| 1835 | double now = esp_now(); | ||
| 1836 | if (now - last_none >= 2.0) { | ||
| 1837 | last_none = now; | ||
| 1838 | logd("melee: no visible target within %.0f", max_range); | ||
| 1839 | } | ||
| 1840 | return false; | ||
| 1841 | } | ||
| 1842 | |||
| 1843 | // --------------------------------------------------------------------------- | ||
| 1844 | // weapon spread control | ||
| 1845 | // --------------------------------------------------------------------------- | ||
| 1846 | // | ||
| 1847 | // HL2's CBaseEntity::FireBullets uses CBaseEntity::GetPredictionRandomSeed() | ||
| 1848 | // (the client usercmd's random_seed) unless a weapon opts into | ||
| 1849 | // m_bUseServerRandomSeed. HL2 weapons do not, so the client can choose the | ||
| 1850 | // 8-bit seed and with it the random cone offset. CShotManipulator::ApplySpread | ||
| 1851 | // draws four uniform floats per attempt; we precompute the best seed for one | ||
| 1852 | // shot and for the shotgun's pellet windows with the engine's own RNG. | ||
| 1853 | |||
| 1854 | static bool g_spread_ready = false; | ||
| 1855 | static int g_spread_seed1 = 0; | ||
| 1856 | static int g_spread_seed8 = 0; // first pellet exact, 7 spread | ||
| 1857 | static int g_spread_seed12 = 0; // all 12 spread | ||
| 1858 | int g_spread_pellets = 1; | ||
| 1859 | |||
| 1860 | static void spread_compute() { | ||
| 1861 | using SeedFn = void (*)(int); | ||
| 1862 | using FloatFn = float (*)(float, float); | ||
| 1863 | SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed"); | ||
| 1864 | FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat"); | ||
| 1865 | if (!seed || !rf) { | ||
| 1866 | size_t slash = mod_engine().path.find_last_of('/'); | ||
| 1867 | if (slash != std::string::npos) { | ||
| 1868 | std::string path = mod_engine().path.substr(0, slash + 1) + "libvstdlib.so"; | ||
| 1869 | void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | ||
| 1870 | if (!h) | ||
| 1871 | h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); | ||
| 1872 | if (h) { | ||
| 1873 | seed = (SeedFn)dlsym(h, "RandomSeed"); | ||
| 1874 | rf = (FloatFn)dlsym(h, "RandomFloat"); | ||
| 1875 | } | ||
| 1876 | } | ||
| 1877 | } | ||
| 1878 | if (!seed || !rf) { | ||
| 1879 | logf("spread: RandomSeed/RandomFloat unavailable"); | ||
| 1880 | return; | ||
| 1881 | } | ||
| 1882 | |||
| 1883 | auto draw = [&](int s, float &x, float &y) { | ||
| 1884 | seed(s); | ||
| 1885 | int it = 0; | ||
| 1886 | do { | ||
| 1887 | // CShotManipulator::ApplySpread with the default bias of 1.0: | ||
| 1888 | // shotBias = 1.0, flatness = 0.5. | ||
| 1889 | x = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f; | ||
| 1890 | y = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f; | ||
| 1891 | it++; | ||
| 1892 | } while (x * x + y * y > 1.0f && it < 64); | ||
| 1893 | }; | ||
| 1894 | |||
| 1895 | float best1 = 1e9f; | ||
| 1896 | for (int s = 0; s < 256; s++) { | ||
| 1897 | float x, y; | ||
| 1898 | draw(s, x, y); | ||
| 1899 | float r2 = x * x + y * y; | ||
| 1900 | if (r2 < best1) { | ||
| 1901 | best1 = r2; | ||
| 1902 | g_spread_seed1 = s; | ||
| 1903 | } | ||
| 1904 | } | ||
| 1905 | |||
| 1906 | auto window = [&](int n, int first) { | ||
| 1907 | int best_s = 0; | ||
| 1908 | float best = 1e9f; | ||
| 1909 | for (int s = 0; s < 256; s++) { | ||
| 1910 | float score = 0.0f; | ||
| 1911 | for (int i = first; i < n; i++) { | ||
| 1912 | float x, y; | ||
| 1913 | draw(s + i, x, y); | ||
| 1914 | score += x * x + y * y; | ||
| 1915 | } | ||
| 1916 | if (score < best) { | ||
| 1917 | best = score; | ||
| 1918 | best_s = s; | ||
| 1919 | } | ||
| 1920 | } | ||
| 1921 | return best_s; | ||
| 1922 | }; | ||
| 1923 | g_spread_seed8 = window(8, 1); | ||
| 1924 | g_spread_seed12 = window(12, 0); | ||
| 1925 | seed(0); | ||
| 1926 | |||
| 1927 | logd("spread: seeds single=%d shotgun8=%d shotgun12=%d", g_spread_seed1, | ||
| 1928 | g_spread_seed8, g_spread_seed12); | ||
| 1929 | g_spread_ready = true; | ||
| 1930 | } | ||
| 1931 | |||
| 1932 | static bool active_weapon_is_shotgun(void *local_ent) { | ||
| 1933 | if (!local_ent || g_net.activeweapon < 0) | ||
| 1934 | return false; | ||
| 1935 | uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); | ||
| 1936 | if (h == 0xFFFFFFFFu) | ||
| 1937 | return false; | ||
| 1938 | int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index | ||
| 1939 | if (idx <= 0 || idx > 4096) | ||
| 1940 | return false; | ||
| 1941 | void *wep = entitylist_get(idx); | ||
| 1942 | void *net = entity_networkable(idx); | ||
| 1943 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 1944 | const char *model = wep ? entity_model_name(wep) : nullptr; | ||
| 1945 | return (cls && strstr(cls, "Shotgun")) || | ||
| 1946 | (model && strstr(model, "shotgun")); | ||
| 1947 | } | ||
| 1948 | |||
| 1949 | bool spread_control_apply(CUserCmd *cmd, void *local_ent) { | ||
| 1950 | if (!feature_active(FEAT_SPREAD) || !spread_no_spread_enabled()) | ||
| 1951 | return false; | ||
| 1952 | if (!(cmd->buttons & (IN_ATTACK | IN_ATTACK2))) | ||
| 1953 | return false; | ||
| 1954 | if (!g_spread_ready) { | ||
| 1955 | static bool tried = false; | ||
| 1956 | if (!tried) { | ||
| 1957 | tried = true; | ||
| 1958 | spread_compute(); | ||
| 1959 | } | ||
| 1960 | } | ||
| 1961 | if (!g_spread_ready) | ||
| 1962 | return false; | ||
| 1963 | |||
| 1964 | bool shotgun = active_weapon_is_shotgun(local_ent); | ||
| 1965 | int pellets = 1; | ||
| 1966 | if (shotgun) | ||
| 1967 | pellets = (cmd->buttons & IN_ATTACK2) ? 12 : 8; | ||
| 1968 | g_spread_pellets = pellets; | ||
| 1969 | int seed = spread_best_seed(pellets); | ||
| 1970 | if (cmd->random_seed == seed) | ||
| 1971 | return false; | ||
| 1972 | cmd->random_seed = seed; | ||
| 1973 | |||
| 1974 | static int logs = 0; | ||
| 1975 | if (logs < 10) { | ||
| 1976 | logs++; | ||
| 1977 | logd("spread: seed=%d pellets=%d", seed, pellets); | ||
| 1978 | } | ||
| 1979 | return true; | ||
| 1980 | } | ||
| 1981 | |||
| 1982 | int spread_best_seed(int pellets) { | ||
| 1983 | if (!g_spread_ready) | ||
| 1984 | return 33; | ||
| 1985 | if (pellets >= 12) | ||
| 1986 | return g_spread_seed12; | ||
| 1987 | if (pellets >= 8) | ||
| 1988 | return g_spread_seed8; | ||
| 1989 | return g_spread_seed1; | ||
| 1990 | } | ||
| 1991 | |||
| 1992 | bool triggerbot_apply(CUserCmd *cmd) { | ||
| 1993 | if (!g_iface.enginetrace || !g_iface.entitylist) | ||
| 1994 | return false; | ||
| 1995 | int local = engine_get_local_player(); | ||
| 1996 | if (local <= 0) | ||
| 1997 | return false; | ||
| 1998 | void *local_ent = entitylist_get(local); | ||
| 1999 | if (!local_ent) | ||
| 2000 | return false; | ||
| 2001 | if (g_net.health >= 0 && | ||
| 2002 | *(int *)((uintptr_t)local_ent + g_net.health) <= 0) | ||
| 2003 | return false; | ||
| 2004 | |||
| 2005 | QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; | ||
| 2006 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 2007 | if (punch_angles(punch_x, punch_y)) { | ||
| 2008 | view.x += punch_x; | ||
| 2009 | view.y += punch_y; | ||
| 2010 | } | ||
| 2011 | Vector fwd = angle_forward_vector(view); | ||
| 2012 | Vector start = g_cam_eye; | ||
| 2013 | Vector end{start.x + fwd.x * 8192.0f, start.y + fwd.y * 8192.0f, | ||
| 2014 | start.z + fwd.z * 8192.0f}; | ||
| 2015 | void *hit = trace_entity(start, end, local_ent); | ||
| 2016 | if (!hit) | ||
| 2017 | return false; | ||
| 2018 | |||
| 2019 | int idx = entptr_lookup(hit); | ||
| 2020 | if (idx <= 0) { | ||
| 2021 | // Cache miss: ESP is off, or this entity appeared since the last scan. | ||
| 2022 | using Fn = int (*)(void *); | ||
| 2023 | int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); | ||
| 2024 | if (hi < 0 || hi > 4096) | ||
| 2025 | hi = 4096; | ||
| 2026 | for (int i = 1; i <= hi; i++) { | ||
| 2027 | if (entitylist_get(i) != hit) | ||
| 2028 | continue; | ||
| 2029 | idx = i; | ||
| 2030 | entptr_store(hit, i); | ||
| 2031 | break; | ||
| 2032 | } | ||
| 2033 | if (idx <= 0) | ||
| 2034 | return false; | ||
| 2035 | } | ||
| 2036 | void *net = entity_networkable(idx); | ||
| 2037 | if (entity_is_dormant(net)) | ||
| 2038 | return false; | ||
| 2039 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 2040 | if (!cls || !is_npc_class(cls)) | ||
| 2041 | return false; | ||
| 2042 | uintptr_t e = (uintptr_t)hit; | ||
| 2043 | int life = g_net.lifestate >= 0 | ||
| 2044 | ? (int)*(unsigned char *)(e + g_net.lifestate) | ||
| 2045 | : -1; | ||
| 2046 | if (life != LIFE_ALIVE) | ||
| 2047 | return false; | ||
| 2048 | const char *model = entity_model_name(hit); | ||
| 2049 | bool neutral = false; | ||
| 2050 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 2051 | return false; | ||
| 2052 | if (!npc_can_be_damaged(idx, model)) | ||
| 2053 | return false; | ||
| 2054 | if (fire_edge_ok(cmd)) | ||
| 2055 | cmd->buttons |= IN_ATTACK; | ||
| 2056 | static int logs = 0; | ||
| 2057 | if (logs < 5) { | ||
| 2058 | logs++; | ||
| 2059 | logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?"); | ||
| 2060 | } | ||
| 2061 | return true; | ||
| 2062 | } | ||
| 2063 | 550 | ||
| 2064 | // Dynamic props are tinted through their client render colour. StudioRender | 551 | // Dynamic props are tinted through their client render colour. StudioRender |
| 2065 | // feeds m_clrRender into the per-model colour modulation each draw, which is | 552 | // feeds m_clrRender into the per-model colour modulation each draw, which is |
diff --git a/src/esp_internal.h b/src/esp_internal.h index 6e66f42..1ffdd7a 100644 --- a/src/esp_internal.h +++ b/src/esp_internal.h | |||
| @@ -11,6 +11,29 @@ | |||
| 11 | 11 | ||
| 12 | extern int g_scr_w, g_scr_h; | 12 | extern int g_scr_w, g_scr_h; |
| 13 | 13 | ||
| 14 | enum { LIFE_ALIVE = 0 }; | ||
| 15 | enum NpcDamageClass { | ||
| 16 | NPC_DMG_NORMAL = 0, | ||
| 17 | NPC_DMG_ARMORED, // only blast damage (RPG) hurts it | ||
| 18 | NPC_DMG_VULNERABLE, // gravity gun / physics damage works well | ||
| 19 | NPC_DMG_IMMUNE // m_takedamage says no damage at all | ||
| 20 | }; | ||
| 21 | |||
| 22 | bool is_npc_class(const char *name); | ||
| 23 | bool npc_is_friendly(int idx, const char *model, int local_idx, bool &neutral); | ||
| 24 | bool npc_can_be_damaged(int idx, const char *model); | ||
| 25 | int npc_damage_class(int idx, const char *model); | ||
| 26 | QAngle angle_to(const Vector &from, const Vector &to); | ||
| 27 | bool punch_angles(float &px, float &py); | ||
| 28 | int entptr_lookup(void *ent); | ||
| 29 | void entptr_store(void *ent, int idx); | ||
| 30 | void weapon_short_name(int idx, char *out, size_t outn); | ||
| 31 | void short_name(const char *model, char *out, size_t outn); | ||
| 32 | |||
| 33 | // Listen-server sanity latch, shared with esp_collect(). | ||
| 34 | extern bool g_srv_checked; | ||
| 35 | extern bool g_srv_trusted; | ||
| 36 | |||
| 14 | double esp_now(); | 37 | double esp_now(); |
| 15 | bool esp_update_screen(); | 38 | bool esp_update_screen(); |
| 16 | 39 | ||
