diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-26 16:04:18 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-26 16:04:18 +0100 |
| commit | 7101b7955071485a9bff33d55fc9f83f117b8f58 (patch) | |
| tree | 3351eca32146d6f88f740ff73da7c84045942a55 /src | |
| parent | Menu: start fully collapsed on first open (diff) | |
| download | hl2h-7101b7955071485a9bff33d55fc9f83f117b8f58.tar.gz hl2h-7101b7955071485a9bff33d55fc9f83f117b8f58.zip | |
Bones: client-side SetupBones/studiohdr groundwork
- bones.cpp: resolve IClientRenderable via IClientUnknown slot 6, validate it
against IVModelInfo GetModel, call SetupBones (slot 16) with engine Time()
(slot 14), and parse the studio header (GetStudiomodel, slot 29) for
bones/bone names/parents and hitboxes
- cache up to 64 entity bone snapshots, refreshed at ~30 Hz
- helpers: bone_find/parent/pos, hitbox_center (e.g. "head")
- debug probe: HL2H_BONE_DEBUG=1 draws a white marker at each NPC's head
hitbox center and logs bone count/head position once
Diffstat (limited to 'src')
| -rw-r--r-- | src/bones.cpp | 288 | ||||
| -rw-r--r-- | src/esp.cpp | 29 | ||||
| -rw-r--r-- | src/hl2h.h | 8 |
3 files changed, 325 insertions, 0 deletions
diff --git a/src/bones.cpp b/src/bones.cpp new file mode 100644 index 0000000..2ff282b --- /dev/null +++ b/src/bones.cpp | |||
| @@ -0,0 +1,288 @@ | |||
| 1 | // bones.cpp — client-side studio bone/hitbox access. | ||
| 2 | // | ||
| 3 | // C_BaseAnimating implements IClientRenderable::SetupBones(), which fills an | ||
| 4 | // array of world-space bone matrices. The studio header (from | ||
| 5 | // IVModelInfoClient::GetStudiomodel) gives bone names/parents and hitboxes. | ||
| 6 | // Everything here is client-side; no server access is involved. | ||
| 7 | |||
| 8 | #include "hl2h.h" | ||
| 9 | #include <cstdint> | ||
| 10 | #include <cstring> | ||
| 11 | #include <ctime> | ||
| 12 | #include <strings.h> | ||
| 13 | |||
| 14 | namespace { | ||
| 15 | |||
| 16 | constexpr int MAX_BONES = 256; | ||
| 17 | constexpr int BONE_USED_BY_ANYTHING = 0x0007FF00; | ||
| 18 | constexpr uint32_t STUDIO_ID = 0x54534449; // ('T'<<24)+('S'<<16)+('D'<<8)+'I' | ||
| 19 | |||
| 20 | struct Matrix3x4 { | ||
| 21 | float m[3][4]; | ||
| 22 | }; | ||
| 23 | |||
| 24 | // Partial mirrors of the Source 2013 studio structures (32-bit). Only the | ||
| 25 | // fields we use are named; layout must still match exactly. | ||
| 26 | struct StudioHdr { | ||
| 27 | int id; | ||
| 28 | int version; | ||
| 29 | int checksum; | ||
| 30 | char name[64]; | ||
| 31 | int length; | ||
| 32 | Vector eyeposition; | ||
| 33 | Vector illumposition; | ||
| 34 | Vector hull_min; | ||
| 35 | Vector hull_max; | ||
| 36 | Vector view_bbmin; | ||
| 37 | Vector view_bbmax; | ||
| 38 | int flags; | ||
| 39 | int numbones; | ||
| 40 | int boneindex; | ||
| 41 | int numbonecontrollers; | ||
| 42 | int bonecontrollerindex; | ||
| 43 | int numhitboxsets; | ||
| 44 | int hitboxsetindex; | ||
| 45 | }; | ||
| 46 | |||
| 47 | struct StudioBone { | ||
| 48 | int sznameindex; | ||
| 49 | int parent; | ||
| 50 | int bonecontroller[6]; | ||
| 51 | Vector pos; | ||
| 52 | float quat[4]; | ||
| 53 | Vector rot; | ||
| 54 | Vector posscale; | ||
| 55 | Vector rotscale; | ||
| 56 | float poseToBone[12]; | ||
| 57 | float qAlignment[4]; | ||
| 58 | int flags; | ||
| 59 | int proctype; | ||
| 60 | int procindex; | ||
| 61 | int physicsbone; | ||
| 62 | int surfacepropidx; | ||
| 63 | int contents; | ||
| 64 | int unused[8]; | ||
| 65 | }; | ||
| 66 | |||
| 67 | struct HitboxSet { | ||
| 68 | int sznameindex; | ||
| 69 | int numhitboxes; | ||
| 70 | int hitboxindex; | ||
| 71 | }; | ||
| 72 | |||
| 73 | struct Hitbox { | ||
| 74 | int bone; | ||
| 75 | int group; | ||
| 76 | Vector bbmin; | ||
| 77 | Vector bbmax; | ||
| 78 | int szhitboxnameindex; | ||
| 79 | int unused[8]; | ||
| 80 | }; | ||
| 81 | |||
| 82 | static_assert(sizeof(Vector) == 12, "Vector layout"); | ||
| 83 | static_assert(sizeof(Matrix3x4) == 48, "matrix3x4 layout"); | ||
| 84 | static_assert(sizeof(StudioHdr) == 0xB4, "studiohdr prefix layout"); | ||
| 85 | static_assert(sizeof(StudioBone) == 0xD8, "mstudiobone_t layout"); | ||
| 86 | static_assert(sizeof(HitboxSet) == 12, "mstudiohitboxset_t layout"); | ||
| 87 | static_assert(sizeof(Hitbox) == 0x44, "mstudiobbox_t layout"); | ||
| 88 | |||
| 89 | struct BoneEntry { | ||
| 90 | int idx; | ||
| 91 | void *ent; | ||
| 92 | double stamp; | ||
| 93 | bool valid; | ||
| 94 | const StudioHdr *hdr; | ||
| 95 | int count; | ||
| 96 | Matrix3x4 mats[MAX_BONES]; | ||
| 97 | }; | ||
| 98 | |||
| 99 | static BoneEntry g_entries[64]; | ||
| 100 | static int g_next = 0; | ||
| 101 | static int g_logged = 0; | ||
| 102 | static bool g_renderable_warned = false; | ||
| 103 | |||
| 104 | static double bone_now() { | ||
| 105 | timespec ts; | ||
| 106 | clock_gettime(CLOCK_MONOTONIC, &ts); | ||
| 107 | return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; | ||
| 108 | } | ||
| 109 | |||
| 110 | static BoneEntry *find_entry(int idx) { | ||
| 111 | for (int i = 0; i < 64; i++) | ||
| 112 | if (g_entries[i].ent && g_entries[i].idx == idx) | ||
| 113 | return &g_entries[i]; | ||
| 114 | return nullptr; | ||
| 115 | } | ||
| 116 | |||
| 117 | static BoneEntry *alloc_entry(int idx, void *ent) { | ||
| 118 | BoneEntry *e = &g_entries[g_next++ % 64]; | ||
| 119 | memset(e, 0, sizeof(*e)); | ||
| 120 | e->idx = idx; | ||
| 121 | e->ent = ent; | ||
| 122 | return e; | ||
| 123 | } | ||
| 124 | |||
| 125 | static const StudioBone *bone_at(const StudioHdr *h, int i) { | ||
| 126 | return (const StudioBone *)((const uint8_t *)h + h->boneindex) + i; | ||
| 127 | } | ||
| 128 | |||
| 129 | static const char *bone_name_raw(const StudioHdr *h, int i) { | ||
| 130 | if (!h || i < 0 || i >= h->numbones) | ||
| 131 | return nullptr; | ||
| 132 | const StudioBone *b = bone_at(h, i); | ||
| 133 | if (b->sznameindex <= 0 || b->sznameindex > 0x10000) | ||
| 134 | return nullptr; | ||
| 135 | const char *n = (const char *)b + b->sznameindex; | ||
| 136 | return n[0] ? n : nullptr; | ||
| 137 | } | ||
| 138 | |||
| 139 | } // namespace | ||
| 140 | |||
| 141 | bool bones_update(int idx, void *ent) { | ||
| 142 | if (!ent || !g_iface.modelinfo || !g_iface.engine || g_net.modelindex < 0) | ||
| 143 | return false; | ||
| 144 | |||
| 145 | BoneEntry *e = find_entry(idx); | ||
| 146 | double now = bone_now(); | ||
| 147 | if (e && e->ent == ent && e->valid && now - e->stamp < 0.03) | ||
| 148 | return true; | ||
| 149 | if (!e) | ||
| 150 | e = alloc_entry(idx, ent); | ||
| 151 | if (e->ent != ent) { | ||
| 152 | e->ent = ent; | ||
| 153 | e->valid = false; | ||
| 154 | } | ||
| 155 | |||
| 156 | int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); | ||
| 157 | if (modelindex <= 0) { | ||
| 158 | e->valid = false; | ||
| 159 | return false; | ||
| 160 | } | ||
| 161 | |||
| 162 | using GetModelFn = const void *(*)(void *); | ||
| 163 | using GetModelIdxFn = const void *(*)(void *, int); | ||
| 164 | using GetStudioFn = const StudioHdr *(*)(void *, const void *); | ||
| 165 | using SetupFn = bool (*)(void *, Matrix3x4 *, int, int, float); | ||
| 166 | using GetRenderableFn = void *(*)(void *); | ||
| 167 | using TimeFn = float (*)(void *); | ||
| 168 | |||
| 169 | const void *expected = | ||
| 170 | vfunc<GetModelIdxFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); | ||
| 171 | if (!expected) | ||
| 172 | return false; | ||
| 173 | |||
| 174 | // IClientUnknown::GetClientRenderable = primary vtable slot 6. | ||
| 175 | void *r = vfunc<GetRenderableFn>(ent, 6)(ent); | ||
| 176 | if (!r) | ||
| 177 | return false; | ||
| 178 | // Validate the interface before using SetupBones on it. | ||
| 179 | const void *rmodel = vfunc<GetModelFn>(r, 9)(r); | ||
| 180 | if (rmodel != expected) { | ||
| 181 | if (!g_renderable_warned) { | ||
| 182 | g_renderable_warned = true; | ||
| 183 | logf("bones: renderable/model mismatch (renderable=%p model=%p " | ||
| 184 | "expected=%p)", | ||
| 185 | r, rmodel, expected); | ||
| 186 | } | ||
| 187 | e->valid = false; | ||
| 188 | return false; | ||
| 189 | } | ||
| 190 | |||
| 191 | const StudioHdr *hdr = | ||
| 192 | vfunc<GetStudioFn>(g_iface.modelinfo, 29)(g_iface.modelinfo, expected); | ||
| 193 | if (!hdr || hdr->id != STUDIO_ID || hdr->numbones <= 0 || | ||
| 194 | hdr->numbones > MAX_BONES || hdr->boneindex <= 0) { | ||
| 195 | e->valid = false; | ||
| 196 | return false; | ||
| 197 | } | ||
| 198 | |||
| 199 | float curtime = vfunc<TimeFn>(g_iface.engine, 14)(g_iface.engine); | ||
| 200 | // IClientRenderable::SetupBones = slot 16. | ||
| 201 | if (!vfunc<SetupFn>(r, 16)(r, e->mats, MAX_BONES, BONE_USED_BY_ANYTHING, | ||
| 202 | curtime)) { | ||
| 203 | e->valid = false; | ||
| 204 | return false; | ||
| 205 | } | ||
| 206 | |||
| 207 | e->hdr = hdr; | ||
| 208 | e->count = hdr->numbones; | ||
| 209 | e->valid = true; | ||
| 210 | e->stamp = now; | ||
| 211 | if (g_logged < 4) { | ||
| 212 | g_logged++; | ||
| 213 | logf("bones: idx=%d count=%d bone0=%s", idx, e->count, | ||
| 214 | bone_name_raw(hdr, 0) ? bone_name_raw(hdr, 0) : "?"); | ||
| 215 | } | ||
| 216 | return true; | ||
| 217 | } | ||
| 218 | |||
| 219 | int bones_count(int idx) { | ||
| 220 | BoneEntry *e = find_entry(idx); | ||
| 221 | return (e && e->valid) ? e->count : 0; | ||
| 222 | } | ||
| 223 | |||
| 224 | int bone_find(int idx, const char *name) { | ||
| 225 | BoneEntry *e = find_entry(idx); | ||
| 226 | if (!e || !e->valid || !name) | ||
| 227 | return -1; | ||
| 228 | for (int i = 0; i < e->count; i++) { | ||
| 229 | const char *n = bone_name_raw(e->hdr, i); | ||
| 230 | if (n && strcasecmp(n, name) == 0) | ||
| 231 | return i; | ||
| 232 | } | ||
| 233 | return -1; | ||
| 234 | } | ||
| 235 | |||
| 236 | int bone_parent(int idx, int bone) { | ||
| 237 | BoneEntry *e = find_entry(idx); | ||
| 238 | if (!e || !e->valid || bone < 0 || bone >= e->count) | ||
| 239 | return -1; | ||
| 240 | return bone_at(e->hdr, bone)->parent; | ||
| 241 | } | ||
| 242 | |||
| 243 | bool bone_pos(int idx, int bone, Vector &out) { | ||
| 244 | BoneEntry *e = find_entry(idx); | ||
| 245 | if (!e || !e->valid || bone < 0 || bone >= e->count) | ||
| 246 | return false; | ||
| 247 | const Matrix3x4 &m = e->mats[bone]; | ||
| 248 | out = {m.m[0][3], m.m[1][3], m.m[2][3]}; | ||
| 249 | return true; | ||
| 250 | } | ||
| 251 | |||
| 252 | bool hitbox_center(int idx, const char *name, Vector &out) { | ||
| 253 | BoneEntry *e = find_entry(idx); | ||
| 254 | if (!e || !e->valid || !name || !e->hdr) | ||
| 255 | return false; | ||
| 256 | const StudioHdr *h = e->hdr; | ||
| 257 | if (h->numhitboxsets <= 0 || h->hitboxsetindex <= 0) | ||
| 258 | return false; | ||
| 259 | const HitboxSet *set = | ||
| 260 | (const HitboxSet *)((const uint8_t *)h + h->hitboxsetindex); | ||
| 261 | if (set->numhitboxes <= 0 || set->numhitboxes > 512 || | ||
| 262 | set->hitboxindex <= 0) | ||
| 263 | return false; | ||
| 264 | const Hitbox *boxes = | ||
| 265 | (const Hitbox *)((const uint8_t *)set + set->hitboxindex); | ||
| 266 | for (int i = 0; i < set->numhitboxes; i++) { | ||
| 267 | const Hitbox &b = boxes[i]; | ||
| 268 | if (b.szhitboxnameindex <= 0) | ||
| 269 | continue; | ||
| 270 | const char *hn = (const char *)&b + b.szhitboxnameindex; | ||
| 271 | if (strcasecmp(hn, name) != 0) | ||
| 272 | continue; | ||
| 273 | if (b.bone < 0 || b.bone >= e->count) | ||
| 274 | return false; | ||
| 275 | Vector local{(b.bbmin.x + b.bbmax.x) * 0.5f, | ||
| 276 | (b.bbmin.y + b.bbmax.y) * 0.5f, | ||
| 277 | (b.bbmin.z + b.bbmax.z) * 0.5f}; | ||
| 278 | const Matrix3x4 &m = e->mats[b.bone]; | ||
| 279 | out.x = local.x * m.m[0][0] + local.y * m.m[0][1] + local.z * m.m[0][2] + | ||
| 280 | m.m[0][3]; | ||
| 281 | out.y = local.x * m.m[1][0] + local.y * m.m[1][1] + local.z * m.m[1][2] + | ||
| 282 | m.m[1][3]; | ||
| 283 | out.z = local.x * m.m[2][0] + local.y * m.m[2][1] + local.z * m.m[2][2] + | ||
| 284 | m.m[2][3]; | ||
| 285 | return true; | ||
| 286 | } | ||
| 287 | return false; | ||
| 288 | } | ||
diff --git a/src/esp.cpp b/src/esp.cpp index 4462f07..b384bc4 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 <cstdlib> | ||
| 6 | #include <ctime> | 7 | #include <ctime> |
| 7 | 8 | ||
| 8 | static constexpr float PI_F = 3.14159265358979323846f; | 9 | static constexpr float PI_F = 3.14159265358979323846f; |
| @@ -16,6 +17,7 @@ enum SurfSlot { | |||
| 16 | enum { LIFE_ALIVE = 0 }; | 17 | enum { LIFE_ALIVE = 0 }; |
| 17 | 18 | ||
| 18 | struct EspTarget { | 19 | struct EspTarget { |
| 20 | int idx; | ||
| 19 | float sx, sy; // screen pos of feet | 21 | float sx, sy; // screen pos of feet |
| 20 | float h; // box height px | 22 | float h; // box height px |
| 21 | float health, maxhealth; | 23 | float health, maxhealth; |
| @@ -242,6 +244,7 @@ static void esp_collect(int local_idx) { | |||
| 242 | if (sx < -(float)g_scr_w || sx > 2.0f * (float)g_scr_w) continue; | 244 | if (sx < -(float)g_scr_w || sx > 2.0f * (float)g_scr_w) continue; |
| 243 | 245 | ||
| 244 | EspTarget &t = g_targets[g_ntargets++]; | 246 | EspTarget &t = g_targets[g_ntargets++]; |
| 247 | t.idx = idx; | ||
| 245 | t.sx = sx; t.sy = sy; t.h = h; | 248 | t.sx = sx; t.sy = sy; t.h = h; |
| 246 | t.health = hp > 0 ? (float)hp : 0.0f; | 249 | t.health = hp > 0 ? (float)hp : 0.0f; |
| 247 | t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; | 250 | t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; |
| @@ -319,4 +322,30 @@ void esp_draw() { | |||
| 319 | if (esp_option(ESP_OPT_SHOW_NAME)) | 322 | if (esp_option(ESP_OPT_SHOW_NAME)) |
| 320 | draw_text_col(left, top - 14, t.name, 255, 255, 255); | 323 | draw_text_col(left, top - 14, t.name, 255, 255, 255); |
| 321 | } | 324 | } |
| 325 | |||
| 326 | // Groundwork probe: HL2H_BONE_DEBUG=1 draws a white marker at each NPC's | ||
| 327 | // "head" hitbox center (from SetupBones + studio hitboxes). | ||
| 328 | static bool bone_debug = getenv("HL2H_BONE_DEBUG") != nullptr; | ||
| 329 | if (bone_debug) { | ||
| 330 | static bool head_logged = false; | ||
| 331 | for (int i = 0; i < g_ntargets; i++) { | ||
| 332 | const EspTarget &t = g_targets[i]; | ||
| 333 | void *ent = entitylist_get(t.idx); | ||
| 334 | if (!ent || !bones_update(t.idx, ent)) | ||
| 335 | continue; | ||
| 336 | Vector head; | ||
| 337 | if (!hitbox_center(t.idx, "head", head)) | ||
| 338 | continue; | ||
| 339 | float hx, hy; | ||
| 340 | if (!world_to_screen(head, hx, hy)) | ||
| 341 | continue; | ||
| 342 | surf_color(255, 255, 255, 255); | ||
| 343 | surf_rect(1, (int)hx - 2, (int)hy - 2, (int)hx + 3, (int)hy + 3); | ||
| 344 | if (!head_logged) { | ||
| 345 | head_logged = true; | ||
| 346 | logf("bone debug: idx=%d bones=%d head=(%.1f %.1f %.1f)", t.idx, | ||
| 347 | bones_count(t.idx), head.x, head.y, head.z); | ||
| 348 | } | ||
| 349 | } | ||
| 350 | } | ||
| 322 | } | 351 | } |
| @@ -168,6 +168,14 @@ void draw_text_col(int x, int y, const char *s, int r, int g, int b); | |||
| 168 | void esp_draw(); | 168 | void esp_draw(); |
| 169 | bool world_to_screen(const Vector &w, float &sx, float &sy); | 169 | bool world_to_screen(const Vector &w, float &sx, float &sy); |
| 170 | 170 | ||
| 171 | // bones.cpp — client-side animated skeleton / hitbox access (see there) | ||
| 172 | bool bones_update(int idx, void *ent); | ||
| 173 | int bones_count(int idx); | ||
| 174 | int bone_find(int idx, const char *name); | ||
| 175 | int bone_parent(int idx, int bone); | ||
| 176 | bool bone_pos(int idx, int bone, Vector &out); | ||
| 177 | bool hitbox_center(int idx, const char *hitbox_name, Vector &out); | ||
| 178 | |||
| 171 | void outer_draw(); | 179 | void outer_draw(); |
| 172 | // raw ISurface slot calls used by the PaintTraverseEx hook (hl2h.cpp) | 180 | // raw ISurface slot calls used by the PaintTraverseEx hook (hl2h.cpp) |
| 173 | using SurfFn = void (*)(void *, ...); | 181 | using SurfFn = void (*)(void *, ...); |
