# mech_loadouts.csv Catch-Up Notes

This document is for future AI agents or maintainers who need to understand how [mech_loadouts.csv](mech_loadouts.csv) was built, where each column comes from, and what files must be updated together when mech data changes.

## Purpose

`mech_loadouts.csv` is a flattened mech audit sheet built from the FireStorm content tree. It combines:
- mech identity and stat data
- current in-game playability status
- installed/default weapon loadouts
- weapon facing and grouping metadata
- zone hardpoint capacity
- notes about missing registration, packaging, or asset support

The CSV is not a primary source of truth. It is a synthesized report built from the mech content files and several registration tables.

## Current CSV Column Layout

The important derived columns are:
- `In_game_playable`
- `Weapons_With_Locations`
- `Default_Installed_Locations`
- `Available_Slot_Capacity_By_Zone`
- `Available_Hardpoints`
- `notes`

The existing stat columns before those are mostly direct mech data fields and were not re-derived during the audit.

## What Each Derived Column Means

### `In_game_playable`

`Yes` if the mech is currently in the active playable roster, `No` otherwise.

This was determined by comparing the mech folder set against the active registration chain:
- `Gameleap/code/mw4/Code/MW4/MechLabHeaders.h`
- `Gameleap/mw4/Content/ShellScripts/MechLabHeaders.h`
- `Gameleap/mw4/Content/Tables/MechChassisTable.tbl`
- `Gameleap/mw4/Content/Tables/MechTable.tbl`
- `Gameleap/mw4/Content/core.build`
- plus supporting string and packaging manifests

Practical rule:
- folder presence alone does not make a mech playable
- playable mechs must have the ID/table/build registrations aligned

### `Weapons_With_Locations`

This column lists each installed weapon in the mech¡¯s default subsystem file, with:
- the weapon name
- the `InternalLocation`
- the `WeaponFacing`
- the weapon group

Current format:
- `WeaponName@Location(Facing) G#`

Examples:
- `MediumPulseLaser@LeftTorso(Rear) G1`
- `SRM4@RightTorso(Front) G2`
- `ClanLRM10@LeftTorso(Side) G2`

Derived from the mech¡¯s `.subsystems` file by reading weapon subsystem blocks.

Important notes:
- `WeaponFacing=0` => `Front`
- `WeaponFacing=1` => `Rear`
- `WeaponFacing=2` => `Side`
- if `WeaponFacing` is absent, it defaults to `Front`

### `Default_Installed_Locations`

A compact count summary of where the mech¡¯s default weapons are installed, based on `InternalLocation` in the `.subsystems` file.

Example:
- `LeftArmx2 | LeftTorsox3 | RightTorsox4 | RightArmx2`

This is about installed weapons, not slot capacity.

### `Available_Slot_Capacity_By_Zone`

A compact summary of zone capacities from the mech¡¯s `.damage` file.

Current format:
- `Zone(M# / P# / B# / O#)`

Where:
- `M` = MissileSlots
- `P` = ProjectileSlots
- `B` = BeamSlots
- `O` = OmniSlots

Example:
- `LeftTorso(M0/P2/B2/O0)`

This reflects what the mech can support in that zone, not what is currently installed.

### `Available_Hardpoints`

A list of zones that have at least one positive slot count in the `.damage` file.

Current format:
- `LeftArm(Front)`
- `LeftTorso(Front+Rear)`
- `RightTorso(Side)`

This column is a directional summary of where weapons can be mounted.

Important:
- this is derived from slot capacity plus facing information from weapon loadouts
- it is not a literal mechanical port map
- if a zone has multiple facing types in the mech¡¯s default loadout, it can show a combined label such as `Front+Rear` or `Front+Side`

## Core Data Sources

### 1. Mech registration and playability

Primary files:
- `Gameleap/code/mw4/Code/MW4/MechLabHeaders.h`
- `Gameleap/mw4/Content/ShellScripts/MechLabHeaders.h`
- `Gameleap/mw4/Content/Tables/MechChassisTable.tbl`
- `Gameleap/mw4/Content/Tables/MechTable.tbl`
- `Gameleap/mw4/Content/core.build`

