// 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; int g_srv_absvelocity = -1; int g_srv_velocity = -1; int g_srv_activeweapon = -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_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR g_srv_activeweapon = datamap_offset("m_hActiveWeapon", 13); // FIELD_EHANDLE g_srv_ready = g_srv_health > 0; if (g_srv_ready) { logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " "modelindex=%d absvel=%d vel=%d activeweapon=%d", g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, g_srv_activeweapon); fail_logged = false; } else if (!fail_logged) { fail_logged = true; logf("server datamap: scan failed (server.so at %s)", g_server.path.c_str()); toastf(TOAST_WARN, "Server datamap scan failed; jumpbug assist limited"); } 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); } // Set the authoritative vertical velocity of a server entity. m_vecAbsVelocity // is what CGameMovement::SetupMove reads through GetAbsVelocity(); writing the // networked m_vecVelocity directly is deliberately avoided because the server // datamap scan can find a same-named field on another class first. The movement // code syncs m_vecVelocity through SetAbsVelocity after the command. bool server_set_velocity_z(int idx, float vz) { if (!g_srv_ready || !g_iface.engineserver || g_srv_absvelocity <= 0) return false; void *base = nullptr; if (server_entity_health(idx, nullptr, nullptr, &base) < 0 || !base) return false; *(float *)((uintptr_t)base + g_srv_absvelocity + 8) = vz; return true; } // Entity index of the player's active weapon from the authoritative server // entity. Used as a fallback if the client m_hActiveWeapon netvar is missing. int server_active_weapon(int idx) { if (!g_srv_ready || !g_iface.engineserver || g_srv_activeweapon <= 0) return -1; void *base = nullptr; if (server_entity_health(idx, nullptr, nullptr, &base) < 0 || !base) return -1; uint32_t h = *(uint32_t *)((uintptr_t)base + g_srv_activeweapon); if (h == 0xFFFFFFFFu) return -1; int wi = (int)(h & 0xFFFF); return (wi > 0 && wi <= 4096) ? wi : -1; }