#!/usr/bin/env python3 """ instance.py - decompiler for a mech `.instance` file. One page named after the chassis, holding the model/armature/subsystem/damage references and the mechlab bar ratings. The record is a single `Mech__CreateMessage`, so the layout comes from the CreateMessage chain rather than the GameModel one: Replicator -> Entity -> Mover -> MWMover -> MWObject -> Vehicle -> Mech `datamap.chain_layout(..., start=16)` computes it -- 16 because the `Connection__Message` header (messageLength, priority, flags) sits in front and is not declared in any of these classes. The result ends at 341 and pads to exactly the 344-byte record, and every offset established independently while decoding `.armature` lands on the nose: classID 16, replicatorID 24, localToParent 28, dataListID 84, alignment 88, jointName 152. Two parser gaps had to be closed to get there, both fields typed with names the member regex did not know: `Stuff::RegisteredClass::ClassID` / `ReplicatorID` in the Replicator base, and `Entity__ExecutionStateEngine::FactoryRequest` / `ObjectID` in Entity. Missing them silently shifted everything after offset 76 by 8 bytes while still producing a plausible-looking table. python3 instance.py [-o out.instance] python3 instance.py --verify """ import argparse, os, re, struct, subprocess, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import datamap, mw4msg, subsystems MANIFEST = "/home/rich/Repositories/FS_Ours_extracted/_manifest.tsv" C = datamap.CODE CHAIN = [ ("Replicator__CreateMessage", C + "/mw4/Libraries/Adept/Replicator.hpp"), ("Entity__CreateMessage", C + "/mw4/Libraries/Adept/Entity.hpp"), ("Mover__CreateMessage", C + "/mw4/Libraries/Adept/Mover.hpp"), ("MWMover__CreateMessage", C + "/mw4/Code/MW4/MWMover.hpp"), ("MWObject__CreateMessage", C + "/mw4/Code/MW4/MWObject.hpp"), ("Vehicle__CreateMessage", C + "/mw4/Code/MW4/Vehicle.hpp"), ("Mech__CreateMessage", C + "/mw4/Code/MW4/Mech.hpp"), ] CONNECTION_HEADER = 16 EXEC_STATE = {1: "NeverExecuteState", 2: "AlwaysExecuteState", 6: "ActiveState"} # Entity.hpp:894 ALIGNMENT = {0: "DefaultAlignment", 1: "Player", 2: "Enemy", 3: "Team1", 4: "Team2", 5: "Team3", 6: "Team4"} # source key -> message member, in the order 61 of 89 sources use KEYS = [ ("Model", "dataListID"), ("ExecutionState", "executionState"), ("Armature", "armatureStreamResourceID"), ("Subsystems", "subsystemStreamResourceID"), ("DamageObjects", "damageStreamResourceID"), ("Alignment", "alignment"), ("CurrentHeat", "currentHeat"), ("CurrentCoolant", "currentCoolant"), ("MaxCoolant", "maxCoolant"), ("DoesHaveInstanceName", "doesHaveInstanceName"), ("PowerRating", "m_powerBar"), ("ArmorRating", "m_armorBar"), ("SpeedRating", "m_speedBar"), ("HeatRating", "m_heatBar"), ] def record(mech_dir): for fn in os.listdir(mech_dir): if fn.lower().endswith(".instance"): return open(os.path.join(mech_dir, fn), "rb").read() raise SystemExit(f"no .instance record in {mech_dir}") def relative(path, chassis): """References are written relative to the mech folder.""" if not path: return path return re.sub(rf'^mechs[\\/]{re.escape(chassis or "")}[\\/]', '', path, flags=re.I) def num(x): return f"{int(x)}" if float(x) == int(x) else f"{x:g}" def decompile(mech_dir, manifest=None): """-> (pageName, [(key, value)])""" manifest = manifest if manifest is not None else subsystems.load_manifest(MANIFEST) blob = record(mech_dir) layout, _ = datamap.chain_layout(CHAIN, {}, start=CONNECTION_HEADER) msgs = list(mw4msg.walk(blob, start=0)) if len(msgs) != 1: raise SystemExit(f"expected one message in {mech_dir}, got {len(msgs)}") msg = msgs[0][1] name = datamap.read(msg, *layout["jointName"]) folder = os.path.basename(mech_dir.rstrip("/")) kv = [] for key, member in KEYS: off, typ, size = layout[member] val = datamap.read(msg, off, typ, size) if member == "executionState": kv.append((key, EXEC_STATE.get(val, str(val)))) elif member == "alignment": kv.append((key, ALIGNMENT.get(val, str(val)))) elif typ == "ResourceID": path = manifest.get(val >> 16) kv.append((key, relative(path, folder) if path else "")) elif typ == "bool": kv.append((key, "yes" if val else "no")) elif typ in datamap.FLOATS: kv.append((key, num(val))) else: kv.append((key, str(val))) return name, kv def emit(name, kv): lines = [f"[{name}]"] + [f"{k}={v}" for k, v in kv] return "\r\n".join(lines) + "\r\n" def main(): ap = argparse.ArgumentParser() ap.add_argument("mech_dir", nargs="?") ap.add_argument("-o", "--output") ap.add_argument("-m", "--manifest", default=MANIFEST) ap.add_argument("--verify", action="store_true") args = ap.parse_args() if args.verify: here = os.path.dirname(os.path.abspath(__file__)) raise SystemExit(subprocess.call([sys.executable, os.path.join(here, "verify_instance.py")])) if not args.mech_dir: ap.error("mech_dir required") name, kv = decompile(args.mech_dir, subsystems.load_manifest(args.manifest)) text = emit(name, kv) if args.output: with open(args.output, "wb") as fh: fh.write(text.encode("latin-1")) print(f"wrote {args.output}") else: sys.stdout.write(text) if __name__ == "__main__": main()