What they control:
- which mechs are actually in the active roster
- which IDs exist in code and script space
- which mech resources are packed into the runtime build

If a mech folder exists but it is not listed in these files, it is usually not playable.

### 2. Default weapon loadout and weapon-facing metadata

Primary file:
- `Gameleap/mw4/Content/Mechs/<MechName>/<mech>.subsystems`

Each weapon block typically contains:
- `Model=` weapon subsystem resource
- `InternalLocation=` where the weapon is mounted
- `Site=` mount port name
- `GroupIndex=` weapon group number
- `WeaponFacing=` optional facing metadata
- `AmmoCount=` for ammo-using weapons

The `.subsystems` file is the source for:
- `Weapons_With_Locations`
- `Default_Installed_Locations`
- facing annotations in the hardpoint summaries

### 3. Slot capacity / hardpoint support

Primary file:
- `Gameleap/mw4/Content/Mechs/<MechName>/<mech>.damage`

Each section such as `[LeftArmInternal]` or `[RightTorsoInternal]` may contain:
- `MissileSlots=`
- `ProjectileSlots=`
- `BeamSlots=`
- `OmniSlots=`

These are used to derive:
- `Available_Slot_Capacity_By_Zone`
- `Available_Hardpoints`

### 4. Weapon slot type rules in code

Relevant code:
- `Gameleap/code/mw4/Code/MW4/MWDamageObject.cpp`
- `Gameleap/code/mw4/Code/MW4/MWDamageObject.hpp`
- `Gameleap/code/mw4/Code/MW4/MechLab.cpp`
- `Gameleap/code/mw4/Code/MW4/Subsystem_Tool.cpp`
- `Gameleap/code/mw4/Code/MW4/Weapon_Tool.cpp`
- `Gameleap/code/mw4/Code/MW4/Weapon.hpp`
- `Gameleap/code/mw4/Code/MW4/Weapon.cpp`
- `Gameleap/code/mw4/Code/MW4/hudweapon.cpp`

What the code does:
- reads slot counts from `.damage`
- reads `InternalLocation` and `WeaponFacing` from `.subsystems`
- maps weapon slot type and size from the weapon model
- assigns weapons to valid locations during mechlab editing

## Facing Semantics

Weapon facing is stored per weapon instance, not as a separate hardpoint label.

Values seen in the data/code:
- `0` = front
- `1` = rear
- `2` = side

Code references:
- `Gameleap/code/mw4/Code/MW4/Weapon_Tool.cpp`
- `Gameleap/code/mw4/Code/MW4/Weapon.hpp`
- `Gameleap/code/mw4/Code/MW4/Weapon.cpp`
- `Gameleap/code/mw4/Code/MW4/hudweapon.cpp`
- `Gameleap/code/mw4/Code/MW4/MechLab.cpp`

Important nuance:
- a zone name like `LeftTorso` does not tell you the facing by itself
- the facing comes from the weapon block¡¯s `WeaponFacing`
- the same zone can contain multiple weapons with different facings

## Group Index Meaning

The `G1`, `G2`, etc. suffix in `Weapons_With_Locations` is the weapon group number from `GroupIndex` in the subsystem file.

This is not a location or facing field.

Meaning:
- `G1` = weapon group 1
- `G2` = weapon group 2
- and so on

## How The Spreadsheet Was Built

The CSV was assembled by cross-referencing:
- mech folder contents under `Gameleap/mw4/Content/Mechs/`
- `.subsystems` weapon blocks
- `.damage` internal zone capacities
- active roster tables and IDs
- string registration files
- build manifests
- loose UI art assets under `Gameleap/mw4/hsh/`

The output is a derived audit sheet, not a direct export from a single source file.

## Known Pitfalls

### 1. Folder presence does not mean playable

Some mech folders exist but are not in the active roster tables or headers. These need ID/table/build integration before they are usable in-game.

### 2. File names are not always canonical

Some mechs use alternate base filenames inside their folder. Do not assume `FolderName/FolderName.damage` or `FolderName/FolderName.subsystems` always exist.

