summaryrefslogtreecommitdiffstats
path: root/src/server.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/server.cpp177
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)
19enum { 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).
22enum { SRV_GetBaseEntity = 6 };
23// CBaseEdict layout: m_fStateFlags@0, serial/index@4, m_pNetworkable@8, m_pUnk@0xC
24enum { 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
28enum { FIELD_INTEGER = 5, FIELD_CHARACTER = 8 };
29
30int g_srv_health = -1;
31int g_srv_maxhealth = -1;
32int g_srv_lifestate = -1;
33int g_srv_modelindex = -1;
34static bool g_srv_ready = false;
35static bool g_srv_checked = false;
36
37static 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).
51static 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
82bool 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.
124int 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
173int 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}