From c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 6 Nov 2026 16:25:48 +0100 Subject: 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. --- src/combat.cpp | 1066 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1066 insertions(+) create mode 100644 src/combat.cpp (limited to 'src/combat.cpp') 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 @@ +// combat.cpp — aimbot, silent aim, melee, spread control, triggerbot. +#include "esp_internal.h" +#include "hl2h.h" +#include +#include +#include + +static int g_aim_lock = -1; +static int g_silent_lock = -1; + +void aimbot_reset_lock() { g_aim_lock = -1; } +void silentaim_reset_lock() { g_silent_lock = -1; } + +// max_fov is the angular radius around the crosshair. If lock_idx is non-null +// and valid, that target is kept while it is alive and visible (the "do not +// switch while the bind is held" rule); a fresh acquisition is ranked by angle +// and the nearest visible selected hitbox point. +static bool select_aim_target(const QAngle &view, unsigned bone_mask, + bool vischeck, float max_fov, const char *tag, + int *lock_idx, int &out_idx, Vector &out_point, + float &out_fov) { + out_idx = -1; + out_point = {}; + out_fov = 0.0f; + // maxs is optional: the per-target code falls back to 72 units when the + // netvar is missing, so it must not disable the whole aimbot here. + if (!g_iface.entitylist || g_net.origin < 0) + return false; + int local = engine_get_local_player(); + if (local <= 0) + return false; + void *local_ent = entitylist_get(local); + if (!local_ent) + return false; + if (g_net.health >= 0 && + *(int *)((uintptr_t)local_ent + g_net.health) <= 0) + return false; + + // With the RPG equipped, armoured units (striders, gunships, dropships, + // hunter-choppers) are the ones ordinary shots cannot hurt, so prefer them + // over normal NPCs when acquiring. + bool rpg = active_weapon_contains(local_ent, "rocket_launcher") || + active_weapon_contains(local_ent, "rpg"); + + // fov_limit <= 0 means "no angular limit" (kept for unlimited acquisition). + auto try_target = [&](int idx, float fov_limit, Vector &pt, + float &fov) -> bool { + if (idx <= 0 || idx == local) + return false; + void *ent = entitylist_get(idx); + if (!ent) + return false; + void *net = entity_networkable(idx); + if (!net || entity_is_dormant(net)) + return false; + const char *cls = entity_class_name(net, nullptr); + if (!cls || !is_npc_class(cls)) + return false; + uintptr_t e = (uintptr_t)ent; + int life = g_net.lifestate >= 0 + ? (int)*(unsigned char *)(e + g_net.lifestate) + : -1; + if (life != LIFE_ALIVE) + return false; + const char *model = entity_model_name(ent); + bool neutral = false; + if (npc_is_friendly(idx, model, local, neutral) || neutral) + return false; + if (!npc_can_be_damaged(idx, model)) + return false; + if (!bones_update(idx, ent)) + return false; + Vector pts[32]; + int npoints = aim_points(idx, bone_mask, pts, 32); + float best = 1e9f; + Vector best_pt{}; + for (int i = 0; i < npoints; i++) { + float f = getfov(view, angle_to(g_cam_eye, pts[i])); + if (fov_limit > 0.0f && f > fov_limit) + continue; + if (f >= best) + continue; + if (vischeck && !is_visible(g_cam_eye, pts[i], local_ent, ent)) + continue; + best = f; + best_pt = pts[i]; + } + if (best > 1e8f) + return false; + pt = best_pt; + fov = best; + return true; + }; + + // Keep the locked target while it is alive and has a visible selected point. + // The lock uses a wider cone than acquisition (hysteresis), so a target that + // drifts slightly off the crosshair is not dropped, but one that swings + // behind the player is: an unbounded lock let silent aim fire backwards. + if (lock_idx && *lock_idx >= 0) { + Vector pt; + float f; + if (try_target(*lock_idx, max_fov * 1.5f, pt, f)) { + out_idx = *lock_idx; + out_point = pt; + out_fov = f; + static int lock_logs = 0; + if (lock_logs < 5) { + lock_logs++; + logd("%s: lock idx=%d fov=%.2f", tag, out_idx, f); + } + return true; + } + *lock_idx = -1; + } + + using Fn = int (*)(void *); + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); + if (hi < 0 || hi > 4096) + hi = 4096; + + struct Candidate { + int idx; + float fov; + Vector approx; + bool armored; + float key; + }; + Candidate cands[16]; + int ncands = 0; + + for (int idx = 1; idx <= hi; idx++) { + if (idx == local) + continue; + void *ent = entitylist_get(idx); + if (!ent) + continue; + void *net = entity_networkable(idx); + if (!net || entity_is_dormant(net)) + continue; + const char *cls = entity_class_name(net, nullptr); + if (!cls || !is_npc_class(cls)) + continue; + + uintptr_t e = (uintptr_t)ent; + int life = g_net.lifestate >= 0 + ? (int)*(unsigned char *)(e + g_net.lifestate) + : -1; + if (life != LIFE_ALIVE) + continue; + const char *model = entity_model_name(ent); + bool neutral = false; + if (npc_is_friendly(idx, model, local, neutral) || neutral) + continue; + if (!npc_can_be_damaged(idx, model)) + continue; + bool armored = npc_damage_class(idx, model) == NPC_DMG_ARMORED; + + float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; + if (!(maxz > 4.0f && maxz < 512.0f)) + maxz = 72.0f; + Vector origin{*(float *)(e + g_net.origin), + *(float *)(e + g_net.origin + 4), + *(float *)(e + g_net.origin + 8)}; + Vector approx{origin.x, origin.y, + origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)}; + float fov = getfov(view, angle_to(g_cam_eye, approx)); + // The prefilter samples a single point, but the selected hitbox can be a + // whole body-width away at close range. Reject only when the target's + // angular radius cannot reach the cone; try_target still enforces the + // exact per-hitbox FOV. + float ddx = approx.x - g_cam_eye.x; + float ddy = approx.y - g_cam_eye.y; + float ddz = approx.z - g_cam_eye.z; + float dist = sqrtf(ddx * ddx + ddy * ddy + ddz * ddz); + float margin = + dist > 1.0f ? atan2f(maxz, dist) * 180.0f / PI_F : 90.0f; + if (fov - margin > max_fov) + continue; + float key = (rpg && armored) ? fov - 1000.0f : fov; + + int pos = ncands; + while (pos > 0 && cands[pos - 1].key > key) + pos--; + if (ncands < 16) + ncands++; + for (int j = ncands - 1; j > pos; j--) + cands[j] = cands[j - 1]; + if (pos < 16) + cands[pos] = {idx, fov, approx, armored, key}; + } + + if (ncands == 0) { + static double last_none = 0.0; + double now = esp_now(); + if (now - last_none >= 2.0) { + last_none = now; + logd("%s: no hostile target within %.0f deg", tag, max_fov); + } + return false; + } + + for (int ci = 0; ci < ncands; ci++) { + Vector pt; + float f; + if (!try_target(cands[ci].idx, max_fov, pt, f)) + continue; + out_idx = cands[ci].idx; + out_point = pt; + out_fov = f; + if (lock_idx) + *lock_idx = out_idx; + static int sel_logs = 0; + if (sel_logs < 5) { + sel_logs++; + logd("%s: target idx=%d fov=%.2f vischeck=%d", tag, out_idx, f, + (int)vischeck); + } + return true; + } + + static double last_vis = 0.0; + double now = esp_now(); + if (now - last_vis >= 2.0) { + last_vis = now; + logd("%s: no visible aim point (vischeck=%d)", tag, (int)vischeck); + } + return false; +} + +bool active_weapon_contains(void *local_ent, const char *needle) { + if (!local_ent || g_net.activeweapon < 0 || !needle) + return false; + uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); + if (h == 0xFFFFFFFFu) + return false; + int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index + if (idx <= 0 || idx > 4096) + return false; + void *wep = entitylist_get(idx); + void *net = entity_networkable(idx); + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + const char *model = wep ? entity_model_name(wep) : nullptr; + return (cls && strcasestr(cls, needle)) || + (model && strcasestr(model, needle)); +} + +// Crossbow bolts and RPG rockets are not hitscan. Lead moving targets by the +// projectile travel time; the crossbow bolt also has gravity 0.05 (30 u/s^2). +static bool projectile_lead_point(int target_idx, const Vector &point, + Vector &out) { + int local = engine_get_local_player(); + if (local <= 0) + return false; + void *local_ent = entitylist_get(local); + if (!local_ent) + return false; + float speed = 0.0f, gravity = 0.0f; + if (active_weapon_contains(local_ent, "crossbow") || + active_weapon_contains(local_ent, "Crossbow")) { + speed = 2500.0f; + gravity = 30.0f; + } else if (active_weapon_contains(local_ent, "rocket_launcher") || + active_weapon_contains(local_ent, "w_rpg") || + active_weapon_contains(local_ent, "RPG")) { + speed = 1500.0f; + gravity = 0.0f; + } else { + return false; + } + void *target = entitylist_get(target_idx); + if (!target) + return false; + // NPCs do not network C_BaseEntity::m_vecVelocity (only the player does), so + // the client field is stale for exactly the targets the aimbot uses. Read + // the listen-server entity when available and fall back to the client value. + float v[3] = {0.0f, 0.0f, 0.0f}; + Vector srv_vel; + bool have_vel = false; + if (server_entity_velocity(target_idx, &srv_vel)) { + v[0] = srv_vel.x; + v[1] = srv_vel.y; + v[2] = srv_vel.z; + have_vel = true; + } else if (g_net.velocity >= 0) { + const float *cv = (const float *)((uintptr_t)target + g_net.velocity); + v[0] = cv[0]; + v[1] = cv[1]; + v[2] = cv[2]; + have_vel = true; + } + if (!have_vel) + return false; + float dx = point.x - g_cam_eye.x; + float dy = point.y - g_cam_eye.y; + float dz = point.z - g_cam_eye.z; + float dist = sqrtf(dx * dx + dy * dy + dz * dz); + float t = dist / speed; + out = point; + out.x += v[0] * t; + out.y += v[1] * t; + out.z += v[2] * t + 0.5f * gravity * t * t; + return true; +} + +// A led point is a prediction, not a verified aim point: acquisition checked +// the un-led position, so the shift has to pass the same FOV and visibility +// tests. On failure the caller keeps the original point instead of firing into +// geometry or outside the configured cone. +static bool lead_point_ok(int target_idx, const Vector &lead, + const QAngle &view, float max_fov) { + if (getfov(view, angle_to(g_cam_eye, lead)) > max_fov) + return false; + if (!aimbot_vischeck_enabled()) + return true; + void *local_ent = entitylist_get(engine_get_local_player()); + void *target_ent = entitylist_get(target_idx); + if (!local_ent || !target_ent) + return false; + return is_visible(g_cam_eye, lead, local_ent, target_ent); +} + +// Semi-auto weapons (pistol, .357, crossbow, shotgun, RPG, gravity gun) only +// fire on the IN_ATTACK press edge, so a synthetic button held every tick fires +// exactly once per buy/equip. Alternate press/release for those; automatic +// weapons keep the button held. +static bool weapon_is_semi_auto(void *local_ent) { + static const char *names[] = {"pistol", "357", + "crossbow", "shotgun", + "rocket_launcher", "physcannon", + "crowbar", nullptr}; + for (int i = 0; names[i]; i++) + if (active_weapon_contains(local_ent, names[i])) + return true; + return false; +} + +static bool fire_edge_ok(CUserCmd *cmd) { + void *local_ent = entitylist_get(engine_get_local_player()); + if (!local_ent || !weapon_is_semi_auto(local_ent)) + return true; + static int last_tick = -1; + static bool phase = false; + if (cmd->tick_count != last_tick) { + last_tick = cmd->tick_count; + phase = !phase; + } + return phase; +} + +bool aimbot_apply(CUserCmd *cmd) { + unsigned bone_mask = aimbot_bone_mask(); + if (!bone_mask) + return false; + QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; + float punch_x = 0.0f, punch_y = 0.0f; + bool punch = punch_angles(punch_x, punch_y); + if (punch) { + view.x += punch_x; + view.y += punch_y; + } + int target = -1; + Vector point{}; + float fov = 0.0f; + float max_fov = (float)aimbot_fov(); + if (max_fov < 1.0f) max_fov = 1.0f; + if (max_fov > 180.0f) max_fov = 180.0f; + int *lock = feature_has_bind(FEAT_AIMBOT) ? &g_aim_lock : nullptr; + if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov, + "aimbot", lock, target, point, fov)) + return false; + Vector lead; + if (projectile_lead_point(target, point, lead) && + lead_point_ok(target, lead, view, max_fov)) + point = lead; + QAngle desired = angle_to(g_cam_eye, point); + if (punch) { + desired.x -= punch_x; + desired.y -= punch_y; + } + static int want_logs = 0; + if (want_logs < 10) { + want_logs++; + logd("aimbot: target=%d aim=(%.1f %.1f %.1f) fov=%.2f want=%.2f/%.2f", + target, point.x, point.y, point.z, fov, desired.x, desired.y); + } + float dp = desired.x - cmd->viewangles[0]; + float dy = desired.y - cmd->viewangles[1]; + while (dy > 180.0f) dy -= 360.0f; + while (dy < -180.0f) dy += 360.0f; + + constexpr float DEADZONE = 0.05f; + float max_step = (float)aimbot_smooth_step(); + if (max_step < 1.0f) max_step = 1.0f; + float maxd = fmaxf(fabsf(dp), fabsf(dy)); + if (maxd <= DEADZONE) + return false; + + float scale = (maxd > max_step) ? (max_step / maxd) : 1.0f; + float old_pitch = cmd->viewangles[0]; + float old_yaw = cmd->viewangles[1]; + float np = cmd->viewangles[0] + dp * scale; + float ny = cmd->viewangles[1] + dy * scale; + if (np > 89.0f) np = 89.0f; + if (np < -89.0f) np = -89.0f; + while (ny > 180.0f) ny -= 360.0f; + while (ny < -180.0f) ny += 360.0f; + cmd->viewangles[0] = np; + cmd->viewangles[1] = ny; + + if (g_iface.engine) { + QAngle out{np, ny, 0.0f}; + using SetViewFn = void (*)(void *, QAngle *); + vfunc(g_iface.engine, 20)(g_iface.engine, &out); + } + static int mod_logs = 0; + if (mod_logs < 10) { + mod_logs++; + logd("aimbot: view %.2f/%.2f -> %.2f/%.2f", old_pitch, old_yaw, np, ny); + } + return true; +} + +// Keep the world-space movement wish direction unchanged when the usercmd +// view angles are temporarily replaced by the silent-aim angles. +static void preserve_movement(CUserCmd *cmd, float old_yaw, float new_yaw) { + float oy = old_yaw * PI_F / 180.0f; + float ay = new_yaw * PI_F / 180.0f; + float ofx = cosf(oy), ofy = sinf(oy); + float orx = sinf(oy), ory = -cosf(oy); + float afx = cosf(ay), afy = sinf(ay); + float arx = sinf(ay), ary = -cosf(ay); + float wx = ofx * cmd->forwardmove + orx * cmd->sidemove; + float wy = ofy * cmd->forwardmove + ory * cmd->sidemove; + cmd->forwardmove = wx * afx + wy * afy; + cmd->sidemove = wx * arx + wy * ary; +} + +bool silent_aim_apply(CUserCmd *cmd) { + unsigned bone_mask = aimbot_bone_mask(); + if (!bone_mask) + return false; + QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; + QAngle view{orig.x, orig.y, 0.0f}; + float punch_x = 0.0f, punch_y = 0.0f; + bool punch = punch_angles(punch_x, punch_y); + if (punch) { + view.x += punch_x; + view.y += punch_y; + } + int target = -1; + Vector point{}; + float fov = 0.0f; + float max_fov = (float)aimbot_fov(); + if (max_fov < 1.0f) max_fov = 1.0f; + if (max_fov > 180.0f) max_fov = 180.0f; + int *lock = feature_has_bind(FEAT_SILENTAIM) ? &g_silent_lock : nullptr; + if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov, + "silentaim", lock, target, point, fov)) + return false; + Vector lead; + if (projectile_lead_point(target, point, lead) && + lead_point_ok(target, lead, view, max_fov)) + point = lead; + QAngle desired = angle_to(g_cam_eye, point); + if (punch) { + desired.x -= punch_x; + desired.y -= punch_y; + } + float np = desired.x; + if (np > 89.0f) np = 89.0f; + if (np < -89.0f) np = -89.0f; + float ny = desired.y; + while (ny > 180.0f) ny -= 360.0f; + while (ny < -180.0f) ny += 360.0f; + + preserve_movement(cmd, orig.y, ny); + + cmd->viewangles[0] = np; + cmd->viewangles[1] = ny; + cmd->viewangles[2] = orig.z; + + // Restore now and again at FRAME_RENDER_START (after the engine consumed + // the aimed usercmd); the frame-stage restore is the one that sticks. + set_silent_restore(orig); + if (g_iface.engine) { + QAngle restore{orig.x, orig.y, orig.z}; + using SetViewFn = void (*)(void *, QAngle *); + vfunc(g_iface.engine, 20)(g_iface.engine, &restore); + } + if (fire_edge_ok(cmd)) + cmd->buttons |= IN_ATTACK; + static int logs = 0; + if (logs < 5) { + logs++; + logd("silentaim: fire idx=%d fov=%.2f aim=%.2f/%.2f", target, fov, np, ny); + } + return true; +} + +// --------------------------------------------------------------------------- +// melee killbot +// --------------------------------------------------------------------------- +// +// The crowbar's CBaseHLBludgeonWeapon::Swing traces 75 units from +// Weapon_ShootPosition along the view forward (CROWBAR_RANGE in +// weapon_crowbar.h). It first tries a line, then a +/-16 hull backed off by +// ~28 units, and ignores targets more than ~45 degrees off the aim direction. +// We therefore only need to point the usercmd at the target for one command; +// the rendered view is restored exactly like silent aim. + +static constexpr float MELEE_RANGE = 75.0f; + +static float vec_dist(const Vector &a, const Vector &b) { + float dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z; + return sqrtf(dx * dx + dy * dy + dz * dz); +} + +bool melee_weapon_active(void *local_ent) { + if (!local_ent) + return false; + int local = engine_get_local_player(); + if (g_iface.engineserver) + resolve_server_netvars(); + + auto check = [&](int idx, const char **cls_out, + const char **model_out) -> bool { + if (idx <= 0 || idx > 4096) + return false; + void *wep = entitylist_get(idx); + void *net = entity_networkable(idx); + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + const char *model = wep ? entity_model_name(wep) : nullptr; + // Client weapon models can be missing/0 after a map change; recover the + // server model index so the crowbar is still recognized. + if ((!model || !*model) && g_iface.engineserver) { + void *sbase = nullptr; + server_entity_health(idx, nullptr, nullptr, &sbase); + if (sbase) + model = model_name_from_index(server_model_index(sbase)); + } + *cls_out = cls; + *model_out = model; + return (cls && strstr(cls, "Crowbar")) || + (model && strstr(model, "crowbar")); + }; + + int idx = -1; + if (g_net.activeweapon >= 0) { + uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); + if (h != 0xFFFFFFFFu) + idx = (int)(h & 0x7FF); // 11-bit entity index; upper bits are serial + } + const char *cls = nullptr, *model = nullptr; + bool crowbar = check(idx, &cls, &model); + + if (!crowbar) { + // Fallback: the authoritative server player's m_hActiveWeapon handle. + // Also covers a client netvar offset that resolves but does not point at + // the weapon the server is actually using. + if (local > 0) { + resolve_server_netvars(); + int si = server_active_weapon(local); + if (si > 0 && si != idx) { + idx = si; + crowbar = check(idx, &cls, &model); + } + } + } + + static int last_idx = -2; + static bool last_crowbar = false; + if (idx != last_idx || crowbar != last_crowbar) { + last_idx = idx; + last_crowbar = crowbar; + logf("melee: active idx=%d cls=%s model=%s crowbar=%d", idx, + cls ? cls : "?", model ? model : "?", (int)crowbar); + if (!crowbar) { + static bool warned = false; + if (!warned) { + warned = true; + toastf(TOAST_WARN, "Melee killbot: equip the crowbar"); + } + } + } + return crowbar; +} + +bool gravity_gun_active(void *local_ent) { + if (!local_ent) + return false; + if (active_weapon_contains(local_ent, "physcannon")) + return true; + // Same server fallback as the crowbar: map transitions can leave the + // client activeweapon handle stale. + int local = engine_get_local_player(); + if (local <= 0 || !g_iface.engineserver) + return false; + resolve_server_netvars(); + int si = server_active_weapon(local); + if (si <= 0) + return false; + void *wep = entitylist_get(si); + void *net = entity_networkable(si); + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + const char *model = wep ? entity_model_name(wep) : nullptr; + if (!model) { + void *sbase = nullptr; + server_entity_health(si, nullptr, nullptr, &sbase); + if (sbase) + model = model_name_from_index(server_model_index(sbase)); + } + return (cls && strcasestr(cls, "physcannon")) || + (model && strcasestr(model, "physcannon")); +} + +// The citadel supercharges the gravity gun (d3_citadel_03 onward). It can then +// punt any NPC, so the melee aimbot should not restrict itself to +// physics-vulnerable units. +static bool supercharged_gravity_gun() { + if (!g_iface.engine) + return false; + using Fn = const char *(*)(void *); + const char *m = vfunc(g_iface.engine, 51)(g_iface.engine); + if (!m) + return false; + bool supe = strstr(m, "d3_citadel_03") || strstr(m, "d3_citadel_04") || + strstr(m, "d3_breen_01"); + static const char *logged_map = nullptr; + if (logged_map != m) { + logged_map = m; + logf("melee: gravity gun map=%s supercharged=%d", m, (int)supe); + } + return supe; +} + +bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { + if (!g_iface.entitylist || g_net.origin < 0) + return false; + int local = engine_get_local_player(); + if (local <= 0) + return false; + void *local_ent = entitylist_get(local); + if (!local_ent) + return false; + if (g_net.health >= 0 && + *(int *)((uintptr_t)local_ent + g_net.health) <= 0) + return false; + + // The punted object can travel much farther than the crowbar. + float max_range = gravity ? 1200.0f : MELEE_RANGE; + bool supercharged = gravity && supercharged_gravity_gun(); + + using Fn = int (*)(void *); + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); + if (hi < 0 || hi > 4096) + hi = 4096; + + struct Cand { + int idx; + float dist; + Vector closest; + Vector center; + }; + Cand cands[16]; + int n = 0; + + for (int idx = 1; idx <= hi; idx++) { + if (idx == local) + continue; + void *ent = entitylist_get(idx); + if (!ent) + continue; + void *net = entity_networkable(idx); + if (!net || entity_is_dormant(net)) + continue; + const char *cls = entity_class_name(net, nullptr); + if (!cls || !is_npc_class(cls)) + continue; + uintptr_t e = (uintptr_t)ent; + int life = g_net.lifestate >= 0 + ? (int)*(unsigned char *)(e + g_net.lifestate) + : -1; + if (life != LIFE_ALIVE) + continue; + const char *model = entity_model_name(ent); + bool neutral = false; + if (npc_is_friendly(idx, model, local, neutral) || neutral) + continue; + if (!npc_can_be_damaged(idx, model)) + continue; + // The normal gravity gun is only the tool for physics-vulnerable units; + // the citadel version behaves like a normal weapon. + if (gravity && !supercharged && + npc_damage_class(idx, model) != NPC_DMG_VULNERABLE) + continue; + + Vector origin{*(float *)(e + g_net.origin), + *(float *)(e + g_net.origin + 4), + *(float *)(e + g_net.origin + 8)}; + float minx = -16.0f, miny = -16.0f, minz = 0.0f; + float maxx = 16.0f, maxy = 16.0f, maxz = 72.0f; + if (g_net.mins >= 0) { + minx = *(float *)(e + g_net.mins); + miny = *(float *)(e + g_net.mins + 4); + minz = *(float *)(e + g_net.mins + 8); + } + if (g_net.maxs >= 0) { + maxx = *(float *)(e + g_net.maxs); + maxy = *(float *)(e + g_net.maxs + 4); + maxz = *(float *)(e + g_net.maxs + 8); + } + // Reject absurd boxes in both directions; a garbage mins value that is + // hugely negative would otherwise produce an AABB spanning the map. + if (!(minz > -512.0f && maxz > minz + 4.0f && maxz < 512.0f)) { + minz = 0.0f; + maxz = 72.0f; + } + if (!(minx > -512.0f && maxx > minx + 1.0f && maxx < 256.0f)) { + minx = -16.0f; + maxx = 16.0f; + } + if (!(miny > -512.0f && maxy > miny + 1.0f && maxy < 256.0f)) { + miny = -16.0f; + maxy = 16.0f; + } + + Vector lo{origin.x + minx, origin.y + miny, origin.z + minz}; + Vector hiV{origin.x + maxx, origin.y + maxy, origin.z + maxz}; + Vector closest{g_cam_eye.x, g_cam_eye.y, g_cam_eye.z}; + if (closest.x < lo.x) closest.x = lo.x; + if (closest.x > hiV.x) closest.x = hiV.x; + if (closest.y < lo.y) closest.y = lo.y; + if (closest.y > hiV.y) closest.y = hiV.y; + if (closest.z < lo.z) closest.z = lo.z; + if (closest.z > hiV.z) closest.z = hiV.z; + float d = vec_dist(g_cam_eye, closest); + if (d > max_range) + continue; + + Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f, + (lo.z + hiV.z) * 0.5f}; + + int pos = n; + while (pos > 0 && cands[pos - 1].dist > d) + pos--; + if (n < 16) + n++; + else if (pos >= 16) + continue; + for (int j = n - 1; j > pos; j--) + cands[j] = cands[j - 1]; + cands[pos] = {idx, d, closest, center}; + } + + for (int ci = 0; ci < n; ci++) { + const Cand &c = cands[ci]; + Vector aim{}; + bool have_aim = false; + + // Prefer the nearest visible hitbox center. This handles small NPCs + // (headcrabs, antlions) whose AABB center can be less useful than the + // actual body hitboxes. + void *tent = entitylist_get(c.idx); + if (tent && bones_update(c.idx, tent)) { + Vector pts[32]; + int np = aim_points(c.idx, 0xFE, pts, 32); + float bestd = 1e9f; + for (int i = 0; i < np; i++) { + float pd = vec_dist(g_cam_eye, pts[i]); + if (pd > max_range || pd >= bestd) + continue; + if (!is_visible(g_cam_eye, pts[i], local_ent, tent)) + continue; + bestd = pd; + aim = pts[i]; + have_aim = true; + } + } + + if (!have_aim) { + aim = c.center; + float ad = vec_dist(g_cam_eye, aim); + if (ad > max_range) { + // The body is in range but its center is not: aim just inside the + // closest point on the AABB. + aim = c.closest; + aim.x += (c.center.x - c.closest.x) * 0.25f; + aim.y += (c.center.y - c.closest.y) * 0.25f; + aim.z += (c.center.z - c.closest.z) * 0.25f; + ad = vec_dist(g_cam_eye, aim); + if (ad > max_range) + aim = c.closest; + } + if (!is_visible(g_cam_eye, aim, local_ent, tent)) + continue; + } + + QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; + QAngle desired = angle_to(g_cam_eye, aim); + // The crowbar uses GetAutoaimVector (adds punch); the gravity gun's + // launch uses EyeVectors (no punch), so only compensate for melee. + float punch_x = 0.0f, punch_y = 0.0f; + if (!gravity && punch_angles(punch_x, punch_y)) { + desired.x -= punch_x; + desired.y -= punch_y; + } + float np = desired.x; + if (np > 89.0f) np = 89.0f; + if (np < -89.0f) np = -89.0f; + float ny = desired.y; + while (ny > 180.0f) ny -= 360.0f; + while (ny < -180.0f) ny += 360.0f; + + preserve_movement(cmd, orig.y, ny); + cmd->viewangles[0] = np; + cmd->viewangles[1] = ny; + cmd->viewangles[2] = orig.z; + set_silent_restore(orig); + if (g_iface.engine) { + QAngle restore{orig.x, orig.y, orig.z}; + using SetViewFn = void (*)(void *, QAngle *); + vfunc(g_iface.engine, 20)(g_iface.engine, &restore); + } + // Gravity gun primary attack launches the held object (EyeVectors); + // secondary would drop it instead. + if (fire_edge_ok(cmd)) + cmd->buttons |= IN_ATTACK; + + static int logs = 0; + if (logs < 10) { + logs++; + logd("melee: %s idx=%d dist=%.1f aim=(%.1f %.1f %.1f)", + gravity ? "punt" : "swing", c.idx, c.dist, aim.x, aim.y, aim.z); + } + return true; + } + + static double last_none = 0.0; + double now = esp_now(); + if (now - last_none >= 2.0) { + last_none = now; + logd("melee: no visible target within %.0f", max_range); + } + return false; +} + +// --------------------------------------------------------------------------- +// weapon spread control +// --------------------------------------------------------------------------- +// +// HL2's CBaseEntity::FireBullets uses CBaseEntity::GetPredictionRandomSeed() +// (the client usercmd's random_seed) unless a weapon opts into +// m_bUseServerRandomSeed. HL2 weapons do not, so the client can choose the +// 8-bit seed and with it the random cone offset. CShotManipulator::ApplySpread +// draws four uniform floats per attempt; we precompute the best seed for one +// shot and for the shotgun's pellet windows with the engine's own RNG. + +static bool g_spread_ready = false; +static int g_spread_seed1 = 0; +static int g_spread_seed8 = 0; // first pellet exact, 7 spread +static int g_spread_seed12 = 0; // all 12 spread +int g_spread_pellets = 1; + +static void spread_compute() { + using SeedFn = void (*)(int); + using FloatFn = float (*)(float, float); + SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed"); + FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat"); + if (!seed || !rf) { + size_t slash = mod_engine().path.find_last_of('/'); + if (slash != std::string::npos) { + std::string path = mod_engine().path.substr(0, slash + 1) + "libvstdlib.so"; + void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + if (!h) + h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); + if (h) { + seed = (SeedFn)dlsym(h, "RandomSeed"); + rf = (FloatFn)dlsym(h, "RandomFloat"); + } + } + } + if (!seed || !rf) { + logf("spread: RandomSeed/RandomFloat unavailable"); + return; + } + + auto draw = [&](int s, float &x, float &y) { + seed(s); + int it = 0; + do { + // CShotManipulator::ApplySpread with the default bias of 1.0: + // shotBias = 1.0, flatness = 0.5. + x = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f; + y = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f; + it++; + } while (x * x + y * y > 1.0f && it < 64); + }; + + float best1 = 1e9f; + for (int s = 0; s < 256; s++) { + float x, y; + draw(s, x, y); + float r2 = x * x + y * y; + if (r2 < best1) { + best1 = r2; + g_spread_seed1 = s; + } + } + + auto window = [&](int n, int first) { + int best_s = 0; + float best = 1e9f; + for (int s = 0; s < 256; s++) { + float score = 0.0f; + for (int i = first; i < n; i++) { + float x, y; + draw(s + i, x, y); + score += x * x + y * y; + } + if (score < best) { + best = score; + best_s = s; + } + } + return best_s; + }; + g_spread_seed8 = window(8, 1); + g_spread_seed12 = window(12, 0); + seed(0); + + logd("spread: seeds single=%d shotgun8=%d shotgun12=%d", g_spread_seed1, + g_spread_seed8, g_spread_seed12); + g_spread_ready = true; +} + +static bool active_weapon_is_shotgun(void *local_ent) { + if (!local_ent || g_net.activeweapon < 0) + return false; + uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); + if (h == 0xFFFFFFFFu) + return false; + int idx = (int)(h & 0x7FF); // networked EHANDLE: 11-bit entity index + if (idx <= 0 || idx > 4096) + return false; + void *wep = entitylist_get(idx); + void *net = entity_networkable(idx); + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + const char *model = wep ? entity_model_name(wep) : nullptr; + return (cls && strstr(cls, "Shotgun")) || + (model && strstr(model, "shotgun")); +} + +bool spread_control_apply(CUserCmd *cmd, void *local_ent) { + if (!feature_active(FEAT_SPREAD) || !spread_no_spread_enabled()) + return false; + if (!(cmd->buttons & (IN_ATTACK | IN_ATTACK2))) + return false; + if (!g_spread_ready) { + static bool tried = false; + if (!tried) { + tried = true; + spread_compute(); + } + } + if (!g_spread_ready) + return false; + + bool shotgun = active_weapon_is_shotgun(local_ent); + int pellets = 1; + if (shotgun) + pellets = (cmd->buttons & IN_ATTACK2) ? 12 : 8; + g_spread_pellets = pellets; + int seed = spread_best_seed(pellets); + if (cmd->random_seed == seed) + return false; + cmd->random_seed = seed; + + static int logs = 0; + if (logs < 10) { + logs++; + logd("spread: seed=%d pellets=%d", seed, pellets); + } + return true; +} + +int spread_best_seed(int pellets) { + if (!g_spread_ready) + return 33; + if (pellets >= 12) + return g_spread_seed12; + if (pellets >= 8) + return g_spread_seed8; + return g_spread_seed1; +} + +bool triggerbot_apply(CUserCmd *cmd) { + if (!g_iface.enginetrace || !g_iface.entitylist) + return false; + int local = engine_get_local_player(); + if (local <= 0) + return false; + void *local_ent = entitylist_get(local); + if (!local_ent) + return false; + if (g_net.health >= 0 && + *(int *)((uintptr_t)local_ent + g_net.health) <= 0) + return false; + + QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; + float punch_x = 0.0f, punch_y = 0.0f; + if (punch_angles(punch_x, punch_y)) { + view.x += punch_x; + view.y += punch_y; + } + Vector fwd = angle_forward_vector(view); + Vector start = g_cam_eye; + Vector end{start.x + fwd.x * 8192.0f, start.y + fwd.y * 8192.0f, + start.z + fwd.z * 8192.0f}; + void *hit = trace_entity(start, end, local_ent); + if (!hit) + return false; + + int idx = entptr_lookup(hit); + if (idx <= 0) { + // Cache miss: ESP is off, or this entity appeared since the last scan. + using Fn = int (*)(void *); + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); + if (hi < 0 || hi > 4096) + hi = 4096; + for (int i = 1; i <= hi; i++) { + if (entitylist_get(i) != hit) + continue; + idx = i; + entptr_store(hit, i); + break; + } + if (idx <= 0) + return false; + } + void *net = entity_networkable(idx); + if (entity_is_dormant(net)) + return false; + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + if (!cls || !is_npc_class(cls)) + return false; + uintptr_t e = (uintptr_t)hit; + int life = g_net.lifestate >= 0 + ? (int)*(unsigned char *)(e + g_net.lifestate) + : -1; + if (life != LIFE_ALIVE) + return false; + const char *model = entity_model_name(hit); + bool neutral = false; + if (npc_is_friendly(idx, model, local, neutral) || neutral) + return false; + if (!npc_can_be_damaged(idx, model)) + return false; + if (fire_edge_ok(cmd)) + cmd->buttons |= IN_ATTACK; + static int logs = 0; + if (logs < 5) { + logs++; + logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?"); + } + return true; +} -- cgit v1.2.3