Prefer:
- parse the `.instance` file when present
- otherwise detect the first matching `.subsystems` / `.damage` file by extension and content

### 3. `Available_Hardpoints` is a summary, not a literal port map

It summarizes zones with capacity and facing hints, but it does not enumerate every exact port or site name.

### 4. Rear-facing weapons are per-instance

If a mech has one rear-mounted weapon in a torso zone, that does not mean all weapons in that zone are rear-facing.

## Update Checklist For Future Changes

If a mech is added or changed, check these in order:
1. update or verify the mech `.data`, `.instance`, `.subsystems`, and `.damage` files
2. ensure the mech is added to the active ID header(s)
3. update `MechChassisTable.tbl` and `MechTable.tbl`
4. update `core.build` and any related build manifests
5. add any needed string IDs or script references
6. confirm the mech has the expected loose or packed art assets
7. rebuild the CSV derived columns if the data changed

## Useful File Map

### Mech data
- `Gameleap/mw4/Content/Mechs/<MechName>/`

### Roster and IDs
- `Gameleap/code/mw4/Code/MW4/MechLabHeaders.h`
- `Gameleap/mw4/Content/ShellScripts/MechLabHeaders.h`
- `Gameleap/mw4/Content/Tables/MechChassisTable.tbl`
- `Gameleap/mw4/Content/Tables/MechTable.tbl`

### Slot/hardpoint logic
- `Gameleap/code/mw4/Code/MW4/MWDamageObject.cpp`
- `Gameleap/code/mw4/Code/MW4/MechLab.cpp`
- `Gameleap/code/mw4/Code/MW4/Subsystem_Tool.cpp`

### Facing logic
- `Gameleap/code/mw4/Code/MW4/Weapon.cpp`
- `Gameleap/code/mw4/Code/MW4/Weapon.hpp`
- `Gameleap/code/mw4/Code/MW4/Weapon_Tool.cpp`
- `Gameleap/code/mw4/Code/MW4/hudweapon.cpp`

### Build manifests
- `Gameleap/mw4/Content/core.build`
- `Gameleap/mw4/Content/props.build`
- `Gameleap/mw4/Content/textures.build`

## Current State Summary

At the time this doc was written:
- `mech_loadouts.csv` includes playability, default loadout, facing, hardpoint, and notes columns
- non-playable mechs are explicitly marked `No`
- Templar and other mechs with rear or side mounts have facing visible in the CSV
- the file is intended to be a living audit artifact for future data cleanup and enablement work

---

## MechEditor Web App — Data Sources, Fields, and Conversions (documented 2026-07-23)

The MechEditor is a single-file Python HTTP server at `/home/rich/Repositories/MechEditor/mech_editor.py`.  
It runs at `localhost:8765`, parses the firestorm content tree, and presents a full mech configuration editor in the browser.  
This section documents every data field it reads, every conversion it performs, and every naming rule it enforces.

---

### Data Files Parsed Per Mech

All files live under `Gameleap/mw4/Content/Mechs/<MechDir>/`.

#### `.data` file  (`parse_data()`)

| Field in file | Editor key | Notes |
|---|---|---|
| `VehicleTonnage` | `tonnage` | chassis base tonnage (float) |
| `MaxVehicleTonnage` | `max_tonnage` | max loadout tonnage (float) |
| `TechType` | `tech` | `$(Tech_IS)` ? `"IS"`, `$(Tech_Clan)` ? `"Clan"` |
| `MaxHeat` | `max_heat` | heat capacity (int) |
| `VehicleTradeValue` | `trade_value` | C-bills |
| `DragoonValue` | `dragoon` | used in Power Rating bar scaling |
| `MinMaxSpeed` | `min_max_speed` | base speed ceiling in **m/s** |
| `MaxSpeed` | `max_speed` | absolute speed ceiling in **m/s** (engine upgrades may not exceed this) |
| `FullStopTurnRate` | `full_stop_turn` | turn rate at zero speed, in **degrees/sec** |
| `TopSpeedTurnRate` | `top_speed_turn` | turn rate at top speed, in **degrees/sec** |
| `Acceleration` | `acceleration` | forward acceleration in **m/s²** |
| `Decceleration` | `decceleration` | forward braking in **m/s²** — **double-c spelling is canonical in the engine source** |
| `ReverseAccelerationMultiplier` | `rev_accel_mult` | multiplier applied to Acceleration for reverse |
| `ReverseDeccelerationMultiplier` | `rev_decel_mult` | multiplier applied to Decceleration for reverse — double-c canonical |
| `MinStandTransitionSpeed` | not surfaced | animation threshold only — see note below |
| `CanLoadJumpJets` | `can_jj` | Yes/No |
| `CanLoadECM` | `can_ecm` | Yes/No |
| `CanLoadBeagle` | `can_bap` | Yes/No |
| `CanLoadLightAmp` | `can_lightamp` | Yes/No |
| `CanLoadAMS` | `can_ams` | Yes/No |
| `CanLoadLAMS` | `can_lams` | Yes/No |
| `CanLoadIFF_Jammer` | `can_iff` | Yes/No |

