// esp.cpp — ESP drawing + shared world_to_screen / entity gather (aimbot reuses these) #include "esp_internal.h" #include "hl2h.h" #include #include #include #include #include #include enum { LIFE_ALIVE = 0 }; struct EspTarget { int idx; float sx, sy; // screen pos of feet float h; // box height px float health, maxhealth; bool show_health; bool friendly; bool neutral; int damage; // NpcDamageClass int state; // server NPC_STATE_* char weapon[24]; char name[48]; }; struct ArrowTarget { Vector head; bool neutral; }; static ArrowTarget g_arrows[32]; static int g_narrows = 0; static EspTarget g_targets[128]; 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; static float g_speed_samples[SPEED_SAMPLES] = {}; static int g_speed_head = 0; static double g_speed_last_sample = 0.0; double esp_now() { timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; } // Refresh the cached screen size every frame. It used to be read once, so a // resolution or window change left every projection, screen-edge clamp and // centering calculation stale for the rest of the session. bool esp_update_screen() { if (!g_surface) return false; int w = 0, h = 0; overlay_screen_size(w, h); if (w > 0) g_scr_w = w; if (h > 0) g_scr_h = h; return g_scr_w > 0 && g_scr_h > 0; } // Local perspective projection used when the engine's debug-overlay // projection refuses a point that is actually in front of the camera. static bool project_manual(const Vector &w, float &sx, float &sy) { if (!g_scr_w || !g_scr_h) return false; float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; float roll = g_cam_view.z * PI_F / 180.0f; float sp = sinf(pitch), cp = cosf(pitch); float syy = sinf(yaw), cy = cosf(yaw); float sr = sinf(roll), cr = cosf(roll); float fwd[3] = {cp * cy, cp * syy, -sp}; // Source's AngleVectors with roll; the old right/up vectors assumed roll 0, // so view punch made the fallback projection rotate against the world. float right[3] = {-sr * sp * cy + cr * syy, -sr * sp * syy - cr * cy, -sr * cp}; float up[3] = {cr * sp * cy + sr * syy, cr * sp * syy - sr * cy, cr * cp}; float dx = w.x - g_cam_eye.x, dy = w.y - g_cam_eye.y, dz = w.z - g_cam_eye.z; float z = dx * fwd[0] + dy * fwd[1] + dz * fwd[2]; if (z < 1.0f) return false; float x = dx * right[0] + dy * right[1] + dz * right[2]; float y = dx * up[0] + dy * up[1] + dz * up[2]; float fov = g_fov_camera > 10 ? (float)g_fov_camera : 75.0f; // Source defines the FOV for a 4:3 reference and widens it on widescreen; // using a raw tan(fov/2) made the fallback ~33% too narrow at 16:9. float aspect = (float)g_scr_w / (float)g_scr_h; float tanfov = tanf(fov * 0.5f * PI_F / 180.0f) * (aspect / (4.0f / 3.0f)); sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov); sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov); return std::isfinite(sx) && std::isfinite(sy) && fabsf(sx) < 1e7f && fabsf(sy) < 1e7f; } bool world_to_screen(const Vector &w, float &sx, float &sy) { // Preferred: the engine's own transform. Uses the real view setup (FOV, // aspect, roll), so boxes do not drift at the screen edges. if (g_iface.debugoverlay) { using Fn = int (*)(void *, const Vector *, Vector *); Vector s; int behind = vfunc(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s); if (behind) { // The overlay transform can report "behind" for points that are in // front but near the view axis/extremes (e.g. something directly below // the player). Use the local projection in that case. float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; float cp = cosf(pitch), sp = sinf(pitch); float fwd[3] = {cp * cosf(yaw), cp * sinf(yaw), -sp}; float z = (w.x - g_cam_eye.x) * fwd[0] + (w.y - g_cam_eye.y) * fwd[1] + (w.z - g_cam_eye.z) * fwd[2]; if (z > 4.0f) return project_manual(w, sx, sy); return false; } // A point on the view axis can produce NaN/inf or enormous coordinates; // drawing those as ints corrupts the surface clip state and hides the // rest of the overlay. if (!std::isfinite(s.x) || !std::isfinite(s.y) || fabsf(s.x) > 1e7f || fabsf(s.y) > 1e7f) return false; sx = s.x; sy = s.y; return true; } return project_manual(w, sx, sy); } // --------------------------------------------------------------------------- // entity helpers (IClientEntityList / IClientNetworkable, header-documented) // --------------------------------------------------------------------------- using GetNetFn = void *(*)(void *, int); using GetCCFn = void *(*)(void *); using GetModelFn = const void *(*)(void *, int); 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. static 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 : g_esp_col_hostile; r = c[0]; g = c[1]; b = c[2]; } static void esp_collect(int local_idx) { // outer_draw() runs once per PaintTraverseEx call (several per frame); cache // the scan briefly so we do not walk the entity list dozens of times a frame. static double last_scan = 0.0; static double last_dbg = 0.0; double now = esp_now(); if (now - last_scan < 0.01) return; last_scan = now; bool dbg = (now - last_dbg) >= 2.0; if (dbg) last_dbg = now; g_ntargets = 0; g_narrows = 0; using Fn = int (*)(void *); int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); // GetHighestEntityIndex if (hi < 0 || hi > 4096) hi = 4096; int npc_alive = 0, dbg_lines = 0; bool srv = resolve_server_netvars(); for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) { if (idx == local_idx) continue; void *ent = entitylist_get(idx); if (!ent) continue; entptr_store(ent, idx); void *net = entity_networkable(idx); if (!net || entity_is_dormant(net)) continue; const char *cls = entity_class_name(net, nullptr); if (!cls) continue; uintptr_t e = (uintptr_t)ent; int cli_life = g_net.lifestate >= 0 ? (int)*(unsigned char *)(e + g_net.lifestate) : -1; int cli_hp = g_net.health >= 0 ? *(int *)(e + g_net.health) : 0; bool npc = is_npc_class(cls); const char *model = entity_model_name(ent); // NPC health is not networked; read it from the listen-server entity. int hp = cli_hp, maxhp = -1, life = cli_life; if (npc && srv) { int slife = -1, smax = -1; void *sbase = nullptr; int shp = server_entity_health(idx, &smax, &slife, &sbase); if (!g_srv_checked && sbase) { int cli_mi = g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1; int srv_mi = server_model_index(sbase); // Wait for both indices: the client index is often 0 for a tick right // after a map load, which used to latch "untrusted" permanently. if (cli_mi > 0 && srv_mi > 0) { g_srv_checked = true; g_srv_trusted = (cli_mi == srv_mi); logd("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d " "hp=%d maxhp=%d life=%d trusted=%d", idx, sbase, cli_mi, srv_mi, shp, smax, slife, (int)g_srv_trusted); if (!g_srv_trusted) logf("ESP: server entity mismatch, ignoring server values"); } } if (g_srv_trusted) { if (shp >= 0) hp = shp; if (smax > 0) maxhp = smax; if (slife >= 0) life = slife; } } if (dbg && npc && dbg_lines < 40) { dbg_lines++; logd("ESP npc %d cls=%s life=%d(cli %d) hp=%d(cli %d) maxhp=%d model=%s", idx, cls, life, cli_life, hp, cli_hp, maxhp, model ? model : "?"); } if (npc) { if (life != LIFE_ALIVE) continue; npc_alive++; } else { // players (HL2DM) are the only entities with a live networked m_iHealth if (!strstr(cls, "Player") || !(hp > 0)) continue; } float minz = g_net.mins >= 0 ? *(float *)(e + g_net.mins + 8) : 0.0f; float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; if (!(maxz > minz + 4.0f && maxz - minz < 512.0f)) { minz = 0.0f; maxz = 72.0f; } Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), *(float *)(e + g_net.origin + 8)}; bool neutral = false; bool friendly = npc && npc_is_friendly(idx, model, local_idx, neutral); // Arrows are collected before projection: a hostile behind the camera // never produces on-screen box coordinates. if (npc && g_narrows < 32 && !friendly && (!neutral || esp_option(ESP_OPT_SHOW_NEUTRAL))) { ArrowTarget &a = g_arrows[g_narrows++]; a.head = Vector{origin.x, origin.y, origin.z + maxz * 0.7f}; a.neutral = neutral; } // Project several points along the vertical axis. Requiring both feet and // head to project dropped the target when the feet were behind the near // plane (common when standing over/next to something). float min_sy = 1e9f, max_sy = -1e9f, low_x = 0.0f; bool projected = false; const int VSAMPLES = 6; for (int k = 0; k <= VSAMPLES; k++) { float z = origin.z + minz + (maxz - minz) * (float)k / (float)VSAMPLES; float px, py; if (!world_to_screen(Vector{origin.x, origin.y, z}, px, py)) continue; if (px < -2.0f * (float)g_scr_w || px > 3.0f * (float)g_scr_w) continue; projected = true; if (py < min_sy) min_sy = py; if (py > max_sy) { max_sy = py; low_x = px; } } if (!projected) { static int fail_logs = 0; float ddx = origin.x - g_cam_eye.x; float ddy = origin.y - g_cam_eye.y; float ddz = origin.z - g_cam_eye.z; if (fail_logs < 8 && ddx * ddx + ddy * ddy + ddz * ddz < 400.0f * 400.0f) { fail_logs++; logf("ESP: close target %d model=%s failed projection at " "(%.1f %.1f %.1f)", idx, model ? model : "?", origin.x, origin.y, origin.z); } continue; } float h = max_sy - min_sy; if (h < 3.0f || max_sy < 0 || min_sy > (float)g_scr_h) continue; // behind / offscreen if (low_x < -(float)g_scr_w || low_x > 2.0f * (float)g_scr_w) continue; float sx = low_x, sy = max_sy; EspTarget &t = g_targets[g_ntargets++]; t.idx = idx; t.sx = sx; t.sy = sy; t.h = h; t.health = hp > 0 ? (float)hp : 0.0f; t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; t.show_health = hp > 0; t.friendly = friendly; t.neutral = neutral; t.damage = npc_damage_class(idx, model); t.state = npc ? server_npc_state(idx) : -1; t.weapon[0] = 0; if (npc) weapon_short_name(idx, t.weapon, sizeof(t.weapon)); short_name(model, t.name, sizeof(t.name)); if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); } if (dbg) logd("ESP scan: hi=%d npc_alive=%d targets=%d", hi, npc_alive, g_ntargets); if (g_ntargets && !g_esp_logged) { g_esp_logged = true; logd("ESP: first targets, %d", g_ntargets); } } void surf_rect(int filled, int x0, int y0, int x1, int y1) { if (filled) overlay_fill(x0, y0, x1, y1); else overlay_outline_rect(x0, y0, x1, y1); } void surf_color(int r, int g, int b, int a) { overlay_color(r, g, b, a); } void surf_line(int x0, int y0, int x1, int y1) { overlay_line(x0, y0, x1, y1); } // Screen projection of the aimbot FOV cone boundary, using the engine's own // world->screen transform so it follows the actual aspect/fov. QAngle engine_view_angles() { QAngle v{g_cam_view.x, g_cam_view.y, g_cam_view.z}; if (g_iface.engine) { using Fn = void (*)(void *, QAngle *); vfunc(g_iface.engine, 19)(g_iface.engine, &v); } return v; } void esp_draw_fov_circle() { if (!g_surface || !aimbot_fov_circle()) return; // No local player means no game world (main menu); the cached camera is stale // and the projected circle would land at a meaningless spot. if (engine_get_local_player() <= 0) return; if (!esp_update_screen()) return; float fov = (float)aimbot_fov(); if (fov < 1.0f || fov >= 89.0f) // beyond this the "circle" is behind the eye return; float alpha = fov * PI_F / 180.0f; QAngle view = engine_view_angles(); float yaw = view.y * PI_F / 180.0f; float pitch = view.x * PI_F / 180.0f; Vector fwd{cosf(pitch) * cosf(yaw), cosf(pitch) * sinf(yaw), -sinf(pitch)}; Vector right{sinf(yaw), -cosf(yaw), 0.0f}; Vector up{right.y * fwd.z - right.z * fwd.y, right.z * fwd.x - right.x * fwd.z, right.x * fwd.y - right.y * fwd.x}; constexpr float D = 1024.0f; float ca = cosf(alpha), sa = sinf(alpha); Vector eh{g_cam_eye.x + (fwd.x * ca + right.x * sa) * D, g_cam_eye.y + (fwd.y * ca + right.y * sa) * D, g_cam_eye.z + (fwd.z * ca + right.z * sa) * D}; Vector ev{g_cam_eye.x + (fwd.x * ca + up.x * sa) * D, g_cam_eye.y + (fwd.y * ca + up.y * sa) * D, g_cam_eye.z + (fwd.z * ca + up.z * sa) * D}; float hsx, hsy, vsx, vsy; if (!world_to_screen(eh, hsx, hsy) || !world_to_screen(ev, vsx, vsy)) return; float cx = (float)g_scr_w * 0.5f, cy = (float)g_scr_h * 0.5f; float rx = fabsf(hsx - cx), ry = fabsf(vsy - cy); if (!(rx > 1.0f && ry > 1.0f) || rx > 1e5f || ry > 1e5f) return; constexpr int N = 64; int px = 0, py = 0; surf_color(255, 255, 255, 160); for (int i = 0; i <= N; i++) { float phi = (2.0f * PI_F * i) / N; int x = (int)(cx + rx * cosf(phi)); int y = (int)(cy + ry * sinf(phi)); if (i > 0) surf_line(px, py, x, y); px = x; py = y; } } // Speed graph: 100 samples at 0.1 s = the last 10 seconds; min is 0, max is // the window maximum. Small and centered in the lower half of the screen. static void speed_graph_sample() { // Only while actually playing: IVEngineClient013 slot 26 IsInGame, // slot 84 IsPaused. if (g_iface.engine) { using BoolFn = bool (*)(void *); if (!vfunc(g_iface.engine, 26)(g_iface.engine)) return; if (vfunc(g_iface.engine, 84)(g_iface.engine)) return; } double now = esp_now(); if (now - g_speed_last_sample < 0.1) return; g_speed_last_sample = now; float speed = 0.0f; int local = engine_get_local_player(); if (local > 0 && g_net.velocity >= 0) { void *ent = entitylist_get(local); if (ent) { const float *v = (const float *)((uintptr_t)ent + g_net.velocity); speed = sqrtf(v[0] * v[0] + v[1] * v[1]); } } g_speed_samples[g_speed_head] = speed; g_speed_head = (g_speed_head + 1) % SPEED_SAMPLES; } static void draw_speed_graph() { if (!esp_option(ESP_OPT_SHOW_SPEEDGRAPH)) return; speed_graph_sample(); float maxv = 0.0f; for (int i = 0; i < SPEED_SAMPLES; i++) if (g_speed_samples[i] > maxv) maxv = g_speed_samples[i]; float scale = maxv > 1.0f ? maxv : 1.0f; const int w = 240, h = 64; int x = (g_scr_w - w) / 2; int y = (g_scr_h * 9) / 10 - h / 2; if (x < 0) x = 0; if (y < 0) y = 0; surf_color(0, 0, 0, 120); surf_rect(1, x, y, x + w, y + h); surf_color(80, 80, 80, 180); surf_rect(0, x, y, x + w, y + h); surf_color(0, 255, 200, 220); int px = 0, py = 0; for (int i = 0; i < SPEED_SAMPLES; i++) { int idx = (g_speed_head + i) % SPEED_SAMPLES; // oldest -> newest float v = g_speed_samples[idx]; int sx = x + 1 + (i * (w - 2)) / (SPEED_SAMPLES - 1); int sy = y + h - 2 - (int)((v / scale) * (float)(h - 4)); if (sy < y + 1) sy = y + 1; if (i > 0) surf_line(px, py, sx, sy); px = sx; py = sy; } char label[32]; snprintf(label, sizeof(label), "max %d", (int)maxv); draw_text_col(x + 3, y + 2, label, 200, 200, 200); int cur_idx = (g_speed_head + SPEED_SAMPLES - 1) % SPEED_SAMPLES; char cur[16]; snprintf(cur, sizeof(cur), "%d", (int)g_speed_samples[cur_idx]); int cur_w = (int)strlen(cur) * 8; draw_text_col(x + w - 4 - cur_w, y + 2, cur, 255, 255, 255); } // Draw the parent->child bone links. Bone snapshots are world-space matrices // from SetupBones; project each bone once and reuse for both endpoints. static void draw_skeleton(const EspTarget &t) { int n = bones_count(t.idx); if (n <= 0 || n > 256) return; Vector wp[256]; float xs[256], ys[256]; bool ok[256]; int core_count = 0; for (int i = 0; i < n; i++) { ok[i] = false; if (!bone_is_skeleton_core(t.idx, i)) continue; core_count++; ok[i] = bone_pos(t.idx, i, wp[i]) && world_to_screen(wp[i], xs[i], ys[i]) && std::isfinite(xs[i]) && std::isfinite(ys[i]); } static int logged = 0; if (core_count > 0 && logged < 4) { logged++; logd("skeleton: idx=%d bones=%d core=%d humanoid=%d", t.idx, n, core_count, (int)bones_is_humanoid(t.idx)); } int sr, sg, sb; relation_rgb(t, sr, sg, sb); surf_color(sr, sg, sb, 255); for (int i = 0; i < n; i++) { if (!ok[i]) continue; int p = bone_nearest_core_parent(t.idx, i); if (p >= 0 && p < n && ok[p]) surf_line((int)xs[p], (int)ys[p], (int)xs[i], (int)ys[i]); } } // --------------------------------------------------------------------------- // aimbot v1: nearest hostile NPC head to the crosshair. // Bounded smoothing: never snap, never overshoot. Each tick moves at most // MAX_STEP degrees toward the target and stops inside DEADZONE. // --------------------------------------------------------------------------- static 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) horiz = 0.001f; QAngle a{}; a.x = -atan2f(dz, horiz) * 180.0f / PI_F; // Source: positive pitch = down a.y = atan2f(dy, dx) * 180.0f / PI_F; a.z = 0.0f; return a; } // 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) { px = py = 0.0f; if (!aim_punch_enabled() || g_net.punch < 0) return false; int local = engine_get_local_player(); if (local <= 0) return false; void *le = entitylist_get(local); if (!le) return false; const float *p = (const float *)((uintptr_t)le + g_net.punch); px = p[0]; py = p[1]; 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 // why tinting the prop material alone had no effect. (The IStudioRender hook // experiment corrupted materials and crashed; see studio.cpp.) void prop_tint_update() { static bool applied = false; static bool logged_missing = false; static bool logged_touched = false; bool on = feature_active(FEAT_WORLD_TINT) && g_tint_prop_on; if (on) studio_props_init(); if (!on && !applied) return; if (!g_iface.entitylist || g_net.clr_render < 0) { if (on && !logged_missing) { logged_missing = true; logf("prop tint: m_clrRender unavailable"); } return; } static double last = 0.0; double now = esp_now(); if (on && now - last < 0.01) return; last = now; using Fn = int (*)(void *); int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); if (hi < 0 || hi > 4096) hi = 4096; uint32_t col = on ? ((uint32_t)g_tint_prop_rgb[0] | ((uint32_t)g_tint_prop_rgb[1] << 8) | ((uint32_t)g_tint_prop_rgb[2] << 16) | (255u << 24)) : 0xFFFFFFFFu; int touched = 0; for (int idx = 1; idx <= hi; idx++) { void *ent = entitylist_get(idx); if (!ent) continue; const char *model = entity_model_name(ent); if (!model) continue; bool prop = strncmp(model, "models/props", 12) == 0 || strncmp(model, "props_", 6) == 0; if (!prop) continue; *(uint32_t *)((uintptr_t)ent + (uintptr_t)g_net.clr_render) = col; touched++; } applied = on; if (on && touched > 0 && !logged_touched) { logged_touched = true; logf("prop tint: %d prop entities", touched); } } // Small left-edge label, stacked above the box (10 px font + 1 px shadow). static void draw_left_tag(int left, int y, const char *text, int r, int g, int b) { int tw = (int)strlen(text) * 6; int tx = left - tw - 3; if (tx < 2) tx = 2; draw_text_small_col(tx + 1, y + 1, text, 0, 0, 0); draw_text_small_col(tx, y, text, r, g, b); } static const char *alert_tag(int state, int &r, int &g, int &b) { switch (state) { case 1: r = 170; g = 180; b = 190; return "IDLE"; case 2: r = 255; g = 220; b = 80; return "ALERT"; case 3: r = 255; g = 90; b = 70; return "COMBAT"; case 4: r = 150; g = 200; b = 255; return "SCRIPT"; default: return nullptr; } } // Direction markers for hostiles outside the view. Drawn at the screen edge // using the engine projection when the target is in front, and the view-right // dot product when it is behind. static void draw_edge_arrows() { if (!g_surface || !g_scr_w || !g_narrows || !esp_option(ESP_OPT_ARROWS)) return; float cx = (float)g_scr_w * 0.5f, cy = (float)g_scr_h * 0.5f; const float margin = 26.0f; int drawn = 0; for (int i = 0; i < g_narrows && drawn < 12; i++) { const ArrowTarget &a = g_arrows[i]; float sx, sy; bool proj = world_to_screen(a.head, sx, sy); if (proj && sx >= 4.0f && sx <= (float)g_scr_w - 4.0f && sy >= 4.0f && sy <= (float)g_scr_h - 4.0f) continue; // already visible as a box float dx, dy; if (proj) { dx = sx - cx; dy = sy - cy; } else { float yaw = g_cam_view.y * PI_F / 180.0f; float wx = a.head.x - g_cam_eye.x; float wy = a.head.y - g_cam_eye.y; float right = wx * sinf(yaw) + wy * (-cosf(yaw)); dx = right >= 0.0f ? 1.0f : -1.0f; dy = 0.0f; } float len = sqrtf(dx * dx + dy * dy); if (len < 1e-3f) { dx = 0.0f; dy = 1.0f; len = 1.0f; } dx /= len; dy /= len; float half_w = cx - margin, half_h = cy - margin; float tx = fabsf(dx) > 1e-4f ? half_w / fabsf(dx) : 1e9f; float ty = fabsf(dy) > 1e-4f ? half_h / fabsf(dy) : 1e9f; float d = tx < ty ? tx : ty; float ex = cx + dx * d, ey = cy + dy * d; float px = -dy, py = dx; const int *col = a.neutral ? g_esp_col_neutral : g_esp_col_hostile; surf_color(col[0], col[1], col[2], 235); surf_line((int)ex, (int)ey, (int)(ex - dx * 13.0f + px * 6.0f), (int)(ey - dy * 13.0f + py * 6.0f)); surf_line((int)ex, (int)ey, (int)(ex - dx * 13.0f - px * 6.0f), (int)(ey - dy * 13.0f - py * 6.0f)); drawn++; } } // Map transition triggers are server-only brush entities. Draw their world // bounds and the target map; the previous map is remembered across level // changes from the worldspawn model name. struct TransitionBox { char next[128]; Vector mins, maxs; }; static TransitionBox g_trans[8]; static int g_ntrans = 0; static char g_map_last[64]; static char g_map_prev[64]; static void map_name_from_world(char *out, size_t outn) { out[0] = 0; void *w = entitylist_get(0); const char *m = w ? entity_model_name(w) : nullptr; if (!m) return; const char *b = strrchr(m, '/'); b = b ? b + 1 : m; size_t n = strcspn(b, "."); if (n >= outn) n = outn - 1; memcpy(out, b, n); out[n] = 0; } static void transitions_scan() { char cur[64]; map_name_from_world(cur, sizeof(cur)); if (strcmp(cur, g_map_last) != 0) { if (g_map_last[0]) snprintf(g_map_prev, sizeof(g_map_prev), "%s", g_map_last); snprintf(g_map_last, sizeof(g_map_last), "%s", cur); } int count = server_entity_count(); int n = 0; for (int i = 1; i <= count && n < 8; i++) { TransitionBox &b = g_trans[n]; b.mins = Vector{0, 0, 0}; b.maxs = Vector{0, 0, 0}; if (!server_trigger_info(i, b.next, sizeof(b.next), b.mins, b.maxs)) continue; if (b.mins.x == b.maxs.x && b.mins.y == b.maxs.y && b.mins.z == b.maxs.z) continue; n++; } g_ntrans = n; static int logged = 0; if (n > 0 && logged < 4) { logged++; logf("transitions: %d changelevel trigger(s), next=%s prev=%s", n, g_trans[0].next, g_map_prev[0] ? g_map_prev : "?"); } } // Clip a 2D segment to the screen (with a small margin). Liang-Barsky. static bool clip_line2d(float &x0, float &y0, float &x1, float &y1, float xmin, float ymin, float xmax, float ymax) { float dx = x1 - x0, dy = y1 - y0; float p[4] = {-dx, dx, -dy, dy}; float q[4] = {x0 - xmin, xmax - x0, y0 - ymin, ymax - y0}; float u1 = 0.0f, u2 = 1.0f; for (int i = 0; i < 4; i++) { if (fabsf(p[i]) < 1e-6f) { if (q[i] < 0.0f) return false; continue; } float r = q[i] / p[i]; if (p[i] < 0.0f) { if (r > u2) return false; if (r > u1) u1 = r; } else { if (r < u1) return false; if (r < u2) u2 = r; } } float nx0 = x0 + u1 * dx, ny0 = y0 + u1 * dy; float nx1 = x0 + u2 * dx, ny1 = y0 + u2 * dy; x0 = nx0; y0 = ny0; x1 = nx1; y1 = ny1; return true; } // Project a world-space segment, clipping it at the near plane first. Without // this an edge with one corner behind the camera was dropped entirely, so a // partially visible transition box or radius ring disappeared. bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0, int &x1, int &y1) { if (!g_scr_w || !g_scr_h) return false; float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; float cp = cosf(pitch), sp = sinf(pitch); float cy = cosf(yaw), sy = sinf(yaw); Vector fwd{cp * cy, cp * sy, -sp}; Vector da{a.x - g_cam_eye.x, a.y - g_cam_eye.y, a.z - g_cam_eye.z}; Vector db{b.x - g_cam_eye.x, b.y - g_cam_eye.y, b.z - g_cam_eye.z}; const float nearz = 5.0f; float za = da.x * fwd.x + da.y * fwd.y + da.z * fwd.z; float zb = db.x * fwd.x + db.y * fwd.y + db.z * fwd.z; if (za <= nearz && zb <= nearz) return false; Vector ca = a, cb = b; if (za <= nearz) { float t = (nearz - za) / (zb - za); ca = Vector{a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t}; } else if (zb <= nearz) { float t = (nearz - zb) / (za - zb); cb = Vector{b.x + (a.x - b.x) * t, b.y + (a.y - b.y) * t, b.z + (a.z - b.z) * t}; } float ax, ay, bx, by; if (!world_to_screen(ca, ax, ay) && !project_manual(ca, ax, ay)) return false; if (!world_to_screen(cb, bx, by) && !project_manual(cb, bx, by)) return false; if (!clip_line2d(ax, ay, bx, by, -8.0f, -8.0f, (float)g_scr_w + 8.0f, (float)g_scr_h + 8.0f)) return false; x0 = (int)ax; y0 = (int)ay; x1 = (int)bx; y1 = (int)by; return true; } static void draw_transitions() { if (!g_surface || !g_ntrans) return; // Corner bits: +x, +y, +z. Twelve edges connect corners differing by one // bit; projecting the corners and joining them gives a proper 3D wireframe // instead of a screen-space rectangle. static const int edges[12][2] = { {0, 1}, {2, 3}, {4, 5}, {6, 7}, {0, 2}, {1, 3}, {4, 6}, {5, 7}, {0, 4}, {1, 5}, {2, 6}, {3, 7}}; for (int i = 0; i < g_ntrans; i++) { const TransitionBox &b = g_trans[i]; Vector corners[8]; float xs[8], ys[8]; bool ok[8]; int seen = 0; for (int c = 0; c < 8; c++) { corners[c] = Vector{(c & 1) ? b.maxs.x : b.mins.x, (c & 2) ? b.maxs.y : b.mins.y, (c & 4) ? b.maxs.z : b.mins.z}; ok[c] = world_to_screen(corners[c], xs[c], ys[c]) && std::isfinite(xs[c]) && std::isfinite(ys[c]); if (ok[c]) seen++; } if (!seen) continue; // The trigger whose target is the map we came from is the way back. bool back = g_map_prev[0] && strcmp(b.next, g_map_prev) == 0; int r = back ? 200 : 255, g = back ? 200 : 170, bl = back ? 200 : 60; surf_color(r, g, bl, 210); for (int e = 0; e < 12; e++) { int a = edges[e][0], c = edges[e][1]; int lx, ly, hx, hy; if (project_segment(corners[a], corners[c], lx, ly, hx, hy)) surf_line(lx, ly, hx, hy); } // Tag above the highest projected corner. float topx = 0.0f, topy = 1e9f; for (int c = 0; c < 8; c++) { if (ok[c] && ys[c] < topy) { topy = ys[c]; topx = xs[c]; } } char line[128]; snprintf(line, sizeof(line), "%s %.100s", back ? "<-" : "->", b.next); int tx = (int)topx; if (tx < 4) tx = 4; if (tx > g_scr_w - 120) tx = g_scr_w - 120; int ty = (int)topy - 16; if (ty < 4) ty = 4; draw_text_small_col(tx + 1, ty + 1, line, 0, 0, 0); draw_text_small_col(tx, ty, line, r, g, bl); } } void esp_draw() { if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return; if (!esp_update_screen()) return; esp_collect(engine_get_local_player()); static double last_trans = 0.0; double trans_now = esp_now(); if (trans_now - last_trans >= 0.25) { last_trans = trans_now; transitions_scan(); } draw_transitions(); for (int i = 0; i < g_ntargets; i++) { const EspTarget &t = g_targets[i]; if (t.neutral && !esp_option(ESP_OPT_SHOW_NEUTRAL)) continue; if (esp_option(ESP_OPT_SHOW_SKELETON)) { void *ent = entitylist_get(t.idx); if (ent && bones_update(t.idx, ent)) draw_skeleton(t); } int cr, cg, cb; relation_rgb(t, cr, cg, cb); surf_color(cr, cg, cb, 255); float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width int left = (int)(t.sx - w / 2); int right = (int)(t.sx + w / 2); int top = (int)(t.sy - t.h); int bot = (int)t.sy; if (esp_option(ESP_OPT_SHOW_BOX)) surf_rect(0, left, top, right, bot); bool draw_bar = t.show_health && esp_option(ESP_OPT_SHOW_HEALTHBAR); bool draw_num = t.show_health && esp_option(ESP_OPT_SHOW_HEALTHNUM); int num_x = right + 3; if (draw_bar) { float frac = t.health / (t.maxhealth > 1.0f ? t.maxhealth : 100.0f); if (frac > 1) frac = 1; if (frac < 0) frac = 0; // vertical bar just right of the box, HP number beside it int bx0 = right + 2, bx1 = right + 5; surf_color(0, 0, 0, 200); surf_rect(1, bx0, top, bx1, bot); int fill_top = bot - (int)((float)(bot - top) * frac); if (fill_top < top) fill_top = top; if (frac > 0.5f) surf_color(40, 255, 40, 255); else if (frac > 0.25f) surf_color(255, 255, 0, 255); else surf_color(255, 40, 40, 255); if (fill_top < bot) surf_rect(1, bx0, fill_top, bx1, bot); num_x = bx1 + 3; } if (draw_num) { char hptext[16]; snprintf(hptext, sizeof(hptext), "%d", (int)t.health); draw_text_col(num_x, top, hptext, 255, 255, 255); } if (esp_option(ESP_OPT_SHOW_NAME)) draw_text_col(left, top - 14, t.name, cr, cg, cb); int tag_y = top + 2; if (esp_option(ESP_OPT_DAMAGE_TAGS)) { const char *tag = nullptr; int tr = 255, tg = 255, tb = 255; if (t.damage == NPC_DMG_ARMORED) { tag = "ARM"; tr = 255; tg = 170; tb = 60; } else if (t.damage == NPC_DMG_VULNERABLE) { tag = "VULN"; tr = 90; tg = 220; tb = 255; } else if (t.damage == NPC_DMG_IMMUNE) { tag = "IMM"; tr = 180; tg = 180; tb = 180; } if (tag) { draw_left_tag(left, tag_y, tag, tr, tg, tb); tag_y += 12; } } if (esp_option(ESP_OPT_ALERT)) { int ar, ag, ab; const char *tag = alert_tag(t.state, ar, ag, ab); if (tag) { draw_left_tag(left, tag_y, tag, ar, ag, ab); tag_y += 12; } } if (esp_option(ESP_OPT_WEAPON) && t.weapon[0]) { draw_left_tag(left, tag_y, t.weapon, 220, 220, 220); tag_y += 12; } } draw_edge_arrows(); // Groundwork probe: HL2H_BONE_DEBUG=1 draws a white marker at each NPC's // "head" hitbox center (from SetupBones + studio hitboxes). static bool bone_debug = getenv("HL2H_BONE_DEBUG") != nullptr; if (bone_debug) { static bool head_logged = false; for (int i = 0; i < g_ntargets; i++) { const EspTarget &t = g_targets[i]; void *ent = entitylist_get(t.idx); if (!ent || !bones_update(t.idx, ent)) continue; Vector head; if (!head_position(t.idx, head)) { static int no_head = 0; if (no_head < 4) { no_head++; logd("head: idx=%d no head hitbox/bone", t.idx); } continue; } float hx, hy; if (!world_to_screen(head, hx, hy)) continue; surf_color(255, 255, 255, 255); surf_rect(1, (int)hx - 2, (int)hy - 2, (int)hx + 3, (int)hy + 3); if (!head_logged) { head_logged = true; logd("bone debug: idx=%d bones=%d head=(%.1f %.1f %.1f)", t.idx, bones_count(t.idx), head.x, head.y, head.z); } } } draw_speed_graph(); } // --------------------------------------------------------------------------- // Valuable item tags // --------------------------------------------------------------------------- enum ItemKind { IK_HEALTH, IK_AMMO, IK_ARMOR, IK_WEAPON, IK_CRATE, IK_OTHER }; struct ItemTarget { int idx; int kind; int ammo_type; // m_iAmmo index for IK_AMMO, -1 otherwise Vector origin; char label[128]; }; static ItemTarget g_item_targets[192]; static int g_nitems = 0; static bool g_items_logged = false; static bool item_icontains(const char *hay, const char *needle) { if (!hay || !needle || !*needle) return false; size_t n = strlen(needle); for (const char *p = hay; *p; p++) if (strncasecmp(p, needle, n) == 0) return true; return false; } static bool item_handle_valid(uint32_t h) { return h != 0 && h != 0xFFFFFFFFu; } // Weapons held by the player or an NPC are not world pickups. bool item_weapon_is_owned(void *ent) { if (!ent) return false; if (g_net.owner >= 0) { uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.owner); if (item_handle_valid(h)) return true; } if (g_net.weapon_owner >= 0) { uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner); if (item_handle_valid(h)) return true; } return false; } // Entity index of the weapon's owner, or -1. Used to distinguish the local // player's inventory from NPC weapons. static int item_weapon_owner_idx(void *ent) { if (!ent) return -1; uint32_t h = 0; if (g_net.owner >= 0) h = *(uint32_t *)((uintptr_t)ent + g_net.owner); if (!item_handle_valid(h) && g_net.weapon_owner >= 0) h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner); if (!item_handle_valid(h)) return -1; return (int)(h & 0x7FF); } static int local_active_weapon_idx(int local_idx) { if (local_idx <= 0 || g_net.activeweapon < 0) return -1; void *local = entitylist_get(local_idx); if (!local) return -1; uint32_t h = *(uint32_t *)((uintptr_t)local + g_net.activeweapon); if (!item_handle_valid(h)) return -1; return (int)(h & 0x7FF); } static int classify_weapon(const char *model, const char *cls, char *label, size_t outn) { if (!item_icontains(model, "models/weapons/w_") && !item_icontains(cls, "weapon")) return 0; if (item_icontains(model, "w_crowbar")) snprintf(label, outn, "Crowbar"); else if (item_icontains(model, "w_pistol")) snprintf(label, outn, "Pistol"); else if (item_icontains(model, "w_357")) snprintf(label, outn, "Revolver"); else if (item_icontains(model, "w_smg1") || item_icontains(model, "w_smg2")) snprintf(label, outn, "SMG"); else if (item_icontains(model, "w_ar2") || item_icontains(model, "w_irifle")) snprintf(label, outn, "AR2"); else if (item_icontains(model, "w_shotgun")) snprintf(label, outn, "Shotgun"); else if (item_icontains(model, "w_crossbow")) snprintf(label, outn, "Crossbow"); else if (item_icontains(model, "w_rocket_launcher")) snprintf(label, outn, "RPG"); else if (item_icontains(model, "w_frag") || item_icontains(model, "w_grenade")) snprintf(label, outn, "Grenade"); else if (item_icontains(model, "w_physcannon") || item_icontains(model, "w_physics")) snprintf(label, outn, "Gravity gun"); else if (item_icontains(model, "w_stunstick") || item_icontains(model, "w_stunbaton")) snprintf(label, outn, "Stunstick"); else if (item_icontains(model, "w_bugbait")) snprintf(label, outn, "Bugbait"); else if (item_icontains(model, "w_sniper")) snprintf(label, outn, "Sniper"); else if (item_icontains(model, "w_slam")) snprintf(label, outn, "SLAM"); else snprintf(label, outn, "%s", model); return 1; } // Weapon identity for the "owned weapon -> fallen weapon is ammo" rule. enum { WT_NONE = 0, WT_PISTOL, WT_357, WT_SMG, WT_AR2, WT_SHOTGUN, WT_CROSSBOW, WT_RPG, WT_GRENADE, WT_SNIPER, WT_COUNT }; static int weapon_type_id(const char *model, const char *cls) { if (item_icontains(model, "pistol") || item_icontains(cls, "pistol")) return WT_PISTOL; if (item_icontains(model, "357") || item_icontains(cls, "357")) return WT_357; if (item_icontains(model, "smg") || item_icontains(cls, "smg")) return WT_SMG; if (item_icontains(model, "ar2") || item_icontains(model, "irifle") || item_icontains(cls, "ar2") || item_icontains(cls, "irifle")) return WT_AR2; if (item_icontains(model, "shotgun") || item_icontains(cls, "shotgun")) return WT_SHOTGUN; if (item_icontains(model, "crossbow") || item_icontains(cls, "crossbow")) return WT_CROSSBOW; if (item_icontains(model, "rocket") || item_icontains(cls, "rpg")) return WT_RPG; if (item_icontains(model, "grenade") || item_icontains(cls, "frag")) return WT_GRENADE; if (item_icontains(model, "sniper") || item_icontains(cls, "sniper")) return WT_SNIPER; return WT_NONE; } static int weapon_ammo_type(int wt) { switch (wt) { case WT_PISTOL: return 2; case WT_357: return 4; case WT_SMG: return 3; case WT_AR2: return 0; case WT_SHOTGUN: return 6; case WT_CROSSBOW: return 5; case WT_RPG: return 7; case WT_GRENADE: return 11; case WT_SNIPER: return 9; default: return -1; } } // Ammo type index used by CBasePlayer::m_iAmmo (order of AddAmmoType in // hl2_gamerules.cpp). static int item_ammo_type(const char *model) { if (!model) return -1; if (item_icontains(model, "boxsrounds") || item_icontains(model, "ammocrate_pistol") || item_icontains(model, "w_pistol")) return 2; if (item_icontains(model, "boxmrounds") || item_icontains(model, "ammocrate_smg1")) return 3; if (item_icontains(model, "boxlrounds") || item_icontains(model, "combine_rifle") || item_icontains(model, "ammocrate_ar2") || item_icontains(model, "w_ar2") || item_icontains(model, "w_irifle")) return 0; if (item_icontains(model, "357ammo") || item_icontains(model, "357ammobox") || item_icontains(model, "w_357")) return 4; if (item_icontains(model, "crossbowrounds") || item_icontains(model, "w_crossbow")) return 5; if (item_icontains(model, "boxbuckshot") || item_icontains(model, "ammocrate_buckshot") || item_icontains(model, "w_shotgun")) return 6; if (item_icontains(model, "w_missile_closed") || item_icontains(model, "ammocrate_rockets") || item_icontains(model, "w_rocket_launcher")) return 7; if (item_icontains(model, "ar2_grenade") || item_icontains(model, "ammocrate_smg2") || item_icontains(model, "smg1_grenade") || item_icontains(model, "smg_grenade")) return 8; if (item_icontains(model, "ammocrate_grenade") || item_icontains(model, "w_grenade") || item_icontains(model, "w_frag") || item_icontains(model, "frag_grenade") || item_icontains(model, "items/grenade")) return 11; if (item_icontains(model, "w_smg1") || item_icontains(model, "w_smg2")) return 3; return -1; } static int convar_int(const char *name) { if (!g_iface.cvar || !name) return -1; using FindFn = void *(*)(void *, const char *); void *v = vfunc(g_iface.cvar, 12)(g_iface.cvar, name); if (!v) return -1; return (int)*(float *)((uintptr_t)v + 0x2c); // ConVar m_Value.f } // Maximum reserve ammo for an m_iAmmo index. Prefers the sk_max_* cvar and // falls back to the stock HL2 cap so the RPG's 3-rocket limit is honoured // even when server cvars are not visible from the client. static int ammo_max(int type) { static int cache[16]; static bool init = false; if (type < 0 || type >= 16) return -1; if (!init) { for (int i = 0; i < 16; i++) cache[i] = -2; init = true; } if (cache[type] != -2) return cache[type]; const char *name = nullptr; int stock = -1; switch (type) { case 0: name = "sk_max_ar2"; stock = 60; break; case 2: name = "sk_max_pistol"; stock = 150; break; case 3: name = "sk_max_smg1"; stock = 225; break; case 4: name = "sk_max_357"; stock = 12; break; case 5: name = "sk_max_crossbow"; stock = 10; break; case 6: name = "sk_max_buckshot"; stock = 30; break; case 7: name = "sk_max_rpg_round"; stock = 3; break; case 8: name = "sk_max_smg1_grenade"; stock = 3; break; case 9: name = "sk_max_sniper_round"; stock = 30; break; case 11: name = "sk_max_grenade"; stock = 5; break; default: break; } int v = convar_int(name); if (v <= 0) v = stock; cache[type] = v; return v; } static int local_ammo(void *local_ent, int type) { if (type < 0 || type >= 32) return -1; // Prefer the authoritative server array; the client m_iAmmo netvar offset // can be off by an element, which made "full" counts flicker as other ammo // was spent. server_player_ammo already skips the CNetworkArray header. int local = engine_get_local_player(); if (local > 0 && g_iface.engineserver) { resolve_server_netvars(); int s = server_player_ammo(local, type); if (s >= 0) return s; } if (local_ent && g_net.ammo >= 0) return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4); return -1; } static int classify_item(const char *model, const char *cls, char *label, size_t outn) { if (!model && !cls) return -1; // Breakable prop chunks are debris, not pickups. if (item_icontains(model, "chunk")) return -1; if (item_icontains(model, "healthkit")) { snprintf(label, outn, "Medkit"); return IK_HEALTH; } if (item_icontains(model, "healthvial")) { snprintf(label, outn, "Health vial"); return IK_HEALTH; } if (item_icontains(model, "health_charger")) { snprintf(label, outn, "Health charger"); return IK_HEALTH; } if (item_icontains(model, "battery")) { snprintf(label, outn, "Battery"); return IK_ARMOR; } if (item_icontains(model, "hevsuit")) { snprintf(label, outn, "HEV suit"); return IK_ARMOR; } if (item_icontains(model, "w_longjump")) { snprintf(label, outn, "Long jump"); return IK_ARMOR; } if (item_icontains(model, "suit_charger")) { snprintf(label, outn, "Suit charger"); return IK_ARMOR; } // Only the two item-crate models are supply crates. Other crates are just // breakable map props and deliberately not tagged. if (item_icontains(model, "item_item_crate") || item_icontains(model, "item_beacon_crate")) { snprintf(label, outn, "Supply crate"); return IK_CRATE; } if (item_icontains(model, "ammocrate_pistol")) { snprintf(label, outn, "Pistol ammo crate"); return IK_AMMO; } if (item_icontains(model, "ammocrate_smg1")) { snprintf(label, outn, "SMG ammo crate"); return IK_AMMO; } if (item_icontains(model, "ammocrate_ar2")) { snprintf(label, outn, "AR2 ammo crate"); return IK_AMMO; } if (item_icontains(model, "ammocrate_rockets")) { snprintf(label, outn, "RPG ammo crate"); return IK_AMMO; } if (item_icontains(model, "ammocrate_buckshot")) { snprintf(label, outn, "Shotgun ammo crate"); return IK_AMMO; } if (item_icontains(model, "ammocrate_grenade")) { snprintf(label, outn, "Grenade crate"); return IK_AMMO; } if (item_icontains(model, "ammocrate_smg2")) { snprintf(label, outn, "SMG grenade crate"); return IK_AMMO; } if (item_icontains(model, "ammocrate")) { snprintf(label, outn, "Ammo crate"); return IK_AMMO; } if (item_icontains(model, "w_missile_closed")) { snprintf(label, outn, "RPG rocket"); return IK_AMMO; } if (item_icontains(model, "boxsrounds")) { snprintf(label, outn, "Pistol ammo"); return IK_AMMO; } if (item_icontains(model, "boxmrounds")) { snprintf(label, outn, "SMG ammo"); return IK_AMMO; } if (item_icontains(model, "boxlrounds") || item_icontains(model, "combine_rifle_cartridge") || item_icontains(model, "combine_rifle_ammo")) { snprintf(label, outn, "AR2 ammo"); return IK_AMMO; } if (item_icontains(model, "357ammo") || item_icontains(model, "357ammobox")) { snprintf(label, outn, ".357 ammo"); return IK_AMMO; } if (item_icontains(model, "crossbowrounds")) { snprintf(label, outn, "Crossbow bolts"); return IK_AMMO; } if (item_icontains(model, "boxbuckshot")) { snprintf(label, outn, "Shotgun shells"); return IK_AMMO; } if (item_icontains(model, "boxsniperrounds")) { snprintf(label, outn, "Sniper ammo"); return IK_AMMO; } if (item_icontains(model, "boxflares") || item_icontains(model, "models/items/flare")) { snprintf(label, outn, "Flares"); return IK_AMMO; } if (item_icontains(model, "ar2_grenade")) { snprintf(label, outn, "AR2 grenade"); return IK_AMMO; } if (item_icontains(model, "w_grenade") || item_icontains(model, "w_frag") || item_icontains(model, "frag_grenade") || item_icontains(model, "items/grenade")) { snprintf(label, outn, "Frag grenade"); return IK_AMMO; } if (item_icontains(model, "models/items/")) { snprintf(label, outn, "Ammo"); return IK_AMMO; } if (classify_weapon(model, cls, label, outn) == 1) return IK_WEAPON; if (item_icontains(cls, "healthkit") || item_icontains(cls, "healthvial") || item_icontains(cls, "wallhealth") || item_icontains(cls, "healthcharger")) { snprintf(label, outn, "Health"); return IK_HEALTH; } if (item_icontains(cls, "battery") || item_icontains(cls, "itemsuit") || item_icontains(cls, "recharge")) { snprintf(label, outn, "Armor"); return IK_ARMOR; } if (item_icontains(cls, "itemcrate")) { snprintf(label, outn, "Supply crate"); return IK_CRATE; } if (item_icontains(cls, "item_ammo") || item_icontains(cls, "itemammo")) { snprintf(label, outn, "Ammo"); return IK_AMMO; } if (item_icontains(cls, "weapon")) { snprintf(label, outn, "Weapon"); return IK_WEAPON; } if (item_icontains(cls, "item") || item_icontains(cls, "pickup")) { snprintf(label, outn, "Item"); return IK_OTHER; } return -1; } static bool item_kind_enabled(int kind) { switch (kind) { case IK_HEALTH: return g_item_health; case IK_AMMO: return g_item_ammo; case IK_ARMOR: return g_item_armor; case IK_WEAPON: return g_item_weapons; case IK_CRATE: return g_item_crates; default: return true; } } // Health chargers are brushes (func_healthcharger/CWallHealth) and suit // chargers are item_suitcharger/CNewRecharge. Both expose m_iJuice. static bool item_is_charger(const char *model, const char *cls) { return item_icontains(model, "charger") || item_icontains(model, "suit_charger") || item_icontains(cls, "charger") || item_icontains(cls, "wallhealth") || item_icontains(cls, "recharge"); } static bool item_is_suit_charger(const char *model, const char *cls) { return item_icontains(model, "suit_charger") || item_icontains(model, "suitcharger") || item_icontains(cls, "recharge") || item_icontains(cls, "suitcharger"); } static void items_collect(int local_idx) { static double last_scan = 0.0; double now = esp_now(); if (now - last_scan < 0.10) return; last_scan = now; g_nitems = 0; if (!g_iface.entitylist || g_net.origin < 0 || g_net.modelindex < 0) return; resolve_server_netvars(); using Fn = int (*)(void *); int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); if (hi < 0 || hi > 4096) hi = 4096; int active_idx = local_active_weapon_idx(local_idx); // Which weapon types the local player already carries. A fallen weapon of // the same type is then just an ammo pickup. Owner handles are unreliable on // this client (owner/weapon_owner netvars resolve to -1), so remember every // type the player has held since the current local entity appeared. static uint32_t owned_mask = 0; static void *last_local = nullptr; void *local_ent = entitylist_get(local_idx); if (local_ent != last_local) { last_local = local_ent; owned_mask = 0; } int owned_idx = active_idx; if (owned_idx <= 0 && g_iface.engineserver) { resolve_server_netvars(); owned_idx = server_active_weapon(local_idx); } if (owned_idx > 0) { void *aw = entitylist_get(owned_idx); void *anet = entity_networkable(owned_idx); const char *acls = anet ? entity_class_name(anet, nullptr) : nullptr; const char *amodel = aw ? entity_model_name(aw) : nullptr; int at = weapon_type_id(amodel, acls); if (at > 0) owned_mask |= 1u << at; } for (int idx = 1; idx <= hi; idx++) { if (idx == local_idx) continue; void *ent = entitylist_get(idx); if (!ent || item_weapon_owner_idx(ent) != local_idx) continue; void *net = entity_networkable(idx); const char *cls = net ? entity_class_name(net, nullptr) : nullptr; const char *model = entity_model_name(ent); int wt = weapon_type_id(model, cls); if (wt > 0) owned_mask |= 1u << wt; } // Server-side inventory, independent of the client recv-table arrays. if (g_iface.engineserver) { for (int slot = 0; slot < 48; slot++) { uint32_t h = server_player_weapon(local_idx, slot); if (!item_handle_valid(h)) continue; int wi = (int)(h & 0x7FF); if (wi <= 0 || wi > 4096) continue; void *wep = entitylist_get(wi); void *net = entity_networkable(wi); const char *wc = net ? entity_class_name(net, nullptr) : nullptr; const char *wm = wep ? entity_model_name(wep) : nullptr; if (!wm) { void *sb = nullptr; server_entity_health(wi, nullptr, nullptr, &sb); if (sb) wm = model_name_from_index(server_model_index(sb)); } int wt = weapon_type_id(wm, wc); if (wt > 0) owned_mask |= 1u << wt; } } bool owned_weapon[WT_COUNT] = {}; for (int t = 1; t < WT_COUNT; t++) owned_weapon[t] = (owned_mask & (1u << t)) != 0; static bool cats_logged = false; if (!cats_logged) { cats_logged = true; char maxbuf[16]; if (g_item_max_dist == 0) snprintf(maxbuf, sizeof(maxbuf), "All"); else snprintf(maxbuf, sizeof(maxbuf), "%d", g_item_max_dist); logf("items: categories health=%d ammo=%d armor=%d weapons=%d " "crates=%d distance=%d max=%s", (int)g_item_health, (int)g_item_ammo, (int)g_item_armor, (int)g_item_weapons, (int)g_item_crates, (int)g_item_distance, maxbuf); } static int dbg_lines = 0; static bool owner_warned = false; if (!owner_warned && g_net.owner < 0 && g_net.weapon_owner < 0) { owner_warned = true; logf("items: weapon owner netvar unavailable; held weapons may tag"); } for (int idx = 1; idx <= hi && g_nitems < 192; idx++) { if (idx == local_idx) 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); const char *scls = nullptr; if (g_iface.engineserver && (!cls || !*cls || strcmp(cls, "CBaseEntity") == 0)) scls = server_entity_classname(idx); const char *model = entity_model_name(ent); // CPhysicsProp's client m_nModelIndex is often 0 (e.g. item_item_crate); // fall back to the server entity's model index, which is set in Spawn(). 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)); } char label[128]; int kind = classify_item(model, cls, label, sizeof(label)); if (kind < 0 && scls) kind = classify_item(model, scls, label, sizeof(label)); if (kind < 0) continue; // Health/suit chargers are dispensers that stay useful after a refill, but // an empty suit charger cannot give anything. The health charger's // m_iJuice offset is still being verified (it read 0 on a full charger), // so it is left visible for now; server_item_juice logs both candidate // values for diagnosis. bool is_charger = item_is_charger(model, cls) || (scls && item_is_charger(model, scls)); bool is_suit = item_is_suit_charger(model, cls) || (scls && item_is_suit_charger(model, scls)); if (is_charger) { int juice = server_item_juice(idx); if (is_suit && juice == 0) continue; } Vector origin{*(float *)((uintptr_t)ent + g_net.origin), *(float *)((uintptr_t)ent + g_net.origin + 4), *(float *)((uintptr_t)ent + g_net.origin + 8)}; float dx = origin.x - g_cam_eye.x; float dy = origin.y - g_cam_eye.y; float dz = origin.z - g_cam_eye.z; float dist = sqrtf(dx * dx + dy * dy + dz * dz); if (kind == IK_WEAPON) { if (idx == active_idx || item_weapon_is_owned(ent)) continue; int wt = weapon_type_id(model, cls); if (wt > 0 && owned_weapon[wt]) { char base_label[160]; snprintf(base_label, sizeof(base_label), "%s", label); snprintf(label, sizeof(label), "%.100s ammo", base_label); kind = IK_AMMO; } } // Distance applies to every tag; 0 means unlimited. if (g_item_max_dist > 0 && dist > (float)g_item_max_dist) continue; ItemTarget &t = g_item_targets[g_nitems++]; t.idx = idx; t.kind = kind; t.ammo_type = -1; if (kind == IK_AMMO) { t.ammo_type = item_ammo_type(model); if (t.ammo_type < 0) t.ammo_type = weapon_ammo_type(weapon_type_id(model, cls)); } t.origin = origin; snprintf(t.label, sizeof(t.label), "%s", label); if (dbg_lines < 8) { dbg_lines++; logd("item: idx=%d kind=%d cls=%s model=%s", idx, kind, cls ? cls : "?", model ? model : "?"); } } if (!g_items_logged) { g_items_logged = true; logf("items: tagging %d valuable entities", g_nitems); } } // Authoritative local health/armor for the "only if usable" item filter. // The health ceiling comes from the server player's m_iMaxHealth, not a // hardcoded 100; when it cannot be read the filter fails open. static void local_vitals(int &hp, int &maxhp, int &armor) { hp = maxhp = armor = -1; int local = engine_get_local_player(); if (local <= 0) return; void *le = entitylist_get(local); if (le && g_net.health >= 0) hp = *(int *)((uintptr_t)le + g_net.health); if (g_iface.engineserver) { int smax = -1; int shp = server_entity_health(local, &smax, nullptr, nullptr); if (shp >= 0) hp = shp; if (smax > 0) maxhp = smax; armor = server_player_armor(local); } if (maxhp < 0 && le && g_net.maxhealth >= 0) maxhp = *(int *)((uintptr_t)le + g_net.maxhealth); } void esp_draw_items() { if (!g_surface || !g_iface.entitylist || g_net.origin < 0) return; if (!g_item_health && !g_item_ammo && !g_item_armor && !g_item_weapons && !g_item_crates) return; int local = engine_get_local_player(); if (local <= 0) return; if (!esp_update_screen()) return; items_collect(local); int vit_hp = -1, vit_max = -1, vit_armor = -1; if (g_item_usable) local_vitals(vit_hp, vit_max, vit_armor); static bool ammo_logged = false; if (g_item_usable && !ammo_logged) { ammo_logged = true; char buf[512]; int pos = 0; void *le = entitylist_get(local); for (int t = 0; t < 16 && pos < 500; t++) { int a = local_ammo(le, t); int m = ammo_max(t); if (a >= 0 && m > 0) pos += snprintf(buf + pos, sizeof(buf) - pos, " %d:%d/%d", t, a, m); } logf("items: ammo%s", buf); } for (int i = 0; i < g_nitems; i++) { ItemTarget &t = g_item_targets[i]; // Follow the entity between the 10 Hz discovery scans. Vector live; if (entity_origin(entitylist_get(t.idx), live)) t.origin = live; if (!item_kind_enabled(t.kind)) continue; // Dispensers (chargers, ammo crates, supply crates) stay useful after a // refill, so the fullness filter does not apply to them. bool dispenser = strcasestr(t.label, "crate") || strcasestr(t.label, "charger"); if (g_item_usable && !dispenser) { if (t.kind == IK_HEALTH && vit_hp >= 0 && vit_max > 0 && vit_hp >= vit_max) continue; if (t.kind == IK_ARMOR && vit_armor >= 0 && vit_armor >= 100) continue; if (t.kind == IK_AMMO && t.ammo_type >= 0) { int maxa = ammo_max(t.ammo_type); int cura = local_ammo(entitylist_get(local), t.ammo_type); if (maxa > 0 && cura >= 0 && cura >= maxa) continue; } } float sx, sy; if (!world_to_screen(t.origin, sx, sy)) continue; if (sx < -40 || sx > (float)g_scr_w + 40 || sy < -20 || sy > (float)g_scr_h + 20) continue; int r = 230, g = 230, b = 230; switch (t.kind) { case IK_HEALTH: r = 90; g = 255; b = 130; break; case IK_AMMO: r = 255; g = 210; b = 80; break; case IK_ARMOR: r = 90; g = 180; b = 255; break; case IK_WEAPON: r = 120; g = 230; b = 255; break; case IK_CRATE: r = 220; g = 150; b = 255; break; default: break; } int x = (int)sx, y = (int)sy; int rad = (t.kind == IK_CRATE) ? 7 : 4; surf_color(r, g, b, 255); surf_rect(0, x - rad, y - rad, x + rad, y + rad); surf_line(x - rad - 4, y, x - rad - 1, y); surf_line(x + rad + 1, y, x + rad + 4, y); surf_line(x, y - rad - 4, x, y - rad - 1); surf_line(x, y + rad + 1, x, y + rad + 4); if (t.kind == IK_CRATE) surf_rect(1, x - 2, y - 2, x + 2, y + 2); float dx = t.origin.x - g_cam_eye.x; float dy = t.origin.y - g_cam_eye.y; float dz = t.origin.z - g_cam_eye.z; int dist = (int)sqrtf(dx * dx + dy * dy + dz * dz); char text[192]; if (g_item_distance) snprintf(text, sizeof(text), "%s %du", t.label, dist); else snprintf(text, sizeof(text), "%s", t.label); int tw = (int)strlen(text) * 9; int tx = x + 10; if (tx + tw > g_scr_w - 4) tx = x - 10 - tw; if (tx < 4) tx = 4; int ty = y - 7; if (ty < 0) ty = 0; if (ty + 16 > g_scr_h) ty = g_scr_h - 16; draw_text_col(tx, ty, text, r, g, b); } } // --------------------------------------------------------------------------- // projectile ESP: grenades, rockets, helicopter bombs, AR2 balls // ---------------------------------------------------------------------------