// 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.