Add MW4COMPARE: .mw4 decompiler toolchain and V4H comparison harness

Tooling built to recover editable source for six 'Mech chassis that exist
in the parallel FS_Build_V4H build but not in this repo. Reverse-engineers
every compiled record type in the .mw4 package format back to the .data /
.instance / .subsystems / .damage / .contents / .torso / .engine /
.armature sources the content pipeline consumes.

Nothing here is wired into the game build. It is a standalone analysis
harness run from Linux.

Package format
--------------
"#VBD" container. Directory records are [len][name][FILETIME][origSize]
[storedSize][offset], payload base at dword 0x0C. A record is stored raw
when storedSize == origSize, otherwise LZW (9->12-bit LSB-first codes,
256=clear, 257=EOF, dict from 258), per Database.cpp:451.

GameModel records are flat /Zp4 structs following the C++ inheritance
chain Entity(0) -> Mover(28) -> MWObject(80) -> Vehicle(664) -> Mech(756),
1636 bytes total. CreateMessage records follow Replicator -> Entity ->
Mover -> MWMover -> MWObject -> Vehicle -> Mech from start=16 (the
undeclared Connection__Message header), ending at 341 and padded to 344.

tools/decompile/
----------------
  datamap.py        header-driven layout engine; CHAIN + ANCHORS
                    {Vehicle:664, Mech:756} assert the struct offsets
  mw4msg.py         CreateMessage reader/walker
  data.py           .data      constants.py  define/table symbol resolution
  damage.py         .damage    contents.py   .contents
  smallmodel.py     .torso + .engine         instance.py  .instance
  armature.py / armature_parts.py  .armature + armaturedata/armaturevideo
  assembly.py       joint hierarchy renderer
  make_generic_doll.py  builds generic MFD/Radar damage dolls
  verify_*.py       per-type round-trip verifiers

Verified round-trip across all 64 shared chassis:
  .armature      2938/2976 pages     .subsystems  7579/7585 keys
  .data map      6071/6071 values    .data trip   8291/8306 keys
  .damage        6605/6605 keys      .contents    7480/7480 keys
  .torso+.engine 1280/1280 keys      .instance     896/896 keys, 64/64 pages
  armature_parts 1202/1202 .data, 1149/1202 .video

Layout-discovery lessons (documented in DECOMPILING.md)
-------------------------------------------------------
- Never let a field map be discovered by the values that verify it. A
  value-matching pass reported 4288/4288 while mis-assigning 34 keys. The
  map was rebuilt from header declaration order, anchored on uniquely
  resolved fields.
- Read the factory, not the data. 12 .data fields and 5 Torso fields are
  declared plain Stuff::Scalar but multiplied by Radians_Per_Degree in
  Mech_Tool.cpp:889 / Torso_Tool.cpp.
- Strip typedefs before walking a header. A stray `typedef int AttributeID;`
  masked a missing ClassID - two 4-byte errors cancelling out, caught only
  by the ANCHORS assertion.
- A verifier that silently narrows its own input reports success. Braced
  blocks must be hidden before splitting pages, replacing both CR and LF,
  because a `Shadow={...}` block contains a line reading `[shadow]` and
  splitlines() also splits on bare CR.
- NSWIZZLE is undefined, so the #else branch is live and orders members
  differently. bool is 1 byte; char x[MaxStringLength] is 256.
- V4H carries stale Mech IDs (their Atlas is 5, ours 6), so 64 of 65 shared
  chassis are off by one; --retarget-ids emits $(M_<Chassis>)/$(IDS_<Chassis>).

