Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.
Layout:
engine/ MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
models) + image codec; the minimal rp/ headers the audio HAL needs
game/ reconstructed BT logic + surviving-original BT source + fwd shims
+ WinMain launcher
content/ full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
docs/ format specs + reconstruction ledgers
reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
tools/ MP console emulator + map/resource scanners
One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
3.1 KiB
Python
72 lines
3.1 KiB
Python
import struct, sys, collections
|
|
|
|
RES = r"C:\git\nick-games\decomp\BTL4.RES"
|
|
data = open(RES, "rb").read()
|
|
|
|
TYPE_NAMES = {
|
|
0:"Null",1:"ModelList",2:"MapList",3:"AudioStreamList",4:"VideoList",5:"SubsystemList",
|
|
6:"ControlMappingsList",7:"DamageZoneList",8:"SkeletonList",9:"BoxedSolidStream",
|
|
10:"VideoModel",11:"StaticAudioStream",12:"InternalAudioStream",13:"ExternalAudioStream",
|
|
14:"MakeMessageStream",15:"GameModel",16:"Animation",17:"SubsystemModelStream",
|
|
18:"GaugeImageStream",19:"ControlMappingStream",20:"DamageZoneStream",21:"SkeletonStream",
|
|
22:"DropZone",23:"EnvironmentZone",24:"InterestZone",25:"VehicleTable",26:"ExistanceBoxStream",
|
|
27:"CameraStream",28:"RegistryStaticObjectStream",29:"DamageLookupTableStream",
|
|
30:"ExplosionTableStream",31:"GaugeAlarmStream",32:"GaugeMissionReviewStream",33:"ScenarioRole"}
|
|
|
|
ver = data[0:4]
|
|
labOnly, maxID = struct.unpack_from("<ii", data, 4)
|
|
print("version bytes:", list(ver), "labOnly:", labOnly, "maxResourceID:", maxID)
|
|
off = 12
|
|
offsets = struct.unpack_from("<%dI" % maxID, data, off)
|
|
|
|
Res = collections.namedtuple("Res", "rid rtype name prio flags off length")
|
|
resources = {}
|
|
for o in offsets:
|
|
if o == 0: continue
|
|
rid, rtype = struct.unpack_from("<ii", data, o)
|
|
name = data[o+8:o+40].split(b"\0")[0].decode("ascii", "replace")
|
|
prio, flags, roff, rlen = struct.unpack_from("<iIII", data, o+40)
|
|
resources[rid] = Res(rid, rtype, name, prio, flags, roff, rlen)
|
|
|
|
bytype = collections.Counter(r.rtype for r in resources.values())
|
|
print("\n== resource count per type ==")
|
|
for t, c in sorted(bytype.items()):
|
|
print("type %2d %-28s : %d" % (t, TYPE_NAMES.get(t, "?"), c))
|
|
|
|
solids = [r for r in resources.values() if r.rtype == 9]
|
|
print("\n== BoxedSolidStream resources (%d) ==" % len(solids))
|
|
for r in sorted(solids, key=lambda x: x.name):
|
|
print(" id=%4d name=%-14s bytes=%6d records=%s" % (r.rid, r.name, r.length, r.length//60))
|
|
|
|
maps14 = [r for r in resources.values() if r.rtype == 14]
|
|
print("\n== MakeMessageStream resources (%d) ==" % len(maps14))
|
|
for r in sorted(maps14, key=lambda x: x.name):
|
|
print(" id=%4d name=%-14s bytes=%d" % (r.rid, r.name, r.length))
|
|
|
|
exist = [r for r in resources.values() if r.rtype == 26]
|
|
print("\n== ExistanceBoxStream (%d) ==" % len(exist))
|
|
for r in sorted(exist, key=lambda x: x.name):
|
|
print(" id=%4d name=%-14s bytes=%d boxes=%s" % (r.rid, r.name, r.length, r.length//24))
|
|
|
|
# Model lists: type 1
|
|
mlists = {r.rid: r for r in resources.values() if r.rtype == 1}
|
|
def list_members(r):
|
|
cnt = struct.unpack_from("<i", data, r.off)[0]
|
|
ids = struct.unpack_from("<%di" % cnt, data, r.off+4)
|
|
return ids
|
|
|
|
print("\n== ModelList count:", len(mlists))
|
|
# which model lists contain a BoxedSolidStream member?
|
|
with_solid = []
|
|
without_solid = []
|
|
for rid, r in mlists.items():
|
|
try:
|
|
ids = list_members(r)
|
|
except Exception as e:
|
|
print(" list decode fail", r.name, e); continue
|
|
types = [resources[i].rtype if i in resources else None for i in ids]
|
|
if 9 in types: with_solid.append(r.name)
|
|
else: without_solid.append(r.name)
|
|
print("model lists WITH solid member: %d" % len(with_solid))
|
|
print("model lists WITHOUT solid member: %d" % len(without_solid))
|