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 /src/combat.cpp | |
| 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.
Diffstat (limited to '')
| -rw-r--r-- | src/combat.cpp | 1066 |
1 files changed, 1066 insertions, 0 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 | } | ||
