summaryrefslogtreecommitdiffstats
path: root/src/entity_query.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/entity_query.cpp')
-rw-r--r--src/entity_query.cpp460
1 files changed, 460 insertions, 0 deletions
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 @@
1// entity_query.cpp — client entity lookups, NPC classification, per-map caches.
2#include "esp_internal.h"
3#include "hl2h.h"
4#include <cmath>
5#include <cstring>
6
7using GetNetFn = void *(*)(void *, int);
8using GetCCFn = void *(*)(void *);
9using GetModelFn = const void *(*)(void *, int);
10using GetModelNameFn = const char *(*)(void *, const void *);
11
12// Listen-server sanity latch. Only ever set once per map (esp_reset_session),
13// and only after both model indices are valid: latching on the transient
14// cli_model == 0 at map load disabled server health for the whole process.
15bool g_srv_checked = false;
16bool g_srv_trusted = false;
17
18// Per-map caches. The ESP resolves a model name for every entity on every scan
19// and the chams hook asks for a relation on every model draw; both are virtual
20// call chains into the engine plus server datamap reads, so memoise them.
21static constexpr int MODEL_CACHE = 16384;
22static const char *g_model_name_cache[MODEL_CACHE];
23static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail
24
25// Direct-mapped entity pointer -> index map, filled by esp_collect. The
26// triggerbot traces a ray and gets a pointer back, and used to find its index
27// by walking up to 4096 entity-list slots on every fire tick.
28static constexpr int ENTPTR_CACHE = 4096;
29static void *g_entptr_key[ENTPTR_CACHE];
30static int g_entptr_idx[ENTPTR_CACHE];
31
32void esp_reset_session() {
33 g_srv_checked = false;
34 g_srv_trusted = false;
35 memset(g_model_name_cache, 0, sizeof(g_model_name_cache));
36 memset(g_model_state_cache, 0, sizeof(g_model_state_cache));
37 memset(g_entptr_key, 0, sizeof(g_entptr_key));
38 memset(g_entptr_idx, 0, sizeof(g_entptr_idx));
39}
40
41void *entity_networkable(int idx) {
42 return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx);
43}
44
45// IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8.
46const char *entity_class_name(void *net, void **out_cc) {
47 if (!net) return nullptr;
48 void *cc = vfunc<GetCCFn>(net, 2)(net);
49 if (out_cc) *out_cc = cc;
50 return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr;
51}
52
53// IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant
54// 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK,
55// so the slot is validated once against the local player, which can never be
56// dormant: a wrong slot disables the check instead of misreporting entities.
57// Dormant entities keep their last networked origin, so drawing or aiming at
58// them produces ghosts at stale positions.
59static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled
60
61static void dormant_selftest() {
62 // Leave the state untested (0) until a local player exists: the first call
63 // can come from the main menu, and latching "disabled" there would turn the
64 // check off for the whole session.
65 if (!g_iface.entitylist)
66 return;
67 int local = engine_get_local_player();
68 if (local <= 0)
69 return;
70 void *net = entity_networkable(local);
71 if (!net)
72 return;
73 void **vt = *(void ***)net;
74 if (!vt || !mod_client().contains((uintptr_t)vt[8])) {
75 g_dormant_state = -1;
76 logf("dormant check disabled: slot 8 outside client module");
77 return;
78 }
79 using DormantFn = unsigned char (*)(void *);
80 if (vfunc<DormantFn>(net, 8)(net) != 0) {
81 g_dormant_state = -1;
82 logf("dormant check disabled: local player reported dormant");
83 return;
84 }
85 g_dormant_state = 1;
86 logd("dormant check enabled");
87}
88
89bool entity_is_dormant(void *net) {
90 if (!net)
91 return false;
92 if (g_dormant_state == 0)
93 dormant_selftest();
94 if (g_dormant_state != 1)
95 return false;
96 using DormantFn = unsigned char (*)(void *);
97 return vfunc<DormantFn>(net, 8)(net) != 0;
98}
99
100// Entity pointer -> index, filled by the entity scans. The triggerbot gets a
101// pointer back from the trace and needs the index; the alternative is walking
102// up to 4096 entity-list slots on every fire tick.
103int entptr_lookup(void *ent) {
104 if (!ent)
105 return -1;
106 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
107 return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1;
108}
109
110void entptr_store(void *ent, int idx) {
111 if (!ent || idx <= 0)
112 return;
113 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
114 g_entptr_key[h] = ent;
115 g_entptr_idx[h] = idx;
116}
117
118// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName.
119// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4.
120const char *model_name_from_index(int modelindex) {
121 if (!g_iface.modelinfo || modelindex <= 0)
122 return nullptr;
123 if (modelindex < MODEL_CACHE) {
124 if (g_model_state_cache[modelindex] == 1)
125 return g_model_name_cache[modelindex];
126 if (g_model_state_cache[modelindex] == 2)
127 return nullptr;
128 }
129 const void *model =
130 vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex);
131 if (!model) {
132 if (modelindex < MODEL_CACHE)
133 g_model_state_cache[modelindex] = 2;
134 return nullptr;
135 }
136 const char *name =
137 vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model);
138 if (modelindex < MODEL_CACHE) {
139 g_model_name_cache[modelindex] = name;
140 g_model_state_cache[modelindex] = name ? 1 : 2;
141 }
142 return name;
143}
144
145const char *entity_model_name(void *ent) {
146 if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr;
147 int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex);
148 return model_name_from_index(modelindex);
149}
150
151// Live origin for a cached entity index. Item/projectile discovery is throttled
152// to 10 Hz (it walks the whole entity list), but the markers must follow the
153// world at render rate; refreshing one netvar read per cached target is cheap.
154bool entity_origin(void *ent, Vector &out) {
155 if (!ent || g_net.origin < 0)
156 return false;
157 uintptr_t e = (uintptr_t)ent;
158 out.x = *(float *)(e + g_net.origin);
159 out.y = *(float *)(e + g_net.origin + 4);
160 out.z = *(float *)(e + g_net.origin + 8);
161 return true;
162}
163
164// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all
165// use the single client class "CAI_BaseNPC" — only special NPCs register their
166// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...).
167bool is_npc_class(const char *name) {
168 if (!name) return false;
169 return strstr(name, "NPC") != nullptr ||
170 strstr(name, "BaseHelicopter") != nullptr;
171}
172
173// Used by the chams hook to decide whether a model-render entity is an NPC.
174bool esp_is_npc_index(int idx) {
175 if (!g_iface.entitylist || idx <= 0 || idx > 8192)
176 return false;
177 void *net = entity_networkable(idx);
178 if (!net)
179 return false;
180 return is_npc_class(entity_class_name(net, nullptr));
181}
182
183bool esp_is_viewmodel_index(int idx) {
184 if (!g_iface.entitylist || idx <= 0 || idx > 8192)
185 return false;
186 void *ent = entitylist_get(idx);
187 if (!ent)
188 return false;
189 void *net = entity_networkable(idx);
190 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
191 if (cls && strcasestr(cls, "viewmodel"))
192 return true;
193 const char *model = entity_model_name(ent);
194 return model && (strstr(model, "/v_") || strstr(model, "\\v_"));
195}
196
197// 0 = hostile, 1 = neutral, 2 = friendly, -1 = not an NPC.
198bool npc_is_friendly(int idx, const char *model, int local_idx,
199 bool &neutral);
200int esp_npc_relation(int idx) {
201 // Called from the chams hook up to three times per entity per frame (visible,
202 // through-wall, wireframe pass) and the work behind it is a chain of server
203 // edict/datamap reads. A short TTL cache keeps that out of the render path.
204 struct RelCache {
205 int idx;
206 double at;
207 int val;
208 };
209 static RelCache cache[256];
210 double now = esp_now();
211 RelCache &c = cache[(unsigned)idx & 255];
212 if (c.idx == idx && now - c.at < 0.05)
213 return c.val;
214
215 int val = -1;
216 if (esp_is_npc_index(idx)) {
217 int local = engine_get_local_player();
218 if (local <= 0) {
219 val = 0;
220 } else {
221 void *ent = entitylist_get(idx);
222 const char *model = ent ? entity_model_name(ent) : nullptr;
223 bool neutral = false;
224 bool friendly = npc_is_friendly(idx, model, local, neutral);
225 val = neutral ? 1 : friendly ? 2 : 0;
226 }
227 }
228 c = {idx, now, val};
229 return val;
230}
231
232static bool model_is_friendly(const char *m) {
233 static const char *patterns[] = {
234 "citizen", "humans/", "male_", "female_", "barney", "alyx",
235 "alex", "scientist", "kleiner", "eli", "mossman", "vortigaunt",
236 "dog", "grigori", "monk", "odessa", "gman", nullptr};
237 if (!m) return false;
238 for (int i = 0; patterns[i]; i++)
239 if (strstr(m, patterns[i]))
240 return true;
241 return false;
242}
243
244// harmless wildlife that also happens to be CAI_BaseNPC
245static bool model_is_neutral(const char *m) {
246 // Lamarr is Kleiner's pet headcrab: scripted and harmless, and the user
247 // wants her shown neutral rather than hostile.
248 static const char *patterns[] = {"crow", "gull", "pigeon", "seagull",
249 "bird", "Lamarr", nullptr};
250 if (!m) return false;
251 for (int i = 0; patterns[i]; i++)
252 if (strstr(m, patterns[i]))
253 return true;
254 return false;
255}
256
257static bool model_is_floor_turret(const char *m) {
258 return m && strstr(m, "floor_turret");
259}
260
261static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx,
262 bool &neutral) {
263 int rel = server_npc_relation(idx, local_idx);
264 int state = server_npc_state(idx);
265 int turret = -1;
266 // Citizen-built and Alyx-hacked turrets never add an m_Relationship row;
267 // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state.
268 if (model_is_floor_turret(model))
269 turret = server_npc_turret_flags(idx);
270 // Debug-only: relation/state changes are the authoritative signals and the
271 // first thing to check when a friendly NPC still looks hostile.
272 static int rel_cache[2048];
273 static int state_cache[2048];
274 static bool cache_init = false;
275 if (!cache_init) {
276 for (int i = 0; i < 2048; i++)
277 rel_cache[i] = state_cache[i] = -999;
278 cache_init = true;
279 }
280 if (idx > 0 && idx < 2048 &&
281 (rel_cache[idx] != rel || state_cache[idx] != state)) {
282 rel_cache[idx] = rel;
283 state_cache[idx] = state;
284 logd("npc %d (%s): relation=%d state=%d followtarget=%d turret=%d "
285 "local=%d", idx, model ? model : "?", rel, state,
286 (int)server_npc_has_follow_target(idx), turret, local_idx);
287 }
288 if (rel == 3) { // D_LI
289 neutral = false;
290 return true;
291 }
292 if (turret > 0) { // citizen-built / Alyx-hacked
293 neutral = false;
294 return true;
295 }
296 if (rel == 4) { // D_NU
297 neutral = true;
298 return false;
299 }
300 // NPC_STATE_SCRIPT = 4: cutscene actors and scripted pets are passive even
301 // when their class relationship says hate (Lamarr, scripted Combine, ...).
302 if (state == 4) {
303 neutral = true;
304 return false;
305 }
306 if (rel == 1 || rel == 2) { // D_HT / D_FR
307 neutral = false;
308 return false;
309 }
310 if (turret >= 0) { // ordinary floor turret
311 neutral = false;
312 return false;
313 }
314 // Bugbait-controlled antlions follow a target even when their default
315 // relationship was never rewritten.
316 if (model && strstr(model, "antlion") &&
317 server_npc_has_follow_target(idx)) {
318 neutral = false;
319 return true;
320 }
321 bool friendly = model_is_friendly(model);
322 neutral = model_is_neutral(model);
323 return friendly;
324}
325
326// Friendly/neutral state changes slowly (hacked turrets, bugbait), while the
327// entity scan and the chams hook ask for it constantly. Cache it briefly so the
328// server lookups happen once per NPC per window instead of per call.
329bool npc_is_friendly(int idx, const char *model, int local_idx,
330 bool &neutral) {
331 struct Cache {
332 int idx;
333 double at;
334 bool friendly;
335 bool neutral;
336 };
337 static Cache cache[256];
338 double now = esp_now();
339 Cache &c = cache[(unsigned)idx & 255];
340 if (c.idx == idx && now - c.at < 0.10) {
341 neutral = c.neutral;
342 return c.friendly;
343 }
344 bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral);
345 c = {idx, now, f, neutral};
346 return f;
347}
348
349
350static bool model_has(const char *model, const char *needle) {
351 return model && strcasestr(model, needle) != nullptr;
352}
353
354// Server m_takedamage first (authoritative immunity), then the HL2 damage
355// rules for units whose OnTakeDamage filters specific damage types.
356static int npc_damage_class_uncached(int idx, const char *model) {
357 int td = server_npc_takedamage(idx);
358 if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY
359 return NPC_DMG_IMMUNE;
360 if (model_has(model, "gunship") || model_has(model, "combine_strider") ||
361 model_has(model, "dropship") || model_has(model, "helicopter") ||
362 model_has(model, "chopper"))
363 return NPC_DMG_ARMORED;
364 if (model_has(model, "headcrab") || model_has(model, "antlion") ||
365 model_has(model, "manhack"))
366 return NPC_DMG_VULNERABLE;
367 return NPC_DMG_NORMAL;
368}
369
370// Damage class depends on m_takedamage (a server read) plus model string
371// matching. It changes even more rarely than the relation, so cache it.
372int npc_damage_class(int idx, const char *model) {
373 struct Cache {
374 int idx;
375 double at;
376 int val;
377 };
378 static Cache cache[256];
379 double now = esp_now();
380 Cache &c = cache[(unsigned)idx & 255];
381 if (c.idx == idx && now - c.at < 0.25)
382 return c.val;
383 int v = npc_damage_class_uncached(idx, model);
384 c = {idx, now, v};
385 return v;
386}
387
388bool active_weapon_contains(void *local_ent, const char *needle);
389
390// Armoured units need blast damage; the RPG is the only stock weapon that
391// passes their OnTakeDamage filters. Immune units can never be damaged.
392bool npc_can_be_damaged(int idx, const char *model) {
393 int dc = npc_damage_class(idx, model);
394 if (dc == NPC_DMG_IMMUNE)
395 return false;
396 if (dc != NPC_DMG_ARMORED)
397 return true;
398 int local = engine_get_local_player();
399 void *le = local > 0 ? entitylist_get(local) : nullptr;
400 return active_weapon_contains(le, "rocket_launcher") ||
401 active_weapon_contains(le, "rpg");
402}
403
404void short_name(const char *model, char *out, size_t outn) {
405 if (!model) { snprintf(out, outn, "NPC"); return; }
406 const char *base = strrchr(model, '/');
407 base = base ? base + 1 : model;
408 size_t n = strcspn(base, ".");
409 if (n >= outn) n = outn - 1;
410 memcpy(out, base, n);
411 out[n] = 0;
412}
413
414// Enemy weapon label. NPC weapons are server entities, so read the server
415// model index first and fall back to the client class name.
416void weapon_short_name(int idx, char *out, size_t outn) {
417 out[0] = 0;
418 int wi = server_active_weapon(idx);
419 if (wi <= 0)
420 return;
421 const char *name = nullptr;
422 void *base = server_entity_base(wi);
423 int mi = base ? server_model_index(base) : -1;
424 if (mi > 0)
425 name = model_name_from_index(mi);
426 if (!name) {
427 void *wep = entitylist_get(wi);
428 if (wep) {
429 void *net = entity_networkable(wi);
430 name = entity_class_name(net, nullptr);
431 }
432 }
433 if (!name)
434 name = server_entity_classname(wi);
435 if (!name)
436 return;
437 static const struct {
438 const char *needle;
439 const char *label;
440 } map[] = {
441 {"smg1", "SMG1"}, {"irifle", "AR2"}, {"ar2", "AR2"},
442 {"shotgun", "Shotgun"}, {"pistol", "Pistol"}, {"357", "357"},
443 {"crossbow", "Crossbow"}, {"rocket", "RPG"}, {"rpg", "RPG"},
444 {"grenade", "Grenade"}, {"frag", "Grenade"}, {"crowbar", "Crowbar"},
445 {"physcannon", "Gravity"}, {"slam", "SLAM"}, {"bugbait", "Bugbait"},
446 {"manhack", "Manhack"}, {"turret", "Turret"}, {"annabelle", "Rifle"},
447 {"alyxgun", "Alyx gun"}, {"sniper", "Sniper"}, {"smg", "SMG"},
448 };
449 for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) {
450 if (strcasestr(name, map[i].needle)) {
451 snprintf(out, outn, "%s", map[i].label);
452 return;
453 }
454 }
455 // Last resort: the short class/model name.
456 short_name(name, out, outn);
457}
458
459// Hostile / neutral / friendly colours come from the ESP colour pickers.
460