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) <noreply@anthropic.com>
142 lines
5.3 KiB
Python
142 lines
5.3 KiB
Python
"""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("<i", d, o)[0]
|
|
def f32(o): return struct.unpack_from("<f", d, o)[0]
|
|
|
|
def parse_table(o0):
|
|
o = o0
|
|
layers = u32(o); o += 4
|
|
if not (1 <= layers <= 12): return None
|
|
out = []
|
|
for _L in range(layers):
|
|
if o + 8 > 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()})
|