summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-26 15:33:02 +0100
committersalmonik <salmonik@tuta.io>2026-10-26 15:33:02 +0100
commitd7edcc9d9e3d90e1c53885870461e046389dee96 (patch)
treed4e80c026f65cbff9804c80184293d0c1b63afa8
parentDefault map d1_town_01 (intro-less, enemies spawned) (diff)
downloadhl2h-d7edcc9d9e3d90e1c53885870461e046389dee96.tar.gz
hl2h-d7edcc9d9e3d90e1c53885870461e046389dee96.zip
ESP working: NPC detection + authoritative server-side health
- entitylist slot 6 (GetHighestEntityIndex); class name via GetClientNetworkable -> GetClientClass (replaces the unsafe slot-2 call) - HL2 SP NPCs: class CAI_BaseNPC + m_lifeState, since NPC m_iHealth is not networked; model names from VModelInfoClient006 for labels/friend-foe - real HP/HP-max from the listen-server entity (IVEngineServer021 -> PEntityOfEntIndex -> edict->m_pUnk -> GetBaseEntity) using offsets scanned from server.so's datamap; validated against the client model index - world_to_screen via IVDebugOverlay003::ScreenPosition (fixes box drift) - maxs sanity clamp, NPC-only debug logging, neutral colour for wildlife - build.sh: restore -o libhl2hake.so and add server.cpp
Diffstat (limited to '')
-rwxr-xr-xbuild.sh3
-rw-r--r--src/cinput.cpp1
-rw-r--r--src/esp.cpp262
-rw-r--r--src/hl2h.cpp9
-rw-r--r--src/hl2h.h15
-rw-r--r--src/modules.cpp3
-rw-r--r--src/netvars.cpp67
-rw-r--r--src/server.cpp177
8 files changed, 477 insertions, 60 deletions
diff --git a/build.sh b/build.sh
index 6e54ccf..d1538ad 100755
--- a/build.sh
+++ b/build.sh
@@ -2,6 +2,7 @@
2set -e 2set -e
3cd "$(dirname "$0")" 3cd "$(dirname "$0")"
4g++ -m32 -O2 -std=c++17 -Wall -Wextra -Werror -fPIC -shared -fvisibility=hidden \ 4g++ -m32 -O2 -std=c++17 -Wall -Wextra -Werror -fPIC -shared -fvisibility=hidden \
5 src/hl2h.cpp src/modules.cpp src/cinput.cpp src/netvars.cpp src/menu.cpp src/esp.cpp \ 5 -o libhl2hake.so \
6 src/hl2h.cpp src/modules.cpp src/cinput.cpp src/netvars.cpp src/menu.cpp src/esp.cpp src/server.cpp \
6 -ldl 7 -ldl
7file libhl2hake.so 8file libhl2hake.so
diff --git a/src/cinput.cpp b/src/cinput.cpp
index 7c40b3b..f4ba322 100644
--- a/src/cinput.cpp
+++ b/src/cinput.cpp
@@ -1,5 +1,6 @@
1// cinput.cpp — CInput discovery, usercmd CRC, get/fix helpers 1// cinput.cpp — CInput discovery, usercmd CRC, get/fix helpers
2#include "hl2h.h" 2#include "hl2h.h"
3uintptr_t g_input = 0;
3 4
4// --------------------------------------------------------------------------- 5// ---------------------------------------------------------------------------
5// CRC-32 (standard IEEE reflected, matches Valve's implementation) 6// CRC-32 (standard IEEE reflected, matches Valve's implementation)
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}
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index 01fdb0f..a451a18 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -29,7 +29,7 @@ void logf(const char *fmt, ...) {
29// globals 29// globals
30// --------------------------------------------------------------------------- 30// ---------------------------------------------------------------------------
31 31
32Module g_client, g_engine; 32Module g_client, g_engine, g_server;
33GameInterfaces g_iface; 33GameInterfaces g_iface;
34NetVars g_net; 34NetVars g_net;
35Vector g_cam_eye{0, 0, 0}; 35Vector g_cam_eye{0, 0, 0};
@@ -335,6 +335,13 @@ static void *setup_thread(void *) {
335 "VEngineClient014=%p", 335 "VEngineClient014=%p",
336 g_iface.client, g_iface.entitylist, g_iface.engine); 336 g_iface.client, g_iface.entitylist, g_iface.engine);
337 337
338 g_iface.modelinfo = ef("VModelInfoClient006", nullptr);
339 g_iface.engineserver = ef("VEngineServer021", nullptr);
340 g_iface.debugoverlay = ef("VDebugOverlay003", nullptr);
341 logf("modelinfo: %p (VModelInfoClient006)", g_iface.modelinfo);
342 logf("engineserver: %p (VEngineServer021), debugoverlay: %p",
343 g_iface.engineserver, g_iface.debugoverlay);
344
338 resolve_netvars(); 345 resolve_netvars();
339 if (!find_input_global()) 346 if (!find_input_global())
340 return nullptr; 347 return nullptr;
diff --git a/src/hl2h.h b/src/hl2h.h
index d6a005e..b693617 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -36,7 +36,7 @@ struct Module {
36 return false; 36 return false;
37 } 37 }
38}; 38};
39extern Module g_client, g_engine; 39extern Module g_client, g_engine, g_server;
40 40
41// --------------------------------------------------------------------------- 41// ---------------------------------------------------------------------------
42// game interfaces 42// game interfaces
@@ -48,6 +48,9 @@ struct GameInterfaces {
48 void *client = nullptr; 48 void *client = nullptr;
49 void *entitylist = nullptr; 49 void *entitylist = nullptr;
50 void *engine = nullptr; 50 void *engine = nullptr;
51 void *modelinfo = nullptr; // IVModelInfoClient006 (model index -> model name)
52 void *engineserver = nullptr; // IVEngineServer021 (edict access, incl. server entities)
53 void *debugoverlay = nullptr; // IVDebugOverlay003 (engine-accurate world->screen)
51}; 54};
52extern GameInterfaces g_iface; 55extern GameInterfaces g_iface;
53 56
@@ -70,9 +73,13 @@ struct NetVars {
70 int viewoffset = -1; 73 int viewoffset = -1;
71 int mins = -1; 74 int mins = -1;
72 int maxs = -1; 75 int maxs = -1;
76 int lifestate = -1;
77 int modelindex = -1;
73 // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 78 // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72
74 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } 79 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; }
75 bool esp_ok() const { return ok() && health >= 0 && maxhealth >= 0 && teamnum >= 0 && origin >= 0 && viewoffset >= 0; } 80 // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100.
81 // m_iHealth only ever updates for players — NPCs are detected via m_lifeState.
82 bool esp_ok() const { return ok() && health >= 0 && teamnum >= 0 && origin >= 0 && viewoffset >= 0; }
76}; 83};
77extern NetVars g_net; 84extern NetVars g_net;
78 85
@@ -139,6 +146,10 @@ void fix_verified_crc(int seq, uint32_t crc);
139uint32_t usercmd_checksum(const CUserCmd *c); 146uint32_t usercmd_checksum(const CUserCmd *c);
140bool find_input_global(); 147bool find_input_global();
141void resolve_netvars(); 148void resolve_netvars();
149int class_health_off(const char *name);
150bool resolve_server_netvars();
151int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out);
152int server_model_index(void *base);
142void find_modules(); 153void find_modules();
143void init_conmsg(); 154void init_conmsg();
144bool feature_active(int feature); 155bool feature_active(int feature);
diff --git a/src/modules.cpp b/src/modules.cpp
index d20fe04..70e2dff 100644
--- a/src/modules.cpp
+++ b/src/modules.cpp
@@ -11,6 +11,7 @@ static bool basename_is(const char *path, const char *base) {
11void find_modules() { 11void find_modules() {
12 g_client = Module(); 12 g_client = Module();
13 g_engine = Module(); 13 g_engine = Module();
14 g_server = Module();
14 FILE *f = fopen("/proc/self/maps", "r"); 15 FILE *f = fopen("/proc/self/maps", "r");
15 if (!f) 16 if (!f)
16 return; 17 return;
@@ -41,6 +42,8 @@ void find_modules() {
41 m = &g_client; 42 m = &g_client;
42 else if (basename_is(clean, "engine.so")) 43 else if (basename_is(clean, "engine.so"))
43 m = &g_engine; 44 m = &g_engine;
45 else if (basename_is(clean, "server.so"))
46 m = &g_server;
44 } else { 47 } else {
45 // anon: absorb only as .bss tail after a file-backed range 48 // anon: absorb only as .bss tail after a file-backed range
46 if (g_client.valid() && g_client.ranges.back().file && 49 if (g_client.valid() && g_client.ranges.back().file &&
diff --git a/src/netvars.cpp b/src/netvars.cpp
index 5c8c296..f702c41 100644
--- a/src/netvars.cpp
+++ b/src/netvars.cpp
@@ -27,7 +27,24 @@ static bool module_string(uintptr_t p, char *out, size_t outn) {
27 27
28static int g_dumped_tables = 0; 28static int g_dumped_tables = 0;
29 29
30static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { 30// per-client-class health offset (m_iHealth lives at different offsets per
31// class chain: CBasePlayer via DT_BasePlayer=128, NPCs via DT_BaseCombatCharacter)
32static struct {
33 char name[32];
34 int off;
35} g_class_health[256];
36static int g_class_health_n = 0;
37
38int class_health_off(const char *name) {
39 for (int i = 0; i < g_class_health_n; i++)
40 if (strcmp(name, g_class_health[i].name) == 0)
41 return g_class_health[i].off;
42 return -1;
43}
44
45static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth,
46 int *out_health)
47{
31 if (depth > 8) 48 if (depth > 8)
32 return; 49 return;
33 uintptr_t props = *(uintptr_t *)(table + 0x00); 50 uintptr_t props = *(uintptr_t *)(table + 0x00);
@@ -67,13 +84,19 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
67 if (dump) 84 if (dump)
68 logf(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2, 85 logf(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2,
69 ptype, poff, poff, ptable ? "yes" : "no", parray ? "yes" : "no"); 86 ptype, poff, poff, ptable ? "yes" : "no", parray ? "yes" : "no");
70
71 if (ptable) { 87 if (ptable) {
72 collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1); 88 collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1,
89 out_health);
73 continue; 90 continue;
74 } 91 }
75 92
76 if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { 93 if (out_health && *out_health < 0 && strcmp(pname2, "m_iHealth") == 0)
94 *out_health = (int)(base_off + (uint32_t)poff);
95 if (strcmp(pname2, "m_lifeState") == 0 && g_net.lifestate < 0) {
96 g_net.lifestate = (int)(base_off + (uint32_t)poff);
97 } else if (strcmp(pname2, "m_nModelIndex") == 0 && g_net.modelindex < 0) {
98 g_net.modelindex = (int)(base_off + (uint32_t)poff);
99 } else if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) {
77 g_net.flags = (int)(base_off + (uint32_t)poff); 100 g_net.flags = (int)(base_off + (uint32_t)poff);
78 } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) { 101 } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) {
79 if (parray && g_client.contains(parray)) { 102 if (parray && g_client.contains(parray)) {
@@ -95,7 +118,8 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
95 } else { 118 } else {
96 g_net.origin = (int)(base_off + (uint32_t)poff); 119 g_net.origin = (int)(base_off + (uint32_t)poff);
97 } 120 }
98 } else if (strcmp(pname2, "m_vecViewOffset") == 0 && g_net.viewoffset < 0) { 121 } else if (strncmp(pname2, "m_vecViewOffset", 15) == 0 &&
122 g_net.viewoffset < 0) {
99 if (parray && g_client.contains(parray)) { 123 if (parray && g_client.contains(parray)) {
100 int32_t eoff = *(int32_t *)(parray + 0x2C); 124 int32_t eoff = *(int32_t *)(parray + 0x2C);
101 g_net.viewoffset = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); 125 g_net.viewoffset = (int)(base_off + (uint32_t)(eoff ? eoff : poff));
@@ -117,9 +141,7 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
117 g_net.maxs = (int)(base_off + (uint32_t)poff); 141 g_net.maxs = (int)(base_off + (uint32_t)poff);
118 } 142 }
119 } 143 }
120 if (g_net.esp_ok()) 144 }
121 return;
122}
123} 145}
124 146
125// prediction datamap scan for m_MoveType (FIELD_CHARACTER, fieldOffset in range) 147// prediction datamap scan for m_MoveType (FIELD_CHARACTER, fieldOffset in range)
@@ -195,21 +217,34 @@ void resolve_netvars() {
195 for (uintptr_t cc = head; cc && g_client.contains(cc) && classes < 1024; 217 for (uintptr_t cc = head; cc && g_client.contains(cc) && classes < 1024;
196 cc = *(uintptr_t *)(cc + 0x10)) { 218 cc = *(uintptr_t *)(cc + 0x10)) {
197 classes++; 219 classes++;
198 collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0); 220 char cname[32];
199 if (g_net.esp_ok() || (g_net.flags >= 0 && g_net.velocity >= 0 && 221 int hoff = -1;
200 g_net.health < 0)) 222 // per-class health offset — NPCs send m_iHealth via their own table chain,
201 break; 223 // not DT_BasePlayer's; g_net.health keeps the player's for aimbot use
224 if (module_string(*(uintptr_t *)(cc + 0x08), cname, sizeof(cname))) {
225 collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0, &hoff);
226 if (hoff >= 0 && g_class_health_n < 256) {
227 char *dst = g_class_health[g_class_health_n].name;
228 const char *src = cname;
229 size_t n = sizeof(g_class_health[g_class_health_n].name) - 1;
230 for (size_t i = 0; i < n && src[i]; i++) dst[i] = src[i];
231 dst[n] = 0;
232 g_class_health[g_class_health_n].off = hoff;
233 g_class_health_n++;
234 }
235 }
202 } 236 }
203 logf("client classes walked: %d", classes); 237 logf("client classes walked: %d, health-offset classes: %d", classes,
238 g_class_health_n);
204 239
205 if (g_net.movetype < 0 && !resolve_movetype()) { 240 if (g_net.movetype < 0 && !resolve_movetype()) {
206 // movetype unresolved — bhop still works, just no noclip/ladder guard 241 // movetype unresolved — bhop still works, just no noclip/ladder guard
207 logf("movetype guard disabled"); 242 logf("movetype guard disabled");
208 } 243 }
209
210 logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " 244 logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d "
211 "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d", 245 "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d "
246 "modelindex=%d",
212 g_net.flags, g_net.velocity, g_net.movetype, g_net.health, 247 g_net.flags, g_net.velocity, g_net.movetype, g_net.health,
213 g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, 248 g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset,
214 g_net.mins, g_net.maxs); 249 g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex);
215} 250}
diff --git a/src/server.cpp b/src/server.cpp
new file mode 100644
index 0000000..6feb55b
--- /dev/null
+++ b/src/server.cpp
@@ -0,0 +1,177 @@
1// server.cpp — authoritative listen-server entity access.
2//
3// HL2 single-player runs the server (server.so) in the same process as the
4// client. The client only receives fields the server actually networks (NPC
5// health is *not* networked), but the server's CBaseEntity still holds the real
6// m_iHealth. We reach it through IVEngineServer021:
7// PEntityOfEntIndex(engine, idx) -> edict_t*
8// edict->m_pUnk (offset 0xC) -> IServerUnknown*
9// IServerUnknown::GetBaseEntity (vtable slot 6) -> CBaseEntity*
10// then read the field offsets from the server datamap, which we resolve by
11// scanning server.so for the typedescription_t entries referencing the field
12// names (same idea as the client RecvTable walk).
13
14#include "hl2h.h"
15#include <cstdlib>
16
17// IVEngineServer (VEngineServer021, no virtual destructor):
18// slot 17 = GetEntityCount, slot 19 = PEntityOfEntIndex(int)
19enum { SRV_GetEntityCount = 17, SRV_PEntityOfEntIndex = 19 };
20// IServerUnknown derives IHandleEntity (virtual dtor = 2 slots, 2 methods),
21// then adds GetCollideable(4), GetNetworkable(5), GetBaseEntity(6).
22enum { SRV_GetBaseEntity = 6 };
23// CBaseEdict layout: m_fStateFlags@0, serial/index@4, m_pNetworkable@8, m_pUnk@0xC
24enum { EDICT_UNKNOWN_OFF = 0x0C };
25
26// typedescription_t (32-bit): fieldType@0, fieldName@4, fieldOffset[0]@8,
27// fieldOffset[1]@0xC, fieldSize@0x10, flags@0x12, externalName@0x14
28enum { FIELD_INTEGER = 5, FIELD_CHARACTER = 8 };
29
30int g_srv_health = -1;
31int g_srv_maxhealth = -1;
32int g_srv_lifestate = -1;
33int g_srv_modelindex = -1;
34static bool g_srv_ready = false;
35static bool g_srv_checked = false;
36
37static uintptr_t find_string(const char *s) {
38 size_t n = strlen(s) + 1;
39 for (auto &r : g_server.ranges) {
40 const uint8_t *base = (const uint8_t *)r.lo;
41 size_t span = r.hi - r.lo;
42 for (size_t i = 0; i + n <= span; i++)
43 if (memcmp(base + i, s, n) == 0)
44 return r.lo + i;
45 }
46 return 0;
47}
48
49// Find the typedescription_t whose fieldName points at `name` and read its
50// fieldOffset. type filters false positives (m_iHealth is FIELD_INTEGER).
51static int datamap_offset(const char *name, int want_type) {
52 uintptr_t str = find_string(name);
53 if (!str)
54 return -1;
55 for (auto &r : g_server.ranges) {
56 const uint8_t *base = (const uint8_t *)r.lo;
57 size_t span = r.hi - r.lo;
58 for (size_t off = 4; off + 0x18 <= span; off += 4) {
59 if (*(const uint32_t *)(base + off) != (uint32_t)str)
60 continue;
61 const uint8_t *e = base + off - 4; // entry->fieldName
62 int ftype = *(const int32_t *)(e + 0x00);
63 int foff = *(const int32_t *)(e + 0x08);
64 int fsize = *(const uint16_t *)(e + 0x10);
65 uintptr_t ext = *(const uintptr_t *)(e + 0x14);
66 if (ftype != want_type)
67 continue;
68 if (foff < 0x40 || foff > 0x2000)
69 continue;
70 if (fsize < 1 || fsize > 64)
71 continue;
72 // DEFINE_KEYFIELD entries have an external name ("health"); plain
73 // DEFINE_FIELD entries have none. Either way it must be in server.so.
74 if (ext && !g_server.contains(ext))
75 continue;
76 return foff;
77 }
78 }
79 return -1;
80}
81
82bool resolve_server_netvars() {
83 if (g_srv_ready)
84 return true;
85 static const bool disabled = getenv("HL2H_NO_SERVER") != nullptr;
86 if (disabled) // escape hatch if server-side access misbehaves
87 return false;
88 // esp_collect runs at ~100 Hz; retry the (cheap-ish) module refresh and
89 // datamap scan about once a second until server.so is up and scan succeeds.
90 static unsigned retry = 0;
91 static bool fail_logged = false;
92 if ((retry++ % 100) != 0)
93 return false;
94 // server.so is only mapped once a map is running; refresh the module list.
95 if (!g_server.valid())
96 find_modules();
97 if (!g_server.valid()) {
98 if (!fail_logged) {
99 fail_logged = true;
100 logf("server datamap: server.so not mapped yet");
101 }
102 return false;
103 }
104 g_srv_health = datamap_offset("m_iHealth", FIELD_INTEGER);
105 g_srv_maxhealth = datamap_offset("m_iMaxHealth", FIELD_INTEGER);
106 g_srv_lifestate = datamap_offset("m_lifeState", FIELD_CHARACTER);
107 g_srv_modelindex = datamap_offset("m_nModelIndex", 7); // FIELD_SHORT
108 g_srv_ready = g_srv_health > 0;
109 if (g_srv_ready) {
110 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d "
111 "modelindex=%d",
112 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate,
113 g_srv_modelindex);
114 fail_logged = false;
115 } else if (!fail_logged) {
116 fail_logged = true;
117 logf("server datamap: scan failed (server.so at %s)",
118 g_server.path.c_str());
119 }
120 return g_srv_ready;
121}
122
123// Returns the authoritative health of the server entity at this index, or -1.
124int server_entity_health(int idx, int *maxhealth, int *lifestate,
125 void **base_out) {
126 if (base_out)
127 *base_out = nullptr;
128 if (!g_srv_ready || !g_iface.engineserver || g_srv_health < 0)
129 return -1;
130
131 using PEntityFn = void *(*)(void *, int);
132 void *edict = vfunc<PEntityFn>(g_iface.engineserver, SRV_PEntityOfEntIndex)(
133 g_iface.engineserver, idx);
134 if (!edict)
135 return -1;
136 void *unk = *(void **)((uintptr_t)edict + EDICT_UNKNOWN_OFF);
137 if (!unk)
138 return -1;
139
140 using GetBaseFn = void *(*)(void *);
141 void *base = vfunc<GetBaseFn>(unk, SRV_GetBaseEntity)(unk);
142 if (!base)
143 return -1;
144
145 uintptr_t b = (uintptr_t)base;
146 int hp = g_srv_health > 0 ? *(int *)(b + g_srv_health) : -1;
147 int mx = g_srv_maxhealth > 0 ? *(int *)(b + g_srv_maxhealth) : -1;
148 int ls = g_srv_lifestate > 0
149 ? (int)*(unsigned char *)(b + g_srv_lifestate)
150 : -1;
151 if (base_out)
152 *base_out = base;
153 if (maxhealth)
154 *maxhealth = mx;
155 if (lifestate)
156 *lifestate = ls;
157
158 if (!g_srv_checked) {
159 g_srv_checked = true;
160 using CountFn = int (*)(void *);
161 int count = vfunc<CountFn>(g_iface.engineserver, SRV_GetEntityCount)(
162 g_iface.engineserver);
163 logf("server access: entity_count=%d first_idx=%d base=%p hp=%d maxhp=%d "
164 "life=%d srv_model=%d",
165 count, idx, base, hp, mx, ls,
166 g_srv_modelindex > 0
167 ? (int)*(short *)(b + g_srv_modelindex)
168 : -1);
169 }
170 return hp;
171}
172
173int server_model_index(void *base) {
174 if (!base || g_srv_modelindex <= 0)
175 return -1;
176 return (int)*(short *)((uintptr_t)base + g_srv_modelindex);
177}