Repair the four broken Radar damage mappings; document the findings

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>
This commit is contained in:
2026-08-09 13:14:32 -05:00
co-authored by Claude Opus 5 GitHub Copilot
parent ef4e014066
commit a26ff967d8
2 changed files with 283 additions and 107 deletions
@@ -145,7 +145,7 @@ float texuv3[65][11][4]={
{{351,185,422,405},{ 84,185,154,405},{407, 52,492,170},{ 18, 52,103,170},{132, 24,178,140},{334, 24,380,140},{200, 5,310,228},{ 0, 0, 0, 0},{249,273,262,284},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Archer
{{356,260,456,490},{ 30,260,132,488},{354,188,454,252},{ 32,188,132,252},{ 62,104,138,180},{358, 94,434,180},{200, 90,308,300},{ 0, 0, 0, 0},{234, 36,262, 56},{360, 8,464, 80},{ 32, 8,136, 80}}, // M_Arctic Wolf
{{400,173,481,397},{ 25,173,106,397},{398, 71,500,132},{ 12, 72,114,133},{122, 84,197,177},{315, 83,390,177},{209, 91,305,241},{ 0, 0, 0, 0},{246,350,268,360},{224,278,290,314},{201, 3,313, 67}}, // M_Ares
{{300,200,448,448},{ 20,200,170,448},{380, 20,490,146},{ 20, 20,124,116},{140, 20,166,100},{300, 20,342,110},{180, 20,292,242},{ 0, 0, 0, 0},{250,300,276,306},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Argus - Skippy
{{347,165,492,400},{ 8,165,153,400},{365, 24,472,148},{ 60, 31,149,125},{161, 40,196,145},{301, 40,341,144},{216, 21,281,225},{ 0, 0, 0, 0},{233,277,265,288},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Argus - Skippy
{{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}}, // M_Assassin2
{{350,260,420,502},{128,246,200,488},{426, 16,500,172},{ 24, 14, 98,172},{162, 26,216,160},{352, 26,406,160},{258, 50,312,268},{ 0, 0, 0, 0},{262, 6,282, 18},{118,118,146,144},{436,218,464,282}}, // M_Atlas
{{384,258,450,484},{ 50,258,116,484},{408, 38,490,112},{ 24, 38, 80,110},{104, 38,152,162},{324, 38,372,162},{198, 38,278,248},{ 0, 0, 0, 0},{230,356,248,372},{224,302,254,332},{ 0, 0, 0, 0}}, // M_Avatar
@@ -168,7 +168,7 @@ float texuv3[65][11][4]={
{{340,272,496,496},{ 14,272,170,496},{428, 92,490,216},{ 20, 92, 82,216},{100, 74,186,218},{314, 74,400,218},{208, 98,300,276},{ 0, 0, 0, 0},{222, 10,286, 54},{218,354,294,408},{ 0, 0, 0, 0}}, // M_Daishi
{{372,258,470,496},{ 42,258,138,496},{398,118,476,218},{ 34,118,112,220},{144,116,216,204},{296,116,368,204},{234,120,278,288},{ 0, 0, 0, 0},{240,320,272,336},{272, 14,346, 78},{166, 14,240, 78}}, // M_Deimos
{{348,238,430,498},{ 94,238,176,498},{430,146,488,222},{ 40,134, 98,206},{ 58, 40,132,128},{412, 40,476,128},{208, 56,326,240},{ 0, 0, 0, 0},{250,314,284,332},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Dragon
{{390,258,488,488},{ 20,260,118,490},{440, 20,500,162},{ 20, 20, 78,162},{160,200,266,466},{320, 20,428,186},{180, 20,308,266},{ 0, 0, 0, 0},{300,300,324,316},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Fafnir - Skippy
{{385,264,481,491},{ 32,264,128,491},{433, 21,493,161},{ 23, 21, 82,161},{104, 10,200,176},{315, 10,411,176},{226, 12,290,250},{ 0, 0, 0, 0},{244,297,271,315},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Fafnir - Skippy
{{220,160,330,432},{ 20,160,128,432},{220,120,228,146},{ 20,120, 28,147},{ 20, 20, 86,108},{220, 20,286,108},{140, 20,206,210},{ 0, 0, 0, 0},{140,240,182,264},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Flea - Skippy
{{420,290,488,494},{ 20,290, 88,494},{420, 50,476,166},{ 20, 70, 70,188},{110, 70,160,208},{340, 40,390,178},{190, 30,286,278},{ 0, 0, 0, 0},{190,350,222,364},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Gladiator - Skippy
{{378,276,468,482},{ 44,276,134,482},{398, 86,442,248},{ 68, 86,118,238},{140, 78,196,202},{316, 74,372,204},{222, 60,290,270},{ 0, 0, 0, 0},{232,306,282,340},{332, 26,368, 56},{ 0, 0, 0, 0}}, // M_Grizzly
@@ -178,9 +178,9 @@ float texuv3[65][11][4]={
{{358,262,432,494},{ 62,262,136,494},{412, 74,488,170},{ 10, 74, 82,182},{ 90, 28,188,150},{296, 28,394,150},{206, 4,278,226},{ 0, 0, 0, 0},{228,320,260,330},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Highlander
{{402,240,474,484},{ 24,240, 98,484},{394, 50,462,156},{ 28, 50,100,156},{186, 18,316,146},{334, 14,386, 98},{218,214,292,288},{ 0, 0, 0, 0},{348,132,366,146},{126, 30,160, 92},{232,162,278,192}}, // M_HollanderII
{{400,262,482,486},{ 16,262, 98,486},{396, 86,450,210},{ 60, 86,116,210},{138, 70,186,218},{322, 46,372,218},{214, 46,294,266},{ 0, 0, 0, 0},{240, 14,268, 34},{230,296,282,354},{ 0, 0, 0, 0}}, // M_Hunchback
{{360,278,456,492},{ 54,278,150,492},{420,160,508,270},{ 4,158, 90,270},{ 94, 22,174,170},{336, 22,416,170},{196,206,320,378},{ 0, 0, 0, 0},{218, 24,294, 98},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Kodiak - GhostHawk
{{309,264,401,467},{109,264,201,467},{416, 70,500,177},{ 11, 70, 94,177},{110, 36,178,203},{330, 36,399,203},{218, 12,292,243},{ 0, 0, 0, 0},{238,280,271,290},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Kodiak - GhostHawk
{{418,266,488,494},{ 22,266, 92,494},{416, 60,474,170},{ 26, 60, 86,170},{136, 38,182,162},{338, 28,386,162},{238, 38,294,220},{ 0, 0, 0, 0},{252,302,278,314},{144,262,200,298},{ 0, 0, 0, 0}}, // M_Loki
{{360,238,436,472},{ 94,236,172,472},{410,120,500,214},{ 16,116,108,210},{120, 14,204,126},{316, 14,402,126},{226, 68,294,192},{ 0, 0, 0, 0},{254,366,272,376},{220,232,302,302},{ 0, 0, 0, 0}}, // M_Longbow - GhostHawk
{{332,246,404,470},{115,246,187,470},{413, 53,499,142},{ 12, 53, 98,142},{118, 47,198,151},{315, 47,395,152},{220, 32,299,212},{ 0, 0, 0, 0},{239,238,280,257},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Longbow - GhostHawk
{{368,252,490,494},{ 16,252,138,494},{416, 96,490,214},{ 6, 96, 82,214},{112, 52,194,182},{314, 52,396,182},{202, 80,304,226},{ 0, 0, 0, 0},{238,276,268,286},{416, 22,498, 90},{ 8, 22, 90, 90}}, // M_Madcat
{{364,276,496,494},{ 10,276,144,494},{416,114,498,176},{ 10,114, 90,176},{122, 32,182,174},{332, 32,394,174},{204, 90,304,232},{ 0, 0, 0, 0},{232,298,274,314},{ 8, 4, 86, 80},{422, 4,498, 80}}, // M_Madcat_MKII
{{330,248,500,494},{ 6,248,176,494},{414, 44,496,162},{ 10, 44, 92,162},{126, 6,186,152},{302, 6,392,152},{226, 4,282,214},{ 0, 0, 0, 0},{230,324,276,342},{ 0, 0, 0, 0},{ 0, 0, 0, 0}}, // M_Masakari
@@ -213,7 +213,7 @@ int offset3[65][11][2]={
{{277,185},{162,185},{334, 52},{ 91, 52},{179, 24},{285, 24},{200, 5},{ 0, 0},{249, 54},{ 0, 0},{ 0, 0}}, // M_Archer
{{256,182},{ 50,182},{274,100},{ 32,100},{ 92, 66},{238, 66},{150, 32},{ 0, 0},{184, 38},{240, 0},{ 62, 0}}, // M_Arctic Wolf
{{280,195},{153,195},{354, 83},{ 58, 84},{163, 84},{276, 83},{209, 91},{ 0, 0},{246,103},{224,170},{201, 25}}, // M_Ares
{{208,158},{ 32,158},{268, 18}, {32, 24},{136, 32},{226, 22},{140, 4},{ 0, 0},{182, 48},{ 0, 0},{ 0, 0}}, // M_Argus - Skippy
{{231,176},{ 62,176},{291, 33},{ 73, 37},{154, 48},{250, 48},{187, 28},{ 0, 0},{201, 63},{ 0, 0},{ 0, 0}}, // M_Argus - Skippy
{{232,170},{114,170},{264, 48},{ 60, 48},{142, 18},{218, 18},{174, 36},{ 0, 0},{192, 78},{178, 2},{ 0, 0}}, // M_Assassin2
{{208,162},{130,162},{266, 18},{ 70, 12},{136, 26},{220, 28},{178, 4},{ 0, 0},{182, 28},{136,118},{256,162}}, // M_Atlas
{{222,174},{ 98,174},{266, 64},{ 62, 64},{118, 44},{220, 44},{152, 10},{ 0, 0},{184, 24},{178,124},{ 0, 0}}, // M_Avatar
@@ -236,7 +236,7 @@ int offset3[65][11][2]={
{{220,182},{ 34,182},{288, 52},{ 60, 52},{120, 34},{204, 34},{158, 58},{ 0, 0},{172, 60},{218, 4},{ 0, 0}}, // M_Daishi
{{222,168},{ 92,168},{288, 68},{ 44, 68},{114, 66},{226, 66},{184, 70},{ 0, 0},{190,100},{222, 4},{116, 4}}, // M_Deimos
{{228,148},{104,148},{280, 76},{ 60, 64},{ 78, 0},{262, 0},{148, 16},{ 0, 0},{190, 24},{ 0, 0},{ 0, 0}}, // M_Dragon
{{214,175},{ 92,175},{332, 13},{ 13, 13},{ 72, 2},{224, 2},{138, 4},{ 0, 0},{190, 99},{ 0, 0},{ 0, 0}}, // M_Fafnir - Skippy
{{223,181},{104,181},{344, 28},{ 21, 28},{ 82, 12},{244, 12},{180, 18},{ 0, 0},{200,111},{ 0, 0},{ 0, 0}}, // M_Fafnir - Skippy
{{234,134},{ 68,134},{276, 74},{126, 74},{120, 22},{224, 22},{174, 8},{ 0, 0},{184, 56},{ 0, 0},{ 0, 0}}, // M_Flea - Skippy
{{220,201},{115,201},{268, 66},{ 87, 66},{128, 38},{228, 38},{154, 3},{ 0, 0},{186, 67},{ 0, 0},{ 0, 0}}, // M_Gladiator - Skippy
{{218,196},{104,196},{288, 46},{ 78, 46},{120, 38},{236, 34},{172, 20},{ 0, 0},{182, 86},{252, 6},{ 0, 0}}, // M_Grizzly
@@ -246,9 +246,9 @@ int offset3[65][11][2]={
{{218,172},{112,172},{282, 74},{ 50, 74},{ 80, 28},{226, 28},{166, 4},{ 0, 0},{188, 30},{ 0, 0},{ 0, 0}}, // M_Highlander
{{232,150},{104,150},{264, 40},{ 78, 40},{136, 8},{214, 4},{168,134},{ 0, 0},{228, 22},{166, 20},{182,102}}, // M_HollanderII
{{220,182},{106,182},{286, 46},{ 70, 46},{118, 30},{242, 6},{164, 6},{ 0, 0},{190, 24},{130, 6},{ 0, 0}}, // M_Hunchback
{{214,196},{100,196},{280, 62},{ 44, 62},{104, 28},{226, 28},{144, 74},{ 0, 0},{168, 2},{ 0, 0},{ 0, 0}}, // M_Kodiak - GhostHawk
{{213,188},{ 99,188},{276, 64},{ 45, 64},{105, 22},{231, 22},{165, 8},{ 0, 0},{183, 23},{ 0, 0},{ 0, 0}}, // M_Kodiak - GhostHawk
{{228,176},{112,176},{276, 60},{ 76, 60},{136, 38},{228, 28},{178, 38},{ 0, 0},{192, 52},{124, 2},{ 0, 0}}, // M_Loki
{{228,154},{112,154},{326, 46},{ 0, 46},{ 90, 38},{240, 38},{174, 24},{ 0, 0},{216, 86},{166,148},{ 0, 0}}, // M_Longbow - GhostHawk
{{221,157},{115,157},{315, 49},{ 4, 50},{ 93, 42},{234, 41},{164, 29},{ 0, 0},{182, 74},{ 0, 0},{ 0, 0}}, // M_Longbow - GhostHawk
{{208,162},{ 76,162},{286, 76},{ 46, 76},{122, 32},{204, 32},{152, 60},{ 0, 0},{188, 66},{256, 2},{ 68, 2}}, // M_Madcat
{{234,186},{ 40,186},{266,114},{ 60,114},{142, 32},{202, 32},{154, 90},{ 0, 0},{182,108},{ 68, 4},{262, 4}}, // M_Madcat_MKII
{{220,158},{ 16,158},{284, 44},{ 40, 44},{116, 6},{202, 6},{176, 4},{ 0, 0},{180, 84},{ 0, 0},{ 0, 0}}, // M_Masakari
+275 -99
View File
@@ -65,37 +65,43 @@ Treat the header comment as stale and preserve the index order above.
## What the values mean
An unexploded rectangle is recorded as:
**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
(x0, y0, x1, y1)
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))
```
`(x0,y0)` is the upper-left pixel and `(x1,y1)` is the lower-right pixel of that component in
the prepared unexploded image. In code this becomes one `texuv2` or `texuv3` entry.
An exploded position is recorded as:
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
(x2, y2)
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
```
It is the upper-left destination position of that component in the exploded layout. In code
this becomes one `offset2` or `offset3` entry. The component keeps the width and height implied
by its unexploded rectangle:
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.
```text
width = x1 - x0
height = y1 - y0
exploded bounds = (x2, y2, x2 + width, y2 + height)
```
Coordinates are normally even because both authoring pipelines were designed around even pixel
boundaries. Do not silently round a visually validated odd value: several supplied sets contain
intentional odd coordinates, including Assassin II Radar S1 `y=3`, Behemoth MFD RA `x=281`, and
Black Hawk Radar RT `x=85`. Radar's integer `/2` truncates odd values at runtime, so changing an
odd authored value can move an edge or origin in the rendered 256x256 view. Work from the
upper-left to lower-right of each component, and do not overlap source rectangles.
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
@@ -160,6 +166,11 @@ coord.cpp (under GameOS/External dependancies)
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
@@ -177,7 +188,8 @@ Start with the finished 1024x1024 full-color 'Mech image.
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.
Enter the rectangle values in `texuv2` and the exploded upper-left values in `offset2`.
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:
@@ -215,7 +227,8 @@ Start with the same finished 1024x1024 full-color image.
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.
Enter the rectangle values in `texuv3` and the exploded upper-left values in `offset3`.
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
@@ -249,10 +262,12 @@ The runtime loads `hsh\radar\hud\<name>.bmp` through
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. Check rectangle bounds, non-overlap, and whether any odd coordinate is intentional and
visually aligned. MFD source geometry is normally inside the 340x340 working area. Radar's
nominal art area is 410x410, but validate imported legacy Radar rectangles against the full
512x512 prepared canvas: accepted Black Hawk geometry extends beyond 410 and aligns visually.
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
@@ -261,18 +276,22 @@ The runtime loads `hsh\radar\hud\<name>.bmp` through
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. Replace only that Mech's row in `texuv2`, `offset2`, `texuv3`, and `offset3` as needed.
7. Preserve `coord.cpp` CRLF line endings and avoid reformatting unrelated rows.
8. Inspect the diff. A one-Mech repair should change at most four rows unless art or roster
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.
9. Rebuild the game because `DXRasterizer.cpp` includes `coord.cpp` directly. Rebuild the needed
12. Rebuild the game because `DXRasterizer.cpp` includes `coord.cpp` directly. Rebuild the needed
Release/Profile targets and redeploy the resulting executable.
10. Compare the supplied exploded BMP to the existing canonical runtime BMP by decoded pixels,
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.
11. Install a changed runtime BMP under the matching `Gameleap/mw4/hsh` path. Loose `hsh` art
does not require a resource-package rebuild.
12. Test both intact and damaged states on the real external MFD and Radar displays.
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
@@ -373,81 +392,119 @@ executable because `DXRasterizer.cpp` includes `coord.cpp` directly.
## Reproducible validation
After editing coordinates, rerun:
> **`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.
```bash
python3 "Finished HUDS from J&J/generate_comparison_maps.py"
```
For the 2026-08-07 data set, the expected terminal result is:
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
Generated 38 maps for 19 display sets: 19 exact, 0 different
Warnings: 0; missing display sets: 7
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:
- `COMPARISON-SUMMARY.md` contains 19 `EXACT` rows and no `DIFFERENT` rows.
- 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.
- The diff changes only intended Mech rows. This installation changes 38 rows total: two rows
for each of 19 display sets.
- `git status --short -- Gameleap/mw4/hsh` is empty when every runtime image was already correct.
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.
## J&J mapping status as of 2026-08-07
## Verified defect catalogue
All 19 complete display sets under `Finished HUDS from J&J/` are installed in `coord.cpp` and
report `EXACT` in `COMPARISON-SUMMARY.md`: 12 MFD sets and 7 Radar sets across 13 chassis.
Seven opposite-display sets have no complete supplied mapping/image inputs and were not changed.
Behemoth II (Mech ID 12) has no separate J&J measurement set because it deliberately shares
Behemoth's geometry and art. Its four rows now inherit Behemoth's validated ID 11 rows, bringing
the effective installed result to 21 MFD/Radar mappings across 14 Mech IDs. The comparison
generator asserts this inheritance on every run and fails if any row diverges.
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.
Installed sets:
### 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 | installed | installed |
| 4 | Argus | installed | no supplied set |
| 5 | Assassin II | installed | installed |
| 7 | Avatar | installed | installed |
| 11 | Behemoth | installed | installed |
| 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 | installed | installed |
| 27 | Fafnir | installed | no supplied set |
| 28 | Flea | installed | no supplied set |
| 29 | Gladiator | installed | no supplied set |
| 33 | Hellspawn | no supplied set | installed |
| 37 | Kodiak | installed | no supplied set |
| 39 | Longbow | installed | no supplied set |
| 62 | Warhammer | installed | installed |
| 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 |
The supplied exploded BMP for every installed display set was also compared to its canonical
runtime destination under `Gameleap/mw4/hsh/hud/` or `Gameleap/mw4/hsh/radar/hud/`. All 19 are
pixel-identical at 512x512. Their file hashes differ because the supplied files use different BMP
encodings, palettes, or headers; the runtime copies are already correct, so none were replaced.
Outstanding, both pre-existing and blocked on missing art:
Pixel-identical runtime files:
- **Annihilator** declares `Special2Internal` with no S2 mapping or art.
- **Behemoth II** declares both specials but inherits Behemoth's rows, which define only S1.
```text
MFD: annihilator, argus, assassin2, avatar, behemoth, blackhawk, Fafnir,
flea, gladiator, kodiak, longbow, warhammer
Radar: annihilator, assassin2, avatar, behemoth, blackhawk, hellspawn, warhammer
```
Behemoth II uses separate canonical files, `hud/behemothii.bmp` and
`radar/hud/behemothii.bmp`. Both were decoded and compared to their corresponding Behemoth
runtime BMP on 2026-08-07; both were already pixel-identical 512x512 grayscale images, so no art
files were copied or replaced.
Three obvious measurement-file transcription errors were normalized without changing the
validated geometry: Behemoth MFD LT `1742` to `174`, Behemoth Radar CT `202.242` to `202,242`,
and Fafnir MFD LL `162.326` to `162,326`. Behemoth Radar's blank S2 was made explicit as `0,0`.
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
@@ -485,33 +542,143 @@ game.
### Implemented Assassin II rows
MFD `texuv2`:
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
{{ 98,142,148,328},{190,142,240,328},{ 56, 48,124,154},{216, 48,282,154},{180, 22,220,120},{120, 22,158,120},{144, 36,194,186},{ 0, 0, 0, 0},{158, 70,180, 86},{148, 10,190, 48},{ 0, 0, 0, 0}}
{{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`:
MFD `offset2` (assembled points):
```cpp
{{ 6,148},{282,148},{ 6, 10},{266, 10},{206, 22},{ 98, 22},{144,180},{ 0, 0},{158,140},{148, 10},{ 0, 0}}
{{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
{{114,170,178,406},{232,170,294,406},{ 60, 48,144,184},{264, 48,348,184},{218, 18,268,140},{142, 18,190,140},{174, 36,234,140},{ 0, 0, 0, 0},{192, 78,218, 98},{178, 2,230, 48},{ 0, 0, 0, 0}}
{{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
{{ 8,220},{336,220},{ 8, 48},{314, 48},{244, 48},{120, 48},{178,206},{ 0, 0},{192, 78},{178, 3},{ 0, 0}}
{{232,170},{114,170},{264, 48},{ 60, 48},{142, 18},{218, 18},{174, 36},{ 0, 0},{192, 78},{178, 2},{ 0, 0}}
```
One supplied Radar S1 offset is odd (`y=3`) despite the general even-coordinate rule. Preserve
the measured value until it is deliberately reviewed against the source pixels; do not silently
round it while transcribing the row.
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
@@ -529,4 +696,13 @@ round it while transcribing the row.
- 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.
- 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`.