// 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; bool humanoid; const StudioHdr *hdr; int count; bool core[MAX_BONES]; 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; } static const HitboxSet *hitbox_set0(const StudioHdr *h) { if (!h || h->numhitboxsets <= 0 || h->hitboxsetindex <= 0) return nullptr; const HitboxSet *s = (const HitboxSet *)((const uint8_t *)h + h->hitboxsetindex); if (s->numhitboxes <= 0 || s->numhitboxes > 512 || s->hitboxindex <= 0) return nullptr; return s; } static const Hitbox *hitboxes(const HitboxSet *s) { return (const Hitbox *)((const uint8_t *)s + s->hitboxindex); } static const char *hitbox_name(const Hitbox &b) { if (b.szhitboxnameindex <= 0 || b.szhitboxnameindex > 0x10000) return ""; return (const char *)&b + b.szhitboxnameindex; } static bool contains_ci(const char *hay, const char *needle) { if (!hay || !needle || !*needle) return false; size_t n = strlen(needle); for (const char *p = hay; *p; p++) if (strncasecmp(p, needle, n) == 0) return true; return false; } static bool hitbox_world(const BoneEntry *e, const Hitbox &b, Vector &out) { 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; } // HL2 humanoids use the ValveBiped skeleton, but many of its bone names are // stripped (the zombie's spine/legs/left arm are unnamed). Instead of matching // names, mark the paths from every hitbox bone up to the root: that yields the // chest/spine, arms and legs, while fingers/toes have no hitboxes and the // headcrab's HC_* hitbox is skipped explicitly. static void build_core_mask(BoneEntry *e) { e->humanoid = false; for (int i = 0; i < e->count; i++) { const char *n = bone_name_raw(e->hdr, i); if (n && strncasecmp(n, "ValveBiped.Bip01_", 17) == 0) { e->humanoid = true; break; } } memset(e->core, 0, sizeof(e->core)); if (!e->humanoid) return; const HitboxSet *s = hitbox_set0(e->hdr); if (!s) return; const Hitbox *boxes = hitboxes(s); for (int i = 0; i < s->numhitboxes; i++) { int b = boxes[i].bone; if (b < 0 || b >= e->count) continue; bool is_head = (boxes[i].group == 1); // HITGROUP_HEAD const char *bn = bone_name_raw(e->hdr, b); if (bn && (contains_ci(bn, "Finger") || contains_ci(bn, "Toe"))) continue; // A zombie's "head" is the headcrab body (HC_Body_Bone); let the head // hitbox through even though its name is HC_*, but keep the headcrab's // extra limbs out. if (!is_head && bn && (contains_ci(bn, "Headcrab") || contains_ci(bn, "HC_"))) continue; int cur = b; for (int guard = 0; cur >= 0 && cur < e->count && guard < 64; guard++) { e->core[cur] = true; cur = bone_at(e->hdr, cur)->parent; } // Include a direct "head" child of the head bone (zombie: HC_Head_Bone // hanging off HC_Body_Bone). if (is_head) { for (int c = 0; c < e->count; c++) { if (bone_at(e->hdr, c)->parent != b) continue; const char *cn = bone_name_raw(e->hdr, c); if (cn && contains_ci(cn, "Head")) e->core[c] = true; } } } } static void dump_hitboxes(const BoneEntry *e) { const StudioHdr *h = e->hdr; logf("hitbox dump: idx=%d sets=%d setidx=%d", e->idx, h->numhitboxsets, h->hitboxsetindex); const HitboxSet *s = hitbox_set0(h); if (!s) { logf("hitbox dump: no usable hitbox set"); return; } const Hitbox *hb = hitboxes(s); logf("hitbox dump: set0 n=%d hitboxindex=%d", s->numhitboxes, s->hitboxindex); int n = s->numhitboxes < 24 ? s->numhitboxes : 24; for (int i = 0; i < n; i++) { const char *bn = (hb[i].bone >= 0 && hb[i].bone < e->count) ? bone_name_raw(h, hb[i].bone) : nullptr; logf(" hb %d name='%s' bone=%d(%s) bb=(%.1f %.1f %.1f)-(%.1f %.1f %.1f)", i, hitbox_name(hb[i]), hb[i].bone, bn ? bn : "?", hb[i].bbmin.x, hb[i].bbmin.y, hb[i].bbmin.z, hb[i].bbmax.x, hb[i].bbmax.y, hb[i].bbmax.z); } } } // 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(); // Refresh every frame; the renderer's bone cache makes this a cheap memcpy // for visible entities. A 30 Hz cache made the head marker visibly lag. if (e && e->ent == ent && e->valid && now - e->stamp < 0.001) 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; build_core_mask(e); 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_is_skeleton_core(int idx, int bone) { BoneEntry *e = find_entry(idx); if (!e || !e->valid || bone < 0 || bone >= e->count) return false; if (!e->humanoid) return true; // non-humanoid: draw its full bone tree return e->core[bone]; } int bone_nearest_core_parent(int idx, int bone) { BoneEntry *e = find_entry(idx); if (!e || !e->valid || bone < 0 || bone >= e->count) return -1; int p = bone_at(e->hdr, bone)->parent; if (!e->humanoid) return (p >= 0 && p < e->count) ? p : -1; for (int guard = 0; p >= 0 && p < e->count && guard < 64; guard++) { if (e->core[p]) return p; p = bone_at(e->hdr, p)->parent; } return -1; } bool bones_is_humanoid(int idx) { BoneEntry *e = find_entry(idx); return e && e->valid && e->humanoid; } 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 HitboxSet *s = hitbox_set0(e->hdr); if (!s) return false; const Hitbox *boxes = hitboxes(s); for (int i = 0; i < s->numhitboxes; i++) { if (strcasecmp(hitbox_name(boxes[i]), name) == 0) return hitbox_world(e, boxes[i], out); } return false; } // Head position with fallbacks, because several HL2 models have no hitbox // names: exact "head" hitbox -> hitbox whose bone name contains "head" -> // head bone origin. Dumps the hitbox set once for diagnosis. bool head_position(int idx, Vector &out) { BoneEntry *e = find_entry(idx); if (!e || !e->valid || !e->hdr) return false; static bool dumped = false; if (!dumped) { dumped = true; dump_hitboxes(e); } if (hitbox_center(idx, "head", out)) return true; const HitboxSet *s = hitbox_set0(e->hdr); if (s) { const Hitbox *boxes = hitboxes(s); // HITGROUP_HEAD == 1. HL2 models often have no hitbox names, so use the // damage-group head hitbox first (zombie: headcrab body, humanoids: head). for (int i = 0; i < s->numhitboxes; i++) { if (boxes[i].group == 1 && hitbox_world(e, boxes[i], out)) return true; } for (int i = 0; i < s->numhitboxes; i++) { if (boxes[i].bone < 0 || boxes[i].bone >= e->count) continue; const char *bn = bone_name_raw(e->hdr, boxes[i].bone); if (bn && contains_ci(bn, "head")) return hitbox_world(e, boxes[i], out); } } for (int i = 0; i < e->count; i++) { const char *bn = bone_name_raw(e->hdr, i); if (bn && contains_ci(bn, "head")) return bone_pos(idx, i, out); } return false; } // Collect world-space aim points for the selected hit groups (HITGROUP_*). // Group 1 (head) uses head_position for its bone fallback; other groups are // the hitbox centers. The caller picks the point nearest to the crosshair. int aim_points(int idx, unsigned group_mask, Vector *out, int max) { if (!out || max <= 0) return 0; BoneEntry *e = find_entry(idx); if (!e || !e->valid || !e->hdr) return 0; int n = 0; if ((group_mask & (1u << 1)) && n < max) { Vector head; if (head_position(idx, head)) out[n++] = head; } const HitboxSet *s = hitbox_set0(e->hdr); if (!s) return n; const Hitbox *boxes = hitboxes(s); for (int i = 0; i < s->numhitboxes && n < max; i++) { int g = boxes[i].group; if (g <= 1 || g > 7) continue; if (!(group_mask & (1u << g))) continue; Vector p; if (hitbox_world(e, boxes[i], p)) out[n++] = p; } return n; }