The whole-tree audit after the previous commit found four Radar rows scoring
0.266-0.638 against their artwork, with boxes bounding blank space. All four are
chassis whose Radar sets J&J never delivered, so the rows had never been checked
against the art. They are pre-existing, not regressions.
With each chassis's MFD now correct, the Radar mapping is derived from it rather
than measured by hand: source boxes come from the Radar art's own connected
components, zone identity is matched from the MFD by normalised position plus a
size-consistency term, and placement scales each piece's ART CENTRE by ~1.25.
Two details that matter and are easy to get wrong. Position-only matching swaps
CT and HD, because a head and a torso can sit at similar normalised positions;
the size term fixes it. And scaling a piece's corner rather than its centre
misplaces it whenever the Radar piece is a different proportion from its MFD
counterpart - the Longbow's Radar CT includes the pelvis while its MFD CT does
not, which put CT 23px off centre until centre-scaling was used.
kodiak 0.266 -> 0.962
longbow 0.379 -> 0.959
argus 0.457 -> 0.956
fafnir 0.638 -> 0.963
Longbow additionally had a phantom special zone. The chassis declares no
Special1Internal, no joint_specialone and ships no *_specialone.erf, yet both
displays mapped S1. On the Radar that piece is the cockpit canopy - it matches
the CT's recess exactly - so it is remapped to HD and S1 is cleared. On the MFD
the S1 piece is the pelvis, a genuinely different component, and is deliberately
retained so the pelvis still renders; it simply never takes damage.
Whole tree after this change: 130 mech/display combinations, mean 0.949, median
0.955, minimum 0.795, none below 0.75 (was four).
MFD-RADAR-MAPPINGS.md is corrected and expanded. Three things it stated were
wrong and had caused real defects:
- it instructed storing the unexploded rect in texuv and the exploded origin
in offset, which is backwards and broke all 21 imported rows;
- it cited Behemoth MFD x=281 and Black Hawk Radar x=85 as intentional odd
coordinates to preserve, when both are transcription errors bounding blank
space (218 transposed, and a lost leading digit of 285);
- it recorded all 19 supplied BMPs as pixel-identical to the runtime art, when
all 19 differ by 6-35% because the art had never been installed.
Added: the handedness rule and its one-line assertion; the .damage cross-check
for special zones; the derivation method above; a catalogue of every verified
defect; and a tooling-pitfalls section covering blob thresholds hiding small
parts, unreliable hole detection on anti-aliased art, ink coverage not implying
correct placement, and overlap audits needing blob ownership to avoid ~107 false
positives. It also notes that generate_comparison_maps.py compares coord.cpp
only to the measurement files and never to the artwork, so it reported "19
exact" throughout every one of these failures.
Requires a Windows VC6 rebuild of MW4.exe and hardware testing on physical
MFD/Radar displays.
Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
708 lines
35 KiB
Markdown
708 lines
35 KiB
Markdown
# MFD and Radar Damage Mapping Guide
|
|
|
|
This document explains how the external MFD and Radar damage-paper-doll mappings are authored,
|
|
stored, rendered, checked, and added for a new 'Mech. It records the workflow established while
|
|
reviewing and installing all usable art under `Finished HUDS from J&J/` on 2026-08-07.
|
|
|
|
## Scope and terminology
|
|
|
|
There are three related damage displays. Do not mix their mapping tables:
|
|
|
|
| Display | Mapping source | Art path |
|
|
|---|---|---|
|
|
| Normal in-cockpit HUD | `Gameleap/code/mw4/Code/MW4/huddamage.cpp`: `texuv` and `offset` | Content texture `hud\<name>` |
|
|
| External MFD | `Gameleap/code/CoreTech/Libraries/GameOS/coord.cpp`: `texuv2` and `offset2` | Loose `Gameleap/mw4/hsh/hud/<name>.bmp` |
|
|
| Radar secondary damage display | `coord.cpp`: `texuv3` and `offset3` | Loose `Gameleap/mw4/hsh/radar/hud/<name>.bmp` |
|
|
|
|
This guide is about the last two displays, both controlled by `coord.cpp`. The nearby `// MFD`
|
|
comments refer to the external display. The normal cockpit HUD has different coordinates in
|
|
`huddamage.cpp`.
|
|
|
|
The small images in `hsh/MFD/` are also a different asset set. `render.cpp` tiles those images
|
|
into a mech texture atlas. They are not the 512x512 damage-mask images mapped by `coord.cpp`.
|
|
|
|
## Controlling code
|
|
|
|
The file to edit is `Gameleap/code/CoreTech/Libraries/GameOS/coord.cpp`. In the VC6 IDE it is
|
|
not a listed source file of any project: it appears under the **GameOS project's "External
|
|
Dependencies" folder**, because `DXRasterizer.cpp` `#include`s it directly. Open it from there
|
|
(or from disk). Because it is an include, editing it requires rebuilding the game executable,
|
|
not merely relinking.
|
|
|
|
`coord.cpp` defines four positional arrays:
|
|
|
|
```cpp
|
|
float texuv2[65][11][4]; // MFD source rectangles
|
|
int offset2[65][11][2]; // MFD exploded destination positions
|
|
float texuv3[65][11][4]; // Radar source rectangles
|
|
int offset3[65][11][2]; // Radar exploded destination positions
|
|
```
|
|
|
|
The first index is the numeric Mech ID. It must match the positional ID sequence in
|
|
`Gameleap/code/mw4/Code/MW4/MechLabHeaders.h`. At present there are 65 active IDs, 0 through
|
|
64. Each array therefore has exactly 65 active rows in the same order, from Annihilator through
|
|
Zeus. Dasher rows exist but are commented out because Dasher is not active in the roster.
|
|
|
|
The second index always uses this eleven-zone order:
|
|
|
|
```text
|
|
0 LL left leg
|
|
1 RL right leg
|
|
2 LA left arm
|
|
3 RA right arm
|
|
4 RT right torso
|
|
5 LT left torso
|
|
6 CT center torso
|
|
7 CTR center torso rear
|
|
8 HD head
|
|
9 S1 special 1
|
|
10 S2 special 2
|
|
```
|
|
|
|
Use `{0,0,0,0}` and `{0,0}` for a zone that has no art. The comment above `offset2` currently
|
|
says `S2 S1`; the array consumers and the other tables use index 9 as S1 and index 10 as S2.
|
|
Treat the header comment as stale and preserve the index order above.
|
|
|
|
## What the values mean
|
|
|
|
**This is the single most important section. Getting it backwards silently breaks every row,
|
|
and it has already happened once ? see "Verified defect catalogue".**
|
|
|
|
`texuv` is a **source rectangle in the runtime BMP**, and the runtime BMP is the **exploded**
|
|
sheet. `offset` is a **destination point on screen**, which is the **assembled** (unexploded)
|
|
layout. So:
|
|
|
|
| Field | Holds | Measured on |
|
|
|---|---|---|
|
|
| `texuv2` / `texuv3` | the rectangle bounding that component's piece | the **exploded** sheet |
|
|
| `offset2` / `offset3` | the upper-left point where the piece is drawn | the **unexploded / assembled** view |
|
|
|
|
```text
|
|
texuv = (x0, y0, x1, y1) the piece's box on the exploded sheet
|
|
offset = (x2, y2) where that piece belongs on the assembled mech
|
|
drawn bounds = (x2, y2, x2 + (x1-x0), y2 + (y1-y0))
|
|
```
|
|
|
|
The authoring steps below record the unexploded rectangle first and the exploded origin second,
|
|
which makes it tempting to store them in that order. **Do not.** The measurement order is the
|
|
reverse of the storage order:
|
|
|
|
```text
|
|
recorded (x,y,x1,y1) on the unexploded view -> supplies offset (its x,y) and the piece SIZE
|
|
recorded (x2,y2) on the exploded view -> supplies texuv (its origin) + that same size
|
|
```
|
|
|
|
The quickest check: **every `texuv` rectangle must tightly bound real artwork in the exploded
|
|
BMP.** If a `texuv` lands on blank space or straddles two pieces, the two fields are transposed.
|
|
|
|
Coordinates are normally even because both pipelines were designed around even pixel boundaries,
|
|
and Radar's integer `/2` at runtime truncates odd values. But **an odd or surprising value is
|
|
more often a transcription error than an intentional choice**. Verify it against the artwork
|
|
before preserving it. Values previously documented here as "intentional odd coordinates" were
|
|
later proved to be typos: Behemoth MFD `x=281` is a digit transposition of `218`, and Black Hawk
|
|
Radar `x=85` lost the leading digit of `285`. Both bounded blank space. Work from the upper-left
|
|
to lower-right of each component and do not overlap source rectangles.
|
|
|
|
## Canonical authoring steps
|
|
|
|
This is the project owner's own specification of the two pipelines, recorded verbatim. It is the
|
|
authority. The expanded prose sections that follow explain and justify these steps but must not
|
|
contradict them.
|
|
|
|
### MFD (verbatim)
|
|
|
|
```text
|
|
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)
|
|
```
|
|
|
|
### Radar (verbatim)
|
|
|
|
```text
|
|
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)
|
|
```
|
|
|
|
### Points that are easy to lose
|
|
|
|
- The two pipelines differ in **when** the canvas becomes 512. Radar expands to 512 **before**
|
|
the unexploded view is saved, so Radar coordinates are measured in 512 space. MFD expands to
|
|
512 **after** all coordinates are recorded, so MFD coordinates are measured in 340 space.
|
|
- MFD separates sections with a **4x4 black line**; Radar outlines components with a **2x2 white
|
|
line**. These separators exist so no source rectangle can bleed into a neighbouring component.
|
|
- Both `(x,y,x1,y1)` rectangles are measured on the **unexploded** view, never on the exploded
|
|
BMP.
|
|
- `(x2,y2)` is recorded by keeping the unexploded view in the upper-left and laying the exploded
|
|
pieces over that same canvas, so offsets share the unexploded coordinate space.
|
|
- The exploded view is saved in **indexed color mode**. That saved exploded 512x512 image is the
|
|
runtime BMP.
|
|
- Prefer even numbers when recording, but never round an odd value that has already been
|
|
visually validated.
|
|
- **The doll faces the viewer.** The mech's own right side must be drawn on the viewer's LEFT,
|
|
so that shooting an enemy's right arm lights the left of your display. Art authored from the
|
|
mech's own viewpoint must have its `L`/`R` zone pairs swapped before it is stored. See
|
|
"Handedness".
|
|
- The measurement order is the reverse of the storage order. See "What the values mean".
|
|
|
|
## MFD authoring pipeline
|
|
|
|
Start with the finished 1024x1024 full-color 'Mech image.
|
|
|
|
1. Reduce the image to 320x320.
|
|
2. Expand the canvas to 340x340, centered. This adds 10 pixels on every side.
|
|
3. Separate adjacent body sections with a 4x4-pixel black line so source rectangles do not
|
|
overlap or bleed into one another.
|
|
4. Save this as the unexploded working view.
|
|
5. For every zone, record its unexploded `(x0,y0,x1,y1)` rectangle, preferring even values.
|
|
6. Starting from the unexploded view in the upper-left, place copies of the separated pieces in
|
|
their exploded positions.
|
|
7. Record each exploded piece's upper-left `(x2,y2)` coordinate, preferring even values.
|
|
8. Expand the canvas to 512x512, anchored at the upper-left. Added area is black.
|
|
9. Convert the exploded view to indexed color mode and save it as the runtime BMP.
|
|
|
|
Store the **exploded** boxes in `texuv2` and the **unexploded** upper-left points in `offset2`.
|
|
This is the reverse of the order in which they are measured; see "What the values mean".
|
|
|
|
At runtime, `huddamage.cpp` draws an MFD component as:
|
|
|
|
```cpp
|
|
mfd_device.DrawTexture(
|
|
offset2[mech][zone][0] + 100,
|
|
offset2[mech][zone][1] + 40,
|
|
color,
|
|
texuv2[mech][zone][0],
|
|
texuv2[mech][zone][1],
|
|
texuv2[mech][zone][2],
|
|
texuv2[mech][zone][3]);
|
|
```
|
|
|
|
The `+100,+40` values position the complete paper doll in the MFD UI. They are not part of the
|
|
authored coordinates and must not be added to `coord.cpp`.
|
|
|
|
`CMFD_Device::LoadDamageTexture()` receives the logical name `hud\<name>` and loads the loose
|
|
bitmap at `hsh\hud\<name>.bmp`.
|
|
|
|
## Radar authoring pipeline
|
|
|
|
Start with the same finished 1024x1024 full-color image.
|
|
|
|
1. Reduce the image to 400x400.
|
|
2. Expand the canvas to 410x410, centered. This adds 5 pixels on every side.
|
|
3. Expand the canvas to 512x512, anchored at the upper-left. Added area is black.
|
|
4. Save this as the unexploded working view.
|
|
5. Split the body into non-overlapping components from each component's upper-left to
|
|
lower-right.
|
|
6. Outline components with a 2x2-pixel white line.
|
|
7. Record each unexploded `(x0,y0,x1,y1)` rectangle, preferring even values.
|
|
8. Starting from the unexploded view in the upper-left, place copies of the pieces in their
|
|
exploded positions.
|
|
9. Record each exploded piece's upper-left `(x2,y2)` coordinate, preferring even values.
|
|
10. Convert the exploded view to indexed color mode and save it as the runtime BMP.
|
|
|
|
Store the **exploded** boxes in `texuv3` and the **unexploded** upper-left points in `offset3`.
|
|
This is the reverse of the order in which they are measured; see "What the values mean".
|
|
|
|
Radar coordinates are authored in the full 512x512 coordinate space. The runtime deliberately
|
|
divides every rectangle and offset coordinate by two, switches the texture dimensions to
|
|
256x256 for the draw, and then adds the Radar UI origin:
|
|
|
|
```cpp
|
|
radar_device.DrawTexture(
|
|
(offset3[mech][zone][0] / 2) + 138,
|
|
(offset3[mech][zone][1] / 2) + 406,
|
|
color,
|
|
texuv3[mech][zone][0] / 2,
|
|
texuv3[mech][zone][1] / 2,
|
|
texuv3[mech][zone][2] / 2,
|
|
texuv3[mech][zone][3] / 2);
|
|
```
|
|
|
|
Do not pre-divide values when editing `coord.cpp`. Author and store the full-size values;
|
|
preserve visually validated odd coordinates because runtime integer division determines their
|
|
final half-resolution placement.
|
|
The runtime loads `hsh\radar\hud\<name>.bmp` through
|
|
`CRadar_Device::LoadRadarDamageTexture()`.
|
|
|
|
## Reading an existing mapping
|
|
|
|
1. Find the Mech ID in `MechLabHeaders.h`.
|
|
2. Confirm that the same row number is used in all four `coord.cpp` arrays. Comments are useful
|
|
labels but do not control the mapping; array position does.
|
|
3. Read the eleven `texuv` rectangles in the fixed zone order above.
|
|
4. Read the eleven matching `offset` points in the same order.
|
|
5. Ignore spelling differences in end-of-row comments unless they indicate an actual row-order
|
|
error. Examples in the current file include `M_HollnaderII` and capitalization differences.
|
|
6. Check that every nonzero rectangle has a corresponding nonzero offset, except where a design
|
|
intentionally draws a component at `(0,0)`.
|
|
7. Score every rectangle against the runtime BMP. This is the only check that catches
|
|
transposition, mirroring and transcription errors at once.
|
|
8. Check rectangle bounds and non-overlap, and treat any odd or surprising coordinate as suspect
|
|
until it is confirmed against the artwork. MFD source geometry is normally inside the 340x340
|
|
working area; Radar's nominal art area is 410x410, though exploded pieces legitimately occupy
|
|
the full 512x512 sheet. A rectangle that bounds blank space is an error, not a style.
|
|
|
|
## Making or fixing a mapping
|
|
|
|
1. Preserve the Mech's row position. Never sort one array independently.
|
|
2. Prepare separate MFD and Radar unexploded images using the pipelines above.
|
|
3. Measure source rectangles in zone order and record them in a text file before touching code.
|
|
4. Prepare exploded layouts and record each upper-left destination point.
|
|
5. Verify visually with overlays.
|
|
6. Check handedness: `offset[RA].x < offset[LA].x`, and likewise for the torso and leg pairs.
|
|
7. Check `S1`/`S2` against the chassis's `.damage` file before mapping either.
|
|
8. Score every rectangle against the artwork; a set should average 0.85 or better excluding `HD`.
|
|
9. Replace only that Mech's row in `texuv2`, `offset2`, `texuv3`, and `offset3` as needed.
|
|
10. Preserve `coord.cpp` CRLF line endings and each row's own indentation and trailing text.
|
|
11. Inspect the diff. A one-Mech repair should change at most four rows unless art or roster
|
|
registration is also being changed.
|
|
12. Rebuild the game because `DXRasterizer.cpp` includes `coord.cpp` directly. Rebuild the needed
|
|
Release/Profile targets and redeploy the resulting executable.
|
|
13. Compare the supplied exploded BMP to the existing canonical runtime BMP by decoded pixels,
|
|
not only by file hash. Replace it only when the pixels differ. See "Runtime art comparison"
|
|
below.
|
|
14. Install a changed runtime BMP under the matching `Gameleap/mw4/hsh` path. Loose `hsh` art
|
|
does not require a resource-package rebuild. Edit the source tree `Gameleap/mw4/hsh`, not the
|
|
`MW4/` deployment, which `deploy-mw4.ps1` regenerates.
|
|
15. Test both intact and damaged states on the real external MFD and Radar displays.
|
|
|
|
When adding a new chassis rather than repairing an existing row, also increase the first
|
|
dimension of all four arrays and add one correctly positioned row to every array. This is part
|
|
of the larger positional-ID workflow documented in `ADDING-A-MECH.md`.
|
|
|
|
## Overlay validation method
|
|
|
|
Visual overlays are the fastest way to distinguish a coordinate problem from an art problem.
|
|
|
|
Generate every available J&J comparison and the status report with:
|
|
|
|
```bash
|
|
python3 "Finished HUDS from J&J/generate_comparison_maps.py"
|
|
```
|
|
|
|
This requires Pillow. It writes two PNGs per available display and updates
|
|
`Finished HUDS from J&J/COMPARISON-SUMMARY.md`. The summary reports exact numeric matches,
|
|
different zones, missing display inputs, and malformed source measurements.
|
|
|
|
Two J&J measurement formats exist:
|
|
|
|
- Modern: the Unexploded file contains four-value source rectangles and the Exploded file
|
|
contains two-value destination origins.
|
|
- Legacy: the Unexploded file contains only each source rectangle's upper-left point, while the
|
|
Exploded file contains the complete destination bounding box. For legacy data, derive the
|
|
source width and height from the exploded box, apply those dimensions at the unexploded point,
|
|
and use the exploded box's upper-left as the destination offset.
|
|
|
|
Some combined `Coords.txt` files contain both sections. Trust the section heading and tuple
|
|
shape, not the filename alone. The generator handles both formats and indexes current mappings
|
|
by numeric Mech ID rather than end-of-row comment spelling.
|
|
|
|
All-zero absent-zone tuples are found in both two-value and four-value forms in legacy files.
|
|
Normalize an all-zero tuple to the width expected by its section; it still means no art. For
|
|
nonzero tuples, a wrong tuple width is an input error and must not be guessed.
|
|
|
|
For an unexploded comparison:
|
|
|
|
- Use the correctly transformed unexploded canvas, not the exploded-pieces BMP.
|
|
- Draw the current `texuv` rectangles in red.
|
|
- Draw proposed rectangles in green.
|
|
- Label each rectangle with its zone.
|
|
- A good rectangle encloses one component, follows its outer extent, and does not include pixels
|
|
from another component.
|
|
|
|
For an exploded comparison:
|
|
|
|
- Use the exploded-pieces BMP.
|
|
- Mark each `offset` as an upper-left cross.
|
|
- Draw a box from that point using the matching unexploded rectangle's width and height.
|
|
- Draw current mappings in red and proposed mappings in green.
|
|
- A good exploded mapping places the box exactly over the intended exploded piece.
|
|
|
|
Do not draw `texuv2` rectangles directly on the raw 1024x1024 source. For MFD, first apply
|
|
1024 -> 320, center on 340, then place at the upper-left of 512. For Radar, apply
|
|
1024 -> 400, center on 410, then place at the upper-left of 512. When a supplied legacy
|
|
unexploded image is already 512x512, use it directly rather than reconstructing and cropping it;
|
|
the supplied coordinates may intentionally reference pixels outside the nominal 410 area.
|
|
|
|
## Runtime art comparison
|
|
|
|
The exploded 512x512 BMP is the runtime asset. The unexploded BMP and full-color source image
|
|
are authoring and measurement references only; do not copy those into `hsh` as the runtime art.
|
|
|
|
Resolve the destination by the canonical runtime texture stem for the numeric Mech ID, not by
|
|
blindly copying the supplied filename. Current destinations are:
|
|
|
|
```text
|
|
MFD: Gameleap/mw4/hsh/hud/<stem>.bmp
|
|
Radar: Gameleap/mw4/hsh/radar/hud/<stem>.bmp
|
|
```
|
|
|
|
Known naming traps from this review:
|
|
|
|
- Assassin II supplied files are misspelled `assian2_*`, but the canonical runtime stem is
|
|
`assassin2`; do not overwrite the separate historical `assassinii.bmp` by accident.
|
|
- The existing Fafnir runtime file is `Fafnir.bmp` with an uppercase `F` on this case-sensitive
|
|
checkout. Resolve destination names case-insensitively, then preserve the existing spelling.
|
|
- Folder names, comments, and supplied filenames are labels. Numeric Mech ID and the runtime
|
|
texture-name table remain authoritative.
|
|
|
|
BMP byte equality is stricter than visual/runtime equality. A supplied file may be indexed `P`,
|
|
grayscale `L`, or `RGB`, and may carry a different palette, header, row padding, or metadata while
|
|
decoding to exactly the same pixels. Compare in this order before replacing anything:
|
|
|
|
1. Confirm both images are 512x512.
|
|
2. If file bytes match, the destination is byte-exact.
|
|
3. Otherwise decode both images, convert both to a common mode such as RGBA, and compare every
|
|
pixel.
|
|
4. If decoded pixels match, the destination is already correct. Keep the existing runtime file;
|
|
replacing it creates binary churn and may exchange a compact indexed/grayscale BMP for RGB.
|
|
5. Copy the supplied exploded BMP only if decoded pixels differ, then repeat the pixel comparison
|
|
against the installed destination.
|
|
|
|
Loose `hsh` art is not packed into a `.mw4` resource for this path, so an actual art replacement
|
|
does not require `build-resources.ps1`. Coordinate changes still require rebuilding the game
|
|
executable because `DXRasterizer.cpp` includes `coord.cpp` directly.
|
|
|
|
## Reproducible validation
|
|
|
|
> **`generate_comparison_maps.py` cannot detect the failures that actually occurred.** It compares
|
|
> `coord.cpp` against the J&J measurement files only ? never against the artwork. It therefore
|
|
> reported "19 exact, 0 different" while the stored rows had `texuv`/`offset` transposed, left and
|
|
> right mirrored, eight transcription errors, and artwork that had never been installed. It
|
|
> confirms transcription fidelity, nothing more. It now reports differences by design, because
|
|
> the stored rows deliberately diverge from the supplied files in the places listed below.
|
|
|
|
The check that does work is scoring each stored rectangle against the artwork it claims to bound.
|
|
For every zone, take the best intersection-over-union against the connected components of the
|
|
runtime BMP, and average per set **excluding `HD`**:
|
|
|
|
```text
|
|
healthy 0.85 - 0.99
|
|
suspect below 0.75
|
|
broken below 0.55
|
|
```
|
|
|
|
The distribution is strongly bimodal, so the threshold is not delicate. Whole-tree state after
|
|
this work, across all 65 chassis and both displays:
|
|
|
|
```text
|
|
130 combinations mean 0.949 median 0.955 minimum 0.795 none below 0.75
|
|
```
|
|
|
|
Also verify all of the following before considering the source complete:
|
|
|
|
- Each of `texuv2`, `offset2`, `texuv3`, and `offset3` still has 65 active rows.
|
|
- Every active row still has exactly 11 zones.
|
|
- The edited `coord.cpp` remains CRLF-only; the repository deliberately uses byte-exact files.
|
|
Note that mech ID 60's rows in `offset2`/`offset3` lack the leading tab, and rows for Avatar,
|
|
Vulture and Zeus have irregular trailing whitespace. Preserve each row's own prefix and suffix
|
|
rather than assuming a uniform format.
|
|
- The diff changes only intended Mech rows.
|
|
- Handedness holds for every row: `offset[RA].x < offset[LA].x`.
|
|
- Special zones agree with each chassis's `.damage` file.
|
|
- A Windows VC6 Release/Profile rebuild and physical MFD/Radar test remain required; the Linux
|
|
comparison workflow validates data and geometry but cannot replace that runtime test.
|
|
|
|
## Verified defect catalogue
|
|
|
|
Every one of these was found by scoring rectangles against artwork and confirmed by eye. They are
|
|
recorded because they show what the failure modes look like.
|
|
|
|
### Systemic
|
|
|
|
| Defect | Effect | Detection |
|
|
|---|---|---|
|
|
| `texuv`/`offset` transposed on all 21 imported rows | no rectangle bounded its own art | every zone scores far below 0.55 |
|
|
| left/right mirrored on all 21 imported rows | damage displayed on the wrong side | `offset[RA].x > offset[LA].x` |
|
|
| artwork never installed | rows described art that was not on disk | 6-35% of pixels differ from the supplied BMP |
|
|
|
|
The handedness fault was confined exactly to the imported rows: all 13 affected chassis were
|
|
mirrored, and all 52 untouched chassis were correct. That correlation is what proved the fault
|
|
arrived with the import rather than existing in the original game data.
|
|
|
|
### Transcription errors in the supplied measurement files
|
|
|
|
| Chassis | Zone | As written | Corrected | Nature |
|
|
|---|---|---|---|---|
|
|
| Annihilator Radar | RA | `30,24,126,42` | `...,142` | dropped digit |
|
|
| Assassin II Radar | CT | 60x104 | 64x192 | omitted the pelvis flare |
|
|
| Behemoth MFD | LA | `x=281` | `218` | digit transposition |
|
|
| Black Hawk MFD | RT | `0,96,152,114` | `96,0,...` | first pair transposed |
|
|
| Black Hawk MFD | HD | `164,146,178,150` | `...,136,...` | box only 4px tall |
|
|
| Black Hawk Radar | LT | `85,56,362,200` | `284,...` | lost leading digit |
|
|
| Black Hawk Radar | LA | `y0=30` | `28` | clipped 2 rows of art |
|
|
| Fafnir MFD | RT, LA | ? | trimmed 2px | box overlapped the neighbouring piece |
|
|
|
|
Previously normalized in the source files without changing geometry: Behemoth MFD LT `1742`
|
|
to `174`, Behemoth Radar CT `202.242` to `202,242`, Fafnir MFD LL `162.326` to `162,326`, and
|
|
Behemoth Radar's blank S2 made explicit as `0,0`.
|
|
|
|
### Mapping errors
|
|
|
|
- **Kodiak MFD `HD`** sat mid-torso on flat plating; moved into the CT's cockpit slot.
|
|
- **Warhammer MFD `CT`** sat 19px right, burying `LT` inside it and throwing `HD` off centre.
|
|
Moving `CT` alone fixed all three symptoms.
|
|
- **Longbow Radar `S1`** was the cockpit canopy, not a special; remapped to `HD`.
|
|
- **Argus, Fafnir, Kodiak, Longbow Radar** were wholly misaligned (0.266-0.638) and were derived
|
|
from their corrected MFD counterparts.
|
|
|
|
## Status as of 2026-08-09
|
|
|
|
All 19 supplied display sets are installed with corrected geometry, and the four broken
|
|
opposite-display Radar mappings have been derived. Artwork for all 19 sets was installed for the
|
|
first time; `behemothii` art was resynced from `behemoth` because ID 12 inherits ID 11's rows.
|
|
|
|
| Mech ID | Chassis | MFD | Radar |
|
|
|---:|---|:---:|:---:|
|
|
| 0 | Annihilator | J&J, corrected | J&J, corrected |
|
|
| 4 | Argus | J&J, corrected | derived 0.956 |
|
|
| 5 | Assassin II | J&J, corrected | J&J, corrected |
|
|
| 7 | Avatar | J&J, corrected | J&J, corrected |
|
|
| 11 | Behemoth | J&J, corrected | J&J, corrected |
|
|
| 12 | Behemoth II | inherited from ID 11 | inherited from ID 11 |
|
|
| 13 | Black Hawk | J&J, corrected | J&J, corrected |
|
|
| 27 | Fafnir | J&J, corrected | derived 0.963 |
|
|
| 28 | Flea | J&J, corrected | original, 0.967 |
|
|
| 29 | Gladiator | J&J, corrected | original, 0.984 |
|
|
| 33 | Hellspawn | original | J&J, corrected |
|
|
| 37 | Kodiak | J&J, corrected | derived 0.962 |
|
|
| 39 | Longbow | J&J, corrected | derived 0.959 |
|
|
| 62 | Warhammer | J&J, corrected | J&J, corrected |
|
|
|
|
Outstanding, both pre-existing and blocked on missing art:
|
|
|
|
- **Annihilator** declares `Special2Internal` with no S2 mapping or art.
|
|
- **Behemoth II** declares both specials but inherits Behemoth's rows, which define only S1.
|
|
|
|
The supplied exploded BMPs were **not** pixel-identical to the runtime files, contrary to what
|
|
this document previously recorded. All 19 differed by 6-35% of pixels, confirming the art had
|
|
never been installed. Sources arrived as a mix of `P`, `L` and `RGB` and were normalised to
|
|
512x512 8-bit greyscale to match the existing `hsh` convention.
|
|
|
|
### Assassin II details
|
|
|
|
Reference art and measurements are under:
|
|
|
|
```text
|
|
Finished HUDS from J&J/AssassinII/
|
|
```
|
|
|
|
The supplied files are:
|
|
|
|
```text
|
|
HUD/assassinII_HUD_unexploded.txt
|
|
HUD/assassinII_hud_exploded.txt
|
|
Radar/assassinII_radar_unexploded.txt
|
|
Radar/assassinII_radar_exploded.txt
|
|
```
|
|
|
|
The supplied Assassin II values were checked against the correctly transformed source art.
|
|
Both corrected MFD and Radar mappings align. All four rows were installed in `coord.cpp` on
|
|
2026-08-07 and are pending an executable rebuild and hardware test.
|
|
|
|
Generated comparison images from the review are:
|
|
|
|
```text
|
|
Finished HUDS from J&J/AssassinII/HUD/assian2_mfd_unexploded_coords_comparison.png
|
|
Finished HUDS from J&J/AssassinII/HUD/assian2_mfd_exploded_coords_comparison.png
|
|
Finished HUDS from J&J/AssassinII/Radar/assian2_radar_unexploded_coords_comparison.png
|
|
Finished HUDS from J&J/AssassinII/Radar/assian2_radar_exploded_coords_comparison.png
|
|
```
|
|
|
|
The generator now draws the installed `coord.cpp` mapping in red and the supplied J&J mapping in
|
|
green, so the colors overlap exactly. These PNGs are review art only and are not loaded by the
|
|
game.
|
|
|
|
### Implemented Assassin II rows
|
|
|
|
These are the corrected rows as stored on 2026-08-09, after the transposition and handedness
|
|
fixes and after extending Radar `CT` to include the pelvis flare. Compare against the raw
|
|
supplied files and the differences are exactly those two systemic corrections plus that one
|
|
zone.
|
|
|
|
MFD `texuv2` (exploded boxes):
|
|
|
|
```cpp
|
|
{{282,148,332,334},{ 6,148, 56,334},{266, 10,332,116},{ 6, 10, 74,116},{ 98, 22,136,120},{206, 22,246,120},{144,180,194,330},{ 0, 0, 0, 0},{158,140,180,156},{148, 10,190, 48},{ 0, 0, 0, 0}}
|
|
```
|
|
|
|
MFD `offset2` (assembled points):
|
|
|
|
```cpp
|
|
{{190,142},{ 98,142},{216, 48},{ 56, 48},{120, 22},{180, 22},{144, 36},{ 0, 0},{158, 70},{148, 10},{ 0, 0}}
|
|
```
|
|
|
|
Radar `texuv3`:
|
|
|
|
```cpp
|
|
{{336,220,398,456},{ 8,220, 72,456},{314, 48,398,184},{ 8, 48, 92,184},{120, 48,168,170},{244, 48,294,170},{178,206,242,398},{ 0, 0, 0, 0},{192, 78,218, 98},{178, 3,230, 49},{ 0, 0, 0, 0}}
|
|
```
|
|
|
|
Radar `offset3`:
|
|
|
|
```cpp
|
|
{{232,170},{114,170},{264, 48},{ 60, 48},{142, 18},{218, 18},{174, 36},{ 0, 0},{192, 78},{178, 2},{ 0, 0}}
|
|
```
|
|
|
|
Note `LL` precedes `RL` in storage order while `offset[RL].x < offset[LL].x` on screen ? that is
|
|
the handedness rule working correctly, not an error.
|
|
|
|
The Radar `S1` rectangle retains an odd `y=3`. Unlike the other odd values once recorded here,
|
|
this one was checked against the artwork and is genuinely the piece's edge, so it stands.
|
|
|
|
## Handedness
|
|
|
|
The paper doll shows the mech **facing the viewer**. Damage must map to the side the shooter
|
|
actually hit, so:
|
|
|
|
```text
|
|
the mech's RIGHT zones (RA, RT, RL) are drawn on the VIEWER'S LEFT
|
|
the mech's LEFT zones (LA, LT, LL) are drawn on the VIEWER'S RIGHT
|
|
```
|
|
|
|
In stored terms, `offset[RA].x < offset[LA].x` for every chassis. This is checkable mechanically
|
|
and should be asserted on any new or imported row:
|
|
|
|
```python
|
|
assert off["RA"][0] < off["LA"][0] # and likewise RT/LT, RL/LL
|
|
```
|
|
|
|
`S1`/`S2` are **not** a left/right pair by convention. Shipped chassis have S1 on either side, so
|
|
only swap them when both exist and they are genuinely mirrored hardpoints. When only one special
|
|
is defined, leave it in `S1`.
|
|
|
|
Art supplied by an external author is frequently labelled from the mech's own viewpoint, which is
|
|
the natural way to think when drawing a mech. The entire J&J delivery was labelled that way. The
|
|
check above catches it in one pass.
|
|
|
|
## Cross-check special zones against the damage model
|
|
|
|
A zone must not be mapped unless the chassis actually declares it. The authority is the mech's
|
|
`.damage` file under `Gameleap/mw4/Content/Mechs/<Chassis>/`:
|
|
|
|
```text
|
|
[joint_specialone] + [Special1Internal] + <prefix>_specialone.erf -> S1 exists
|
|
[Special2Internal] -> S2 exists
|
|
neither -> S1 and S2 must be {0,0,0,0}
|
|
```
|
|
|
|
Run this whenever rows are imported. It caught two real problems:
|
|
|
|
- **Longbow** declares no special zone at all, yet both displays had `S1` mapped. On the Radar
|
|
that "S1" piece was in fact the **cockpit canopy** and is now mapped to `HD`. On the MFD it is
|
|
the pelvis; it is deliberately retained so the pelvis still renders, and simply never takes
|
|
damage.
|
|
- **Annihilator** declares `Special2Internal` and **Behemoth II** declares both specials, but
|
|
neither has the corresponding mapping. These remain unmapped for lack of art.
|
|
|
|
The general lesson: a piece labelled `S1` by an art supplier is only a guess. Confirm what the
|
|
component actually is before trusting the label.
|
|
|
|
## Deriving a missing display from the other
|
|
|
|
Six chassis were delivered with only one of the two displays. Where the opposite display's art
|
|
exists but its mapping is wrong, the mapping can be derived rather than measured by hand, using
|
|
the corrected display as a template. This produced 0.956-0.963 mean fits for Argus, Fafnir,
|
|
Kodiak and Longbow Radar.
|
|
|
|
Method:
|
|
|
|
1. Detect the connected components in the target display's exploded BMP. Their bounding boxes
|
|
are the `texuv` rectangles directly.
|
|
2. Assign zone identity by matching each piece against the **known-good** other display, scoring
|
|
normalised position **plus a size-consistency term**. Position alone is not enough ? it
|
|
confidently swapped `CT` and `HD` on every chassis, because a head and a torso can sit at
|
|
similar normalised positions.
|
|
3. Compute the scale as the median of matched piece sizes. MFD to Radar is about **1.25-1.30**,
|
|
not 2, despite Radar's runtime `/2`.
|
|
4. Place each piece by scaling its **art centre**, not its bounding-box corner. Radar pieces are
|
|
not the same proportion as their MFD counterparts ? the Longbow's Radar `CT` includes the
|
|
pelvis while its MFD `CT` does not ? so scaling the corner misplaces them. Centre-scaling put
|
|
`CT` within 0-2px of the `RT`/`LT` midpoint on all four chassis; corner-scaling was 23px out.
|
|
5. A zone present in the template but absent from the target's art (a head drawn into the CT
|
|
rather than as a separate piece) is placed by its position **relative to CT in the assembled
|
|
layout**, then cut from the CT piece. Do not use its position within the CT's *sheet* box ?
|
|
the two are unrelated.
|
|
6. Clamp the finished layout so nothing lands at a negative coordinate.
|
|
7. Review every zone visually and adjust. Automation gets the fit; only the eye gets the pose.
|
|
|
|
## Tooling pitfalls
|
|
|
|
Hard-won, all of which produced a wrong answer at least once during this work:
|
|
|
|
- **Blob detection hides small parts.** A minimum-area threshold of 300px silently drops heads
|
|
and small pods, making a perfectly good zone score 0.00. Use ~30px and confirm before
|
|
concluding a zone is broken. Four "defects" evaporated when the threshold was lowered.
|
|
- **A low IoU on `HD` is usually normal.** Heads are slivers cut from the CT and often have no
|
|
dedicated blob. Score sets on the mean **excluding** `HD`.
|
|
- **Enclosed-hole detection is unreliable on anti-aliased art.** A thin trail of mid-grey edge
|
|
pixels can connect a real recess to unrelated dark regions, inflating the bounding box until
|
|
"centre the head in the hole" pushes it onto solid plating. Always render the host piece with
|
|
the detected hole outlined before trusting it. This produced one wrong Longbow edit that had
|
|
to be reverted; the Kodiak edit survived the same test because its slot is a clean isolated
|
|
rectangle.
|
|
- **High ink coverage does not mean correct placement.** A head box scoring 100% ink may simply
|
|
be sitting on the solid interior of the torso. Compare candidate positions visually.
|
|
- **Overlap audits need blob ownership.** Naively flagging any box that overlaps any blob reports
|
|
~107 false positives, because detail features inside a piece register as separate blobs. Assign
|
|
each blob to its best-matching zone first, then flag only cross-zone overlaps: that reduced the
|
|
same batch to 4 real cases.
|
|
- **Mirrored pairs must be levelled on the ART, not the box.** Mirrored pieces often carry
|
|
different internal padding, so matching box edges leaves the visible art unlevel. Measure the
|
|
first and last inked row.
|
|
- **`str.replace` on a config file hits every match.** An override keyed `("behemoth","mfd","LA")`
|
|
existed in two dicts and a single replace corrupted both. Anchor edits or rewrite the file.
|
|
|
|
## Common mistakes
|
|
|
|
- Drawing MFD boxes on the raw 1024 image instead of the transformed 340 working canvas.
|
|
- Drawing unexploded rectangles on the exploded-pieces BMP.
|
|
- Using `texuv`/`offset` from `huddamage.cpp` when the task is external MFD/Radar mapping.
|
|
- Using the small `hsh/MFD` atlas image as the external damage-mask source.
|
|
- Pre-dividing Radar coordinates by two before putting them in `coord.cpp`.
|
|
- Adding the MFD `+100,+40` or Radar `+138,+406` UI origins to authored offsets.
|
|
- Treating `(x2,y2)` as a second rectangle instead of an upper-left destination point.
|
|
- Measuring overlapping component rectangles or omitting separator/outline pixels.
|
|
- Swapping S1 and S2 because of the stale `offset2` header comment.
|
|
- Inserting a row into only one parallel array and shifting every later Mech out of alignment.
|
|
- Re-encoding or normalizing the entire legacy source file while changing four rows.
|
|
- Treating a different BMP hash as different art without decoding and comparing pixels.
|
|
- Copying an unexploded/reference BMP instead of the exploded 512x512 runtime BMP.
|
|
- Using the supplied `assian2` typo as the destination instead of canonical `assassin2.bmp`.
|
|
- Rounding odd coordinates despite a visually aligned overlay.
|
|
- Storing the unexploded rectangle in `texuv` and the exploded origin in `offset`. They are the
|
|
other way round; this broke all 21 imported rows.
|
|
- Accepting art labelled from the mech's own viewpoint without swapping the L/R zone pairs.
|
|
- Trusting an `S1`/`S2` label from an art supplier without checking the chassis's `.damage` file.
|
|
- Trusting `generate_comparison_maps.py` as a correctness check. It only compares `coord.cpp` to
|
|
the measurement files, never to the artwork.
|
|
- Concluding a zone is broken from a low IoU without first lowering the blob-area threshold, or
|
|
from a low `HD` score, which is normal.
|
|
- Editing `MW4/hsh` instead of the source tree `Gameleap/mw4/hsh`. |