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
File diff suppressed because it is too large Load Diff
+732
View File
@@ -0,0 +1,732 @@
# 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`.*
+15
View File
@@ -0,0 +1,15 @@
take 1024 image
reduce image to 320
expand canvas to 340 (centered)
Using a 4x4 black line seperate the sections
***save as unexploded***
split components (no overlaps from upper left to lower right of component)
record coords from upper left to lower right, use even numbers.
(x,y,x1,y1)
Now using the Unexploded view (image in upper left corner) overlay the exploded pieces and record the upper left corner coord.
(x2,y2)
expand canvas to 512x512 (upper left corner)
Save the exploded view in Index Mode.
Modify file(s):
coord.cpp (under GameOS/External dependancies)
+15
View File
@@ -0,0 +1,15 @@
take 1024 image
reduce to 400
expand canvas to 410 (centered)
expand canvas to 512 (upper left corner)
***Save this as Unexploded View***
split components (no overlaps from upper left to lower right of component)
Outline components in white 2x2 pixel
record coords from upper left to lower right, use even numbers.
(x,y,x1,y1)
Now using the Unexploded view (image in upper left corner) overlay the exploded pieces and record the upper left corner coord.
(x2,y2)
Save the exploded view in Index Mode.
Modify file(s):
coord.cpp (under GameOS/External dependancies)
@@ -0,0 +1,77 @@
mechnames[ 0 ] = localize$(DNL_ANNIHILATOR)
mechnames[ 1 ] = localize$(DNL_ARCHER)
mechnames[ 2 ] = localize$(DNL_ARCTICWOLF)
mechnames[ 3 ] = localize$(DNL_ARES)
mechnames[ 4 ] = localize$(DNL_ARGUS)
mechnames[ 5 ] = localize$(DNL_ASSASSIN2)
mechnames[ 6 ] = localize$(DNL_ATLAS)
mechnames[ 7 ] = localize$(DNL_AVATAR)
mechnames[ 8 ] = localize$(DNL_AWESOME)
mechnames[ 9 ] = localize$(DNL_BATTLEMASTER)
mechnames[ 10 ] = localize$(DNL_BATTLEMASTERIIC)
mechnames[ 11 ] = localize$(DNL_BEHEMOTH)
mechnames[ 12 ] = localize$(DNL_BEHEMOTHII)
mechnames[ 13 ] = localize$(DNL_BLACKHAWK)
mechnames[ 14 ] = localize$(DNL_BLACKKNIGHT)
mechnames[ 15 ] = localize$(DNL_BLACKLANNER)
mechnames[ 16 ] = localize$(DNL_BRIGAND)
mechnames[ 17 ] = localize$(DNL_BUSHWACKER)
mechnames[ 18 ] = localize$(DNL_CATAPULT)
mechnames[ 19 ] = localize$(DNL_CAULDRONBORN)
mechnames[ 20 ] = localize$(DNL_CHIMERA)
mechnames[ 21 ] = localize$(DNL_COMMANDO)
mechnames[ 22 ] = localize$(DNL_COUGAR)
mechnames[ 23 ] = localize$(DNL_CYCLOPS)
mechnames[ 24 ] = localize$(DNL_DAISHI)
mechnames[ 25 ] = localize$(DNL_DEIMOS)
mechnames[ 26 ] = localize$(DNL_DRAGON)
mechnames[ 27 ] = localize$(DNL_FAFNIR)
mechnames[ 28 ] = localize$(DNL_FLEA)
mechnames[ 29 ] = localize$(DNL_GLADIATOR)
mechnames[ 30 ] = localize$(DNL_GRIZZLY)
mechnames[ 31 ] = localize$(DNL_HAUPTMANN)
mechnames[ 32 ] = localize$(DNL_HELLHOUND)
mechnames[ 33 ] = localize$(DNL_HELLSPAWN)
mechnames[ 34 ] = localize$(DNL_HIGHLANDER)
mechnames[ 35 ] = localize$(DNL_HOLLANDERII)
mechnames[ 36 ] = localize$(DNL_HUNCHBACK)
mechnames[ 37 ] = localize$(DNL_KODIAK)
mechnames[ 38 ] = localize$(DNL_LOKI)
mechnames[ 39 ] = localize$(DNL_LONGBOW)
mechnames[ 40 ] = localize$(DNL_MADCAT)
mechnames[ 41 ] = localize$(DNL_MADCAT2)
mechnames[ 42 ] = localize$(DNL_MASAKARI)
mechnames[ 43 ] = localize$(DNL_MAULER)
mechnames[ 44 ] = localize$(DNL_NOVACAT)
mechnames[ 45 ] = localize$(DNL_OSIRIS)
mechnames[ 46 ] = localize$(DNL_OWENS)
mechnames[ 47 ] = localize$(DNL_PUMA)
mechnames[ 48 ] = localize$(DNL_RAVEN)
mechnames[ 49 ] = localize$(DNL_RIFLEMAN)
mechnames[ 50 ] = localize$(DNL_RYOKEN)
mechnames[ 51 ] = localize$(DNL_SHADOWCAT)
mechnames[ 52 ] = localize$(DNL_SOLITAIRE)
mechnames[ 53 ] = localize$(DNL_SUNDER)
mechnames[ 54 ] = localize$(DNL_TEMPLAR)
mechnames[ 55 ] = localize$(DNL_THANATOS)
mechnames[ 56 ] = localize$(DNL_THOR)
mechnames[ 57 ] = localize$(DNL_ULLER)
mechnames[ 58 ] = localize$(DNL_URBANMECH)
mechnames[ 59 ] = localize$(DNL_UZIEL)
mechnames[ 60 ] = localize$(DNL_VICTOR)
mechnames[ 61 ] = localize$(DNL_VULTURE)
mechnames[ 62 ] = localize$(DNL_WARHAMMER)
mechnames[ 63 ] = localize$(DNL_WOLFHOUND)
mechnames[ 64 ] = localize$(DNL_ZEUS)
mechnames[ 65 ] = localize$(DNL_CHAMPION)
mechnames[ 66 ] = localize$(DNL_JENNER2C)
mechnames[ 67 ] = localize$(DNL_DASHER)
mechnames[ 68 ] = localize$(DNL_MARAUDER)
mechnames[ 69 ] = localize$(DNL_THUNDERBOLT)
mechnames[ 70 ] = localize$(DNL_GRIFFIN)
//mechnames[ 70 ] = localize$(IDS_ML_NA_INFO)
//mechnames[ 71 ] = localize$(IDS_ML_NA_INFO)
+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)
+24
View File
@@ -0,0 +1,24 @@
538,539c538,557
< 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 ;
>
>
> //sip
> //not needed here
> //M_CameraShip = LastMechID+1;
> //EmptyMechID = LastMechID+2;
> //sip
> //sip
> LastMP1MechID = M_Solitaire;
>
> //
>
+90
View File
@@ -0,0 +1,90 @@
6,7c6,7
< playericon=Content\Textures\stockdecals\decal_48.tga
< teamicon=Content\Textures\stockdecals\decal_48.tga
---
> playericon=Content\Textures\stockdecals\decal_49.tga
> teamicon=Content\Textures\stockdecals\decal_50.tga
29c29
< bitdepth=16
---
> bitdepth=32
56c56
< visibility=1
---
> visibility=0
60c60
< heaton=0
---
> heaton=1
68,69c68,69
< unlimitedammo=1
< friendlyfirepercentage=0
---
> unlimitedammo=0
> friendlyfirepercentage=100
79c79
< ruletype=2
---
> ruletype=17
84c84
< allowedbeam1=ffffffff
---
> allowedbeam1=fffffffd
86c86
< allowedmissile1=ffffffff
---
> allowedmissile1=ff3bffff
88c88
< allowedprojectile1=ffffffff
---
> allowedprojectile1=ffffdfff
112c112
< lrpt=072003082ab4b906fabd1c154d3d8fd77942028e
---
> lrpt=032020102ab4b906fabd1c154d3d8fd77942028e
115,118c115
< RuleBook=1
< DawnWar=1
< BiggieSizeIt=1
< CanYouHearTheFootSteps=1
---
>
144,181d140
<
< [RookieMission]
< // Default mission loaded when the console opens and when "Default" is clicked.
< // All entries are optional - omit any to keep the hardcoded built-in default.
< //
< // MissionName - exact display name as it appears in the map dropdown
< // default: ScarabStronghold - Attrition
< // GameType - 0-based index into the game-type dropdown (2 = Attrition)
< // default: 2
< // TimeLimit - time limit in minutes; -1 means use the server's default time
< // default: -1 (uses g_nTimeList_Value, currently 7 min)
< // Visibility - 0=Clear 1=Light Fog 2=Medium Fog 3=Heavy Fog
< // Weather - 0=Off 1=Rain
< // TimeOfDay - 0=Day 1=Night
< // Radar - 0=Novice 1=Off 2=Bars 3=Unlimited
< // HeatOn - 0=Off 1=On
< // FriendlyFire - 0=Off 100=Full (percentage)
< // SplashDamage - 0=Off 1=On
< // UnlimitedAmmo - 0=Off 1=On
< // WeaponJam - 0=Off 1=On
< // AdvanceMode - 0=Off 1=On
< // ArmorMode - 0=Off 1=On
< //
< // Example: uncomment and edit lines below to customize
< //MissionName=ScarabStronghold - Attrition
< //GameType=2
< //TimeLimit=-1
< //Visibility=0
< //Weather=0
< //TimeOfDay=0
< //Radar=0
< //HeatOn=0
< //FriendlyFire=0
< //SplashDamage=0
< //UnlimitedAmmo=1
< //WeaponJam=0
< //AdvanceMode=0
< //ArmorMode=0
+117
View File
@@ -0,0 +1,117 @@
Firestorm Battletech SMT Build Four(v4h).
This is a more secure release build and has been tested with about 80 variants on the extra models.
Controls defaults reset. Menu updated again. Skins amended and should be duplicate free.
The six hard to kill mechs are added and stable.The base/stock variant will not have and advanced
gyro as a starting addition. All weapons sites are based on mech design and are fixed as displayed.
The ammo limit continues to be set at three tons per weapon maximum. This ensures balance and resolves conflicts.
Restrictions on the new mechs are fixed and if the model can't accomodate a function it will no be
added.
Updated maps within Solaris will contain the occasional drop out of voice sounds.Live with it!. Some of the mech generic sounds overide additional voices in the pods. Live with it.
Any music will only play in the Server pod. All additinal audio will continue to work as normal.
Tested in the stock build with up to Six Pilots with no issues.
Firestorm Battletech SMT Build Four(v4f).
Important.
All spinning mechs displayed on the Secondary screen pre mission are accurate. The main details below the mech are correct except class and type. If the spinning model is not the same model as chosen from the console the build has been corrupted. This can happen when variants are saved and during game crashes.
When adding new variants you must test each new variant as a corrupted variant can corrupt the build.
Do create new variants on the new build but you must check they do not corrupt the build when added.
You are advised to create new variants for the new mechs on the newest build only. Test as you go.
Save the new variants seperately once tested so you can add again if you get a corrupted build.
one additional five level mech added. This a std chassis and does not include Jumpjets options for this model.No animation for Jump jets applicable for this model.
Mech lab is still as issue for crashes when saving use scroll lock to break out.Watch for corrupt variants
The hsh folder has been archived as hshold.
The new hsh folder is a compromise for the new mechs to standardise the block hud display.
Huds are not amended use the bar chart huds for models which are accurate.
All printouts are accurate for mechs including new versions.
Amended Hud and new Mech tested on Eighty plus test runs.Stock and Variants with no problems.
Firestorm Battletech SMT Build Four(v4e).
one additional five level mech added.
Mech lab is still as issue for crashes when saving use scroll lock to break out.
This is still the best way to exit a crash and will help protect against gpu/cpu failures.
Additional stock mechs are limited to 1990 basic technical stats as standard.
Huds are not amended use the bar chart huds for models which are accurate.
All new mechs use low level block display.
All printouts are accurate for new mechs.
Main view display will show all new mechs as Unclassified.
So you can ignore technical info on the Main view screen pre game launch.
Game lag limited for additional models.
Actual lag in each pod will be dependent on the pea sized cpu/gpu running on your pod.
New mechs should have better starting groups for weapons pre set.
Any new mech will not have rear facing weapons.
Firestorm Battletech SMT Build Four(v4d).
one additional high level mech added.
Subsystems error amended to limit crashes in game with high level LOD.
Mech lab is still an issue for crashes when saving use scroll lock to break out.
Huds are not amended use the bar chart huds for models.
All printouts are accurate.
Main view display correct model but will show parent mech details.So ignore technical info on the Main view screen pre game launch.
Firestorm Battletech SMT Build Four(v4c)
one additional mech added . Huds and displays will still not match correct mech visuals. This is an index link issue . The additional mech is the only point of this update to test the high level lod on the erf in the pod to see how much lag there is in the pods.
It is best to simply amend your huds to the bar chart hud in this build as it always shows the correct damage.
Still be aware of the mech lab crash always use scroll lock to exit when this crashes/freezes this will help minimise any problems with your gpu caused by this type of crash.
Firestorm Battletech SMT Build Four(v4)
MechLab.
Weapon box sizes set to minimum size to show more of the background mech being edited. Special Weapon boxes locations have been clearly indicated on the chassis weapon tab and not simply mixed with other weapon boxes. If they don't have a special displayed in the mech lab the
model does not have one. Weapon ammo is limited to no more than three tons for every individual weapon. Example Ac10 cannot mount more than three tons of ammo . All mechs can be created from the new mech button and the subsequent drop down box. These will all show in the main mech lab
after they have been created and can be edited from here. The Battlemaster and Behmoth will only display in the mechlab list once created but both versions will show in that drop down. So behemoth,behemothII and all there variants can be edited from the parent mech.
All mechs are playable in both Dragon Multiplayer and Console Multiplayer. There are also no conflicts with printed reports on mission with the correct mech represented with each report. This has been tested heavily. The hooks for additional mechs are in place.
There is a specific crash within the build while working within the mech lab . This crash occurs most often when you try to save a mech when in the group tab. When editing a mech, less crashes occur when saving from the chassis tab and not the weapon tab. Should there be a crash the scroll lockkey will exit the crash. Best way to exit a crash in this GOS build.
Callsigns.
Callsigns within the Dragon multiplayer can be amended within the firestorm.h file. The length of the Player name needs to be shorter than twelve characters to not interfere with scoring. Callsigns within the console cannot be edited from this file. they can only be edited in the code
or when built from source. There are a number of changes in the callsigns for console multiplayer. e g Blackthorn, Sipstrassi and names with all Capital letters removed.
Hud displays in Dragon multiplayer will not reflect the mech actually being used on all occasions. But reflect the chassis used for this.Its a compromise based on the HSH limits.
+52
View File
@@ -0,0 +1,52 @@
#define NUM_CALLSIGNS 20
//These need to cover the full range of levels 0 to 9 at least one of each
//Twelve Characters at the most please as the length is apparent on scoreboard
callsigns
{
GUI_CREATE
{
string names[NUM_CALLSIGNS]
int levels[NUM_CALLSIGNS]
names[0] = "Vlad Ward"
names[1] = "Blackthorns"
names[2] = "Phelan Kell"
names[3] = "Morgan Kell"
names[4] = "Vance Rezak"
names[5] = "T. Sandoval"
names[6] = "Jerimiah Rose"
names[7] = "Sun Liao"
names[8] = "A. Focht"
names[9] = "Sipstrassi"
names[10] = "Diana Pryde"
names[11] = "Sturm Kintaro"
names[12] = "Angela Bekker"
names[13] = "Constant Tseng"
names[14] = "Tamoe Sakade"
names[15] = "Theo Kurita"
names[16] = "Shin Yodama"
names[17] = "Maeve Wolf"
names[18] = "Kael Pershaw"
names[19] = "Salome Ward"
levels[0] = 8
levels[1] = 9
levels[2] = 8
levels[3] = 8
levels[4] = 5
levels[5] = 9
levels[6] = 5
levels[7] = 7
levels[8] = 9
levels[9] = 8
levels[10] = 9
levels[11] = 9
levels[12] = 8
levels[13] = 0
levels[14] = 1
levels[15] = 6
levels[16] = 5
levels[17] = 4
levels[18] = 3
levels[19] = 2
}
}
+144
View File
@@ -0,0 +1,144 @@
#define NUM_CALLSIGNS 64
//These need to cover the full range of levels 0 to 9 at least one of each
//Twelve Characters at the most please.
// If you update the names here, please update Code\MW4\MW4Shell.cpp as well! Just search for NUM_CALLSIGNS.
callsigns
{
GUI_CREATE
{
string names[NUM_CALLSIGNS]
int levels[NUM_CALLSIGNS]
names[0] = "Sipstrassi"
names[1] = "Scarab"
names[2] = "Pocus"
names[3] = "Hacksaw"
names[4] = "Gnaw"
names[5] = "Darklord"
names[6] = "Spice"
names[7] = "Rocker"
names[8] = "Leepus"
names[9] = "Coolhand"
names[10] = "Piotr"
names[11] = "Propwash"
names[12] = "Mecca"
names[13] = "Rusty"
names[14] = "Faith"
names[15] = "Thomcat"
names[16] = "Anubis"
names[17] = "Tryptic"
names[18] = "Isis"
names[19] = "Suredude"
names[20] = "Jerimiah Rose"
names[21] = "Lucky"
names[22] = "Gothmog"
names[23] = "Grayson"
names[24] = "Arioch"
names[25] = "Blackthorns"
names[26] = "Splotch"
names[27] = "Paladin"
names[28] = "Macleod"
names[29] = "ArcLight"
names[30] = "BooYah"
names[31] = "Von"
names[32] = "Azero"
names[33] = "Wraith"
names[34] = "Undead"
names[35] = "Mantis"
names[36] = "Wicced"
names[37] = "Tiny"
names[38] = "Crusher"
names[39] = "Darkman"
names[40] = "Oxen"
names[41] = "Morfane"
names[42] = "Darkheart"
names[43] = "Joker"
names[44] = "Mouse"
names[45] = "Greywolf"
names[46] = "Pyrotech"
names[47] = "Elengil"
names[48] = "Undomiel"
names[49] = "Marz"
names[50] = "Kuroshii"
names[51] = "Crow"
names[52] = "Con"
names[53] = "Lysosome"
names[54] = "Dax"
names[55] = "HamHouke"
names[56] = "Intrepid"
names[57] = "Toad"
names[58] = "Asterix"
names[59] = "Apophis"
names[60] = "Thor"
names[61] = "Sauron"
names[62] = "Pharaoh"
names[63] = "Dicion"
levels[0] = 1
levels[1] = 7
levels[2] = 6
levels[3] = 8
levels[4] = 6
levels[5] = 7
levels[6] = 7
levels[7] = 2
levels[8] = 5
levels[9] = 6
levels[10] = 7
levels[11] = 8
levels[12] = 5
levels[13] = 2
levels[14] = 7
levels[15] = 4
levels[16] = 3
levels[17] = 2
levels[18] = 4
levels[19] = 7
levels[20] = 2
levels[21] = 2
levels[22] = 4
levels[23] = 7
levels[24] = 4
levels[25] = 2
levels[26] = 5
levels[27] = 6
levels[28] = 7
levels[29] = 8
levels[30] = 8
levels[31] = 6
levels[32] = 2
levels[33] = 2
levels[34] = 5
levels[35] = 6
levels[36] = 3
levels[37] = 2
levels[38] = 7
levels[39] = 6
levels[40] = 7
levels[41] = 6
levels[42] = 2
levels[43] = 6
levels[44] = 5
levels[45] = 6
levels[46] = 8
levels[47] = 7
levels[48] = 5
levels[49] = 6
levels[50] = 5
levels[51] = 7
levels[52] = 3
levels[53] = 5
levels[54] = 4
levels[55] = 4
levels[56] = 4
levels[57] = 4
levels[58] = 4
levels[59] = 4
levels[60] = 4
levels[61] = 8
levels[62] = 7
levels[63] = 0
}
}
+54
View File
@@ -0,0 +1,54 @@
//THIS FILE CONTAINS MERCS CLIENT CONFIGURABLE OPTIONS
// DO NOT EDIT THIS FILE UNLESS YOU KNOW WHAT YOU ARE DOING
//************************************************************
// set SHOW_TIME to 'true' to enable the wall clock, 'false'
// to disable it.
#define SHOW_TIME false
//************************************************************
//sip // 1 is firestorm standard and there is no zeus true
//sip // 0 is standard mech list including zeus false
#define USE_ALLOWED_MECHS 1
//************************************************************
// set Battle_Ammo to 'true' to enable the ammo limit per weapon, 'false'
// to disable it.
// max three ton per weapon as per mech space specification.
#define Battle_Ammo true
//************************************************************
// set SERVER_DETAIL to 'true' to enable server details
// in the Server Lobby, 'false' to disable it.
#define SERVER_DETAIL false
//************************************************************
// set this value to 'true' to enable observer mode, or
// 'false' to disable it.
#define USE_OBSERVER true
//************************************************************
// set this value to how many seconds to wait before
// auto-launching into camera mode. Setting to 15 or 20 seconds
// is a good value.
#define OBSERVER_WAIT_TIMEOUT 15
//************************************************************
// setting this value sets the base priority you want the
// game to run at. Note, this only has an effect on WinXP and
// Win2k systems. You must have one of these selected regardless.
//#define BASE_PRIORITY "BELOW_NORMAL"
#define BASE_PRIORITY "NORMAL"
//#define BASE_PRIORITY "ABOVE_NORMAL"
//************************************************************
// This is the music files you wish to loop. It must be in
// .wav format.
#define SHELL_MUSIC_FILENAME "mechevolution.wav"
//#define SHELL_MUSIC_FILENAME "nextmove_music.wav"
+4
View File
@@ -0,0 +1,4 @@
# Regenerate everything with ../run-comparison.sh
# The two manifest-*.tsv files are ~5 MB each and are intermediate build products.
manifest-*.tsv
manifest-*.err
+13
View File
@@ -0,0 +1,13 @@
# V4H=14 files, OURS=46074 files | identical=2 differ=1 V4H_only=11 OURS_only=46071
+V4H shellscripts/clock.script 988
+V4H shellscripts/cm_listboxes.script 18427
+V4H shellscripts/firestorm.h 1207
+V4H shellscripts/firestorm64.h 3052
+V4H shellscripts/globals.script 469
+V4H shellscripts/graphics/multiplayer/lobbydecals/decal_.tga 4140
+V4H shellscripts/mechbay/buildmechnames.script 3112
+V4H shellscripts/mechbay/mechdescriptions.script 74883
+V4H shellscripts/mechbay/mechnames.script 3108
+V4H shellscripts/mechbay/old mechdescriptions.script 74438
+V4H shellscripts/options.h 2011
~DIF ABLscripts/mwconst.abi V4H=26905B OURS=26643B
File diff suppressed because it is too large Load Diff
+304
View File
@@ -0,0 +1,304 @@
# V4H=279 files, OURS=456 files | identical=182 differ=68 V4H_only=29 OURS_only=206
+V4H decals/DDC.bmp 30056
+V4H hud/Champion.bmp 263224
+V4H hud/Convert.txt 586
+V4H hud/Dasher.bmp 263224
+V4H hud/Griffin.bmp 263224
+V4H hud/jenner2c.bmp 263224
+V4H hud/Marauder.bmp 263224
+V4H hud/Thunderbolt.bmp 263224
+V4H Mechs/battlemasteriic.bmp 600054
+V4H Mechs/champion.bmp 600054
+V4H Mechs/Dasher.bmp 600054
+V4H Mechs/griffin.bmp 600054
+V4H Mechs/jenner2c.bmp 600054
+V4H Mechs/mad cat mk.ii.bmp 600054
+V4H Mechs/marauder.bmp 600054
+V4H Mechs/thunderbolt.bmp 600054
+V4H MFD/champion.bmp 43254
+V4H MFD/Dasher.bmp 43254
+V4H MFD/griffin.bmp 43254
+V4H MFD/jenner2c.bmp 43254
+V4H MFD/marauder.bmp 43254
+V4H MFD/thunderbolt.bmp 43254
+V4H radar/hud/champion.bmp 263224
+V4H radar/hud/Convert.txt 579
+V4H radar/hud/dasher.bmp 263224
+V4H radar/hud/griffin.bmp 263224
+V4H radar/hud/Jenner2c.bmp 263224
+V4H radar/hud/marauder.bmp 263224
+V4H radar/hud/thunderbolt.bmp 263224
-OURS decals/decal_46.bmp 11080
-OURS decals/decal_47.bmp 30056
-OURS decals/decal_49.bmp 30056
-OURS hud/annihilator.bmp 263224
-OURS hud/archer.bmp 263224
-OURS hud/arcticwolf.bmp 263224
-OURS hud/ares.bmp 263224
-OURS hud/argus.bmp 263224
-OURS hud/assassin2.bmp 263224
-OURS hud/assassinii.bmp 263224
-OURS hud/Atlas.bmp 263224
-OURS hud/avatar.bmp 263224
-OURS hud/awesome.bmp 263224
-OURS hud/battlemaster.bmp 263224
-OURS hud/battlemasteriic.bmp 263224
-OURS hud/behemoth.bmp 263224
-OURS hud/behemothii.bmp 263224
-OURS hud/blackhawk.bmp 263224
-OURS hud/blackknight.bmp 263224
-OURS hud/blacklanner.bmp 263224
-OURS hud/brigand.bmp 263224
-OURS hud/bushwacker.bmp 263224
-OURS hud/catapult.bmp 263224
-OURS hud/cauldronborn.bmp 263224
-OURS hud/chimera.bmp 263224
-OURS hud/commando.bmp 263224
-OURS hud/cougar.bmp 263224
-OURS hud/cyclops.bmp 263224
-OURS hud/daishi.bmp 263224
-OURS hud/deimos.bmp 263224
-OURS hud/dragon.bmp 263224
-OURS hud/fafnir.bmp 263224
-OURS hud/flea.bmp 263224
-OURS hud/gladiator.bmp 263224
-OURS hud/grizzly.bmp 263224
-OURS hud/hauptmann.bmp 263224
-OURS hud/hellhound.bmp 263224
-OURS hud/hellspawn.bmp 263224
-OURS hud/highlander.bmp 263224
-OURS hud/hollanderii.bmp 263224
-OURS hud/hunchback.bmp 263224
-OURS hud/kodiak.bmp 263224
-OURS hud/loki.bmp 263224
-OURS hud/longbow.bmp 263224
-OURS hud/madcat.bmp 263224
-OURS hud/madcat2.bmp 263224
-OURS hud/masakari.bmp 263224
-OURS hud/mauler.bmp 263224
-OURS hud/novacat.bmp 263224
-OURS hud/osiris.bmp 263224
-OURS hud/owens.bmp 263224
-OURS hud/puma.bmp 263224
-OURS hud/raven.bmp 263224
-OURS hud/ryoken.bmp 263224
-OURS hud/shadowcat.bmp 263224
-OURS hud/solitaire.bmp 263224
-OURS hud/sunder.bmp 263224
-OURS hud/templar.bmp 263224
-OURS hud/thanatos.bmp 263224
-OURS hud/thor.bmp 263224
-OURS hud/uller.bmp 263224
-OURS hud/urbanmech.bmp 263224
-OURS hud/uziel.bmp 263224
-OURS hud/victor.bmp 263224
-OURS hud/vulture.bmp 263224
-OURS hud/warhammer.bmp 263224
-OURS hud/wolfhound.bmp 263224
-OURS hud/zeus.bmp 263224
-OURS Mechs/battlemaster iic.bmp 201068
-OURS Mechs/behemoth ii.bmp 201080
-OURS Mechs/behemoth.bmp 201080
-OURS Mechs/black hawk.bmp 201080
-OURS Mechs/longbow.bmp 201080
-OURS Mechs/mad cat mkii.bmp 201080
-OURS Mechs/solitare.bmp 201080
-OURS Mechs/victor.bmp 201080
-OURS MFD/annihilator.bmp 15480
-OURS MFD/archer.bmp 43256
-OURS MFD/arcticwolf.bmp 43256
-OURS MFD/ares.bmp 15480
-OURS MFD/argus.bmp 43256
-OURS MFD/assassin2.bmp 15480
-OURS MFD/atlas.bmp 43256
-OURS MFD/avatar.bmp 15480
-OURS MFD/awesome.bmp 43256
-OURS MFD/battlemaster.bmp 15480
-OURS MFD/battlemasteriic.bmp 15480
-OURS MFD/behemoth.bmp 15480
-OURS MFD/behemothii.bmp 15480
-OURS MFD/blackhawk.bmp 15480
-OURS MFD/blackknight.bmp 43256
-OURS MFD/blacklanner.bmp 15320
-OURS MFD/brigand.bmp 43256
-OURS MFD/bushwacker.bmp 43256
-OURS MFD/catapult.bmp 43256
-OURS MFD/cauldronborn.bmp 43256
-OURS MFD/chimera.bmp 43256
-OURS MFD/commando.bmp 43256
-OURS MFD/cougar.bmp 15480
-OURS MFD/cyclops.bmp 43256
-OURS MFD/daishi.bmp 15480
-OURS MFD/deimos.bmp 15480
-OURS MFD/dragon.bmp 15480
-OURS MFD/fafnir.bmp 15480
-OURS MFD/flea.bmp 15480
-OURS MFD/gladiator.bmp 15480
-OURS MFD/grizzly.bmp 43256
-OURS MFD/hauptmann.bmp 43256
-OURS MFD/hellhound.bmp 43256
-OURS MFD/hellspawn.bmp 15480
-OURS MFD/highlander.bmp 43256
-OURS MFD/hollanderii.bmp 43256
-OURS MFD/hunchback.bmp 43256
-OURS MFD/kodiak.bmp 15480
-OURS MFD/loki.bmp 15480
-OURS MFD/longbow.bmp 15480
-OURS MFD/madcat.bmp 15476
-OURS MFD/madcat2.bmp 43256
-OURS MFD/masakari.bmp 43256
-OURS MFD/mauler.bmp 43256
-OURS MFD/novacat.bmp 43256
-OURS MFD/osiris.bmp 43256
-OURS MFD/owens.bmp 43256
-OURS MFD/puma.bmp 43256
-OURS MFD/raven.bmp 43256
-OURS MFD/ryoken.bmp 43256
-OURS MFD/shadowcat.bmp 15480
-OURS MFD/solitaire.bmp 15480
-OURS MFD/sunder.bmp 15480
-OURS MFD/templar.bmp 43256
-OURS MFD/thanatos.bmp 15480
-OURS MFD/thor.bmp 43256
-OURS MFD/uller.bmp 43256
-OURS MFD/urbanmech.bmp 43256
-OURS MFD/uziel.bmp 43256
-OURS MFD/victor.bmp 15480
-OURS MFD/vulture.bmp 43256
-OURS MFD/warhammer.bmp 15480
-OURS MFD/wolfhound.bmp 43256
-OURS MFD/zeus.bmp 43256
-OURS radar/hud/annihilator.bmp 263224
-OURS radar/hud/archer.bmp 263224
-OURS radar/hud/arcticwolf.bmp 263224
-OURS radar/hud/ares.bmp 263224
-OURS radar/hud/argus.bmp 263224
-OURS radar/hud/assassin2.bmp 263224
-OURS radar/hud/assassinii.bmp 263224
-OURS radar/hud/Atlas.bmp 263224
-OURS radar/hud/avatar.bmp 263224
-OURS radar/hud/awesome.bmp 263224
-OURS radar/hud/battlemaster.bmp 263224
-OURS radar/hud/battlemasteriic.bmp 263224
-OURS radar/hud/behemoth.bmp 263224
-OURS radar/hud/behemothii.bmp 263224
-OURS radar/hud/blackhawk.bmp 263224
-OURS radar/hud/blackknight.bmp 263224
-OURS radar/hud/blacklanner.bmp 263224
-OURS radar/hud/brigand.bmp 263224
-OURS radar/hud/bushwacker.bmp 263224
-OURS radar/hud/catapult.bmp 263224
-OURS radar/hud/cauldronborn.bmp 263224
-OURS radar/hud/chimera.bmp 263224
-OURS radar/hud/commando.bmp 263224
-OURS radar/hud/cougar.bmp 263224
-OURS radar/hud/cyclops.bmp 263224
-OURS radar/hud/daishi.bmp 263224
-OURS radar/hud/deimos.bmp 263224
-OURS radar/hud/dragon.bmp 263224
-OURS radar/hud/Fafnir.bmp 263224
-OURS radar/hud/flea.bmp 263224
-OURS radar/hud/gladiator.bmp 263224
-OURS radar/hud/grizzly.bmp 263224
-OURS radar/hud/hauptmann.bmp 263224
-OURS radar/hud/hellhound.bmp 263224
-OURS radar/hud/hellspawn.bmp 263224
-OURS radar/hud/highlander.bmp 263224
-OURS radar/hud/hollanderii.bmp 263224
-OURS radar/hud/hunchback.bmp 263224
-OURS radar/hud/kodiak.bmp 263224
-OURS radar/hud/loki.bmp 263224
-OURS radar/hud/longbow.bmp 263224
-OURS radar/hud/madcat.bmp 263224
-OURS radar/hud/madcat2.bmp 263224
-OURS radar/hud/masakari.bmp 263224
-OURS radar/hud/mauler.bmp 263224
-OURS radar/hud/novacat.bmp 263224
-OURS radar/hud/osiris.bmp 263224
-OURS radar/hud/owens.bmp 263224
-OURS radar/hud/puma.bmp 263224
-OURS radar/hud/raven.bmp 263224
-OURS radar/hud/ryoken.bmp 263224
-OURS radar/hud/shadowcat.bmp 263224
-OURS radar/hud/solitaire.bmp 263224
-OURS radar/hud/sunder.bmp 263224
-OURS radar/hud/templar.bmp 263224
-OURS radar/hud/thanatos.bmp 263224
-OURS radar/hud/thor.bmp 263224
-OURS radar/hud/Thumbs.db 8704
-OURS radar/hud/uller.bmp 263224
-OURS radar/hud/urbanmech.bmp 263224
-OURS radar/hud/uziel.bmp 263224
-OURS radar/hud/victor.bmp 263224
-OURS radar/hud/vulture.bmp 263224
-OURS radar/hud/warhammer.bmp 263224
-OURS radar/hud/wolfhound.bmp 263224
-OURS radar/hud/zeus.bmp 263224
~DIF decals/Thumbs.db V4H=8704B OURS=8704B
~DIF hud/rifleman.bmp V4H=263224B OURS=263224B
~DIF hud/Thumbs.db V4H=8192B OURS=830768B
~DIF logo.bmp V4H=481080B OURS=481080B
~DIF Mechs/annihilator.bmp V4H=600056B OURS=201080B
~DIF Mechs/archer.bmp V4H=600054B OURS=201080B
~DIF Mechs/arctic wolf.bmp V4H=600054B OURS=201080B
~DIF Mechs/ares.bmp V4H=600054B OURS=201080B
~DIF Mechs/argus.bmp V4H=600054B OURS=201072B
~DIF Mechs/assassin ii.bmp V4H=600056B OURS=201080B
~DIF Mechs/atlas.bmp V4H=600054B OURS=201080B
~DIF Mechs/avatar.bmp V4H=600054B OURS=201080B
~DIF Mechs/awesome.bmp V4H=600054B OURS=201080B
~DIF Mechs/battlemaster.bmp V4H=600054B OURS=201072B
~DIF Mechs/black knight.bmp V4H=600056B OURS=201080B
~DIF Mechs/black lanner.bmp V4H=600056B OURS=201080B
~DIF Mechs/brigand.bmp V4H=600056B OURS=201080B
~DIF Mechs/bushwacker.bmp V4H=600054B OURS=201080B
~DIF Mechs/catapult.bmp V4H=600054B OURS=201080B
~DIF Mechs/cauldronborn.bmp V4H=600054B OURS=201080B
~DIF Mechs/chimera.bmp V4H=600054B OURS=201080B
~DIF Mechs/commando.bmp V4H=600056B OURS=201080B
~DIF Mechs/cougar.bmp V4H=600054B OURS=201080B
~DIF Mechs/cyclops.bmp V4H=600054B OURS=201080B
~DIF Mechs/daishi.bmp V4H=600056B OURS=201080B
~DIF Mechs/deimos.bmp V4H=600054B OURS=201080B
~DIF Mechs/dragon.bmp V4H=600054B OURS=201080B
~DIF Mechs/fafnir.bmp V4H=600054B OURS=201080B
~DIF Mechs/flea.bmp V4H=600054B OURS=201080B
~DIF Mechs/gladiator.bmp V4H=600054B OURS=201080B
~DIF Mechs/grizzly.bmp V4H=600056B OURS=201080B
~DIF Mechs/Hauptmann.bmp V4H=600054B OURS=201080B
~DIF Mechs/hellhound.bmp V4H=600054B OURS=201080B
~DIF Mechs/hellspawn.bmp V4H=600054B OURS=201080B
~DIF Mechs/highlander.bmp V4H=600054B OURS=201080B
~DIF Mechs/hollander ii.bmp V4H=600054B OURS=201080B
~DIF Mechs/hunchback.bmp V4H=600056B OURS=201080B
~DIF Mechs/kodiak.bmp V4H=600054B OURS=201072B
~DIF Mechs/loki.bmp V4H=600054B OURS=201080B
~DIF Mechs/mad cat.bmp V4H=600054B OURS=201080B
~DIF Mechs/Masakari.bmp V4H=600054B OURS=201080B
~DIF Mechs/mauler.bmp V4H=600054B OURS=201080B
~DIF Mechs/nova cat.bmp V4H=600054B OURS=201080B
~DIF Mechs/osiris.bmp V4H=600054B OURS=201080B
~DIF Mechs/owens.bmp V4H=600054B OURS=201080B
~DIF Mechs/puma.bmp V4H=600054B OURS=201080B
~DIF Mechs/raven.bmp V4H=600054B OURS=201080B
~DIF Mechs/rifleman.bmp V4H=600054B OURS=201080B
~DIF Mechs/Ryoken.bmp V4H=600054B OURS=201080B
~DIF Mechs/shadow cat.bmp V4H=600054B OURS=201080B
~DIF Mechs/sunder.bmp V4H=600054B OURS=201080B
~DIF Mechs/templar.bmp V4H=600054B OURS=201080B
~DIF Mechs/thanatos.bmp V4H=600054B OURS=201080B
~DIF Mechs/thor.bmp V4H=600054B OURS=201080B
~DIF Mechs/Thumbs.db V4H=79872B OURS=140800B
~DIF Mechs/Uller.bmp V4H=600054B OURS=201080B
~DIF Mechs/urbanmech.bmp V4H=600054B OURS=201080B
~DIF Mechs/uziel.bmp V4H=600054B OURS=201080B
~DIF Mechs/vulture.bmp V4H=600054B OURS=201080B
~DIF Mechs/warhammer.bmp V4H=600054B OURS=201080B
~DIF Mechs/wolfhound.bmp V4H=600056B OURS=201080B
~DIF Mechs/zeus.bmp V4H=600054B OURS=201080B
~DIF MFD/rifleman.bmp V4H=43256B OURS=15476B
~DIF MFD/Thumbs.db V4H=8704B OURS=288256B
~DIF missionover.bmp V4H=481080B OURS=481080B
~DIF radar/hud/rifleman.bmp V4H=263224B OURS=263224B
~DIF radar/Thumbs.db V4H=7680B OURS=18432B
~DIF Thumbs.db V4H=30208B OURS=45056B
+206
View File
@@ -0,0 +1,206 @@
# V4HORIG=447 files, OURS=456 files | identical=275 differ=148 V4HORIG_only=24 OURS_only=33
+V4HORIG decals/DDC.bmp 30056
+V4HORIG hud/Champion.bmp 263224
+V4HORIG hud/Convert.txt 586
+V4HORIG hud/Dasher.bmp 263224
+V4HORIG hud/jenner2c.bmp 263224
+V4HORIG hud/Marauder.bmp 263224
+V4HORIG Mechs/battlemasteriic.bmp 600054
+V4HORIG Mechs/champion.bmp 600054
+V4HORIG Mechs/Dasher.bmp 600054
+V4HORIG Mechs/jenner2c.bmp 600054
+V4HORIG Mechs/mad cat mk.ii.bmp 600054
+V4HORIG Mechs/marauder.bmp 600054
+V4HORIG MFD/assassinii.bmp 43256
+V4HORIG MFD/champion.bmp 43254
+V4HORIG MFD/Dasher.bmp 43254
+V4HORIG MFD/jenner2c.bmp 43254
+V4HORIG MFD/mad cat mkii.bmp 43254
+V4HORIG MFD/mad cat.bmp 43254
+V4HORIG MFD/marauder.bmp 43254
+V4HORIG radar/hud/champion.bmp 263224
+V4HORIG radar/hud/Convert.txt 579
+V4HORIG radar/hud/dasher.bmp 263224
+V4HORIG radar/hud/Jenner2c.bmp 263224
+V4HORIG radar/hud/marauder.bmp 263224
-OURS decals/decal_46.bmp 11080
-OURS decals/decal_47.bmp 30056
-OURS decals/decal_49.bmp 30056
-OURS hud/assassin2.bmp 263224
-OURS hud/behemoth.bmp 263224
-OURS hud/behemothii.bmp 263224
-OURS Mechs/battlemaster iic.bmp 201068
-OURS Mechs/behemoth ii.bmp 201080
-OURS Mechs/behemoth.bmp 201080
-OURS Mechs/black hawk.bmp 201080
-OURS Mechs/longbow.bmp 201080
-OURS Mechs/mad cat mkii.bmp 201080
-OURS Mechs/solitare.bmp 201080
-OURS Mechs/victor.bmp 201080
-OURS MFD/assassin2.bmp 15480
-OURS MFD/behemoth.bmp 15480
-OURS MFD/behemothii.bmp 15480
-OURS MFD/fafnir.bmp 15480
-OURS MFD/flea.bmp 15480
-OURS MFD/gladiator.bmp 15480
-OURS MFD/hellspawn.bmp 15480
-OURS MFD/kodiak.bmp 15480
-OURS MFD/longbow.bmp 15480
-OURS MFD/madcat.bmp 15476
-OURS MFD/madcat2.bmp 43256
-OURS MFD/sunder.bmp 15480
-OURS MFD/Thumbs.db 288256
-OURS MFD/victor.bmp 15480
-OURS radar/hud/assassin2.bmp 263224
-OURS radar/hud/behemoth.bmp 263224
-OURS radar/hud/behemothii.bmp 263224
-OURS radar/hud/Thumbs.db 8704
-OURS radar/Thumbs.db 18432
~DIF decals/Thumbs.db V4HORIG=8704B OURS=8704B
~DIF hud/annihilator.bmp V4HORIG=263224B OURS=263224B
~DIF hud/archer.bmp V4HORIG=263224B OURS=263224B
~DIF hud/ares.bmp V4HORIG=263224B OURS=263224B
~DIF hud/argus.bmp V4HORIG=263224B OURS=263224B
~DIF hud/avatar.bmp V4HORIG=263224B OURS=263224B
~DIF hud/battlemaster.bmp V4HORIG=263224B OURS=263224B
~DIF hud/battlemasteriic.bmp V4HORIG=263224B OURS=263224B
~DIF hud/blackhawk.bmp V4HORIG=263224B OURS=263224B
~DIF hud/Fafnir.bmp V4HORIG=263224B OURS=263224B
~DIF hud/flea.bmp V4HORIG=263224B OURS=263224B
~DIF hud/gladiator.bmp V4HORIG=263224B OURS=263224B
~DIF hud/hellspawn.bmp V4HORIG=263224B OURS=263224B
~DIF hud/kodiak.bmp V4HORIG=263224B OURS=263224B
~DIF hud/longbow.bmp V4HORIG=263224B OURS=263224B
~DIF hud/rifleman.bmp V4HORIG=263224B OURS=263224B
~DIF hud/sunder.bmp V4HORIG=263224B OURS=263224B
~DIF hud/Thumbs.db V4HORIG=8192B OURS=830768B
~DIF hud/victor.bmp V4HORIG=263224B OURS=263224B
~DIF hud/warhammer.bmp V4HORIG=263224B OURS=263224B
~DIF logo.bmp V4HORIG=481080B OURS=481080B
~DIF Mechs/annihilator.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/archer.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/arctic wolf.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/ares.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/argus.bmp V4HORIG=600054B OURS=201072B
~DIF Mechs/assassin ii.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/atlas.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/avatar.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/awesome.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/battlemaster.bmp V4HORIG=600054B OURS=201072B
~DIF Mechs/black knight.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/black lanner.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/brigand.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/bushwacker.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/catapult.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/cauldronborn.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/chimera.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/commando.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/cougar.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/cyclops.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/daishi.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/deimos.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/dragon.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/fafnir.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/flea.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/gladiator.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/grizzly.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/Hauptmann.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/hellhound.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/hellspawn.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/highlander.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/hollander ii.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/hunchback.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/kodiak.bmp V4HORIG=600054B OURS=201072B
~DIF Mechs/loki.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/mad cat.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/Masakari.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/mauler.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/nova cat.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/osiris.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/owens.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/puma.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/raven.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/rifleman.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/Ryoken.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/shadow cat.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/sunder.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/templar.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/thanatos.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/thor.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/Thumbs.db V4HORIG=6144B OURS=140800B
~DIF Mechs/Uller.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/urbanmech.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/uziel.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/vulture.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/warhammer.bmp V4HORIG=600054B OURS=201080B
~DIF Mechs/wolfhound.bmp V4HORIG=600056B OURS=201080B
~DIF Mechs/zeus.bmp V4HORIG=600054B OURS=201080B
~DIF MFD/annihilator.bmp V4HORIG=43256B OURS=15480B
~DIF MFD/archer.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/arcticwolf.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/ares.bmp V4HORIG=43256B OURS=15480B
~DIF MFD/argus.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/atlas.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/avatar.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/awesome.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/battlemaster.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/battlemasteriic.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/blackhawk.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/blackknight.bmp V4HORIG=43256B OURS=43256B
~DIF MFD/blacklanner.bmp V4HORIG=43256B OURS=15320B
~DIF MFD/brigand.bmp V4HORIG=43256B OURS=43256B
~DIF MFD/bushwacker.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/catapult.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/cauldronborn.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/chimera.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/commando.bmp V4HORIG=43256B OURS=43256B
~DIF MFD/cougar.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/cyclops.bmp V4HORIG=43256B OURS=43256B
~DIF MFD/daishi.bmp V4HORIG=43256B OURS=15480B
~DIF MFD/deimos.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/dragon.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/grizzly.bmp V4HORIG=43256B OURS=43256B
~DIF MFD/hauptmann.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/hellhound.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/highlander.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/hollanderii.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/hunchback.bmp V4HORIG=43256B OURS=43256B
~DIF MFD/loki.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/masakari.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/mauler.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/novacat.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/osiris.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/owens.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/puma.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/raven.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/rifleman.bmp V4HORIG=43256B OURS=15476B
~DIF MFD/ryoken.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/shadowcat.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/solitaire.bmp V4HORIG=43256B OURS=15480B
~DIF MFD/templar.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/thanatos.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/thor.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/uller.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/urbanmech.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/uziel.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/vulture.bmp V4HORIG=43254B OURS=43256B
~DIF MFD/warhammer.bmp V4HORIG=43254B OURS=15480B
~DIF MFD/wolfhound.bmp V4HORIG=43256B OURS=43256B
~DIF MFD/zeus.bmp V4HORIG=43254B OURS=43256B
~DIF missionover.bmp V4HORIG=481080B OURS=481080B
~DIF radar/hud/annihilator.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/archer.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/ares.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/argus.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/avatar.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/battlemaster.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/battlemasteriic.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/blackhawk.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/Fafnir.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/hellspawn.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/kodiak.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/longbow.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/rifleman.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/victor.bmp V4HORIG=263224B OURS=263224B
~DIF radar/hud/warhammer.bmp V4HORIG=263224B OURS=263224B
~DIF Thumbs.db V4HORIG=8192B OURS=45056B
+75
View File
@@ -0,0 +1,75 @@
### alpine02.mw4
# entries: A=458 B=458 A_only=0 B_only=0 decoded-differ=0
### arctic04.mw4
# entries: A=341 B=341 A_only=0 B_only=0 decoded-differ=0
### arctic06.mw4
# entries: A=342 B=342 A_only=0 B_only=0 decoded-differ=0
### colsm01.mw4
# entries: A=182 B=182 A_only=0 B_only=0 decoded-differ=1
~ maps/colsm01/colsm01.data{gamemodel} A=184B B=184B
### colsm02.mw4
# entries: A=248 B=248 A_only=0 B_only=0 decoded-differ=2
~ maps/colsm02/colsm02.instance A=100B B=100B
~ textures/skysc2f1.tga A=196652B B=196652B
### darklord.mw4
# entries: A=135 B=135 A_only=0 B_only=0 decoded-differ=0
### desert07.mw4
# entries: A=399 B=399 A_only=0 B_only=0 decoded-differ=1
~ maps/desert07/desert07.data{gamemodel} A=184B B=184B
### fact01.mw4
# entries: A=201 B=201 A_only=0 B_only=0 decoded-differ=1
~ maps/fact01/fact01.data{gamemodel} A=184B B=184B
### firestorm.mw4
# entries: A=63 B=63 A_only=0 B_only=0 decoded-differ=1
~ maps/firestorm/firestorm.data{gamemodel} A=184B B=184B
### grassland.mw4
# entries: A=287 B=287 A_only=0 B_only=0 decoded-differ=0
### hotplate.mw4
# entries: A=219 B=219 A_only=0 B_only=0 decoded-differ=1
~ maps/hotplate/hotplate.data{gamemodel} A=184B B=184B
### ice3.mw4
# entries: A=219 B=219 A_only=0 B_only=0 decoded-differ=1
~ maps/ice3/ice3.data{gamemodel} A=184B B=184B
### jung02.mw4
# entries: A=174 B=174 A_only=0 B_only=0 decoded-differ=1
~ maps/jung02/jung02.data{gamemodel} A=184B B=184B
### lunar01.mw4
# entries: A=249 B=249 A_only=0 B_only=0 decoded-differ=1
~ maps/lunar01/lunar01.data{gamemodel} A=184B B=184B
### minerl03.mw4
# entries: A=358 B=358 A_only=0 B_only=0 decoded-differ=1
~ maps/minerl03/minerl03.data{gamemodel} A=184B B=184B
### nazca.mw4
# entries: A=224 B=224 A_only=0 B_only=0 decoded-differ=1
~ maps/nazca/nazca.data{gamemodel} A=184B B=184B
### palace01.mw4
# entries: A=244 B=244 A_only=0 B_only=0 decoded-differ=1
~ maps/palace01/palace01.data{gamemodel} A=184B B=184B
### peaks.mw4
# entries: A=219 B=219 A_only=0 B_only=0 decoded-differ=1
~ maps/peaks/peaks.data{gamemodel} A=184B B=184B
### reduex.mw4
# entries: A=248 B=248 A_only=0 B_only=0 decoded-differ=1
~ maps/reduex/reduex.data{gamemodel} A=184B B=184B
### rookiearena2-terrain-v1.mw4
# entries: A=63 B=63 A_only=0 B_only=0 decoded-differ=1
~ maps/rookiearena2-terrain-v1/rookiearena2-terrain-v1.data{gamemodel} A=184B B=184B
### scrub02.mw4
# entries: A=355 B=355 A_only=0 B_only=0 decoded-differ=1
~ maps/scrub02/scrub02.data{gamemodel} A=184B B=184B
### stormcanyon.mw4
# entries: A=128 B=128 A_only=0 B_only=0 decoded-differ=1
~ maps/stormcanyon/stormcanyon.data{gamemodel} A=184B B=184B
### stormcanyonsiege.mw4
# entries: A=137 B=137 A_only=0 B_only=0 decoded-differ=1
~ maps/stormcanyonsiege/stormcanyonsiege.data{gamemodel} A=184B B=184B
### swamp01.mw4
# entries: A=488 B=488 A_only=0 B_only=0 decoded-differ=2
~ maps/swamp01/crater.data{gamemodel} A=32B B=32B
~ maps/swamp01/swamp01.data{gamemodel} A=184B B=184B
### urban01.mw4
# entries: A=446 B=446 A_only=0 B_only=0 decoded-differ=0
### urban02.mw4
# entries: A=396 B=396 A_only=0 B_only=0 decoded-differ=0
### urban05.mw4
# entries: A=396 B=396 A_only=0 B_only=0 decoded-differ=0
@@ -0,0 +1,173 @@
### bigcity.mw4
# entries: A=80 B=80 A_only=0 B_only=0 decoded-differ=3
~ missions/bigcity/bigcity.contents A=585438B B=585438B
~ missions/bigcity/bigcity.lights A=131340B B=131340B
~ missions/bigcity/bigcitynight.lights A=131340B B=131340B
### cantina.mw4
# entries: A=31 B=31 A_only=0 B_only=0 decoded-differ=3
~ missions/cantina/cantina.contents A=80886B B=80886B
~ missions/cantina/cantina.lights A=131347B B=131347B
~ missions/cantina/cantinanight.lights A=131347B B=131347B
### cantinasiege.mw4
# entries: A=50 B=50 A_only=0 B_only=0 decoded-differ=3
~ missions/cantinasiege/cantinasiege.contents A=91546B B=91546B
~ missions/cantinasiege/cantinasiege.lights A=131352B B=131352B
~ missions/cantinasiege/cantinasiegenight.lights A=131352B B=131352B
### coliseum.mw4
# entries: A=47 B=47 A_only=0 B_only=0 decoded-differ=3
~ missions/coliseum/coliseum.contents A=52954B B=52954B
~ missions/coliseum/coliseum.lights A=33033B B=33033B
~ missions/coliseum/coliseumnight.lights A=33033B B=33033B
### cpark.mw4
# entries: A=31 B=31 A_only=0 B_only=0 decoded-differ=3
~ missions/centralpark/centralpark.contents A=321566B B=321566B
~ missions/centralpark/centralpark.lights A=131355B B=131355B
~ missions/centralpark/centralparknight.lights A=131355B B=131355B
### dustbowl.mw4
# entries: A=28 B=28 A_only=0 B_only=0 decoded-differ=3
~ missions/dustbowl/dustbowl.contents A=10794B B=10794B
~ missions/dustbowl/dustbowl.lights A=131433B B=131433B
~ missions/dustbowl/dustbowlnight.lights A=131433B B=131433B
### factory.mw4
# entries: A=25 B=25 A_only=0 B_only=0 decoded-differ=2
~ missions/factory/factory.contents A=136990B B=136990B
~ missions/factory/factory.lights A=33042B B=33042B
### fbite.mw4
# entries: A=32 B=32 A_only=0 B_only=0 decoded-differ=3
~ missions/frostbite/frostbite.contents A=91850B B=91850B
~ missions/frostbite/frostbite.lights A=131351B B=131351B
~ missions/frostbite/frostbitenight.lights A=131351B B=131351B
### freezer.mw4
# entries: A=165 B=135 A_only=30 B_only=0 decoded-differ=4
+A audio/vo/generic/mp/ahh.wav 54966B
+A audio/vo/generic/mp/ahh.wav{handle} 10B
+A audio/vo/generic/mp/aww.wav 11958B
+A audio/vo/generic/mp/aww.wav{handle} 10B
+A audio/vo/generic/mp/boo1.wav 97718B
+A audio/vo/generic/mp/boo1.wav{handle} 10B
+A audio/vo/generic/mp/boo2.wav 131510B
+A audio/vo/generic/mp/boo2.wav{handle} 10B
+A audio/vo/generic/mp/boo3.wav 138166B
+A audio/vo/generic/mp/boo3.wav{handle} 10B
+A audio/vo/generic/mp/cheer1.wav 200118B
+A audio/vo/generic/mp/cheer1.wav{handle} 10B
+A audio/vo/generic/mp/cheer2.wav 102838B
+A audio/vo/generic/mp/cheer2.wav{handle} 10B
+A audio/vo/generic/mp/cheer3.wav 95670B
+A audio/vo/generic/mp/cheer3.wav{handle} 10B
+A audio/vo/generic/mp/cheer4.wav 152246B
+A audio/vo/generic/mp/cheer4.wav{handle} 10B
+A audio/vo/generic/mp/cheer5.wav 92086B
+A audio/vo/generic/mp/cheer5.wav{handle} 10B
+A audio/vo/generic/mp/cheer8.wav 14076B
+A audio/vo/generic/mp/cheer8.wav{handle} 10B
+A audio/vo/generic/mp/cheer9.wav 63670B
+A audio/vo/generic/mp/cheer9.wav{handle} 10B
+A audio/vo/generic/mp/jeers.wav 127414B
+A audio/vo/generic/mp/jeers.wav{handle} 10B
+A audio/vo/generic/mp/roar.wav 9654B
+A audio/vo/generic/mp/roar.wav{handle} 10B
+A audio/vo/generic/mp/stadamb.wav 23478B
+A audio/vo/generic/mp/stdamb.wav{handle} 10B
~ missions/freezer/freezer.contents A=16338B B=16338B
~ missions/freezer/freezer.lights A=189B B=189B
~ missions/freezer/freezernight.lights A=189B B=189B
~ missions/freezer/scripts/freezer_teamattrition.abl A=10710B B=832B
### gbait.mw4
# entries: A=32 B=32 A_only=0 B_only=0 decoded-differ=3
~ missions/gatorbait/gatorbait.contents A=131102B B=131102B
~ missions/gatorbait/gatorbait.lights A=131344B B=131344B
~ missions/gatorbait/gatorbaitnight.lights A=131344B B=131344B
### ghosthighway.mw4
# entries: A=30 B=30 A_only=0 B_only=0 decoded-differ=3
~ missions/ghosthighway/ghosthighway.contents A=46262B B=46262B
~ missions/ghosthighway/ghosthighway.lights A=131356B B=131356B
~ missions/ghosthighway/ghosthighwaynight.lights A=131356B B=131356B
### grassland.mw4
# entries: A=133 B=133 A_only=0 B_only=0 decoded-differ=3
~ missions/grassland/grassland.contents A=191662B B=191662B
~ missions/grassland/grassland.lights A=189B B=189B
~ missions/grassland/grasslandnight.lights A=189B B=189B
### hideaway.mw4
# entries: A=131 B=131 A_only=0 B_only=0 decoded-differ=3
~ missions/hideaway/hideaway.contents A=26798B B=26798B
~ missions/hideaway/hideaway.lights A=33136B B=33136B
~ missions/hideaway/hideawaynight.lights A=33136B B=33136B
### hotplate.mw4
# entries: A=129 B=129 A_only=0 B_only=0 decoded-differ=3
~ missions/hotplate/hotplate.contents A=134446B B=134446B
~ missions/hotplate/hotplate.lights A=189B B=189B
~ missions/hotplate/hotplatenight.lights A=189B B=189B
### icity.mw4
# entries: A=26 B=26 A_only=0 B_only=0 decoded-differ=3
~ missions/innercity/innercity.contents A=228938B B=228938B
~ missions/innercity/innercity.lights A=131351B B=131351B
~ missions/innercity/innercitynight.lights A=131351B B=131351B
### jungle.mw4
# entries: A=36 B=36 A_only=0 B_only=0 decoded-differ=3
~ missions/jungle/jungle.contents A=109430B B=109430B
~ missions/jungle/jungle.lights A=33121B B=33121B
~ missions/jungle/junglenight.lights A=33030B B=33030B
### lunacy.mw4
# entries: A=25 B=25 A_only=0 B_only=0 decoded-differ=3
~ missions/lunacy/lunacy.contents A=33574B B=33574B
~ missions/lunacy/lunacy.lights A=74085B B=74085B
~ missions/lunacy/lunacynight.lights A=74085B B=74085B
### nazca.mw4
# entries: A=134 B=134 A_only=0 B_only=0 decoded-differ=3
~ missions/nazca/nazca.contents A=35498B B=35498B
~ missions/nazca/nazca.lights A=189B B=189B
~ missions/nazca/nazcanight.lights A=189B B=189B
### peaks.mw4
# entries: A=129 B=129 A_only=0 B_only=0 decoded-differ=3
~ missions/peaks/peaks.contents A=103198B B=103198B
~ missions/peaks/peaks.lights A=189B B=189B
~ missions/peaks/peaksnight.lights A=189B B=189B
### pgates.mw4
# entries: A=155 B=155 A_only=0 B_only=0 decoded-differ=3
~ missions/palacegates/palacegates.contents A=141158B B=141158B
~ missions/palacegates/palacegates.lights A=74095B B=74095B
~ missions/palacegates/palacegatesnight.lights A=74095B B=74095B
### reduex.mw4
# entries: A=128 B=128 A_only=0 B_only=0 decoded-differ=3
~ missions/reduex/reduex.contents A=183102B B=183102B
~ missions/reduex/reduex.lights A=74081B B=74081B
~ missions/reduex/reduexnight.lights A=98B B=98B
### reduexsiege.mw4
# entries: A=137 B=137 A_only=0 B_only=0 decoded-differ=3
~ missions/reduexsiege/reduexsiege.contents A=225374B B=225374B
~ missions/reduexsiege/reduexsiege.lights A=74081B B=74081B
~ missions/reduexsiege/reduexsiegenight.lights A=98B B=98B
### scarabstronghold.mw4
# entries: A=123 B=123 A_only=0 B_only=0 decoded-differ=3
~ missions/scarabstronghold/scarabstronghold.contents A=45846B B=45846B
~ missions/scarabstronghold/scarabstronghold.lights A=189B B=189B
~ missions/scarabstronghold/scarabstrongholdnight.lights A=98B B=98B
### snowjob.mw4
# entries: A=29 B=29 A_only=0 B_only=0 decoded-differ=3
~ missions/snowjob/snowjob.contents A=19570B B=19570B
~ missions/snowjob/snowjob.lights A=131347B B=131347B
~ missions/snowjob/snowjobnight.lights A=131347B B=131347B
### stormcanyon.mw4
# entries: A=181 B=181 A_only=0 B_only=0 decoded-differ=2
~ missions/stormcanyon/stormcanyon.contents A=96058B B=96058B
~ missions/stormcanyon/stormcanyonnight.lights A=189B B=189B
### stormcanyonsiege.mw4
# entries: A=163 B=163 A_only=0 B_only=0 decoded-differ=2
~ missions/stormcanyonsiege/stormcanyonsiege.contents A=188522B B=188522B
~ missions/stormcanyonsiege/stormcanyonsiegenight.lights A=189B B=189B
### tline.mw4
# entries: A=31 B=31 A_only=0 B_only=0 decoded-differ=3
~ missions/timberline/timberline.contents A=119674B B=119674B
~ missions/timberline/timberline.lights A=131346B B=131346B
~ missions/timberline/timberlinenight.lights A=131346B B=131346B
### tribeincursionmission.mw4
# entries: A=79 B=79 A_only=0 B_only=0 decoded-differ=3
~ missions/tribeincursionmission/tribeincursionmission.contents A=156486B B=156486B
~ missions/tribeincursionmission/tribeincursionmission.lights A=189B B=189B
~ missions/tribeincursionmission/tribeincursionmissionnight.lights A=189B B=189B
### tribeincursion.mw4
# entries: A=71 B=71 A_only=0 B_only=0 decoded-differ=3
~ missions/tribeincursion/tribeincursion.contents A=126646B B=126646B
~ missions/tribeincursion/tribeincursion.lights A=189B B=189B
~ missions/tribeincursion/tribeincursionnight.lights A=189B B=189B
File diff suppressed because it is too large Load Diff
+578
View File
@@ -0,0 +1,578 @@
# entries: A=10779 B=10627 A_only=155 B_only=3 decoded-differ=419
+A mechs/jenner2c/animation/j2c_back.mw4anim 8219B
+A mechs/jenner2c/animation/j2c_backd.mw4anim 8021B
+A mechs/jenner2c/animation/j2c_backl.mw4anim 8001B
+A mechs/jenner2c/animation/j2c_backr.mw4anim 8561B
+A mechs/jenner2c/animation/j2c_backstand.mw4anim 4491B
+A mechs/jenner2c/animation/j2c_backstandd.mw4anim 4605B
+A mechs/jenner2c/animation/j2c_backstandl.mw4anim 4389B
+A mechs/jenner2c/animation/j2c_backstandr.mw4anim 4249B
+A mechs/jenner2c/animation/j2c_backstandrev.mw4anim 4491B
+A mechs/jenner2c/animation/j2c_backstandrevd.mw4anim 4605B
+A mechs/jenner2c/animation/j2c_backstandrevl.mw4anim 4249B
+A mechs/jenner2c/animation/j2c_backstandrevr.mw4anim 4389B
+A mechs/jenner2c/animation/j2c_backstandrevu.mw4anim 4665B
+A mechs/jenner2c/animation/j2c_backstandu.mw4anim 4665B
+A mechs/jenner2c/animation/j2c_backu.mw4anim 8665B
+A mechs/jenner2c/animation/j2c_fallback.mw4anim 18365B
+A mechs/jenner2c/animation/j2c_fallforward.mw4anim 17909B
+A mechs/jenner2c/animation/j2c_fallleft.mw4anim 18445B
+A mechs/jenner2c/animation/j2c_fallpose.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_fallright.mw4anim 18445B
+A mechs/jenner2c/animation/j2c_getup.mw4anim 13201B
+A mechs/jenner2c/animation/j2c_getupright.mw4anim 13201B
+A mechs/jenner2c/animation/j2c_jump.mw4anim 4181B
+A mechs/jenner2c/animation/j2c_landforward.mw4anim 8067B
+A mechs/jenner2c/animation/j2c_landstand.mw4anim 6143B
+A mechs/jenner2c/animation/j2c_landstandd.mw4anim 6223B
+A mechs/jenner2c/animation/j2c_landstandl.mw4anim 6443B
+A mechs/jenner2c/animation/j2c_landstandr.mw4anim 6443B
+A mechs/jenner2c/animation/j2c_landstandu.mw4anim 6163B
+A mechs/jenner2c/animation/j2c_lgimp.mw4anim 7991B
+A mechs/jenner2c/animation/j2c_lgimpd.mw4anim 8009B
+A mechs/jenner2c/animation/j2c_lgimpl.mw4anim 7769B
+A mechs/jenner2c/animation/j2c_lgimppose.mw4anim 1543B
+A mechs/jenner2c/animation/j2c_lgimpposed.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_lgimpposel.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_lgimpposer.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_lgimpposeu.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_lgimpr.mw4anim 7969B
+A mechs/jenner2c/animation/j2c_lgimpstand.mw4anim 4247B
+A mechs/jenner2c/animation/j2c_lgimpstandd.mw4anim 4277B
+A mechs/jenner2c/animation/j2c_lgimpstandl.mw4anim 4001B
+A mechs/jenner2c/animation/j2c_lgimpstandr.mw4anim 4213B
+A mechs/jenner2c/animation/j2c_lgimpstandu.mw4anim 4237B
+A mechs/jenner2c/animation/j2c_lgimpturnleft.mw4anim 4423B
+A mechs/jenner2c/animation/j2c_lgimpturnleftd.mw4anim 4241B
+A mechs/jenner2c/animation/j2c_lgimpturnleftl.mw4anim 4321B
+A mechs/jenner2c/animation/j2c_lgimpturnleftr.mw4anim 4241B
+A mechs/jenner2c/animation/j2c_lgimpturnleftu.mw4anim 4241B
+A mechs/jenner2c/animation/j2c_lgimpturnright.mw4anim 3895B
+A mechs/jenner2c/animation/j2c_lgimpturnrightd.mw4anim 3789B
+A mechs/jenner2c/animation/j2c_lgimpturnrightl.mw4anim 3809B
+A mechs/jenner2c/animation/j2c_lgimpturnrightr.mw4anim 3749B
+A mechs/jenner2c/animation/j2c_lgimpturnrightu.mw4anim 3689B
+A mechs/jenner2c/animation/j2c_lgimpu.mw4anim 7989B
+A mechs/jenner2c/animation/j2c_powerdown.mw4anim 3025B
+A mechs/jenner2c/animation/j2c_powerdownd.mw4anim 3025B
+A mechs/jenner2c/animation/j2c_powerdownl.mw4anim 3145B
+A mechs/jenner2c/animation/j2c_powerdownr.mw4anim 3145B
+A mechs/jenner2c/animation/j2c_powerdownu.mw4anim 3025B
+A mechs/jenner2c/animation/j2c_powerup.mw4anim 3025B
+A mechs/jenner2c/animation/j2c_powerupd.mw4anim 3045B
+A mechs/jenner2c/animation/j2c_powerupl.mw4anim 3165B
+A mechs/jenner2c/animation/j2c_powerupr.mw4anim 3165B
+A mechs/jenner2c/animation/j2c_powerupu.mw4anim 3025B
+A mechs/jenner2c/animation/j2c_rgimp.mw4anim 7991B
+A mechs/jenner2c/animation/j2c_rgimpd.mw4anim 8009B
+A mechs/jenner2c/animation/j2c_rgimpl.mw4anim 7969B
+A mechs/jenner2c/animation/j2c_rgimppose.mw4anim 1543B
+A mechs/jenner2c/animation/j2c_rgimpposed.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_rgimpposel.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_rgimpposer.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_rgimpposeu.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_rgimpr.mw4anim 7769B
+A mechs/jenner2c/animation/j2c_rgimpstand.mw4anim 4227B
+A mechs/jenner2c/animation/j2c_rgimpstandd.mw4anim 4277B
+A mechs/jenner2c/animation/j2c_rgimpstandl.mw4anim 4213B
+A mechs/jenner2c/animation/j2c_rgimpstandr.mw4anim 4001B
+A mechs/jenner2c/animation/j2c_rgimpstandu.mw4anim 4237B
+A mechs/jenner2c/animation/j2c_rgimpturnleft.mw4anim 3895B
+A mechs/jenner2c/animation/j2c_rgimpturnleftd.mw4anim 3789B
+A mechs/jenner2c/animation/j2c_rgimpturnleftl.mw4anim 3749B
+A mechs/jenner2c/animation/j2c_rgimpturnleftr.mw4anim 3809B
+A mechs/jenner2c/animation/j2c_rgimpturnleftu.mw4anim 3689B
+A mechs/jenner2c/animation/j2c_rgimpturnright.mw4anim 4423B
+A mechs/jenner2c/animation/j2c_rgimpturnrightd.mw4anim 4241B
+A mechs/jenner2c/animation/j2c_rgimpturnrightl.mw4anim 4241B
+A mechs/jenner2c/animation/j2c_rgimpturnrightr.mw4anim 4321B
+A mechs/jenner2c/animation/j2c_rgimpturnrightu.mw4anim 4241B
+A mechs/jenner2c/animation/j2c_rgimpu.mw4anim 7989B
+A mechs/jenner2c/animation/j2c_run.mw4anim 8443B
+A mechs/jenner2c/animation/j2c_rund.mw4anim 7705B
+A mechs/jenner2c/animation/j2c_runl.mw4anim 8089B
+A mechs/jenner2c/animation/j2c_runr.mw4anim 7849B
+A mechs/jenner2c/animation/j2c_runu.mw4anim 8209B
+A mechs/jenner2c/animation/j2c_squatdown.mw4anim 6813B
+A mechs/jenner2c/animation/j2c_squatdownd.mw4anim 6713B
+A mechs/jenner2c/animation/j2c_squatdownl.mw4anim 7293B
+A mechs/jenner2c/animation/j2c_squatdownr.mw4anim 6853B
+A mechs/jenner2c/animation/j2c_squatdownu.mw4anim 7153B
+A mechs/jenner2c/animation/j2c_squatup.mw4anim 6733B
+A mechs/jenner2c/animation/j2c_squatupd.mw4anim 6653B
+A mechs/jenner2c/animation/j2c_squatupl.mw4anim 7173B
+A mechs/jenner2c/animation/j2c_squatupr.mw4anim 6733B
+A mechs/jenner2c/animation/j2c_squatupu.mw4anim 7033B
+A mechs/jenner2c/animation/j2c_standback.mw4anim 4731B
+A mechs/jenner2c/animation/j2c_standbackd.mw4anim 4877B
+A mechs/jenner2c/animation/j2c_standbackl.mw4anim 4669B
+A mechs/jenner2c/animation/j2c_standbackr.mw4anim 4617B
+A mechs/jenner2c/animation/j2c_standbacku.mw4anim 4557B
+A mechs/jenner2c/animation/j2c_standlgimp.mw4anim 5007B
+A mechs/jenner2c/animation/j2c_standlgimpd.mw4anim 4733B
+A mechs/jenner2c/animation/j2c_standlgimpl.mw4anim 4745B
+A mechs/jenner2c/animation/j2c_standlgimpr.mw4anim 4861B
+A mechs/jenner2c/animation/j2c_standlgimpu.mw4anim 4909B
+A mechs/jenner2c/animation/j2c_standpose.mw4anim 1543B
+A mechs/jenner2c/animation/j2c_standposed.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_standposel.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_standposer.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_standposeu.mw4anim 1377B
+A mechs/jenner2c/animation/j2c_standrgimp.mw4anim 5007B
+A mechs/jenner2c/animation/j2c_standrgimpd.mw4anim 4733B
+A mechs/jenner2c/animation/j2c_standrgimpl.mw4anim 4861B
+A mechs/jenner2c/animation/j2c_standrgimpr.mw4anim 4745B
+A mechs/jenner2c/animation/j2c_standrgimpu.mw4anim 4909B
+A mechs/jenner2c/animation/j2c_standwalk.mw4anim 4491B
+A mechs/jenner2c/animation/j2c_standwalkd.mw4anim 4609B
+A mechs/jenner2c/animation/j2c_standwalkl.mw4anim 4369B
+A mechs/jenner2c/animation/j2c_standwalkr.mw4anim 4229B
+A mechs/jenner2c/animation/j2c_standwalku.mw4anim 4605B
+A mechs/jenner2c/animation/j2c_turnleft.mw4anim 3795B
+A mechs/jenner2c/animation/j2c_turnleftd.mw4anim 3533B
+A mechs/jenner2c/animation/j2c_turnleftl.mw4anim 3733B
+A mechs/jenner2c/animation/j2c_turnleftr.mw4anim 3733B
+A mechs/jenner2c/animation/j2c_turnleftu.mw4anim 3693B
+A mechs/jenner2c/animation/j2c_turnright.mw4anim 3795B
+A mechs/jenner2c/animation/j2c_turnrightd.mw4anim 3533B
+A mechs/jenner2c/animation/j2c_turnrightl.mw4anim 3733B
+A mechs/jenner2c/animation/j2c_turnrightr.mw4anim 3733B
+A mechs/jenner2c/animation/j2c_turnrightu.mw4anim 3693B
+A mechs/jenner2c/animation/j2c_walk.mw4anim 8783B
+A mechs/jenner2c/animation/j2c_walkd.mw4anim 8693B
+A mechs/jenner2c/animation/j2c_walkl.mw4anim 8589B
+A mechs/jenner2c/animation/j2c_walkr.mw4anim 8585B
+A mechs/jenner2c/animation/j2c_walkstand.mw4anim 4847B
+A mechs/jenner2c/animation/j2c_walkstandd.mw4anim 4597B
+A mechs/jenner2c/animation/j2c_walkstandl.mw4anim 4573B
+A mechs/jenner2c/animation/j2c_walkstandr.mw4anim 4557B
+A mechs/jenner2c/animation/j2c_walkstandrev.mw4anim 4675B
+A mechs/jenner2c/animation/j2c_walkstandrevd.mw4anim 4597B
+A mechs/jenner2c/animation/j2c_walkstandrevl.mw4anim 4557B
+A mechs/jenner2c/animation/j2c_walkstandrevr.mw4anim 4573B
+A mechs/jenner2c/animation/j2c_walkstandrevu.mw4anim 4741B
+A mechs/jenner2c/animation/j2c_walkstandu.mw4anim 4761B
+A mechs/jenner2c/animation/j2c_walku.mw4anim 8549B
+A mechs/jenner2c/jenner2c.animscript 37976B
-B shellscripts/graphics/multiplayer/lobbydecals/decal_46.tga 1375B
-B shellscripts/graphics/multiplayer/lobbydecals/decal_47.tga 4140B
-B shellscripts/graphics/multiplayer/lobbydecals/decal_49.tga 4140B
~ aiplayers/playerai/playerai.data{gamemodel} A=28B B=28B
~ buildings/base_turret/base_turret.subsystems A=978B B=978B
~ buildings/calliope/calliope.subsystems A=3794B B=3794B
~ buildings/calliope/calliope.torso{gamemodel} A=1608B B=1608B
~ buildings/elitex_mp_turret/elitex_mp_turret.subsystems A=1358B B=1358B
~ buildings/factory1_destroyed/factory1_destroyed.instance A=96B B=96B
~ buildings/factory2_destroyed/factory2_destroyed.instance A=96B B=96B
~ buildings/factory4_destroyed/factory4_destroyed.instance A=96B B=96B
~ buildings/factory6_destroyed/factory6_destroyed.instance A=96B B=96B
~ buildings/factory7_destroyed/factory7_destroyed.instance A=96B B=96B
~ buildings/farm3_destroyed/farm3_destroyed.data{gamemodel} A=28B B=28B
~ buildings/farm4_destroyed/farm4_destroyed.data{gamemodel} A=28B B=28B
~ buildings/fuel_tank_destroyed/fuel_tank_destroyed.data{gamemodel} A=28B B=28B
~ buildings/hrothgart/gun1.torso{gamemodel} A=1608B B=1608B
~ buildings/hrothgart/gun2.torso{gamemodel} A=1608B B=1608B
~ buildings/hrothgart/gun3.torso{gamemodel} A=1608B B=1608B
~ buildings/hrothgart/gun4.torso{gamemodel} A=1608B B=1608B
~ buildings/hrothgart/hrothgart.contents[joint_dropship]{armature} A=282B B=282B
~ buildings/hrothgart/hrothgart.contents[joint_engines]{armature} A=1122B B=1122B
~ buildings/hrothgart/hrothgart.subsystems A=9438B B=9438B
~ buildings/large_guardtower/large_guardtower.contents A=282B B=282B
~ buildings/large_guardtower/large_guardtower.contents[joint_base]{armature} A=282B B=282B
~ buildings/large_guardtower/large_guardtower.instance A=308B B=308B
~ buildings/large_guardtower/large_guardtower.subsystems A=3034B B=3034B
~ buildings/large_guardtower/large_guardtower.torso{gamemodel} A=1608B B=1608B
~ buildings/listening_post/listening_post.data{gamemodel} A=664B B=664B
~ buildings/lrm_turret/lrm_turret.data{gamemodel} A=664B B=664B
~ buildings/lrm_turret/lrm_turret.subsystems A=1030B B=1030B
~ buildings/lrm_turret/lrm_turret.torso{gamemodel} A=1608B B=1608B
~ buildings/marsh_lrm_turret/marsh_lrm_turret.contents[joint_base]{armature} A=282B B=282B
~ buildings/marsh_lrm_turret/marsh_lrm_turret.subsystems A=1030B B=1030B
~ buildings/marsh_lrm_turret/marsh_lrm_turret.torso{gamemodel} A=1608B B=1608B
~ buildings/marsh_radar_station/marsh_radar_station.contents A=282B B=282B
~ buildings/marsh_radar_station/marsh_radar_station.contents[joint_base]{armature} A=282B B=282B
~ buildings/mercwall_towerb/mercwall_towerb.instance A=308B B=308B
~ buildings/ml_cannon/ml_cannon.subsystems A=598B B=598B
~ buildings/ml_cannon/ml_cannon.torso{gamemodel} A=1608B B=1608B
~ buildings/overlordt/overlordt.contents[joint_overlord]{armature} A=1962B B=1962B
~ buildings/overlordt/overlordt.subsystems A=4446B B=4446B
~ buildings/radiotower/radiotower.instance A=308B B=308B
~ buildings/repair_platform/repair_platform.instance A=308B B=308B
~ buildings/roadblock_1/roadblock_1.instance A=308B B=308B
~ buildings/roadblock_2/roadblock_2.instance A=308B B=308B
~ buildings/ruined1/ruined1.instance A=308B B=308B
~ buildings/ruined2/ruined2.instance A=308B B=308B
~ buildings/sp_turret/sp_turret.subsystems A=978B B=978B
~ buildings/strongpoint_defense/strongpoint_defense.contents[joint_root]{armature} A=842B B=842B
~ buildings/strongpoint_defense/strongpoint_defense.subsystems A=2982B B=2982B
~ buildings/strongpoint_defense/strongpoint_defense.torso{gamemodel} A=1608B B=1608B
~ buildings/talont/talont.contents[joint_hull]{armature} A=2802B B=2802B
~ buildings/talont/talont.subsystems A=5418B B=5418B
~ buildings/trailer_turret/trailer_turret.contents[joint_base]{armature} A=282B B=282B
~ buildings/trailer_turret/trailer_turret.subsystems A=1030B B=1030B
~ buildings/trailer_turret/trailer_turret.torso{gamemodel} A=1608B B=1608B
~ buildings/urban_lg_04_destroyed/urban_lg_04_destroyed.instance A=96B B=96B
~ culturals/rock_volcan_small03/rock_volcan_small03.data{gamemodel} A=40B B=40B
~ culturals/rock_volcan_small03/rock_volcan_small03.instance A=100B B=100B
~ culturals/tree_alpine_evergreen02/tree_alpine_evergreen02.data{gamemodel} A=40B B=40B
~ culturals/tree_alpine_evergreen02/tree_alpine_evergreen02.instance A=100B B=100B
~ culturals/tree_arctic_snowy02/tree_arctic_snowy02.data{gamemodel} A=40B B=40B
~ culturals/tree_arctic_snowy02/tree_arctic_snowy02.instance A=100B B=100B
~ culturals/tree_arctic_snowy03/tree_arctic_snowy03.data{gamemodel} A=40B B=40B
~ culturals/tree_arctic_snowy03/tree_arctic_snowy03.instance A=100B B=100B
~ culturals/tree_arctic_snowy_multi01/tree_arctic_snowy_multi01.instance A=100B B=100B
~ culturals/tree_tropical_beachshrub/tree_tropical_beachshrub.data{gamemodel} A=40B B=40B
~ effects/buildingexplosions/air_control_tower/air_control_tower.audio A=128B B=128B
~ effects/buildingexplosions/barriercap/barriercap.audio A=128B B=128B
~ effects/buildingexplosions/bridge/bridge.audio A=128B B=128B
~ effects/buildingexplosions/colpillar/colpillar.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/colsm90_torch/colsm90_torch.audio A=128B B=128B
~ effects/buildingexplosions/command_center/command_center.audio A=128B B=128B
~ effects/buildingexplosions/communications_dish/communications_dish.audio A=128B B=128B
~ effects/buildingexplosions/crate_stack/crate_stack.audio A=128B B=128B
~ effects/buildingexplosions/factory1/factory1.audio A=128B B=128B
~ effects/buildingexplosions/factory2/factory2.audio A=128B B=128B
~ effects/buildingexplosions/factory3/factory3.audio A=128B B=128B
~ effects/buildingexplosions/factory4/factory4.audio A=128B B=128B
~ effects/buildingexplosions/factory5/factory5.audio A=128B B=128B
~ effects/buildingexplosions/factory6/factory6.audio A=128B B=128B
~ effects/buildingexplosions/factory7/factory7.audio A=128B B=128B
~ effects/buildingexplosions/fuel_tanks/fuel_tanks.audio A=128B B=128B
~ effects/buildingexplosions/generator_control/generator_control.audio A=128B B=128B
~ effects/buildingexplosions/generator_control/generator_control.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/generic_building/generic_building.audio A=128B B=128B
~ effects/buildingexplosions/generic_building/generic_building.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/generic_building2/generic_building2.audio A=128B B=128B
~ effects/buildingexplosions/generic_building2/generic_building2.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/generic_flatlarge/generic_flatlarge.audio A=128B B=128B
~ effects/buildingexplosions/generic_flatlarge/generic_flatlarge.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/generic_flatmedium/generic_flatmedium.audio A=128B B=128B
~ effects/buildingexplosions/generic_flatmedium/generic_flatmedium.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/generic_large/generic_large.audio A=128B B=128B
~ effects/buildingexplosions/generic_large/generic_large.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/generic_large_blast/generic_large_blast.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/generic_medium/generic_medium.audio A=128B B=128B
~ effects/buildingexplosions/generic_medium/generic_medium.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/generic_small/generic_small.audio A=128B B=128B
~ effects/buildingexplosions/generic_small/generic_small.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/generic_tiny/generic_tiny.audio A=128B B=128B
~ effects/buildingexplosions/generic_tiny/generic_tiny.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/geot_generator/geot_generator.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/guard_comissary/guard_comissary.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/helopad/helopad.audio A=128B B=128B
~ effects/buildingexplosions/helopad/helopad.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/large_guardtower/large_guardtower.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/listening_post/listening_post.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/lrm_turret/lrm_turret.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/marsh_helo_station/marsh_helo_station.audio A=128B B=128B
~ effects/buildingexplosions/marsh_helo_station/marsh_helo_station.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/marsh_hover_station/marsh_hover_station.audio A=128B B=128B
~ effects/buildingexplosions/marsh_hover_station/marsh_hover_station.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/marsh_lrm_turret/marsh_lrm_turret.audio A=128B B=128B
~ effects/buildingexplosions/marsh_lrm_turret/marsh_lrm_turret.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/marsh_radar_station/marsh_radar_station.audio A=128B B=128B
~ effects/buildingexplosions/marsh_radar_station/marsh_radar_station.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/mech_bay/mech_bay.audio A=128B B=128B
~ effects/buildingexplosions/mech_bay/mech_bay.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/mech_hanger/mech_hanger.audio A=128B B=128B
~ effects/buildingexplosions/mech_hanger/mech_hanger.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/mech_repair_bldg/mech_repair_bldg.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/ml_cannon/ml_cannon.audio A=128B B=128B
~ effects/buildingexplosions/ml_cannon/ml_cannon.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/palace_mausoleum/palace_mausoleum.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/prisoner_barracks/prisoner_barracks.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/radiotower/radiotower.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/repair_platform/repair_platform.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/satelite_control/satelite_control.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/shilone_hangar/shilone_hangar.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/tent1/tent1.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/tent2/tent2.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/twinmechbay/twinmechbay.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/vehicle_hangar/vehicle_hangar.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/vehicle_hangar2/vehicle_hangar2.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/vehicle_trailer/vehicle_trailer.data{gamemodel} A=120B B=120B
~ effects/buildingexplosions/weapon_warehouse/weapon_warehouse.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/hrothgar_destruction.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/hrothgar_launch_ground.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/hrothgar_launch_ship.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/hrothgar_predestruction.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/overlord_destruction.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/overlord_launch_ground.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/overlord_launch_ship.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/overlord_predestruction.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/talon_destruction.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/talon_launch_ground.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/talon_launch_ship.data{gamemodel} A=120B B=120B
~ effects/dropship_effects/talon_predestruction.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/bldg_flare.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/burning_wreck.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lavabubble.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lunar_transmit.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lunar_transmit_blue.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lunar_transmit_green.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lunar_transmit_red.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lunar_transmit_yellow.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lunarenv_bubble_up.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lunarenv_crevice_steam.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lunarenv_geyser_1.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/lunarenv_geyser_2.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/sand.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/sparkles_ground.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/sparkles_large.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/sparkles_medium.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/surface_drill_effect.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/vent_steam.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/vent_steam_grey.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/volcanic_crack_smoke.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/volcanic_debris.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/volcanic_smoke.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/volcanic_smoke_small.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/waterfall.data{gamemodel} A=120B B=120B
~ effects/environmental_effects/waterfall_splash.data{gamemodel} A=120B B=120B
~ effects/hovergroundtrail/hover_trail_brown.data{gamemodel} A=120B B=120B
~ effects/hovergroundtrail/hover_trail_darkbrown.data{gamemodel} A=120B B=120B
~ effects/hovergroundtrail/hover_trail_darkgrey.data{gamemodel} A=120B B=120B
~ effects/hovergroundtrail/hover_trail_grey.data{gamemodel} A=120B B=120B
~ effects/hovergroundtrail/hover_trail_water.data{gamemodel} A=120B B=120B
~ effects/hovergroundtrail/hovergroundtrail.data{gamemodel} A=120B B=120B
~ effects/leaveseffect/leaveseffect.data{gamemodel} A=120B B=120B
~ effects/lightningeffect/lightningeffect.audio A=128B B=128B
~ effects/lightningeffect/lightningeffect.data{gamemodel} A=120B B=120B
~ effects/materialeffectholders/hover_trail.contents A=100B B=100B
~ effects/motobike_trail/motobiketrail.audio A=128B B=128B
~ effects/motobike_trail/motobiketrail.data{gamemodel} A=120B B=120B
~ effects/possumeffect/possumeffect.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_minerl_large01/rock_minerl.audio A=128B B=128B
~ effects/rock_destructions/rock_minerl_large01/rock_minerl_large01.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_minerl_large02/rock_minerl_large02.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_minerl_large03/rock_minerl_large03.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_minerl_medium01/rock_minerl_medium01.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_minerl_medium02/rock_minerl_medium02.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_minerl_medium03/rock_minerl_medium03.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_minerl_small/rock_minerl.audio A=128B B=128B
~ effects/rock_destructions/rock_minerl_small/rock_minerl_small.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_volcan_large01/rock_volcan_large01.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_volcan_large02/rock_volcan_large02.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_volcan_large03/rock_volcan_large03.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_volcan_medium01/rock_volcan_medium01.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_volcan_medium02/rock_volcan_medium02.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_volcan_medium03/rock_volcan_medium03.data{gamemodel} A=120B B=120B
~ effects/rock_destructions/rock_volcan_small/rock_volcan_small.data{gamemodel} A=120B B=120B
~ effects/tank_trail/tank_trail_brown.data{gamemodel} A=120B B=120B
~ effects/tank_trail/tank_trail_darkbrown.data{gamemodel} A=120B B=120B
~ effects/tank_trail/tank_trail_darkgrey.data{gamemodel} A=120B B=120B
~ effects/tank_trail/tank_trail_grey.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/cactus01/cactus01_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/cactus01/cactus01_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/cactus02/cactus02_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/cactus02/cactus02_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/cactus03/cactus03_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/cactus03/cactus03_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/evergreen01/evergreen01_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/evergreen01/evergreen01_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/evergreen02/evergreen02_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/evergreen02/evergreen02_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/fir01/fir01_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/fir01/fir01_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/fir02/fir02_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/fir02/fir02_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/joshua01/joshua01_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/joshua01/joshua01_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/joshua02/joshua02_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/joshua02/joshua02_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/joshua03/joshua03_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/joshua03/joshua03_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/mossdraped02/mossdraped02_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/mossdraped02/mossdraped02_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/multi01/multi01_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/pine01/pine01_death.audio A=128B B=128B
~ effects/treeexplosions/pine01/pine01_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/pine01/pine01_shot.audio A=128B B=128B
~ effects/treeexplosions/pine01/pine01_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/pine02/pine02_death.audio A=128B B=128B
~ effects/treeexplosions/pine02/pine02_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/pine02/pine02_shot.audio A=128B B=128B
~ effects/treeexplosions/pine02/pine02_shot.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/sequoia01/sequoia01_death.audio A=128B B=128B
~ effects/treeexplosions/sequoia01/sequoia01_death.data{gamemodel} A=120B B=120B
~ effects/treeexplosions/sequoia01/sequoia01_shot.audio A=128B B=128B
~ effects/treeexplosions/sequoia02/sequoia02_death.audio A=128B B=128B
~ effects/treeexplosions/sequoia02/sequoia02_shot.audio A=128B B=128B
~ effects/treeexplosions/streetlight_death/streetlight_death.audio A=128B B=128B
~ effects/treeexplosions/test_shot/test_shot.audio A=128B B=128B
~ effects/vehicle_destruction/peregrine/peregrine.audio A=128B B=128B
~ interfaces/common/chiinterface.control A=5646B B=5646B
~ interfaces/common/daninterface.control A=5646B B=5646B
~ interfaces/common/fininterface.control A=5646B B=5646B
~ interfaces/common/freinterface.control A=5646B B=5646B
~ interfaces/common/gerinterface.control A=5646B B=5646B
~ interfaces/common/interface.control A=5970B B=5970B
~ interfaces/common/itainterface.control A=5646B B=5646B
~ interfaces/common/japinterface.control A=5646B B=5646B
~ interfaces/common/korinterface.control A=5646B B=5646B
~ interfaces/common/norinterface.control A=5646B B=5646B
~ interfaces/common/porinterface.control A=5646B B=5646B
~ interfaces/common/spainterface.control A=5646B B=5646B
~ interfaces/common/sweinterface.control A=5646B B=5646B
~ misc/flag_team0/flag_team0.data{gamemodel} A=664B B=664B
~ misc/flag_team1/flag_team1.data{gamemodel} A=664B B=664B
~ misc/flag_team1/flag_team1.instance A=308B B=308B
~ misc/flag_team2/flag_team2.data{gamemodel} A=664B B=664B
~ misc/flag_team3/flag_team3.data{gamemodel} A=664B B=664B
~ misc/flag_team3/flag_team3.instance A=308B B=308B
~ misc/flag_team4/flag_team4.data{gamemodel} A=664B B=664B
~ misc/flag_team5/flag_team5.data{gamemodel} A=664B B=664B
~ misc/flag_team5/flag_team5.instance A=308B B=308B
~ misc/flag_team6/flag_team6.data{gamemodel} A=664B B=664B
~ misc/flag_team7/flag_team7.data{gamemodel} A=664B B=664B
~ misc/flag_team7/flag_team7.instance A=308B B=308B
~ misc/flag_team8/flag_team8.data{gamemodel} A=664B B=664B
~ misc/navpoint/navpoint.data{gamemodel} A=664B B=664B
~ misc/navpoint/navpoint.instance A=308B B=308B
~ misc/objective/objective.instance A=1148B B=1148B
~ misc/team/team.data{gamemodel} A=756B B=756B
~ misc/team/team.instance A=308B B=308B
~ shellscripts/computerplayer.script A=6233B B=6226B
~ shellscripts/conlobby.script A=141044B B=140532B
~ shellscripts/credits.script A=8684B B=8614B
~ shellscripts/graphics/multiplayer/lobbyskins/skinc4.tga A=3643B B=3090B
~ shellscripts/graphics/multiplayer/lobbyskins/skinu4.tga A=3642B B=3116B
~ shellscripts/listboxes.script A=30785B B=29269B
~ shellscripts/mainmenu.script A=15247B B=15212B
~ shellscripts/mc_listboxes.script A=58773B B=58767B
~ shellscripts/mc_listboxes_controls.script A=52698B B=52692B
~ shellscripts/mechbay/advancetime.script A=12677B B=12671B
~ shellscripts/mechbay/armor.script A=64539B B=61807B
~ shellscripts/mechbay/chassis.script A=82596B B=81881B
~ shellscripts/mechbay/graphics/installed/weapon_-1_1_0.tga A=1036B B=6540B
~ shellscripts/mechbay/graphics/installed/weapon_-1_1_1.tga A=1036B B=6540B
~ shellscripts/mechbay/graphics/installed/weapon_-1_1_2.tga A=1036B B=6540B
~ shellscripts/mechbay/graphics/installed/weapon_-1_1_3.tga A=1036B B=6540B
~ shellscripts/mechbay/graphics/installed/weapon_-1_2_0.tga A=1114B B=13036B
~ shellscripts/mechbay/graphics/installed/weapon_-1_2_1.tga A=1114B B=13036B
~ shellscripts/mechbay/graphics/installed/weapon_-1_2_2.tga A=1114B B=13036B
~ shellscripts/mechbay/graphics/installed/weapon_-1_2_3.tga A=1114B B=13036B
~ shellscripts/mechbay/graphics/installed/weapon_-1_3_0.tga A=1357B B=19532B
~ shellscripts/mechbay/graphics/installed/weapon_-1_3_1.tga A=1357B B=19532B
~ shellscripts/mechbay/graphics/installed/weapon_-1_3_2.tga A=1357B B=19532B
~ shellscripts/mechbay/graphics/installed/weapon_-1_3_3.tga A=1357B B=19532B
~ shellscripts/mechbay/graphics/installed/weapon_-1_4_0.tga A=1620B B=26028B
~ shellscripts/mechbay/graphics/installed/weapon_-1_4_1.tga A=1620B B=26028B
~ shellscripts/mechbay/graphics/installed/weapon_-1_4_2.tga A=1620B B=26028B
~ shellscripts/mechbay/graphics/installed/weapon_-1_4_3.tga A=1620B B=26028B
~ shellscripts/mechbay/graphics/installed/weapon_-1_5_0.tga A=1843B B=32524B
~ shellscripts/mechbay/graphics/installed/weapon_-1_5_1.tga A=1843B B=32524B
~ shellscripts/mechbay/graphics/installed/weapon_-1_5_2.tga A=1843B B=32524B
~ shellscripts/mechbay/graphics/installed/weapon_-1_5_3.tga A=1843B B=32524B
~ shellscripts/mechbay/graphics/installed/weapon_-1_6_0.tga A=2096B B=39020B
~ shellscripts/mechbay/graphics/installed/weapon_-1_6_1.tga A=2096B B=39020B
~ shellscripts/mechbay/graphics/installed/weapon_-1_6_2.tga A=2096B B=39020B
~ shellscripts/mechbay/graphics/installed/weapon_-1_6_3.tga A=2096B B=39020B
~ shellscripts/mechbay/gs_chassis.script A=34465B B=34389B
~ shellscripts/mechbay/gs_mechbay_main.script A=14412B B=14406B
~ shellscripts/mechbay/infobox.script A=43875B B=41595B
~ shellscripts/mechbay/mc_listboxes_weapon.script A=47284B B=47278B
~ shellscripts/mechbay/mechbay.script A=1282B B=1276B
~ shellscripts/mechbay/mechbay_main.script A=94517B B=61432B
~ shellscripts/mechbay/weapons.script A=102025B B=83653B
~ shellscripts/mechlabheaders.h A=9142B B=8994B
~ shellscripts/mechweight.script A=5636B B=5630B
~ shellscripts/multiplayer/buildrestriction.script A=16443B B=15713B
~ shellscripts/multiplayer/con_skins_listbox.script A=28607B B=28601B
~ shellscripts/multiplayer/conlobbymission.script A=60376B B=60362B
~ shellscripts/multiplayer/hostlobbyserver.script A=22524B B=22520B
~ shellscripts/multiplayer/listofgames.script A=25575B B=25571B
~ shellscripts/multiplayer/mc_listboxes.script A=54403B B=54397B
~ shellscripts/multiplayer/mech_var_listbox.script A=28886B B=28845B
~ shellscripts/multiplayer/multicolumnlist.script A=39743B B=39741B
~ shellscripts/multiplayer/multiplayerconsole.script A=16189B B=16187B
~ shellscripts/netlobby.script A=163740B B=163088B
~ shellscripts/scriptstrings.h A=71608B B=70811B
~ shellscripts/stddefs.h A=5313B B=5307B
~ shellscripts/teslaplayer.script A=19744B B=19743B
~ vehicles/ammo_carrier/ammo_carrier.audio A=330B B=330B
~ vehicles/ammo_carrier/ammo_carrier.instance A=308B B=308B
~ vehicles/ammo_carrier_destroyed/ammo_carrier_destroyed.data{gamemodel} A=28B B=28B
~ vehicles/apc/apc.audio A=330B B=330B
~ vehicles/apc/apc.contents A=282B B=282B
~ vehicles/apc/apc.instance A=308B B=308B
~ vehicles/apc/apc.subsystems A=598B B=598B
~ vehicles/apc/apc.torso{gamemodel} A=1608B B=1608B
~ vehicles/apc/armaturedata/hull.data{gamemodel} A=80B B=80B
~ vehicles/apc_destroyed/apc_destroyed.data{gamemodel} A=28B B=28B
~ vehicles/artillery_launcher/artillery_launcher.audio A=330B B=330B
~ vehicles/artillery_launcher/artillery_launcher.torso{gamemodel} A=1608B B=1608B
~ vehicles/boxvan_destroyed/boxvan_destroyed.data{gamemodel} A=28B B=28B
~ vehicles/bulldog/bulldog.contents[joint_hull]{armature} A=282B B=282B
~ vehicles/bulldog/bulldog.contents[joint_twist]{armature} A=282B B=282B
~ vehicles/bulldog/bulldog.subsystems A=1358B B=1358B
~ vehicles/bulldog/bulldog.torso{gamemodel} A=1608B B=1608B
~ vehicles/bus/bus.audio A=330B B=330B
~ vehicles/bus_destroyed/bus_destroyed.data{gamemodel} A=28B B=28B
~ vehicles/car/car.audio A=330B B=330B
~ vehicles/car/car.instance A=308B B=308B
~ vehicles/cargotrack_destroyed/cargotrack_destroyed.data{gamemodel} A=28B B=28B
~ vehicles/condor/condor.contents[joint_hull]{armature} A=282B B=282B
~ vehicles/condor/condor.subsystems A=1738B B=1738B
~ vehicles/condor/condor.torso{gamemodel} A=1608B B=1608B
~ vehicles/destroyer1/destroyer1.contents A=282B B=282B
~ vehicles/destroyer1/destroyer1.instance A=308B B=308B
~ vehicles/destroyer1/destroyer1.subsystems A=2714B B=2714B
~ vehicles/field_base/field_base.contents[joint_base]{armature} A=1682B B=1682B
~ vehicles/field_base/field_base.data{gamemodel} A=776B B=776B
~ vehicles/field_base/field_base.instance A=308B B=308B
~ vehicles/field_base_destroyed/field_base_destroyed.data{gamemodel} A=28B B=28B
~ vehicles/firetruck/firetruck.subsystems A=1298B B=1298B
~ vehicles/firetruck/firetruck.torso{gamemodel} A=1608B B=1608B
~ vehicles/harasser/harasser.subsystems A=1358B B=1358B
~ vehicles/harasser/harasser.torso{gamemodel} A=1608B B=1608B
~ vehicles/hrothgar/gun1.torso{gamemodel} A=1608B B=1608B
~ vehicles/hrothgar/gun2.torso{gamemodel} A=1608B B=1608B
~ vehicles/hrothgar/gun3.torso{gamemodel} A=1608B B=1608B
~ vehicles/hrothgar/gun4.torso{gamemodel} A=1608B B=1608B
~ vehicles/hrothgar/hrothgar.contents[joint_engines]{armature} A=1122B B=1122B
~ vehicles/hrothgar/hrothgar.subsystems A=8522B B=8522B
~ vehicles/humvee/humvee.torso{gamemodel} A=1608B B=1608B
~ vehicles/karnov/karnov.contents A=282B B=282B
~ vehicles/karnov/karnov.instance A=312B B=312B
~ vehicles/karnov/karnov.subsystems A=870B B=870B
~ vehicles/loader/loader.instance A=308B B=308B
~ vehicles/lrm_carrier/lrm_carrier.audio A=330B B=330B
~ vehicles/lrm_carrier/lrm_carrier.subsystems A=1358B B=1358B
~ vehicles/magi_tank/magi_tank.contents[joint_hull]{armature} A=282B B=282B
~ vehicles/magi_tank/magi_tank.subsystems A=1738B B=1738B
~ vehicles/magi_tank_destroyed/magi_tank_destroyed.data{gamemodel} A=28B B=28B
~ vehicles/mobile_apu/mobile_apu.instance A=308B B=308B
~ vehicles/mobile_turret_control/mobile_turret_control.contents A=282B B=282B
~ vehicles/mobile_turret_control/mobile_turret_control.instance A=308B B=308B
~ vehicles/mobile_turret_control/mobile_turret_control.subsystems A=218B B=218B
~ vehicles/mobile_turret_control/mobile_turret_control.torso{gamemodel} A=1608B B=1608B
~ vehicles/nightshade/nightshade.contents A=282B B=282B
~ vehicles/nightshade/nightshade.instance A=312B B=312B
~ vehicles/overlord/overlord.contents[joint_cannonringparent]{armature} A=282B B=282B
~ vehicles/overlord/overlord.contents[joint_lowergun01parent]{armature} A=282B B=282B
~ vehicles/overlord/overlord.contents[joint_lowergun02parent]{armature} A=282B B=282B
~ vehicles/overlord/overlord.contents[joint_lowergun03parent]{armature} A=282B B=282B
~ vehicles/overlord/overlord.contents[joint_overlord]{armature} A=1962B B=1962B
~ vehicles/overlord/overlord.contents[joint_uppergun01parent]{armature} A=282B B=282B
~ vehicles/overlord/overlord.contents[joint_uppergun02parent]{armature} A=282B B=282B
~ vehicles/overlord/overlord.contents[joint_uppergun03parent]{armature} A=282B B=282B
~ vehicles/overlord/overlord.subsystems A=4670B B=4670B
~ vehicles/patrolboat1/patrolboat1.torso{gamemodel} A=1608B B=1608B
~ vehicles/patrolboat2/patrolboat2.torso{gamemodel} A=1608B B=1608B
~ vehicles/srm_carrier/srm_carrier.subsystems A=1358B B=1358B
~ vehicles/swiftwind_destroyed/swiftwind_destroyed.data{gamemodel} A=28B B=28B
~ vehicles/talon/armaturedata/logunar.data{gamemodel} A=80B B=80B
~ vehicles/talon/talon.contents[joint_hull]{armature} A=2802B B=2802B
~ vehicles/talon/talon.subsystems A=4122B B=4122B
~ vehicles/talon/talon_lal.torso{gamemodel} A=1608B B=1608B
~ vehicles/talon/talon_lar.torso{gamemodel} A=1608B B=1608B
~ vehicles/talon/talon_lfl.torso{gamemodel} A=1608B B=1608B
~ vehicles/talon/talon_lfr.torso{gamemodel} A=1608B B=1608B
~ vehicles/talon/talon_mid.torso{gamemodel} A=1608B B=1608B
~ vehicles/talon/talon_ual.torso{gamemodel} A=1608B B=1608B
~ vehicles/talon/talon_uar.torso{gamemodel} A=1608B B=1608B
~ vehicles/talon/talon_ufl.torso{gamemodel} A=1608B B=1608B
~ vehicles/talon/talon_ufr.torso{gamemodel} A=1608B B=1608B
~ vehicles/vedette/vedette.subsystems A=978B B=978B
~ vehicles/vedette/vedette.torso{gamemodel} A=1608B B=1608B
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,301 @@
# entries: A=10419 B=10120 A_only=299 B_only=0 decoded-differ=1
+A mechs/champion/champion_cage.erf 11787B
+A mechs/champion/chp_hip.erf 6655B
+A mechs/champion/chp_hip_dam.erf 6655B
+A mechs/champion/chp_lbtoe.erf 1211B
+A mechs/champion/chp_lbtoe_dam.erf 1211B
+A mechs/champion/chp_ldleg.erf 8372B
+A mechs/champion/chp_ldleg_dam.erf 8372B
+A mechs/champion/chp_lfoot.erf 2017B
+A mechs/champion/chp_lfoot_dam.erf 2017B
+A mechs/champion/chp_lftoe.erf 2309B
+A mechs/champion/chp_lftoe_dam.erf 2309B
+A mechs/champion/chp_lgun.erf 8328B
+A mechs/champion/chp_luarm.erf 853B
+A mechs/champion/chp_luarm_dam.erf 853B
+A mechs/champion/chp_luleg.erf 10067B
+A mechs/champion/chp_luleg_dam.erf 10067B
+A mechs/champion/chp_rbtoe.erf 1211B
+A mechs/champion/chp_rbtoe_dam.erf 1211B
+A mechs/champion/chp_rdleg.erf 8372B
+A mechs/champion/chp_rdleg_dam.erf 8372B
+A mechs/champion/chp_rfoot.erf 2017B
+A mechs/champion/chp_rfoot_dam.erf 2017B
+A mechs/champion/chp_rftoe.erf 2309B
+A mechs/champion/chp_rftoe_dam.erf 2309B
+A mechs/champion/chp_rgun.erf 8328B
+A mechs/champion/chp_ruarm.erf 853B
+A mechs/champion/chp_ruarm_dam.erf 853B
+A mechs/champion/chp_ruleg.erf 10067B
+A mechs/champion/chp_ruleg_dam.erf 10067B
+A mechs/champion/chp_specialone.erf 3759B
+A mechs/champion/chp_specialone_dam.erf 1290B
+A mechs/champion/chp_specialtwo.erf 3759B
+A mechs/champion/chp_specialtwo_dam.erf 1290B
+A mechs/champion/chp_torso.erf 63022B
+A mechs/champion/chp_torso_dam.erf 63022B
+A mechs/champion/runninglights.erf 1552B
+A mechs/champion_destroyed/champion_destroyed.erf 14157B
+A mechs/dasher/das_hip.erf 51546B
+A mechs/dasher/das_hip_dam.erf 51546B
+A mechs/dasher/das_ldleg.erf 20552B
+A mechs/dasher/das_ldleg_dam.erf 20552B
+A mechs/dasher/das_lfoot.erf 11228B
+A mechs/dasher/das_lfoot_dam.erf 11228B
+A mechs/dasher/das_lgun.erf 27396B
+A mechs/dasher/das_lgun_dam.erf 783B
+A mechs/dasher/das_ltoe.erf 5172B
+A mechs/dasher/das_ltoe_dam.erf 5172B
+A mechs/dasher/das_luarm.erf 15258B
+A mechs/dasher/das_luarm_dam.erf 15258B
+A mechs/dasher/das_luleg.erf 10648B
+A mechs/dasher/das_luleg_dam.erf 10648B
+A mechs/dasher/das_rdleg.erf 20260B
+A mechs/dasher/das_rdleg_dam.erf 20260B
+A mechs/dasher/das_rfoot.erf 11228B
+A mechs/dasher/das_rfoot_dam.erf 11228B
+A mechs/dasher/das_rgun.erf 27396B
+A mechs/dasher/das_rtoe.erf 5172B
+A mechs/dasher/das_rtoe_dam.erf 5172B
+A mechs/dasher/das_ruarm.erf 15226B
+A mechs/dasher/das_ruarm_dam.erf 943B
+A mechs/dasher/das_ruleg.erf 10692B
+A mechs/dasher/das_ruleg_dam.erf 10692B
+A mechs/dasher/das_torso.erf 81366B
+A mechs/dasher/das_torso_dam.erf 81366B
+A mechs/dasher/dasher_cage.erf 12523B
+A mechs/dasher/runninglights__.erf 10824B
+A mechs/dasher_destroyed/dasher_destroyed.erf 14624B
+A mechs/griffin/grf_hip.erf 2831B
+A mechs/griffin/grf_hip_dam.erf 2831B
+A mechs/griffin/grf_lbtoe.erf 4029B
+A mechs/griffin/grf_lbtoe_dam.erf 4029B
+A mechs/griffin/grf_ldleg.erf 5784B
+A mechs/griffin/grf_ldleg_dam.erf 5784B
+A mechs/griffin/grf_lfoot.erf 791B
+A mechs/griffin/grf_lfoot_dam.erf 791B
+A mechs/griffin/grf_lftoe.erf 1479B
+A mechs/griffin/grf_lftoe_dam.erf 1479B
+A mechs/griffin/grf_lgun.erf 16111B
+A mechs/griffin/grf_lgun_dam.erf 16111B
+A mechs/griffin/grf_luarm.erf 4819B
+A mechs/griffin/grf_luarm_dam.erf 4819B
+A mechs/griffin/grf_luleg.erf 4933B
+A mechs/griffin/grf_luleg_dam.erf 4933B
+A mechs/griffin/grf_rbtoe.erf 4029B
+A mechs/griffin/grf_rbtoe_dam.erf 4029B
+A mechs/griffin/grf_rdleg.erf 5784B
+A mechs/griffin/grf_rdleg_dam.erf 5784B
+A mechs/griffin/grf_rfoot.erf 791B
+A mechs/griffin/grf_rfoot_dam.erf 791B
+A mechs/griffin/grf_rftoe.erf 1479B
+A mechs/griffin/grf_rftoe_dam.erf 1479B
+A mechs/griffin/grf_rgun.erf 14785B
+A mechs/griffin/grf_rgun_dam.erf 14785B
+A mechs/griffin/grf_ruarm.erf 4819B
+A mechs/griffin/grf_ruarm_dam.erf 4819B
+A mechs/griffin/grf_ruleg.erf 4933B
+A mechs/griffin/grf_ruleg_dam.erf 4933B
+A mechs/griffin/grf_specialone.erf 2511B
+A mechs/griffin/grf_specialone_dam.erf 2559B
+A mechs/griffin/grf_specialtwo.erf 3659B
+A mechs/griffin/grf_specialtwo_dam.erf 3707B
+A mechs/griffin/grf_torso.erf 24032B
+A mechs/griffin/grf_torso_dam.erf 24032B
+A mechs/griffin/griffin_cage.erf 13884B
+A mechs/griffin/runninglights.erf 541B
+A mechs/griffin_destroyed/griffin_destroyed.erf 16697B
+A mechs/jenner2c/jec_hip.erf 3936B
+A mechs/jenner2c/jec_hip_dam.erf 3936B
+A mechs/jenner2c/jec_lbtoe.erf 3191B
+A mechs/jenner2c/jec_lbtoe_dam.erf 3191B
+A mechs/jenner2c/jec_ldleg.erf 8438B
+A mechs/jenner2c/jec_ldleg_dam.erf 8438B
+A mechs/jenner2c/jec_lfoot.erf 2667B
+A mechs/jenner2c/jec_lfoot_dam.erf 2667B
+A mechs/jenner2c/jec_lftoe.erf 3599B
+A mechs/jenner2c/jec_lftoe_dam.erf 3599B
+A mechs/jenner2c/jec_lgun.erf 5943B
+A mechs/jenner2c/jec_lgun_dam.erf 5847B
+A mechs/jenner2c/jec_luleg.erf 13003B
+A mechs/jenner2c/jec_luleg_dam.erf 13003B
+A mechs/jenner2c/jec_rbtoe.erf 3191B
+A mechs/jenner2c/jec_rbtoe_dam.erf 3191B
+A mechs/jenner2c/jec_rdleg.erf 8438B
+A mechs/jenner2c/jec_rdleg_dam.erf 8438B
+A mechs/jenner2c/jec_rfoot.erf 2667B
+A mechs/jenner2c/jec_rfoot_dam.erf 2667B
+A mechs/jenner2c/jec_rftoe.erf 3599B
+A mechs/jenner2c/jec_rftoe_dam.erf 3599B
+A mechs/jenner2c/jec_rgun.erf 5943B
+A mechs/jenner2c/jec_rgun_dam.erf 6287B
+A mechs/jenner2c/jec_ruleg.erf 13003B
+A mechs/jenner2c/jec_ruleg_dam.erf 13003B
+A mechs/jenner2c/jec_torso.erf 39244B
+A mechs/jenner2c/jec_torso_dam.erf 39244B
+A mechs/jenner2c/jenner_2c_cage.erf 13103B
+A mechs/jenner2c/runninglights.erf 3062B
+A mechs/jenner2c_destroyed/jenner2c_destroyed.erf 11866B
+A mechs/marauder/mar_hip.erf 22691B
+A mechs/marauder/mar_hip_dam.erf 6571B
+A mechs/marauder/mar_lbtoe.erf 7962B
+A mechs/marauder/mar_ldleg.erf 39714B
+A mechs/marauder/mar_ldleg_dam.erf 11612B
+A mechs/marauder/mar_lfoot.erf 9492B
+A mechs/marauder/mar_lfoot_dam.erf 2755B
+A mechs/marauder/mar_lgun.erf 18985B
+A mechs/marauder/mar_litoe.erf 4394B
+A mechs/marauder/mar_litoe_dam.erf 1345B
+A mechs/marauder/mar_lotoe.erf 4394B
+A mechs/marauder/mar_luarm.erf 11161B
+A mechs/marauder/mar_luarm_dam.erf 9173B
+A mechs/marauder/mar_luleg.erf 8672B
+A mechs/marauder/mar_luleg_dam.erf 2499B
+A mechs/marauder/mar_rbtoe.erf 7962B
+A mechs/marauder/mar_rdleg.erf 38879B
+A mechs/marauder/mar_rdleg_dam.erf 11676B
+A mechs/marauder/mar_rfoot.erf 9594B
+A mechs/marauder/mar_rfoot_dam.erf 2755B
+A mechs/marauder/mar_rgun.erf 18341B
+A mechs/marauder/mar_ritoe.erf 4394B
+A mechs/marauder/mar_ritoe_dam.erf 1345B
+A mechs/marauder/mar_rotoe.erf 4394B
+A mechs/marauder/mar_rotoe_dam.erf 1345B
+A mechs/marauder/mar_ruarm.erf 11008B
+A mechs/marauder/mar_ruarm_dam.erf 5875B
+A mechs/marauder/mar_ruleg.erf 8672B
+A mechs/marauder/mar_ruleg_dam.erf 2499B
+A mechs/marauder/mar_specialone.erf 21560B
+A mechs/marauder/mar_specialone_dam.erf 7325B
+A mechs/marauder/mar_specialtwo.erf 15696B
+A mechs/marauder/mar_specialtwo_dam.erf 4731B
+A mechs/marauder/mar_torso.erf 54047B
+A mechs/marauder/mar_torso_dam.erf 13479B
+A mechs/marauder/marauder_cage.erf 22678B
+A mechs/marauder/runninglights.erf 541B
+A mechs/marauder_destroyed/marauder_destroyed.erf 18503B
+A mechs/thunderbolt/runninglights.erf 3062B
+A mechs/thunderbolt/thu_hip.erf 5811B
+A mechs/thunderbolt/thu_hip_dam.erf 5811B
+A mechs/thunderbolt/thu_lbtoe.erf 2723B
+A mechs/thunderbolt/thu_ldleg.erf 9171B
+A mechs/thunderbolt/thu_ldleg_dam.erf 9171B
+A mechs/thunderbolt/thu_lfoot.erf 1441B
+A mechs/thunderbolt/thu_lfoot_dam.erf 1441B
+A mechs/thunderbolt/thu_lftoe.erf 2555B
+A mechs/thunderbolt/thu_lftoe_dam.erf 2555B
+A mechs/thunderbolt/thu_lgun.erf 10687B
+A mechs/thunderbolt/thu_luarm.erf 2569B
+A mechs/thunderbolt/thu_luarm_dam.erf 2569B
+A mechs/thunderbolt/thu_luleg.erf 6794B
+A mechs/thunderbolt/thu_luleg_dam.erf 6794B
+A mechs/thunderbolt/thu_rbtoe.erf 2723B
+A mechs/thunderbolt/thu_rbtoe_dam.erf 2723B
+A mechs/thunderbolt/thu_rdleg.erf 9171B
+A mechs/thunderbolt/thu_rdleg_dam.erf 9171B
+A mechs/thunderbolt/thu_rfoot.erf 1441B
+A mechs/thunderbolt/thu_rfoot_dam.erf 1441B
+A mechs/thunderbolt/thu_rftoe.erf 2555B
+A mechs/thunderbolt/thu_rftoe_dam.erf 2555B
+A mechs/thunderbolt/thu_rgun.erf 12097B
+A mechs/thunderbolt/thu_rgun_dam.erf 12097B
+A mechs/thunderbolt/thu_ruarm.erf 2569B
+A mechs/thunderbolt/thu_ruarm_dam.erf 2569B
+A mechs/thunderbolt/thu_ruleg.erf 6794B
+A mechs/thunderbolt/thu_ruleg_dam.erf 6794B
+A mechs/thunderbolt/thu_specialone.erf 4371B
+A mechs/thunderbolt/thu_torso.erf 12695B
+A mechs/thunderbolt/thu_torso_dam.erf 12695B
+A mechs/thunderbolt/thunderbolt_cage.erf 20919B
+A mechs/thunderbolt_destroyed/thunderbolt_destroyed.erf 12593B
+A textures/@achp0.tga 1048594B
+A textures/@achp0.tga{hint} 4B
+A textures/@achp1.tga 262162B
+A textures/@achp1.tga{hint} 4B
+A textures/@achp2.tga 65554B
+A textures/@achp2.tga{hint} 4B
+A textures/@achp3.tga 16402B
+A textures/@achp3.tga{hint} 4B
+A textures/@achp4.tga 4114B
+A textures/@achp4.tga{hint} 4B
+A textures/@achp5.tga 1042B
+A textures/@achp5.tga{hint} 4B
+A textures/@agrf0.tga 1048594B
+A textures/@agrf0.tga{hint} 4B
+A textures/@agrf1.tga 262162B
+A textures/@agrf1.tga{hint} 4B
+A textures/@agrf2.tga 65554B
+A textures/@agrf2.tga{hint} 4B
+A textures/@agrf3.tga 16402B
+A textures/@agrf3.tga{hint} 4B
+A textures/@agrf4.tga 4114B
+A textures/@agrf4.tga{hint} 4B
+A textures/@agrf5.tga 1042B
+A textures/@agrf5.tga{hint} 4B
+A textures/@ajec0.tga 1048594B
+A textures/@ajec0.tga{hint} 4B
+A textures/@ajec1.tga 262162B
+A textures/@ajec1.tga{hint} 4B
+A textures/@ajec2.tga 65554B
+A textures/@ajec2.tga{hint} 4B
+A textures/@ajec3.tga 16402B
+A textures/@ajec3.tga{hint} 4B
+A textures/@ajec4.tga 4114B
+A textures/@ajec4.tga{hint} 4B
+A textures/@ajec5.tga 1042B
+A textures/@ajec5.tga{hint} 4B
+A textures/@amar0.tga 926350B
+A textures/@amar0.tga{hint} 4B
+A textures/@amar1.tga 247836B
+A textures/@amar1.tga{hint} 4B
+A textures/@amar2.tga 66075B
+A textures/@amar2.tga{hint} 4B
+A textures/@amar3.tga 16923B
+A textures/@amar3.tga{hint} 4B
+A textures/@amar4.tga 4635B
+A textures/@amar4.tga{hint} 4B
+A textures/@amar5.tga 1563B
+A textures/@amar5.tga{hint} 4B
+A textures/@athu0.tga 1048594B
+A textures/@athu0.tga{hint} 4B
+A textures/@athu1.tga 262162B
+A textures/@athu1.tga{hint} 4B
+A textures/@athu2.tga 65554B
+A textures/@athu2.tga{hint} 4B
+A textures/@athu3.tga 16402B
+A textures/@athu3.tga{hint} 4B
+A textures/@athu4.tga 4114B
+A textures/@athu4.tga{hint} 4B
+A textures/@athu5.tga 1042B
+A textures/@athu5.tga{hint} 4B
+A textures/footsteps/champion_default.tga 16428B
+A textures/footsteps/champion_default.tga{hint} 4B
+A textures/footsteps/champion_dirt.tga 16923B
+A textures/footsteps/champion_dirt.tga{hint} 4B
+A textures/footsteps/champion_snow.tga 16923B
+A textures/footsteps/champion_snow.tga{hint} 4B
+A textures/footsteps/griffin_default.tga 16428B
+A textures/footsteps/griffin_default.tga{hint} 4B
+A textures/footsteps/griffin_dirt.tga 16923B
+A textures/footsteps/griffin_dirt.tga{hint} 4B
+A textures/footsteps/griffin_snow.tga 16923B
+A textures/footsteps/griffin_snow.tga{hint} 4B
+A textures/footsteps/jenner2c_default.tga 16428B
+A textures/footsteps/jenner2c_default.tga{hint} 4B
+A textures/footsteps/jenner2c_dirt.tga 16428B
+A textures/footsteps/jenner2c_dirt.tga{hint} 4B
+A textures/footsteps/jenner2c_snow.tga 16428B
+A textures/footsteps/jenner2c_snow.tga{hint} 4B
+A textures/footsteps/marauder_default.tga 16923B
+A textures/footsteps/marauder_default.tga{hint} 4B
+A textures/footsteps/marauder_dirt.tga 16923B
+A textures/footsteps/marauder_dirt.tga{hint} 4B
+A textures/footsteps/marauder_snow.tga 16923B
+A textures/footsteps/marauder_snow.tga{hint} 4B
+A textures/footsteps/thunderbolt_default.tga 16428B
+A textures/footsteps/thunderbolt_default.tga{hint} 4B
+A textures/footsteps/thunderbolt_dirt.tga 16428B
+A textures/footsteps/thunderbolt_dirt.tga{hint} 4B
+A textures/footsteps/thunderbolt_snow.tga 16428B
+A textures/footsteps/thunderbolt_snow.tga{hint} 4B
~ textures/stockdecals/decal_49.tga A=16428B B=16428B
+81
View File
@@ -0,0 +1,81 @@
# variant packages: 797 (files on disk: 797)
# chassis defined in V4H core.mw4 : 71
# chassis defined in OUR core.mw4 : 65
# chassis only V4H defines : ['champion', 'dasher', 'griffin', 'jenner_2c', 'marauder', 'thunderbolt']
chassis count status
warhammer 36 loadable in OUR build today
rifleman 30 loadable in OUR build today
nova 24 loadable in OUR build today
atlas 23 loadable in OUR build today
madcat 23 loadable in OUR build today
thunderbolt 22 needs the new chassis ported
archer 21 loadable in OUR build today
battlemaster 21 loadable in OUR build today
hunchback 21 loadable in OUR build today
thor 20 loadable in OUR build today
vulture 19 loadable in OUR build today
zeus 19 loadable in OUR build today
ryoken 18 loadable in OUR build today
shadowcat 18 loadable in OUR build today
catapult 17 loadable in OUR build today
dragon 17 loadable in OUR build today
griffin 15 needs the new chassis ported
loki 15 loadable in OUR build today
marauder 15 needs the new chassis ported
commando 13 loadable in OUR build today
highlander 13 loadable in OUR build today
templar 13 loadable in OUR build today
wolfhound 13 loadable in OUR build today
avatar 12 loadable in OUR build today
awesome 12 loadable in OUR build today
battlemaster2c 12 loadable in OUR build today
victor 12 loadable in OUR build today
annihilator 11 loadable in OUR build today
daishi 11 loadable in OUR build today
gladiator 11 loadable in OUR build today
puma 11 loadable in OUR build today
raven 11 loadable in OUR build today
sunder 11 loadable in OUR build today
uller 11 loadable in OUR build today
dasher 10 needs the new chassis ported
owens 10 loadable in OUR build today
blacklanner 9 loadable in OUR build today
cougar 9 loadable in OUR build today
kodiak 9 loadable in OUR build today
masakari 9 loadable in OUR build today
urbanmech 9 loadable in OUR build today
arcticwolf 8 loadable in OUR build today
blacknight 8 loadable in OUR build today
cauldronborn 8 loadable in OUR build today
hellhound 8 loadable in OUR build today
hollander 8 loadable in OUR build today
longbow 8 loadable in OUR build today
novacat 8 loadable in OUR build today
assassin2 7 loadable in OUR build today
cyclops 7 loadable in OUR build today
flea 7 loadable in OUR build today
hauptmann 7 loadable in OUR build today
mauler 7 loadable in OUR build today
behemoth2 6 loadable in OUR build today
brigand 6 loadable in OUR build today
bushwacker 6 loadable in OUR build today
champion 5 needs the new chassis ported
deimos 5 loadable in OUR build today
hellspawn 5 loadable in OUR build today
madcat_mkii 5 loadable in OUR build today
thanatos 5 loadable in OUR build today
uziel 5 loadable in OUR build today
argus 4 loadable in OUR build today
fafnir 4 loadable in OUR build today
jenner_iic1 3 BROKEN - chassis missing from V4H too
osiris 3 loadable in OUR build today
behemoth 2 loadable in OUR build today
chimera 2 loadable in OUR build today
grizzly 2 loadable in OUR build today
solitaire 2 loadable in OUR build today
loadable in our build today : 727
blocked on new chassis : 67
broken / orphaned : 3
orphan chassis 'jenner_iic1': Variants/Jenner2c 1.mw4, Variants/Jenner2c 4.mw4, Variants/Jenner2c Pharaoh.mw4
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# run-comparison.sh - regenerate every report in MW4COMPARE/reports/.
#
# ./run-comparison.sh [V4H_ROOT] [OURS_ROOT]
#
# Defaults:
# V4H_ROOT = /home/rich/Repositories/FS_Build_V4H (deployed 3rd-party build)
# OURS_ROOT = <repo>/Gameleap/mw4 (our dev/source tree)
#
# Takes a few minutes; the decoded props/textures comparisons dominate.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
T="$HERE/tools"
R="$HERE/reports"
V4H="${1:-/home/rich/Repositories/FS_Build_V4H}"
OURS="${2:-$(cd "$HERE/.." && pwd)/Gameleap/mw4}"
mkdir -p "$R"
echo "V4H = $V4H"
echo "OURS = $OURS"
echo "[1/8] package manifests"
python3 "$T/mw4index.py" "$V4H/resource" > "$R/manifest-v4h.tsv" 2> "$R/manifest-v4h.err"
python3 "$T/mw4index.py" "$OURS/Resource" > "$R/manifest-ours.tsv" 2> "$R/manifest-ours.err"
echo "[2/8] package + entry name diff"
python3 "$T/diffindex.py" "$R/manifest-v4h.tsv" "$R/manifest-ours.tsv" V4H OURS \
> "$R/packages-entrydiff.txt"
echo "[3/8] core.mw4 decoded diff"
python3 "$T/pkgcmp.py" "$V4H/resource/core.mw4" "$OURS/Resource/core.mw4" \
> "$R/core-decoded-diff.txt"
echo "[4/8] props.mw4 decoded diff (~1 min)"
python3 "$T/pkgcmp.py" "$V4H/resource/props.mw4" "$OURS/Resource/props.mw4" \
> "$R/props-decoded-diff.txt"
echo "[5/8] textures.mw4 decoded diff (~2 min)"
python3 "$T/pkgcmp.py" "$V4H/resource/textures.mw4" "$OURS/Resource/textures.mw4" \
> "$R/textures-decoded-diff.txt"
echo "[6/8] per-mission and per-map decoded diffs"
: > "$R/missions-decoded-diff.txt"
for f in "$V4H"/resource/Missions/*.mw4; do
b="$(basename "$f")"; o="$OURS/Resource/Missions/$b"
[ -f "$o" ] || continue
{ echo "### $b"; python3 "$T/pkgcmp.py" "$f" "$o"; } >> "$R/missions-decoded-diff.txt"
done
: > "$R/maps-decoded-diff.txt"
for f in "$V4H"/resource/maps/*.mw4; do
b="$(basename "$f")"; o="$OURS/Resource/maps/$b"
[ -f "$o" ] || continue
{ echo "### $b"; python3 "$T/pkgcmp.py" "$f" "$o"; } >> "$R/maps-decoded-diff.txt"
done
echo "[7/8] loose file trees"
python3 "$T/treediff.py" "$V4H/hsh" "$OURS/hsh" V4H OURS > "$R/hsh-diff.txt"
python3 "$T/treediff.py" "$V4H/hshoriginal" "$OURS/hsh" V4HORIG OURS > "$R/hshoriginal-diff.txt"
python3 "$T/treediff.py" "$V4H/resource" "$OURS/Resource" V4H OURS > "$R/resource-files-diff.txt"
# V4H ships only 14 loose Content files against our ~46k-file source tree, so the
# "-OURS" side is pure noise here; keep only the header, additions and conflicts.
python3 "$T/treediff.py" "$V4H/content" "$OURS/Content" V4H OURS \
| grep -v '^-OURS' > "$R/content-diff.txt"
echo "[8/8] variant inventory"
python3 "$T/variants-report.py" "$V4H/resource/Variants" "$R/manifest-v4h.tsv" "$R/manifest-ours.tsv" \
> "$R/variants-inventory.txt"
echo "done -> $R"
+1
View File
@@ -0,0 +1 @@
__pycache__/
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
classify-survivors.py - split a pruned extracted tree into NEW vs DIFFERS.
python3 classify-survivors.py <prunedExtractedDir>
Run after prune-identical.py. Everything still present is something we do not
have byte-for-byte; this reports which of two reasons applies:
NEW no file at that entry path exists on our side at all
DIFFERS we have that entry path, but the bytes differ
"Our side" means our extracted packages OR our Content/ source tree, matched
case-insensitively - the same notion of "have it" the pruner uses.
Writes _classified.tsv next to the tree and prints a summary.
"""
import sys, os, collections
OURS_EXTRACTED = "/home/rich/Repositories/FS_Ours_extracted"
OUR_SOURCE = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content"
def index_paths(root, skip_top=()):
out = {}
for dp, dirs, fs in os.walk(root):
if dp == root:
dirs[:] = [d for d in dirs if d.lower() not in skip_top]
for f in fs:
p = os.path.join(dp, f)
out[os.path.relpath(p, root).replace("\\", "/").lower()] = p
return out
def package_of(rel):
parts = rel.split("/")
head = parts[0].lower()
if head in ("core", "props", "textures"):
return parts[0], "/".join(parts[1:])
if head in ("maps", "missions", "variants") and len(parts) > 2:
return "/".join(parts[:2]), "/".join(parts[2:])
if head == "pilots" and len(parts) > 3:
return "/".join(parts[:3]), "/".join(parts[3:])
return parts[0], "/".join(parts[1:])
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
root = sys.argv[1]
merged = os.path.join(root, "_merged")
ours = index_paths(OURS_EXTRACTED, skip_top={"_merged"})
ours_entries = set()
for rel in ours:
ours_entries.add(package_of(rel)[1])
src_entries = set(index_paths(OUR_SOURCE))
rows, summary = [], collections.Counter()
for dp, _dirs, fs in os.walk(root):
if dp == merged or dp.startswith(merged + os.sep):
continue
for f in fs:
rel = os.path.relpath(os.path.join(dp, f), root).replace("\\", "/")
if rel.startswith("_"):
continue
pkg, entry = package_of(rel)
e = entry.lower()
verdict = "DIFFERS" if (rel.lower() in ours or e in ours_entries
or e in src_entries) else "NEW"
rows.append((verdict, rel, pkg, entry))
summary[(verdict, pkg.split("/")[0])] += 1
with open(os.path.join(root, "_classified.tsv"), "w", encoding="utf-8") as fh:
fh.write("verdict\tpath\tpackage\tentry\n")
for r in sorted(rows):
fh.write("\t".join(r) + "\n")
tot = collections.Counter(v for v, _, _, _ in rows)
print(f"survivors: {len(rows)} NEW={tot['NEW']} DIFFERS={tot['DIFFERS']}\n")
print(f"{'package group':16s} {'NEW':>7s} {'DIFFERS':>8s}")
groups = sorted({g for _v, g in summary})
for g in groups:
print(f"{g:16s} {summary[('NEW', g)]:7d} {summary[('DIFFERS', g)]:8d}")
print(f"\n-> {os.path.join(root, '_classified.tsv')}")
if __name__ == "__main__":
main()
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
armature.py - rebuild a mech's .armature source from its packed records.
python3 armature.py <mechRecordDir> [-o out.armature]
python3 armature.py --verify # check against all 65 known chassis
The packer merges <mech>.armature into <mech>.contents (via `!include=`) and
then, for every contents page that has Child= entries, emits two records
(MWMover_Tool.cpp ~78):
<mech>.contents[<joint>]{sites} children whose name starts 'site_'
but is not 'site_eye*' - stored as
YawPitchRoll + Point3D + name
<mech>.contents[<joint>]{armature} every other child, as a CreateMessage
carrying jointName + localToParent
Between them those two records hold every page of the original .armature.
Verified on Atlas: 61 of 61 page names recovered, values exact.
Rotation notes
--------------
* {sites} stores YawPitchRoll directly, so those angles are exact.
* Joint messages only carry a matrix. Across all 65 chassis, 1348 of 1453 joint
matrices are the identity, i.e. `Rotation=0 0 0`.
* site_lfoot / site_rfoot appear in BOTH streams - a deliberate hack in
MWMover_Tool.cpp ("Jerry this will get deleted when you fix your foot
problem") - and their matrix is the identity while the real angle is in the
{sites} record. The {sites} value always wins.
* site_eye* goes only to the armature stream, so its angle comes from the
matrix.
"""
import sys, os, glob, math, struct, re, argparse, collections
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mw4msg
OUR_MECH_SOURCE = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs"
OUR_RECORDS = "/home/rich/Repositories/FS_Ours_extracted/core/mechs"
REC_RE = re.compile(r'^(?P<chassis>.+?)\.contents\[(?P<parent>[^\]]+)\]\{(?P<kind>armature|sites)\}$',
re.I)
def ypr_from_matrix(r):
"""3x3 -> the (a, b, c) triple as written in Rotation=, in degrees.
Stuff's YawPitchRoll writes the X-axis angle in the middle slot; confirmed
against kodiak/site_eyepoint (matrix gives 0.0688 deg, source says
0.069104).
"""
m00, m01, m02, m10, m11, m12, m20, m21, m22 = r
pitch = math.asin(max(-1.0, min(1.0, m21)))
if abs(m21) < 0.999999:
yaw = math.atan2(-m20, m22)
roll = math.atan2(-m01, m11)
else:
yaw = math.atan2(m02, m00)
roll = 0.0
return (math.degrees(yaw), math.degrees(pitch), math.degrees(roll))
def is_identity(r, eps=1e-5):
ident = (1, 0, 0, 0, 1, 0, 0, 0, 1)
return all(abs(a - b) < eps for a, b in zip(r, ident))
def rebuild(mech_dir):
"""-> (chassisName, entries, children)
entries list of {'name','parent','rot','trans'} - one per armature page.
Keyed on (parent, name), not name alone: Victor legitimately has
two different [site_lshellport] pages hanging off different joints.
children {parentName: [childName, ...]} preserving order and multiplicity.
"""
entries = [] # one dict per armature page occurrence
children = collections.defaultdict(list)
site_children = collections.defaultdict(set)
chassis = None
# {sites} first: site_lfoot/site_rfoot are written to BOTH streams and only
# the {sites} copy carries their real angle.
records = (sorted(glob.glob(os.path.join(mech_dir, "*{sites}"))) +
sorted(glob.glob(os.path.join(mech_dir, "*{armature}"))))
for path in records:
m = REC_RE.match(os.path.basename(path))
if not m:
continue
chassis = chassis or m.group("chassis")
parent, kind = m.group("parent"), m.group("kind").lower()
with open(path, "rb") as fh:
data = fh.read()
children[parent] # ensure the parent exists
if kind == "sites":
for name, rot, trans in mw4msg.read_sites(data):
entries.append({
"name": name, "parent": parent,
"rot": tuple(math.degrees(a) for a in rot), "trans": trans})
children[parent].append(name)
site_children[parent].add(name)
else:
for _off, msg in mw4msg.walk(data):
name = mw4msg.joint_name(msg)
if not name:
continue
# site_lfoot/site_rfoot are deliberately written to both streams;
# counting the armature copy too would double them.
if name in site_children[parent]:
continue
children[parent].append(name)
r = mw4msg.rotation3x3(msg)
entries.append({
"name": name, "parent": parent,
"rot": (0.0, 0.0, 0.0) if is_identity(r) else ypr_from_matrix(r),
"trans": mw4msg.translation(msg)})
return chassis, entries, children
def emit(entries, children):
"""Render as .armature text, children before the joint that owns them."""
by_name = {}
for e in entries:
by_name.setdefault(e["name"], []).append(e)
out, done = [], set()
def visit(name):
if name in done:
return
done.add(name)
for c in children.get(name, ()):
visit(c)
for e in by_name.get(name, [{"rot": (0.0, 0.0, 0.0), "trans": (0.0, 0.0, 0.0)}]):
out.append(f"[{name}]")
out.append("Rotation=%.6f %.6f %.6f" % (e["rot"] or (0.0, 0.0, 0.0)))
out.append("Translation=%.6f %.6f %.6f" % (e["trans"] or (0.0, 0.0, 0.0)))
for c in children.get(name, ()):
out.append(f"Child={c}")
out.append("")
all_children = {c for lst in children.values() for c in lst}
for name in list(children) + list(by_name):
if name not in all_children:
visit(name)
for name in list(children) + list(by_name):
visit(name)
return "\r\n".join(out) + "\r\n"
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("mechdir", nargs="?", help="directory of packed records for one mech")
ap.add_argument("-o", "--out", help="write .armature here instead of stdout")
ap.add_argument("--verify", action="store_true",
help="rebuild all 65 known chassis and diff against their source")
args = ap.parse_args()
if args.verify:
here = os.path.dirname(os.path.abspath(__file__))
os.execvp("python3", ["python3", os.path.join(here, "verify_armature.py")])
if not args.mechdir:
ap.error("give a mech record directory, or --verify")
chassis, entries, children = rebuild(args.mechdir)
text = emit(entries, children)
if args.out:
os.makedirs(os.path.dirname(args.out), exist_ok=True)
open(args.out, "wb").write(text.encode("latin-1"))
print(f"{chassis}: {len(entries)} pages -> {args.out}")
else:
sys.stdout.write(text)
if __name__ == "__main__":
main()
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""
armature_parts.py - generate a mech's `armaturedata/` and `armaturevideo/` files.
Every joint in a mech's `.contents` points at `armaturedata\\<part>.data`, which
in turn points at `armaturevideo\\<part>.video`, which names the geometry. Both
are packed as compiled records -- the `.data` becomes a 12-byte stub and the
`.video` becomes a binary renderer graph -- so neither survives extraction as
text and both have to be regenerated.
They are formulaic. Measured over the 89 shipped mechs:
* `armaturedata/<part>.data` -- **one single shape** across all 1336 files,
parameterised only by the part name.
* `armaturevideo/<part>.video` -- three shapes cover 91% of 1354 files:
with a damaged LOD, without one, and with a running-lights block.
Rules, and their measured accuracy:
* A `DAMLOD` block iff `<part>_dam.erf` exists next to the mech. This holds for
1327 of 1354 shipped files. **All 27 exceptions run the same way**: the
`_dam.erf` exists but the author's `.video` ignores it. No shipped `.video`
ever references a `_dam.erf` that is absent, so generating by this rule is
safe by construction -- it can never produce a dangling reference, only
occasionally more damage-state switching than the original author chose.
The exceptions are mostly `_gun` and `_toe` parts and follow no clean rule.
* A running-lights block iff the part is the torso and a running-lights `.erf`
is present. 68 of the 71 torsos carry it; almost nothing else does.
python3 armature_parts.py <mech-source-dir>
python3 armature_parts.py --verify
"""
import argparse, os, re, sys, collections
DATA_TEMPLATE = """[gamedata]
Class=MechWarrior4::MWMover
[renderers]
VideoRenderer=armaturevideo\\{part}.video
"""
LIGHT_BLOCK = """[lightlod]
Type=ShapeComponent
Geometry={lights}
Unique=true
[lightwatcher]
Type=AttributeWatcherOfInt
Attribute=IsDark
SimulationShouldExecute=1
[lightswitch]
Type=SwitchComponent
Input=LightWatcher
Child=LightLOD
"""
LOD_BLOCK = """[lod]
Type=ShapeComponent
Geometry={part}.erf
Unique=true
"""
DAM_BLOCK = """[damlod]
Type=ShapeComponent
Geometry={part}_DAM.erf
Unique=true
"""
# joint_cage has its own shape: two empty group components, then the cage
# geometry twice (intact and destroyed), shadow-disabled.
CAGE_TEMPLATE = """[fake1]
Type=GroupComponent
[fake2]
Type=GroupComponent
[cage]
Type=ShapeComponent
Geometry={cage}
Unique=true
DisableShadow=true
[destroyedcage]
Type=ShapeComponent
Geometry={cage}
Unique=true
DisableShadow=true
[watcher]
Type=AttributeWatcherOfInt
Attribute=VisualRepresentation
SimulationShouldExecute=1
[damageappearance]
Type=SwitchComponent
Input=Watcher
Child=Fake1
Child=Fake2
Child=Cage
Child=DestroyedCage
[locator]
Child=DamageAppearance
"""
def parts_of(mech_dir):
"""Part names referenced as armaturedata\\<part>.data by the .contents."""
out = []
for fn in os.listdir(mech_dir):
if not fn.lower().endswith(".contents"):
continue
t = open(os.path.join(mech_dir, fn), "rb").read().decode("latin-1")
for m in re.finditer(r'armaturedata[\\/]([A-Za-z0-9_\-]+)\.data', t, re.I):
if m.group(1) not in out:
out.append(m.group(1))
return out
def video_text(mech_dir, part):
files = {f.lower(): f for f in os.listdir(mech_dir)}
cage = next((files[f] for f in files if f.endswith("_cage.erf")), None)
if part.lower() == "joint_cage" and cage:
return CAGE_TEMPLATE.format(cage=cage)
has_dam = f"{part.lower()}_dam.erf" in files
lights = next((files[f] for f in files
if f.startswith("runninglights") and f.endswith(".erf")), None)
is_torso = part.lower().endswith("torso")
text = ""
if lights and is_torso:
text += LIGHT_BLOCK.format(lights=lights)
text += LOD_BLOCK.format(part=part)
if has_dam:
text += DAM_BLOCK.format(part=part)
text += """[watcher]
Type=AttributeWatcherOfInt
Attribute=VisualRepresentation
SimulationShouldExecute=1
[damageappearance]
Type=SwitchComponent
Input=Watcher
Child=LOD
"""
if has_dam:
text += "Child=DAMLOD\n"
text += "\n[locator]\n"
if lights and is_torso:
text += "Child=LightSwitch\n"
text += "Child=DamageAppearance\n"
return text
def generate(mech_dir, write=True):
"""-> {relativePath: text} for every part."""
out = {}
for part in parts_of(mech_dir):
out[f"armaturedata/{part}.data"] = DATA_TEMPLATE.format(part=part)
out[f"armaturevideo/{part}.video"] = video_text(mech_dir, part)
if write:
for rel, text in out.items():
p = os.path.join(mech_dir, rel)
os.makedirs(os.path.dirname(p), exist_ok=True)
with open(p, "wb") as fh:
fh.write(text.replace("\n", "\r\n").encode("latin-1"))
return out
def norm(text, part):
t = re.sub(r'//[^\n]*', '', text).replace("\r", "")
t = re.sub(re.escape(part), "PART", t, flags=re.I)
return re.sub(r'\n+', "\n", t).strip().lower()
def verify():
import glob
MECHS = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs"
t = collections.Counter()
diffs = collections.Counter()
for d in sorted(glob.glob(MECHS + "/*")):
if not os.path.isdir(d) or not glob.glob(d + "/armaturedata/*.data"):
continue
gen = generate(d, write=False)
for rel, text in gen.items():
p = os.path.join(d, rel)
part = os.path.basename(rel).rsplit(".", 1)[0]
kind = rel.split("/")[0]
if not os.path.exists(p):
t[kind + " absent"] += 1
continue
t[kind] += 1
want = open(p, "rb").read().decode("latin-1")
if norm(want, part) == norm(text, part):
t[kind + " ok"] += 1
else:
diffs[kind] += 1
print(f"armaturedata : {t['armaturedata ok']}/{t['armaturedata']} reproduced")
print(f"armaturevideo: {t['armaturevideo ok']}/{t['armaturevideo']} reproduced")
if t["armaturedata absent"] or t["armaturevideo absent"]:
print(f" referenced but absent on disk: "
f"{t['armaturedata absent']} data, {t['armaturevideo absent']} video")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("mech_dir", nargs="?")
ap.add_argument("--verify", action="store_true")
args = ap.parse_args()
if args.verify:
verify()
return
if not args.mech_dir:
ap.error("mech_dir required")
out = generate(args.mech_dir)
print(f"wrote {len(out)} files into {args.mech_dir}")
if __name__ == "__main__":
main()
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""
assembly.py - show how a mech's parts bolt together.
There is no `.erf` viewer on Linux, but the assembly itself is not hidden: the
`.armature` gives every joint's parent, offset and rotation, and the `.contents`
plus `armaturevideo/` say which geometry hangs off each joint. Printing that as
a tree answers "what are the parts and how do they fit" without a renderer.
For actual 3D you need MW4Ed2 on the Windows box (Gameleap/mw4/run-editor.bat) --
its Game View renders a loaded mech through DDrawCompat.
python3 assembly.py <mech-source-dir> [--geometry-only]
"""
import argparse, collections, os, re, sys
def pages(path):
"""-> OrderedDict pageName -> [(key, value)]"""
txt = re.sub(r'//[^\n]*', '', open(path, "rb").read().decode("latin-1"))
out, cur = collections.OrderedDict(), None
for line in txt.splitlines():
line = line.strip()
if not line or line.startswith("!"):
continue
m = re.match(r'^\[([^\]]+)\]$', line)
if m:
cur = m.group(1)
out.setdefault(cur, [])
elif "=" in line and cur is not None:
k, v = line.split("=", 1)
out[cur].append((k.strip(), v.strip()))
return out
def find(mech_dir, ext):
for f in sorted(os.listdir(mech_dir)):
if f.lower().endswith(ext) and "}" not in f:
return os.path.join(mech_dir, f)
return None
def geometry_for(mech_dir, part):
"""The .erf a joint draws, via armaturevideo/<part>.video.
Takes the [lod] block specifically -- a torso's file starts with the
running-lights block, whose geometry is not the part.
"""
vid = os.path.join(mech_dir, "armaturevideo", part + ".video")
if not os.path.exists(vid):
return None
t = open(vid, "rb").read().decode("latin-1")
m = re.search(r'^\[lod\][^\[]*?Geometry=([^\r\n]+)', t, re.M | re.I | re.S)
if not m:
m = re.search(r'Geometry=([^\r\n]+)', t)
return m.group(1).strip() if m else None
def build(mech_dir):
arm = find(mech_dir, ".armature")
con = find(mech_dir, ".contents")
if not arm:
raise SystemExit(f"no .armature in {mech_dir}")
apages = pages(arm)
children = collections.defaultdict(list)
info = {}
for name, kv in apages.items():
d = dict(kv)
info[name] = d
for k, v in kv:
if k.lower() == "child":
children[name].append(v.strip())
models = {}
if con:
for name, kv in pages(con).items():
for k, v in kv:
if k.lower() == "model":
models[name.lower()] = v.strip()
parented = {c for cs in children.values() for c in cs}
roots = [n for n in apages if n not in parented]
return apages, children, info, models, roots
def render(mech_dir, geometry_only=False):
apages, children, info, models, roots = build(mech_dir)
sizes = {f.lower(): os.path.getsize(os.path.join(mech_dir, f))
for f in os.listdir(mech_dir) if f.lower().endswith(".erf")}
lines = []
def walk(name, depth, last, prefix):
d = info.get(name, {})
model = models.get(name.lower(), "")
part = None
m = re.match(r'armaturedata[\\/](.+)\.data', model, re.I)
if m:
part = m.group(1)
geo = geometry_for(mech_dir, part) if part else None
size = sizes.get((geo or "").lower())
tag = ""
if geo:
tag = f" <- {geo}" + (f" ({size:,} B)" if size else "")
elif model and model.lower() != "basic.data":
tag = f" <- {model}"
trans = d.get("Translation", d.get("translation", ""))
rot = d.get("Rotation", d.get("rotation", ""))
pos = f" @[{trans}]" if trans and trans.strip() not in ("0.0 0.0 0.0", "0 0 0") else ""
if geometry_only and not geo:
pass
else:
branch = "" if depth == 0 else ("`-- " if last else "|-- ")
lines.append(f"{prefix}{branch}{name}{tag}{pos}")
kids = children.get(name, [])
newprefix = prefix + ("" if depth == 0 else (" " if last else "| "))
for i, k in enumerate(kids):
walk(k, depth + 1, i == len(kids) - 1, newprefix)
for r in roots:
walk(r, 0, True, "")
return lines, sizes
def main():
ap = argparse.ArgumentParser()
ap.add_argument("mech_dir")
ap.add_argument("--geometry-only", action="store_true",
help="hide joints and sites that draw nothing")
args = ap.parse_args()
lines, sizes = render(args.mech_dir, args.geometry_only)
name = os.path.basename(args.mech_dir.rstrip("/"))
print(f"=== {name}: {len(lines)} nodes, {len(sizes)} .erf, "
f"{sum(sizes.values()):,} B of geometry ===")
print("\n".join(lines))
if __name__ == "__main__":
main()
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
constants.py - reverse tables for the `.data` keys authored as symbolic names.
Four `[GameData]` keys are written in the source as a symbol rather than a
literal, and the record stores only the resolved integer. Emitting a faithful
`.data` therefore needs the int -> symbol direction:
MechID $(M_Annihilator) MechLabHeaders.h #define M_Annihilator 0
TechType $(Tech_IS) MechLabHeaders.h #define Tech_IS 0
NameIndex $(IDS_Annihilator) MissionLang.defines #define IDS_ANNIHILATOR 576
MoveTypeFlag LEGJUMPMOVETYPE MWObject.hpp enum + MWObject_Tool.cpp:20
MoveTypeFlag is the odd one out: a bare token, not `$(...)`, matched by the
`stricmp` chain in `MWObject__GameModel::ConvertStringToMoveType`. Its values
come from the anonymous enum at MWObject.hpp:136, where LEG is 0 and the order
is NOT the same as the stricmp chain, so the enum is the authority.
The `.defines` and `.h` files spell symbols in mixed case while sources
reference them in any case, so lookups here are case-insensitive.
"""
import re
REPO = "/home/rich/Repositories/firestorm/Gameleap"
MECHLAB_HEADERS = REPO + "/mw4/Content/ShellScripts/MechLabHeaders.h"
MISSIONLANG_DEFINES = REPO + "/mw4/Content/Defines/MissionLang.defines"
# MWObject.hpp:136. Declaration order is the value order.
MOVE_TYPE = [
"LEGMOVETYPE", "LEGJUMPMOVETYPE", "TRACKMOVETYPE", "WHEELMOVETYPE",
"FLYERMOVETYPE", "HOVERMOVETYPE", "HELIMOVETYPE", "NONEMOVETYPE",
"DROPSHIPMOVETYPE", "WATERMOVETYPE",
]
_DEFINE = re.compile(r'^\s*#define\s+(\w+)\s+(-?\d+)\s*$', re.M)
def defines(path, prefix):
"""-> {value: [symbols]} for every `#define <prefix>NAME <int>` in a file.
Values are not unique: IDS_FIRSTSKIN and IDS_WOLFHOUND are both 501, so a
single winner cannot be picked here without knowing which mech is being
emitted.
"""
txt = open(path, encoding="latin-1", errors="replace").read()
out = {}
for name, val in _DEFINE.findall(txt):
if name.lower().startswith(prefix.lower()):
out.setdefault(int(val), []).append(name)
return out
def tables():
"""-> {sourceKey: {intValue: [sourceToken]}} for the four symbolic keys."""
wrap = lambda d: {v: [f"$({s})" for s in syms] for v, syms in d.items()}
return {
"MechID": wrap(defines(MECHLAB_HEADERS, "M_")),
"TechType": wrap(defines(MECHLAB_HEADERS, "Tech_")),
"NameIndex": wrap(defines(MISSIONLANG_DEFINES, "IDS_")),
"MoveTypeFlag": {i: [s] for i, s in enumerate(MOVE_TYPE)},
}
def symbol(tbl, key, value, hint=None):
"""Pick the source token for a value, preferring one naming the chassis."""
syms = tbl.get(key, {}).get(value)
if not syms:
return None
if hint and len(syms) > 1:
h = norm(hint)
for s in syms:
if h and h in norm(s):
return s
return syms[0]
def norm(tok):
"""Compare symbols ignoring case and $() wrapping."""
return re.sub(r'[^a-z0-9_]', '', tok.lower())
if __name__ == "__main__":
for key, tbl in tables().items():
print(f"{key:14s} {len(tbl):5d} values e.g. {list(tbl.items())[:2]}")
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""
contents.py - decompiler for a mech `<chassis>.contents` file.
`.contents` is the thin half of the pair `armature.py` already handles. It
`!include`s `<chassis>.armature` and then gives every joint and site exactly two
entries:
[joint_torso]
Model=basic.data
ExecutionState=AlwaysExecuteState
Both values ride in the same CreateMessages `armature.py` walks -- the `.armature`
source contributes a page's geometry, the `.contents` source contributes its
Model and ExecutionState, and the packer merges them into one message per page.
So nothing new has to be located: `Model` is `dataListID` (record id in the HIGH
word) and `ExecutionState` is the enum at offset 76.
Site pages are the exception. `{sites}` records store only name, rotation and
translation, so a site's Model and ExecutionState are not in the package at all.
They do not need to be: all **2926** site pages across the 89 mech `.contents`
files carry the identical pair `basic.data` / `AlwaysExecuteState`, so they are
emitted as constants rather than guessed per mech.
python3 contents.py <chassis-record-dir> [-o out.contents]
python3 contents.py --verify
"""
import argparse, collections, glob, os, re, struct, subprocess, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mw4msg, subsystems
MANIFEST = "/home/rich/Repositories/FS_Ours_extracted/_manifest.tsv"
REC_RE = re.compile(r'^(?P<chassis>.+?)\.contents\[(?P<parent>[^\]]+)\]\{(?P<kind>armature|sites)\}$',
re.I)
EXEC_STATE = {1: "NeverExecuteState", 2: "AlwaysExecuteState", 6: "ActiveState"}
SITE_DEFAULTS = [("Model", "basic.data"), ("ExecutionState", "AlwaysExecuteState")]
EXEC_OFF = 76
DATALIST_OFF = 84
def relative(path, chassis, folder=None):
"""Model= is written relative to the mech folder, not from the package root.
The package path uses the FOLDER name, which need not match the record stem
(V4H's jenner2c/ holds jenner_2c.* records), so both are tried.
"""
if not path:
return path
for name in (chassis, folder):
if name:
path = re.sub(rf'^mechs[\\/]{re.escape(name)}[\\/]', '', path, flags=re.I)
return path
def decompile(mech_dir, manifest=None):
"""-> (chassis, [(pageName, [(key, value)])])"""
manifest = manifest if manifest is not None else subsystems.load_manifest(MANIFEST)
chassis = None
folder = os.path.basename(mech_dir.rstrip("/"))
pages = collections.OrderedDict()
root = [p for p in glob.glob(os.path.join(mech_dir, "*.contents"))
if not re.search(r'[\[\{]', os.path.basename(p))]
for path in sorted(glob.glob(os.path.join(mech_dir, "*{armature}"))) + root:
base = os.path.basename(path)
m = REC_RE.match(base)
if m:
chassis = chassis or m.group("chassis")
else:
chassis = chassis or base[:-len(".contents")]
with open(path, "rb") as fh:
data = fh.read()
for _off, msg in mw4msg.walk(data):
name = mw4msg.joint_name(msg)
if not name:
continue
model = manifest.get(mw4msg.record_id(msg, DATALIST_OFF))
state = EXEC_STATE.get(struct.unpack_from("<i", msg, EXEC_OFF)[0])
pages.setdefault(name, [("Model", relative(model, chassis, folder)),
("ExecutionState", state)])
for path in sorted(glob.glob(os.path.join(mech_dir, "*{sites}"))):
with open(path, "rb") as fh:
for name, _rot, _trans in mw4msg.read_sites(fh.read()):
pages.setdefault(name, list(SITE_DEFAULTS))
return chassis, list(pages.items())
def emit(chassis, pages):
lines = ["[includes]", f"!include={chassis}.armature", ""]
for name, kv in pages:
lines.append(f"[{name}]")
lines.extend(f"{k}={v}" for k, v in kv)
lines.append("")
return "\r\n".join(lines)
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_contents.py")]))
if not args.mech_dir:
ap.error("mech_dir required")
chassis, pages = decompile(args.mech_dir, subsystems.load_manifest(args.manifest))
text = emit(chassis, pages)
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()
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""
damage.py - decompiler for a mech `<chassis>.damage` file.
The record is a bare concatenation of variable-length objects with no index and
no length prefixes: parsing means walking forward, reading a classID, and using
it to decide what follows. Written by `MWObject::CreateDamageStream`
(MWObject_Tool.cpp:1149), which iterates the source pages in order and dispatches
on whether a page carries a `DamageZone` entry.
Armour page -- DamageObject::ConstructDamageObjectStream (DamageObject.cpp:157):
classID, baseArmorValue, currentArmorValue, scaleSplashDamage,
damageObjectName (MString), internalDamageZoneID, armorZone, damageLevel,
armorType, maxArmorValue, attachedToZone
Internal page -- InternalDamageObject::ConstructInternalDamageObjectStream
(DamageObject.cpp:931) plus the MW4 subclass (MWDamageObject.cpp:88):
classID, baseInternalDamage, currentInternalDamage,
parentEntityName (MString), damageMode, damageZone, damagePropagationZone,
internalType, attachedTo, damageEffects[], missileSlots, projectileSlots,
beamSlots, omniSlots
MString is an int length NOT counting the terminator, the characters, then a NUL
-- the same encoding as the {FootSteps} stream.
Two traps:
* The armour page stores its own name, but the internal page does NOT. Internal
page names are reconstructed as `<Zone>Internal`, which every one of the 89
mech .damage files follows, using the damageZone that IS stored.
* ArmorZone and InternalZone are DIFFERENT enums. ArmorZone has
CenterRearTorso at 7 and Head at 8; InternalZone has Head at 7 and no rear
torso entry. Conflating them silently mislabels head and torso zones.
python3 damage.py <chassis-record-dir> [-o out.damage]
python3 damage.py --verify
"""
import argparse, collections, os, re, struct, subprocess, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import subsystems
MANIFEST = "/home/rich/Repositories/FS_Ours_extracted/_manifest.tsv"
ARMOR_CLASS = 468 # Adept::DamageObject
INTERNAL_CLASS = 1162 # MechWarrior4::MWInternalDamageObject
# DamageObject.hpp:363 -- note CenterRearTorso, absent from the internal enum.
ARMOR_ZONE = {
-1: "NullZone", 0: "LeftLeg", 1: "RightLeg", 2: "LeftArm", 3: "RightArm",
4: "RightTorso", 5: "LeftTorso", 6: "CenterTorso", 7: "CenterRearTorso",
8: "Head", 9: "Special1", 10: "Special2", 11: "DefaultZone",
}
# DamageObject.hpp:204
INTERNAL_ZONE = {
-1: "NullZone", 0: "LeftLeg", 1: "RightLeg", 2: "LeftArm", 3: "RightArm",
4: "RightTorso", 5: "LeftTorso", 6: "CenterTorso", 7: "Head",
8: "Special1", 9: "Special2", 10: "VehicleHull", 11: "VehicleWeapon",
12: "VehicleSpecial", 13: "DefaultZone",
}
# DamageObject.hpp:185
DAMAGE_MODE = {
0: "GeneralDamageMode", 1: "GimpLeftDamageMode", 2: "GimpRightDamageMode",
3: "DestructionDamageMode", 4: "DetachableDamageMode", 5: "EngineDamageMode",
6: "NextDamageMode", 7: "HeadShotDamageMode", 8: "GyroHitDamageMode",
9: "TorsoLeftDamageMode", 10: "TorsoRightDamageMode",
11: "ArmLeftDamageMode", 12: "ArmRightDamageMode",
}
class Reader:
def __init__(self, blob):
self.b, self.o = blob, 0
def i32(self):
v = struct.unpack_from("<i", self.b, self.o)[0]
self.o += 4
return v
def u32(self):
v = struct.unpack_from("<I", self.b, self.o)[0]
self.o += 4
return v
def f32(self):
v = struct.unpack_from("<f", self.b, self.o)[0]
self.o += 4
return v
def mstring(self):
n = self.u32()
s = self.b[self.o:self.o + n].decode("latin-1")
self.o += n + 1 # length excludes the terminator
return s
def done(self):
return self.o >= len(self.b)
def parse(blob):
"""-> [(kind, {field: value})] in stream order."""
r = Reader(blob)
out = []
while not r.done():
class_id = r.i32()
if class_id == ARMOR_CLASS:
rec = {
"baseArmorValue": r.f32(),
"currentArmorValue": r.f32(),
"scaleSplashDamage": r.f32(),
"name": r.mstring(),
"internalDamageZone": r.i32(),
"armorZone": r.i32(),
"damageLevel": r.i32(),
"armorType": r.i32(),
"maxArmorValue": r.f32(),
"attachedToZone": r.i32(),
}
out.append(("armor", rec))
elif class_id == INTERNAL_CLASS:
rec = {
"baseInternalDamage": r.f32(),
"currentInternalDamage": r.f32(),
"parentEntityName": r.mstring(),
"damageMode": r.i32(),
"damageZone": r.i32(),
"damagePropagationZone": r.i32(),
"internalType": r.i32(),
"attachedTo": r.i32(),
}
count = r.i32()
rec["effects"] = [(r.u32(), r.f32()) for _ in range(count)]
rec["missileSlots"] = r.i32()
rec["projectileSlots"] = r.i32()
rec["beamSlots"] = r.i32()
rec["omniSlots"] = r.i32()
out.append(("internal", rec))
else:
raise ValueError(f"unknown classID {class_id} at offset {r.o - 4}")
return out
def fmt(x):
"""Match the authored style: 1.0, 80.0, 0.14, .99"""
if x == int(x):
return f"{x:.1f}"
return f"{x:g}"
def decompile(mech_dir, manifest=None):
"""-> [(pageName, [(key, value)])] in stream order."""
manifest = manifest if manifest is not None else subsystems.load_manifest(MANIFEST)
blob = record(mech_dir)
pages = []
for kind, r in parse(blob):
if kind == "armor":
kv = [
("BaseArmorValue", fmt(r["baseArmorValue"])),
("MaxArmorValue", fmt(r["maxArmorValue"])),
("ScaleSplashDamage", f"{r['scaleSplashDamage']:g}"),
("InternalDamageZone", INTERNAL_ZONE.get(r["internalDamageZone"])),
("ArmorZone", ARMOR_ZONE.get(r["armorZone"])),
]
if r["attachedToZone"] != -1:
kv.append(("SpecialAttachedToZone", ARMOR_ZONE.get(r["attachedToZone"])))
pages.append((r["name"], kv))
else:
zone = INTERNAL_ZONE.get(r["damageZone"], "Null")
kv = [
("DamageZone", zone),
("BaseInternalDamage", fmt(r["baseInternalDamage"])),
]
for rid, pct in r["effects"]:
path = manifest.get(rid >> 16, f"<unresolved:{rid}>")
kv.append(("DamageEffect", f"{path},{pct:g}"))
# no source writes GeneralDamageMode explicitly, so 0 means "omitted"
if r["damageMode"]:
kv.append(("DamageMode", DAMAGE_MODE.get(r["damageMode"])))
kv.append(("ParentEntityName", r["parentEntityName"]))
if r["damagePropagationZone"] != -1:
kv.append(("DamagePropagationZone",
INTERNAL_ZONE.get(r["damagePropagationZone"])))
if r["attachedTo"] != -1:
kv.append(("SpecialAttachedToZone", ARMOR_ZONE.get(r["attachedTo"])))
for key, field in (("MissileSlots", "missileSlots"),
("ProjectileSlots", "projectileSlots"),
("BeamSlots", "beamSlots"),
("OmniSlots", "omniSlots")):
if r[field]:
kv.append((key, str(r[field])))
pages.append((zone + "Internal", kv))
return pages
def record(mech_dir):
ch = os.path.basename(mech_dir.rstrip("/")).lower()
for fn in os.listdir(mech_dir):
if fn.lower().endswith(".damage"):
return open(os.path.join(mech_dir, fn), "rb").read()
raise SystemExit(f"no .damage record in {mech_dir}")
def emit(pages):
lines = []
for name, kv in pages:
lines.append(f"[{name}]")
lines.extend(f"{k}={v}" for k, v in kv)
lines.append("")
return "\r\n".join(lines)
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_damage.py")]))
if not args.mech_dir:
ap.error("mech_dir required")
text = emit(decompile(args.mech_dir, subsystems.load_manifest(args.manifest)))
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()
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env python3
"""
data.py - decompiler for a mech `<chassis>.data` file.
Rebuilds the `[GameData]` page from the compiled records:
<chassis>.data{GameModel} 1636-byte flat struct (datamap.py)
<chassis>.data{FootSteps} foot-step texture list
<chassis>.data[shadow] inline Shadow notation block
Value sources, in order of preference:
* a struct member - typed read through datamap.chain_layout()
* a ResourceID member - record id (HIGH word) resolved via the manifest
* a symbolic constant - int reversed through constants.py
* an explicit factory key - handled below, because SaveGameModel writes these
outside the attribute table
Four keys cannot be recovered and are deliberately omitted: BattleDamageRatio,
BattleKillBonus, DragoonValue and VehicleTradeValue. They appear in every source
.data but are read by NOTHING in the engine - no factory, no attribute
registration, no runtime reference - so they never enter the package. They are
authoring metadata; omitting them changes no behaviour.
python3 data.py <chassis-record-dir> [-o out.data]
python3 data.py --verify
"""
import argparse, collections, os, glob, re, struct, subprocess, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import datamap, constants, subsystems
MANIFEST = "/home/rich/Repositories/FS_Ours_extracted/_manifest.tsv"
# Adept.hpp:212. NoMaterial is 0.
MATERIALS = [
"NoMaterial", "Grass", "Water", "Concrete", "GreyDirt", "BrownDirt", "Rock",
"DarkConcrete", "DarkGreyDirt", "DarkBrownDirt", "DarkRock", "Blacktop",
"Snow", "Wood", "Lava", "Glass", "Steel", "Us", "Them", "LightMineral",
"DarkMineral", "Ash", "CrackedLava", "OpenLava",
]
# Identical in all 64 chassis; SaveGameModel writes them outside the attribute table.
CONSTANTS = {
"Class": "MechWarrior4::Mech",
"FaceLighting": "yes",
"LookupLighting": "yes",
"VertexLighting": "yes",
"LightMapLighting": "no",
# byte-identical in all 89 mech .data files, destroyed variants included
"Shadow": "{\n[shadow]\nLightType=Shadow\nInnerRadius=4.0\nOuterRadius=10.0\n"
"BlobDistance=200.0\nShadowMap=ShadowMask\nIntensity=0.4\n}",
}
# Keys whose source name differs from the struct member name.
ALIASES = {
"AnimationScript": "animScriptName",
"HeatManager": "heatManagerResource",
"FootEffectsFile": "footFallEffectsTable",
}
# CraterName is stored as MString::GetHashValue (DeathEntity_Tool.cpp:34), which
# is one-way. All 64 chassis hold the same hash, so the single authored value is
# recoverable by constancy rather than by inversion.
CRATER_HASH = {217688981: "crater01"}
# Read by nothing in the engine - see module docstring.
UNRECOVERABLE = ["BattleDamageRatio", "BattleKillBonus", "DragoonValue", "VehicleTradeValue"]
SYMBOLIC = ("MechID", "TechType", "NameIndex", "MoveTypeFlag")
def stem(mech_dir):
"""File stem used inside a mech folder; it need not match the folder name.
jenner2c/ holds jenner_2c.*, the same trap Black Hawk/nova sprang on
.subsystems -- never assume the folder name.
"""
names = [f for f in os.listdir(mech_dir) if f.lower().endswith(".data")]
if names:
return names[0][:-len(".data")]
for f in os.listdir(mech_dir):
m = re.match(r'(.+)\.data[\{\[]', f, re.I)
if m:
return m.group(1)
return os.path.basename(mech_dir.rstrip("/"))
def record(mech_dir, suffix):
"""Read one qualified record. Not glob -- '[shadow]' is a character class."""
want = (stem(mech_dir) + suffix).lower()
for fn in os.listdir(mech_dir):
if fn.lower() == want:
return open(os.path.join(mech_dir, fn), "rb").read()
return None
def bool_words():
"""-> {key: (trueWord, falseWord)}; sources spell these inconsistently.
Collider and CanBeShot are written true/false, the CanLoad* flags Yes/No.
"""
seen = collections.defaultdict(collections.Counter)
for _ch, kv, _b in datamap.corpus():
for k, v in kv.items():
w = v.strip().lower()
if w in ("true", "false", "yes", "no"):
seen[k][w] += 1
out = {}
for k, c in seen.items():
plain = c["true"] + c["false"] >= c["yes"] + c["no"]
out[k] = ("true", "false") if plain else ("Yes", "No")
return out
def foot_steps(blob):
"""-> (defaultTexture, [(texture, materialName)]).
Stream written by Mech_Tool.cpp:196: int material (-1 = default), int length
NOT counting the terminator, the characters, a NUL, then a one-byte
isDefault flag.
"""
default, rows, off = None, [], 0
while off + 8 <= len(blob):
material, length = struct.unpack_from("<iI", blob, off)
off += 8
text = blob[off:off + length].decode("latin-1")
off += length + 1 # skip the terminator
is_default = blob[off] if off < len(blob) else 0
off += 1
if is_default or material < 0:
default = text
else:
name = MATERIALS[material] if 0 <= material < len(MATERIALS) else str(material)
rows.append((text, name))
return default, rows
def decompile(mech_dir, manifest=None, retarget_ids=False, obb_dir=None):
"""-> OrderedDict key -> value or [values]."""
manifest = manifest if manifest is not None else subsystems.load_manifest(MANIFEST)
ch = os.path.basename(mech_dir.rstrip("/"))
gm = record(mech_dir, ".data{GameModel}")
if gm is None:
raise SystemExit(f"no GameModel record in {mech_dir}")
layout = datamap.chain_layout()[0]
angles = datamap.angle_fields()
tables = constants.tables()
words = bool_words()
by_member = {datamap.norm(n): n for n in layout}
out = collections.OrderedDict()
def member(key):
n = ALIASES.get(key) or by_member.get(datamap.norm(key))
return (n, *layout[n]) if n in layout else None
# every key the corpus knows about, so output matches the authored shape.
# Keys only a handful of mechs author (VehicleBattleValue: 1 of 64) are left
# out rather than emitted at their default.
corpus = datamap.corpus()
common = collections.Counter(k for _c, kv, _b in corpus for k in kv)
for key in sorted(common):
if key in UNRECOVERABLE or common[key] * 2 < len(corpus):
continue
if key in CONSTANTS:
out[key] = CONSTANTS[key]
continue
if key == "CraterName":
name = CRATER_HASH.get(datamap.read(gm, *layout["m_craterID"]))
if name:
out[key] = name
continue
m = member(key)
if key in SYMBOLIC and m:
_n, off, typ, size = m
raw = datamap.read(gm, off, typ, size)
sym = constants.symbol(tables, key, raw, hint=ch)
# MechID/NameIndex are authored as $(M_Chassis)/$(IDS_Chassis). A
# foreign package may store an int from a different roster -- V4H's
# are shifted by one against ours -- so --retarget-ids emits the
# chassis's own symbol and lets the build resolve it.
if retarget_ids and key in ("MechID", "NameIndex"):
prefix = "M_" if key == "MechID" else "IDS_"
if not sym or constants.norm(ch) not in constants.norm(sym):
out[key] = f"$({prefix}{ch[:1].upper() + ch[1:]})"
continue
if sym:
out[key] = sym
continue
if m:
name, off, typ, size = m
val = datamap.read(gm, off, typ, size, datamap.norm(name) in angles
or typ == "Radian")
if typ == "ResourceID":
path = manifest.get(val >> 16)
if path:
out[key] = path
elif typ in datamap.VECTORS:
out[key] = " ".join(f"{v:g}" for v in val)
elif typ in ("bool", "BYTE"):
yes, no = words.get(key, ("true", "false"))
out[key] = yes if val else no
elif typ in datamap.FLOATS:
out[key] = f"{val:g}"
elif typ == "char":
if val:
out[key] = val
else:
out[key] = str(val)
# The OBB filenames are source-side names the package never stores. Prefer the
# .obb files actually shipped next to the output so the keys cannot disagree
# with them; fall back to the usual convention when none are present.
solid = hier = None
if obb_dir and os.path.isdir(obb_dir):
for fn in sorted(os.listdir(obb_dir)):
if not fn.lower().endswith(".obb"):
continue
if fn.lower().endswith("_solid.obb"):
solid = fn
else:
hier = fn
out["SolidOBB"] = solid or f"{ch}_Skeleton_SOLID.obb"
out["HierarchicalOBB"] = hier or f"{ch}_Skeleton.obb"
fs = record(mech_dir, ".data{FootSteps}")
if fs:
default, rows = foot_steps(fs)
if default:
out["DefaultFootStepTexture"] = default
if rows:
out["FootStepTexture"] = [f"{t},{m}" for t, m in rows]
sh = record(mech_dir, ".data[shadow]")
if sh is None:
out.pop("Shadow", None)
return out
def emit(kv):
lines = ["[GameData]"]
for key, val in kv.items():
for v in (val if isinstance(val, list) else [val]):
lines.append(f"{key}={v}")
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,
help="package manifest for resolving ResourceIDs")
ap.add_argument("--retarget-ids", action="store_true",
help="emit the chassis's own MechID/NameIndex symbol when the "
"stored int belongs to a different roster")
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_roundtrip.py")]))
if not args.mech_dir:
ap.error("mech_dir required")
text = emit(decompile(args.mech_dir, subsystems.load_manifest(args.manifest),
retarget_ids=args.retarget_ids,
obb_dir=os.path.dirname(args.output) if args.output else None))
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()
+267
View File
@@ -0,0 +1,267 @@
#!/usr/bin/env python3
"""
datamap.py - field map for the mech `<chassis>.data{GameModel}` record (1636 B).
Built from the engine headers, not from value matching.
Why: value matching alone cannot separate keys that hold the same value in every
mech. `dampenWorldJoint` and `fallAdjustmentSeconds` are both 0.5 everywhere, so
34 of the 80 numeric keys came out ambiguous, and ordering them by their
appearance in the source file assigned several of them wrongly - it put
`tiltSpeed` at 672, which is really `slopeDecel2`, and swapped
`percentageOfTurnToStartTilt` with `percentageOfSpeedToStartTilt`.
So the layout is taken from the declaration order in
Vehicle__GameModel mw4/Code/MW4/Vehicle.hpp
Mech__GameModel mw4/Code/MW4/Mech.hpp
and each block is anchored using the fields that value matching *did* resolve
unambiguously. Every anchor agrees:
Mech block base 756: footReturnSeconds 764, dampenTorsoJoint 800,
undampenRootJoint 816, undampenHipJoint 824,
scaleInternalTiltDegree 832
Vehicle block base 664: minSpeed 692, maxSpeed 696, acceleration 720,
decceleration 724, reverseAccelerationMultiplier 728
All fields in both blocks are 4-byte Scalar/Radian, so field i sits at
base + 4*i.
"""
import glob, math, os, re, struct, collections
REPO = "/home/rich/Repositories/firestorm/Gameleap"
SRC = REPO + "/mw4/Content/Mechs"
CODE = REPO + "/code"
REC = "/home/rich/Repositories/FS_Ours_extracted/core/mechs"
GAMEMODEL_SIZE = 1636
# The GameModel inheritance chain for a mech, base class first. Bases are NOT
# hardcoded: each block starts where the previous one ended. MWMover__GameModel
# is just `typedef Adept::Mover__GameModel` (MWMover.hpp:173), so it adds
# nothing. `Entity__GameModel` declares no base class and wraps its members in
# `#if NSWIZZLE`, which is never defined anywhere in the tree - the #else branch
# is live, and the two branches order their members DIFFERENTLY.
CHAIN = [
("Entity__GameModel", CODE + "/mw4/Libraries/Adept/Entity.hpp"),
("Mover__GameModel", CODE + "/mw4/Libraries/Adept/Mover.hpp"),
("MWObject__GameModel", CODE + "/mw4/Code/MW4/MWObject.hpp"),
("Vehicle__GameModel", CODE + "/mw4/Code/MW4/Vehicle.hpp"),
("Mech__GameModel", CODE + "/mw4/Code/MW4/Mech.hpp"),
]
# Independently measured block starts, used to check the computed chain.
ANCHORS = {"Vehicle__GameModel": 664, "Mech__GameModel": 756}
# The factories that convert authored degrees into stored radians.
FACTORIES = [CODE + "/mw4/Code/MW4/Mech_Tool.cpp", CODE + "/mw4/Code/MW4/Vehicle_Tool.cpp"]
SCALAR_TYPES = r'(?:Stuff::Scalar|Stuff::Radian|Stuff::Angle|float)'
DEG2RAD = math.pi / 180.0
MAX_STRING_LENGTH = 256 # Entity.hpp:199
# Member sizes. bool is ONE byte, not four -- getting this wrong shifts every
# field after the six m_canLoad* flags by exactly 16 bytes.
SIZES = {
"Scalar": 4, "Radian": 4, "Angle": 4, "float": 4, "int": 4, "unsigned": 4,
"DWORD": 4, "WORD": 2, "BYTE": 1, "bool": 1, "ResourceID": 4,
"Point3D": 12, "Vector3D": 12, "UnitQuaternion": 16, "RGBAColor": 16,
"Motion3D": 24, "LinearMatrix4D": 48, "char": 1,
"ClassID": 4, "ReplicatorID": 4, "FactoryRequest": 4, "ObjectID": 4,
}
MEMBER_RE = re.compile(
r'\b((?:Stuff::|Adept::)?(?:Scalar|Radian|Angle|Point3D|Vector3D|UnitQuaternion'
r'|RGBAColor|Motion3D|LinearMatrix4D|ResourceID)|(?:Stuff::)?(?:RegisteredClass::)?ClassID'
r'|ReplicatorID|ObjectID|(?:\w+::)?FactoryRequest|float|int|bool|BYTE|WORD|DWORD'
r'|unsigned|char)\s+([A-Za-z_][\w\s,\[\]]*?);', re.S)
def members(cls, path):
"""-> [(typeName, memberName, sizeInBytes)] in declaration order."""
txt = open(path, encoding="latin-1").read()
m = re.search(rf'class\s+{cls}\s*(?::[^{{;]*)?\{{', txt, re.S)
if not m:
return []
depth, i = 1, m.end() # brace-match; indentation is inconsistent between headers
while i < len(txt) and depth:
depth += (txt[i] == "{") - (txt[i] == "}")
i += 1
body = txt[m.end():i - 1]
ctor = body.find(f"{cls}(")
if ctor > 0:
body = body[:ctor]
body = re.sub(r'//[^\n]*', '', body)
# `typedef int AttributeID;` is not a member. This masked a missing ClassID
# for a while: two 4-byte errors that happened to cancel.
body = re.sub(r'\btypedef\b[^;]*;', '', body, flags=re.S)
body = re.sub(r'#\s*if\s+NSWIZZLE\b.*?#\s*else', '', body, flags=re.S)
body = re.sub(r'#\s*(endif|else|if\w*|ifdef|ifndef)[^\n]*', '', body)
out = []
for d in MEMBER_RE.finditer(body):
base = (d.group(1).replace("Stuff::", "").replace("Adept::", "")
.replace("RegisteredClass::", ""))
base = base.rsplit("::", 1)[-1]
for name in d.group(2).split(","):
name = name.strip()
arr = re.fullmatch(r'([A-Za-z_]\w*)\s*\[\s*([A-Za-z_]\w*|\d+)\s*\]', name)
if arr:
n = arr.group(2)
count = MAX_STRING_LENGTH if n == "MaxStringLength" else int(n)
out.append((base, arr.group(1), SIZES[base] * count))
elif re.fullmatch(r'[A-Za-z_]\w*', name):
out.append((base, name, SIZES[base]))
return out
def chain_layout(chain=None, anchors=None, start=0):
"""-> ({memberName: (offset, typeName, size)}, {className: baseOffset}).
Each block starts where the previous ended; /Zp4 means align = min(4, size).
Raises if a computed base contradicts an independently measured anchor.
Defaults to the mech chain; pass another for Torso/Engine and friends.
"""
chain = CHAIN if chain is None else chain
anchors = ANCHORS if anchors is None else anchors
fields, bases, off = {}, {}, start
for cls, path in chain:
off += (-off) % 4
bases[cls] = off
want = anchors.get(cls)
if want is not None and off != want:
raise AssertionError(f"{cls} computed at {off}, measured {want}")
for typ, name, size in members(cls, path):
off += (-off) % min(4, size)
fields.setdefault(name, (off, typ, size))
off += size
return fields, bases
def angle_fields():
"""Fields the tool factory scales by Radians_Per_Degree.
This cannot be inferred from the header: torsoHitSpringMotionLimit and its
siblings are declared plain Stuff::Scalar, yet Mech_Tool.cpp:889 stores
`model->torsoHitSpringMotionLimit * Radians_Per_Degree`. Only the writer
knows. Stuff::Radian fields (tiltSpeed, tiltDegree, topSpeedTurnRate,
fullStopTurnRate) are handled by their declared type as well.
"""
out = set()
for path in FACTORIES:
if not os.path.exists(path):
continue
txt = open(path, encoding="latin-1").read()
for m in re.finditer(r'model->(\w+)\s*=\s*model->\w+\s*\*\s*Radians_Per_Degree', txt):
out.add(norm(m.group(1)))
return out
def declared_fields(cls, path):
"""-> [(fieldName, isAngle)] in declaration order, scalars only.
isAngle marks Stuff::Radian / Stuff::Angle. Those are authored in DEGREES in
the .data but stored in RADIANS in the record, so they need a pi/180 factor
on the way in and 180/pi on the way back out.
"""
txt = open(path, encoding="latin-1").read()
m = re.search(rf'class\s+{cls}\s*:(.*?)^\t\t\}};', txt, re.S | re.M)
if not m:
return []
body = m.group(1)
ctor = body.find(f"{cls}(")
if ctor > 0:
body = body[:ctor]
body = re.sub(r'//[^\n]*', '', body)
out = []
for decl in re.finditer(rf'\b({SCALAR_TYPES})\s+([A-Za-z_][\w\s,]*?);', body, re.S):
angle = decl.group(1) in ("Stuff::Radian", "Stuff::Angle")
for name in decl.group(2).split(","):
n = name.strip()
if re.fullmatch(r'[A-Za-z_]\w*', n):
out.append((n, angle))
return out
def norm(name):
return re.sub(r'^m_', '', name).replace("_", "").lower()
def header_field_map():
"""normalised member name -> (offset, typeName, size, isAngle)."""
angles = angle_fields()
out = {}
for name, (off, typ, size) in chain_layout()[0].items():
key = norm(name)
out.setdefault(key, (off, typ, size, typ == "Radian" or key in angles))
return out
def gamedata(path):
txt = open(path, "rb").read().decode("latin-1")
# Shadow={...} contains a line reading "[shadow]"; without hiding braced
# blocks the page scan stops there and every later key is lost silently.
txt = re.sub(r'\{.*?\}',
lambda x: x.group(0).replace("\r", "").replace("\n", "\x01"),
txt, flags=re.S)
m = re.search(r'^\[GameData\]\r?\n(.*?)(?=^\[[A-Za-z]|\Z)', txt, re.M | re.S)
if not m:
return collections.OrderedDict()
kv = collections.OrderedDict()
for k, v in re.findall(r'^([A-Za-z0-9_]+)=([^\r\n]*)', m.group(1), re.M):
kv.setdefault(k, v.replace("\x01", "\n"))
return kv
def corpus():
"""-> [(chassis, {key: value}, gameModelBytes)] for our 64 mech chassis."""
out = []
for d in sorted(glob.glob(REC + "/*")):
ch = os.path.basename(d)
gm = [g for g in glob.glob(d + "/*.data{GameModel}")
if os.path.basename(g).lower().startswith(ch.lower() + ".data")]
if not gm:
continue
blob = open(gm[0], "rb").read()
if len(blob) != GAMEMODEL_SIZE:
continue
s = [p for p in glob.glob(SRC + "/*/*.data")
if os.path.basename(p).lower() == ch.lower() + ".data"]
if s:
out.append((ch, gamedata(s[0]), blob))
return out
def build():
"""-> (corpus, {sourceKey: (offset, typeName, size, isAngle)})"""
pairs = corpus()
hmap = header_field_map()
keys = collections.Counter()
for _c, kv, _b in pairs:
keys.update(kv.keys())
return pairs, {k: hmap[norm(k)] for k in keys if norm(k) in hmap}
FLOATS = {"Scalar", "Radian", "Angle", "float"}
VECTORS = {"Point3D": 3, "Vector3D": 3, "RGBAColor": 4, "UnitQuaternion": 4}
def read(blob, off, typ="Scalar", size=4, angle=False):
"""Decoded value in the units and form the .data source uses."""
if typ in FLOATS:
v = struct.unpack_from("<f", blob, off)[0]
return v / DEG2RAD if angle else v
if typ in VECTORS:
return list(struct.unpack_from(f"<{VECTORS[typ]}f", blob, off))
if typ == "char":
return blob[off:off + size].split(b"\0")[0].decode("latin-1")
if typ in ("bool", "BYTE"):
return blob[off]
if typ == "WORD":
return struct.unpack_from("<H", blob, off)[0]
if typ == "ResourceID":
return struct.unpack_from("<I", blob, off)[0]
return struct.unpack_from("<i", blob, off)[0]
if __name__ == "__main__":
pairs, field_map = build()
print(f"chassis: {len(pairs)} mapped keys: {len(field_map)}")
for k, (off, typ, size, angle) in sorted(field_map.items(), key=lambda kv: kv[1][0]):
note = " [degrees]" if angle else ""
print(f" {off:5d} {typ:14s} {k}{note}")
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""
destroyed.py - regenerate the .data and .video of a *_destroyed mech variant.
python3 destroyed.py <mechDir> [--erf X.erf] [--obb X.obb] [-o outDir]
python3 destroyed.py --verify # regenerate all 89 of ours and diff
A destroyed variant is four files:
<stem>.data text, DeathEntity boilerplate
<stem>.video text, four-page render graph
<stem>.erf geometry, verbatim in the package
<stem>_SOLID.obb collision, verbatim in the package
The .erf and .obb come straight out of the package. The .data and .video are
compiled away (a 12-byte stub plus {Element}/{GameModel}, and a binary element
tree with embedded #FRE/#RLM blobs), so they are regenerated from a template.
That is safe here because the template is invariant. Across all 89 destroyed
variants in our own Content tree, every one of Class, OBBCollides, Collider,
CanBeShot, CanBeWalkedOn, VertexLighting, FaceLighting, LookupLighting,
LightMapLighting and CraterName is identical. The only things that vary are the
three filename references, and those are read from the files actually present
rather than guessed - which also preserves V4H's own `champion_stroyed` typo,
since that is the name their build really uses.
The .data and .video each appear in two forms in our tree, differing only by a
trailing blank line; the majority form is emitted.
"""
import sys, os, re, glob, argparse, collections
DATA_TEMPLATE = (
"[GameData]\r\n"
"Class=MechWarrior4::DeathEntity\r\n"
"SolidOBB={obb}\r\n"
"OBBCollides=false\r\n"
"Collider=false\r\n"
"CanBeShot=false\r\n"
"CanBeWalkedOn=false\r\n"
"VertexLighting=yes\r\n"
"FaceLighting=yes\r\n"
"LookupLighting=no\r\n"
"LightMapLighting=no\r\n"
"CraterName=crater1\r\n"
"\r\n"
"[Renderers]\r\n"
"VideoRenderer={video}\r\n"
"\r\n"
)
VIDEO_TEMPLATE = (
"[lod]\r\n"
"Type=ShapeComponent\r\n"
"Geometry={erf}\r\n"
"\r\n"
"[watcher]\r\n"
"Type=AttributeWatcherOfInt\r\n"
"Attribute=VisualRepresentation\r\n"
"SimulationShouldExecute=1\r\n"
"\r\n"
"[damageappearance]\r\n"
"Type=SwitchComponent\r\n"
"Input=Watcher\r\n"
"Child=LOD\r\n"
"\r\n"
"[locator]\r\n"
"Child=DamageAppearance\r\n"
"\r\n"
)
OUR_MECH_SOURCE = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs"
def find_parts(mech_dir, compiled_dir=None):
"""-> (stem, erfName, obbName). Looks in mech_dir, then the compiled tree."""
erfs = [f for f in os.listdir(mech_dir) if f.lower().endswith(".erf")]
obbs = [f for f in os.listdir(mech_dir) if f.lower().endswith(".obb")]
stem = None
if compiled_dir and os.path.isdir(compiled_dir):
datas = [f for f in os.listdir(compiled_dir)
if f.lower().endswith(".data")]
if datas:
stem = datas[0][:-len(".data")]
if stem is None and obbs:
stem = re.sub(r"_solid\.obb$", "", obbs[0], flags=re.I)
stem = re.sub(r"\.obb$", "", stem, flags=re.I)
return stem, (erfs[0] if erfs else None), (obbs[0] if obbs else None)
def generate(stem, erf, obb):
return (DATA_TEMPLATE.format(obb=obb, video=f"{stem}.video"),
VIDEO_TEMPLATE.format(erf=erf))
def verify():
ok = collections.Counter()
problems = []
for src_dir in sorted(glob.glob(OUR_MECH_SOURCE + "/*_[Dd]estroyed") +
glob.glob(OUR_MECH_SOURCE + "/*_DESTROYED")):
datas = glob.glob(src_dir + "/*.data")
videos = glob.glob(src_dir + "/*.video")
if not datas or not videos:
continue
stem = os.path.basename(datas[0])[:-len(".data")]
want_data = open(datas[0], "rb").read().decode("latin-1")
want_video = open(videos[0], "rb").read().decode("latin-1")
kv = dict(re.findall(r'^([A-Za-z0-9_]+)=([^\r\n]*)', want_data, re.M))
erf = re.search(r'Geometry=([^\r\n]*)', want_video).group(1).strip()
got_data, got_video = generate(stem, erf, kv["SolidOBB"].strip())
ok["files"] += 1
for what, want, got, exact_key, soft_key in (
(".data", want_data, got_data, "data_exact", "data_soft"),
(".video", want_video, got_video, "video_exact", "video_soft")):
if got == want:
ok[exact_key] += 1
elif got.rstrip("\r\n").lower() == want.rstrip("\r\n").lower():
# NotationFile compares with _stricmp throughout, and the sources
# themselves are inconsistent (one uses [renderers], others
# [Renderers]), so case and a trailing blank line are cosmetic.
ok[soft_key] += 1
else:
problems.append((os.path.basename(src_dir), what, want, got))
print(f"destroyed variants checked : {ok['files']}")
print(f" .data equivalent : {ok['data_exact'] + ok['data_soft']}"
f" (byte-exact {ok['data_exact']}, +{ok['data_soft']} case / trailing-blank-line only)")
print(f" .video equivalent : {ok['video_exact'] + ok['video_soft']}"
f" (byte-exact {ok['video_exact']}, +{ok['video_soft']} case / trailing-blank-line only)")
if problems:
print(f"\n{len(problems)} real differences:")
for name, what, want, got in problems[:6]:
print(f" {name} {what}")
for w, g in zip(want.splitlines(), got.splitlines()):
if w != g:
print(f" source={w!r}\n got ={g!r}")
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("mechdir", nargs="?", help="the *_destroyed folder holding .erf/.obb")
ap.add_argument("--compiled", help="matching folder in the _compiled tree, for the stem")
ap.add_argument("-o", "--out", help="output folder (defaults to mechdir)")
ap.add_argument("--verify", action="store_true")
args = ap.parse_args()
if args.verify:
verify()
return
if not args.mechdir:
ap.error("give a *_destroyed folder, or --verify")
stem, erf, obb = find_parts(args.mechdir, args.compiled)
if not (stem and erf and obb):
sys.exit(f"{args.mechdir}: need a stem, an .erf and an .obb "
f"(got stem={stem!r} erf={erf!r} obb={obb!r})")
data, video = generate(stem, erf, obb)
out = args.out or args.mechdir
os.makedirs(out, exist_ok=True)
open(os.path.join(out, f"{stem}.data"), "wb").write(data.encode("latin-1"))
open(os.path.join(out, f"{stem}.video"), "wb").write(video.encode("latin-1"))
print(f"{os.path.basename(args.mechdir)}: {stem}.data + {stem}.video "
f"(SolidOBB={obb}, Geometry={erf})")
if __name__ == "__main__":
main()
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""
instance.py - decompiler for a mech `<chassis>.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 <chassis-record-dir> [-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()
@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""
make_generic_doll.py - build a generic MFD/Radar damage paper doll.
The six chassis imported from V4H have no damage art. V4H shipped Mad Cat copies
under six names, which renders an aligned doll that lies about the mech. Instead
this builds one honest generic doll from art the game already owns.
Source: `hsh/mfd_texture.bmp`, the MFD sprite sheet, which contains a generic
mech already separated into components, together with a validated 11-zone
mapping in `huddamage.cpp` (`sm2_texture` / `sm2_offset`, lines 472 and 487) --
the same zone order `coord.cpp` uses. `RenderAux4SmallMech` draws it, so the
geometry below is proven in game rather than measured by us.
Output is the exploded runtime BMP: every zone appears exactly once, and no two
source rectangles touch, so each `texuv` isolates one component. The game
reassembles the figure from the `offset` values.
python3 make_generic_doll.py [-o OUTDIR]
"""
import argparse, os, sys
from PIL import Image, ImageDraw
HSH = "/home/rich/Repositories/firestorm/Gameleap/mw4/hsh"
# huddamage.cpp:472 -- source rects into mfd_texture.bmp, in coord.cpp zone order
SM2_TEXTURE = [
(172, 0, 231, 140, "LL", "left leg"),
(109, 0, 168, 140, "RL", "right leg"),
(30, 144, 67, 246, "LA", "left arm"),
(0, 36, 36, 128, "RA", "right arm"),
(37, 24, 66, 101, "RT", "right torso"),
(0, 132, 28, 209, "LT", "left torso"),
(67, 33, 99, 125, "CT", "center torso"),
(None, None, None, None, "CTR", "center torso rear"),
(67, 0, 99, 31, "HD", "head"),
(None, None, None, None, "S1", "special 1"),
(None, None, None, None, "S2", "special 2"),
]
# huddamage.cpp:487 -- assembled positions (the *3 is already applied here)
SM2_OFFSET = [
(29 * 3, 35 * 3), (6 * 3, 35 * 3), (43 * 3, 11 * 3), (0 * 3, 11 * 3),
(12 * 3, 7 * 3), (33 * 3, 7 * 3), (22 * 3, 10 * 3), None,
(22 * 3, 0 * 3), None, None,
]
# The native figure is 166x245. Real dolls assemble to roughly 330x335 (Atlas,
# Assassin II), so 1.5 lands in the right range and keeps the arithmetic exact:
# the generic centre torso becomes 48x138 against Atlas's 48x174.
#
# Radar is authored on a 410 working canvas against the MFD's 340, and real rows
# follow that -- Assassin II's radar rects run about 1.27x its MFD ones -- so the
# Radar doll is scaled up to match. The runtime halves Radar values at draw time.
MFD_SCALE = 1.5
RADAR_SCALE = 1.8
CANVAS = 512
def even(v):
"""Coordinates are authored even; runtime Radar halves them with integer /2."""
return int(round(v / 2.0)) * 2
def components(scale):
sheet = Image.open(os.path.join(HSH, "mfd_texture.bmp")).convert("L")
out = []
for (rect, off) in zip(SM2_TEXTURE, SM2_OFFSET):
x0, y0, x1, y1, zone, label = rect
if x0 is None:
out.append((zone, label, None, None, None))
continue
piece = sheet.crop((x0, y0, x1, y1))
w = even((x1 - x0) * scale)
h = even((y1 - y0) * scale)
piece = piece.resize((w, h), Image.LANCZOS)
out.append((zone, label, piece, (even(off[0] * scale), even(off[1] * scale)), (w, h)))
return out
def layout(comps, outline=False):
"""Place every piece in the 512 canvas without touching. -> (image, {zone: rect})"""
img = Image.new("L", (CANVAS, CANVAS), 0)
draw = ImageDraw.Draw(img)
rects = {}
x, y, row_h, gap = 4, 4, 0, 8
for zone, _label, piece, _off, size in comps:
if piece is None:
rects[zone] = (0, 0, 0, 0)
continue
w, h = size
if x + w + gap > CANVAS:
x = 4
y += row_h + gap
row_h = 0
img.paste(piece, (x, y))
if outline:
draw.rectangle([x, y, x + w - 1, y + h - 1], outline=255, width=2)
rects[zone] = (x, y, x + w, y + h)
x += w + gap
row_h = max(row_h, h)
return img, rects
def row(values, width=3):
return "{" + ",".join("{" + ",".join(f"{v:{width}d}" for v in t) + "}" for t in values) + "}"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("-o", "--outdir", default="/home/rich/Repositories/FS_Build_V4H_extracted")
args = ap.parse_args()
comps = components(MFD_SCALE)
order = [c[0] for c in comps]
written = []
tables = {}
for kind, sub, outlined, scale in (("MFD", "hsh/hud", False, MFD_SCALE),
("Radar", "hsh/radar/hud", True, RADAR_SCALE)):
comps = components(scale)
img, rects = layout(comps, outline=outlined)
d = os.path.join(args.outdir, sub)
os.makedirs(d, exist_ok=True)
p = os.path.join(d, "generic.bmp")
img.save(p)
written.append(p)
texuv = [rects[z] for z in order]
offset = [(c[3] if c[3] else (0, 0)) for c in comps]
offset = [o if rects[z] != (0, 0, 0, 0) else (0, 0) for z, o in zip(order, offset)]
tables[kind] = (texuv, offset)
doc = os.path.join(args.outdir, "hsh", "GENERIC-DOLL-COORDS.txt")
with open(doc, "w", newline="\r\n") as fh:
fh.write(TEXT.format(
scale=MFD_SCALE,
rscale=RADAR_SCALE,
zones=" ".join(f"{i}:{z}" for i, z in enumerate(order)),
mfd_texuv=row(tables["MFD"][0]),
mfd_offset=row(tables["MFD"][1]),
rad_texuv=row(tables["Radar"][0]),
rad_offset=row(tables["Radar"][1]),
))
written.append(doc)
for p in written:
print("wrote", p)
TEXT = """GENERIC MECH DAMAGE PAPER DOLL - coordinates for coord.cpp
=========================================================
WHAT THIS IS
A generic 'Mech damage doll for chassis that have no bespoke MFD/Radar art.
Six imported V4H chassis (champion, dasher, griffin, jenner2c, marauder,
thunderbolt) ship with none. V4H filled the gap with pixel-identical copies
of the Mad Cat doll under six names, which renders an aligned display that
misrepresents the mech. This is the honest placeholder instead.
WHERE THE ART CAME FROM
Not drawn from scratch. The game already owns a generic 'Mech, separated
into components, inside hsh/mfd_texture.bmp (the MFD sprite sheet), with a
validated eleven-zone mapping in huddamage.cpp:
sm2_texture[][4] line 472 source rectangles
sm2_offset[][2] line 487 assembled positions
drawn by RenderAux4SmallMech(). Those tables use the same zone order as
coord.cpp, so the geometry below is proven in game, not measured by us.
Components were scaled {scale}x for the MFD and {rscale}x for the Radar. The
native figure is 166x245 and real dolls assemble to roughly 330x335, so this
lands in the right range; the generic centre torso ends up 48x138 against the
Atlas's 48x174. The Radar is larger because it is authored on a 410 working
canvas against the MFD's 340, and the runtime halves Radar values at draw.
FILES
hsh/hud/generic.bmp 512x512 external MFD doll
hsh/radar/hud/generic.bmp 512x512 Radar doll, components outlined 2px
white per the Radar pipeline
Both are the EXPLODED runtime view: each zone appears exactly once and no
two source rectangles touch, so a texuv isolates one component. The game
reassembles the figure from the offsets.
ZONE ORDER (index: zone)
{zones}
CTR, S1 and S2 are all-zero, and that is normal rather than a shortcut. Across
the 65 shipped rows CTR is zero in ALL 65, S1 in 31 and S2 in 49. Neither
draw loop guards zero rectangles - a degenerate quad simply renders nothing -
so absent zones are safe. The generic figure has no rear silhouette and no
special hardpoints.
ONE FILE SERVES ALL SIX
The BMP name comes only from `texturename[]` in huddamage.cpp; both displays
use it (`LoadDamageTexture` and `LoadRadarDamageTexture` are both called with
`texturename[m_MechID]`). Nothing in the mech data files names it. So six
entries pointing at "hud\\\\generic" share one pair of images - do not make
per-chassis copies. (`m_HudMap` is unrelated: that is the mission map.)
--------------------------------------------------------------------------
HOW TO INSTALL
--------------------------------------------------------------------------
1. Copy the art
hsh/hud/generic.bmp -> Gameleap/mw4/hsh/hud/generic.bmp
hsh/radar/hud/generic.bmp -> Gameleap/mw4/hsh/radar/hud/generic.bmp
Loose hsh art is not packed into a .mw4, so no resource rebuild is needed.
2. huddamage.cpp - add one texturename[] entry per new chassis, all pointing at
the same generic art. The array is sized [LastMechID+1], so LastMechID in
MechLabHeaders.h must also rise from 64 to 70.
"hud\\\\generic", // M_Champion 65
"hud\\\\generic", // M_Jenner2c 66
"hud\\\\generic", // M_Dasher 67
"hud\\\\generic", // M_Marauder 68
"hud\\\\generic", // M_Thunderbolt 69
"hud\\\\generic", // M_Griffin 70
Keep V4H's append order (65-70). Inserting alphabetically would renumber
every existing chassis, and Mech IDs are positional across code, tables and
shell scripts.
3. coord.cpp - widen all four arrays from [65] to [71] and append the SAME row
six times to each, once per new chassis. All six share one doll, so all six
rows are identical.
texuv2 / offset2 are the external MFD. texuv3 / offset3 are the Radar, which
the runtime divides by two at draw time - store the full-size values here,
do not pre-divide.
texuv2 (MFD source rectangles)
{mfd_texuv}
offset2 (MFD exploded positions)
{mfd_offset}
texuv3 (Radar source rectangles)
{rad_texuv}
offset3 (Radar exploded positions)
{rad_offset}
4. Rebuild. DXRasterizer.cpp includes coord.cpp directly, so a coordinate change
needs a Release/Profile rebuild and redeploy of MW4.exe. The art alone does
not.
--------------------------------------------------------------------------
NOTES AND LIMITS
--------------------------------------------------------------------------
* Dasher's four commented-out rows were REMOVED from coord.cpp on 2026-08-08.
They were unique authored Dasher geometry rather than a copy, but the art they
mapped has never existed, and leaving them invited someone to enable them
against wrong art. Recoverable from git (last touched in deafc2b0) if real
Dasher art is ever produced. The matching commented `"hud\\\\dasher"` entry at
huddamage.cpp:81 is still there and should go with them.
* The generic cannot represent chassis-specific zones. Marauder's cage and
Dasher's S1/S2 will not appear.
* This is a placeholder. The point of a generic silhouette over a borrowed one
is that it reads as "no art yet" rather than as the wrong mech. Replace it
per chassis when real 1024x1024 renders exist; the authoring pipelines are
in MFD-RADAR-MAPPINGS.md.
* PRE-EXISTING BUGS, unrelated to this work but found while checking it. Three
chassis share another mech's rows while having their own art. Overlaying each
row on its own art shows:
Rifleman (id 49, uses Mad Cat's rows) - BROKEN. CT lands in empty
space beside the mech, S1/S2 float in empty corners, and the
leg boxes run well past the feet.
Battlemaster (id 9, uses Atlas's rows) - BROKEN. CT sits over a
left-side arm piece and the leg boxes are narrow centre
strips while the real legs fall outside them.
Templar (id 54, uses Archer's rows) - fine. Its art was evidently
authored to that layout; only the leg boxes clip slightly.
Rifleman and Battlemaster need their own measured rows, or art authored to
the borrowed layout.
"""
if __name__ == "__main__":
main()
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
mw4msg.py - reader for the GameOS "CreateMessage" streams found inside .mw4
records (.subsystems, [joint_*]{armature}, .contents, .instance, ...).
Layout, derived from the engine source rather than guessed:
MWObject::CreateSubsystemStream / CreateArmatureStream (MWObject_Tool.cpp)
WORD span number of replicator IDs consumed
N x CreateMessage one per [Page] that was serialised
Each message is a plain C struct, /Zp4, built by a per-class factory in one of
the 51 mw4/Code/MW4/*_Tool.cpp files. The common prefix is:
off 0 u32 messageLength Connection__Message
off 4 u32 messageID
off 8 u32 priority
off 12 u32 messageFlags
off 16 u32 classID Replicator__CreateMessage
off 20 u32 replicatorFlags
off 24 u32 replicatorID
off 28 12f localToParent Entity__CreateMessage (LinearMatrix4D)
off 76 u32 executionState
off 80 f32 initialAge
off 84 u32 dataListID (ResourceID; record id is the HIGH word)
off 88 u32 alignment
off 92 u32 nameID
Mover__CreateMessage then adds two 24-byte Motion3D fields (96, 120), and
MWMover__CreateMessage adds:
off 144 u32 siteStreamResourceID
off 148 u32 armatureStreamResourceID
off 152 char jointName[128]
Verified: a joint message is exactly 280 bytes, which is 152 + 128.
localToParent is 3 rows of 4 floats; the rotation is columns 0..2 and the
translation is column 3 of each row. Checked against every joint of all 65
chassis: 1453/1453 translations matched the source .armature exactly.
"""
import struct
HDR_LEN = 0
HDR_CLASSID = 16
HDR_REPLICATORID = 24
ENT_MATRIX = 28
ENT_EXECSTATE = 76
ENT_INITIALAGE = 80
ENT_DATALISTID = 84
ENT_ALIGNMENT = 88
ENT_NAMEID = 92
MWMOVER_SITEID = 144
MWMOVER_ARMID = 148
MWMOVER_JOINTNAME = 152
def walk(data, start=2):
"""Yield (offset, messageBytes) for each message in a span-prefixed stream."""
off = start
while off + 4 <= len(data):
length, = struct.unpack_from("<I", data, off)
if length < 16 or off + length > len(data):
raise ValueError(f"bad messageLength {length} at offset {off}")
yield off, data[off:off + length]
off += length
if off != len(data):
raise ValueError(f"trailing {len(data) - off} bytes")
def span(data):
return struct.unpack_from("<H", data, 0)[0]
def u32(msg, off):
return struct.unpack_from("<I", msg, off)[0]
def class_id(msg):
return u32(msg, HDR_CLASSID)
def record_id(msg, off=ENT_DATALISTID):
"""ResourceID packs the package record id in its high word."""
return u32(msg, off) >> 16
def matrix(msg):
return struct.unpack_from("<12f", msg, ENT_MATRIX)
def translation(msg):
m = matrix(msg)
return (m[3], m[7], m[11])
def rotation3x3(msg):
m = matrix(msg)
return (m[0], m[1], m[2], m[4], m[5], m[6], m[8], m[9], m[10])
def joint_name(msg):
if len(msg) <= MWMOVER_JOINTNAME:
return None
return msg[MWMOVER_JOINTNAME:].split(b"\0")[0].decode("latin-1")
def read_sites(data):
"""{sites} record: repeated [YawPitchRoll 3f][Point3D 3f][u32 len][name][NUL].
Written by MWMover__CreateMessage::ConstructCreateMessage (MWMover_Tool.cpp
~145): `site_stream << rotation; << translation; << site_name;`
Angles are radians.
"""
out, off = [], 0
while off + 28 <= len(data):
rx, ry, rz, tx, ty, tz = struct.unpack_from("<6f", data, off)
off += 24
n, = struct.unpack_from("<I", data, off)
off += 4
name = data[off:off + n].decode("latin-1")
off += n + 1 # names are length-prefixed AND NUL-terminated
out.append((name, (rx, ry, rz), (tx, ty, tz)))
return out
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
smallmodel.py - decompilers for a mech's `.torso` and `.engine` files.
Both are single-page `[GameData]` subsystem models stored as flat structs, so
they reuse `datamap.chain_layout()` with their own class chains:
Entity__GameModel -> Subsystem__GameModel -> Torso__GameModel 1608 bytes
Entity__GameModel -> Subsystem__GameModel -> Engine__GameModel 64 bytes
Both chains compute to exactly the record size with no anchoring, which is the
check that the member list and alignment are right.
Sources author the numbers as `$(SYMBOL)` macros from a `!include`d defines file
(`!NAME=value` syntax, not `#define`), and the record keeps only the resolved
float, so the symbol is restored by reverse lookup where one matches.
Two quirks worth keeping:
* The five Torso angles are declared plain `Stuff::Scalar` but `Torso_Tool.cpp`
multiplies them by `Radians_Per_Degree`, exactly like the Mech spring fields.
* `TotalCritLocations` is read by **nothing** in the engine -- only the 3DS Max
export plugin writes it, and the factory reads `TotalSlotsTaken`, which no
source sets (the record holds the default 1 while every source says 2). It
is emitted as the constant it always is rather than derived.
python3 smallmodel.py torso|engine <chassis-record-dir> [-o out]
python3 smallmodel.py --verify
"""
import argparse, collections, os, re, subprocess, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import datamap
C = datamap.CODE
CONTENT = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content"
SPECS = {
"torso": {
"chain": [("Entity__GameModel", C + "/mw4/Libraries/Adept/Entity.hpp"),
("Subsystem__GameModel", C + "/mw4/Code/MW4/Subsystem.hpp"),
("Torso__GameModel", C + "/mw4/Code/MW4/Torso.hpp")],
"size": 1608,
"factory": C + "/mw4/Code/MW4/Torso_Tool.cpp",
"defines": CONTENT + "/Defines/MechTorso.defines",
"include": r"Content\Defines\MechTorso.defines",
"class": "MechWarrior4::Torso",
"keys": [("TwistJointName", "twistJointName"),
("PitchJointName", "pitchJointName"),
("LeftArmJointName", "leftArmJointName"),
("RightArmJointName", "rightArmJointName"),
("EyeJointName", "eyeJointName"),
("ArmRatioAngle", "armRatioAngle"),
("TwistSpeed", "twistSpeed"),
("PitchSpeed", "pitchSpeed"),
("TwistRadius", "twistRadius"),
("PitchRadius", "pitchRadius"),
("CageJointName", "cageJointName"),
("CageRatioAngle", "cageRatioAngle")],
},
"engine": {
"chain": [("Entity__GameModel", C + "/mw4/Libraries/Adept/Entity.hpp"),
("Subsystem__GameModel", C + "/mw4/Code/MW4/Subsystem.hpp"),
("Engine__GameModel", C + "/mw4/Code/MW4/Engine.hpp")],
"size": 64,
"factory": C + "/mw4/Code/MW4/Engine_Tool.cpp",
"defines": CONTENT + "/Subsystems/HeatSink.Defines",
"include": r"Content\Subsystems\HeatSink.defines",
"class": "Mechwarrior4::Engine", # lowercase 'w' in every source
"keys": [("NumHeatSinks", "m_numHeatSinks"),
("TonsPerUpgrade", "m_tonsPerUpgrade"),
("MPSPerUpgrade", "m_mpsPerUpgrade"),
("HeatSinkEfficiency", "m_heatSinkEfficiency")],
},
}
# Written only by the 3DS Max exporter; the engine never reads it. Uniformly 2.
TOTAL_CRIT_LOCATIONS = "2"
def defines(path):
"""-> {value: symbol} from a `!NAME=value` defines file."""
out = {}
if not os.path.exists(path):
return out
txt = open(path, encoding="latin-1", errors="replace").read()
txt = re.sub(r'//[^\n]*', '', txt)
for name, val in re.findall(r'^\s*!(\w+)\s*=\s*([-\d.]+)\s*$', txt, re.M):
out.setdefault(round(float(val), 6), name)
return out
def angle_members(factory):
out = set()
if not os.path.exists(factory):
return out
txt = open(factory, encoding="latin-1").read()
for m in re.finditer(r'model->(\w+)\s*=\s*model->\w+\s*\*\s*Radians_Per_Degree', txt):
out.add(m.group(1))
return out
def record(mech_dir, kind):
for fn in os.listdir(mech_dir):
if fn.lower().endswith(f".{kind}{{gamemodel}}"):
return open(os.path.join(mech_dir, fn), "rb").read()
return None
def num(x):
return f"{int(x)}" if float(x) == int(x) else f"{x:g}"
def decompile(mech_dir, kind):
"""-> [(key, value)] for the [GameData] page."""
spec = SPECS[kind]
blob = record(mech_dir, kind)
if blob is None:
raise SystemExit(f"no .{kind}{{GameModel}} record in {mech_dir}")
layout, _ = datamap.chain_layout(spec["chain"], {})
angles = angle_members(spec["factory"])
symbols = defines(spec["defines"])
kv = [("Class", spec["class"]), ("TotalCritLocations", TOTAL_CRIT_LOCATIONS)]
for key, member in spec["keys"]:
off, typ, size = layout[member]
val = datamap.read(blob, off, typ, size, member in angles)
if typ == "char":
kv.append((key, val))
elif typ == "int":
kv.append((key, str(val)))
else:
sym = symbols.get(round(val, 6))
kv.append((key, f"$({sym})" if sym else num(val)))
return kv
def emit(kind, kv):
lines = [f"!include = {SPECS[kind]['include']}", "", "[GameData]"]
lines += [f"{k}={v}" for k, v in kv]
return "\r\n".join(lines) + "\r\n"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("kind", nargs="?", choices=sorted(SPECS))
ap.add_argument("mech_dir", nargs="?")
ap.add_argument("-o", "--output")
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_smallmodel.py")]))
if not args.kind or not args.mech_dir:
ap.error("kind and mech_dir required")
text = emit(args.kind, decompile(args.mech_dir, args.kind))
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()
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
subsystems.py - rebuild a mech's .subsystems source from its packed record.
python3 subsystems.py <record.subsystems> <manifest.tsv> [-o out.subsystems]
python3 subsystems.py --verify # check against all known chassis
The packed record is `WORD span` followed by one CreateMessage per [Page]
(MWObject::CreateSubsystemStream, MWObject_Tool.cpp:1196). Message layout and
the derivation of every offset below is documented in ../../DECOMPILING.md.
Layout beyond the Entity header (Subsystem.hpp, Weapon.hpp, Armor.hpp):
off 96 i32 subsystemIndex
off 100 u8 locationID -> InternalLocation
off 104 i32 criticalHitsTaken -> CriticalHitsTaken
Armor (152):
off 108 9xf32 armour points -> tons, see ARMOR_POINTS_PER_TON
off 144 i32 m_armorType -> ArmorType
off 148 i32 m_internalType -> InternalType
Engine (112):
off 108 i32 m_engineUpgrades -> EngineUpgrades
SearchLight (236):
off 108 char[128] siteName -> Site
Weapon (380, or 376 without the trailing field):
off 108 char[128] siteName -> Site
off 236 char[128] ejectSiteName -> EjectSite
off 364 i32 groupIndex -> GroupIndex
off 368 i32 ammoCount -> AmmoCount
off 372 i32 initialAmmoCount
off 376 i32 m_weaponFacing -> WeaponFacing (absent when len == 376)
`m_weaponFacing` is the "MSL 5.04 Rear Firing Weapons" field. V4H packed
champion/griffin/marauder without it (376 bytes) - matching their own release
note "Any new mech will not have rear facing weapons" - while dasher, jenner2c
and thunderbolt have it. Both forms are read; re-packing with our own exe always
emits the 380-byte form.
PAGE NAMES ARE NOT STORED. The packer only serialises page order, so names are
regenerated from the Model= reference with a per-kind counter. They are labels;
the runtime keys on order.
"""
import sys, os, re, glob, struct, argparse, collections
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mw4msg
OUR_MECH_SOURCE = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs"
OUR_RECORDS = "/home/rich/Repositories/FS_Ours_extracted/core/mechs"
OUR_MANIFEST = "/home/rich/Repositories/FS_Ours_extracted/_manifest.tsv"
ARMOR_DATA = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Subsystems/Armor.data"
# Derived empirically from 63 aligned chassis and cross-checked against the
# engine's own text tables.
EXEC_STATE = {1: "NeverExecuteState", 2: "AlwaysExecuteState", 6: "ActiveState"}
# InternalDamageObject enum, DamageObject.hpp:205
ZONE = {255: "NullZone", 0: "LeftLeg", 1: "RightLeg", 2: "LeftArm", 3: "RightArm",
4: "RightTorso", 5: "LeftTorso", 6: "CenterTorso", 7: "Head",
8: "Special1", 9: "Special2", 10: "VehicleHull", 11: "VehicleWeapon",
12: "VehicleSpecial", 13: "DefaultZone"}
ARMOR_TYPE = {0: "Standard", 1: "FerroFiberus", 2: "Reactive", 3: "Reflective",
4: "Solarian"}
# Armor.hpp:275 - a separate 2-value enum, not the armour table. (The engine's own
# InternalTypeAsciiToText returns the typo "Statndard"; TextToAscii wants "Standard".)
INTERNAL_TYPE = {0: "Standard", 1: "EndoSteel"}
ARMOR_KEYS = ["LeftLeg", "RightLeg", "LeftArm", "RightArm", "LeftFrontTorso",
"RightFrontTorso", "CenterFrontTorso", "CenterRearTorso", "Head"]
CLASS_ENGINE, CLASS_LAMS = 1073, 1183
# Model= basename -> page-name stem. Anything unmatched falls back to the
# weapon-category table below.
PAGE_STEM = {
"heatsinksubsystem.data": "HeatSink", "armor.data": "Armor",
"advancedgyrosubsystem.data": "AdvancedGyro", "sensorsubsystem.data": "Sensor",
"searchlightsubsystem.data": "SearchLight", "jumpjetsubsystem.data": "JumpJet",
"ecmsubsystem.data": "ECM", "beaglesubsystem.data": "Beagle",
"lams.data": "LAMS", "narcbeacon.data": "Narc",
}
WEAPON_CATEGORY = [
("laserweaponsubsystem", "Beam"), ("pulselaserweaponsubsystem", "Beam"),
("ppcweaponsubsystem", "Beam"), ("flamerweaponsubsystem", "Beam"),
("lrmweaponsubsystem", "Missile"), ("srmweaponsubsystem", "Missile"),
("ssrmweaponsubsystem", "Missile"), ("smrmweaponsubsystem", "Missile"),
("missileweaponsubsystem", "Missile"), ("narcbeaconweaponsubsystem", "Narc"),
("machinegunweaponsubsystem", "Ballistic"), ("ultraacweaponsubsystem", "Ballistic"),
("acweaponsubsystem", "Ballistic"), ("gaussweaponsubsystem", "Ballistic"),
("lbxweaponsubsystem", "Ballistic"), ("rtxweaponsubsystem", "Ballistic"),
]
def armor_points_per_ton(path=ARMOR_DATA):
txt = open(path, "rb").read().decode("latin-1")
def get(key, default):
m = re.search(rf"^{key}=(\d+)", txt, re.M | re.I)
return int(m.group(1)) if m else default
return {0: get("PointsPerStandardTon", 32), 1: get("PointsPerFerroTon", 38),
2: get("PointsPerReactiveTon", 30), 3: get("PointsPerReflectiveTon", 30),
4: get("PointsPerSolarianTon", 60)}
def load_manifest(path, package="core.mw4"):
"""record id -> entry name, for resolving dataListID back to a Model= path."""
out = {}
with open(path, encoding="latin-1") as fh:
for line in fh:
parts = line.rstrip("\n").split("\t")
if len(parts) > 2 and parts[0].lower() == package.lower():
out[int(parts[1])] = parts[2]
return out
def cstr(msg, off, size=128):
return msg[off:off + size].split(b"\0")[0].decode("latin-1").strip()
def group_flags_to_list(flags):
"""groupIndex is a BITMASK, not an index (Weapon_Tool.cpp ~76).
A page may carry several `GroupIndex=` lines; the factory ORs
`1 << (n-1)` for each. Returns the group numbers, so the caller can emit one
line per group.
"""
return [n for n in range(1, 7) if flags & (1 << (n - 1))]
def page_name(model, counters):
base = os.path.basename(model.replace("\\", "/")).lower()
stem = PAGE_STEM.get(base)
if stem is None:
low = model.lower().replace("\\", "/")
stem = next((s for key, s in WEAPON_CATEGORY if key in low), None)
if stem is None:
stem = "Torso" if base.endswith(".torso") else \
"Engine" if base.endswith(".engine") else "Subsystem"
counters[stem] += 1
# Singletons keep a bare name, matching how the sources are written.
if stem in ("Armor", "AdvancedGyro", "Sensor", "SearchLight", "Torso",
"Engine", "ECM", "Beagle", "LAMS"):
return stem if counters[stem] == 1 else f"{stem}{counters[stem]}"
return f"{stem}{counters[stem]}"
def decompile(record_path, manifest, ppt=None):
ppt = ppt or armor_points_per_ton()
data = open(record_path, "rb").read()
# Model= is written relative to the mech's own directory in the source, but the
# package stores the full entry path. Use the parent folder, not the file name:
# Black Hawk's chassis files are nova.* inside mechs/blackhawk/.
own_dir = "mechs\\" + os.path.basename(os.path.dirname(record_path)).lower() + "\\"
counters = collections.Counter()
pages = []
for _off, msg in mw4msg.walk(data):
cid, n = mw4msg.class_id(msg), len(msg)
model = manifest.get(mw4msg.record_id(msg), "?")
if model.lower().startswith(own_dir):
model = model[len(own_dir):]
kv = collections.OrderedDict()
kv["Model"] = model
kv["ExecutionState"] = EXEC_STATE.get(struct.unpack_from("<i", msg, 76)[0], "?")
kv["InternalLocation"] = ZONE.get(msg[100], f"?{msg[100]}")
crit = struct.unpack_from("<i", msg, 104)[0]
if n == 152: # Armor
vals = struct.unpack_from("<9f", msg, 108)
atype, itype = struct.unpack_from("<2i", msg, 144)
kv["ArmorType"] = ARMOR_TYPE.get(atype, str(atype))
kv["InternalType"] = INTERNAL_TYPE.get(itype, str(itype))
mult = ppt.get(atype, 32)
for key, v in zip(ARMOR_KEYS, vals):
kv[key] = f"{v / mult:g}"
elif n == 112 and cid == CLASS_ENGINE:
kv["EngineUpgrades"] = str(struct.unpack_from("<i", msg, 108)[0])
elif n == 112 and cid == CLASS_LAMS:
ammo = struct.unpack_from("<i", msg, 108)[0]
if ammo >= 0:
kv["AmmoCount"] = str(ammo)
elif n == 236: # SearchLight
kv["Site"] = cstr(msg, 108)
elif n in (376, 380): # Weapon
kv["Site"] = cstr(msg, 108)
eject = cstr(msg, 236)
grp, ammo, _init = struct.unpack_from("<3i", msg, 364)
kv["GroupIndex"] = group_flags_to_list(grp)
if ammo >= 0:
kv["AmmoCount"] = str(ammo)
if eject:
kv["EjectSite"] = eject
if n == 380:
facing = struct.unpack_from("<i", msg, 376)[0]
if facing:
kv["WeaponFacing"] = str(facing)
if crit:
kv["CriticalHitsTaken"] = str(crit)
pages.append((page_name(model, counters), kv))
return pages
def emit(pages):
out = []
for name, kv in pages:
out.append(f"[{name}]")
for k, v in kv.items():
if isinstance(v, list): # GroupIndex: one line per group
out.extend(f"{k}={n}" for n in v)
else:
out.append(f"{k}={v}")
out.append("")
return "\r\n".join(out) + "\r\n"
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("record", nargs="?")
ap.add_argument("manifest", nargs="?", default=OUR_MANIFEST)
ap.add_argument("-o", "--out")
ap.add_argument("--verify", action="store_true")
args = ap.parse_args()
if args.verify:
here = os.path.dirname(os.path.abspath(__file__))
os.execvp("python3", ["python3", os.path.join(here, "verify_subsystems.py")])
if not args.record:
ap.error("give a packed .subsystems record, or --verify")
pages = decompile(args.record, load_manifest(args.manifest))
text = emit(pages)
if args.out:
os.makedirs(os.path.dirname(args.out), exist_ok=True)
open(args.out, "wb").write(text.encode("latin-1"))
print(f"{os.path.basename(args.record)}: {len(pages)} pages -> {args.out}")
else:
sys.stdout.write(text)
if __name__ == "__main__":
main()
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Verification harness for the .armature decompiler.
Rebuilds every chassis from our own packed records and compares against the
known source .armature. Compares page transforms as a multiset keyed on
(name, transform) so duplicate page names - Victor has two [site_lshellport]
pages under different joints - are handled.
python3 verify_armature.py
"""
import sys, os, glob, re, collections
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import armature
def source_entries(path):
"""-> (list of (name, rot, trans), {parent: [child, ...]})"""
txt = open(path, "rb").read().decode("latin-1")
entries, children = [], {}
for m in re.finditer(r'^\[([^\]]+)\]\s*\r?\n(.*?)(?=^\[|\Z)', txt, re.M | re.S):
name, body = m.group(1).lower(), m.group(2)
r = re.search(r'Rotation=([-\d.eE ]+)', body)
t = re.search(r'Translation=([-\d.eE ]+)', body)
kids = [c.lower() for c in re.findall(r'Child=(\S+)', body)]
if kids:
children[name] = kids
if r and t:
entries.append((name,
tuple(float(x) for x in r.group(1).split()),
tuple(float(x) for x in t.group(1).split())))
return entries, children
def close(a, b, tol):
return a and b and max(abs(x - y) for x, y in zip(a, b)) <= tol
def angles_close(a, b, tol=0.01):
return a and b and max(abs(((x - y + 180) % 360) - 180) for x, y in zip(a, b)) <= tol
def main():
src_paths = {os.path.basename(p)[:-len(".armature")].lower(): p
for p in glob.glob(armature.OUR_MECH_SOURCE + "/*/*.armature")}
t = collections.Counter()
residual = collections.Counter()
for mech_dir in sorted(glob.glob(armature.OUR_RECORDS + "/*")):
if not glob.glob(mech_dir + "/*{armature}"):
continue
chassis, entries, children = armature.rebuild(mech_dir)
if not chassis or chassis.lower() not in src_paths:
continue
t["chassis"] += 1
src_list, src_children = source_entries(src_paths[chassis.lower()])
got = [(e["name"].lower(), e["rot"], e["trans"]) for e in entries]
pool = list(got)
for name, rot, tr in src_list:
t["pages"] += 1
hit = next((g for g in pool if g[0] == name
and close(g[2], tr, 2e-3) and angles_close(g[1], rot)), None)
if hit:
pool.remove(hit)
t["exact"] += 1
continue
near = next((g for g in pool if g[0] == name and close(g[2], tr, 2e-3)), None)
if near:
pool.remove(near)
t["rot_only"] += 1
residual[name] += 1
else:
t["missing"] += 1
residual["MISSING " + name] += 1
for parent, kids in src_children.items():
t["childlists"] += 1
if sorted(kids) == sorted(c.lower() for c in children.get(parent, ())):
t["childlists_ok"] += 1
else:
residual["CHILDREN " + parent] += 1
print(f"chassis : {t['chassis']}")
print(f"pages compared : {t['pages']}")
print(f" fully exact : {t['exact']}")
print(f" translation ok, rotation lost by the packer : {t['rot_only']}")
print(f" not recovered : {t['missing']}")
print(f"child lists : {t['childlists_ok']}/{t['childlists']} exact")
if residual:
print("\nresidual, by page:")
for k, v in residual.most_common(10):
print(f" {k:22s} x{v}")
if __name__ == "__main__":
main()
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Round-trip verifier for the .contents decompiler.
Regenerates each `.contents` and compares page names and both key values against
the authored source. Page order is not compared -- NotationFile is
order-independent, and the packer groups pages by parent joint rather than
preserving the authored sequence.
python3 verify_contents.py [--show CHASSIS]
"""
import argparse, collections, glob, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import contents, subsystems
REC = "/home/rich/Repositories/FS_Ours_extracted/core/mechs"
SRC = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs"
def source_pages(path):
txt = open(path, "rb").read().decode("latin-1")
txt = re.sub(r'//[^\n]*', '', txt)
out = {}
for m in re.finditer(r'^\[([^\]]+)\]\r?\n(.*?)(?=^\[|\Z)', txt, re.M | re.S):
name, body = m.group(1), m.group(2)
if name.lower() == "includes":
continue
kv = dict(re.findall(r'^([A-Za-z_]\w*)=([^\r\n]*)', body, re.M))
out[name.lower()] = {k.lower(): v.strip().lower().replace("/", "\\")
for k, v in kv.items()}
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--show")
args = ap.parse_args()
manifest = subsystems.load_manifest(contents.MANIFEST)
t = collections.Counter()
bad = collections.Counter()
examples = []
missing_pages = collections.Counter()
extra_pages = collections.Counter()
for d in sorted(glob.glob(REC + "/*")):
ch = os.path.basename(d)
src = [p for p in glob.glob(SRC + "/*/*.contents")
if os.path.basename(p).lower() == ch.lower() + ".contents"]
if not src:
continue
t["chassis"] += 1
want = source_pages(src[0])
chassis, pages = contents.decompile(d, manifest)
got = {n.lower(): {k.lower(): (v or "").lower().replace("/", "\\")
for k, v in kv} for n, kv in pages}
if args.show and args.show.lower() == ch.lower():
sys.stdout.write(contents.emit(chassis, pages))
return
for name in want:
if name not in got:
missing_pages[name] += 1
for name in got:
if name not in want:
extra_pages[name] += 1
for name in set(want) & set(got):
t["pages"] += 1
for key in ("model", "executionstate"):
t["keys"] += 1
if want[name].get(key) == got[name].get(key):
t["ok"] += 1
else:
bad[key] += 1
if len(examples) < 10:
examples.append((ch, name, key,
want[name].get(key), got[name].get(key)))
print(f"chassis : {t['chassis']}")
print(f"pages compared : {t['pages']}")
print(f"keys compared : {t['keys']} exact: {t['ok']} wrong: {t['keys'] - t['ok']}")
if missing_pages:
print(f"\npages in source but not decoded: {sum(missing_pages.values())}")
for n, c in missing_pages.most_common(10):
print(f" {n:28s} x{c}")
if extra_pages:
print(f"\npages decoded but not in source: {sum(extra_pages.values())}")
for n, c in extra_pages.most_common(10):
print(f" {n:28s} x{c}")
if bad:
print("\nvalue mismatches:")
for k, c in bad.most_common():
print(f" {k:20s} x{c}")
for e in examples:
print(f" {e[0]:14s} {e[1]:24s} {e[2]:16s} want={e[3]!r} got={e[4]!r}")
if __name__ == "__main__":
main()
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Round-trip verifier for the .damage decompiler.
Regenerates a whole `.damage` from the compiled record for every chassis and
compares it page by page against the authored source: page names, page order,
key sets and values.
Comparison is semantic. Sources carry an `!include` line and comments, spell
numbers freely (`.99` vs `0.99`, `1.0` vs `1`), and Windows path lookup is
case-insensitive, so none of those count as differences.
python3 verify_damage.py [--show CHASSIS]
"""
import argparse, collections, glob, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import damage, subsystems
REC = "/home/rich/Repositories/FS_Ours_extracted/core/mechs"
SRC = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs"
NUM = re.compile(r'^-?(?:\d+\.?\d*|\.\d+)$')
def canon(v):
v = str(v).strip()
if NUM.match(v):
return f"{float(v):.4g}"
if "," in v: # DamageEffect: path,percent
path, _, pct = v.rpartition(",")
return canon(path) + "," + (f"{float(pct):.4g}" if NUM.match(pct.strip()) else pct)
v = re.sub(r'^content[\\/]', '', v.replace("/", "\\"), flags=re.I)
return v.lower()
def source_pages(path):
"""-> [(pageName, [(key, value)])] preserving order and repeats."""
txt = open(path, "rb").read().decode("latin-1")
txt = re.sub(r'//[^\n]*', '', txt)
pages, cur = [], None
for line in txt.splitlines():
line = line.strip()
if not line or line.startswith("!"):
continue
m = re.match(r'^\[([^\]]+)\]$', line)
if m:
cur = (m.group(1), [])
pages.append(cur)
elif "=" in line and cur is not None:
k, v = line.split("=", 1)
cur[1].append((k.strip(), v.strip()))
return pages
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--show")
args = ap.parse_args()
manifest = subsystems.load_manifest(damage.MANIFEST)
t = collections.Counter()
bad = collections.Counter()
examples = []
order_problems = []
for d in sorted(glob.glob(REC + "/*")):
ch = os.path.basename(d)
src = [p for p in glob.glob(SRC + "/*/*.damage")
if os.path.basename(p).lower() == ch.lower() + ".damage"]
if not src or not glob.glob(os.path.join(d, "*.damage")):
continue
t["chassis"] += 1
want = source_pages(src[0])
got = damage.decompile(d, manifest)
if args.show and args.show.lower() == ch.lower():
sys.stdout.write(damage.emit(got))
return
if [n.lower() for n, _ in want] != [n.lower() for n, _ in got]:
order_problems.append((ch, [n for n, _ in want], [n for n, _ in got]))
continue
t["pages"] += len(want)
for (wname, wkv), (_gname, gkv) in zip(want, got):
wmap = collections.defaultdict(list)
for k, v in wkv:
wmap[k.lower()].append(canon(v))
gmap = collections.defaultdict(list)
for k, v in gkv:
gmap[k.lower()].append(canon(v))
for key in set(wmap) | set(gmap):
t["keys"] += 1
w, g = sorted(wmap.get(key, [])), sorted(gmap.get(key, []))
# The writer defaults MaxArmorValue to BaseArmorValue, so a source
# that omits it and one that states it equal produce identical
# bytes -- 51 pages do state it. The distinction is unrecoverable
# and harmless, so emitting it explicitly counts as a match.
if not w and key == "maxarmorvalue" and g == sorted(gmap.get("basearmorvalue", [])):
t["ok"] += 1
t["implicit_max"] += 1
elif w == g:
t["ok"] += 1
else:
bad[key] += 1
if len(examples) < 12:
examples.append((ch, wname, key, wmap.get(key), gmap.get(key)))
print(f"chassis : {t['chassis']}")
print(f"pages compared : {t['pages']}")
print(f"keys compared : {t['keys']} exact: {t['ok']} wrong: {t['keys'] - t['ok']}")
print(f" MaxArmorValue omitted by source, implied by BaseArmorValue: {t['implicit_max']}")
if order_problems:
print(f"\npage name/order mismatches: {len(order_problems)}")
for ch, w, g in order_problems[:3]:
print(f" {ch}\n want {w}\n got {g}")
if bad:
print("\nkeys not matching:")
for k, n in bad.most_common(15):
print(f" {k:28s} x{n}")
print("\nexamples (chassis, page, key, source, decoded):")
for e in examples:
print(f" {e[0]:14s} {e[1]:22s} {e[2]:22s} {e[3]} != {e[4]}")
if __name__ == "__main__":
main()
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Verification harness for the mech .data{GameModel} field map.
Reads every mapped value through its header-derived offset and compares against
the source .data, across all 64 chassis we hold both forms for.
Unlike a value-discovered map this one can genuinely fail: the offsets come from
declaration order, so a mis-parsed header shows up at once as a whole column of
wrong values. Keys that miss consistently by a constant factor are reported with
their decoded/source ratio, which is how unit conversions (deg -> rad, kph ->
m/s) announce themselves.
python3 verify_data.py
"""
import sys, os, re, collections
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import datamap
def close(a, b):
return abs(a - b) <= max(1e-4, abs(b) * 1e-5)
def matches(got, want_text, typ):
"""Compare a decoded value against the raw source text."""
if typ in datamap.VECTORS:
nums = [float(x) for x in re.findall(r'-?\d+\.?\d*(?:[eE][-+]?\d+)?', want_text)]
return len(nums) == len(got) and all(close(a, b) for a, b in zip(got, nums))
if typ == "char":
return got.lower() == want_text.strip().lower()
if typ in ("bool", "BYTE"):
w = want_text.strip().lower()
if w in ("true", "yes", "1"):
return got == 1
if w in ("false", "no", "0"):
return got == 0
return None
try:
return close(float(got), float(want_text))
except ValueError:
return None
def main():
pairs, field_map = datamap.build()
t = collections.Counter()
bad = collections.Counter()
examples = []
for ch, kv, blob in pairs:
for key, (off, typ, size, angle) in field_map.items():
if key not in kv:
continue
got = datamap.read(blob, off, typ, size, angle)
ok = matches(got, kv[key], typ)
if ok is None:
t["skipped"] += 1
continue
t["values"] += 1
if ok:
t["ok"] += 1
else:
bad[key] += 1
if len(examples) < 12:
examples.append((ch, key, typ, kv[key], got, off))
print(f"chassis : {len(pairs)}")
print(f"mapped keys : {len(field_map)}")
print(f"values compared : {t['values']} exact: {t['ok']} wrong: {t['values'] - t['ok']}")
print(f"not comparable : {t['skipped']} (enum/resource text, decoded separately)")
if bad:
print("\nkeys not matching:")
for k, v in bad.most_common(25):
print(f" {k:34s} x{v}")
print("\nexamples (chassis, key, type, source, decoded, offset):")
for ch, key, typ, src, got, off in examples:
print(f" {ch:14s} {key:28s} {typ:10s} src={src!r:24s} got={got!r} @{off}")
if __name__ == "__main__":
main()
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Round-trip verifier for the .instance decompiler.
Regenerates each `.instance` and compares the page name and every key against
the authored source. Keys the source carries but the decompiler does not emit
are reported separately, so an unhandled key can never be mistaken for a pass.
python3 verify_instance.py [--show CHASSIS]
"""
import argparse, collections, glob, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import instance, subsystems
REC = "/home/rich/Repositories/FS_Ours_extracted/core/mechs"
SRC = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs"
NUM = re.compile(r'^-?(?:\d+\.?\d*|\.\d+)$')
def canon(v):
v = str(v).strip()
if NUM.match(v):
return f"{float(v):.5g}"
return re.sub(r'^content[\\/]', '', v.replace("/", "\\"), flags=re.I).lower()
def source_page(path):
txt = re.sub(r'//[^\n]*', '', open(path, "rb").read().decode("latin-1"))
m = re.search(r'^\[([^\]]+)\]', txt, re.M)
kv = [(k, v.strip()) for k, v in re.findall(r'^([A-Za-z]\w*)=([^\r\n]*)', txt, re.M)]
return (m.group(1) if m else None), kv
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--show")
args = ap.parse_args()
manifest = subsystems.load_manifest(instance.MANIFEST)
t = collections.Counter()
bad = collections.Counter()
unhandled = collections.Counter()
examples = []
name_bad = []
for d in sorted(glob.glob(REC + "/*")):
ch = os.path.basename(d)
src = [p for p in glob.glob(SRC + "/*/*.instance")
if os.path.basename(p).lower() == ch.lower() + ".instance"]
if not src or not glob.glob(os.path.join(d, "*.instance")):
continue
t["chassis"] += 1
want_name, want = source_page(src[0])
got_name, got = instance.decompile(d, manifest)
if args.show and args.show.lower() == ch.lower():
sys.stdout.write(instance.emit(got_name, got))
return
if (want_name or "").lower() != (got_name or "").lower():
name_bad.append((ch, want_name, got_name))
gmap = {k.lower(): v for k, v in got}
for key, wv in want:
if key.lower() not in gmap:
unhandled[key] += 1
continue
t["keys"] += 1
if canon(wv) == canon(gmap[key.lower()]):
t["ok"] += 1
else:
bad[key] += 1
if len(examples) < 12:
examples.append((ch, key, wv, gmap[key.lower()]))
print(f"chassis : {t['chassis']}")
print(f"page names : {t['chassis'] - len(name_bad)}/{t['chassis']} match")
print(f"keys compared : {t['keys']} exact: {t['ok']} wrong: {t['keys'] - t['ok']}")
if unhandled:
print("\nkeys present in source but NOT emitted:")
for k, n in unhandled.most_common():
print(f" {k:26s} x{n}")
if name_bad:
print("\npage name mismatches:")
for e in name_bad[:5]:
print(f" {e[0]:14s} want={e[1]!r} got={e[2]!r}")
if bad:
print("\nvalue mismatches:")
for k, n in bad.most_common(15):
print(f" {k:26s} x{n}")
print("\nexamples (chassis, key, source, decoded):")
for e in examples:
print(f" {e[0]:14s} {e[1]:22s} want={e[2]!r:32s} got={e[3]!r}")
if __name__ == "__main__":
main()
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""Round-trip verifier for the mech .data decompiler.
Regenerates a whole `.data` from the compiled records for every chassis we hold
both forms of, and compares it key by key against the authored source. This is
the standard `.armature` and `.subsystems` were held to; nothing should be
emitted for the new chassis until this passes.
Comparison is semantic, not byte-exact: NotationFile is order-independent, and
Windows path lookup is case-insensitive (our own tree writes both
`mechs\\atlas_destroyed\\...` and `Mechs\\Atlas_Destroyed\\...` for the same
key), so keys are matched case-insensitively and numbers within tolerance.
python3 verify_roundtrip.py [--show CHASSIS]
"""
import argparse, collections, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import datamap, data, subsystems
REC = "/home/rich/Repositories/FS_Ours_extracted/core/mechs"
NUM = re.compile(r'^-?\d+\.?\d*(?:[eE][-+]?\d+)?$')
def canon(v):
"""Normalise one value for comparison."""
v = str(v).strip()
v = re.sub(r'^content[\\/]', '', v.replace("/", "\\"), flags=re.I)
v = re.sub(r'\s+', " ", v).lower()
# normalise every number in place so "20.0 20.0 20.0" == "20 20 20"
return re.sub(r'-?\d+\.\d+(?:[eE][-+]?\d+)?|-?\d+',
lambda m: f"{float(m.group(0)):.4g}", v)
def canon_set(val):
"""Sorted, de-duplicated: some sources repeat a key with the same value."""
return sorted({canon(v) for v in (val if isinstance(val, list) else [val])})
def source_kv(path):
"""Authored [GameData] page, keeping repeated keys as lists."""
txt = open(path, "rb").read().decode("latin-1")
# protect braced blocks first: Shadow={...} contains a line reading
# "[shadow]", which otherwise looks like the start of the next page.
# Both CR and LF must go -- splitlines() splits on a bare CR too.
txt = re.sub(r'\{.*?\}',
lambda x: x.group(0).replace("\r", "").replace("\n", "\x01"),
txt, flags=re.S)
m = re.search(r'^\[GameData\]\r?\n(.*?)(?=^\[[A-Za-z]|\Z)', txt, re.M | re.S)
body = m.group(1) if m else ""
kv = collections.OrderedDict()
for line in body.splitlines():
if "=" not in line or line.lstrip().startswith("//"):
continue
k, v = line.split("=", 1)
kv.setdefault(k.strip(), []).append(v.replace("\x01", "\n").strip())
return kv
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--show")
args = ap.parse_args()
manifest = subsystems.load_manifest(data.MANIFEST)
totals = collections.Counter()
wrong = collections.Counter()
missing = collections.Counter()
extra = collections.Counter()
examples = []
chassis = 0
for ch, kv_ignored, _blob in datamap.corpus():
src_path = [p for p in
__import__("glob").glob(datamap.SRC + "/*/*.data")
if os.path.basename(p).lower() == ch.lower() + ".data"]
if not src_path:
continue
chassis += 1
want = source_kv(src_path[0])
got = data.decompile(os.path.join(REC, ch), manifest)
got_l = {k.lower(): v for k, v in got.items()}
for key, wvals in want.items():
if key in data.UNRECOVERABLE:
totals["omitted"] += 1
continue
totals["keys"] += 1
if key.lower() not in got_l:
missing[key] += 1
continue
if canon_set(got_l[key.lower()]) == canon_set(wvals):
totals["ok"] += 1
else:
wrong[key] += 1
if len(examples) < 12:
examples.append((ch, key, wvals, got_l[key.lower()]))
for key in got:
if key.lower() not in {k.lower() for k in want}:
extra[key] += 1
if args.show and args.show.lower() == ch.lower():
sys.stdout.write(data.emit(got))
return
print(f"chassis : {chassis}")
print(f"keys compared : {totals['keys']} exact: {totals['ok']} "
f"wrong: {sum(wrong.values())} missing: {sum(missing.values())}")
print(f"deliberately omitted: {totals['omitted']} (unreadable by the engine)")
if missing:
print("\nmissing from output:")
for k, n in missing.most_common(15):
print(f" {k:30s} x{n}")
if wrong:
print("\nvalue mismatches:")
for k, n in wrong.most_common(15):
print(f" {k:30s} x{n}")
print("\nexamples:")
for ch, k, w, g in examples:
print(f" {ch:12s} {k:26s} want={w!r:44s} got={g!r}")
if extra:
print("\nemitted but not in source:")
for k, n in extra.most_common(10):
print(f" {k:30s} x{n}")
if __name__ == "__main__":
main()
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Round-trip verifier for the .torso and .engine decompilers.
Regenerates both files for every chassis and compares key order and values
against the authored source. `$(SYMBOL)` and its numeric expansion are treated
as equal, since the record stores only the resolved float.
python3 verify_smallmodel.py [--show KIND CHASSIS]
"""
import argparse, collections, glob, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import smallmodel
REC = "/home/rich/Repositories/FS_Ours_extracted/core/mechs"
SRC = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content/Mechs"
NUM = re.compile(r'^-?(?:\d+\.?\d*|\.\d+)$')
def resolve(value, symbols):
"""$(NAME) -> its numeric value, so symbol and literal compare equal."""
m = re.fullmatch(r'\$\((\w+)\)', value.strip())
if m:
return symbols.get(m.group(1).lower(), value.strip().lower())
return value.strip().lower()
def canon(value, symbols):
v = resolve(value, symbols)
return f"{float(v):.5g}" if NUM.match(str(v)) else str(v)
def symbol_values(path):
"""-> {symbolNameLower: numericString}"""
out = {}
if os.path.exists(path):
txt = re.sub(r'//[^\n]*', '', open(path, encoding="latin-1", errors="replace").read())
for name, val in re.findall(r'^\s*!(\w+)\s*=\s*([-\d.]+)\s*$', txt, re.M):
out[name.lower()] = val
return out
def source_kv(path):
txt = re.sub(r'//[^\n]*', '', open(path, "rb").read().decode("latin-1"))
return [(m.group(1), m.group(2).strip())
for m in re.finditer(r'^([A-Za-z]\w*)=([^\r\n]*)', txt, re.M)]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--show", nargs=2, metavar=("KIND", "CHASSIS"))
args = ap.parse_args()
t = collections.Counter()
bad = collections.Counter()
examples = []
order_bad = []
for kind, spec in sorted(smallmodel.SPECS.items()):
symbols = symbol_values(spec["defines"])
for d in sorted(glob.glob(REC + "/*")):
ch = os.path.basename(d)
src = [p for p in glob.glob(f"{SRC}/*/*.{kind}")
if os.path.basename(p).lower() == f"{ch.lower()}.{kind}"]
if not src or smallmodel.record(d, kind) is None:
continue
t[f"{kind} files"] += 1
want = source_kv(src[0])
got = smallmodel.decompile(d, kind)
if args.show and args.show[0] == kind and args.show[1].lower() == ch.lower():
sys.stdout.write(smallmodel.emit(kind, got))
return
if [k.lower() for k, _ in want] != [k.lower() for k, _ in got]:
order_bad.append((kind, ch, [k for k, _ in want], [k for k, _ in got]))
continue
for (wk, wv), (_gk, gv) in zip(want, got):
t["keys"] += 1
if canon(wv, symbols) == canon(gv, symbols):
t["ok"] += 1
else:
bad[f"{kind}.{wk}"] += 1
if len(examples) < 12:
examples.append((kind, ch, wk, wv, gv))
for kind in sorted(smallmodel.SPECS):
print(f"{kind:8s} files : {t[kind + ' files']}")
print(f"keys compared : {t['keys']} exact: {t['ok']} wrong: {t['keys'] - t['ok']}")
if order_bad:
print(f"\nkey order mismatches: {len(order_bad)}")
for kind, ch, w, g in order_bad[:3]:
print(f" {kind} {ch}\n want {w}\n got {g}")
if bad:
print("\nvalue mismatches:")
for k, n in bad.most_common(15):
print(f" {k:34s} x{n}")
print("\nexamples (kind, chassis, key, source, decoded):")
for e in examples:
print(f" {e[0]:7s} {e[1]:14s} {e[2]:22s} want={e[3]!r:22s} got={e[4]!r}")
if __name__ == "__main__":
main()
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Verification harness for the .subsystems decompiler.
Rebuilds every chassis from our own packed records and compares the resulting
key/value pairs, page by page, against the known source .subsystems.
Page names are ignored - the packer does not store them (see subsystems.py), so
only order and content can be verified.
Chassis whose source has moved on since the package was built are reported
separately rather than counted as failures; core.mw4 has not been repacked since
the initial mirror, so battlemaster and battlemaster2c legitimately differ.
python3 verify_subsystems.py
"""
import sys, os, glob, re, collections
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import subsystems
def source_pages(path):
"""Parse a source .subsystems leniently.
The runtime NotationFile accepts a page header with no closing bracket -
hellspawn had '[HeatSink10' and sunder '[HeatSink16' - so match on a leading
'[' rather than a full bracketed pattern.
GroupIndex may appear several times in one page, so values are collected as
lists rather than overwritten.
"""
txt = open(path, "rb").read().decode("latin-1")
out = []
for m in re.finditer(r'^\[([^\r\n\]]*)\]?[ \t]*\r?\n(.*?)(?=^\[|\Z)', txt, re.M | re.S):
kv = collections.OrderedDict()
for k, v in re.findall(r'^([A-Za-z0-9_]+)=([^\r\n]*)', m.group(2), re.M):
if k in kv:
kv[k] = (kv[k] if isinstance(kv[k], list) else [kv[k]]) + [v]
else:
kv[k] = v
out.append((m.group(1), kv))
return out
def norm(key, value):
"""Compare as a sorted list so a single value and a one-element list match."""
if not isinstance(value, list):
value = [value]
return sorted(_norm1(key, v) for v in value)
def _norm1(key, value):
v = str(value).strip()
if key in ("Model", "Site", "EjectSite", "InternalLocation", "ExecutionState",
"ArmorType", "InternalType"):
return v.lower().replace("\\", "/")
try:
return f"{float(v):.4g}"
except ValueError:
return v.lower()
def main():
manifest = subsystems.load_manifest(subsystems.OUR_MANIFEST)
ppt = subsystems.armor_points_per_ton()
srcs = {os.path.basename(p)[:-len(".subsystems")].lower(): p
for p in glob.glob(subsystems.OUR_MECH_SOURCE + "/*/*.subsystems")}
t = collections.Counter()
mismatched_keys = collections.Counter()
stale, examples = [], []
for rec in sorted(glob.glob(subsystems.OUR_RECORDS + "/*/*.subsystems")):
chassis = os.path.basename(rec)[:-len(".subsystems")].lower()
if chassis not in srcs:
continue
src = source_pages(srcs[chassis])
got = subsystems.decompile(rec, manifest, ppt)
if len(src) != len(got):
stale.append((chassis, len(got), len(src)))
continue
t["chassis"] += 1
for (_sname, skv), (_gname, gkv) in zip(src, got):
t["pages"] += 1
page_ok = True
for key, sval in skv.items():
if key in ("SubsystemIndex",): # not emitted; defaults to 0
continue
# An explicit "=0" and an omitted key are identical to the packer.
if gkv.get(key) is None and str(sval).strip() in ("0", "0.0"):
continue
t["keys"] += 1
gval = gkv.get(key)
if gval is not None and norm(key, gval) == norm(key, sval):
t["keys_ok"] += 1
else:
page_ok = False
mismatched_keys[key] += 1
if len(examples) < 12:
examples.append((chassis, key, sval, gval))
extra = [k for k in gkv if k not in skv]
if extra:
page_ok = False
for k in extra:
mismatched_keys["EXTRA:" + k] += 1
t["pages_ok"] += page_ok
print(f"chassis verified : {t['chassis']}")
print(f"pages compared : {t['pages']} fully exact: {t['pages_ok']}")
print(f"keys compared : {t['keys']} exact: {t['keys_ok']}"
f" wrong: {t['keys'] - t['keys_ok']}")
if mismatched_keys:
print("\nmismatches by key:")
for k, v in mismatched_keys.most_common(12):
print(f" {k:22s} x{v}")
if examples:
print("\nexamples (chassis, key, source, decompiled):")
for e in examples:
print(f" {e[0]:14s} {e[1]:18s} src={e[2]!r:28s} got={e[3]!r}")
if stale:
print("\nskipped - source has moved on since the package was built:")
for ch, g, s in stale:
print(f" {ch:16s} messages={g} sourcePages={s}")
if __name__ == "__main__":
main()
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""
diffindex.py - case-insensitive package/entry diff between two mw4index manifests.
python3 diffindex.py <A.tsv> <B.tsv> [labelA] [labelB]
Per package it prints:
* name-set diff - which source assets exist on each side (the reliable signal)
* blob multiset - stored-byte md5 counts, names ignored (a rough upper bound
on how much payload differs; see the caveat in mw4index.py)
Packages present on only one side are reported as A-ONLY / B-ONLY.
"""
import sys, collections
def load(path):
names = collections.defaultdict(set)
blobs = collections.defaultdict(collections.Counter)
sizes = collections.defaultdict(dict)
with open(path, encoding="latin-1") as fh:
for line in fh:
line = line.rstrip("\n")
if not line:
continue
pkg, _rid, name, dlen, _rlen, h = line.split("\t")
pkg = pkg.lower()
key = name.lower().replace("\\", "/")
names[pkg].add(key)
blobs[pkg][h] += 1
sizes[pkg][key] = int(dlen)
return names, blobs, sizes
def main():
if len(sys.argv) < 3:
sys.exit(__doc__)
la = sys.argv[3] if len(sys.argv) > 3 else "A"
lb = sys.argv[4] if len(sys.argv) > 4 else "B"
an, ab, asz = load(sys.argv[1])
bn, bb, bsz = load(sys.argv[2])
for pkg in sorted(set(an) | set(bn)):
if pkg not in an:
print(f"== {pkg}: {lb}-ONLY PACKAGE ({len(bn[pkg])} entries)")
continue
if pkg not in bn:
print(f"== {pkg}: {la}-ONLY PACKAGE ({len(an[pkg])} entries)")
continue
aonly = sorted(an[pkg] - bn[pkg])
bonly = sorted(bn[pkg] - an[pkg])
shared = sum((ab[pkg] & bb[pkg]).values())
print(f"== {pkg}: {la}={len(an[pkg])} {lb}={len(bn[pkg])} | "
f"names: common={len(an[pkg] & bn[pkg])} {la}_only={len(aonly)} {lb}_only={len(bonly)} | "
f"blobs: identical={shared} {la}_uniq={sum(ab[pkg].values()) - shared} "
f"{lb}_uniq={sum(bb[pkg].values()) - shared}")
for n in aonly:
print(f" +{la} {n} ({asz[pkg][n]}B)")
for n in bonly:
print(f" -{lb} {n} ({bsz[pkg][n]}B)")
if __name__ == "__main__":
main()
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""
dumprec.py - extract one record from a *.mw4, decoded.
python3 dumprec.py <pkg.mw4> "<entry name>" [outfile]
The entry name is matched case-insensitively with backslashes normalised to
'/', e.g. "mechs/atlas/atlas.subsystems". With no outfile the bytes go to
stdout. Pass "--list" as the entry name to print every entry name instead.
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mw4db import read_records, norm
def main():
if len(sys.argv) < 3:
sys.exit(__doc__)
path, want = sys.argv[1], norm(sys.argv[2])
_content, recs = read_records(path)
if want == "--list":
for rid, name, dlen, rlen, _blob in recs:
print(f"{rid}\t{name}\t{dlen}\t{rlen}")
return
for _rid, name, _dlen, _rlen, blob in recs:
if norm(name) == want:
if len(sys.argv) > 3:
open(sys.argv[3], "wb").write(blob)
else:
sys.stdout.buffer.write(blob)
return
sys.exit(f"not found: {want}")
if __name__ == "__main__":
main()
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
extract-all.py - unpack every *.mw4 under a resource root into a real directory tree.
python3 extract-all.py <resourceRoot> <outDir> [--no-merged]
Layout
------
<outDir>/<packagePathWithoutExtension>/<entryPath>
e.g. core.mw4 entry mechs\\atlas\\atlas.subsystems
-> <outDir>/core/mechs/atlas/atlas.subsystems
Missions/freezer.mw4 entry missions\\freezer\\freezer.contents
-> <outDir>/Missions/freezer/missions/freezer/freezer.contents
This is lossless: 842 entry paths are claimed by more than one package (281 of
them with different content - typically a mission packing its own copy of a
global props.mw4 asset), so a single flat tree cannot represent the data.
<outDir>/_merged/ is then built as a flattened view of the whole set using
HARDLINKS (no extra disk). It mirrors the layout of Gameleap/mw4/Content, which
makes it directly diffable against our source tree. Where two packages disagree,
precedence is props > core > textures > maps > missions and every conflict is
listed in _conflicts.tsv.
Entry-name qualifiers are preserved verbatim in the filename:
foo.data{gamemodel} foo.contents[joint_hip]{armature} bar.tga{hint}
All characters used by these packages are legal on Linux ('!' '#' '$' '%' '^'
appear in lobby skin names; no ':' or NUL).
"""
import sys, os, hashlib
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mw4db import read_records, walk_packages
# Higher wins when the same entry path comes from several packages.
PRECEDENCE = {"props": 50, "core": 40, "textures": 30, "maps": 20, "missions": 10}
# Saved mechlab loadouts and pilot options name their records '{Mech}',
# '<chassis>{Subsystem}' etc. with no directory part, so they have no place in a
# Content-shaped tree - 797 variants would all collide on the same few names.
MERGE_EXCLUDE = ("variants/", "pilots/")
def in_merged(pkgrel):
return not pkgrel.lower().startswith(MERGE_EXCLUDE)
def precedence(pkgrel):
head = pkgrel.split("/")[0].lower()
if head.endswith(".mw4"):
head = head[:-4]
return PRECEDENCE.get(head, 0)
def entry_path(name):
"""Entry name -> relative filesystem path. Qualifiers kept as-is."""
p = name.replace("\\", "/").strip("/")
# Defensive: never let an entry escape the output directory.
parts = [seg for seg in p.split("/") if seg not in ("", ".", "..")]
return "/".join(parts) if parts else "_unnamed"
def main():
args = [a for a in sys.argv[1:] if not a.startswith("--")]
if len(args) != 2:
sys.exit(__doc__)
root, out = args
want_merged = "--no-merged" not in sys.argv
os.makedirs(out, exist_ok=True)
manifest = open(os.path.join(out, "_manifest.tsv"), "w", encoding="utf-8")
manifest.write("package\trecordId\tentryName\tdataLen\trecLen\tmd5\toutPath\n")
notes = open(os.path.join(out, "_notes.txt"), "w", encoding="utf-8")
# merged bookkeeping: mergedRelPath -> (precedence, pkgrel, md5, absSourceFile)
best = {}
seen_paths = {} # mergedRelPath -> {md5: [pkgrel, ...]}
total_files = total_bytes = 0
packages = 0
for path, pkgrel in walk_packages(root):
res = read_records(path)
if res is None:
notes.write(f"SKIP not-a-#VBD-package: {pkgrel}\n")
continue
_content, recs = res
packages += 1
pkgdir = os.path.join(out, pkgrel[:-4] if pkgrel.lower().endswith(".mw4") else pkgrel)
written = {} # relPath -> md5, for intra-package dupes
for rid, name, dlen, rlen, blob in recs:
rel = entry_path(name)
h = hashlib.md5(blob).hexdigest()
if rel in written: # duplicate entry name in one package
if written[rel] == h:
notes.write(f"DUP-IDENTICAL {pkgrel}\t{name}\trec{rid}\n")
continue
rel = f"{rel}#rec{rid}"
notes.write(f"DUP-DIFFERENT {pkgrel}\t{name}\trec{rid} -> {rel}\n")
written[rel] = h
dest = os.path.join(pkgdir, rel)
os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, "wb") as fh:
fh.write(blob)
total_files += 1
total_bytes += len(blob)
manifest.write(f"{pkgrel}\t{rid}\t{name}\t{dlen}\t{rlen}\t{h}\t"
f"{os.path.relpath(dest, out)}\n")
if want_merged and in_merged(pkgrel):
seen_paths.setdefault(rel, {}).setdefault(h, []).append(pkgrel)
pr = precedence(pkgrel)
cur = best.get(rel)
if cur is None or pr > cur[0]:
best[rel] = (pr, pkgrel, h, dest)
print(f" {pkgrel}: {len(recs)} records", flush=True)
manifest.close()
conflicts = 0
if want_merged:
mroot = os.path.join(out, "_merged")
with open(os.path.join(out, "_conflicts.tsv"), "w", encoding="utf-8") as cf:
cf.write("mergedPath\tchosenPackage\tallVersions\n")
for rel, (_pr, pkgrel, _h, src) in best.items():
dest = os.path.join(mroot, rel)
os.makedirs(os.path.dirname(dest), exist_ok=True)
if os.path.lexists(dest):
os.unlink(dest)
os.link(src, dest)
versions = seen_paths[rel]
if len(versions) > 1:
conflicts += 1
detail = "; ".join(f"{h[:8]}={','.join(ps)}" for h, ps in versions.items())
cf.write(f"{rel}\t{pkgrel}\t{detail}\n")
notes.write(f"\npackages={packages} files={total_files} bytes={total_bytes} "
f"mergedPaths={len(best)} conflicts={conflicts}\n")
notes.close()
print(f"\npackages : {packages}")
print(f"files : {total_files}")
print(f"bytes : {total_bytes/1e9:.2f} GB")
if want_merged:
print(f"merged : {len(best)} paths, {conflicts} with conflicting versions")
print(f"out : {out}")
if __name__ == "__main__":
main()
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
mw4db.py - reader for GameOS "#VBD" resource packages (*.mw4).
Format (from mw4\\Libraries\\stuff\\Database.cpp):
header (20 bytes, little-endian)
char[4] tag "#VBD"
u32 format
u32 contentVersion (63 for this codebase, VER_CONTENTVERSION)
u32 indexSize byte offset of the payload area
u16 recordCount
u16 nextId
then `recordCount` directory records, packed, no alignment:
s64 FILETIME when this entry was last (re)packed
u32 dataLength decompressed size
u32 recLength stored size
u32 dataOffset offset from `indexSize`
u16 recordId
u8 nameLength
char[] name (latin-1, backslash-separated, may carry
{qualifier} / [page] suffixes)
payload at indexSize + dataOffset, `recLength` bytes.
Storage rule (Database.cpp:451): recLength == dataLength -> raw,
otherwise LZW-compressed with gos_LZCompress.
The decompressor is a faithful port of gos_LZDecompress from
CoreTech\\Libraries\\GameOS\\FileIO.cpp - variable-width LZW, LSB-first,
9 -> 12 bits, CLEAR = 256, EOF = 257, dictionary starts at 258.
"""
import struct, hashlib, os
def lz_decompress(src: bytes) -> bytes:
src = bytes(src) + b"\x00\x00\x00\x00" # u32-read padding margin
bitpos = 0
def getcode(width, mask):
nonlocal bitpos
i = bitpos >> 3
v = src[i] | (src[i + 1] << 8) | (src[i + 2] << 16) | (src[i + 3] << 24)
v = (v >> (bitpos & 7)) & mask
bitpos += width
return v
CLEAR, EOF, FREE = 256, 257, 258
chain = [0] * 8192
suffix = [0] * 8192
width, mask, maxindex, free = 9, 511, 512, FREE
oldchain = oldsuffix = 0
out = bytearray()
while True:
code = getcode(width, mask)
if code == EOF:
break
if code == CLEAR:
width, mask, maxindex, free = 9, 511, 512, FREE
code = getcode(width, mask) # ClearHash emits one literal
out.append(code & 0xFF)
oldchain, oldsuffix = code, code & 0xFF
continue
stack = []
if code >= free: # KwKwK special case
stack.append(oldsuffix)
suffix[code] = oldsuffix
chain[code] = oldchain
walk = oldchain
else:
walk = code
while walk >= 256:
stack.append(suffix[walk])
walk = chain[walk]
stack.append(walk)
oldsuffix = walk & 0xFF
out += bytes(reversed(stack))
suffix[free] = oldsuffix
chain[free] = oldchain
free += 1
oldchain = code
if free == maxindex and width != 12:
width += 1
maxindex <<= 1
mask = (mask << 1) | 1
return bytes(out)
def read_index(path):
"""Parse directory only. Fast - no decompression.
Returns (contentVersion, [(recordId, name, dataLen, recLen, storedBytes), ...])
or None when the file is not a #VBD package.
"""
with open(path, "rb") as fh:
data = fh.read()
if data[:4] != b"#VBD":
return None
_tag, _fmt, content, indexsize, nrec, _nextid = struct.unpack_from("<4sIIIHH", data, 0)
out, off = [], 20
for _ in range(nrec):
off += 8 # FILETIME
dlen, rlen, doff = struct.unpack_from("<III", data, off); off += 12
rid, nlen = struct.unpack_from("<HB", data, off); off += 3
name = data[off:off + nlen].decode("latin-1"); off += nlen
raw = data[indexsize + doff: indexsize + doff + rlen] if dlen else b""
out.append((rid, name, dlen, rlen, raw))
return content, out
def read_records(path):
"""Parse and decode every record.
Returns (contentVersion, [(recordId, name, dataLen, recLen, decodedBytes), ...])
"""
r = read_index(path)
if r is None:
return None
content, recs = r
out = []
for rid, name, dlen, rlen, raw in recs:
blob = b"" if not dlen else (raw if rlen == dlen else lz_decompress(raw))
out.append((rid, name, dlen, rlen, blob))
return content, out
def norm(name: str) -> str:
"""Canonical comparison key for an entry name.
Package entry names are case-inconsistent between builds (V4H's packer
lowercased everything) and use backslashes. Always compare through this.
"""
return name.lower().replace("\\", "/")
def md5(b: bytes) -> str:
return hashlib.md5(b).hexdigest()
def walk_packages(root):
"""Yield (absolutePath, relativePathWithForwardSlashes) for every *.mw4 under root."""
for dirpath, _dirs, files in os.walk(root):
for f in sorted(files):
if f.lower().endswith(".mw4"):
p = os.path.join(dirpath, f)
yield p, os.path.relpath(p, root).replace("\\", "/")
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""
mw4index.py - fast TSV manifest of every *.mw4 package under a directory.
python3 mw4index.py <resource-root> > manifest.tsv
Columns: pkgRelPath \t recordId \t entryName \t dataLen \t recLen \t md5(storedBytes)
Directory-only, so a 780 MB resource tree indexes in about a second.
CAUTION: md5 of the *stored* bytes is NOT a reliable equality test. Two builds
that packed identical source can still produce different stored bytes (raw vs
LZW selection, dictionary state). Use it only as a cheap "definitely identical"
signal; confirm real differences with pkgcmp.py, which decodes.
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mw4db import read_index, walk_packages, md5
def main():
if len(sys.argv) != 2:
sys.exit(__doc__)
root = sys.argv[1]
for path, rel in walk_packages(root):
try:
r = read_index(path)
except Exception as e: # truncated / unreadable package
print(f"!ERR\t{rel}\t{e}", file=sys.stderr)
continue
if r is None:
print(f"!NOTVBD\t{rel}", file=sys.stderr)
continue
_content, recs = r
for rid, name, dlen, rlen, raw in recs:
print(f"{rel}\t{rid}\t{name}\t{dlen}\t{rlen}\t{md5(raw)}")
if __name__ == "__main__":
main()
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""
pkgcmp.py - authoritative content diff of two versions of the same *.mw4.
python3 pkgcmp.py <A.mw4> <B.mw4>
Decodes every record on both sides and compares the *decompressed* bytes, so
the result is free of packer noise. This is the only trustworthy equality test;
stored-byte comparison badly over-reports (peaks.mw4: 9 stored-byte diffs, 1
real one).
Throughput is roughly 2 MB/s of package (props.mw4, 114 MB, takes ~50 s).
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mw4db import read_records, norm, md5
def load(path):
_content, recs = read_records(path)
return {norm(name): (dlen, md5(blob), blob) for _rid, name, dlen, _rlen, blob in recs}
def main():
if len(sys.argv) != 3:
sys.exit(__doc__)
a, b = load(sys.argv[1]), load(sys.argv[2])
aonly = sorted(set(a) - set(b))
bonly = sorted(set(b) - set(a))
diff = sorted(k for k in set(a) & set(b) if a[k][1] != b[k][1])
print(f"# entries: A={len(a)} B={len(b)} "
f"A_only={len(aonly)} B_only={len(bonly)} decoded-differ={len(diff)}")
for k in aonly:
print(f"+A {k} {a[k][0]}B")
for k in bonly:
print(f"-B {k} {b[k][0]}B")
for k in diff:
print(f"~ {k} A={a[k][0]}B B={b[k][0]}B")
if __name__ == "__main__":
main()
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
prune-identical.py - delete everything from an extracted tree that we already have,
leaving only genuine differences and additions.
python3 prune-identical.py <extractedDir> [--apply]
Dry-run by default; nothing is deleted without --apply.
Why two baselines
-----------------
A .mw4 is not a zip of the source tree. Our own packer rewrites ~7,400 files on
the way in (.data .video .instance .contents .audio .damage .torso .subsystems
.engine .lights) and generates a further ~17,700 qualified records that have no
source file at all ('foo.data{gamemodel}', 'foo.contents[joint_hip]{armature}',
'bar.tga{hint}'). Comparing only against Content/ would therefore "keep" about
24,000 files we in fact already have.
So each candidate is tested, in order:
1. same package + same entry path in OUR extracted packages, identical bytes
2. same entry path anywhere in OUR extracted packages, identical bytes
3. unqualified name + same path in our Content/ source tree, identical bytes
Any hit means we have that exact asset -> delete. Rule 3 is what recognises
raw pass-through assets (.tga, .wav, .erf, scripts); rules 1-2 are what
recognise the packer-generated forms.
Afterwards _merged/ is rebuilt from the survivors and empty directories removed.
"""
import sys, os, hashlib, collections, shutil
OURS_EXTRACTED = "/home/rich/Repositories/FS_Ours_extracted"
OUR_SOURCE = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content"
PRECEDENCE = {"props": 50, "core": 40, "textures": 30, "maps": 20, "missions": 10}
MERGE_EXCLUDE = ("variants/", "pilots/")
def digest(path, cache={}):
key = (path, os.path.getsize(path))
if key not in cache:
with open(path, "rb") as fh:
cache[key] = hashlib.md5(fh.read()).hexdigest()
return cache[key]
def index_paths(root, skip_top=()):
"""relative-lowercased-path -> absolute path. No hashing (lazy)."""
out = {}
for dp, dirs, fs in os.walk(root):
if dp == root:
dirs[:] = [d for d in dirs if d.lower() not in skip_top]
for f in fs:
p = os.path.join(dp, f)
out[os.path.relpath(p, root).replace("\\", "/").lower()] = p
return out
def package_of(rel):
"""Split '<pkgdir>/<entryPath>' for an extracted file.
Package directories are: core, props, textures, maps/<x>, Missions/<x>,
Variants/<x>, Pilots/Tesla/options
"""
parts = rel.split("/")
head = parts[0].lower()
if head in ("core", "props", "textures"):
return parts[0], "/".join(parts[1:])
if head in ("maps", "missions", "variants") and len(parts) > 2:
return "/".join(parts[:2]), "/".join(parts[2:])
if head == "pilots" and len(parts) > 3:
return "/".join(parts[:3]), "/".join(parts[3:])
return parts[0], "/".join(parts[1:])
def merged_precedence(pkg):
return PRECEDENCE.get(pkg.split("/")[0].lower(), 0)
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
root = sys.argv[1]
apply_ = "--apply" in sys.argv
merged_root = os.path.join(root, "_merged")
print("indexing our extracted packages ...", flush=True)
ours_pkg = index_paths(OURS_EXTRACTED, skip_top={"_merged"})
ours_by_entry = {}
for rel, p in ours_pkg.items():
if rel.startswith("_"):
continue
_pkg, entry = package_of(rel)
ours_by_entry.setdefault(entry, []).append(p)
print(f" {len(ours_pkg)} files, {len(ours_by_entry)} distinct entry paths")
print("indexing our Content source tree ...", flush=True)
src = index_paths(OUR_SOURCE)
print(f" {len(src)} files")
stats = collections.Counter()
doomed, kept = [], []
for dp, _dirs, fs in os.walk(root):
if dp == merged_root or dp.startswith(merged_root + os.sep):
continue
for f in fs:
p = os.path.join(dp, f)
rel = os.path.relpath(p, root).replace("\\", "/")
if rel.startswith("_"):
continue
pkg, entry = package_of(rel)
h = digest(p)
reason = None
cand = ours_pkg.get(rel.lower())
if cand and digest(cand) == h:
reason = "same-package"
if reason is None:
for c in ours_by_entry.get(entry.lower(), ()):
if digest(c) == h:
reason = "other-package"
break
if reason is None and "{" not in f and "[" not in f:
c = src.get(entry.lower())
if c and digest(c) == h:
reason = "our-source"
if reason:
stats["deleted:" + reason] += 1
doomed.append((rel, reason))
else:
stats["kept"] += 1
kept.append((rel, pkg, entry, h))
total = len(doomed) + len(kept)
print(f"\nexamined {total} files")
for k, v in sorted(stats.items()):
print(f" {v:7d} {k}")
print(f"\n -> would delete {len(doomed)}, keep {len(kept)}")
with open(os.path.join(root, "_survivors.tsv"), "w", encoding="utf-8") as fh:
fh.write("path\tpackage\tentry\tmd5\n")
for rel, pkg, entry, h in sorted(kept):
fh.write(f"{rel}\t{pkg}\t{entry}\t{h}\n")
if not apply_:
print(f"\nDRY RUN - nothing changed. Survivor list written to "
f"{os.path.join(root, '_survivors.tsv')}. Re-run with --apply.")
return
for rel, _reason in doomed:
os.remove(os.path.join(root, rel))
with open(os.path.join(root, "_pruned.tsv"), "w", encoding="utf-8") as fh:
fh.write("deletedPath\tmatchedVia\n")
for rel, reason in sorted(doomed):
fh.write(f"{rel}\t{reason}\n")
# rebuild _merged from survivors
shutil.rmtree(merged_root, ignore_errors=True)
best = {}
for rel, pkg, entry, h in kept:
if pkg.lower().startswith(MERGE_EXCLUDE):
continue
pr = merged_precedence(pkg)
cur = best.get(entry.lower())
if cur is None or pr > cur[0]:
best[entry.lower()] = (pr, entry, os.path.join(root, rel))
for _pr, entry, srcfile in best.values():
dest = os.path.join(merged_root, entry)
os.makedirs(os.path.dirname(dest), exist_ok=True)
os.link(srcfile, dest)
removed = 0
for dp, dirs, fs in os.walk(root, topdown=False):
if dp == root or not os.path.isdir(dp):
continue
if not os.listdir(dp):
os.rmdir(dp)
removed += 1
print(f"\ndeleted {len(doomed)} files, removed {removed} empty directories")
print(f"_merged rebuilt with {len(best)} paths")
print(f"log: {os.path.join(root, '_pruned.tsv')}")
if __name__ == "__main__":
main()
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""
restructure.py - reshape an extracted tree to match the repo's directory layout.
python3 restructure.py <extractedDir> [--apply]
Dry-run by default.
The extractor writes one directory per package, which is lossless but does not
look like the repo. This flattens it to the shape of Gameleap/mw4/Content:
<root>/Content/Mechs/Champion/champion.subsystems
<root>/Content/Maps/alpine02/...
<root>/Resource/Variants/<name>/... (variant records have no path)
<root>/Resource/Pilots/Tesla/options/...
<root>/_pkgroots/... (4-byte package-root pseudo-entries)
Path components are re-cased to match the repo wherever a counterpart exists, so
'mechs/longbow/longbow.data{element}' becomes
'Content/Mechs/Longbow/longbow.data{Element}'. Components with no counterpart
(the new chassis, for instance) keep the name the packer used.
REFUSES TO RUN if two packages would land on the same path with different bytes.
Flatten only trees where that has been checked - the pruned V4H tree has zero
collisions; our own full extraction has 60 (mission-local copies of shared
Culturals props) and must stay per-package.
Provenance is preserved in _layout.tsv: newPath, package, original entry path.
"""
import sys, os, hashlib, collections
REPO_CONTENT = "/home/rich/Repositories/firestorm/Gameleap/mw4/Content"
def build_case_map(root):
"""lowercased relative path -> the repo's actual spelling, for dirs and files."""
out = {}
for dp, dirs, files in os.walk(root):
rel = os.path.relpath(dp, root).replace("\\", "/")
base = "" if rel == "." else rel
for name in dirs + files:
r = f"{base}/{name}" if base else name
out[r.lower()] = r
return out
def recase(path, case_map):
"""Re-case each component against the repo, keeping unknown ones as-is."""
done = []
for part in path.split("/"):
probe = "/".join(done + [part]).lower()
canonical = case_map.get(probe)
done.append(canonical.rsplit("/", 1)[-1] if canonical else part)
return "/".join(done)
def package_of(rel):
parts = rel.split("/")
head = parts[0].lower()
if head in ("core", "props", "textures"):
return parts[0], "/".join(parts[1:])
if head in ("maps", "missions", "variants") and len(parts) > 2:
return "/".join(parts[:2]), "/".join(parts[2:])
if head == "pilots" and len(parts) > 3:
return "/".join(parts[:3]), "/".join(parts[3:])
return parts[0], "/".join(parts[1:])
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
root = sys.argv[1]
apply_ = "--apply" in sys.argv
print("indexing repo Content/ for canonical casing ...", flush=True)
case_map = build_case_map(REPO_CONTENT)
print(f" {len(case_map)} path components")
plan = [] # (oldRel, newRel, package, entry)
claims = collections.defaultdict(dict) # newRel -> md5 -> [package]
for dp, dirs, fs in os.walk(root):
if dp == root:
dirs[:] = [d for d in dirs if d not in ("_merged", "Content", "Resource", "_pkgroots")]
for f in fs:
old = os.path.relpath(os.path.join(dp, f), root).replace("\\", "/")
if old.startswith("_"):
continue
pkg, entry = package_of(old)
low = pkg.lower()
if low.startswith("variants/"):
new = f"Resource/{pkg}/{entry}"
elif low.startswith("pilots/"):
new = f"Resource/{pkg}/{entry}"
elif entry.lower().startswith("resource/"):
new = f"_pkgroots/{pkg}/{entry}"
else:
new = "Content/" + recase(entry, case_map)
plan.append((old, new, pkg, entry))
if new.startswith("Content/"):
with open(os.path.join(root, old), "rb") as fh:
h = hashlib.md5(fh.read()).hexdigest()
claims[new].setdefault(h, []).append(pkg)
clashes = {k: v for k, v in claims.items() if len(v) > 1}
dupes = {k: v for k, v in claims.items()
if len(v) == 1 and len(next(iter(v.values()))) > 1}
print(f"\n{len(plan)} files -> {len({n for _o, n, _p, _e in plan})} destinations")
print(f" identical copies from several packages : {len(dupes)}")
print(f" CONFLICTING copies (different bytes) : {len(clashes)}")
if clashes:
for k, v in list(clashes.items())[:10]:
print(f" {k} " + "; ".join(f"{h[:6]}={','.join(p)}" for h, p in v.items()))
sys.exit("\nrefusing to flatten: resolve the conflicts first")
sample = [(o, n) for o, n, _p, _e in plan if o != n][:8]
print("\nsample:")
for o, n in sample:
print(f" {o}\n -> {n}")
if not apply_:
print("\nDRY RUN - nothing changed. Re-run with --apply.")
return
import shutil
shutil.rmtree(os.path.join(root, "_merged"), ignore_errors=True)
moved, deduped = 0, 0
with open(os.path.join(root, "_layout.tsv"), "w", encoding="utf-8") as fh:
fh.write("newPath\tpackage\toriginalEntry\n")
for old, new, pkg, entry in sorted(plan):
src, dest = os.path.join(root, old), os.path.join(root, new)
os.makedirs(os.path.dirname(dest), exist_ok=True)
if os.path.exists(dest): # identical copy from another package
os.remove(src)
deduped += 1
else:
os.replace(src, dest)
moved += 1
fh.write(f"{new}\t{pkg}\t{entry}\n")
removed = 0
for dp, _dirs, _fs in os.walk(root, topdown=False):
if dp == root or not os.path.isdir(dp):
continue
if not os.listdir(dp):
os.rmdir(dp)
removed += 1
print(f"\nmoved {moved}, dropped {deduped} identical duplicates, "
f"removed {removed} empty directories")
print(f"provenance: {os.path.join(root, '_layout.tsv')}")
if __name__ == "__main__":
main()
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""
split-source.py - separate repackable source files from compiled records, and
rename the ones that need it, so the tree can be dropped into Content/.
python3 split-source.py <extractedDir> [--apply]
Dry-run by default.
The problem
-----------
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. Classification was derived
empirically by extracting OUR OWN packages and hash-matching every record against
Gameleap/mw4/Content (22,138 matched, 28,728 did not):
verbatim 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
Worth being concrete: 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 CANNOT be recovered from a package - they have to be
re-authored.
.armature likewise does not exist in a package. The packer splits it into
per-joint X.contents[joint_*]{armature} records, which are compiled.
What this does
--------------
* X.data{solidobb} -> X_skeleton_SOLID.obb (mechs)
X.data{hierarchicalobb} -> X_skeleton.obb (mechs)
...or _SOLID.obb / .obb outside Content/Mechs. These really are .obb files
('#BBO' magic, headers byte-identical to ours). The exact source spelling is
declared inside the .data text, which we do not have, so a convention is
applied and recorded in _source-vs-compiled.tsv - whatever .data gets
authored later must reference the same name.
* everything classified as compiled moves to _compiled/, keeping its path
* everything left under Content/ is genuine, repackable source
Resource/ (variants, pilot options) is left alone: those are whole .mw4
packages, best taken from FS_Build_V4H/resource/Variants/*.mw4 directly rather
than from their unpacked records.
"""
import sys, os, re, collections
SOURCE_EXT = {".tga", ".erf", ".mw4anim", ".bid", ".wav", ".bsp", ".material",
".abl", ".obb", ".ebf", ".bounds", ".animscript", ".fgd", ".tcf",
".mlr", ".d3f", ".gaf", ".h", ".abi", ".hpp", ".include", ".tpl"}
COMPILED_EXT = {".data", ".instance", ".subsystems", ".damage", ".torso",
".engine", ".audio", ".video", ".lights", ".mw4"}
OBB_QUALS = {"{solidobb}": ("_skeleton_SOLID.obb", "_SOLID.obb"),
"{hierarchicalobb}": ("_skeleton.obb", ".obb")}
QUAL = re.compile(r'(\[[^\]]*\]|\{[^}]*\})')
def is_text(path, limit=8192):
with open(path, "rb") as fh:
chunk = fh.read(limit)
if not chunk:
return False
printable = sum(1 for b in chunk if 9 <= b <= 13 or 32 <= b < 127)
return printable / len(chunk) > 0.90
def classify(path, name, rel):
quals = "".join(QUAL.findall(name)).lower()
stem = QUAL.sub("", name)
ext = os.path.splitext(stem)[1].lower()
if quals in OBB_QUALS:
mech = "/mechs/" in rel.lower()
suffix = OBB_QUALS[quals][0 if mech else 1]
return "source", stem.rsplit(".", 1)[0] + suffix
if quals:
return "compiled", name
if ext in SOURCE_EXT:
return "source", name
if ext in COMPILED_EXT:
return "compiled", name
# .script and .contents are genuinely mixed - decide on content
return ("source" if is_text(path) else "compiled"), name
def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
root = sys.argv[1]
apply_ = "--apply" in sys.argv
content = os.path.join(root, "Content")
if not os.path.isdir(content):
sys.exit(f"no Content/ in {root} - run restructure.py first")
plan, stats = [], collections.Counter()
renames = collections.Counter()
for dp, _dirs, fs in os.walk(content):
for f in fs:
p = os.path.join(dp, f)
rel = os.path.relpath(p, root).replace("\\", "/")
verdict, newname = classify(p, f, rel)
new = ("Content/" if verdict == "source" else "_compiled/Content/") + \
os.path.relpath(os.path.join(dp, newname), content).replace("\\", "/")
plan.append((rel, new, verdict))
stats[verdict] += 1
if verdict == "source" and newname != f:
renames[os.path.splitext(f)[1] or f] += 1
print(f"Content/: {sum(stats.values())} files")
print(f" repackable source : {stats['source']}")
print(f" compiled records : {stats['compiled']} (-> _compiled/)")
if renames:
print("\nrenamed:")
for k, v in renames.most_common():
print(f" {v:5d} {k}")
print("\nsample renames:")
shown = 0
for old, new, v in plan:
if v == "source" and os.path.basename(old) != os.path.basename(new) and shown < 6:
print(f" {os.path.basename(old)} -> {os.path.basename(new)}")
shown += 1
if not apply_:
print("\nDRY RUN - nothing changed. Re-run with --apply.")
return
for old, new, _v in plan:
src, dest = os.path.join(root, old), os.path.join(root, new)
os.makedirs(os.path.dirname(dest), exist_ok=True)
os.replace(src, dest)
with open(os.path.join(root, "_source-vs-compiled.tsv"), "w", encoding="utf-8") as fh:
fh.write("verdict\toriginalPath\tnewPath\n")
for old, new, v in sorted(plan):
fh.write(f"{v}\t{old}\t{new}\n")
removed = 0
for dp, _dirs, _fs in os.walk(root, topdown=False):
if dp == root or not os.path.isdir(dp):
continue
if not os.listdir(dp):
os.rmdir(dp)
removed += 1
print(f"\nmoved {len(plan)} files, removed {removed} empty directories")
print(f"log: {os.path.join(root, '_source-vs-compiled.tsv')}")
if __name__ == "__main__":
main()
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
treediff.py - case-insensitive loose-file diff of two directory trees.
python3 treediff.py <dirA> <dirB> [labelA] [labelB]
Compares by relative path (lowercased, '/'-normalised) and md5. Written for the
hsh/, content/ and root config comparisons, where the two builds disagree on
filename case but not on identity.
"""
import sys, os, hashlib
def scan(root):
out = {}
for dirpath, _dirs, files in os.walk(root):
for f in files:
p = os.path.join(dirpath, f)
rel = os.path.relpath(p, root).replace("\\", "/")
with open(p, "rb") as fh:
h = hashlib.md5(fh.read()).hexdigest()
out[rel.lower()] = (os.path.getsize(p), h, rel)
return out
def main():
if len(sys.argv) < 3:
sys.exit(__doc__)
la = sys.argv[3] if len(sys.argv) > 3 else "A"
lb = sys.argv[4] if len(sys.argv) > 4 else "B"
a, b = scan(sys.argv[1]), scan(sys.argv[2])
aonly = sorted(set(a) - set(b))
bonly = sorted(set(b) - set(a))
diff = sorted(k for k in set(a) & set(b) if a[k][1] != b[k][1])
print(f"# {la}={len(a)} files, {lb}={len(b)} files | "
f"identical={len(set(a) & set(b)) - len(diff)} differ={len(diff)} "
f"{la}_only={len(aonly)} {lb}_only={len(bonly)}")
for k in aonly:
print(f"+{la}\t{a[k][2]}\t{a[k][0]}")
for k in bonly:
print(f"-{lb}\t{b[k][2]}\t{b[k][0]}")
for k in diff:
print(f"~DIF\t{a[k][2]}\t{la}={a[k][0]}B {lb}={b[k][0]}B")
if __name__ == "__main__":
main()
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""
variants-report.py - inventory of resource/Variants/*.mw4 (saved mechlab loadouts).
python3 variants-report.py <variantsDir> <v4hManifest.tsv> <oursManifest.tsv>
A variant package holds 4 records:
[0] 'Resource\\Variants\\<name>' 4 B (raw id)
[1] '{Mech}' ~350 B
[2] '<chassis>{Subsystem}' ~4 KB <- names the chassis it needs
[3] '<8-char token>' 16 B
So the chassis a variant depends on is readable straight from the manifest,
with no decompression: the record whose name ends in '{Subsystem}'.
The report groups variants by chassis and flags any whose chassis is absent
from the target build's core.mw4 (those will not load).
"""
import sys, os, collections
def chassis_in_core(manifest):
"""Set of chassis names that core.mw4 defines (one *.subsystems per chassis)."""
out = set()
with open(manifest, encoding="latin-1") as fh:
for line in fh:
pkg, _rid, name, *_ = line.rstrip("\n").split("\t")
if pkg.lower() != "core.mw4":
continue
n = name.lower().replace("\\", "/")
if n.endswith(".subsystems"):
out.add(n.rsplit("/", 1)[-1][:-len(".subsystems")])
return out
def variant_chassis(manifest):
"""variantPkgRelPath -> chassis name it requires."""
out = {}
with open(manifest, encoding="latin-1") as fh:
for line in fh:
pkg, _rid, name, *_ = line.rstrip("\n").split("\t")
if not pkg.lower().startswith("variants/"):
continue
if name.endswith("{Subsystem}"):
out[pkg] = name[:-len("{Subsystem}")].lower()
return out
def main():
if len(sys.argv) != 4:
sys.exit(__doc__)
vdir, v4h_manifest, ours_manifest = sys.argv[1:4]
have_v4h = chassis_in_core(v4h_manifest)
have_ours = chassis_in_core(ours_manifest)
vc = variant_chassis(v4h_manifest)
print(f"# variant packages: {len(vc)} (files on disk: "
f"{len([f for f in os.listdir(vdir) if f.lower().endswith('.mw4')])})")
print(f"# chassis defined in V4H core.mw4 : {len(have_v4h)}")
print(f"# chassis defined in OUR core.mw4 : {len(have_ours)}")
print(f"# chassis only V4H defines : {sorted(have_v4h - have_ours)}")
print()
by = collections.defaultdict(list)
for pkg, ch in vc.items():
by[ch].append(pkg)
loadable_now = broken = needs_new_mech = 0
print(f"{'chassis':22s} {'count':>5s} status")
for ch in sorted(by, key=lambda c: (-len(by[c]), c)):
n = len(by[ch])
if ch in have_ours:
status = "loadable in OUR build today"
loadable_now += n
elif ch in have_v4h:
status = "needs the new chassis ported"
needs_new_mech += n
else:
status = "BROKEN - chassis missing from V4H too"
broken += n
print(f"{ch:22s} {n:5d} {status}")
print()
print(f"loadable in our build today : {loadable_now}")
print(f"blocked on new chassis : {needs_new_mech}")
print(f"broken / orphaned : {broken}")
for ch in sorted(by):
if ch not in have_v4h:
print(f" orphan chassis '{ch}': " + ", ".join(sorted(by[ch])))
if __name__ == "__main__":
main()