1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
|
// entity_query.cpp — client entity lookups, NPC classification, per-map caches.
#include "esp_internal.h"
#include "hl2h.h"
#include <cmath>
#include <cstring>
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<GetNetFn>(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<GetCCFn>(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<DormantFn>(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<DormantFn>(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<GetModelFn>(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<GetModelNameFn>(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.
|