From d7edcc9d9e3d90e1c53885870461e046389dee96 Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 26 Oct 2026 15:33:02 +0100 Subject: 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 --- build.sh | 3 +- src/cinput.cpp | 1 + src/esp.cpp | 262 +++++++++++++++++++++++++++++++++++++++++++++++--------- src/hl2h.cpp | 9 +- src/hl2h.h | 15 +++- src/modules.cpp | 3 + src/netvars.cpp | 67 +++++++++++---- src/server.cpp | 177 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 477 insertions(+), 60 deletions(-) create mode 100644 src/server.cpp diff --git a/build.sh b/build.sh index 6e54ccf..d1538ad 100755 --- a/build.sh +++ b/build.sh @@ -2,6 +2,7 @@ set -e cd "$(dirname "$0")" g++ -m32 -O2 -std=c++17 -Wall -Wextra -Werror -fPIC -shared -fvisibility=hidden \ - src/hl2h.cpp src/modules.cpp src/cinput.cpp src/netvars.cpp src/menu.cpp src/esp.cpp \ + -o libhl2hake.so \ + src/hl2h.cpp src/modules.cpp src/cinput.cpp src/netvars.cpp src/menu.cpp src/esp.cpp src/server.cpp \ -ldl file 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 @@ // cinput.cpp — CInput discovery, usercmd CRC, get/fix helpers #include "hl2h.h" +uintptr_t g_input = 0; // --------------------------------------------------------------------------- // 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 @@ #include #include #include +#include static constexpr float PI_F = 3.14159265358979323846f; @@ -12,11 +13,15 @@ enum SurfSlot { SURF_GetScreenSize = 38, }; +enum { LIFE_ALIVE = 0 }; + struct EspTarget { float sx, sy; // screen pos of feet float h; // box height px float health, maxhealth; - int teamnum; + bool show_health; + bool friendly; + bool neutral; char name[48]; }; static EspTarget g_targets[128]; @@ -24,7 +29,27 @@ static int g_ntargets = 0; static int g_scr_w = 0, g_scr_h = 0; static bool g_esp_logged = false; +static double esp_now() { + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + bool world_to_screen(const Vector &w, float &sx, float &sy) { + // Preferred: the engine's own transform. Uses the real view setup (FOV, + // aspect, roll), so boxes do not drift at the screen edges. + if (g_iface.debugoverlay) { + using Fn = int (*)(void *, const Vector *, Vector *); + Vector s; + int behind = + vfunc(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s); + if (behind) + return false; + sx = s.x; + sy = s.y; + return true; + } + // Fallback: local math with the default HL2 FOV. float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; float sp = sinf(pitch), cp = cosf(pitch); @@ -43,39 +68,195 @@ bool world_to_screen(const Vector &w, float &sx, float &sy) { return true; } +// --------------------------------------------------------------------------- +// entity helpers (IClientEntityList / IClientNetworkable, header-documented) +// --------------------------------------------------------------------------- + +using GetNetFn = void *(*)(void *, int); +using GetCCFn = void *(*)(void *); +using GetModelFn = const void *(*)(void *, int); +using GetModelNameFn = const char *(*)(void *, const void *); + +// entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the +// IClientNetworkable subobject offset inside the entity (see projectz). +static void *entity_networkable(int idx) { + return vfunc(g_iface.entitylist, 0)(g_iface.entitylist, idx); +} + +// IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8. +static 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; +} + +// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. +// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. +static 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); + if (modelindex <= 0) return nullptr; + const void *model = + vfunc(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); + if (!model) + return nullptr; + return vfunc(g_iface.modelinfo, 4)(g_iface.modelinfo, model); +} + +// 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, ...). +static bool is_npc_class(const char *name) { + if (!name) return false; + return strstr(name, "NPC") != nullptr || + strstr(name, "BaseHelicopter") != nullptr; +} + +static bool model_is_friendly(const char *m) { + static const char *patterns[] = { + "citizen", "humans/", "barney", "alyx", "alex", "scientist", + "kleiner", "eli", "mossman", "vortigaunt", "dog", "grigori", + "monk", "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) { + static const char *patterns[] = {"crow", "gull", "pigeon", "seagull", + "bird", nullptr}; + if (!m) return false; + for (int i = 0; patterns[i]; i++) + if (strstr(m, patterns[i])) + return true; + return false; +} + +static 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; +} + static void esp_collect(int local_idx) { + // outer_draw() runs once per PaintTraverseEx call (several per frame); cache + // the scan briefly so we do not walk the entity list dozens of times a frame. + static double last_scan = 0.0; + static double last_dbg = 0.0; + double now = esp_now(); + if (now - last_scan < 0.01) + return; + last_scan = now; + bool dbg = (now - last_dbg) >= 2.0; + if (dbg) last_dbg = now; + g_ntargets = 0; using Fn = int (*)(void *); - int maxent = vfunc(g_iface.entitylist, 5)(g_iface.entitylist); // GetHighestEntityIndex - if (maxent < 0 || maxent > 512) maxent = 512; - for (int idx = 0; idx <= maxent && g_ntargets < 128; idx++) { + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); // GetHighestEntityIndex + if (hi < 0 || hi > 4096) hi = 4096; + int npc_alive = 0, dbg_lines = 0; + bool srv = resolve_server_netvars(); + static bool srv_checked = false; + static bool srv_trusted = false; + + for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) { if (idx == local_idx) continue; void *ent = entitylist_get(idx); if (!ent) continue; + void *net = entity_networkable(idx); + if (!net) continue; + const char *cls = entity_class_name(net, nullptr); + if (!cls) continue; + uintptr_t e = (uintptr_t)ent; - float hpv = (float)*(int *)(e + g_net.health); // m_iHealth is int - if (!(hpv > 0)) continue; + int cli_life = g_net.lifestate >= 0 + ? (int)*(unsigned char *)(e + g_net.lifestate) + : -1; + int cli_hp = g_net.health >= 0 ? *(int *)(e + g_net.health) : 0; + bool npc = is_npc_class(cls); + const char *model = entity_model_name(ent); + + // NPC health is not networked; read it from the listen-server entity. + int hp = cli_hp, maxhp = -1, life = cli_life; + if (npc && srv) { + int slife = -1, smax = -1; + void *sbase = nullptr; + int shp = server_entity_health(idx, &smax, &slife, &sbase); + if (!srv_checked && sbase) { + srv_checked = true; + int cli_mi = + g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1; + int srv_mi = server_model_index(sbase); + srv_trusted = (cli_mi == srv_mi); + logf("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d " + "hp=%d maxhp=%d life=%d trusted=%d", + idx, sbase, cli_mi, srv_mi, shp, smax, slife, (int)srv_trusted); + if (!srv_trusted) + logf("ESP: server entity mismatch, ignoring server values"); + } + if (srv_trusted) { + if (shp >= 0) hp = shp; + if (smax > 0) maxhp = smax; + if (slife >= 0) life = slife; + } + } + + if (dbg && npc && dbg_lines < 40) { + dbg_lines++; + logf("ESP npc %d cls=%s life=%d(cli %d) hp=%d(cli %d) maxhp=%d model=%s", + idx, cls, life, cli_life, hp, cli_hp, maxhp, + model ? model : "?"); + } + + if (npc) { + if (life != LIFE_ALIVE) continue; + npc_alive++; + } else { + // players (HL2DM) are the only entities with a live networked m_iHealth + if (!strstr(cls, "Player") || !(hp > 0)) continue; + } + + float minz = g_net.mins >= 0 ? *(float *)(e + g_net.mins + 8) : 0.0f; + float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; + if (!(maxz > minz + 4.0f && maxz - minz < 512.0f)) { + minz = 0.0f; + maxz = 72.0f; + } Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), *(float *)(e + g_net.origin + 8)}; - // ponytail: maxs netvar may be unresolved; fixed 72-unit torso fallback - float maxsz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs) : 72.0f; - Vector top{origin.x, origin.y, origin.z + maxsz}; + Vector bottom{origin.x, origin.y, origin.z + minz}; + Vector top{origin.x, origin.y, origin.z + maxz}; float sx, sy, tx, ty; - if (!world_to_screen(origin, sx, sy) || !world_to_screen(top, tx, ty)) + if (!world_to_screen(bottom, sx, sy) || !world_to_screen(top, tx, ty)) continue; float h = sy - ty; - if (h < 4.0f || sy < 0 || ty > (float)g_scr_h) continue; // behind / offscreen + if (h < 3.0f || sy < 0 || ty > (float)g_scr_h) continue; // behind / offscreen + if (sx < -(float)g_scr_w || sx > 2.0f * (float)g_scr_w) continue; + EspTarget &t = g_targets[g_ntargets++]; t.sx = sx; t.sy = sy; t.h = h; - t.health = hpv; - t.maxhealth = (float)*(int *)(e + g_net.maxhealth); - if (t.maxhealth <= 0) t.maxhealth = hpv; - // classname via IClientNetworkable::GetClientClass (slot 2) -> m_pNetworkName @ +8 - using CCFn = void *(*)(void *); - void *cc = vfunc(ent, 2)(ent); - const char *nn = cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; - strncpy(t.name, nn ? nn : "?", sizeof(t.name) - 1); - t.name[sizeof(t.name) - 1] = 0; + t.health = hp > 0 ? (float)hp : 0.0f; + t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; + t.show_health = hp > 0; + t.friendly = model_is_friendly(model); + t.neutral = model_is_neutral(model); + short_name(model, t.name, sizeof(t.name)); + if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); + } + + if (dbg) + logf("ESP scan: hi=%d npc_alive=%d targets=%d", hi, npc_alive, g_ntargets); + if (g_ntargets && !g_esp_logged) { + g_esp_logged = true; + logf("ESP: first targets, %d", g_ntargets); } } @@ -97,28 +278,29 @@ void esp_draw() { if (!g_scr_w) return; } esp_collect(engine_get_local_player()); - if (g_ntargets && !g_esp_logged) { - g_esp_logged = true; - logf("ESP: first draw, %d targets", g_ntargets); - } for (int i = 0; i < g_ntargets; i++) { const EspTarget &t = g_targets[i]; - // team color: 3 combine red, 2 rebels green, else white - if (t.teamnum == 3) surf_color(255, 60, 60, 255); - else if (t.teamnum == 2) surf_color(60, 255, 60, 255); - else surf_color(255, 255, 255, 255); - float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width — swap for m_vecMins/maxs span later + if (t.friendly) surf_color(60, 255, 60, 255); + else if (t.neutral) surf_color(220, 220, 220, 255); + else surf_color(255, 60, 60, 255); + float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width surf_rect(0, (int)(t.sx - w / 2), (int)(t.sy - t.h), (int)(t.sx + w / 2), (int)t.sy); - // health bar under box - float frac = t.health / t.maxhealth; - if (frac > 1) frac = 1; - surf_color(0, 0, 0, 200); - surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 1, (int)(t.sx + w / 2), (int)t.sy + 4); - if (frac > 0.5f) surf_color(40, 255, 40, 255); - else if (frac > 0.25f) surf_color(255, 255, 0, 255); - else surf_color(255, 40, 40, 255); - surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 2, - (int)(t.sx - w / 2 + w * frac), (int)t.sy + 3); - draw_text_col((int)(t.sx - w / 2), (int)(t.sy - t.h) - 14, t.name, 255, 255, 255); + if (t.show_health) { + float frac = t.health / (t.maxhealth > 1.0f ? t.maxhealth : 100.0f); + if (frac > 1) frac = 1; + surf_color(0, 0, 0, 200); + surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 1, (int)(t.sx + w / 2), (int)t.sy + 4); + if (frac > 0.5f) surf_color(40, 255, 40, 255); + else if (frac > 0.25f) surf_color(255, 255, 0, 255); + else surf_color(255, 40, 40, 255); + surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 2, + (int)(t.sx - w / 2 + w * frac), (int)t.sy + 3); + } + char label[64]; + if (t.show_health) + snprintf(label, sizeof(label), "%s %d", t.name, (int)t.health); + else + snprintf(label, sizeof(label), "%s", t.name); + draw_text_col((int)(t.sx - w / 2), (int)(t.sy - t.h) - 14, label, 255, 255, 255); } } 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, ...) { // globals // --------------------------------------------------------------------------- -Module g_client, g_engine; +Module g_client, g_engine, g_server; GameInterfaces g_iface; NetVars g_net; Vector g_cam_eye{0, 0, 0}; @@ -335,6 +335,13 @@ static void *setup_thread(void *) { "VEngineClient014=%p", g_iface.client, g_iface.entitylist, g_iface.engine); + g_iface.modelinfo = ef("VModelInfoClient006", nullptr); + g_iface.engineserver = ef("VEngineServer021", nullptr); + g_iface.debugoverlay = ef("VDebugOverlay003", nullptr); + logf("modelinfo: %p (VModelInfoClient006)", g_iface.modelinfo); + logf("engineserver: %p (VEngineServer021), debugoverlay: %p", + g_iface.engineserver, g_iface.debugoverlay); + resolve_netvars(); if (!find_input_global()) 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 { return false; } }; -extern Module g_client, g_engine; +extern Module g_client, g_engine, g_server; // --------------------------------------------------------------------------- // game interfaces @@ -48,6 +48,9 @@ struct GameInterfaces { void *client = nullptr; void *entitylist = nullptr; void *engine = nullptr; + void *modelinfo = nullptr; // IVModelInfoClient006 (model index -> model name) + void *engineserver = nullptr; // IVEngineServer021 (edict access, incl. server entities) + void *debugoverlay = nullptr; // IVDebugOverlay003 (engine-accurate world->screen) }; extern GameInterfaces g_iface; @@ -70,9 +73,13 @@ struct NetVars { int viewoffset = -1; int mins = -1; int maxs = -1; + int lifestate = -1; + int modelindex = -1; // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } - bool esp_ok() const { return ok() && health >= 0 && maxhealth >= 0 && teamnum >= 0 && origin >= 0 && viewoffset >= 0; } + // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100. + // m_iHealth only ever updates for players — NPCs are detected via m_lifeState. + bool esp_ok() const { return ok() && health >= 0 && teamnum >= 0 && origin >= 0 && viewoffset >= 0; } }; extern NetVars g_net; @@ -139,6 +146,10 @@ void fix_verified_crc(int seq, uint32_t crc); uint32_t usercmd_checksum(const CUserCmd *c); bool find_input_global(); void resolve_netvars(); +int class_health_off(const char *name); +bool resolve_server_netvars(); +int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); +int server_model_index(void *base); void find_modules(); void init_conmsg(); bool 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) { void find_modules() { g_client = Module(); g_engine = Module(); + g_server = Module(); FILE *f = fopen("/proc/self/maps", "r"); if (!f) return; @@ -41,6 +42,8 @@ void find_modules() { m = &g_client; else if (basename_is(clean, "engine.so")) m = &g_engine; + else if (basename_is(clean, "server.so")) + m = &g_server; } else { // anon: absorb only as .bss tail after a file-backed range 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) { static int g_dumped_tables = 0; -static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { +// per-client-class health offset (m_iHealth lives at different offsets per +// class chain: CBasePlayer via DT_BasePlayer=128, NPCs via DT_BaseCombatCharacter) +static struct { + char name[32]; + int off; +} g_class_health[256]; +static int g_class_health_n = 0; + +int class_health_off(const char *name) { + for (int i = 0; i < g_class_health_n; i++) + if (strcmp(name, g_class_health[i].name) == 0) + return g_class_health[i].off; + return -1; +} + +static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth, + int *out_health) +{ if (depth > 8) return; uintptr_t props = *(uintptr_t *)(table + 0x00); @@ -67,13 +84,19 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { if (dump) logf(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2, ptype, poff, poff, ptable ? "yes" : "no", parray ? "yes" : "no"); - if (ptable) { - collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1); + collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1, + out_health); continue; } - if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { + if (out_health && *out_health < 0 && strcmp(pname2, "m_iHealth") == 0) + *out_health = (int)(base_off + (uint32_t)poff); + if (strcmp(pname2, "m_lifeState") == 0 && g_net.lifestate < 0) { + g_net.lifestate = (int)(base_off + (uint32_t)poff); + } else if (strcmp(pname2, "m_nModelIndex") == 0 && g_net.modelindex < 0) { + g_net.modelindex = (int)(base_off + (uint32_t)poff); + } else if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { g_net.flags = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) { if (parray && g_client.contains(parray)) { @@ -95,7 +118,8 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { } else { g_net.origin = (int)(base_off + (uint32_t)poff); } - } else if (strcmp(pname2, "m_vecViewOffset") == 0 && g_net.viewoffset < 0) { + } else if (strncmp(pname2, "m_vecViewOffset", 15) == 0 && + g_net.viewoffset < 0) { if (parray && g_client.contains(parray)) { int32_t eoff = *(int32_t *)(parray + 0x2C); 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) { g_net.maxs = (int)(base_off + (uint32_t)poff); } } - if (g_net.esp_ok()) - return; -} + } } // prediction datamap scan for m_MoveType (FIELD_CHARACTER, fieldOffset in range) @@ -195,21 +217,34 @@ void resolve_netvars() { for (uintptr_t cc = head; cc && g_client.contains(cc) && classes < 1024; cc = *(uintptr_t *)(cc + 0x10)) { classes++; - collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0); - if (g_net.esp_ok() || (g_net.flags >= 0 && g_net.velocity >= 0 && - g_net.health < 0)) - break; + char cname[32]; + int hoff = -1; + // per-class health offset — NPCs send m_iHealth via their own table chain, + // not DT_BasePlayer's; g_net.health keeps the player's for aimbot use + if (module_string(*(uintptr_t *)(cc + 0x08), cname, sizeof(cname))) { + collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0, &hoff); + if (hoff >= 0 && g_class_health_n < 256) { + char *dst = g_class_health[g_class_health_n].name; + const char *src = cname; + size_t n = sizeof(g_class_health[g_class_health_n].name) - 1; + for (size_t i = 0; i < n && src[i]; i++) dst[i] = src[i]; + dst[n] = 0; + g_class_health[g_class_health_n].off = hoff; + g_class_health_n++; + } + } } - logf("client classes walked: %d", classes); + logf("client classes walked: %d, health-offset classes: %d", classes, + g_class_health_n); if (g_net.movetype < 0 && !resolve_movetype()) { // movetype unresolved — bhop still works, just no noclip/ladder guard logf("movetype guard disabled"); } - logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " - "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d", + "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d " + "modelindex=%d", g_net.flags, g_net.velocity, g_net.movetype, g_net.health, g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, - g_net.mins, g_net.maxs); + g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex); } 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 @@ +// server.cpp — authoritative listen-server entity access. +// +// HL2 single-player runs the server (server.so) in the same process as the +// client. The client only receives fields the server actually networks (NPC +// health is *not* networked), but the server's CBaseEntity still holds the real +// m_iHealth. We reach it through IVEngineServer021: +// PEntityOfEntIndex(engine, idx) -> edict_t* +// edict->m_pUnk (offset 0xC) -> IServerUnknown* +// IServerUnknown::GetBaseEntity (vtable slot 6) -> CBaseEntity* +// then read the field offsets from the server datamap, which we resolve by +// scanning server.so for the typedescription_t entries referencing the field +// names (same idea as the client RecvTable walk). + +#include "hl2h.h" +#include + +// IVEngineServer (VEngineServer021, no virtual destructor): +// slot 17 = GetEntityCount, slot 19 = PEntityOfEntIndex(int) +enum { SRV_GetEntityCount = 17, SRV_PEntityOfEntIndex = 19 }; +// IServerUnknown derives IHandleEntity (virtual dtor = 2 slots, 2 methods), +// then adds GetCollideable(4), GetNetworkable(5), GetBaseEntity(6). +enum { SRV_GetBaseEntity = 6 }; +// CBaseEdict layout: m_fStateFlags@0, serial/index@4, m_pNetworkable@8, m_pUnk@0xC +enum { EDICT_UNKNOWN_OFF = 0x0C }; + +// typedescription_t (32-bit): fieldType@0, fieldName@4, fieldOffset[0]@8, +// fieldOffset[1]@0xC, fieldSize@0x10, flags@0x12, externalName@0x14 +enum { FIELD_INTEGER = 5, FIELD_CHARACTER = 8 }; + +int g_srv_health = -1; +int g_srv_maxhealth = -1; +int g_srv_lifestate = -1; +int g_srv_modelindex = -1; +static bool g_srv_ready = false; +static bool g_srv_checked = false; + +static uintptr_t find_string(const char *s) { + size_t n = strlen(s) + 1; + for (auto &r : g_server.ranges) { + const uint8_t *base = (const uint8_t *)r.lo; + size_t span = r.hi - r.lo; + for (size_t i = 0; i + n <= span; i++) + if (memcmp(base + i, s, n) == 0) + return r.lo + i; + } + return 0; +} + +// Find the typedescription_t whose fieldName points at `name` and read its +// fieldOffset. type filters false positives (m_iHealth is FIELD_INTEGER). +static int datamap_offset(const char *name, int want_type) { + uintptr_t str = find_string(name); + if (!str) + return -1; + for (auto &r : g_server.ranges) { + const uint8_t *base = (const uint8_t *)r.lo; + size_t span = r.hi - r.lo; + for (size_t off = 4; off + 0x18 <= span; off += 4) { + if (*(const uint32_t *)(base + off) != (uint32_t)str) + continue; + const uint8_t *e = base + off - 4; // entry->fieldName + int ftype = *(const int32_t *)(e + 0x00); + int foff = *(const int32_t *)(e + 0x08); + int fsize = *(const uint16_t *)(e + 0x10); + uintptr_t ext = *(const uintptr_t *)(e + 0x14); + if (ftype != want_type) + continue; + if (foff < 0x40 || foff > 0x2000) + continue; + if (fsize < 1 || fsize > 64) + continue; + // DEFINE_KEYFIELD entries have an external name ("health"); plain + // DEFINE_FIELD entries have none. Either way it must be in server.so. + if (ext && !g_server.contains(ext)) + continue; + return foff; + } + } + return -1; +} + +bool resolve_server_netvars() { + if (g_srv_ready) + return true; + static const bool disabled = getenv("HL2H_NO_SERVER") != nullptr; + if (disabled) // escape hatch if server-side access misbehaves + return false; + // esp_collect runs at ~100 Hz; retry the (cheap-ish) module refresh and + // datamap scan about once a second until server.so is up and scan succeeds. + static unsigned retry = 0; + static bool fail_logged = false; + if ((retry++ % 100) != 0) + return false; + // server.so is only mapped once a map is running; refresh the module list. + if (!g_server.valid()) + find_modules(); + if (!g_server.valid()) { + if (!fail_logged) { + fail_logged = true; + logf("server datamap: server.so not mapped yet"); + } + return false; + } + g_srv_health = datamap_offset("m_iHealth", FIELD_INTEGER); + g_srv_maxhealth = datamap_offset("m_iMaxHealth", FIELD_INTEGER); + g_srv_lifestate = datamap_offset("m_lifeState", FIELD_CHARACTER); + g_srv_modelindex = datamap_offset("m_nModelIndex", 7); // FIELD_SHORT + g_srv_ready = g_srv_health > 0; + if (g_srv_ready) { + logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " + "modelindex=%d", + g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, + g_srv_modelindex); + fail_logged = false; + } else if (!fail_logged) { + fail_logged = true; + logf("server datamap: scan failed (server.so at %s)", + g_server.path.c_str()); + } + return g_srv_ready; +} + +// Returns the authoritative health of the server entity at this index, or -1. +int server_entity_health(int idx, int *maxhealth, int *lifestate, + void **base_out) { + if (base_out) + *base_out = nullptr; + if (!g_srv_ready || !g_iface.engineserver || g_srv_health < 0) + return -1; + + using PEntityFn = void *(*)(void *, int); + void *edict = vfunc(g_iface.engineserver, SRV_PEntityOfEntIndex)( + g_iface.engineserver, idx); + if (!edict) + return -1; + void *unk = *(void **)((uintptr_t)edict + EDICT_UNKNOWN_OFF); + if (!unk) + return -1; + + using GetBaseFn = void *(*)(void *); + void *base = vfunc(unk, SRV_GetBaseEntity)(unk); + if (!base) + return -1; + + uintptr_t b = (uintptr_t)base; + int hp = g_srv_health > 0 ? *(int *)(b + g_srv_health) : -1; + int mx = g_srv_maxhealth > 0 ? *(int *)(b + g_srv_maxhealth) : -1; + int ls = g_srv_lifestate > 0 + ? (int)*(unsigned char *)(b + g_srv_lifestate) + : -1; + if (base_out) + *base_out = base; + if (maxhealth) + *maxhealth = mx; + if (lifestate) + *lifestate = ls; + + if (!g_srv_checked) { + g_srv_checked = true; + using CountFn = int (*)(void *); + int count = vfunc(g_iface.engineserver, SRV_GetEntityCount)( + g_iface.engineserver); + logf("server access: entity_count=%d first_idx=%d base=%p hp=%d maxhp=%d " + "life=%d srv_model=%d", + count, idx, base, hp, mx, ls, + g_srv_modelindex > 0 + ? (int)*(short *)(b + g_srv_modelindex) + : -1); + } + return hp; +} + +int server_model_index(void *base) { + if (!base || g_srv_modelindex <= 0) + return -1; + return (int)*(short *)((uintptr_t)base + g_srv_modelindex); +} -- cgit v1.2.3