From 47d9a6140251c0bf01b013612e37456cd8d2096e Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sun, 2 Aug 2026 12:52:20 -0500 Subject: [PATCH] hit-location cylinder: raw-bytes audit of the shipped tables + chassis->table map + wedge orientation Audit of Cyd's hit-model artifact against THE BINARY (user mandate: not vs the port). res29_scan.py parses the type-29 streams straight from BTL4.RES bytes: 18 streams / 8 distinct confirmed; the 7-wedge cells (BLH band 6, VUL band 4) and the all-fixed BLH/OWN twist patterns are authored fact; every artifact spot-check cell matched the raw bytes verbatim. Chassis->table resolved by live-dump matching (MadCat and Avatar ride DIFFERENT same-zone-set tables). Authored slice names fix the wedge orientation: FRONT = the +Z arc, W1|W2 seam = dead ahead -- corrects the #92 comment's W6-frontal claim. Collision type-0 divert re-grounded at 0x4a0361/0x4a036d: rams never touch the cylinder. Co-Authored-By: Claude Opus 5 (1M context) --- context/combat-damage.md | 31 +++++++ scratchpad/night9/res29_scan.py | 141 ++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 scratchpad/night9/res29_scan.py diff --git a/context/combat-damage.md b/context/combat-damage.md index b7bf16a..c0938ce 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -965,6 +965,37 @@ returned NULL) -- the cascade "ran" and touched nothing, which is why blown-off arms left firing gun pods across every chassis (Conn Man's three-chassis audit). See reconstruction-gotchas #25. +## The hit-location CYLINDER -- raw-bytes audit + chassis->table map (2026-08-02) [T1] + +Full independent verification of the type-29 DamageLookupTable streams, parsed +STRAIGHT from BTL4.RES bytes with `scratchpad/night9/res29_scan.py` (no port +code involved; grammar = the binary ctors @0x49ea48/@0x49e5e4). Results: + +* **18 streams, 8 distinct by content** (copies 3,3,2,2,2,2,2,2). All 7-layer. +* **Slice counts are authored, not fixed 8**: Black Hawk band 6 and the + Vulture/Battlemaster band 4 are **7-wedge** rings; everything else 8. +* rotateWithTorso: layers 3-6 on six tables; **all-zero on Black Hawk + Owens**. +* Chassis -> content hash (live-dump matched, dump-identical rounding): + ava1->1b9958b4, bhk1->5150f823, lok1->b0a0e692, **mad1->c02bfc7a**, + own1->f0f03c87, snd1->0e1a9ca0, thr1->0163740f, vul1->739b1a6b. + The two same-zone-set "Avatar/MadCat" tables ARE distinguishable by loader: + **the Mad Cat rides one (c02bfc7a), the Avatar the other (1b9958b4)**. +* **Wedge orientation, from the AUTHORED slice names** [T1 data]: W0/W7 = + "Right*", W1/W2 = "Front*" -- with theta = atan2(z,x) from +X toward +Z, the + mech-local FRONT is the **+Z arc (theta 45-135 deg)** and dead-ahead is the + W1|W2 SEAM. (Corrects the earlier #92 comment that put frontal hits in W6.) + Frontal unaimed foot-band hits therefore straddle the RightFoot/LeftFoot + cells by impact-x sign, and the authored cross-bleed (20% opposite foot, + 10+10% opposite leg) is a data-true source of "hit the opposite leg" reports. +* Resolver mechanics re-confirmed from the decomp (@0049eb54/@0049e678/ + @0049de14): WorldToLocal; local y CLAMPED to [0, heightRef] BEFORE scaling + (heightRef = live collision cylinder, owner+0x2ec->+0xc); layer floor+clamp; + atan2(z,x) +2pi wrap, on-axis -> random angle; rotateWithTorso adds the live + torso heading (torso+0x1d8) BEFORE slicing; uniform roll vs ascending + cumulatives, -1 fallthrough. **Collision damage (type 0) NEVER reaches the + cylinder** -- diverted at the handler head (0x4a0361: test type; 0x4a036d: + call 0x49ffcc; jmp out) [T1 disasm]. + ## Key Relationships - Weapons/roster: [[subsystems]]. Aim source: [[locomotion]] (drive/facing). Effects: [[rendering]]. - P5 forensics: `docs/HARD_PROBLEMS.md`. Data: [[decomp-reference]] ยง4-5. diff --git a/scratchpad/night9/res29_scan.py b/scratchpad/night9/res29_scan.py new file mode 100644 index 0000000..8470eac --- /dev/null +++ b/scratchpad/night9/res29_scan.py @@ -0,0 +1,141 @@ +"""Parse every type-29 DamageLookupTable stream STRAIGHT OUT of BTL4.RES bytes. + +Audit tool for Cyd's hit-location artifact: zero port code involved -- the +stream grammar is the one byte-verified from the binary's ctors +(@0x49ea48 table / PieSlice / @0x49e5e4 leaf ReadEntries): + + Table { i32 layerCount; Layer[layerCount] } + Layer { i32 rotateWithTorso; i32 sliceCount; Slice[sliceCount] } + Slice { i32 nameLen; char name[nameLen]; u8 NUL; i32 entryCount; + Entry { f32 cumulative; i32 zoneIndex } [entryCount] } + +The scanner walks the whole file trying that grammar at every offset whose +u32 is a plausible layer count, with strict validation (printable names, +ascending cumulatives ending ~1.0, sane counts). False positives cannot +survive the full-table parse. +""" +import struct, hashlib, json, sys + +RES = r"C:\git\bt411\content\BTL4.RES" +d = open(RES, "rb").read() +N = len(d) + +def u32(o): return struct.unpack_from(" N: return None + rot = u32(o); scount = u32(o + 4); o += 8 + if rot not in (0, 1) or not (1 <= scount <= 16): return None + slices = [] + for _S in range(scount): + if o + 4 > N: return None + nl = u32(o); o += 4 + if not (1 <= nl <= 24) or o + nl + 1 > N: return None + name = d[o:o + nl] + if not all(32 <= b < 127 for b in name): return None + if d[o + nl] != 0: return None + o += nl + 1 + if o + 4 > N: return None + ec = u32(o); o += 4 + if not (1 <= ec <= 16): return None + ents = [] + prev = -1e-6 + for _E in range(ec): + if o + 8 > N: return None + cum = f32(o); zi = u32(o + 4); o += 8 + if not (0.0 < cum <= 1.0001) or cum < prev - 1e-5: return None + if not (0 <= zi <= 63): return None + ents.append((round(cum, 5), zi)) + prev = cum + if abs(ents[-1][0] - 1.0) > 0.02: return None + slices.append((name.decode(), ents)) + out.append((rot, slices)) + return out, o - o0 + +tables = [] +o = 0 +while o < N - 8: + v = u32(o) + if 1 <= v <= 12: + r = parse_table(o) + if r: + tables.append((o, r[0])) + o += r[1] + continue + o += 1 + +print("tables parsed from raw RES bytes: %d" % len(tables)) + +def canon(t): + return json.dumps([[rot, [[n, e] for n, e in sl]] for rot, sl in t]) + +groups = {} +for off, t in tables: + h = hashlib.md5(canon(t).encode()).hexdigest()[:8] + groups.setdefault(h, []).append((off, t)) + +print("distinct by content: %d" % len(groups)) +for h, g in groups.items(): + t = g[0][1] + rots = "".join(str(r) for r, _ in t) + sls = ",".join(str(len(sl)) for _, sl in t) + print(" %s copies=%d layers=%d rot=%s slices=%s" % + (h, len(g), len(t), rots, sls)) + +# ---- spot checks against the artifact's DATA (percent deltas, entry order) ---- +def deltas(ents): + out = []; prev = 0.0 + for cum, zi in ents: + out.append(round(cum - prev, 4)); prev = cum + return out + +def find(pred, what): + hits = [h for h, g in groups.items() if pred(g[0][1])] + print("%-58s %s" % (what, hits if hits else "NOT FOUND")) + return hits + +print("\n--- artifact spot checks (band index 0 = first parsed = bottom) ---") +find(lambda t: len(t) == 7 and len(t[6][1]) == 8 and + t[6][1][0][0] == "TopRight" and deltas(t[6][1][0][1]) == [0.6, 0.4], + "LOK band6 TopRight 60/40 (missle-first)") +find(lambda t: len(t) == 7 and len(t[6][1]) == 8 and + t[6][1][0][0] == "TopRight" and deltas(t[6][1][0][1]) == [0.4, 0.6], + "THR band6 TopRight 40/60") +find(lambda t: len(t) == 7 and + all(len(ents) == 1 and abs(ents[0][0] - 1.0) < .001 + for _n, ents in t[6][1]), + "OWN band6 single-entry 100% cells") +find(lambda t: len(t) == 7 and len(t[6][1]) == 7, + "BLH: 7-slice band 6") +find(lambda t: len(t) == 7 and len(t[4][1]) == 7, + "VUL/BAT: 7-slice band 4") +find(lambda t: len(t) == 7 and len(t[5][1]) == 8 and + t[5][1][1][0] == "FrontChest" and deltas(t[5][1][1][1]) == [0.1, 0.1, 0.1, 0.1, 0.5, 0.1], + "AVA-A band5 FrontChest ..50% utorso") +find(lambda t: len(t) == 7 and len(t[6][1]) == 8 and + t[6][1][0][0] == "TopRight" and deltas(t[6][1][0][1]) == [0.5, 0.15, 0.35], + "AVA-B/VUL/SND2 band6 TopRight 50/15/35") +find(lambda t: len(t) == 7 and len(t[4][1]) == 8 and + t[4][1][1][0] == "FrontWaist" and deltas(t[4][1][1][1]) == [0.3, 0.3, 0.4], + "SND2 band4 FrontWaist 30/30/40") +find(lambda t: t[0][1][0][0] == "RightFoot" and deltas(t[0][1][0][1]) == [0.5, 0.3, 0.2], + "band0 RightFoot 50/30/20 (common)") +find(lambda t: len(t) == 7 and len(t[3][1]) == 8 and + t[3][1][0][0] == "RightHip" and deltas(t[3][1][0][1]) == [0.4, 0.3, 0.3], + "BLH band3 RightHip 40/30/30") +find(lambda t: len(t) == 7 and len(t[4][1]) == 7 and + t[4][1][0][0] == "RightWaist" and deltas(t[4][1][0][1]) == [0.15, 0.1, 0.25, 0.25, 0.1, 0.15], + "VUL band4 RightWaist 15/10/25/25/10/15") + +# twist-pattern census +pats = {} +for h, g in groups.items(): + rots = "".join(str(r) for r, _ in g[0][1]) + pats.setdefault(rots, []).append(h) +print("\nrot patterns:", {k: len(v) for k, v in pats.items()})