**MinStandTransitionSpeed** is the speed (m/s) below which the mech switches from its walking animation to its idle/standing animation.  
It is NOT a hard movement limit.  It is purely an animation state machine threshold in `Mech.cpp`.  
Code path: `animStateEngine?RequestState(StandState)` when `currentSpeedMPS <= minStandTransitionSpeed`.  
Must be > 0 (validated in `Vehicle_Tool.cpp`).  Argus = 12.631 m/s = 45.5 kph.

#### `.instance` file  (`parse_instance()`)

| Field | Editor key | Notes |
|---|---|---|
| `PowerRating` | `PowerRating` | mechlab bar value, 0–100 |
| `ArmorRating` | `ArmorRating` | mechlab bar value, 0–100 |
| `SpeedRating` | `SpeedRating` | mechlab bar value, 0–100 |
| `HeatRating` | `HeatRating` | mechlab bar value, 0–100 |
| `DoesHaveLightAmp` | `has_lightamp` | 0 or 1, default 1 — whether LightAmp is currently installed |

#### `.subsystems` file  (`parse_subsystems()`)

Provides: armor type + per-zone multipliers, installed heatsinks, jump jets, engine upgrades, weapons, electronics.

**Armor block:**
- `ArmorType=` ? armor type string (`Standard`, `FerroFiberus`, `Reactive`, `Reflective`, `Solarian`)
- Per-zone entries: `LeftArm=1.0`, `RightTorso=2.5`, etc. — multiplier for that zone

**Engine:**
- `EngineUpgrade` blocks counted ? `engine_upgrades` (0–5)

**Weapons** — each weapon block contains:
- `Model=` ? weapon subsystem resource path (name extracted)
- `InternalLocation=` ? zone name
- `Site=` ? mount port name (from armature)
- `GroupIndex=` ? weapon group number
- `WeaponFacing=` ? 0=Front, 1=Rear, 2=Side (absent = Front)
- `AmmoCount=` ? rounds for ammo-using weapons
- `EjectSite=` ? optional ejection site for ammo

**Electronics:** ECM, Beagle (BAP), AMS, LAMS, IFF_Jammer detected by subsystem model path.

#### `.damage` file  (`parse_damage()`)

Per zone section `[ZoneInternal]`:
- `BaseArmorValue` ? starting armor (float)
- `MaxArmorValue` ? hard cap on armor pts for that zone
- `InternalHPValue` ? internal structure HP
- `OmniSlots`, `BeamSlots`, `MissileSlots`, `ProjectileSlots` ? weapon slot counts

Special zones:
- `SpecialAttachedToZone=` ? which body section this special zone is attached to
- `DamagePropagationZone=` ? where overflow damage propagates

#### `.engine` file  (`parse_engine()`)

| Field | Editor key | Notes |
|---|---|---|
| `NumHeatSinks` | `NumHeatSinks` | free heatsinks from engine (not in subsystems) |
| `TonsPerUpgrade` | `TonsPerUpgrade` | tonnage cost per engine upgrade tier |
| `MPSPerUpgrade` | `MPSPerUpgrade` | m/s speed gain per upgrade tier |

