- Emitters: heatPortion closed form = heatCostToFire x 1e7 x (charge/seekV)^2 (PPC 1.1e8 -> +632K on its own sink); the missing projectile/missile heat adds (raw -- resources author pre-scaled 1e7 units). - The bank's AMBIENT RADIATOR @4ae73c reconstructed (the system's ONLY heat exit; conductance x 0.1 x HeatSinkCount -- _DAT_004ae974 float80 = 0.1) + the link-attach guard corrected (skip = the 0xBBE bank, not Condenser; the inversion blocked condenser->bank links and closed the system). - Constant corrections (all byte-verified float80s): coolant epsilons 0.0025/0.003/1e-4 (was a single 1e-4 serving three sites); CoolantCapacityScale 0.05 (was 1.738). - Verified: heat flows + exits; max-rate autofire overheats weapons into the authentic heatLoad range-cutout (thermal spam unsustainable by design). - KNOWN REMAINING: linked-sink routing scrambled (heat pools in Condenser1; authored map says PPC->C4/C6) -- the heat-stream offset audit, filed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
import struct
|
|
data = open(r'C:/git/bt411/content/BTL4.RES','rb').read()
|
|
n = len(data)
|
|
|
|
def cstr(off, maxlen=32):
|
|
s = data[off:off+maxlen].split(b'\0')[0]
|
|
try: return s.decode('ascii')
|
|
except: return None
|
|
|
|
def f32(off): return struct.unpack_from('<f', data, off)[0]
|
|
def i32(off): return struct.unpack_from('<i', data, off)[0]
|
|
|
|
# find ALL subsystem records (same heuristic as scan_weapsub.py)
|
|
recs = []
|
|
i = 0
|
|
while i < n - 0x28:
|
|
cid = struct.unpack_from('<I', data, i+0x20)[0]
|
|
if 0xBB0 <= cid <= 0xBE5:
|
|
sz = struct.unpack_from('<I', data, i+0x24)[0]
|
|
if 0xE0 <= sz <= 0x300:
|
|
name = cstr(i)
|
|
if name and 2 <= len(name) <= 31 and all(32 < ord(c) < 127 for c in name):
|
|
pad = data[i+len(name):i+0x20]
|
|
if pad.count(0) >= len(pad) - 2:
|
|
recs.append((i, name, cid, sz))
|
|
i += 0x28
|
|
continue
|
|
i += 1
|
|
|
|
# group into chains (consecutive: next record at p+sz)
|
|
chains = []
|
|
cur = []
|
|
for k,(p,name,cid,sz) in enumerate(recs):
|
|
if cur and cur[-1][0] + cur[-1][3] == p:
|
|
cur.append(recs[k])
|
|
else:
|
|
if cur: chains.append(cur)
|
|
cur = [recs[k]]
|
|
if cur: chains.append(cur)
|
|
|
|
mechnames = [b'madcat', b'bhk', b'vulture', b'thor', b'blackhawk', b'blh', b'owens', b'strider', b'sunder', b'cauldron', b'avatar', b'mdc']
|
|
low = data.lower()
|
|
namepos = []
|
|
for m in mechnames:
|
|
j = 0
|
|
while True:
|
|
j = low.find(m, j)
|
|
if j < 0: break
|
|
namepos.append((j, m.decode()))
|
|
j += 1
|
|
namepos.sort()
|
|
|
|
def nearest_label(p):
|
|
best = None
|
|
for pos, name in namepos:
|
|
if pos < p: best = (pos, name)
|
|
else: break
|
|
return best
|
|
|
|
HEAT = {0xBBD:'Condenser', 0xBBE:'AggregateHeatSink', 0xBC0:'Reservoir', 0xBBB:'HeatSink?', 0xBBC:'?'}
|
|
WEAP = {0xBC8:'Emitter', 0xBD4:'PPC', 0xBCD:'ProjWeapon', 0xBCE:'GaussRifle', 0xBD0:'MissileLauncher'}
|
|
|
|
for ci, ch in enumerate(chains):
|
|
lbl = nearest_label(ch[0][0])
|
|
names = [c[1] for c in ch]
|
|
print(f'--- chain {ci} @0x{ch[0][0]:x} n={len(ch)} nearest-name={lbl}')
|
|
for idx,(p,name,cid,sz) in enumerate(ch):
|
|
tag = HEAT.get(cid) or WEAP.get(cid)
|
|
if cid in HEAT:
|
|
st, dg, ft = f32(p+0xE4), f32(p+0xE8), f32(p+0xEC)
|
|
tc, tm = f32(p+0xF0), f32(p+0xF4)
|
|
hsi = i32(p+0xF8)
|
|
line = (f' [{idx:2d}] {name:20s} cid=0x{cid:X}({tag}) sz=0x{sz:X} '
|
|
f'StartT={st:g} DegT={dg:g} FailT={ft:g} Conduct={tc:g} ThermMass={tm:g} HeatSinkIdx={hsi}')
|
|
if cid == 0xBBD:
|
|
line += f' RefrigFactor={f32(p+0xFC):g}'
|
|
elif cid == 0xBBE:
|
|
line += f' HeatSinkCount={i32(p+0xFC)}'
|
|
elif cid == 0xBC0:
|
|
line += f' CoolantCap={f32(p+0xFC):g} SquirtMass={f32(p+0x100):g}'
|
|
print(line)
|
|
elif cid in WEAP:
|
|
heat = f32(p+0x1A4)
|
|
print(f' [{idx:2d}] {name:20s} cid=0x{cid:X}({tag}) Heat={heat:g}')
|
|
else:
|
|
print(f' [{idx:2d}] {name:20s} cid=0x{cid:X} sz=0x{sz:X}')
|