reports/ holds generated diffs. The two ~5 MB manifest-*.tsv intermediates
are gitignored; regenerate everything with run-comparison.sh.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
This commit is contained in:
2026-08-08 16:47:57 -05:00
co-authored by Claude Opus 5 GitHub Copilot
parent e088555b96
commit 83478b7666
60 changed files with 13442 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
mech_name[ 0 ] = localize$(DNL_ANNIHILATOR)
mech_name[ 1 ] = localize$(DNL_ARCHER)
mech_name[ 2 ] = localize$(DNL_ARCTICWOLF)
mech_name[ 3 ] = localize$(DNL_ARES)
mech_name[ 4 ] = localize$(DNL_ARGUS)
mech_name[ 5 ] = localize$(DNL_ASSASSIN2)
mech_name[ 6 ] = localize$(DNL_ATLAS)
mech_name[ 7 ] = localize$(DNL_AVATAR)
mech_name[ 8 ] = localize$(DNL_AWESOME)
mech_name[ 9 ] = localize$(DNL_BATTLEMASTER)
mech_name[ 10 ] = localize$(DNL_BATTLEMASTERIIC)
mech_name[ 11 ] = localize$(DNL_BEHEMOTH)
mech_name[ 12 ] = localize$(DNL_BEHEMOTHII)
mech_name[ 13 ] = localize$(DNL_BLACKHAWK)
mech_name[ 14 ] = localize$(DNL_BLACKKNIGHT)
mech_name[ 15 ] = localize$(DNL_BLACKLANNER)
mech_name[ 16 ] = localize$(DNL_BRIGAND)
mech_name[ 17 ] = localize$(DNL_BUSHWACKER)
mech_name[ 18 ] = localize$(DNL_CATAPULT)
mech_name[ 19 ] = localize$(DNL_CAULDRONBORN)
mech_name[ 20 ] = localize$(DNL_CHIMERA)
mech_name[ 21 ] = localize$(DNL_COMMANDO)
mech_name[ 22 ] = localize$(DNL_COUGAR)
mech_name[ 23 ] = localize$(DNL_CYCLOPS)
mech_name[ 24 ] = localize$(DNL_DAISHI)
mech_name[ 25 ] = localize$(DNL_DEIMOS)
mech_name[ 26 ] = localize$(DNL_DRAGON)
mech_name[ 27 ] = localize$(DNL_FAFNIR)
mech_name[ 28 ] = localize$(DNL_FLEA)
mech_name[ 29 ] = localize$(DNL_GLADIATOR)
mech_name[ 30 ] = localize$(DNL_GRIZZLY)
mech_name[ 31 ] = localize$(DNL_HAUPTMANN)
mech_name[ 32 ] = localize$(DNL_HELLHOUND)
mech_name[ 33 ] = localize$(DNL_HELLSPAWN)
mech_name[ 34 ] = localize$(DNL_HIGHLANDER)
mech_name[ 35 ] = localize$(DNL_HOLLANDERII)
mech_name[ 36 ] = localize$(DNL_HUNCHBACK)
mech_name[ 37 ] = localize$(DNL_KODIAK)
mech_name[ 38 ] = localize$(DNL_LOKI)
mech_name[ 39 ] = localize$(DNL_LONGBOW)
mech_name[ 40 ] = localize$(DNL_MADCAT)
mech_name[ 41 ] = localize$(DNL_MADCAT2)
mech_name[ 42 ] = localize$(DNL_MASAKARI)
mech_name[ 43 ] = localize$(DNL_MAULER)
mech_name[ 44 ] = localize$(DNL_NOVACAT)
mech_name[ 45 ] = localize$(DNL_OSIRIS)
mech_name[ 46 ] = localize$(DNL_OWENS)
mech_name[ 47 ] = localize$(DNL_PUMA)
mech_name[ 48 ] = localize$(DNL_RAVEN)
mech_name[ 49 ] = localize$(DNL_RIFLEMAN)
mech_name[ 50 ] = localize$(DNL_RYOKEN)
mech_name[ 51 ] = localize$(DNL_SHADOWCAT)
mech_name[ 52 ] = localize$(DNL_SOLITAIRE)
mech_name[ 53 ] = localize$(DNL_SUNDER)
mech_name[ 54 ] = localize$(DNL_TEMPLAR)
mech_name[ 55 ] = localize$(DNL_THANATOS)
mech_name[ 56 ] = localize$(DNL_THOR)
mech_name[ 57 ] = localize$(DNL_ULLER)
mech_name[ 58 ] = localize$(DNL_URBANMECH)
mech_name[ 59 ] = localize$(DNL_UZIEL)
mech_name[ 60 ] = localize$(DNL_VICTOR)
mech_name[ 61 ] = localize$(DNL_VULTURE)
mech_name[ 62 ] = localize$(DNL_WARHAMMER)
mech_name[ 63 ] = localize$(DNL_WOLFHOUND)
mech_name[ 64 ] = localize$(DNL_ZEUS)
mech_name[ 65 ] = localize$(DNL_CHAMPION)
mech_name[ 66 ] = localize$(DNL_JENNER2C)
mech_name[ 67 ] = localize$(DNL_DASHER)
mech_name[ 68 ] = localize$(DNL_MARAUDER)
mech_name[ 69 ] = localize$(DNL_THUNDERBOLT)
mech_name[ 70 ] = localize$(DNL_GRIFFIN)
mech_name[ 71 ] = localize$(IDS_ML_NA_INFO)
mech_name[ 72 ] = localize$(IDS_ML_NA_INFO)