#### `.torso` file  (`parse_torso()`)

| Field | Notes |
|---|---|
| `TwistSpeed` | torso horizontal rotation speed — may be a macro reference |
| `PitchSpeed` | torso vertical rotation speed — may be a macro reference |
| `TwistRadius` | max horizontal twist angle — may be a macro reference |
| `PitchRadius` | max vertical pitch angle — may be a macro reference |
| `ArmRatioAngle` | ratio of arm tracking vs torso rotation — may be a macro reference |

All torso fields may reference macros from `Content/Defines/MechTorso.defines`.  
The editor resolves them using a preloaded `TORSO_DEFINES` dict.  Notable values:

```
NORMAL_RATIO   = 40
SNAIL_TSPEED   = 40
NORMAL_TSPEED  = 60
FAST_TSPEED    = 80
WIDE_TRADIUS   = 160
NORMAL_PRADIUS = 40
```

---

### Calculated Ratings and Conversions

#### Speed (kph)

```
top_speed_kph = min(MinMaxSpeed + MPSPerUpgrade × engine_upgrades, MaxSpeed) × 3.6
```

- `MinMaxSpeed` and `MaxSpeed` from `.data` (m/s)
- `MPSPerUpgrade` from `.engine` (m/s per tier)
- `engine_upgrades` from `.subsystems` (0–5)
- Multiply by 3.6 to convert m/s ? kph
- Argus example: (20.28 + 1.11 × 10) × 3.6 = 113.5 kph

#### Speed Rating (bar)

```
speed_rating = (top_speed_mps / MaxSpeed) × 100   [capped at 100]
```

#### Turn Rate — degrees to radians

The `.data` file stores turn rates in **degrees/sec**.  The mechlab UI label was changed to show rad/sec:
- `StringResource.rc`: `IDS_ML_CH_TURNRATE` ? `"Turn Rate (Top Speed Rad/Sec):"`
- Conversion: `radians = degrees × ?/180` where `?/180 ? 0.017453`
- Argus: FullStopTurn = 75° = **1.309 rad/sec**, TopSpeedTurn = 45° = **0.785 rad/sec**

#### Acceleration / Deceleration (m/s²)

Stored directly in `.data`.  Reverse values are derived:
```
reverse_accel = Acceleration × ReverseAccelerationMultiplier
reverse_decel = Decceleration × ReverseDeccelerationMultiplier
```

**The double-c spelling (`Decceleration`, `ReverseDeccelerationMultiplier`) is canonical — it matches the engine source.  Do not "fix" the spelling.**

#### Heat Rating (bar)

```
total_hs = NumHeatSinks (engine) + installed_heatsinks (subsystems)
effective_hs = total_hs × (2 if Double else 1)
heat_rating = (effective_hs / MaxHeat) × 100   [capped at 100]
```

#### Power Rating (bar)

```
total_damage = ? (DamageAmount × NumFire)  for each installed weapon
power_rating = (total_damage / 80) × 100   [capped at 100]
```

- `DamageAmount` and `NumFire` come from `WeaponSubsystems/<weapon>.data` following `!include` chains
- Parsed by `load_weapon_damages()` at server startup, cached in `Handler.weapon_damages`
- Argus example with default load: ~36.2 total damage ? 45 rating (stored = 42)

#### Armor Rating (bar)

```
for each zone:
    pts = multiplier × ARMOR_PTS_PER_TON[armor_type]
    effective = min(pts, MaxArmorValue[zone])
armor_rating = (? effective / ? MaxArmorValue) × 100
```

Armor pts per ton by type (from `Adept/ResourceImagePool.cpp` and game design):

| Type | Pts/ton |
|---|---|
| Standard | 32 |
| FerroFiberus | 38 |
| Reactive | 30 |
| Reflective | 30 |
| Solarian | 60 |

Note: `FerroFiberus` is the canonical internal token (not player-visible).  The player sees `DNL_FERROFIB = "Ferro Fibrous"` via string lookup.

---

### Active-in-Game Detection

Source: `Gameleap/mw4/Content/Tables/MechChassisTable.tbl`

