# # dmgscan -- extract the type-29 cylinder hit-location tables from BTL4.RES. # # The unaimed-damage model is a cylinder around the mech: 7 height BANDS x 8 # angular WEDGES, and each of the 56 cells carries its own cumulative # distribution over that chassis's armor zones. DMGTABLE.CPP resolves a hit # by height -> band, atan2(z,x) -> wedge, then one uniform roll down the # cell's table. # # The stream format is documented in source410/BT/DMGTABLE.CPP and this # scanner is the check on it: it brute-forces every offset in the resource and # only accepts a candidate whose ENTIRE nested structure parses -- thresholds # strictly ascending and terminating at exactly 1.0, zone indices in range, # names NUL-terminated. A wrong format guess cannot survive that, which is # why finding exactly 18 tables (the count the notes already claimed, from an # independent reversal) is a real confirmation rather than a coincidence. # # Findings, 2026-07-30: # * 18 tables, every one 7 rows x 8 cells. # * Only 8 are DISTINCT by content; the rest are duplicates. # * Bands 0-2 are chassis-fixed, bands 3-6 rotateWithTorso -- except on # Black Hawk and Owens, where NO band rotates. # * 18 wedge names in 6 anatomical rings: Foot, Leg, Hip, Waist, Chest, Top. # * Zone counts (17/20/21/22/24) match the per-chassis .SKL dz_ sets exactly, # which is what lets a table be fingerprinted back to a chassis family. # # Usage: python3 dmgscan.py [path/to/BTL4.RES] # # Format (DMGTABLE.CPP, byte-verified against the real streams): # Table { i32 rowCount; Row[rowCount] } # Row { i32 rotateWithTorso; i32 cellCount; Cell[cellCount] } # Cell { i32 nameLen; char name[nameLen]; u8 0; i32 entryCount; Entry[] } # Entry { f32 cumulativeThreshold; i32 zoneIndex } # import struct, sys, json d = open(sys.argv[1] if len(sys.argv) > 1 else r'../ALPHA_1/REL410/BT/BTL4.RES', 'rb').read() N = len(d) u32 = lambda o: struct.unpack_from(' N: return None ln = i32(o) if not (0 <= ln <= 64): return None o2 = o + 4 if o2 + ln + 1 > N: return None nm = d[o2:o2+ln] if ln and not all(32 <= c < 127 for c in nm): return None if d[o2+ln] != 0: return None o2 += ln + 1 if o2 + 4 > N: return None ec = i32(o2); o2 += 4 if not (1 <= ec <= 40): return None if o2 + 8*ec > N: return None ents, prev = [], -1.0 for k in range(ec): t = f32(o2); z = i32(o2+4); o2 += 8 if not (0.0 < t <= 1.0001) or t < prev - 1e-6: return None if not (0 <= z < 64): return None ents.append((t, z)); prev = t if abs(ents[-1][0] - 1.0) > 1e-3: return None return nm.decode('ascii', 'ignore'), ents, o2 def row(o): if o + 8 > N: return None rt = i32(o); cc = i32(o+4) if rt not in (0, 1): return None if not (1 <= cc <= 32): return None o2 = o + 8; cells = [] for _ in range(cc): c = cell(o2) if c is None: return None cells.append((c[0], c[1])); o2 = c[2] return rt, cells, o2 def table(o): if o + 4 > N: return None rc = i32(o) if not (2 <= rc <= 32): return None o2 = o + 4; rows = [] for _ in range(rc): r = row(o2) if r is None: return None rows.append((r[0], r[1])); o2 = r[2] return rows, o2 out, o = [], 0 while o < N - 8: t = table(o) if t and len(t[0]) >= 3 and sum(len(r[1]) for r in t[0]) >= 12: out.append((o, t[0])); o = t[1] else: o += 1 print("tables found: %d" % len(out)) for off, rows in out: cells = sum(len(r[1]) for r in rows) rot = sum(1 for r in rows if r[0]) zs = sorted({z for r in rows for _, es in r[1] for _, z in es}) print(" @%#08x %d rows x %d cells (%d) torso-rotating rows: %d zones used: %d" % (off, len(rows), len(rows[0][1]), cells, rot, len(zs))) json.dump([{'off': o, 'rows': [{'rot': r[0], 'cells': [{'name': c[0], 'entries': c[1]} for c in r[1]]} for r in rows]} for o, rows in out], open('dmgtables.json', 'w'))