diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-26 15:33:02 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-26 15:33:02 +0100 |
| commit | d7edcc9d9e3d90e1c53885870461e046389dee96 (patch) | |
| tree | d4e80c026f65cbff9804c80184293d0c1b63afa8 /src/server.cpp | |
| parent | Default map d1_town_01 (intro-less, enemies spawned) (diff) | |
| download | hl2h-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 'src/server.cpp')
| -rw-r--r-- | src/server.cpp | 177 |
1 files changed, 177 insertions, 0 deletions
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) | ||
| 19 | enum { 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). | ||
| 22 | enum { SRV_GetBaseEntity = 6 }; | ||
| 23 | // CBaseEdict layout: m_fStateFlags@0, serial/index@4, m_pNetworkable@8, m_pUnk@0xC | ||
| 24 | enum { 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 | ||
| 28 | enum { FIELD_INTEGER = 5, FIELD_CHARACTER = 8 }; | ||
| 29 | |||
| 30 | int g_srv_health = -1; | ||
| 31 | int g_srv_maxhealth = -1; | ||
| 32 | int g_srv_lifestate = -1; | ||
| 33 | int g_srv_modelindex = -1; | ||
| 34 | static bool g_srv_ready = false; | ||
| 35 | static bool g_srv_checked = false; | ||
| 36 | |||
| 37 | static 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). | ||
| 51 | static 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 | |||
| 82 | bool 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. | ||
| 124 | int 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 | |||
| 173 | int 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 | } | ||