Format:
```
DisplayKey=Mechs\DirName\FileName.data
//CommentedKey=Mechs\DirName\FileName.data   <- inactive
```

- Active = entry exists AND is not prefixed with `//`
- Currently the only inactive mech: **Dasher** (commented out)
- The editor shows a green **ACTIVE IN GAME** or red **NOT IN GAME** banner at top of Stats tab

---

### hsh/ Image Naming Conventions

The `hsh/` directory under `Gameleap/mw4/` holds loose BMP files loaded at runtime (not packed into `.mw4`).  
There are four relevant subdirectories, each with a different naming authority.

#### `hsh/hud/` — in-game HUD damage silhouette (own mech)
#### `hsh/MFD/` — MFD target display silhouette (target mech)
#### `hsh/radar/hud/` — radar damage overlay

**All three use identical stems** sourced from `huddamage.cpp` `texturename[]` array.

Load path:
- hud/MFD: `hsh\<texturename>.bmp` where texturename = `hud\<stem>`  ?  file = `hsh/hud/<stem>.bmp`
- radar: `hsh\radar\<texturename>.bmp`  ?  file = `hsh/radar/hud/<stem>.bmp`

Code: `render.cpp` `CRadar_Device::LoadRadarDamageTexture()` and `huddamage.cpp` `HUDDamage`.

#### `hsh/Mechs/` — mw4print scorecard portrait

Load path: `recscore.cpp` ? `GetLocString(model->m_nameIndex)` ? `DNL_*` string from `StringResource.rc` ? lowercased filename.  
Different naming authority from the other three.

---

### Complete Canonical Name Table

Key: mech directory name (case-insensitive) ? canonical stem for `hsh/hud/`, `hsh/MFD/`, `hsh/radar/hud/`.  
Entries in **bold** differ from the directory name.

| Directory | hud/MFD/radar stem | hsh/Mechs/ portrait filename |
|---|---|---|
| Annihilator | annihilator | annihilator.bmp |
| Archer | archer | archer.bmp |
| ArcticWolf | arcticwolf | arctic wolf.bmp |
| Ares | ares | ares.bmp |
| Argus | argus | argus.bmp |
| Assassin2 | assassin2 | **assassin ii.bmp** |
| Atlas | atlas | atlas.bmp |
| Avatar | avatar | avatar.bmp |
| Awesome | awesome | awesome.bmp |
| Battlemaster | battlemaster | battlemaster.bmp |
| Battlemaster2c | **battlemasteriic** | **battlemaster iic.bmp** |
| Behemoth | behemoth | behemoth.bmp |
| Behemoth2 | **behemothii** | **behemoth ii.bmp** |
| Blackhawk | blackhawk | black hawk.bmp |
| Blacknight | **blackknight** | black knight.bmp |
| Blacklanner | blacklanner | black lanner.bmp |
| Brigand | brigand | brigand.bmp |
| Bushwacker | bushwacker | bushwacker.bmp |
| Catapult | catapult | catapult.bmp |
| CauldronBorn | cauldronborn | **cauldronborn.bmp** (table key has hyphen; DNL does not) |
| Chimera | chimera | chimera.bmp |
| Commando | commando | commando.bmp |
| Cougar | cougar | cougar.bmp |
| Cyclops | cyclops | cyclops.bmp |
| Daishi | daishi | daishi.bmp |
| Deimos | deimos | deimos.bmp |
| Dragon | dragon | dragon.bmp |
| Fafnir | fafnir | fafnir.bmp |
| Flea | flea | flea.bmp |
| Gladiator | gladiator | gladiator.bmp |
| Grizzly | grizzly | grizzly.bmp |
| Hauptmann | hauptmann | hauptmann.bmp |
| Hellhound | hellhound | hellhound.bmp |
| Hellspawn | hellspawn | hellspawn.bmp |
| Highlander | highlander | highlander.bmp |
| Hollander | **hollanderii** | **hollander ii.bmp** |
| Hunchback | hunchback | hunchback.bmp |
| Kodiak | kodiak | kodiak.bmp |
| Loki | loki | loki.bmp |
| Longbow | longbow | longbow.bmp |
| Madcat | madcat | mad cat.bmp |
| Madcat_MKII | **madcat2** | mad cat mkii.bmp |
| Masakari | masakari | masakari.bmp |
| Mauler | mauler | mauler.bmp |
| Novacat | novacat | nova cat.bmp |
| Osiris | osiris | osiris.bmp |
| Owens | owens | owens.bmp |
| Puma | puma | puma.bmp |
| Raven | raven | raven.bmp |
| Rifleman | rifleman | rifleman.bmp |
| Ryoken | ryoken | ryoken.bmp |
| Shadowcat | shadowcat | shadow cat.bmp |
| Solitaire | solitaire | solitaire.bmp |
| Sunder | sunder | sunder.bmp |
| Templar | templar | templar.bmp |
| Thanatos | thanatos | thanatos.bmp |
| Thor | thor | thor.bmp |
| Uller | uller | uller.bmp |
| Urbanmech | urbanmech | urbanmech.bmp |
| Uziel | uziel | uziel.bmp |
| Victor | victor | victor.bmp |
| Vulture | vulture | vulture.bmp |
| Warhammer | warhammer | warhammer.bmp |
| Wolfhound | wolfhound | wolfhound.bmp |
| Zeus | zeus | zeus.bmp |

