summaryrefslogtreecommitdiffstats
path: root/src/esp.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/esp.cpp262
1 files changed, 222 insertions, 40 deletions
diff --git a/src/esp.cpp b/src/esp.cpp
index 87d0b80..d12c718 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -3,6 +3,7 @@
3#include <cmath> 3#include <cmath>
4#include <cstring> 4#include <cstring>
5#include <cstddef> 5#include <cstddef>
6#include <ctime>
6 7
7static constexpr float PI_F = 3.14159265358979323846f; 8static constexpr float PI_F = 3.14159265358979323846f;
8 9
@@ -12,11 +13,15 @@ enum SurfSlot {
12 SURF_GetScreenSize = 38, 13 SURF_GetScreenSize = 38,
13}; 14};
14 15
16enum { LIFE_ALIVE = 0 };
17
15struct EspTarget { 18struct EspTarget {
16 float sx, sy; // screen pos of feet 19 float sx, sy; // screen pos of feet
17 float h; // box height px 20 float h; // box height px
18 float health, maxhealth; 21 float health, maxhealth;
19 int teamnum; 22 bool show_health;
23 bool friendly;
24 bool neutral;
20 char name[48]; 25 char name[48];
21}; 26};
22static EspTarget g_targets[128]; 27static EspTarget g_targets[128];
@@ -24,7 +29,27 @@ static int g_ntargets = 0;
24static int g_scr_w = 0, g_scr_h = 0; 29static int g_scr_w = 0, g_scr_h = 0;
25static bool g_esp_logged = false; 30static bool g_esp_logged = false;
26 31
32static double esp_now() {
33 timespec ts;
34 clock_gettime(CLOCK_MONOTONIC, &ts);
35 return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
36}
37
27bool world_to_screen(const Vector &w, float &sx, float &sy) { 38bool world_to_screen(const Vector &w, float &sx, float &sy) {
39 // Preferred: the engine's own transform. Uses the real view setup (FOV,
40 // aspect, roll), so boxes do not drift at the screen edges.
41 if (g_iface.debugoverlay) {
42 using Fn = int (*)(void *, const Vector *, Vector *);
43 Vector s;
44 int behind =
45 vfunc<Fn>(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s);
46 if (behind)
47 return false;
48 sx = s.x;
49 sy = s.y;
50 return true;
51 }
52 // Fallback: local math with the default HL2 FOV.
28 float pitch = g_cam_view.x * PI_F / 180.0f; 53 float pitch = g_cam_view.x * PI_F / 180.0f;
29 float yaw = g_cam_view.y * PI_F / 180.0f; 54 float yaw = g_cam_view.y * PI_F / 180.0f;
30 float sp = sinf(pitch), cp = cosf(pitch); 55 float sp = sinf(pitch), cp = cosf(pitch);
@@ -43,39 +68,195 @@ bool world_to_screen(const Vector &w, float &sx, float &sy) {
43 return true; 68 return true;
44} 69}
45 70
71// ---------------------------------------------------------------------------
72// entity helpers (IClientEntityList / IClientNetworkable, header-documented)
73// ---------------------------------------------------------------------------
74
75using GetNetFn = void *(*)(void *, int);
76using GetCCFn = void *(*)(void *);
77using GetModelFn = const void *(*)(void *, int);
78using GetModelNameFn = const char *(*)(void *, const void *);
79
80// entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the
81// IClientNetworkable subobject offset inside the entity (see projectz).
82static void *entity_networkable(int idx) {
83 return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx);
84}
85
86// IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8.
87static const char *entity_class_name(void *net, void **out_cc) {
88 if (!net) return nullptr;
89 void *cc = vfunc<GetCCFn>(net, 2)(net);
90 if (out_cc) *out_cc = cc;
91 return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr;
92}
93
94// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName.
95// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4.
96static const char *entity_model_name(void *ent) {
97 if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr;
98 int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex);
99 if (modelindex <= 0) return nullptr;
100 const void *model =
101 vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex);
102 if (!model)
103 return nullptr;
104 return vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model);
105}
106
107// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all
108// use the single client class "CAI_BaseNPC" — only special NPCs register their
109// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...).
110static bool is_npc_class(const char *name) {
111 if (!name) return false;
112 return strstr(name, "NPC") != nullptr ||
113 strstr(name, "BaseHelicopter") != nullptr;
114}
115
116static bool model_is_friendly(const char *m) {
117 static const char *patterns[] = {
118 "citizen", "humans/", "barney", "alyx", "alex", "scientist",
119 "kleiner", "eli", "mossman", "vortigaunt", "dog", "grigori",
120 "monk", "gman", nullptr};
121 if (!m) return false;
122 for (int i = 0; patterns[i]; i++)
123 if (strstr(m, patterns[i]))
124 return true;
125 return false;
126}
127
128// harmless wildlife that also happens to be CAI_BaseNPC
129static bool model_is_neutral(const char *m) {
130 static const char *patterns[] = {"crow", "gull", "pigeon", "seagull",
131 "bird", nullptr};
132 if (!m) return false;
133 for (int i = 0; patterns[i]; i++)
134 if (strstr(m, patterns[i]))
135 return true;
136 return false;
137}
138
139static void short_name(const char *model, char *out, size_t outn) {
140 if (!model) { snprintf(out, outn, "NPC"); return; }
141 const char *base = strrchr(model, '/');
142 base = base ? base + 1 : model;
143 size_t n = strcspn(base, ".");
144 if (n >= outn) n = outn - 1;
145 memcpy(out, base, n);
146 out[n] = 0;
147}
148
46static void esp_collect(int local_idx) { 149static void esp_collect(int local_idx) {
150 // outer_draw() runs once per PaintTraverseEx call (several per frame); cache
151 // the scan briefly so we do not walk the entity list dozens of times a frame.
152 static double last_scan = 0.0;
153 static double last_dbg = 0.0;
154 double now = esp_now();
155 if (now - last_scan < 0.01)
156 return;
157 last_scan = now;
158 bool dbg = (now - last_dbg) >= 2.0;
159 if (dbg) last_dbg = now;
160
47 g_ntargets = 0; 161 g_ntargets = 0;
48 using Fn = int (*)(void *); 162 using Fn = int (*)(void *);
49 int maxent = vfunc<Fn>(g_iface.entitylist, 5)(g_iface.entitylist); // GetHighestEntityIndex 163 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); // GetHighestEntityIndex
50 if (maxent < 0 || maxent > 512) maxent = 512; 164 if (hi < 0 || hi > 4096) hi = 4096;
51 for (int idx = 0; idx <= maxent && g_ntargets < 128; idx++) { 165 int npc_alive = 0, dbg_lines = 0;
166 bool srv = resolve_server_netvars();
167 static bool srv_checked = false;
168 static bool srv_trusted = false;
169
170 for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) {
52 if (idx == local_idx) continue; 171 if (idx == local_idx) continue;
53 void *ent = entitylist_get(idx); 172 void *ent = entitylist_get(idx);
54 if (!ent) continue; 173 if (!ent) continue;
174 void *net = entity_networkable(idx);
175 if (!net) continue;
176 const char *cls = entity_class_name(net, nullptr);
177 if (!cls) continue;
178
55 uintptr_t e = (uintptr_t)ent; 179 uintptr_t e = (uintptr_t)ent;
56 float hpv = (float)*(int *)(e + g_net.health); // m_iHealth is int 180 int cli_life = g_net.lifestate >= 0
57 if (!(hpv > 0)) continue; 181 ? (int)*(unsigned char *)(e + g_net.lifestate)
182 : -1;
183 int cli_hp = g_net.health >= 0 ? *(int *)(e + g_net.health) : 0;
184 bool npc = is_npc_class(cls);
185 const char *model = entity_model_name(ent);
186
187 // NPC health is not networked; read it from the listen-server entity.
188 int hp = cli_hp, maxhp = -1, life = cli_life;
189 if (npc && srv) {
190 int slife = -1, smax = -1;
191 void *sbase = nullptr;
192 int shp = server_entity_health(idx, &smax, &slife, &sbase);
193 if (!srv_checked && sbase) {
194 srv_checked = true;
195 int cli_mi =
196 g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1;
197 int srv_mi = server_model_index(sbase);
198 srv_trusted = (cli_mi == srv_mi);
199 logf("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d "
200 "hp=%d maxhp=%d life=%d trusted=%d",
201 idx, sbase, cli_mi, srv_mi, shp, smax, slife, (int)srv_trusted);
202 if (!srv_trusted)
203 logf("ESP: server entity mismatch, ignoring server values");
204 }
205 if (srv_trusted) {
206 if (shp >= 0) hp = shp;
207 if (smax > 0) maxhp = smax;
208 if (slife >= 0) life = slife;
209 }
210 }
211
212 if (dbg && npc && dbg_lines < 40) {
213 dbg_lines++;
214 logf("ESP npc %d cls=%s life=%d(cli %d) hp=%d(cli %d) maxhp=%d model=%s",
215 idx, cls, life, cli_life, hp, cli_hp, maxhp,
216 model ? model : "?");
217 }
218
219 if (npc) {
220 if (life != LIFE_ALIVE) continue;
221 npc_alive++;
222 } else {
223 // players (HL2DM) are the only entities with a live networked m_iHealth
224 if (!strstr(cls, "Player") || !(hp > 0)) continue;
225 }
226
227 float minz = g_net.mins >= 0 ? *(float *)(e + g_net.mins + 8) : 0.0f;
228 float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f;
229 if (!(maxz > minz + 4.0f && maxz - minz < 512.0f)) {
230 minz = 0.0f;
231 maxz = 72.0f;
232 }
58 Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), 233 Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4),
59 *(float *)(e + g_net.origin + 8)}; 234 *(float *)(e + g_net.origin + 8)};
60 // ponytail: maxs netvar may be unresolved; fixed 72-unit torso fallback 235 Vector bottom{origin.x, origin.y, origin.z + minz};
61 float maxsz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs) : 72.0f; 236 Vector top{origin.x, origin.y, origin.z + maxz};
62 Vector top{origin.x, origin.y, origin.z + maxsz};
63 float sx, sy, tx, ty; 237 float sx, sy, tx, ty;
64 if (!world_to_screen(origin, sx, sy) || !world_to_screen(top, tx, ty)) 238 if (!world_to_screen(bottom, sx, sy) || !world_to_screen(top, tx, ty))
65 continue; 239 continue;
66 float h = sy - ty; 240 float h = sy - ty;
67 if (h < 4.0f || sy < 0 || ty > (float)g_scr_h) continue; // behind / offscreen 241 if (h < 3.0f || sy < 0 || ty > (float)g_scr_h) continue; // behind / offscreen
242 if (sx < -(float)g_scr_w || sx > 2.0f * (float)g_scr_w) continue;
243
68 EspTarget &t = g_targets[g_ntargets++]; 244 EspTarget &t = g_targets[g_ntargets++];
69 t.sx = sx; t.sy = sy; t.h = h; 245 t.sx = sx; t.sy = sy; t.h = h;
70 t.health = hpv; 246 t.health = hp > 0 ? (float)hp : 0.0f;
71 t.maxhealth = (float)*(int *)(e + g_net.maxhealth); 247 t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f;
72 if (t.maxhealth <= 0) t.maxhealth = hpv; 248 t.show_health = hp > 0;
73 // classname via IClientNetworkable::GetClientClass (slot 2) -> m_pNetworkName @ +8 249 t.friendly = model_is_friendly(model);
74 using CCFn = void *(*)(void *); 250 t.neutral = model_is_neutral(model);
75 void *cc = vfunc<CCFn>(ent, 2)(ent); 251 short_name(model, t.name, sizeof(t.name));
76 const char *nn = cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; 252 if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC");
77 strncpy(t.name, nn ? nn : "?", sizeof(t.name) - 1); 253 }
78 t.name[sizeof(t.name) - 1] = 0; 254
255 if (dbg)
256 logf("ESP scan: hi=%d npc_alive=%d targets=%d", hi, npc_alive, g_ntargets);
257 if (g_ntargets && !g_esp_logged) {
258 g_esp_logged = true;
259 logf("ESP: first targets, %d", g_ntargets);
79 } 260 }
80} 261}
81 262
@@ -97,28 +278,29 @@ void esp_draw() {
97 if (!g_scr_w) return; 278 if (!g_scr_w) return;
98 } 279 }
99 esp_collect(engine_get_local_player()); 280 esp_collect(engine_get_local_player());
100 if (g_ntargets && !g_esp_logged) {
101 g_esp_logged = true;
102 logf("ESP: first draw, %d targets", g_ntargets);
103 }
104 for (int i = 0; i < g_ntargets; i++) { 281 for (int i = 0; i < g_ntargets; i++) {
105 const EspTarget &t = g_targets[i]; 282 const EspTarget &t = g_targets[i];
106 // team color: 3 combine red, 2 rebels green, else white 283 if (t.friendly) surf_color(60, 255, 60, 255);
107 if (t.teamnum == 3) surf_color(255, 60, 60, 255); 284 else if (t.neutral) surf_color(220, 220, 220, 255);
108 else if (t.teamnum == 2) surf_color(60, 255, 60, 255); 285 else surf_color(255, 60, 60, 255);
109 else surf_color(255, 255, 255, 255); 286 float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width
110 float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width — swap for m_vecMins/maxs span later
111 surf_rect(0, (int)(t.sx - w / 2), (int)(t.sy - t.h), (int)(t.sx + w / 2), (int)t.sy); 287 surf_rect(0, (int)(t.sx - w / 2), (int)(t.sy - t.h), (int)(t.sx + w / 2), (int)t.sy);
112 // health bar under box 288 if (t.show_health) {
113 float frac = t.health / t.maxhealth; 289 float frac = t.health / (t.maxhealth > 1.0f ? t.maxhealth : 100.0f);
114 if (frac > 1) frac = 1; 290 if (frac > 1) frac = 1;
115 surf_color(0, 0, 0, 200); 291 surf_color(0, 0, 0, 200);
116 surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 1, (int)(t.sx + w / 2), (int)t.sy + 4); 292 surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 1, (int)(t.sx + w / 2), (int)t.sy + 4);
117 if (frac > 0.5f) surf_color(40, 255, 40, 255); 293 if (frac > 0.5f) surf_color(40, 255, 40, 255);
118 else if (frac > 0.25f) surf_color(255, 255, 0, 255); 294 else if (frac > 0.25f) surf_color(255, 255, 0, 255);
119 else surf_color(255, 40, 40, 255); 295 else surf_color(255, 40, 40, 255);
120 surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 2, 296 surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 2,
121 (int)(t.sx - w / 2 + w * frac), (int)t.sy + 3); 297 (int)(t.sx - w / 2 + w * frac), (int)t.sy + 3);
122 draw_text_col((int)(t.sx - w / 2), (int)(t.sy - t.h) - 14, t.name, 255, 255, 255); 298 }
299 char label[64];
300 if (t.show_health)
301 snprintf(label, sizeof(label), "%s %d", t.name, (int)t.health);
302 else
303 snprintf(label, sizeof(label), "%s", t.name);
304 draw_text_col((int)(t.sx - w / 2), (int)(t.sy - t.h) - 14, label, 255, 255, 255);
123 } 305 }
124} 306}