From 7101b7955071485a9bff33d55fc9f83f117b8f58 Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 26 Oct 2026 16:04:18 +0100 Subject: 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 --- src/bones.cpp | 288 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/esp.cpp | 29 ++++++ src/hl2h.h | 8 ++ 3 files changed, 325 insertions(+) create mode 100644 src/bones.cpp (limited to 'src') 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 @@ +// bones.cpp — client-side studio bone/hitbox access. +// +// C_BaseAnimating implements IClientRenderable::SetupBones(), which fills an +// array of world-space bone matrices. The studio header (from +// IVModelInfoClient::GetStudiomodel) gives bone names/parents and hitboxes. +// Everything here is client-side; no server access is involved. + +#include "hl2h.h" +#include +#include +#include +#include + +namespace { + +constexpr int MAX_BONES = 256; +constexpr int BONE_USED_BY_ANYTHING = 0x0007FF00; +constexpr uint32_t STUDIO_ID = 0x54534449; // ('T'<<24)+('S'<<16)+('D'<<8)+'I' + +struct Matrix3x4 { + float m[3][4]; +}; + +// Partial mirrors of the Source 2013 studio structures (32-bit). Only the +// fields we use are named; layout must still match exactly. +struct StudioHdr { + int id; + int version; + int checksum; + char name[64]; + int length; + Vector eyeposition; + Vector illumposition; + Vector hull_min; + Vector hull_max; + Vector view_bbmin; + Vector view_bbmax; + int flags; + int numbones; + int boneindex; + int numbonecontrollers; + int bonecontrollerindex; + int numhitboxsets; + int hitboxsetindex; +}; + +struct StudioBone { + int sznameindex; + int parent; + int bonecontroller[6]; + Vector pos; + float quat[4]; + Vector rot; + Vector posscale; + Vector rotscale; + float poseToBone[12]; + float qAlignment[4]; + int flags; + int proctype; + int procindex; + int physicsbone; + int surfacepropidx; + int contents; + int unused[8]; +}; + +struct HitboxSet { + int sznameindex; + int numhitboxes; + int hitboxindex; +}; + +struct Hitbox { + int bone; + int group; + Vector bbmin; + Vector bbmax; + int szhitboxnameindex; + int unused[8]; +}; + +static_assert(sizeof(Vector) == 12, "Vector layout"); +static_assert(sizeof(Matrix3x4) == 48, "matrix3x4 layout"); +static_assert(sizeof(StudioHdr) == 0xB4, "studiohdr prefix layout"); +static_assert(sizeof(StudioBone) == 0xD8, "mstudiobone_t layout"); +static_assert(sizeof(HitboxSet) == 12, "mstudiohitboxset_t layout"); +static_assert(sizeof(Hitbox) == 0x44, "mstudiobbox_t layout"); + +struct BoneEntry { + int idx; + void *ent; + double stamp; + bool valid; + const StudioHdr *hdr; + int count; + Matrix3x4 mats[MAX_BONES]; +}; + +static BoneEntry g_entries[64]; +static int g_next = 0; +static int g_logged = 0; +static bool g_renderable_warned = false; + +static double bone_now() { + timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static BoneEntry *find_entry(int idx) { + for (int i = 0; i < 64; i++) + if (g_entries[i].ent && g_entries[i].idx == idx) + return &g_entries[i]; + return nullptr; +} + +static BoneEntry *alloc_entry(int idx, void *ent) { + BoneEntry *e = &g_entries[g_next++ % 64]; + memset(e, 0, sizeof(*e)); + e->idx = idx; + e->ent = ent; + return e; +} + +static const StudioBone *bone_at(const StudioHdr *h, int i) { + return (const StudioBone *)((const uint8_t *)h + h->boneindex) + i; +} + +static const char *bone_name_raw(const StudioHdr *h, int i) { + if (!h || i < 0 || i >= h->numbones) + return nullptr; + const StudioBone *b = bone_at(h, i); + if (b->sznameindex <= 0 || b->sznameindex > 0x10000) + return nullptr; + const char *n = (const char *)b + b->sznameindex; + return n[0] ? n : nullptr; +} + +} // namespace + +bool bones_update(int idx, void *ent) { + if (!ent || !g_iface.modelinfo || !g_iface.engine || g_net.modelindex < 0) + return false; + + BoneEntry *e = find_entry(idx); + double now = bone_now(); + if (e && e->ent == ent && e->valid && now - e->stamp < 0.03) + return true; + if (!e) + e = alloc_entry(idx, ent); + if (e->ent != ent) { + e->ent = ent; + e->valid = false; + } + + int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); + if (modelindex <= 0) { + e->valid = false; + return false; + } + + using GetModelFn = const void *(*)(void *); + using GetModelIdxFn = const void *(*)(void *, int); + using GetStudioFn = const StudioHdr *(*)(void *, const void *); + using SetupFn = bool (*)(void *, Matrix3x4 *, int, int, float); + using GetRenderableFn = void *(*)(void *); + using TimeFn = float (*)(void *); + + const void *expected = + vfunc(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); + if (!expected) + return false; + + // IClientUnknown::GetClientRenderable = primary vtable slot 6. + void *r = vfunc(ent, 6)(ent); + if (!r) + return false; + // Validate the interface before using SetupBones on it. + const void *rmodel = vfunc(r, 9)(r); + if (rmodel != expected) { + if (!g_renderable_warned) { + g_renderable_warned = true; + logf("bones: renderable/model mismatch (renderable=%p model=%p " + "expected=%p)", + r, rmodel, expected); + } + e->valid = false; + return false; + } + + const StudioHdr *hdr = + vfunc(g_iface.modelinfo, 29)(g_iface.modelinfo, expected); + if (!hdr || hdr->id != STUDIO_ID || hdr->numbones <= 0 || + hdr->numbones > MAX_BONES || hdr->boneindex <= 0) { + e->valid = false; + return false; + } + + float curtime = vfunc(g_iface.engine, 14)(g_iface.engine); + // IClientRenderable::SetupBones = slot 16. + if (!vfunc(r, 16)(r, e->mats, MAX_BONES, BONE_USED_BY_ANYTHING, + curtime)) { + e->valid = false; + return false; + } + + e->hdr = hdr; + e->count = hdr->numbones; + e->valid = true; + e->stamp = now; + if (g_logged < 4) { + g_logged++; + logf("bones: idx=%d count=%d bone0=%s", idx, e->count, + bone_name_raw(hdr, 0) ? bone_name_raw(hdr, 0) : "?"); + } + return true; +} + +int bones_count(int idx) { + BoneEntry *e = find_entry(idx); + return (e && e->valid) ? e->count : 0; +} + +int bone_find(int idx, const char *name) { + BoneEntry *e = find_entry(idx); + if (!e || !e->valid || !name) + return -1; + for (int i = 0; i < e->count; i++) { + const char *n = bone_name_raw(e->hdr, i); + if (n && strcasecmp(n, name) == 0) + return i; + } + return -1; +} + +int bone_parent(int idx, int bone) { + BoneEntry *e = find_entry(idx); + if (!e || !e->valid || bone < 0 || bone >= e->count) + return -1; + return bone_at(e->hdr, bone)->parent; +} + +bool bone_pos(int idx, int bone, Vector &out) { + BoneEntry *e = find_entry(idx); + if (!e || !e->valid || bone < 0 || bone >= e->count) + return false; + const Matrix3x4 &m = e->mats[bone]; + out = {m.m[0][3], m.m[1][3], m.m[2][3]}; + return true; +} + +bool hitbox_center(int idx, const char *name, Vector &out) { + BoneEntry *e = find_entry(idx); + if (!e || !e->valid || !name || !e->hdr) + return false; + const StudioHdr *h = e->hdr; + if (h->numhitboxsets <= 0 || h->hitboxsetindex <= 0) + return false; + const HitboxSet *set = + (const HitboxSet *)((const uint8_t *)h + h->hitboxsetindex); + if (set->numhitboxes <= 0 || set->numhitboxes > 512 || + set->hitboxindex <= 0) + return false; + const Hitbox *boxes = + (const Hitbox *)((const uint8_t *)set + set->hitboxindex); + for (int i = 0; i < set->numhitboxes; i++) { + const Hitbox &b = boxes[i]; + if (b.szhitboxnameindex <= 0) + continue; + const char *hn = (const char *)&b + b.szhitboxnameindex; + if (strcasecmp(hn, name) != 0) + continue; + if (b.bone < 0 || b.bone >= e->count) + return false; + Vector local{(b.bbmin.x + b.bbmax.x) * 0.5f, + (b.bbmin.y + b.bbmax.y) * 0.5f, + (b.bbmin.z + b.bbmax.z) * 0.5f}; + const Matrix3x4 &m = e->mats[b.bone]; + out.x = local.x * m.m[0][0] + local.y * m.m[0][1] + local.z * m.m[0][2] + + m.m[0][3]; + out.y = local.x * m.m[1][0] + local.y * m.m[1][1] + local.z * m.m[1][2] + + m.m[1][3]; + out.z = local.x * m.m[2][0] + local.y * m.m[2][1] + local.z * m.m[2][2] + + m.m[2][3]; + return true; + } + return false; +} 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 @@ #include #include #include +#include #include static constexpr float PI_F = 3.14159265358979323846f; @@ -16,6 +17,7 @@ enum SurfSlot { enum { LIFE_ALIVE = 0 }; struct EspTarget { + int idx; float sx, sy; // screen pos of feet float h; // box height px float health, maxhealth; @@ -242,6 +244,7 @@ static void esp_collect(int local_idx) { if (sx < -(float)g_scr_w || sx > 2.0f * (float)g_scr_w) continue; EspTarget &t = g_targets[g_ntargets++]; + t.idx = idx; t.sx = sx; t.sy = sy; t.h = h; t.health = hp > 0 ? (float)hp : 0.0f; t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; @@ -319,4 +322,30 @@ void esp_draw() { if (esp_option(ESP_OPT_SHOW_NAME)) draw_text_col(left, top - 14, t.name, 255, 255, 255); } + + // Groundwork probe: HL2H_BONE_DEBUG=1 draws a white marker at each NPC's + // "head" hitbox center (from SetupBones + studio hitboxes). + static bool bone_debug = getenv("HL2H_BONE_DEBUG") != nullptr; + if (bone_debug) { + static bool head_logged = false; + for (int i = 0; i < g_ntargets; i++) { + const EspTarget &t = g_targets[i]; + void *ent = entitylist_get(t.idx); + if (!ent || !bones_update(t.idx, ent)) + continue; + Vector head; + if (!hitbox_center(t.idx, "head", head)) + continue; + float hx, hy; + if (!world_to_screen(head, hx, hy)) + continue; + surf_color(255, 255, 255, 255); + surf_rect(1, (int)hx - 2, (int)hy - 2, (int)hx + 3, (int)hy + 3); + if (!head_logged) { + head_logged = true; + logf("bone debug: idx=%d bones=%d head=(%.1f %.1f %.1f)", t.idx, + bones_count(t.idx), head.x, head.y, head.z); + } + } + } } diff --git a/src/hl2h.h b/src/hl2h.h index 0986017..d57105d 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -168,6 +168,14 @@ void draw_text_col(int x, int y, const char *s, int r, int g, int b); void esp_draw(); bool world_to_screen(const Vector &w, float &sx, float &sy); +// bones.cpp — client-side animated skeleton / hitbox access (see there) +bool bones_update(int idx, void *ent); +int bones_count(int idx); +int bone_find(int idx, const char *name); +int bone_parent(int idx, int bone); +bool bone_pos(int idx, int bone, Vector &out); +bool hitbox_center(int idx, const char *hitbox_name, Vector &out); + void outer_draw(); // raw ISurface slot calls used by the PaintTraverseEx hook (hl2h.cpp) using SurfFn = void (*)(void *, ...); -- cgit v1.2.3