Three root causes, all fixed: 1. kDamageScale was 1.0 -- _DAT_004bafbc is an x87 float80 = 1e-7, cancelling the ctor's x1e7: damagePortion = authored DamageAmount x (charge/seekV)^2 (closed form at fire time; madcat AC=25/LRM=50/ERLL=6, bhk1 PPC=12/SRM=35). The observed "0.25" was the degenerate EC=1 fallback, never authored data. 2. CheckFireEdge NaN latch: TriggerState carries ControlsButton INTS; the release value (-65) is a negative NaN. The binary's x87 unordered compare read it as "released"; IEEE-correct float compares latched the edge detector shut after the first release -- the reason the emitter discharge chain NEVER fired in-game. Fixed with bit-pattern sign compares. 3. The weapon-side submission LIVE: Emitter::FireWeapon fills damageData (amount + damageForce=target-muzzle [the gyro directional-bounce feed] + impact) -> MechWeapon::SendDamageMessage (@004b9728 real body) -> messmgr consolidation with per-weapon records + explosion bundling. The mech4 bring-up damage block + flat kShotDamage retired to diag hooks. Bonus: LODReuseHysteresis 0.82 -> 0.33 (double misread). Zone model verified byte-exact (no change). Solo end-to-end: 5-record volleys (2 PPC + 3 ERML with authored amounts), per-weapon zone granularity, explosions, kill. Heat stays bring-up scale pending the heat-calibration audit [T3]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
71 lines
2.7 KiB
Python
71 lines
2.7 KiB
Python
import struct, sys
|
|
data = open(r'C:\git\bt411\content\BTL4.RES', 'rb').read()
|
|
labOnly, maxID = struct.unpack_from('<ii', data, 4)
|
|
offs = struct.unpack_from('<%dI' % maxID, data, 12)
|
|
|
|
# ResourcePhysicalFormat: id i32, type i32, name[32], prio i32, flags i32, offset i32, length i32
|
|
recs = []
|
|
for o in offs:
|
|
if o == 0: continue
|
|
rid, rt = struct.unpack_from('<ii', data, o)
|
|
nm = data[o+8:o+40].split(b'\0')[0].decode('ascii','replace')
|
|
prio, flags, roff, rlen = struct.unpack_from('<iiII', data, o+40)
|
|
recs.append((rid, rt, nm, roff, rlen))
|
|
|
|
DZ = [r for r in recs if r[1] == 20]
|
|
print('type-0x14 DamageZoneStream resources:')
|
|
for r in DZ:
|
|
print(' id=%d name=%r off=0x%x len=%d' % (r[0], r[2], r[3], r[4]))
|
|
|
|
def parse_zone_stream(buf, tag):
|
|
p = 0
|
|
def i32():
|
|
nonlocal p
|
|
v = struct.unpack_from('<i', buf, p)[0]; p += 4; return v
|
|
def f32():
|
|
nonlocal p
|
|
v = struct.unpack_from('<f', buf, p)[0]; p += 4; return v
|
|
def cstr():
|
|
nonlocal p
|
|
n = i32()
|
|
s = buf[p:p+n].decode('ascii','replace'); p += n + 1
|
|
return s
|
|
count = i32()
|
|
print('\n===== %s: zoneCount=%d streamLen=%d =====' % (tag, count, len(buf)))
|
|
for z in range(count):
|
|
name = cstr()
|
|
sites = []
|
|
for j in range(5):
|
|
c = i32()
|
|
sites.append([i32() for _ in range(c)])
|
|
dap = f32()
|
|
scales = [f32() for _ in range(5)]
|
|
sklcount = i32()
|
|
mats = []
|
|
for s in range(sklcount):
|
|
styp = i32()
|
|
mc = i32()
|
|
names = [cstr() for _ in range(mc)]
|
|
mats.append((styp, names))
|
|
descend = i32(); sibs = i32(); segidx = i32()
|
|
lleg = i32(); rleg = i32(); vital = i32(); ncrit = i32()
|
|
crits = []
|
|
for c in range(ncrit):
|
|
w = f32(); pct = f32(); sidx = i32()
|
|
crits.append((w, pct, sidx))
|
|
nred = i32()
|
|
redirects = [i32() for _ in range(nred)]
|
|
# implied per-type armor points: scale = 1/points -> points = 1/scale
|
|
pts = ['%g' % (1.0/s if s else 0) for s in scales]
|
|
print('zone %2d %-24s armor(default)=%g scale=[%s] -> points=[%s]' %
|
|
(z, name, dap, ' '.join('%.6g' % s for s in scales), ' '.join(pts)))
|
|
print(' seg=%d vital=%d legs(L%d,R%d) descend=%d sibs=%d mats=%d crits=%s redirects=%s' %
|
|
(segidx, vital, lleg, rleg, descend, sibs, sklcount,
|
|
[(round(w,3), round(pc,3), si) for (w,pc,si) in crits], redirects))
|
|
print('consumed %d of %d bytes' % (p, len(buf)))
|
|
|
|
want = [n.lower() for n in (sys.argv[1:] or ['madcat','bhk1'])]
|
|
for (rid, rt, nm, roff, rlen) in DZ:
|
|
if any(w in nm.lower() for w in want):
|
|
parse_zone_stream(data[roff:roff+rlen], 'id=%d %s' % (rid, nm))
|