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 --- src/server.cpp | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 src/server.cpp (limited to 'src/server.cpp') 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