Files
firestorm/build-env/variant-to-subsystems.py
Cyd 2b8ca921cb Initial full mirror of c:\VWE (source + assets + toolchain + outputs) via Git LFS
Complete disaster-recovery snapshot: engine/game source, game data assets,
VC6 toolchain + DX SDKs, build outputs, deployed game, and _UNUSED archive.
Large binaries in Git LFS; text preserved byte-for-byte (core.autocrlf=false,
no eol attributes). See RECOVERY.md for the one-clone rebuild procedure.
2026-06-24 21:28:16 -05:00

215 lines
9.7 KiB
Python

#!/usr/bin/env python
"""
variant-to-subsystems.py - translate a MechLab-saved variant .mw4 back into the
human-readable .subsystems text format.
A variant .mw4 stores the loadout as compiled binary replication CreateMessages
(Mech::SaveMakeMessage). This decodes the {Subsystem} record:
- each subsystem = a length-prefixed message; base message = 108 bytes
- dataListID (Adept::ResourceID {short fileID, WORD recordID}) @ off 84
recordID -> record name in core.mw4 (the weapon/component .data path)
- locationID (BYTE) @ off 100 -> InternalDamageObject zone enum
- Weapon messages append: siteName[128]@108, ejectSiteName[128]@236,
groupIndex@364, ammoCount@368, initialAmmoCount@372, weaponFacing@376
- SearchLight appends siteName[128]@108 ; Engine appends EngineUpgrades int @108
Usage: python variant-to-subsystems.py "Arctic Wolf 1.mw4" [...] [--core PATH] [-o DIR]
"""
import struct, sys, os, re, argparse, glob
LOC = {0:"LeftLeg",1:"RightLeg",2:"LeftArm",3:"RightArm",4:"RightTorso",
5:"LeftTorso",6:"CenterTorso",7:"Head",8:"Special1",9:"Special2",
255:"Null"}
ARMOR_TYPE = {0:"Standard",1:"FerroFiberus",2:"Reactive",3:"Reflective",4:"Solarian"}
INTERNAL_TYPE = {0:"Standard",1:"EndoSteel"}
# Points-per-ton from Content\Subsystems\Armor.data; the compiled message stores
# absolute points = (.subsystems tonnage value) * pointsPerTon, so we divide to recover tonnage.
POINTS_PER_TON = {0:32.0, 1:38.0, 2:30.0, 3:30.0, 4:60.0}
def fmtf(v):
s=f"{v:.4f}".rstrip("0").rstrip(".")
return s if "." in s else s+".0"
# ---- LZW (gos_LZDecompress port) ----
def lz_decompress(src):
src = bytes(src) + b"\x00\x00\x00\x00"
bitpos = 0
def getcode(width, mask):
nonlocal bitpos
i = bitpos >> 3
v = src[i] | (src[i+1]<<8) | (src[i+2]<<16) | (src[i+3]<<24)
v = (v >> (bitpos & 7)) & mask; bitpos += width; return v
chain=[0]*8192; suffix=[0]*8192
width,mask,maxindex,free = 9,511,512,258
oldchain=oldsuffix=0; out=bytearray()
while True:
code=getcode(width,mask)
if code==257: break
if code==256:
width,mask,maxindex,free=9,511,512,258
code=getcode(width,mask); out.append(code&0xff)
oldchain,oldsuffix=code,code&0xff; continue
stack=[]
if code>=free:
stack.append(oldsuffix); suffix[code]=oldsuffix; chain[code]=oldchain; walk=oldchain
else: walk=code
while walk>=256: stack.append(suffix[walk]); walk=chain[walk]
stack.append(walk); oldsuffix=walk&0xff; out+=bytes(reversed(stack))
suffix[free]=oldsuffix; chain[free]=oldchain; free+=1; oldchain=code
if free==maxindex and width!=12:
width+=1; maxindex<<=1; mask=(mask<<1)|1
return bytes(out)
# ---- #VBD container ----
def parse_db(path):
data=open(path,"rb").read()
tag,fmt,content,indexsize,nrec,nextid=struct.unpack_from("<4sIIIHH",data,0)
assert tag==b"#VBD", f"{path}: not #VBD"
recs={}; ids={}; off=20
for _ in range(nrec):
off+=8
dlen,rlen,doff=struct.unpack_from("<III",data,off); off+=12
rid,nlen=struct.unpack_from("<HB",data,off); off+=3
name=data[off:off+nlen].decode("latin-1"); off+=nlen
blob=b""
if dlen:
raw=data[indexsize+doff:indexsize+doff+rlen]
blob=lz_decompress(raw) if rlen<dlen else raw
recs[name]=blob; ids[rid]=name
return content, recs, ids
def core_id2name(core_path):
f=open(core_path,"rb"); hdr=f.read(20)
_,_,_,indexsize,nrec,_=struct.unpack_from("<4sIIIHH",hdr,0)
idx=f.read(indexsize-20); off=0; m={}
for _ in range(nrec):
off+=8; off+=12
rid,nlen=struct.unpack_from("<HB",idx,off); off+=3
m[rid]=idx[off:off+nlen].decode("latin-1"); off+=nlen
return m
def propercase_map(content_root):
"""lowercased relative path -> actual-case relative path, for Model= fidelity."""
m={}
for dp,_,fns in os.walk(content_root):
for fn in fns:
full=os.path.join(dp,fn)
rel=os.path.relpath(full,content_root)
m[rel.lower().replace("/","\\")]=rel.replace("/","\\")
return m
def cstr(b,o):
e=b.find(b"\x00",o); return b[o:e if e>=0 else len(b)].decode("latin-1")
def weapon_section(path):
p=path.lower()
if any(x in p for x in ("laserweaponsubsystem","pulselaser","ppcweapon","flamerweapon","firehose","bombast")):
return "Beam"
if any(x in p for x in ("acweaponsubsystem","gaussweapon","machinegun","lbxac","ultraac","rtxac")):
return "Ballistic"
return "Missile"
def translate(path, core_map, case_map):
content, recs, ids = parse_db(path)
sub_key=[k for k in recs if k.endswith("{Subsystem}")]
if not sub_key: raise SystemExit(f"{path}: no {{Subsystem}} record")
blob=recs[sub_key[0]]
chassis=sub_key[0][:-len("{Subsystem}")]
n,=struct.unpack_from("<H",blob,0); pos=2; msgs=[]
while pos+4<=len(blob):
mlen,=struct.unpack_from("<I",blob,pos)
if mlen<=0 or pos+mlen>len(blob): break
msgs.append(blob[pos:pos+mlen]); pos+=mlen
def model_of(b):
fid,rid=struct.unpack_from("<hH",b,84)
nm=core_map.get(rid,f"<rec {rid}>")
return case_map.get(nm, nm) # restore proper case if known
def loc_of(b):
return LOC.get(b[100], f"loc{b[100]}")
def crit_of(b):
return struct.unpack_from("<i",b,104)[0]
out=[]; counters={"Beam":0,"Ballistic":0,"Missile":0,"HeatSink":0}
for b in msgs:
model=model_of(b); loc=loc_of(b); ml=model.lower(); L=len(b)
base=os.path.basename(ml)
if ml.endswith(".engine"):
up,=struct.unpack_from("<i",b,108) if L>=112 else (0,)
out.append(("Engine","NeverExecuteState",
[("Model",os.path.basename(model)),("InternalLocation",loc),("EngineUpgrades",up)]))
elif base=="armor.data":
ll,rl,la,ra,lf,rf,cf,cr,hd,atype,itype=struct.unpack_from("<9f2i",b,108)
ppt=POINTS_PER_TON.get(atype,1.0) # convert absolute points back to tonnage
out.append(("Armor","NeverExecuteState",[
("Model","Subsystems\\Armor.data"),("InternalLocation",loc),
("ArmorType",ARMOR_TYPE.get(atype,atype)),("InternalType",INTERNAL_TYPE.get(itype,itype)),
("LeftArm",fmtf(la/ppt)),("RightArm",fmtf(ra/ppt)),("LeftLeg",fmtf(ll/ppt)),("RightLeg",fmtf(rl/ppt)),
("LeftFrontTorso",fmtf(lf/ppt)),("RightFrontTorso",fmtf(rf/ppt)),("CenterFrontTorso",fmtf(cf/ppt)),
("CenterRearTorso",fmtf(cr/ppt)),("Head",fmtf(hd/ppt))]))
elif "advancedgyro" in base:
out.append(("AdvancedGyro","NeverExecuteState",
[("Model","Subsystems\\AdvancedGyroSubsystem.data"),("InternalLocation",loc)]))
elif "sensorsubsystem" in base:
out.append(("Sensor","ActiveState",
[("Model","Subsystems\\SensorSubsystem.data"),("InternalLocation",loc)]))
elif ml.endswith(".torso"):
out.append(("Torso","AlwaysExecuteState",
[("Model",os.path.basename(model)),("InternalLocation",loc)]))
elif "searchlight" in base:
out.append(("SearchLight","AlwaysExecuteState",
[("Model","Subsystems\\SearchLightSubsystem.data"),("CriticalHitsTaken",crit_of(b)),
("Site",cstr(b,108)),("InternalLocation",loc)]))
elif "heatsink" in base:
counters["HeatSink"]+=1
out.append((f"HeatSink{counters['HeatSink']}","NeverExecuteState",
[("Model","Subsystems\\HeatSinkSubsystem.data"),("InternalLocation",loc)]))
elif "jumpjet" in base:
out.append(("JumpJet","NeverExecuteState",
[("Model","Subsystems\\JumpJetSubsystem.data"),("CriticalHitsTaken",crit_of(b)),("InternalLocation",loc)]))
elif "weaponsubsystems" in ml:
site=cstr(b,108)
grp,ammo,iammo,facing=struct.unpack_from("<iiii",b,364) if L>=380 else (0,0,0,0)
sec=weapon_section(ml); counters[sec]+=1
fields=[("Model",model),("CriticalHitsTaken",crit_of(b)),("Site",site),
("GroupIndex",grp),("InternalLocation",loc)]
if ammo > 0: fields.append(("AmmoCount",ammo)) # energy weapons report -1 (no ammo)
if facing: fields.append(("WeaponFacing",facing))
out.append((f"{sec} {counters[sec]}","NeverExecuteState",fields))
else:
out.append((f"Subsystem({base})","NeverExecuteState",[("Model",model),("InternalLocation",loc)]))
return chassis, out
def render(chassis, sections):
lines=[f"// translated from variant .mw4 (chassis: {chassis})",""]
for name,exec_state,fields in sections:
lines.append(f"[{name}]")
lines.append(f"ExecutionState={exec_state}")
for k,v in fields:
lines.append(f"{k}={v}")
lines.append("")
return "\n".join(lines)
def main():
ap=argparse.ArgumentParser()
ap.add_argument("files",nargs="+")
ap.add_argument("--core",default=r"Gameleap/mw4/resource/core.mw4")
ap.add_argument("--content",default=r"Gameleap/mw4/Content")
ap.add_argument("-o","--outdir")
a=ap.parse_args()
core_map=core_id2name(a.core)
case_map=propercase_map(a.content) if os.path.isdir(a.content) else {}
for f in a.files:
chassis,secs=translate(f,core_map,case_map)
text=render(chassis,secs)
if a.outdir:
os.makedirs(a.outdir,exist_ok=True)
fn=os.path.join(a.outdir,os.path.splitext(os.path.basename(f))[0]+".subsystems")
open(fn,"w").write(text); print("wrote",fn)
else:
print(f"\n######## {os.path.basename(f)} ########")
print(text)
if __name__=="__main__":
main()