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>
733 lines
36 KiB
Markdown
733 lines
36 KiB
Markdown
# MW4COMPARE -- FireStorm source tree vs. the `FS_Build_V4H` binary build
|
|
|
|
Comparison of our source repository against **FireStorm Battletech SMT Build Four (v4h)**,
|
|
a parallel, independently-maintained *binary-only* build of the same game, located at
|
|
`/home/rich/Repositories/FS_Build_V4H` (1.7 GB, dated Oct/Nov 2021).
|
|
|
|
Goal: work out what each side has that the other does not, so the two can eventually be
|
|
merged into one build containing all the mechs, maps, game types and variants.
|
|
|
|
**Status: Part 1 (survey) complete. Part 2 (extraction + pruning) complete.** Nothing has
|
|
been merged into the repo. Every V4H asset is unpacked to a parallel tree outside the repo
|
|
at `/home/rich/Repositories/FS_Build_V4H_extracted`, reduced to **differences only** --
|
|
5,385 files / 65 MB out of an original 46,707 files / 1.24 GB -- and reshaped to mirror
|
|
`Gameleap/mw4`. See section 5.
|
|
|
|
---
|
|
|
|
## 0. TL;DR
|
|
|
|
| | V4H has, we don't | We have, V4H doesn't |
|
|
|---|---|---|
|
|
| **Mechs** | **6 new chassis** (IDs 65-70): Champion, Jenner IIC, Dasher, Marauder, Thunderbolt, Griffin -- full geometry, textures, HUD/MFD/radar art, footsteps | Source for ~25 unregistered/experimental chassis (Blackheart, Bowman, Canis, Crab, Gargoyle, koto, locust2c, marauder2, pitbull, privateer, razorback, reaper, reaver, shadowhawk, strider, urbanmech_iic, ursus, vulture2, vulturec, ...) plus 4 Elite\_\* pilot variants |
|
|
| **Variants** | **797 saved mechlab variants** (`resource/Variants/*.mw4`); we ship 1 | -- |
|
|
| **Maps** | none | **19 maps** (conroe01-03, desert, doneg01, gage, minerl01, mountn01/03, ngoth, ruin03, scrub01, scrub06, volcan01/03, ddc_msl, freezer, ...) |
|
|
| **Missions** | none | **17 missions** + 5 UserMissions (aspen, canyon, conroe01-03, gagetown, gladiatorpit, lakeside, mechworks, minehq, newgothem, rubble, sanddunes, spaceport, vbase, editortemplate) |
|
|
| **Skies** | *missing entirely* -- no `skies.mw4` | `skies.mw4` (40 MB, 2975 entries) |
|
|
| **Shell scripts** | reworked MechLab + 11 loose script/header files we don't have at all | our own console/lobby work (RookieMission, `-tmfds`, etc.) |
|
|
| **Engine** | binary only -- cannot be recovered | full source |
|
|
|
|
The single most important structural finding:
|
|
|
|
> **Mech IDs 0-64 are byte-for-byte identical in both forks.** V4H *appended* its six new
|
|
> mechs as IDs 65-70. There is no re-indexing to do -- a merge is additive on both sides.
|
|
|
|
---
|
|
|
|
## 1. What `FS_Build_V4H` is
|
|
|
|
A deployed game folder, not a source tree. Notable contents:
|
|
|
|
```
|
|
FS_Build_V4H/
|
|
MW4.exe 3,633,152 B 2021-09-08 (their fork; no source available)
|
|
ScriptStrings.dll 163,840 B 2021-02-03 (has the 6 new mech display names)
|
|
MissionLang.dll 188,416 B 2021-09-01
|
|
content/ 14 loose files (shell scripts + 1 ABL header)
|
|
hsh/ 279 files -- the "block HUD" set introduced in v4f
|
|
hshoriginal/ 447 files -- the pre-v4f HUD set, archived
|
|
resource/
|
|
core.mw4 12,709 entries
|
|
props.mw4 10,779 entries
|
|
textures.mw4 10,419 entries
|
|
maps/ 27 maps Missions/ 29 missions
|
|
Variants/ 797 saved mechlab loadouts
|
|
Pilots/Tesla/options.mw4
|
|
(no skies.mw4)
|
|
stats/zWorldStats/ stock 2001-era MS stats uploader, logs end 2001 -- dead weight
|
|
uploadbuildv4h/ 777 MB self-extracting installer of this same build
|
|
firestorm build V4h.txt changelog for v4 -> v4h (also in uploadbuildv4h/)
|
|
```
|
|
|
|
Their own changelog (`firestorm build V4h.txt`) is worth reading; it documents the
|
|
six new mechs, the 3-ton-per-weapon ammo cap, the MechLab save crash, and the fact that
|
|
corrupt variants can corrupt the whole build.
|
|
|
|
---
|
|
|
|
## 2. Method
|
|
|
|
The `.mw4` files are GameOS `Stuff::Database` packages (magic `#VBD`) -- a flat directory of
|
|
named records, each optionally LZW-compressed. See `tools/mw4db.py` for the format and a
|
|
faithful port of `gos_LZDecompress`.
|
|
|
|
Two comparison levels were used:
|
|
|
|
1. **Index-only** (`mw4index.py` + `diffindex.py`) -- parses the directory, no decompression.
|
|
~1 s for the whole 780 MB tree. Gives an exact **entry-name** diff.
|
|
2. **Decoded** (`pkgcmp.py`) -- decompresses every record and compares plaintext bytes.
|
|
Slower (~2 MB/s) but is the only trustworthy content test.
|
|
|
|
### Two traps that cost time -- do not repeat them
|
|
|
|
* **Case.** V4H's packer lowercased every entry name (`ablscripts\bots\0-bot.abl` vs our
|
|
`ablscripts\bots\0-Bot.abl`). A case-sensitive comparison reported 5,909 "new" and 5,155
|
|
"missing" entries in `core.mw4` that were the *same files*. Always compare through
|
|
`mw4db.norm()`.
|
|
* **Stored-byte md5 is not an equality test.** Comparing the compressed payloads
|
|
over-reports massively: `maps/peaks.mw4` showed 9 differing blobs by stored bytes but
|
|
only **1** after decompression. Use `pkgcmp.py` before believing any difference.
|
|
|
|
---
|
|
|
|
## 3. Findings
|
|
|
|
### 3.1 The six new mechs (the headline)
|
|
|
|
`core.mw4` in V4H is a **strict superset** of ours: 754 extra entries, **zero** entries we
|
|
have that they lack. All 754 belong to six new chassis and their destroyed models:
|
|
|
|
| chassis | core entries | textures | props (animation) |
|
|
|---|---|---|---|
|
|
| marauder | 63 armaturedata + 49 root + 21 armaturevideo + 5 destroyed | 37 + 1 | reuses existing |
|
|
| griffin | 57 + 54 + 19 + 5 | 38 + 1 | reuses existing |
|
|
| champion | 57 + 51 + 19 + 5 | 36 + 1 | reuses existing |
|
|
| thunderbolt | 54 + 48 + 18 + 5 | 33 + 1 | reuses existing |
|
|
| dasher | 45 + 48 + 15 + 5 | 29 + 1 | reuses existing |
|
|
| jenner2c | 45 + 46 + 15 + 5 | 30 + 1 | **155 new entries** under `mechs/jenner2c/animation` |
|
|
|
|
Each chassis brings the full set: `.contents` (+ per-joint `{armature}` / `{sites}` pages),
|
|
`.damage`, `.data` (+ `{element}` `{footsteps}` `{gamemodel}` `{hierarchicalobb}` `{solidobb}`),
|
|
`.engine`, `.instance`, `.subsystems`, `.torso`.
|
|
|
|
`textures.mw4` adds 299 entries: the six mechs' skins, six sets of
|
|
`textures/@a{chp,grf,jec,mar,thu,...}0-5.tga` cockpit/pilot art, and
|
|
`textures/footsteps/<mech>_{default,dirt,snow}.tga`.
|
|
|
|
**Nothing else is new in `core.mw4`.** No new weapons, no new subsystem types, no new
|
|
tables. Whatever balance changes V4H made live in the per-mech `.subsystems` records and
|
|
in their `MW4.exe`.
|
|
|
|
### 3.2 Mech ID space -- identical for 0-64
|
|
|
|
`content/ABLscripts/mwconst.abi` differs from ours by exactly this:
|
|
|
|
```diff
|
|
-LastMechID = 64 ;
|
|
-NoMechID = 65 ;
|
|
+M_Champion = 65 ;
|
|
+M_Jenner2c = 66 ;
|
|
+M_Dasher = 67 ;
|
|
+M_Marauder = 68 ;
|
|
+M_Thunderbolt = 69 ;
|
|
+M_Griffin = 70 ;
|
|
+LastMechID = 70 ;
|
|
+NoMechID = 71 ;
|
|
+LastMP1MechID = M_Solitaire;
|
|
```
|
|
|
|
Corroborated three ways:
|
|
|
|
* `content/shellscripts/mechbay/mechnames.script` and `buildmechnames.script` list
|
|
`mech_name[0..64]` exactly matching our `mechnames[]` in
|
|
`CoreTech/Libraries/GameOS/render.cpp`, then append 65-70.
|
|
* `core.mw4` defines 65 `*.subsystems` on our side, 71 on theirs; the extra six are
|
|
`champion, dasher, griffin, jenner_2c, marauder, thunderbolt`.
|
|
* `ScriptStrings.dll` (UTF-16) gains display strings `Champion`, `Jenner2c`, `Dasher`,
|
|
`Marauder`, `Thunderbolt`, `Griffin`.
|
|
|
|
Note the naming inconsistency in their own build: the chassis directory is `jenner2c`
|
|
but the subsystems record is `jenner_2c`, while three of their variants still reference
|
|
`jenner_iic1` (see section 3.3).
|
|
|
|
### 3.3 797 mech variants
|
|
|
|
`resource/Variants/*.mw4` -- saved MechLab loadouts. We ship exactly one
|
|
(`MW4/Resource/Variants/Argus Variant1.mw4`).
|
|
|
|
Each variant package is 4 records:
|
|
|
|
```
|
|
[0] 'Resource\Variants\<name>' 4 B raw
|
|
[1] '{Mech}' ~350 B
|
|
[2] '<chassis>{Subsystem}' ~4 KB <- names the chassis it needs
|
|
[3] '<8-char token>' 16 B
|
|
```
|
|
|
|
Because the chassis is named in record 2, dependency analysis needs no decompression
|
|
(`tools/variants-report.py`). Result:
|
|
|
|
```
|
|
loadable in our build TODAY : 727
|
|
blocked on the 6 new chassis: 67 (thunderbolt 22, griffin 15, marauder 15,
|
|
dasher 10, champion 5)
|
|
broken / orphaned : 3 (chassis 'jenner_iic1' does not exist even in V4H --
|
|
Jenner2c 1 / Jenner2c 4 / Jenner2c Pharaoh)
|
|
```
|
|
|
|
Top chassis by variant count: warhammer 36, rifleman 30, nova 24, madcat 23, atlas 23,
|
|
thunderbolt 22, hunchback 21, battlemaster 21, archer 21, thor 20.
|
|
|
|
Full table: `reports/variants-inventory.txt`.
|
|
|
|
### 3.4 Maps and missions -- we are ahead, they changed a little
|
|
|
|
* **Maps**: V4H has 27, we have 46. **Zero** V4H-only maps. Common maps are effectively
|
|
identical -- file sizes differ by 0-2 bytes and `peaks.mw4` decodes to exactly **one**
|
|
differing record (`peaks.data{gamemodel}`, an internal reference blob).
|
|
* **Missions**: V4H has 29, we have 46 + 5 UserMissions. **Zero** V4H-only missions.
|
|
Every common mission decodes to 2-4 differing records, always the same shape:
|
|
* `<name>.lights` and `<name>night.lights` -- **3 bytes** differ out of 33 KB. Noise.
|
|
* `<name>.contents` -- **real**. Coliseum: 11,588 of 52,954 bytes differ, and the
|
|
differing region contains new ASCII entity names. Their mission layouts have been
|
|
edited. Worth a proper look before merging either direction.
|
|
* `Missions/freezer.mw4` additionally has **30 V4H-only records**: stadium crowd VO
|
|
(`audio/vo/generic/mp/{cheer1-9,boo1-3,jeers,roar,ahh,aww,stadamb}.wav`) and a
|
|
rewritten `scripts/freezer_teamattrition.abl` (10,710 B vs our 832 B).
|
|
* All 71 loose `.nfo` / `.tga` / `.txt` files in `resource/` (mission metadata, game-type
|
|
definitions, thumbnails) are **byte-identical**. No game-type divergence.
|
|
* **`skies.mw4` is absent from V4H entirely.** 40 MB / 2,975 entries that only we have.
|
|
|
|
### 3.5 Shell scripts -- V4H has a diverged MechLab and 11 files we don't have at all
|
|
|
|
`props.mw4` decodes to 419 differing records, only 3 entries missing on their side
|
|
(`lobbydecals/decal_46/47/49.tga`) and 155 extra (jenner2c animation).
|
|
The interesting differences (V4H size vs ours):
|
|
|
|
| script | V4H | ours | delta |
|
|
|---|---|---|---|
|
|
| `mechbay/mechbay_main.script` | 94,517 | 61,432 | **+33 KB** |
|
|
| `mechbay/weapons.script` | 102,025 | 83,653 | **+18 KB** |
|
|
| `mechbay/armor.script` | 64,539 | 61,807 | +2.7 KB |
|
|
| `mechbay/infobox.script` | 43,875 | 41,595 | +2.3 KB |
|
|
| `listboxes.script` | 30,785 | 29,269 | +1.5 KB |
|
|
| `scriptstrings.h` | 71,608 | 70,811 | +797 B |
|
|
| `netlobby.script` | 163,740 | 163,088 | +652 B |
|
|
| `multiplayer/buildrestriction.script` | 16,443 | 15,713 | +730 B |
|
|
| `mechbay/chassis.script` | 82,596 | 81,881 | +715 B |
|
|
| `conlobby.script` | 141,044 | 140,532 | +512 B |
|
|
| ...plus ~15 more with small deltas | | | |
|
|
|
|
Also: 24 `mechbay/graphics/installed/weapon_-1_*.tga` slot images are **much smaller** in
|
|
V4H (1,036 B vs 6,540 B etc.) -- this is the "weapon box sizes set to minimum" change from
|
|
their v4 changelog.
|
|
|
|
**Eleven loose files in `content/shellscripts/` exist in V4H and nowhere in our repo:**
|
|
|
|
```
|
|
clock.script wall-clock overlay
|
|
cm_listboxes.script console mech list box
|
|
firestorm.h 20 console callsigns
|
|
firestorm64.h 64 Dragon-MP callsigns
|
|
globals.script shared script globals
|
|
options.h USE_ALLOWED_MECHS, Battle_Ammo, USE_OBSERVER, ...
|
|
mechbay/buildmechnames.script 71-entry mech name table
|
|
mechbay/mechnames.script 71-entry mech name table
|
|
mechbay/mechdescriptions.script 74 KB of mech description text
|
|
mechbay/old mechdescriptions.script
|
|
graphics/multiplayer/lobbydecals/decal_.tga
|
|
```
|
|
|
|
These are loaded loose from disk (they are in neither `core.mw4` nor `props.mw4` on either
|
|
side). Their `computerplayer.script` `#include`s `firestorm.h` where ours `#include`s
|
|
`callsign.h` -- so this is a genuinely diverged shell-script codebase, not just extra data.
|
|
|
|
### 3.6 HUD art (`hsh/`)
|
|
|
|
V4H maintains **two** HUD sets:
|
|
|
|
* `hshoriginal/` (447 files) -- the detailed paper-doll set, close to ours.
|
|
* `hsh/` (279 files) -- the "block HUD" compromise introduced in v4f so the new mechs
|
|
would have *something* to display. Their changelog admits HUDs do not always match the
|
|
mech actually in use.
|
|
|
|
Against our `hsh/` (456 files):
|
|
|
|
| | identical | differ | V4H-only | ours-only |
|
|
|---|---|---|---|---|
|
|
| `hsh/` | 182 | 68 | 29 | 206 |
|
|
| `hshoriginal/` | 275 | 148 | 24 | 33 |
|
|
|
|
V4H-only files are the six new mechs' `hud/`, `MFD/`, `radar/hud/` and `Mechs/` art,
|
|
`decals/DDC.bmp`, plus `Mechs/battlemasteriic.bmp` and `Mechs/mad cat mk.ii.bmp`
|
|
(pre-rename spellings of files we renamed).
|
|
|
|
Colour depth differs systematically: their `Mechs/*.bmp` are 24-bpp 500x400 (600,054 B),
|
|
ours are 8-bpp palettised (201,080 B). Same for `MFD/rifleman.bmp` (43,256 vs 15,476).
|
|
|
|
`hsh/hud/Convert.txt` and `hsh/radar/hud/Convert.txt` are **the authoring recipe for the
|
|
paper-doll art** and explicitly say the coordinates go into `coord.cpp` under
|
|
GameOS/External dependencies. Independent corroboration of `MFD-RADAR-MAPPINGS.md`.
|
|
They are reproduced verbatim in `reference/Convert-hud.txt` / `reference/Convert-radar.txt`.
|
|
|
|
### 3.7 Per-mech `.subsystems` differ for every single chassis
|
|
|
|
Every one of the 65 shared chassis has a differing `.subsystems` record -- same length,
|
|
different content (Atlas: 1,830 of 6,082 bytes). This is the compiled form, not the source
|
|
text, so it is not directly readable. It is consistent with V4H's documented changes:
|
|
3-ton ammo cap per weapon, revised default weapon groups, restricted hardpoints on the new
|
|
mechs.
|
|
|
|
**This is the one area where merging is genuinely risky** -- see the open questions.
|
|
|
|
Also differing in `core.mw4` for essentially every mech: `.data{gamemodel}`,
|
|
`.torso{gamemodel}`, and the `contents[joint_torsoabove]{armature}` /
|
|
`contents[joint_*belowankle]{armature}` records. Byte inspection of
|
|
`atlas.data{gamemodel}` shows one side stores an inline path string where the other
|
|
stores a numeric reference -- i.e. a packer/format difference, most likely benign.
|
|
Confirm before treating any of these as content changes.
|
|
|
|
### 3.8 Root config and binaries
|
|
|
|
Identical: 37 root files, including all three `ctcl-*.ini`.
|
|
|
|
Differing binaries (theirs older/forked, ours newer): `MW4.exe`, `autoconfig.exe`,
|
|
`Launcher.exe`, `mw4print.exe`, `ctcls.dll`, `Language.dll`, `MissionLang.dll`,
|
|
`ScriptStrings.dll`.
|
|
|
|
V4H-only root files: `Console launcher.bat` (`mw4.exe -window`), `optionssmt.ini`,
|
|
`optionssmtv4.ini`, `options.ini.rp411old`, `pre-uninstall.bat`, `RMTSHARE.EXE`,
|
|
`SetNetworkAccess.reg`, `firestorm build V4h.txt`.
|
|
|
|
`options.ini` differences worth flagging:
|
|
|
|
| key | V4H | ours |
|
|
|---|---|---|
|
|
| `bitdepth` | **32** | 16 |
|
|
| `ruletype` | 17 | 2 |
|
|
| `unlimitedammo` | 0 | 1 |
|
|
| `friendlyfirepercentage` | 100 | 0 |
|
|
| `heaton` | 1 | 0 |
|
|
| `allowedbeam1` | `fffffffd` | `ffffffff` |
|
|
| `allowedmissile1` | `ff3bffff` | `ffffffff` |
|
|
| `allowedprojectile1` | `ffffdfff` | `ffffffff` |
|
|
| `playericon` / `teamicon` | decal\_49 / decal\_50 | decal\_48 / decal\_48 |
|
|
| `[RookieMission]` section | absent | present (ours) |
|
|
| `RuleBook`/`DawnWar`/`BiggieSizeIt`/`CanYouHearTheFootSteps` | absent | present (ours) |
|
|
|
|
`bitdepth=32` is interesting given all the `DWM8And16BitMitigation` work recorded in the
|
|
repo memory -- V4H apparently runs the console at 32 bpp.
|
|
|
|
`optionssmtv4.ini` additionally carries a full `[cameraship params]` block (over-shoulder
|
|
offsets, death-sequence camera, HUD name/chat toggles) that ours does not.
|
|
|
|
### 3.9 What *we* have that V4H does not
|
|
|
|
Besides the maps/missions/skies above, `Gameleap/mw4/Content/Mechs` contains 93
|
|
non-destroyed chassis directories, of which **27 are not in V4H's 71-mech roster**:
|
|
|
|
```
|
|
Blackheart Bowman Canis Crab Duangung Gargoyle Gesu Grimreaper_DNU
|
|
koto locust2c marauder2 pitbull privateer razorback reaper reaver
|
|
shadowhawk strider urbanmech_iic ursus vulture2 vulturec
|
|
Elite_Daishi_Castro Elite_Daishi_William Elite_Madcat_James Elite_Thor_Duncan
|
|
jenner2c (chassis 'jenner2c' -- V4H spells theirs 'jenner_2c')
|
|
```
|
|
|
|
Two of these matter immediately:
|
|
|
|
* **`Dasher_DNU/`** -- chassis `dasher`, 71 source files including `.subsystems`,
|
|
`.damage`, `.animscript`, geometry. We already have Dasher source; it is simply not
|
|
registered in the roster or packed into `core.mw4`.
|
|
* **`jenner2c/`** -- 80 source files, plus `jenner2c_destroyed/`. Likewise unregistered.
|
|
|
|
So of V4H's six new mechs we may already hold source for two. `marauder2` is *Marauder II*,
|
|
a different chassis from their `marauder`.
|
|
|
|
---
|
|
|
|
## 4. Tools
|
|
|
|
All under `tools/`. Python 3, no dependencies.
|
|
|
|
| file | what it does |
|
|
|---|---|
|
|
| `mw4db.py` | `#VBD` package reader + `gos_LZDecompress` port. Import this; everything else builds on it. Documents the on-disk format. |
|
|
| `mw4index.py` | `mw4index.py <resourceRoot> > manifest.tsv` -- directory-only manifest of every `*.mw4`. ~1 s for 780 MB. |
|
|
| `diffindex.py` | `diffindex.py A.tsv B.tsv [labelA] [labelB]` -- case-insensitive package/entry-name diff. |
|
|
| `pkgcmp.py` | `pkgcmp.py A.mw4 B.mw4` -- **authoritative** decoded content diff of one package. |
|
|
| `dumprec.py` | `dumprec.py pkg.mw4 "entry/name" [out]` -- extract one decoded record; `--list` lists entries. |
|
|
| `treediff.py` | `treediff.py dirA dirB [labelA] [labelB]` -- case-insensitive loose-file tree diff by md5. |
|
|
| `variants-report.py` | chassis-dependency inventory of `resource/Variants`, flags variants that cannot load. |
|
|
| `extract-all.py` | `extract-all.py <resourceRoot> <outDir>` -- unpack every package into a real directory tree. See section 5. |
|
|
| `prune-identical.py` | `prune-identical.py <extractedDir> [--apply]` -- delete everything we already have byte-for-byte. Dry-run by default. |
|
|
| `classify-survivors.py` | `classify-survivors.py <prunedDir>` -- split what is left into NEW vs DIFFERS. |
|
|
| `restructure.py` | `restructure.py <extractedDir> [--apply]` -- reshape a pruned tree to the repo's layout and casing. Refuses to run if any two packages would collide. |
|
|
| `split-source.py` | `split-source.py <extractedDir> [--apply]` -- separate repackable source from compiled records and rename the `.obb`s. |
|
|
|
|
`run-comparison.sh` regenerates every report:
|
|
|
|
```bash
|
|
cd MW4COMPARE
|
|
./run-comparison.sh # uses the default paths
|
|
./run-comparison.sh /path/to/OTHER_BUILD /path/to/our/mw4
|
|
```
|
|
|
|
Runtime is a few minutes; `textures.mw4` dominates.
|
|
|
|
There is an older, standalone version of the same decompressor at
|
|
`build-env/extract-mw4.py`. `tools/mw4db.py` supersedes it and is the one to extend.
|
|
|
|
## 5. The extracted asset tree
|
|
|
|
Every record of every V4H package, unpacked to real files at
|
|
**`/home/rich/Repositories/FS_Build_V4H_extracted`** -- deliberately *outside* the repo.
|
|
Nothing has been merged; this is derived data and is regenerable:
|
|
|
|
```bash
|
|
python3 MW4COMPARE/tools/extract-all.py \
|
|
/home/rich/Repositories/FS_Build_V4H/resource \
|
|
/home/rich/Repositories/FS_Build_V4H_extracted
|
|
```
|
|
|
|
857 packages -> **46,707 files, 1.24 GB**, about 3 minutes.
|
|
|
|
| directory | files | size | from |
|
|
|---|---:|---:|---|
|
|
| `core/` | 12,709 | 3.4 MB | `core.mw4` |
|
|
| `props/` | 10,779 | 178 MB | `props.mw4` |
|
|
| `textures/` | 10,419 | 358 MB | `textures.mw4` |
|
|
| `maps/<map>/` | 7,219 | 576 MB | 27 map packages |
|
|
| `Missions/<name>/` | 2,391 | 60 MB | 29 mission packages |
|
|
| `Variants/<name>/` | 3,188 | 4.8 MB | 797 saved loadouts |
|
|
| `Pilots/Tesla/options/` | 2 | 5 KB | |
|
|
| `_merged/` | 42,100 | *hardlinks* | flattened Content-shaped view |
|
|
|
|
Extraction is **per package**, because 842 entry paths are claimed by more than one
|
|
package and 70 of those hold genuinely different content (mostly a mission packing its own
|
|
copy of a global `props.mw4` asset -- 29 are Coliseum/Freezer crowd VO). A single flat tree
|
|
would silently lose data.
|
|
|
|
`_merged/` is a convenience view built with **hardlinks** (no extra disk) that mirrors
|
|
`Gameleap/mw4/Content`, so it diffs directly against our source tree. Conflicts resolve
|
|
`props > core > textures > maps > missions` and every one is listed in `_conflicts.tsv`.
|
|
Variants and Pilots are excluded from it -- their records have no directory part
|
|
(`{Mech}`, `<chassis>{Subsystem}`) so all 797 variants would collide.
|
|
Treat `_merged/` as read-only; hardlinks mean editing there edits the per-package copy.
|
|
|
|
Full details, including the duplicate-entry handling, are in the tree's own `_README.txt`.
|
|
|
|
### How close packed records are to real source
|
|
|
|
Verified against `mechs/jenner2c`:
|
|
|
|
* **Recovered as-is** -- `.contents`, `.damage`, `.data`, `.engine`, `.instance`,
|
|
`.subsystems`, `.torso`, `.animscript`, every `*.erf` geometry part, `*_cage.erf`,
|
|
`runninglights.erf`, `armaturedata/`, `armaturevideo/`. Decoded TGAs are valid
|
|
(`Targa image data - RGB 512 x 512 x 32`); `core/ablscripts/mwconst.abi` extracts
|
|
byte-identical to V4H's loose copy, which independently validates the decompressor.
|
|
* **Recoverable by renaming a qualified record** --
|
|
`X.data{solidobb}` -> `X_skeleton_SOLID.obb` and
|
|
`X.data{hierarchicalobb}` -> `X_skeleton.obb`. Both carry the `#BBO` magic and
|
|
byte-identical headers to our source `.obb` files.
|
|
* **Not directly present** -- `X.armature`. The packer splits it into per-joint
|
|
`X.contents[joint_*]{armature}` records.
|
|
|
|
Also worth knowing before any merge: **V4H's Jenner IIC is not a rename of ours.** Their
|
|
part prefix is `jec_` and chassis files are `jenner_2c.*`; ours are `j2c_` and
|
|
`jenner2c.*`. Zero files in common. They are separately authored assets.
|
|
|
|
### Pruning to differences only
|
|
|
|
Our own packages were extracted the same way to
|
|
**`/home/rich/Repositories/FS_Ours_extracted`** (102 packages, 51,382 files, 1.76 GB),
|
|
then everything V4H has that we already hold byte-for-byte was deleted:
|
|
|
|
```bash
|
|
python3 MW4COMPARE/tools/prune-identical.py /home/rich/Repositories/FS_Build_V4H_extracted --apply
|
|
python3 MW4COMPARE/tools/classify-survivors.py /home/rich/Repositories/FS_Build_V4H_extracted
|
|
```
|
|
|
|
**46,707 files / 1.24 GB -> 5,385 files / 66 MB.** 41,322 deleted (88.5%), 1,912 empty
|
|
directories removed. `_pruned.tsv` logs every deletion and which rule matched.
|
|
|
|
| package group | NEW | DIFFERS | |
|
|
|---|---:|---:|---|
|
|
| `Variants` | 3,188 | 0 | 797 loadouts x 4 records; we ship 1 |
|
|
| `core` | 746 | 440 | 6 new mechs + per-chassis changes |
|
|
| `props` | 155 | 419 | jenner2c animation + reworked MechLab |
|
|
| `textures` | 297 | 2 | new mechs' skins / footsteps |
|
|
| `Missions` | 1 | 114 | `.contents`, `.lights`, freezer crowd VO |
|
|
| `maps` | 0 | 21 | |
|
|
| `Pilots` | 0 | 2 | |
|
|
| **total** | **4,387** | **998** | |
|
|
|
|
#### Why our source tree alone is not a sufficient baseline
|
|
|
|
A `.mw4` is not a zip of `Content/`. Measured:
|
|
|
|
* **17,683 records (42%)** are qualified derivatives the packer *generates* and that have
|
|
no source file at all -- `foo.data{gamemodel}`, `foo.contents[joint_hip]{armature}`,
|
|
`bar.tga{hint}`.
|
|
* **~7,400 more** have a source path but the packer **rewrites the bytes**. Proven by
|
|
comparing *our own* `Content/` against *our own* `.mw4` output: 3,238 `.data`,
|
|
2,320 `.video`, 583 `.instance`, 361 `.contents`, 345 `.audio`, 235 `.damage`,
|
|
117 `.torso`, 103 `.subsystems`, 65 `.engine`, 58 `.lights`.
|
|
|
|
Matching against `Content/` alone would have "kept" roughly 24,000 files we already have.
|
|
So each candidate is tested in order: (1) same package + entry path in our extracted
|
|
packages, (2) same entry path in any of our packages, (3) unqualified name at the same path
|
|
in `Content/`. In practice 41,320 matched rule 1 and 2 matched rule 3.
|
|
|
|
> **Case warning.** V4H's packer lowercased every entry name; ours did not. The same record
|
|
> is `core/mechs/longbow/longbow.data{element}` in V4H and
|
|
> `core/Mechs/Longbow/longbow.data{Element}` in ours. Every comparison here is
|
|
> case-insensitive -- a plain `diff -r` between the two trees is meaningless.
|
|
|
|
#### Reading the 998 DIFFERS
|
|
|
|
By extension: 352 `data{gamemodel}`, 96 `torso{gamemodel}`, 93 `subsystems`,
|
|
114 `contents[joint_*]{armature}`, 55 `lights`, 54 `audio`, 38 `instance`, 37 `contents`,
|
|
29 `script`, 28 `tga`, 15 `wav`.
|
|
|
|
* **Probably packer noise** -- the `{gamemodel}` records encode cross-package references
|
|
(one side stores an inline path string where the other stores a numeric id), and the
|
|
`.lights` differences measured 3 bytes out of 33 KB. Verify before treating as content.
|
|
* **Real, needs a design decision** -- the 93 `.subsystems` (V4H's balance pass) and the
|
|
37 mission `.contents` (substantial layout edits).
|
|
* **Real, straightforward** -- the 29 `.script` files (their MechLab rework) and the
|
|
freezer crowd VO.
|
|
|
|
### Reshaped to the repo layout
|
|
|
|
Once pruned, the V4H tree has **zero cross-package collisions** -- 5,385 files map to 5,385
|
|
distinct destinations -- so it was flattened to the repo's shape:
|
|
|
|
```bash
|
|
python3 MW4COMPARE/tools/restructure.py /home/rich/Repositories/FS_Build_V4H_extracted --apply
|
|
```
|
|
|
|
```
|
|
FS_Build_V4H_extracted/
|
|
Content/ same shape and casing as Gameleap/mw4/Content
|
|
Resource/Variants/<name>/ 797 saved loadouts (records have no path)
|
|
Resource/Pilots/Tesla/options/
|
|
```
|
|
|
|
Path components are re-cased against the repo wherever a counterpart exists, so
|
|
`mechs/longbow/longbow.data{element}` became
|
|
`Content/Mechs/Longbow/longbow.data{Element}`. Components with no counterpart keep the
|
|
packer's spelling -- which is why the six new chassis sit at `Content/Mechs/champion`,
|
|
`dasher`, `griffin`, `jenner2c`, `marauder`, `thunderbolt` in lower case.
|
|
`_layout.tsv` records the originating package and entry name for every file, so the
|
|
per-package view can be reconstructed at any time.
|
|
|
|
**`FS_Ours_extracted` is deliberately left in per-package form.** Unpruned it still has 60
|
|
genuine collisions (mission-local copies of shared `Culturals/` props, e.g.
|
|
`arena_debris_large01.data` differing across Coliseum / Reduex / StormCanyon) plus 2,617
|
|
harmless duplicate paths. Flattening it would lose data, and it is regenerable in ~4 minutes
|
|
anyway. `restructure.py` refuses to run on a tree with collisions for exactly this reason.
|
|
|
|
### Source vs compiled -- only 502 of the 2,195 files are repackable
|
|
|
|
**A `.mw4` does not store the source tree.** Some records are the source file byte for byte;
|
|
others are what the packer produced from it. Which is which was established empirically by
|
|
hash-matching every record of *our own* packages against `Gameleap/mw4/Content`
|
|
(22,138 matched a source file, 28,728 did not):
|
|
|
|
| | |
|
|
|---|---|
|
|
| **repackable source** | `.tga` `.erf` `.mw4anim` `.bid` `.wav` `.bsp` `.material` `.abl` `.obb` `.ebf` `.bounds` `.animscript` `.fgd` `.tcf` `.mlr` `.d3f` `.gaf` `.script` `.h` `.abi` |
|
|
| **compiled** | `.data` `.instance` `.subsystems` `.damage` `.torso` `.engine` `.audio` `.video` `.lights` `.mw4`, and every `{hint}` `{handle}` `{gamemodel}` `{element}` `{footsteps}` `{zones}` `{nametable}` `[shadow]` `[joint_*]{armature}` `[joint_*]{sites}` record |
|
|
|
|
The decisive evidence: our source `Content/Mechs/Atlas/atlas.data` is **4,891 bytes of text**;
|
|
the record packed under that name is a **12-byte binary stub**. The real payload went into
|
|
the `{gamemodel}` / `{element}` records. So a mech's `.data`, `.damage`, `.subsystems`,
|
|
`.instance`, `.contents`, `.torso` and `.engine` **cannot be recovered from any package** --
|
|
they must be re-authored. `.armature` does not exist in a package either; the packer splits it
|
|
into per-joint `X.contents[joint_*]{armature}` records.
|
|
|
|
```bash
|
|
python3 MW4COMPARE/tools/split-source.py /home/rich/Repositories/FS_Build_V4H_extracted --apply
|
|
```
|
|
|
|
```
|
|
FS_Build_V4H_extracted/
|
|
Content/ 502 files repackable source, repo shape + casing - drop straight in
|
|
_compiled/ 1693 files packer output, reference only
|
|
Resource/ variants + pilot options (use the original .mw4 files)
|
|
```
|
|
|
|
Renames applied -- these genuinely are `.obb` files (`#BBO` magic, headers byte-identical to
|
|
ours):
|
|
|
|
| record | becomes |
|
|
|---|---|
|
|
| `champion.data{solidobb}` | `champion_skeleton_SOLID.obb` |
|
|
| `champion.data{hierarchicalobb}` | `champion_skeleton.obb` |
|
|
|
|
(`_SOLID.obb` / `.obb` outside `Content/Mechs`.) The real source spelling is declared inside
|
|
the `.data` text we don't have, so a convention was applied and logged in
|
|
`_source-vs-compiled.tsv` -- whatever `.data` gets authored later must reference these names.
|
|
|
|
What survives in `Content/`: 208 `.erf`, 154 `.mw4anim`, 73 `.tga`, 29 `.script`, 17 `.obb`,
|
|
15 `.wav`, 3 `.h`, 1 each `.animscript` `.abl` `.abi`.
|
|
|
|
The resulting `Content/Mechs/dasher/` now matches our own `Content/Mechs/Dasher_DNU/` naming
|
|
exactly (`das_hip.erf`, `dasher_cage.erf`, `dasher_skeleton.obb`, `dasher_skeleton_SOLID.obb`).
|
|
|
|
> For the six new chassis we therefore have **all geometry, animation and art**, but their
|
|
> definition files must be written by hand. Note we already hold complete source for two of
|
|
> them in our own repo: `Content/Mechs/Dasher_DNU` and `Content/Mechs/jenner2c`.
|
|
|
|
---
|
|
|
|
## 6. Reports
|
|
|
|
Written to `reports/` by `run-comparison.sh`. The two `manifest-*.tsv` files are ~5 MB
|
|
intermediates and are gitignored.
|
|
|
|
| file | contents |
|
|
|---|---|
|
|
| `packages-entrydiff.txt` | per-package entry-name diff, all 857 V4H packages vs our 102 |
|
|
| `core-decoded-diff.txt` | 433 real differences + 754 V4H-only entries in `core.mw4` |
|
|
| `props-decoded-diff.txt` | 419 real differences + 155 V4H-only in `props.mw4` |
|
|
| `textures-decoded-diff.txt` | 1 real difference + 299 V4H-only in `textures.mw4` |
|
|
| `maps-decoded-diff.txt` | per-map decoded diff (all <=1 real difference) |
|
|
| `missions-decoded-diff.txt` | per-mission decoded diff (`.contents` / `.lights` / freezer) |
|
|
| `hsh-diff.txt`, `hshoriginal-diff.txt` | HUD art trees |
|
|
| `content-diff.txt` | the 11 loose scripts + `mwconst.abi` |
|
|
| `resource-files-diff.txt` | package-file and loose-file level diff of `resource/` |
|
|
| `variants-inventory.txt` | 797 variants grouped by chassis with load status |
|
|
|
|
`reference/` holds verbatim copies of small, high-value V4H text files that are not in our
|
|
tree, so they survive independently of the `FS_Build_V4H` folder:
|
|
|
|
| file | source |
|
|
|---|---|
|
|
| `v4h-changelog.txt` | `firestorm build V4h.txt` -- their v4 -> v4h release notes |
|
|
| `Convert-hud.txt`, `Convert-radar.txt` | `hsh/hud/Convert.txt`, `hsh/radar/hud/Convert.txt` -- paper-doll authoring recipe, points at `coord.cpp` |
|
|
| `mechnames.script`, `buildmechnames.script` | the 71-entry mech name tables |
|
|
| `v4h-firestorm.h`, `v4h-firestorm64.h` | 20 console / 64 Dragon-MP callsigns |
|
|
| `v4h-options.h` | `USE_ALLOWED_MECHS`, `Battle_Ammo`, `USE_OBSERVER`, ... |
|
|
| `mwconst.abi.diff` | the `M_Champion..M_Griffin` / `LastMechID=70` delta |
|
|
| `options.ini.diff` | ours -> theirs |
|
|
|
|
Not copied (too large, read them from `FS_Build_V4H/content/shellscripts/mechbay/`):
|
|
`mechdescriptions.script` (74,883 B) and `old mechdescriptions.script` (74,438 B).
|
|
|
|
---
|
|
|
|
## 6b. Decompiling the compiled records
|
|
|
|
**Full write-up: [DECOMPILING.md](DECOMPILING.md).** Read that before touching any of this.
|
|
|
|
The compiled records are not a dead end. The engine source contains both the writer
|
|
(`MWTool::BuildResource` -> `MWObject::CreateSubsystemStream` -> `Tool::ConstructCreateMessage`
|
|
-> a per-class factory in one of 51 `mw4/Code/MW4/*_Tool.cpp`) and the reader, and the message
|
|
structs are plain PODs declared in our own headers. We also have a closed-loop test harness:
|
|
65 chassis where we hold both the source text and the compiled record.
|
|
|
|
| record type | status |
|
|
|---|---|
|
|
| `.armature` | **done and verified** -- 2,938 of 2,976 pages exact across 65 chassis, 1,849/1,849 child lists exact. All 38 residuals are data the packer itself discarded, so the game never sees them either. |
|
|
| `.subsystems` | **done and verified** -- 7,579 of 7,585 keys exact across 63 chassis. The 6 residuals are Behemoth/Behemoth II `GroupIndex`, changed in source by `e45a67a8` after the package was built. |
|
|
| `.data` `.damage` `.contents` `.instance` `.torso` `.engine` | same mechanism, not started |
|
|
|
|
```bash
|
|
python3 MW4COMPARE/tools/decompile/armature.py --verify # 65-chassis harness
|
|
python3 MW4COMPARE/tools/decompile/armature.py <recordDir> -o out.armature
|
|
```
|
|
|
|
`.armature` files have been generated for all six new chassis into
|
|
`FS_Build_V4H_extracted/Content/Mechs/*/`.
|
|
|
|
Two things found along the way that matter beyond this section:
|
|
|
|
* The extracted baseline reflects the **packages**, not `Content/` at HEAD. `core.mw4` and
|
|
`textures.mw4` have never been repacked since the initial mirror (`2b8ca921`); `props.mw4` is
|
|
current as of `8bfaf9b9`. Check this before concluding anything is missing. See
|
|
DECOMPILING.md section 8.
|
|
* `ResourceID` packs the package record id in its **high word**, which is how `Model=`
|
|
references resolve back to a path.
|
|
|
|
---
|
|
## 7. Merge assessment
|
|
|
|
Ranked by value/risk. **None of this has been done.**
|
|
|
|
### Low risk, high value -- take from V4H
|
|
|
|
1. **727 mech variants** that reference chassis we already have. Drop-in: they are
|
|
self-contained `.mw4` files in `Resource/Variants/`, keyed by chassis *name*, and our
|
|
chassis names match. Sanity-check a sample in the MechLab first -- their changelog warns
|
|
that a corrupt variant can corrupt a build.
|
|
2. **The 11 loose shell scripts** -- at minimum `mechdescriptions.script` (74 KB of mech
|
|
text we simply do not have) and the two mech-name tables. `firestorm.h`/`firestorm64.h`
|
|
callsign lists are a straight content lift.
|
|
3. **Freezer stadium crowd VO** (17 wavs) and the expanded `freezer_teamattrition.abl`.
|
|
4. **HUD/MFD/radar art for the six new mechs**, and the 24-bpp `Mechs/*.bmp` masters if we
|
|
want higher-quality portraits than our 8-bpp copies.
|
|
|
|
### Medium risk -- the six new mechs
|
|
|
|
The assets are all extractable from `core.mw4`/`textures.mw4`/`props.mw4`. The blockers are
|
|
on *our* side and are all code, not data:
|
|
|
|
* `render.cpp` `mechnames[]` -- append 6 entries (currently 65, `"dasher"` is commented out).
|
|
* `coord.cpp` -- MFD/radar paper-doll coordinate tables must gain 6 entries.
|
|
* `mwconst.abi` -- apply the `M_Champion..M_Griffin` / `LastMechID=70` delta.
|
|
* `ScriptStrings.dll` (`StringResource.rc`) -- add `DNL_CHAMPION`, `DNL_JENNER2C`,
|
|
`DNL_DASHER`, `DNL_MARAUDER`, `DNL_THUNDERBOLT`, `DNL_GRIFFIN`.
|
|
* MechLab scripts must learn about IDs 65-70 (this is what their +33 KB
|
|
`mechbay_main.script` is doing).
|
|
* `MW4Shell.cpp` / anything else with a hard-coded 65.
|
|
|
|
Because IDs 0-64 are identical, this is purely additive.
|
|
|
|
Extracting the mech *source* (rather than the packed records) is the open question -- the
|
|
records in `core.mw4` are the compiled forms, and we need `.erf`/`.obb`/`.data` sources to
|
|
rebuild `core.mw4` with our own packer. We already hold source for `dasher` and `jenner2c`.
|
|
|
|
### High risk -- do not merge blind
|
|
|
|
* **`.subsystems` for the 65 shared chassis.** All 65 differ. Taking theirs imports their
|
|
entire balance pass (3-ton ammo cap, revised weapon groups) wholesale; taking ours keeps
|
|
our balance. This is a *design* decision, not a merge decision.
|
|
* **Mission `.contents`.** Real, substantial edits on their side for every shared mission.
|
|
Needs a proper structural diff before any decision.
|
|
* **MechLab scripts.** Their MechLab is documented (by them) as crash-prone on save.
|
|
Do not import their `mechbay_*.script` wholesale.
|
|
|
|
### Not worth taking
|
|
|
|
`stats/zWorldStats` (dead 2001 telemetry), `RMTSHARE.EXE`, `uploadbuildv4h/` (777 MB
|
|
installer of the same content), `options.ini.rp411old`.
|
|
|
|
---
|
|
|
|
## 8. Open questions for the project owner
|
|
|
|
1. **Balance.** Do we adopt V4H's `.subsystems` (3-ton ammo cap, their weapon groups) for
|
|
the 65 shared mechs, keep ours, or merge selectively per mech--
|
|
2. **Mission layouts.** Their `.contents` differ for every shared mission. Is that
|
|
deliberate content work on their side (more drop zones? different props?) or drift we
|
|
should ignore--
|
|
3. **Mech source.** Is there a source tree for V4H anywhere, or is `core.mw4` the only copy
|
|
of the six new mechs? That decides whether we re-pack from source or have to write an
|
|
unpacker that reconstructs `.erf`/`.obb` from packed records.
|
|
4. **`bitdepth=32`.** V4H runs the console at 32 bpp; we run 16. Given the
|
|
`DWM8And16BitMitigation` dependency documented in the repo memory, is their build
|
|
actually rendering at 32 bpp, and should we try it--
|
|
5. **The 25 unregistered chassis on our side** (Canis, Gargoyle, razorback, reaver, ...) --
|
|
are these finished assets that were never wired up, or abandoned work? If usable, the
|
|
combined roster could be far larger than 71.
|
|
6. **Variant hygiene.** Their changelog warns that corrupt variants can corrupt the build.
|
|
Do we import all 727 loadable variants, or curate--
|
|
7. **`jenner_iic1`.** Three variants reference a chassis that exists in neither build.
|
|
Drop them, or re-point at `jenner_2c`--
|
|
|
|
---
|
|
|
|
*Generated 2026-08-08 against `FS_Build_V4H` (Oct/Nov 2021) and branch `5.1.0b-in-progress`.*
|