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 | |
| 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 '')
| -rw-r--r-- | src/cinput.cpp | 1 | ||||
| -rw-r--r-- | src/esp.cpp | 262 | ||||
| -rw-r--r-- | src/hl2h.cpp | 9 | ||||
| -rw-r--r-- | src/hl2h.h | 15 | ||||
| -rw-r--r-- | src/modules.cpp | 3 | ||||
| -rw-r--r-- | src/netvars.cpp | 67 | ||||
| -rw-r--r-- | src/server.cpp | 177 |
7 files changed, 475 insertions, 59 deletions
diff --git a/src/cinput.cpp b/src/cinput.cpp index 7c40b3b..f4ba322 100644 --- a/src/cinput.cpp +++ b/src/cinput.cpp | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | // cinput.cpp — CInput discovery, usercmd CRC, get/fix helpers | 1 | // cinput.cpp — CInput discovery, usercmd CRC, get/fix helpers |
| 2 | #include "hl2h.h" | 2 | #include "hl2h.h" |
| 3 | uintptr_t g_input = 0; | ||
| 3 | 4 | ||
| 4 | // --------------------------------------------------------------------------- | 5 | // --------------------------------------------------------------------------- |
| 5 | // CRC-32 (standard IEEE reflected, matches Valve's implementation) | 6 | // CRC-32 (standard IEEE reflected, matches Valve's implementation) |
diff --git a/src/esp.cpp b/src/esp.cpp index 87d0b80..d12c718 100644 --- a/src/esp.cpp +++ b/src/esp.cpp | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | #include <cmath> | 3 | #include <cmath> |
| 4 | #include <cstring> | 4 | #include <cstring> |
| 5 | #include <cstddef> | 5 | #include <cstddef> |
| 6 | #include <ctime> | ||
| 6 | 7 | ||
| 7 | static constexpr float PI_F = 3.14159265358979323846f; | 8 | static constexpr float PI_F = 3.14159265358979323846f; |
| 8 | 9 | ||
| @@ -12,11 +13,15 @@ enum SurfSlot { | |||
| 12 | SURF_GetScreenSize = 38, | 13 | SURF_GetScreenSize = 38, |
| 13 | }; | 14 | }; |
| 14 | 15 | ||
| 16 | enum { LIFE_ALIVE = 0 }; | ||
| 17 | |||
| 15 | struct EspTarget { | 18 | struct EspTarget { |
| 16 | float sx, sy; // screen pos of feet | 19 | float sx, sy; // screen pos of feet |
| 17 | float h; // box height px | 20 | float h; // box height px |
| 18 | float health, maxhealth; | 21 | float health, maxhealth; |
| 19 | int teamnum; | 22 | bool show_health; |
| 23 | bool friendly; | ||
| 24 | bool neutral; | ||
| 20 | char name[48]; | 25 | char name[48]; |
| 21 | }; | 26 | }; |
| 22 | static EspTarget g_targets[128]; | 27 | static EspTarget g_targets[128]; |
| @@ -24,7 +29,27 @@ static int g_ntargets = 0; | |||
| 24 | static int g_scr_w = 0, g_scr_h = 0; | 29 | static int g_scr_w = 0, g_scr_h = 0; |
| 25 | static bool g_esp_logged = false; | 30 | static bool g_esp_logged = false; |
| 26 | 31 | ||
| 32 | static double esp_now() { | ||
| 33 | timespec ts; | ||
| 34 | clock_gettime(CLOCK_MONOTONIC, &ts); | ||
| 35 | return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; | ||
| 36 | } | ||
| 37 | |||
| 27 | bool world_to_screen(const Vector &w, float &sx, float &sy) { | 38 | bool world_to_screen(const Vector &w, float &sx, float &sy) { |
| 39 | // Preferred: the engine's own transform. Uses the real view setup (FOV, | ||
| 40 | // aspect, roll), so boxes do not drift at the screen edges. | ||
| 41 | if (g_iface.debugoverlay) { | ||
| 42 | using Fn = int (*)(void *, const Vector *, Vector *); | ||
| 43 | Vector s; | ||
| 44 | int behind = | ||
| 45 | vfunc<Fn>(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s); | ||
| 46 | if (behind) | ||
| 47 | return false; | ||
| 48 | sx = s.x; | ||
| 49 | sy = s.y; | ||
| 50 | return true; | ||
| 51 | } | ||
| 52 | // Fallback: local math with the default HL2 FOV. | ||
| 28 | float pitch = g_cam_view.x * PI_F / 180.0f; | 53 | float pitch = g_cam_view.x * PI_F / 180.0f; |
| 29 | float yaw = g_cam_view.y * PI_F / 180.0f; | 54 | float yaw = g_cam_view.y * PI_F / 180.0f; |
| 30 | float sp = sinf(pitch), cp = cosf(pitch); | 55 | float sp = sinf(pitch), cp = cosf(pitch); |
| @@ -43,39 +68,195 @@ bool world_to_screen(const Vector &w, float &sx, float &sy) { | |||
| 43 | return true; | 68 | return true; |
| 44 | } | 69 | } |
| 45 | 70 | ||
| 71 | // --------------------------------------------------------------------------- | ||
| 72 | // entity helpers (IClientEntityList / IClientNetworkable, header-documented) | ||
| 73 | // --------------------------------------------------------------------------- | ||
| 74 | |||
| 75 | using GetNetFn = void *(*)(void *, int); | ||
| 76 | using GetCCFn = void *(*)(void *); | ||
| 77 | using GetModelFn = const void *(*)(void *, int); | ||
| 78 | using GetModelNameFn = const char *(*)(void *, const void *); | ||
| 79 | |||
| 80 | // entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the | ||
| 81 | // IClientNetworkable subobject offset inside the entity (see projectz). | ||
| 82 | static void *entity_networkable(int idx) { | ||
| 83 | return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx); | ||
| 84 | } | ||
| 85 | |||
| 86 | // IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8. | ||
| 87 | static const char *entity_class_name(void *net, void **out_cc) { | ||
| 88 | if (!net) return nullptr; | ||
| 89 | void *cc = vfunc<GetCCFn>(net, 2)(net); | ||
| 90 | if (out_cc) *out_cc = cc; | ||
| 91 | return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; | ||
| 92 | } | ||
| 93 | |||
| 94 | // m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. | ||
| 95 | // Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. | ||
| 96 | static const char *entity_model_name(void *ent) { | ||
| 97 | if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr; | ||
| 98 | int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); | ||
| 99 | if (modelindex <= 0) return nullptr; | ||
| 100 | const void *model = | ||
| 101 | vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); | ||
| 102 | if (!model) | ||
| 103 | return nullptr; | ||
| 104 | return vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model); | ||
| 105 | } | ||
| 106 | |||
| 107 | // HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all | ||
| 108 | // use the single client class "CAI_BaseNPC" — only special NPCs register their | ||
| 109 | // own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). | ||
| 110 | static bool is_npc_class(const char *name) { | ||
| 111 | if (!name) return false; | ||
| 112 | return strstr(name, "NPC") != nullptr || | ||
| 113 | strstr(name, "BaseHelicopter") != nullptr; | ||
| 114 | } | ||
| 115 | |||
| 116 | static bool model_is_friendly(const char *m) { | ||
| 117 | static const char *patterns[] = { | ||
| 118 | "citizen", "humans/", "barney", "alyx", "alex", "scientist", | ||
| 119 | "kleiner", "eli", "mossman", "vortigaunt", "dog", "grigori", | ||
| 120 | "monk", "gman", nullptr}; | ||
| 121 | if (!m) return false; | ||
| 122 | for (int i = 0; patterns[i]; i++) | ||
| 123 | if (strstr(m, patterns[i])) | ||
| 124 | return true; | ||
| 125 | return false; | ||
| 126 | } | ||
| 127 | |||
| 128 | // harmless wildlife that also happens to be CAI_BaseNPC | ||
| 129 | static bool model_is_neutral(const char *m) { | ||
| 130 | static const char *patterns[] = {"crow", "gull", "pigeon", "seagull", | ||
| 131 | "bird", nullptr}; | ||
| 132 | if (!m) return false; | ||
| 133 | for (int i = 0; patterns[i]; i++) | ||
| 134 | if (strstr(m, patterns[i])) | ||
| 135 | return true; | ||
| 136 | return false; | ||
| 137 | } | ||
| 138 | |||
| 139 | static void short_name(const char *model, char *out, size_t outn) { | ||
| 140 | if (!model) { snprintf(out, outn, "NPC"); return; } | ||
| 141 | const char *base = strrchr(model, '/'); | ||
| 142 | base = base ? base + 1 : model; | ||
| 143 | size_t n = strcspn(base, "."); | ||
| 144 | if (n >= outn) n = outn - 1; | ||
| 145 | memcpy(out, base, n); | ||
| 146 | out[n] = 0; | ||
| 147 | } | ||
| 148 | |||
| 46 | static void esp_collect(int local_idx) { | 149 | static void esp_collect(int local_idx) { |
| 150 | // outer_draw() runs once per PaintTraverseEx call (several per frame); cache | ||
| 151 | // the scan briefly so we do not walk the entity list dozens of times a frame. | ||
| 152 | static double last_scan = 0.0; | ||
| 153 | static double last_dbg = 0.0; | ||
| 154 | double now = esp_now(); | ||
| 155 | if (now - last_scan < 0.01) | ||
| 156 | return; | ||
| 157 | last_scan = now; | ||
| 158 | bool dbg = (now - last_dbg) >= 2.0; | ||
| 159 | if (dbg) last_dbg = now; | ||
| 160 | |||
| 47 | g_ntargets = 0; | 161 | g_ntargets = 0; |
| 48 | using Fn = int (*)(void *); | 162 | using Fn = int (*)(void *); |
| 49 | int maxent = vfunc<Fn>(g_iface.entitylist, 5)(g_iface.entitylist); // GetHighestEntityIndex | 163 | int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); // GetHighestEntityIndex |
| 50 | if (maxent < 0 || maxent > 512) maxent = 512; | 164 | if (hi < 0 || hi > 4096) hi = 4096; |
| 51 | for (int idx = 0; idx <= maxent && g_ntargets < 128; idx++) { | 165 | int npc_alive = 0, dbg_lines = 0; |
| 166 | bool srv = resolve_server_netvars(); | ||
| 167 | static bool srv_checked = false; | ||
| 168 | static bool srv_trusted = false; | ||
| 169 | |||
| 170 | for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) { | ||
| 52 | if (idx == local_idx) continue; | 171 | if (idx == local_idx) continue; |
| 53 | void *ent = entitylist_get(idx); | 172 | void *ent = entitylist_get(idx); |
| 54 | if (!ent) continue; | 173 | if (!ent) continue; |
| 174 | void *net = entity_networkable(idx); | ||
| 175 | if (!net) continue; | ||
| 176 | const char *cls = entity_class_name(net, nullptr); | ||
| 177 | if (!cls) continue; | ||
| 178 | |||
| 55 | uintptr_t e = (uintptr_t)ent; | 179 | uintptr_t e = (uintptr_t)ent; |
| 56 | float hpv = (float)*(int *)(e + g_net.health); // m_iHealth is int | 180 | int cli_life = g_net.lifestate >= 0 |
| 57 | if (!(hpv > 0)) continue; | 181 | ? (int)*(unsigned char *)(e + g_net.lifestate) |
| 182 | : -1; | ||
| 183 | int cli_hp = g_net.health >= 0 ? *(int *)(e + g_net.health) : 0; | ||
| 184 | bool npc = is_npc_class(cls); | ||
| 185 | const char *model = entity_model_name(ent); | ||
| 186 | |||
| 187 | // NPC health is not networked; read it from the listen-server entity. | ||
| 188 | int hp = cli_hp, maxhp = -1, life = cli_life; | ||
| 189 | if (npc && srv) { | ||
| 190 | int slife = -1, smax = -1; | ||
| 191 | void *sbase = nullptr; | ||
| 192 | int shp = server_entity_health(idx, &smax, &slife, &sbase); | ||
| 193 | if (!srv_checked && sbase) { | ||
| 194 | srv_checked = true; | ||
| 195 | int cli_mi = | ||
| 196 | g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1; | ||
| 197 | int srv_mi = server_model_index(sbase); | ||
| 198 | srv_trusted = (cli_mi == srv_mi); | ||
| 199 | logf("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d " | ||
| 200 | "hp=%d maxhp=%d life=%d trusted=%d", | ||
| 201 | idx, sbase, cli_mi, srv_mi, shp, smax, slife, (int)srv_trusted); | ||
| 202 | if (!srv_trusted) | ||
| 203 | logf("ESP: server entity mismatch, ignoring server values"); | ||
| 204 | } | ||
| 205 | if (srv_trusted) { | ||
| 206 | if (shp >= 0) hp = shp; | ||
| 207 | if (smax > 0) maxhp = smax; | ||
| 208 | if (slife >= 0) life = slife; | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | if (dbg && npc && dbg_lines < 40) { | ||
| 213 | dbg_lines++; | ||
| 214 | logf("ESP npc %d cls=%s life=%d(cli %d) hp=%d(cli %d) maxhp=%d model=%s", | ||
| 215 | idx, cls, life, cli_life, hp, cli_hp, maxhp, | ||
| 216 | model ? model : "?"); | ||
| 217 | } | ||
| 218 | |||
| 219 | if (npc) { | ||
| 220 | if (life != LIFE_ALIVE) continue; | ||
| 221 | npc_alive++; | ||
| 222 | } else { | ||
| 223 | // players (HL2DM) are the only entities with a live networked m_iHealth | ||
| 224 | if (!strstr(cls, "Player") || !(hp > 0)) continue; | ||
| 225 | } | ||
| 226 | |||
| 227 | float minz = g_net.mins >= 0 ? *(float *)(e + g_net.mins + 8) : 0.0f; | ||
| 228 | float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; | ||
| 229 | if (!(maxz > minz + 4.0f && maxz - minz < 512.0f)) { | ||
| 230 | minz = 0.0f; | ||
| 231 | maxz = 72.0f; | ||
| 232 | } | ||
| 58 | Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), | 233 | Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), |
| 59 | *(float *)(e + g_net.origin + 8)}; | 234 | *(float *)(e + g_net.origin + 8)}; |
| 60 | // ponytail: maxs netvar may be unresolved; fixed 72-unit torso fallback | 235 | Vector bottom{origin.x, origin.y, origin.z + minz}; |
| 61 | float maxsz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs) : 72.0f; | 236 | Vector top{origin.x, origin.y, origin.z + maxz}; |
| 62 | Vector top{origin.x, origin.y, origin.z + maxsz}; | ||
| 63 | float sx, sy, tx, ty; | 237 | float sx, sy, tx, ty; |
| 64 | if (!world_to_screen(origin, sx, sy) || !world_to_screen(top, tx, ty)) | 238 | if (!world_to_screen(bottom, sx, sy) || !world_to_screen(top, tx, ty)) |
| 65 | continue; | 239 | continue; |
| 66 | float h = sy - ty; | 240 | float h = sy - ty; |
| 67 | if (h < 4.0f || sy < 0 || ty > (float)g_scr_h) continue; // behind / offscreen | 241 | if (h < 3.0f || sy < 0 || ty > (float)g_scr_h) continue; // behind / offscreen |
| 242 | if (sx < -(float)g_scr_w || sx > 2.0f * (float)g_scr_w) continue; | ||
| 243 | |||
| 68 | EspTarget &t = g_targets[g_ntargets++]; | 244 | EspTarget &t = g_targets[g_ntargets++]; |
| 69 | t.sx = sx; t.sy = sy; t.h = h; | 245 | t.sx = sx; t.sy = sy; t.h = h; |
| 70 | t.health = hpv; | 246 | t.health = hp > 0 ? (float)hp : 0.0f; |
| 71 | t.maxhealth = (float)*(int *)(e + g_net.maxhealth); | 247 | t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; |
| 72 | if (t.maxhealth <= 0) t.maxhealth = hpv; | 248 | t.show_health = hp > 0; |
| 73 | // classname via IClientNetworkable::GetClientClass (slot 2) -> m_pNetworkName @ +8 | 249 | t.friendly = model_is_friendly(model); |
| 74 | using CCFn = void *(*)(void *); | 250 | t.neutral = model_is_neutral(model); |
| 75 | void *cc = vfunc<CCFn>(ent, 2)(ent); | 251 | short_name(model, t.name, sizeof(t.name)); |
| 76 | const char *nn = cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; | 252 | if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); |
| 77 | strncpy(t.name, nn ? nn : "?", sizeof(t.name) - 1); | 253 | } |
| 78 | t.name[sizeof(t.name) - 1] = 0; | 254 | |
| 255 | if (dbg) | ||
| 256 | logf("ESP scan: hi=%d npc_alive=%d targets=%d", hi, npc_alive, g_ntargets); | ||
| 257 | if (g_ntargets && !g_esp_logged) { | ||
| 258 | g_esp_logged = true; | ||
| 259 | logf("ESP: first targets, %d", g_ntargets); | ||
| 79 | } | 260 | } |
| 80 | } | 261 | } |
| 81 | 262 | ||
| @@ -97,28 +278,29 @@ void esp_draw() { | |||
| 97 | if (!g_scr_w) return; | 278 | if (!g_scr_w) return; |
| 98 | } | 279 | } |
| 99 | esp_collect(engine_get_local_player()); | 280 | esp_collect(engine_get_local_player()); |
| 100 | if (g_ntargets && !g_esp_logged) { | ||
| 101 | g_esp_logged = true; | ||
| 102 | logf("ESP: first draw, %d targets", g_ntargets); | ||
| 103 | } | ||
| 104 | for (int i = 0; i < g_ntargets; i++) { | 281 | for (int i = 0; i < g_ntargets; i++) { |
| 105 | const EspTarget &t = g_targets[i]; | 282 | const EspTarget &t = g_targets[i]; |
| 106 | // team color: 3 combine red, 2 rebels green, else white | 283 | if (t.friendly) surf_color(60, 255, 60, 255); |
| 107 | if (t.teamnum == 3) surf_color(255, 60, 60, 255); | 284 | else if (t.neutral) surf_color(220, 220, 220, 255); |
| 108 | else if (t.teamnum == 2) surf_color(60, 255, 60, 255); | 285 | else surf_color(255, 60, 60, 255); |
| 109 | else surf_color(255, 255, 255, 255); | 286 | float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width |
| 110 | float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width — swap for m_vecMins/maxs span later | ||
| 111 | surf_rect(0, (int)(t.sx - w / 2), (int)(t.sy - t.h), (int)(t.sx + w / 2), (int)t.sy); | 287 | surf_rect(0, (int)(t.sx - w / 2), (int)(t.sy - t.h), (int)(t.sx + w / 2), (int)t.sy); |
| 112 | // health bar under box | 288 | if (t.show_health) { |
| 113 | float frac = t.health / t.maxhealth; | 289 | float frac = t.health / (t.maxhealth > 1.0f ? t.maxhealth : 100.0f); |
| 114 | if (frac > 1) frac = 1; | 290 | if (frac > 1) frac = 1; |
| 115 | surf_color(0, 0, 0, 200); | 291 | surf_color(0, 0, 0, 200); |
| 116 | surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 1, (int)(t.sx + w / 2), (int)t.sy + 4); | 292 | surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 1, (int)(t.sx + w / 2), (int)t.sy + 4); |
| 117 | if (frac > 0.5f) surf_color(40, 255, 40, 255); | 293 | if (frac > 0.5f) surf_color(40, 255, 40, 255); |
| 118 | else if (frac > 0.25f) surf_color(255, 255, 0, 255); | 294 | else if (frac > 0.25f) surf_color(255, 255, 0, 255); |
| 119 | else surf_color(255, 40, 40, 255); | 295 | else surf_color(255, 40, 40, 255); |
| 120 | surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 2, | 296 | surf_rect(1, (int)(t.sx - w / 2), (int)t.sy + 2, |
| 121 | (int)(t.sx - w / 2 + w * frac), (int)t.sy + 3); | 297 | (int)(t.sx - w / 2 + w * frac), (int)t.sy + 3); |
| 122 | draw_text_col((int)(t.sx - w / 2), (int)(t.sy - t.h) - 14, t.name, 255, 255, 255); | 298 | } |
| 299 | char label[64]; | ||
| 300 | if (t.show_health) | ||
| 301 | snprintf(label, sizeof(label), "%s %d", t.name, (int)t.health); | ||
| 302 | else | ||
| 303 | snprintf(label, sizeof(label), "%s", t.name); | ||
| 304 | draw_text_col((int)(t.sx - w / 2), (int)(t.sy - t.h) - 14, label, 255, 255, 255); | ||
| 123 | } | 305 | } |
| 124 | } | 306 | } |
diff --git a/src/hl2h.cpp b/src/hl2h.cpp index 01fdb0f..a451a18 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp | |||
| @@ -29,7 +29,7 @@ void logf(const char *fmt, ...) { | |||
| 29 | // globals | 29 | // globals |
| 30 | // --------------------------------------------------------------------------- | 30 | // --------------------------------------------------------------------------- |
| 31 | 31 | ||
| 32 | Module g_client, g_engine; | 32 | Module g_client, g_engine, g_server; |
| 33 | GameInterfaces g_iface; | 33 | GameInterfaces g_iface; |
| 34 | NetVars g_net; | 34 | NetVars g_net; |
| 35 | Vector g_cam_eye{0, 0, 0}; | 35 | Vector g_cam_eye{0, 0, 0}; |
| @@ -335,6 +335,13 @@ static void *setup_thread(void *) { | |||
| 335 | "VEngineClient014=%p", | 335 | "VEngineClient014=%p", |
| 336 | g_iface.client, g_iface.entitylist, g_iface.engine); | 336 | g_iface.client, g_iface.entitylist, g_iface.engine); |
| 337 | 337 | ||
| 338 | g_iface.modelinfo = ef("VModelInfoClient006", nullptr); | ||
| 339 | g_iface.engineserver = ef("VEngineServer021", nullptr); | ||
| 340 | g_iface.debugoverlay = ef("VDebugOverlay003", nullptr); | ||
| 341 | logf("modelinfo: %p (VModelInfoClient006)", g_iface.modelinfo); | ||
| 342 | logf("engineserver: %p (VEngineServer021), debugoverlay: %p", | ||
| 343 | g_iface.engineserver, g_iface.debugoverlay); | ||
| 344 | |||
| 338 | resolve_netvars(); | 345 | resolve_netvars(); |
| 339 | if (!find_input_global()) | 346 | if (!find_input_global()) |
| 340 | return nullptr; | 347 | return nullptr; |
| @@ -36,7 +36,7 @@ struct Module { | |||
| 36 | return false; | 36 | return false; |
| 37 | } | 37 | } |
| 38 | }; | 38 | }; |
| 39 | extern Module g_client, g_engine; | 39 | extern Module g_client, g_engine, g_server; |
| 40 | 40 | ||
| 41 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
| 42 | // game interfaces | 42 | // game interfaces |
| @@ -48,6 +48,9 @@ struct GameInterfaces { | |||
| 48 | void *client = nullptr; | 48 | void *client = nullptr; |
| 49 | void *entitylist = nullptr; | 49 | void *entitylist = nullptr; |
| 50 | void *engine = nullptr; | 50 | void *engine = nullptr; |
| 51 | void *modelinfo = nullptr; // IVModelInfoClient006 (model index -> model name) | ||
| 52 | void *engineserver = nullptr; // IVEngineServer021 (edict access, incl. server entities) | ||
| 53 | void *debugoverlay = nullptr; // IVDebugOverlay003 (engine-accurate world->screen) | ||
| 51 | }; | 54 | }; |
| 52 | extern GameInterfaces g_iface; | 55 | extern GameInterfaces g_iface; |
| 53 | 56 | ||
| @@ -70,9 +73,13 @@ struct NetVars { | |||
| 70 | int viewoffset = -1; | 73 | int viewoffset = -1; |
| 71 | int mins = -1; | 74 | int mins = -1; |
| 72 | int maxs = -1; | 75 | int maxs = -1; |
| 76 | int lifestate = -1; | ||
| 77 | int modelindex = -1; | ||
| 73 | // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 | 78 | // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 |
| 74 | bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } | 79 | bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } |
| 75 | bool esp_ok() const { return ok() && health >= 0 && maxhealth >= 0 && teamnum >= 0 && origin >= 0 && viewoffset >= 0; } | 80 | // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100. |
| 81 | // m_iHealth only ever updates for players — NPCs are detected via m_lifeState. | ||
| 82 | bool esp_ok() const { return ok() && health >= 0 && teamnum >= 0 && origin >= 0 && viewoffset >= 0; } | ||
| 76 | }; | 83 | }; |
| 77 | extern NetVars g_net; | 84 | extern NetVars g_net; |
| 78 | 85 | ||
| @@ -139,6 +146,10 @@ void fix_verified_crc(int seq, uint32_t crc); | |||
| 139 | uint32_t usercmd_checksum(const CUserCmd *c); | 146 | uint32_t usercmd_checksum(const CUserCmd *c); |
| 140 | bool find_input_global(); | 147 | bool find_input_global(); |
| 141 | void resolve_netvars(); | 148 | void resolve_netvars(); |
| 149 | int class_health_off(const char *name); | ||
| 150 | bool resolve_server_netvars(); | ||
| 151 | int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); | ||
| 152 | int server_model_index(void *base); | ||
| 142 | void find_modules(); | 153 | void find_modules(); |
| 143 | void init_conmsg(); | 154 | void init_conmsg(); |
| 144 | bool feature_active(int feature); | 155 | bool feature_active(int feature); |
diff --git a/src/modules.cpp b/src/modules.cpp index d20fe04..70e2dff 100644 --- a/src/modules.cpp +++ b/src/modules.cpp | |||
| @@ -11,6 +11,7 @@ static bool basename_is(const char *path, const char *base) { | |||
| 11 | void find_modules() { | 11 | void find_modules() { |
| 12 | g_client = Module(); | 12 | g_client = Module(); |
| 13 | g_engine = Module(); | 13 | g_engine = Module(); |
| 14 | g_server = Module(); | ||
| 14 | FILE *f = fopen("/proc/self/maps", "r"); | 15 | FILE *f = fopen("/proc/self/maps", "r"); |
| 15 | if (!f) | 16 | if (!f) |
| 16 | return; | 17 | return; |
| @@ -41,6 +42,8 @@ void find_modules() { | |||
| 41 | m = &g_client; | 42 | m = &g_client; |
| 42 | else if (basename_is(clean, "engine.so")) | 43 | else if (basename_is(clean, "engine.so")) |
| 43 | m = &g_engine; | 44 | m = &g_engine; |
| 45 | else if (basename_is(clean, "server.so")) | ||
| 46 | m = &g_server; | ||
| 44 | } else { | 47 | } else { |
| 45 | // anon: absorb only as .bss tail after a file-backed range | 48 | // anon: absorb only as .bss tail after a file-backed range |
| 46 | if (g_client.valid() && g_client.ranges.back().file && | 49 | if (g_client.valid() && g_client.ranges.back().file && |
diff --git a/src/netvars.cpp b/src/netvars.cpp index 5c8c296..f702c41 100644 --- a/src/netvars.cpp +++ b/src/netvars.cpp | |||
| @@ -27,7 +27,24 @@ static bool module_string(uintptr_t p, char *out, size_t outn) { | |||
| 27 | 27 | ||
| 28 | static int g_dumped_tables = 0; | 28 | static int g_dumped_tables = 0; |
| 29 | 29 | ||
| 30 | static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { | 30 | // per-client-class health offset (m_iHealth lives at different offsets per |
| 31 | // class chain: CBasePlayer via DT_BasePlayer=128, NPCs via DT_BaseCombatCharacter) | ||
| 32 | static struct { | ||
| 33 | char name[32]; | ||
| 34 | int off; | ||
| 35 | } g_class_health[256]; | ||
| 36 | static int g_class_health_n = 0; | ||
| 37 | |||
| 38 | int class_health_off(const char *name) { | ||
| 39 | for (int i = 0; i < g_class_health_n; i++) | ||
| 40 | if (strcmp(name, g_class_health[i].name) == 0) | ||
| 41 | return g_class_health[i].off; | ||
| 42 | return -1; | ||
| 43 | } | ||
| 44 | |||
| 45 | static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth, | ||
| 46 | int *out_health) | ||
| 47 | { | ||
| 31 | if (depth > 8) | 48 | if (depth > 8) |
| 32 | return; | 49 | return; |
| 33 | uintptr_t props = *(uintptr_t *)(table + 0x00); | 50 | uintptr_t props = *(uintptr_t *)(table + 0x00); |
| @@ -67,13 +84,19 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { | |||
| 67 | if (dump) | 84 | if (dump) |
| 68 | logf(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2, | 85 | logf(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2, |
| 69 | ptype, poff, poff, ptable ? "yes" : "no", parray ? "yes" : "no"); | 86 | ptype, poff, poff, ptable ? "yes" : "no", parray ? "yes" : "no"); |
| 70 | |||
| 71 | if (ptable) { | 87 | if (ptable) { |
| 72 | collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1); | 88 | collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1, |
| 89 | out_health); | ||
| 73 | continue; | 90 | continue; |
| 74 | } | 91 | } |
| 75 | 92 | ||
| 76 | if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { | 93 | if (out_health && *out_health < 0 && strcmp(pname2, "m_iHealth") == 0) |
| 94 | *out_health = (int)(base_off + (uint32_t)poff); | ||
| 95 | if (strcmp(pname2, "m_lifeState") == 0 && g_net.lifestate < 0) { | ||
| 96 | g_net.lifestate = (int)(base_off + (uint32_t)poff); | ||
| 97 | } else if (strcmp(pname2, "m_nModelIndex") == 0 && g_net.modelindex < 0) { | ||
| 98 | g_net.modelindex = (int)(base_off + (uint32_t)poff); | ||
| 99 | } else if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { | ||
| 77 | g_net.flags = (int)(base_off + (uint32_t)poff); | 100 | g_net.flags = (int)(base_off + (uint32_t)poff); |
| 78 | } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) { | 101 | } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) { |
| 79 | if (parray && g_client.contains(parray)) { | 102 | if (parray && g_client.contains(parray)) { |
| @@ -95,7 +118,8 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { | |||
| 95 | } else { | 118 | } else { |
| 96 | g_net.origin = (int)(base_off + (uint32_t)poff); | 119 | g_net.origin = (int)(base_off + (uint32_t)poff); |
| 97 | } | 120 | } |
| 98 | } else if (strcmp(pname2, "m_vecViewOffset") == 0 && g_net.viewoffset < 0) { | 121 | } else if (strncmp(pname2, "m_vecViewOffset", 15) == 0 && |
| 122 | g_net.viewoffset < 0) { | ||
| 99 | if (parray && g_client.contains(parray)) { | 123 | if (parray && g_client.contains(parray)) { |
| 100 | int32_t eoff = *(int32_t *)(parray + 0x2C); | 124 | int32_t eoff = *(int32_t *)(parray + 0x2C); |
| 101 | g_net.viewoffset = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); | 125 | g_net.viewoffset = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); |
| @@ -117,9 +141,7 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { | |||
| 117 | g_net.maxs = (int)(base_off + (uint32_t)poff); | 141 | g_net.maxs = (int)(base_off + (uint32_t)poff); |
| 118 | } | 142 | } |
| 119 | } | 143 | } |
| 120 | if (g_net.esp_ok()) | 144 | } |
| 121 | return; | ||
| 122 | } | ||
| 123 | } | 145 | } |
| 124 | 146 | ||
| 125 | // prediction datamap scan for m_MoveType (FIELD_CHARACTER, fieldOffset in range) | 147 | // prediction datamap scan for m_MoveType (FIELD_CHARACTER, fieldOffset in range) |
| @@ -195,21 +217,34 @@ void resolve_netvars() { | |||
| 195 | for (uintptr_t cc = head; cc && g_client.contains(cc) && classes < 1024; | 217 | for (uintptr_t cc = head; cc && g_client.contains(cc) && classes < 1024; |
| 196 | cc = *(uintptr_t *)(cc + 0x10)) { | 218 | cc = *(uintptr_t *)(cc + 0x10)) { |
| 197 | classes++; | 219 | classes++; |
| 198 | collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0); | 220 | char cname[32]; |
| 199 | if (g_net.esp_ok() || (g_net.flags >= 0 && g_net.velocity >= 0 && | 221 | int hoff = -1; |
| 200 | g_net.health < 0)) | 222 | // per-class health offset — NPCs send m_iHealth via their own table chain, |
| 201 | break; | 223 | // not DT_BasePlayer's; g_net.health keeps the player's for aimbot use |
| 224 | if (module_string(*(uintptr_t *)(cc + 0x08), cname, sizeof(cname))) { | ||
| 225 | collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0, &hoff); | ||
| 226 | if (hoff >= 0 && g_class_health_n < 256) { | ||
| 227 | char *dst = g_class_health[g_class_health_n].name; | ||
| 228 | const char *src = cname; | ||
| 229 | size_t n = sizeof(g_class_health[g_class_health_n].name) - 1; | ||
| 230 | for (size_t i = 0; i < n && src[i]; i++) dst[i] = src[i]; | ||
| 231 | dst[n] = 0; | ||
| 232 | g_class_health[g_class_health_n].off = hoff; | ||
| 233 | g_class_health_n++; | ||
| 234 | } | ||
| 235 | } | ||
| 202 | } | 236 | } |
| 203 | logf("client classes walked: %d", classes); | 237 | logf("client classes walked: %d, health-offset classes: %d", classes, |
| 238 | g_class_health_n); | ||
| 204 | 239 | ||
| 205 | if (g_net.movetype < 0 && !resolve_movetype()) { | 240 | if (g_net.movetype < 0 && !resolve_movetype()) { |
| 206 | // movetype unresolved — bhop still works, just no noclip/ladder guard | 241 | // movetype unresolved — bhop still works, just no noclip/ladder guard |
| 207 | logf("movetype guard disabled"); | 242 | logf("movetype guard disabled"); |
| 208 | } | 243 | } |
| 209 | |||
| 210 | logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " | 244 | logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " |
| 211 | "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d", | 245 | "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d " |
| 246 | "modelindex=%d", | ||
| 212 | g_net.flags, g_net.velocity, g_net.movetype, g_net.health, | 247 | g_net.flags, g_net.velocity, g_net.movetype, g_net.health, |
| 213 | g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, | 248 | g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, |
| 214 | g_net.mins, g_net.maxs); | 249 | g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex); |
| 215 | } | 250 | } |
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 | } | ||
