Files
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

190 lines
8.8 KiB
Python

#!/usr/bin/env python
"""
replay-mr.py - parse and "replay" a MechWarrior4 / FireStorm mission-review file
(the mreplay2 ".mr" format, magic 0xab9a655f, written by CMRPFull::SaveMR).
A .mr is a full network replay: session info, the player roster (names, clans,
AI scripts, team, mech/tonnage, and each player's vehicle CreateMessage), then a
chain of per-frame "packs" of network messages (incl. mech-damage events).
A true *visual* replay needs the game engine (CMRPFull::DoReplay reconstructs the
match from the recorded CreateMessages + update messages). This script does the
standalone, scriptable part: it decodes the structure and replays it as a textual
play-by-play -- match info, roster, frame timeline, and a timestamped combat log
built from the MRP_MECHDAMAGETAKEN events.
Usage:
python replay-mr.py FILE.mr [--frames] [--commands] [--core PATH]
"""
import struct, sys, argparse, os
MAGIC = 0xab9a655f
MAGIC_OLDA = 0xab9a655e # g_dwMR_MetaOldA (packs have no per-frame time)
# SMRP command ids (mreplay2.h)
MRP_ADD_PLAYER_NAME, MRP_REMOVE_PLAYER_NAME = 1000, 1001
MRP_STOP_FRAME, MRP_FULLCONFIRM, MRP_RESPAWN = 2000, 2001, 2002
MRP_MECHDAMAGETAKEN = 3000
CMD_NAME = {1000:"ADD_PLAYER",1001:"REMOVE_PLAYER",2000:"STOP_FRAME",
2001:"FULLCONFIRM",2002:"RESPAWN",3000:"MECH_DAMAGE"}
class Reader:
def __init__(self, buf): self.b=buf; self.p=0
def bytes(self,n):
v=self.b[self.p:self.p+n]; self.p+=n
if len(v)!=n: raise EOFError("short read")
return v
def u8(self): return self.bytes(1)[0]
def s8(self): return struct.unpack("<b",self.bytes(1))[0]
def u16(self): return struct.unpack("<H",self.bytes(2))[0]
def i32(self): return struct.unpack("<i",self.bytes(4))[0]
def u32(self): return struct.unpack("<I",self.bytes(4))[0]
def f32(self): return struct.unpack("<f",self.bytes(4))[0]
def mstr(self): # MString: [u32 len]; if len>0: len+1 bytes (chars+null)
n=self.i32()
if n<0 or n>1_000_000: raise ValueError(f"bad MString len {n} @ {self.p-4}")
return self.bytes(n+1)[:n].decode("latin-1") if n else ""
def resid(self): # Adept::ResourceID {short fileID, WORD recordID}
fid=struct.unpack("<h",self.bytes(2))[0]; rid=self.u16(); return (fid,rid)
def parse(path):
r=Reader(open(path,"rb").read())
meta=r.u32()
if meta not in (MAGIC, MAGIC_OLDA):
raise SystemExit(f"{path}: not an mreplay2 .mr (magic={meta:#x})")
# DPSESSIONDESC2 (80 bytes): pull a few useful fields, skip the rest
sess=r.bytes(80)
dwFlags, = struct.unpack_from("<I",sess,4)
maxPlayers,curPlayers = struct.unpack_from("<II",sess,40)
game = {"meta":meta,"dwFlags":dwFlags,"maxPlayers":maxPlayers,"curPlayers":curPlayers}
game["gameName"]=r.mstr()
game["myDPID"]=r.u32()
game["player"]=r.mstr()
game["password"]=r.mstr()
flags=r.u8()
game.update(starting=bool(flags&1),started=bool(flags&2),stopping=bool(flags&4),
stopped=bool(flags&8),teamGame=bool(flags&16))
msz=r.i32(); game["missionParam"]=r.bytes(msz)
nbot=r.i32(); nload=r.i32(); nbcast=r.i32()
game.update(nBOT=nbot,nLoading=nload,nBroadcast=nbcast)
players=[]
for _ in range(nbot+nload+nbcast):
bot=bool(r.u8()&1) # WriteBit + WriteByteAlign => 1 byte
pl={"bot":bot,"name":r.mstr(),"clan":r.mstr(),"script":r.mstr(),
"conn":r.u8(),"team":r.i32(),"pilotDecals":r.i32(),"teamDecals":r.i32(),
"mechChassisID":r.i32(),"pilotSkins":r.s8(),"tonnage":r.i32(),
"aiModel":r.resid()}
vcmlen=r.i32(); pl["vcm"]=r.bytes(vcmlen)
players.append(pl)
game["frameTime0"]=r.f32()
npacks=r.i32(); packs=[]
for _ in range(npacks):
ncount=r.i32(); ftime=0.0 if meta==MAGIC_OLDA else r.f32()
entries=[]
for _ in range(ncount):
cmd=r.i32(); typ=r.i32(); ln=r.i32(); payload=r.bytes(ln)
entries.append((cmd,typ,payload))
packs.append((ftime,entries))
return game, players, packs, r.p, len(r.b)
def core_id2name(core_path):
try: f=open(core_path,"rb")
except OSError: return {}
hdr=f.read(20)
if hdr[:4]!=b"#VBD": return {}
_,_,_,indexsize,nrec,_=struct.unpack_from("<4sIIIHH",hdr,0)
idx=f.read(indexsize-20); off=0; m={}
for _ in range(nrec):
off+=8+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 mech_of(player, core):
"""The captured VCM stub is [BYTE][WORD count][WORD modelRecordID]; the model id
indexes a core.mw4 record (mechs\\X\\X.instance). Returns the chassis folder or None."""
vcm=player["vcm"]
if len(vcm)>=5:
rid=struct.unpack_from("<H",vcm,3)[0]
n=core.get(rid,"")
if n.startswith("mechs\\"):
return n.split("\\")[1]
return None
def fmt_time(t): return f"{int(t)//60:d}:{t%60:05.2f}"
def main():
ap=argparse.ArgumentParser()
ap.add_argument("file"); ap.add_argument("--frames",action="store_true")
ap.add_argument("--commands",action="store_true")
ap.add_argument("--core",default=r"Gameleap/mw4/resource/core.mw4")
a=ap.parse_args()
game,players,packs,consumed,total=parse(a.file)
core=core_id2name(a.core)
for p in players: p["mech"]=mech_of(p,core)
# ---- collect damage events keyed by full ReplicatorID (connectionID, localID) ----
# id1 = victim (took damage), id2 = attacker (inflicted)
dmg_dealt={}; dmg_taken={}; events=[]; cmd_hist={}; ids=set()
for ftime,entries in packs:
for cmd,typ,payload in entries:
cmd_hist[cmd]=cmd_hist.get(cmd,0)+1
if cmd==MRP_MECHDAMAGETAKEN and len(payload)>=12:
dmg,con1,wid,lid1,con2,dtype,lid2=struct.unpack_from("<fBBHBBH",payload,0)
vic=(con1,lid1); att=(con2,lid2)
events.append((ftime,att,vic,dmg,wid,dtype)); ids|={vic,att}
dmg_dealt[att]=dmg_dealt.get(att,0.0)+dmg
dmg_taken[vic]=dmg_taken.get(vic,0.0)+dmg
# ---- resolve ReplicatorID -> name ----
# Humans pilot entity (conn, localID 2) on their own connection (reliable).
# Bots are all replicated on the host connection -> resolved by spawn order (inferred '~').
name_of={}
humans=[p for p in players if not p["bot"] and p["mech"]]
for p in humans: name_of[(p["conn"],2)] = p["name"]
bot_ids=sorted(i for i in ids if i not in name_of)
bots=[p for p in players if p["bot"]] # roster (spawn) order
inferred=set()
for i,bid in enumerate(bot_ids):
if i<len(bots): name_of[bid]=bots[i]["name"]+"~"; inferred.add(bid)
else: name_of[bid]=f"AI[{bid[0]}:{bid[1]}]"
def who(rid): return name_of.get(rid, f"{rid[0]}:{rid[1]}")
print(f"=========== MISSION REVIEW: {os.path.basename(a.file)} ===========")
print(f"game name : {game['gameName']}")
print(f"host : {game['player']} team game: {game['teamGame']} maxPlayers: {game['maxPlayers']}")
print(f"players : {len(players)} (bots={game['nBOT']} loading={game['nLoading']} bcast={game['nBroadcast']})")
print(f"frames : {len(packs)} end time: {fmt_time(packs[-1][0]) if packs else '0'}")
print(f"parsed {consumed}/{total} bytes" + ("" if consumed==total else " <-- WARNING: trailing bytes"))
print("\n--- ROSTER ---")
print(f"{'conn':>4} {'BOT':>3} {'team':>4} {'ton':>4} {'mech':<10} {'clan':<14} name")
for p in sorted(players,key=lambda x:(x["bot"],x["conn"])):
mech=p["mech"] or ("(spectator)" if not p["bot"] else f"id{p['mechChassisID']}")
print(f"{p['conn']:>4} {'Y' if p['bot'] else '-':>3} {p['team']:>4} {p['tonnage']:>4} "
f"{mech:<10} {p['clan'][:14]:<14} {p['name']}"
+ (f" [AI:{os.path.basename(p['script'])}]" if p['bot'] and p['script'] else ""))
print("\n--- DAMAGE TOTALS ---")
print(f"{'dealt':>10} {'taken':>10} combatant")
for rid in sorted(ids, key=lambda r:-dmg_dealt.get(r,0)):
print(f"{dmg_dealt.get(rid,0):>10.1f} {dmg_taken.get(rid,0):>10.1f} {who(rid)}")
print(f"\n--- COMBAT LOG ({len(events)} hits) ---")
show = events if a.frames else events[:50]
for ftime,att,vic,dmg,wid,dtype in show:
print(f" [{fmt_time(ftime)}] {who(att):>16} -> {who(vic):<16} {dmg:6.1f} dmg (weaponID {wid}, type {dtype})")
if not a.frames and len(events)>len(show):
print(f" ... {len(events)-len(show)} more (use --frames for all)")
if inferred:
print("\n (~ = bot identity inferred from spawn order; bots replicate on the host"
"\n connection so their exact name<->entity link isn't stored in the file.)")
if a.commands:
print("\n--- COMMAND HISTOGRAM ---")
for cmd,cnt in sorted(cmd_hist.items()):
print(f" {CMD_NAME.get(cmd, f'net_msg_type_{cmd}'):<18} {cnt}")
if __name__=="__main__":
main()