**Critical mismatches where directory name ? hud/MFD stem (files must use the stem, not the dir name):**

| Directory | Wrong name (dir-based) | Correct name (stem) |
|---|---|---|
| Battlemaster2c | battlemaster2c.bmp | **battlemasteriic.bmp** |
| Behemoth2 | behemoth2.bmp | **behemothii.bmp** |
| Blacknight | blacknight.bmp | **blackknight.bmp** |
| Hollander | hollander.bmp | **hollanderii.bmp** |
| Madcat_MKII | madcat_mkii.bmp | **madcat2.bmp** |

**Portrait mismatches for hsh/Mechs/ (table key ? DNL string):**

The `MechChassisTable.tbl` display key and `GetLocString()` DNL string differ for these mechs.  
mw4print uses the DNL string.  The table key is NOT the correct portrait filename for these 5 mechs.

| Directory | Table key (wrong for mw4print) | DNL string (correct portrait stem) |
|---|---|---|
| Assassin2 | AssassinII ? assassinii | `DNL_ASSASSIN2 "Assassin II"` ? **assassin ii.bmp** |
| Battlemaster2c | BattlemasterIIC ? battlemasteriic | `DNL_BATTLEMASTERIIC "Battlemaster IIc"` ? **battlemaster iic.bmp** |
| Behemoth2 | BehemothII ? behemothii | `DNL_BEHEMOTHII "Behemoth II"` ? **behemoth ii.bmp** |
| CauldronBorn | Cauldron-Born ? cauldron-born | `DNL_CAULDRONBORN "Cauldronborn"` ? **cauldronborn.bmp** |
| Hollander | HollanderII ? hollanderii | `DNL_HOLLANDERII "Hollander II"` ? **hollander ii.bmp** |

---

### MechEditor Implementation Notes

The editor encodes all of the above knowledge in two Python dicts:

**`MECH_HSH_STEMS`** (in `mech_editor.py`)  
Maps lowercase dir name ? canonical stem for `hsh/hud/`, `hsh/MFD/`, `hsh/radar/hud/`.  
Source: `huddamage.cpp` `texturename[]` array.

**`MECH_PORTRAIT_OVERRIDES`** (in `mech_editor.py`)  
Maps lowercase dir name ? portrait stem for `hsh/Mechs/` where the DNL string differs from the chassis table key.  
Source: `DNL_*` entries in `Gameleap/code/mw4/Code/scriptstrings/StringResource.rc`.

For all other mechs, the portrait stem is derived dynamically from `MechChassisTable.tbl` (display key, lowercased).

The Assets tab in the editor shows all four image types.  When an image is missing, it displays:
```
Wants: hsh/<subdir>/<expected_filename>.bmp
```
so the user knows exactly what to rename or create.

