// 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_BOOLEAN = 6, FIELD_CHARACTER = 8, FIELD_EMBEDDED = 10, FIELD_CUSTOM = 11, FIELD_EHANDLE = 13, FIELD_TIME = 16, FIELD_FLOAT = 1 }; // SF_FLOOR_TURRET_CITIZEN (npc_turret_floor.h). Citizen-built turrets // Classify() as CLASS_PLAYER_ALLY, i.e. they are friendly without ever adding // an entry to m_Relationship. static const int TURRET_FLAG_CITIZEN = 0x200; 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 int g_srv_relationship = -1; static int g_srv_followtarget = -1; static int g_srv_spawnflags = -1; static int g_srv_hacked = -1; static int g_srv_npcstate = -1; static int g_srv_takedamage = -1; static int g_srv_mapname = -1; static int g_srv_armor = -1; static int g_srv_detonate = -1; static int g_srv_grenade_radius = -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). // max_off is generous on purpose: derived-class fields (m_bHackedByAlyx, // m_hFollowTarget) live well past the base class size. static int datamap_offset(const char *name, int want_type, int min_off = 0x40, int max_off = 0x4000) { 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 < min_off || foff > max_off) continue; if (fsize < 1 || fsize > 128) 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; logd("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", FIELD_EHANDLE); // m_Relationship is DEFINE_UTLVECTOR, i.e. FIELD_CUSTOM (older builds used // FIELD_EMBEDDED), never FIELD_EMBEDDED in the modern SDK. g_srv_relationship = datamap_offset("m_Relationship", FIELD_CUSTOM); if (g_srv_relationship < 0) g_srv_relationship = datamap_offset("m_Relationship", FIELD_EMBEDDED); g_srv_followtarget = datamap_offset("m_hFollowTarget", FIELD_EHANDLE); // CBaseEntity::m_spawnflags is a DEFINE_KEYFIELD at a low offset. g_srv_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4); g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN); g_srv_npcstate = datamap_offset("m_NPCState", FIELD_INTEGER); // CBaseEntity::m_takedamage is a char in the 2013 SDK // (CNetworkVarForDerived(char, m_takedamage)); older builds used int. g_srv_takedamage = datamap_offset("m_takedamage", FIELD_CHARACTER); if (g_srv_takedamage < 0) g_srv_takedamage = datamap_offset("m_takedamage", FIELD_INTEGER); // CChangeLevel::m_szMapName is a char[cchMapNameMost] on trigger_changelevel. g_srv_mapname = datamap_offset("m_szMapName", FIELD_CHARACTER, 0x40, 0x4000); // CBasePlayer::m_ArmorValue (int). g_srv_armor = datamap_offset("m_ArmorValue", FIELD_INTEGER); // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked). g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME); g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT); 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 relationship=%d " "followtarget=%d spawnflags=%d hacked=%d npcstate=%d " "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%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, g_srv_relationship, g_srv_followtarget, g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage, g_srv_mapname, g_srv_armor, g_srv_detonate, g_srv_grenade_radius); 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); } int server_entity_count() { if (!g_iface.engineserver) return 0; using CountFn = int (*)(void *); int n = vfunc(g_iface.engineserver, SRV_GetEntityCount)( g_iface.engineserver); return (n > 0 && n <= 8192) ? n : 0; } static void *edict_unknown(int idx) { if (!g_iface.engineserver) return nullptr; using PEntityFn = void *(*)(void *, int); void *edict = vfunc(g_iface.engineserver, SRV_PEntityOfEntIndex)( g_iface.engineserver, idx); if (!edict) return nullptr; return *(void **)((uintptr_t)edict + EDICT_UNKNOWN_OFF); } void *server_entity_base(int idx) { void *unk = edict_unknown(idx); if (!unk) return nullptr; using GetBaseFn = void *(*)(void *); return vfunc(unk, SRV_GetBaseEntity)(unk); } // Linked entity class name (e.g. "weapon_smg1", "trigger_changelevel"). const char *server_entity_classname(int idx) { void *unk = edict_unknown(idx); if (!unk) return nullptr; using GetNetFn = void *(*)(void *); void *net = vfunc(unk, 5)(unk); if (!net) return nullptr; using NameFn = const char *(*)(void *); return vfunc(net, 3)(net); } // A trigger_changelevel's target map and world-space brush bounds. The class // name comes from IServerNetworkable::GetClassName (slot 3); the volume from // ICollideable::WorldSpaceSurroundingBounds (slot 17, no virtual dtor). bool server_trigger_info(int idx, char *map, size_t mapn, Vector &mins, Vector &maxs) { if (!g_srv_ready || g_srv_mapname < 0) return false; void *unk = edict_unknown(idx); if (!unk) return false; using GetNetFn = void *(*)(void *); void *net = vfunc(unk, 5)(unk); if (!net) return false; using NameFn = const char *(*)(void *); const char *cls = vfunc(net, 3)(net); if (!cls || !strcasestr(cls, "changelevel")) return false; void *base = server_entity_base(idx); if (!base) return false; const char *m = (const char *)((uintptr_t)base + g_srv_mapname); size_t n = strnlen(m, 32); // cchMapNameMost if (n == 0 || n >= 32) return false; for (size_t i = 0; i < n; i++) { unsigned char c = (unsigned char)m[i]; if (c < 0x20 || c > 0x7e) return false; } if (map && mapn) { if (n >= mapn) n = mapn - 1; memcpy(map, m, n); map[n] = 0; } using GetCollFn = void *(*)(void *); void *coll = vfunc(unk, 4)(unk); if (coll) { using BoundsFn = void (*)(void *, Vector *, Vector *); vfunc(coll, 17)(coll, &mins, &maxs); } return true; } // 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. // Server-side AI relationship (D_ER=0, D_HT=1, D_FR=2, D_LI=3, D_NU=4). // m_Relationship is a CUtlVector; each entry is // EHANDLE entity, Class_T classType, Disposition_t disposition, int priority. int server_npc_relation(int idx, int local_idx) { if (!g_srv_ready || g_srv_relationship < 0 || local_idx <= 0) return -1; void *base = nullptr; server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1; uintptr_t rel = (uintptr_t)base + g_srv_relationship; int count = *(int *)(rel + 12); if (count <= 0 || count > 64) return -1; uintptr_t elems = *(uintptr_t *)(rel + 16); if (!elems) elems = *(uintptr_t *)(rel + 0); if (!elems || (elems & 3)) return -1; int class_relation = -1; for (int i = 0; i < count; i++) { uintptr_t e = elems + (uintptr_t)i * 16; uint32_t h = *(uint32_t *)(e + 0); int class_type = *(int *)(e + 4); int disposition = *(int *)(e + 8); if (h != 0 && h != 0xFFFFFFFFu) { if ((int)(h & 0x7FF) == local_idx) return disposition; } else if (class_type == 1) { // CLASS_PLAYER class_relation = disposition; } } return class_relation; } // NPC_STATE_SCRIPT = 4 (ai_npcstate.h). Scripted NPCs are passive: cutscene // actors, Kleiner's Lamarr, scripted soldiers standing down, and so on. int server_npc_state(int idx) { if (!g_srv_ready || g_srv_npcstate < 0) return -1; void *base = nullptr; server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1; return *(int *)((uintptr_t)base + g_srv_npcstate); } // TAKEDAMAGE: 0 = DAMAGE_NO, 1 = DAMAGE_EVENTS_ONLY, 2 = DAMAGE_YES, // 3 = DAMAGE_AIM. 0/1 cannot be damaged at all. The field is a char, so read // a byte even if the older int layout was resolved. int server_npc_takedamage(int idx) { if (!g_srv_ready || g_srv_takedamage < 0) return -1; void *base = nullptr; server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1; return (int)*(unsigned char *)((uintptr_t)base + g_srv_takedamage); } bool server_npc_has_follow_target(int idx) { if (!g_srv_ready || g_srv_followtarget < 0) return false; void *base = nullptr; server_entity_health(idx, nullptr, nullptr, &base); if (!base) return false; uint32_t h = *(uint32_t *)((uintptr_t)base + g_srv_followtarget); return h != 0 && h != 0xFFFFFFFFu; } // Turret friendliness that never appears in m_Relationship: CNPC_FloorTurret's // Classify() reports CLASS_PLAYER_ALLY for citizen-built (SF_FLOOR_TURRET_CITIZEN) // and Alyx-hacked turrets, which makes the default class table D_LI toward the // player. Returns -1 if unknown, else bit0 = citizen, bit1 = hacked. int server_npc_turret_flags(int idx) { if (!g_srv_ready || (g_srv_spawnflags < 0 && g_srv_hacked < 0)) return -1; void *base = nullptr; server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1; uintptr_t b = (uintptr_t)base; int flags = -1; int hacked = -1; if (g_srv_spawnflags > 0) flags = *(int *)(b + g_srv_spawnflags); if (g_srv_hacked > 0) hacked = (int)*(unsigned char *)(b + g_srv_hacked); int out = 0; if (flags > 0 && (flags & TURRET_FLAG_CITIZEN)) out |= 1; if (hacked > 0) out |= 2; return out; } 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; } // Local player's HEV suit armor from the authoritative server entity. int server_player_armor(int idx) { if (!g_srv_ready || g_srv_armor < 0) return -1; void *base = nullptr; server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1; return *(int *)((uintptr_t)base + g_srv_armor); } // CBaseGrenade fuse time and blast radius from the server entity. Both are // non-networked (m_flDetonateTime is commented out of the send table). float server_grenade_detonate(int idx) { if (!g_srv_ready || g_srv_detonate < 0) return -1.0f; void *base = nullptr; server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1.0f; return *(float *)((uintptr_t)base + g_srv_detonate); } float server_grenade_radius(int idx) { if (!g_srv_ready || g_srv_grenade_radius < 0) return -1.0f; void *base = nullptr; server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1.0f; return *(float *)((uintptr_t)base + g_srv_grenade_radius); } // 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 & 0x7FF); // 11-bit entity index return (wi > 0 && wi <= 4096) ? wi : -1; }