// 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); }