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/esp.cpp | 1519 +---------------------------------------------------------- 1 file changed, 3 insertions(+), 1516 deletions(-) (limited to 'src/esp.cpp') 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 @@ #include #include -enum { LIFE_ALIVE = 0 }; + struct EspTarget { int idx; @@ -35,35 +35,6 @@ static int g_ntargets = 0; int g_scr_w = 0, g_scr_h = 0; static bool g_esp_logged = false; -// Listen-server sanity latch. Only ever set once per map (esp_reset_session), -// and only after both model indices are valid: latching on the transient -// cli_model == 0 at map load disabled server health for the whole process. -static bool g_srv_checked = false; -static bool g_srv_trusted = false; - -// Per-map caches. The ESP resolves a model name for every entity on every scan -// and the chams hook asks for a relation on every model draw; both are virtual -// call chains into the engine plus server datamap reads, so memoise them. -static constexpr int MODEL_CACHE = 16384; -static const char *g_model_name_cache[MODEL_CACHE]; -static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail - -// Direct-mapped entity pointer -> index map, filled by esp_collect. The -// triggerbot traces a ray and gets a pointer back, and used to find its index -// by walking up to 4096 entity-list slots on every fire tick. -static constexpr int ENTPTR_CACHE = 4096; -static void *g_entptr_key[ENTPTR_CACHE]; -static int g_entptr_idx[ENTPTR_CACHE]; - -void esp_reset_session() { - g_srv_checked = false; - g_srv_trusted = false; - g_esp_logged = false; - memset(g_model_name_cache, 0, sizeof(g_model_name_cache)); - memset(g_model_state_cache, 0, sizeof(g_model_state_cache)); - memset(g_entptr_key, 0, sizeof(g_entptr_key)); - memset(g_entptr_idx, 0, sizeof(g_entptr_idx)); -} // Speed graph: 100 samples at 0.1 s = the last 10 seconds. static constexpr int SPEED_SAMPLES = 100; @@ -173,431 +144,6 @@ using GetModelNameFn = const char *(*)(void *, const void *); // entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the // IClientNetworkable subobject offset inside the entity (see projectz). -void *entity_networkable(int idx) { - return vfunc(g_iface.entitylist, 0)(g_iface.entitylist, idx); -} - -// IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8. -const char *entity_class_name(void *net, void **out_cc) { - if (!net) return nullptr; - void *cc = vfunc(net, 2)(net); - if (out_cc) *out_cc = cc; - return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; -} - -// IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant -// 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK, -// so the slot is validated once against the local player, which can never be -// dormant: a wrong slot disables the check instead of misreporting entities. -// Dormant entities keep their last networked origin, so drawing or aiming at -// them produces ghosts at stale positions. -static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled - -static void dormant_selftest() { - // Leave the state untested (0) until a local player exists: the first call - // can come from the main menu, and latching "disabled" there would turn the - // check off for the whole session. - if (!g_iface.entitylist) - return; - int local = engine_get_local_player(); - if (local <= 0) - return; - void *net = entity_networkable(local); - if (!net) - return; - void **vt = *(void ***)net; - if (!vt || !mod_client().contains((uintptr_t)vt[8])) { - g_dormant_state = -1; - logf("dormant check disabled: slot 8 outside client module"); - return; - } - using DormantFn = unsigned char (*)(void *); - if (vfunc(net, 8)(net) != 0) { - g_dormant_state = -1; - logf("dormant check disabled: local player reported dormant"); - return; - } - g_dormant_state = 1; - logd("dormant check enabled"); -} - -bool entity_is_dormant(void *net) { - if (!net) - return false; - if (g_dormant_state == 0) - dormant_selftest(); - if (g_dormant_state != 1) - return false; - using DormantFn = unsigned char (*)(void *); - return vfunc(net, 8)(net) != 0; -} - -// Entity pointer -> index, filled by the entity scans. The triggerbot gets a -// pointer back from the trace and needs the index; the alternative is walking -// up to 4096 entity-list slots on every fire tick. -static int entptr_lookup(void *ent) { - if (!ent) - return -1; - unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); - return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1; -} - -static void entptr_store(void *ent, int idx) { - if (!ent || idx <= 0) - return; - unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); - g_entptr_key[h] = ent; - g_entptr_idx[h] = idx; -} - -// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. -// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. -const char *model_name_from_index(int modelindex) { - if (!g_iface.modelinfo || modelindex <= 0) - return nullptr; - if (modelindex < MODEL_CACHE) { - if (g_model_state_cache[modelindex] == 1) - return g_model_name_cache[modelindex]; - if (g_model_state_cache[modelindex] == 2) - return nullptr; - } - const void *model = - vfunc(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); - if (!model) { - if (modelindex < MODEL_CACHE) - g_model_state_cache[modelindex] = 2; - return nullptr; - } - const char *name = - vfunc(g_iface.modelinfo, 4)(g_iface.modelinfo, model); - if (modelindex < MODEL_CACHE) { - g_model_name_cache[modelindex] = name; - g_model_state_cache[modelindex] = name ? 1 : 2; - } - return name; -} - -const char *entity_model_name(void *ent) { - if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr; - int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); - return model_name_from_index(modelindex); -} - -// Live origin for a cached entity index. Item/projectile discovery is throttled -// to 10 Hz (it walks the whole entity list), but the markers must follow the -// world at render rate; refreshing one netvar read per cached target is cheap. -bool entity_origin(void *ent, Vector &out) { - if (!ent || g_net.origin < 0) - return false; - uintptr_t e = (uintptr_t)ent; - out.x = *(float *)(e + g_net.origin); - out.y = *(float *)(e + g_net.origin + 4); - out.z = *(float *)(e + g_net.origin + 8); - return true; -} - -// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all -// use the single client class "CAI_BaseNPC" — only special NPCs register their -// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). -static bool is_npc_class(const char *name) { - if (!name) return false; - return strstr(name, "NPC") != nullptr || - strstr(name, "BaseHelicopter") != nullptr; -} - -// Used by the chams hook to decide whether a model-render entity is an NPC. -bool esp_is_npc_index(int idx) { - if (!g_iface.entitylist || idx <= 0 || idx > 8192) - return false; - void *net = entity_networkable(idx); - if (!net) - return false; - return is_npc_class(entity_class_name(net, nullptr)); -} - -bool esp_is_viewmodel_index(int idx) { - if (!g_iface.entitylist || idx <= 0 || idx > 8192) - return false; - void *ent = entitylist_get(idx); - if (!ent) - return false; - void *net = entity_networkable(idx); - const char *cls = net ? entity_class_name(net, nullptr) : nullptr; - if (cls && strcasestr(cls, "viewmodel")) - return true; - const char *model = entity_model_name(ent); - return model && (strstr(model, "/v_") || strstr(model, "\\v_")); -} - -// 0 = hostile, 1 = neutral, 2 = friendly, -1 = not an NPC. -static bool npc_is_friendly(int idx, const char *model, int local_idx, - bool &neutral); -int esp_npc_relation(int idx) { - // Called from the chams hook up to three times per entity per frame (visible, - // through-wall, wireframe pass) and the work behind it is a chain of server - // edict/datamap reads. A short TTL cache keeps that out of the render path. - struct RelCache { - int idx; - double at; - int val; - }; - static RelCache cache[256]; - double now = esp_now(); - RelCache &c = cache[(unsigned)idx & 255]; - if (c.idx == idx && now - c.at < 0.05) - return c.val; - - int val = -1; - if (esp_is_npc_index(idx)) { - int local = engine_get_local_player(); - if (local <= 0) { - val = 0; - } else { - void *ent = entitylist_get(idx); - const char *model = ent ? entity_model_name(ent) : nullptr; - bool neutral = false; - bool friendly = npc_is_friendly(idx, model, local, neutral); - val = neutral ? 1 : friendly ? 2 : 0; - } - } - c = {idx, now, val}; - return val; -} - -static bool model_is_friendly(const char *m) { - static const char *patterns[] = { - "citizen", "humans/", "male_", "female_", "barney", "alyx", - "alex", "scientist", "kleiner", "eli", "mossman", "vortigaunt", - "dog", "grigori", "monk", "odessa", "gman", nullptr}; - if (!m) return false; - for (int i = 0; patterns[i]; i++) - if (strstr(m, patterns[i])) - return true; - return false; -} - -// harmless wildlife that also happens to be CAI_BaseNPC -static bool model_is_neutral(const char *m) { - // Lamarr is Kleiner's pet headcrab: scripted and harmless, and the user - // wants her shown neutral rather than hostile. - static const char *patterns[] = {"crow", "gull", "pigeon", "seagull", - "bird", "Lamarr", nullptr}; - if (!m) return false; - for (int i = 0; patterns[i]; i++) - if (strstr(m, patterns[i])) - return true; - return false; -} - -static bool model_is_floor_turret(const char *m) { - return m && strstr(m, "floor_turret"); -} - -static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx, - bool &neutral) { - int rel = server_npc_relation(idx, local_idx); - int state = server_npc_state(idx); - int turret = -1; - // Citizen-built and Alyx-hacked turrets never add an m_Relationship row; - // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state. - if (model_is_floor_turret(model)) - turret = server_npc_turret_flags(idx); - // Debug-only: relation/state changes are the authoritative signals and the - // first thing to check when a friendly NPC still looks hostile. - static int rel_cache[2048]; - static int state_cache[2048]; - static bool cache_init = false; - if (!cache_init) { - for (int i = 0; i < 2048; i++) - rel_cache[i] = state_cache[i] = -999; - cache_init = true; - } - if (idx > 0 && idx < 2048 && - (rel_cache[idx] != rel || state_cache[idx] != state)) { - rel_cache[idx] = rel; - state_cache[idx] = state; - logd("npc %d (%s): relation=%d state=%d followtarget=%d turret=%d " - "local=%d", idx, model ? model : "?", rel, state, - (int)server_npc_has_follow_target(idx), turret, local_idx); - } - if (rel == 3) { // D_LI - neutral = false; - return true; - } - if (turret > 0) { // citizen-built / Alyx-hacked - neutral = false; - return true; - } - if (rel == 4) { // D_NU - neutral = true; - return false; - } - // NPC_STATE_SCRIPT = 4: cutscene actors and scripted pets are passive even - // when their class relationship says hate (Lamarr, scripted Combine, ...). - if (state == 4) { - neutral = true; - return false; - } - if (rel == 1 || rel == 2) { // D_HT / D_FR - neutral = false; - return false; - } - if (turret >= 0) { // ordinary floor turret - neutral = false; - return false; - } - // Bugbait-controlled antlions follow a target even when their default - // relationship was never rewritten. - if (model && strstr(model, "antlion") && - server_npc_has_follow_target(idx)) { - neutral = false; - return true; - } - bool friendly = model_is_friendly(model); - neutral = model_is_neutral(model); - return friendly; -} - -// Friendly/neutral state changes slowly (hacked turrets, bugbait), while the -// entity scan and the chams hook ask for it constantly. Cache it briefly so the -// server lookups happen once per NPC per window instead of per call. -static bool npc_is_friendly(int idx, const char *model, int local_idx, - bool &neutral) { - struct Cache { - int idx; - double at; - bool friendly; - bool neutral; - }; - static Cache cache[256]; - double now = esp_now(); - Cache &c = cache[(unsigned)idx & 255]; - if (c.idx == idx && now - c.at < 0.10) { - neutral = c.neutral; - return c.friendly; - } - bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral); - c = {idx, now, f, neutral}; - return f; -} - -enum NpcDamageClass { - NPC_DMG_NORMAL = 0, - NPC_DMG_ARMORED, // only blast damage (RPG) hurts it - NPC_DMG_VULNERABLE, // gravity gun / physics damage works well - NPC_DMG_IMMUNE // m_takedamage says no damage at all -}; - -static bool model_has(const char *model, const char *needle) { - return model && strcasestr(model, needle) != nullptr; -} - -// Server m_takedamage first (authoritative immunity), then the HL2 damage -// rules for units whose OnTakeDamage filters specific damage types. -static int npc_damage_class_uncached(int idx, const char *model) { - int td = server_npc_takedamage(idx); - if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY - return NPC_DMG_IMMUNE; - if (model_has(model, "gunship") || model_has(model, "combine_strider") || - model_has(model, "dropship") || model_has(model, "helicopter") || - model_has(model, "chopper")) - return NPC_DMG_ARMORED; - if (model_has(model, "headcrab") || model_has(model, "antlion") || - model_has(model, "manhack")) - return NPC_DMG_VULNERABLE; - return NPC_DMG_NORMAL; -} - -// Damage class depends on m_takedamage (a server read) plus model string -// matching. It changes even more rarely than the relation, so cache it. -static int npc_damage_class(int idx, const char *model) { - struct Cache { - int idx; - double at; - int val; - }; - static Cache cache[256]; - double now = esp_now(); - Cache &c = cache[(unsigned)idx & 255]; - if (c.idx == idx && now - c.at < 0.25) - return c.val; - int v = npc_damage_class_uncached(idx, model); - c = {idx, now, v}; - return v; -} - -bool active_weapon_contains(void *local_ent, const char *needle); - -// Armoured units need blast damage; the RPG is the only stock weapon that -// passes their OnTakeDamage filters. Immune units can never be damaged. -static bool npc_can_be_damaged(int idx, const char *model) { - int dc = npc_damage_class(idx, model); - if (dc == NPC_DMG_IMMUNE) - return false; - if (dc != NPC_DMG_ARMORED) - return true; - int local = engine_get_local_player(); - void *le = local > 0 ? entitylist_get(local) : nullptr; - return active_weapon_contains(le, "rocket_launcher") || - active_weapon_contains(le, "rpg"); -} - -static void short_name(const char *model, char *out, size_t outn) { - if (!model) { snprintf(out, outn, "NPC"); return; } - const char *base = strrchr(model, '/'); - base = base ? base + 1 : model; - size_t n = strcspn(base, "."); - if (n >= outn) n = outn - 1; - memcpy(out, base, n); - out[n] = 0; -} - -// Enemy weapon label. NPC weapons are server entities, so read the server -// model index first and fall back to the client class name. -static void weapon_short_name(int idx, char *out, size_t outn) { - out[0] = 0; - int wi = server_active_weapon(idx); - if (wi <= 0) - return; - const char *name = nullptr; - void *base = server_entity_base(wi); - int mi = base ? server_model_index(base) : -1; - if (mi > 0) - name = model_name_from_index(mi); - if (!name) { - void *wep = entitylist_get(wi); - if (wep) { - void *net = entity_networkable(wi); - name = entity_class_name(net, nullptr); - } - } - if (!name) - name = server_entity_classname(wi); - if (!name) - return; - static const struct { - const char *needle; - const char *label; - } map[] = { - {"smg1", "SMG1"}, {"irifle", "AR2"}, {"ar2", "AR2"}, - {"shotgun", "Shotgun"}, {"pistol", "Pistol"}, {"357", "357"}, - {"crossbow", "Crossbow"}, {"rocket", "RPG"}, {"rpg", "RPG"}, - {"grenade", "Grenade"}, {"frag", "Grenade"}, {"crowbar", "Crowbar"}, - {"physcannon", "Gravity"}, {"slam", "SLAM"}, {"bugbait", "Bugbait"}, - {"manhack", "Manhack"}, {"turret", "Turret"}, {"annabelle", "Rifle"}, - {"alyxgun", "Alyx gun"}, {"sniper", "Sniper"}, {"smg", "SMG"}, - }; - for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { - if (strcasestr(name, map[i].needle)) { - snprintf(out, outn, "%s", map[i].label); - return; - } - } - // Last resort: the short class/model name. - short_name(name, out, outn); -} - -// Hostile / neutral / friendly colours come from the ESP colour pickers. static void relation_rgb(const EspTarget &t, int &r, int &g, int &b) { const int *c = t.friendly ? g_esp_col_friendly : t.neutral ? g_esp_col_neutral @@ -971,7 +517,7 @@ static void draw_skeleton(const EspTarget &t) { // MAX_STEP degrees toward the target and stops inside DEADZONE. // --------------------------------------------------------------------------- -static QAngle angle_to(const Vector &from, const Vector &to) { +QAngle angle_to(const Vector &from, const Vector &to) { float dx = to.x - from.x, dy = to.y - from.y, dz = to.z - from.z; float horiz = sqrtf(dx * dx + dy * dy); if (horiz < 0.001f) @@ -985,7 +531,7 @@ static QAngle angle_to(const Vector &from, const Vector &to) { // Server bullet direction adds m_Local.m_vecPunchAngle to the usercmd view // (GetAutoaimVector), so aim angles have to be pre-subtracted by it. -static bool punch_angles(float &px, float &py) { +bool punch_angles(float &px, float &py) { px = py = 0.0f; if (!aim_punch_enabled() || g_net.punch < 0) return false; @@ -1001,1065 +547,6 @@ static bool punch_angles(float &px, float &py) { return std::isfinite(px) && std::isfinite(py); } -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; -} // Dynamic props are tinted through their client render colour. StudioRender // feeds m_clrRender into the per-model colour modulation each draw, which is -- cgit v1.2.3