From c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 6 Nov 2026 16:25:48 +0100 Subject: ESP: finish the split into entity, combat, item and projectile units esp.cpp keeps the scan and drawing (~1000 lines, down from 3665). Entity lookups and NPC classification move to entity_query.cpp together with the per-map caches and the session reset; combat (aimbot, silent aim, melee, spread, triggerbot) moves to combat.cpp. Shared helpers are declared in esp_internal.h. --- src/entity_query.cpp | 460 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 src/entity_query.cpp (limited to 'src/entity_query.cpp') diff --git a/src/entity_query.cpp b/src/entity_query.cpp new file mode 100644 index 0000000..4697bf2 --- /dev/null +++ b/src/entity_query.cpp @@ -0,0 +1,460 @@ +// entity_query.cpp — client entity lookups, NPC classification, per-map caches. +#include "esp_internal.h" +#include "hl2h.h" +#include +#include + +using GetNetFn = void *(*)(void *, int); +using GetCCFn = void *(*)(void *); +using GetModelFn = const void *(*)(void *, int); +using GetModelNameFn = const char *(*)(void *, const void *); + +// 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. +bool g_srv_checked = false; +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; + 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)); +} + +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. +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; +} + +void entptr_store(void *ent, int idx) { + if (!ent || idx <= 0) + return; + unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); + g_entptr_key[h] = ent; + g_entptr_idx[h] = idx; +} + +// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. +// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. +const char *model_name_from_index(int modelindex) { + if (!g_iface.modelinfo || modelindex <= 0) + return nullptr; + if (modelindex < MODEL_CACHE) { + if (g_model_state_cache[modelindex] == 1) + return g_model_name_cache[modelindex]; + if (g_model_state_cache[modelindex] == 2) + return nullptr; + } + const void *model = + vfunc(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); + if (!model) { + if (modelindex < MODEL_CACHE) + g_model_state_cache[modelindex] = 2; + return nullptr; + } + const char *name = + vfunc(g_iface.modelinfo, 4)(g_iface.modelinfo, model); + if (modelindex < MODEL_CACHE) { + g_model_name_cache[modelindex] = name; + g_model_state_cache[modelindex] = name ? 1 : 2; + } + return name; +} + +const char *entity_model_name(void *ent) { + if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr; + int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); + return model_name_from_index(modelindex); +} + +// Live origin for a cached entity index. Item/projectile discovery is throttled +// to 10 Hz (it walks the whole entity list), but the markers must follow the +// world at render rate; refreshing one netvar read per cached target is cheap. +bool entity_origin(void *ent, Vector &out) { + if (!ent || g_net.origin < 0) + return false; + uintptr_t e = (uintptr_t)ent; + out.x = *(float *)(e + g_net.origin); + out.y = *(float *)(e + g_net.origin + 4); + out.z = *(float *)(e + g_net.origin + 8); + return true; +} + +// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all +// use the single client class "CAI_BaseNPC" — only special NPCs register their +// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). +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. +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. +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; +} + + +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. +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. +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"); +} + +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. +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. + -- cgit v1.2.3