The operator recalled the damage model as "a pie wedged cylinder" and asked how it maps across the mechs. It is exactly that, and the shipped data is now extracted rather than described. dmgscan.py brute-forces every offset in BTL4.RES and accepts a candidate only if the ENTIRE nested type-29 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, so finding exactly 18 tables -- the count DAMAGE-MODEL.md already claimed from an independent reversal -- is a confirmation of the format, not a coincidence. MEASURED: 18 tables, every one 7 bands x 8 wedges = 56 cells. Only EIGHT are distinct by content; the other ten are duplicates. 22 zones 4 twisting x3 Avatar / Mad Cat class -- table A 22 zones 4 twisting x2 Avatar / Mad Cat class -- table B 21 zones 4 twisting x3 Loki 22 zones 4 twisting x2 Thor 21 zones 4 twisting x2 SND2 24 zones 4 twisting x2 Battlemaster / Vulture 20 zones 0 twisting x2 Black Hawk 17 zones 0 twisting x2 Owens BLACK HAWK AND OWENS ROTATE NO BAND WITH THE TORSO. Every other chassis rotates its upper four. That is a real behavioural difference in the shipped data, not an absence of it. The zone COUNTS match the per-chassis .SKL dz_ sets exactly, which is what lets a table be fingerprinted back to a chassis. It is not always unique -- Avatar and Mad Cat share a zone set but have two DIFFERENT tables, and no chassis name sits near the stream, so they are recorded A/B rather than guessed. Stated as undetermined in both the notes and the visual. THE GEOMETRY, now named: 18 wedge names in six anatomical rings (Foot, Leg, Hip, Waist, Chest, Top). Slot 0 starts at angle 0 spanning 45 degrees, so under atan2(z,x) the mech's +X is right and +Z is front. Each named face covers TWO adjacent wedges (Right = 7,0 / Front = 1,2 / Left = 3,4 / Rear = 5,6) -- so dead ahead is the SEAM between two Front cells, never the centre of one. Bands 0-2 are chassis-fixed; the live torso twist is added to the impact angle before the wedge pick on the rest. And the scatter is generous in a way worth knowing at the controls: a clean foot-wedge hit is only 50% that foot, 30% the lower leg, and 20% of the time the OTHER foot entirely. ALSO: an interactive plate of all of it -- every cell of all 8 tables, plan and elevation, and a twist slider that rotates the upper bands live -- published for the playtesters. Its dataset regenerates from dmgscan.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
4.2 KiB
Python
106 lines
4.2 KiB
Python
#
|
|
# 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('<I', d, o)[0]
|
|
i32 = lambda o: struct.unpack_from('<i', d, o)[0]
|
|
f32 = lambda o: struct.unpack_from('<f', d, o)[0]
|
|
|
|
def cell(o):
|
|
if o + 4 > 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'))
|