Files
firestorm/MFD-RADAR-MAPPINGS.md
T
777f338c31 Document the paper-doll loader, its format law, and art-replacement checks
The guide covered authoring and coordinates but not the code that consumes the
result, which is where the constraints actually come from. Adds what an agent
or a human needs to work on this system without rediscovering it.

New: "How the runtime loads and draws the art". The call chain from
huddamage.cpp texturename[] through CMFD_Device::LoadDamageTexture /
CRadar_Device::LoadRadarDamageTexture / CMR_Device::LoadMRTargetTexture into
CreateATextureFromBitmap, with the path each device builds.

The important part is the format law. CreateATextureFromBitmap uses the raw
palette INDEX as both brightness and alpha and never looks the palette up:

    wA = data[x];
    WORD wBit = (WORD)((wA<<8)&0xF000|0x0FFF);
    *bits++ = (WORD)((wA>0)?wBit:0);

So a doll must be 8-bit with an identity greyscale palette, alpha is the top
nibble (index < 16 invisible, 0 dropped), and >8bpp is unsupported. A file can
look perfect in a viewer and still be wrong: the Battlemaster radar art arrived
with a 131-entry optimised palette running opposite to brightness, which would
have rendered near-black and mostly transparent. The rule explicitly does NOT
extend to hsh/MFD tiles or hsh/Mechs portraits, which load through GDI.

Audited every doll the code actually references rather than the directory:
65/65 correct on both displays, the only absence being the commented-out Dasher
entry at huddamage.cpp:81, which pairs with its commented-out coord.cpp rows.
Scoping matters -- hsh/hud holds ~197 files but only ~65 are dolls, so a
folder-wide audit produces 130 false positives.

New: "Replacing art for an existing chassis" -- per-zone bounds, reassembly,
and spill. Re-shading inside an unchanged box is safe; a moved piece silently
scrambles the doll and is invisible until something takes damage in game.

Also records hsh/mr_texture.bmp as the game's only chassis-independent doll,
and that hudchat.cpp:853's claim that it holds no image is wrong.

Co-authored-by: Claude Opus 5 (Anthropic) <noreply@anthropic.com>
Co-authored-by: GitHub Copilot <copilot@github.com>
2026-08-09 20:45:13 -05:00

46 KiB

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.

A fourth consumer shares the MFD art without having its own table: the cameraship target display (CMR_Device::LoadMRTargetTexture) reads the same hsh/hud/<name>.bmp files. Changing MFD art therefore changes what the cameraship shows. Its own generic doll is a separate asset -- see "The generic fallback doll" below.

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, and they are not bound by the greyscale format law that governs the dolls.

Start with "How the runtime loads and draws the art" if you are new to this system: the file format is dictated by the loader, and the constraint is invisible in the artwork itself.

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 #includes 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:

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:

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.

How the runtime loads and draws the art

Both displays follow the same path. huddamage.cpp owns the name table and calls the loaders once per mech, passing a stem that already carries the hud\ prefix:

huddamage.cpp:55    const char *texturename[LastMechID+1] = { "hud\\annihilator", ... "hud\\zeus" }
huddamage.cpp:414   mfd_device.LoadDamageTexture      (texturename[m_MechID])
huddamage.cpp:415   radar_device.LoadRadarDamageTexture(texturename[m_MechID])
huddamage.cpp:2054  radar_device.LoadRadarDamageTexture(texturename[m_TargetMechID])

Each device prepends its own root:

Device Function Path built Resolves to
External MFD CMFD_Device::LoadDamageTexture, render.cpp:2073 hsh\<stem>.bmp hsh\hud\<name>.bmp
Radar CRadar_Device::LoadRadarDamageTexture, render.cpp:1648 hsh\radar\<stem>.bmp hsh\radar\hud\<name>.bmp
Cameraship target CMR_Device::LoadMRTargetTexture, render.cpp:1403 hsh\<stem>.bmp hsh\hud\<name>.bmp

All three call CreateATextureFromFile -> CreateATextureFromBitmap (render.cpp:412).

Note the entry commented out at huddamage.cpp:81 (hud\\dasher), which pairs with the commented-out Dasher rows in coord.cpp. Keep those two in step: if Dasher is ever activated, both must be uncommented together, and the art must exist for both displays.

The format law: 8-bit identity greyscale, and why it is not negotiable

This is the single hardest constraint in the whole system, and nothing warns you when it is broken. CreateATextureFromBitmap is the entire reason:

wA = data[x];                                  // the raw palette INDEX, not a colour
WORD wBit = (WORD)((wA<<8)&0xF000|0x0FFF);     // top nibble of that index -> alpha
*bits++   = (WORD)((wA>0)?wBit:0);             // index 0 -> discarded entirely

It never looks the palette up. The byte stored in the BMP is the pixel value, used as both brightness and alpha. Consequences that govern every doll:

  • The file must carry an identity greyscale palette: entry N must be RGB(N,N,N). Pillow mode L produces exactly that. Mode P with an optimised palette does not, even if every entry in it happens to be grey.
  • Alpha is the top nibble, so anything below index 16 is effectively invisible and index 0 is dropped outright. Faint anti-aliased edges will not render.
  • More than 8 bits per pixel is not supported on this path at all.

A file can look perfect in an image viewer and still be wrong here. A real example: a supplied Battlemaster radar doll carried a 131-entry optimised greyscale palette whose order ran roughly opposite to brightness. Index 1 was pure white in the palette, but the engine reads index 1 as level 1 -- near black, alpha nibble 0, invisible. Peak brightness would have rendered at 129/255 with alpha capped at 8 of 15. Broken, not subtly off.

Check before installing anything:

from PIL import Image
im = Image.open(path)
assert im.mode == "L"          # or every palette entry N must equal (N, N, N)
assert im.size == (512, 512)

Convert with Image.open(src).convert("L").save(dst), which resolves each index through the palette, then confirm the converted pixels equal the supplied artwork.

Do not generalise this rule. hsh/MFD/ atlas tiles, hsh/Mechs/ score-sheet portraits (recscore.cpp:2959) and the map images load through Win32 LoadImage + BitBlt or DrawBitmapToSurface, all of which honour the palette. Those directories legitimately hold a mix of L, P and RGB, and normalising them would be pointless churn.

Auditing the whole set

Scope the audit to the stems in texturename[], not to the directory. hsh/hud/ holds around 197 files but only the ~65 doll images are subject to the format law; the rest are other HUD art at other sizes on other code paths, and auditing the folder produces a flood of false alarms.

For each stem, check hsh/<stem>.bmp and hsh/radar/<stem>.bmp for: exists (case-insensitively), 512x512, 8bpp, mode L or an identity palette. Whole-tree state as of 2026-08-09: 65/65 correct on both displays, the only absence being the commented-out Dasher entry.

The generic fallback doll

hsh/mr_texture.bmp (shaded, 126 grey levels) and hsh/mr_texturea.bmp (mask, 29 levels) hold a generic articulated mech -- head, centre and side torsos, both arms with gun ports, both legs -- in their top-left corner. CMR_Device loads them at render.cpp:1276-1277 for the cameraship Map/Armour screen. The mech occupies columns 0-55; a shield glyph sits at 66-78.

It is the only chassis-independent paper doll in the game, so it is the right starting point for any generic or placeholder doll. Note that hudchat.cpp:853 claims "there is no actual image on mr_texture.bmp & mr_texturea.bmp" -- that comment is wrong, and is probably why the asset stayed unnoticed.

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
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:

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)

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)

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:

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:

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:

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:

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.

Replacing art for an existing chassis

New artwork for a chassis that already has rows in coord.cpp must fit the rows that are already there. If a piece moved, the reassembled doll comes apart -- and nothing warns you, because the engine will happily crop from the wrong place and draw the result. The failure is invisible until someone takes damage in game.

Run three checks against the current runtime file before installing a replacement. All three are cheap and none needs the game.

  1. Per-zone bounds. For each zone, crop the texuv rectangle from both the old and the new sheet and take the bounding box of lit pixels inside that crop. The boxes must agree within a pixel or two.

    • identical boxes, lower IoU -> the piece was re-shaded in place. Fine.
    • shifted box -> the piece moved. The row must be re-measured or the art re-exported.
  2. Reassembly. Do exactly what the engine does: crop each texuv rect and paste it at its offset. Compare the assembled silhouettes. Coordinate-compatible art scores above about 0.95 IoU with an assembled bounding box within a couple of pixels. This is the check that actually answers "do the coordinates still map", because it exercises both tables together.

  3. Spill. Count lit pixels falling outside every texuv rectangle; those get clipped. Then look at their grey levels before reacting:

    • below roughly 60 -> anti-aliasing halo, invisible once the alpha nibble is applied.
    • bright pixels outside a box -> real artwork is being cut off, and the rectangle needs widening rather than the art being accepted as-is.

A worked result, replacing the Battlemaster radar doll: every zone's bounding box matched, the three lowest per-zone IoUs (0.867-0.915) were re-shading inside unchanged boxes, assembled IoU was 0.9676 with bbox (91,4,370,397) vs (92,6,370,397), and all 81 spilled pixels were faint (max level 59, none above 128). Verdict: coordinate-compatible, no coord.cpp change needed. The visible difference was an intentional new gun barrel on the left arm, which sits inside its box.

Do this before installing, not after. Also confirm the replacement satisfies the format law above; a supplied file is far more likely to have a palette problem than a geometry problem.

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:

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:

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:

Finished HUDS from J&J/AssassinII/

The supplied files are:

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:

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):

{{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):

{{190,142},{ 98,142},{216, 48},{ 56, 48},{120, 22},{180, 22},{144, 36},{  0,  0},{158, 70},{148, 10},{  0,  0}}

Radar texuv3:

{{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:

{{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:

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:

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>/:

[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.

Regenerating a crude MFD sheet from the Radar art

Some original chassis have a much cruder MFD sheet than their Radar sheet, with loose boxes and pieces that do not meet. Because the two pipelines are 1024->320 and 1024->400, the MFD is exactly 0.8x the Radar, so a whole MFD sheet and its rows can be regenerated from the Radar:

  1. Take the Radar exploded BMP, threshold it, and fill each component's interior holes to get a solid silhouette.
  2. Scale by 0.8, resample, re-threshold, and place at the upper-left of a 512x512 canvas.
  3. Scale texuv3 and offset3 by the same 0.8 to get texuv2 and offset2.
  4. Spread the pieces outward from the CT centre so nothing collides, keeping CT and HD anchored, then mirror each L/R pair exactly about the CT centre.
  5. Review and nudge by eye.

Things this exposed that are worth reusing:

  • Test collisions on pixels, not bounding boxes. These are concave silhouettes; a piece can nest into a notch in its neighbour while the boxes overlap heavily. On the Atlas, a position the box test rejected outright had zero touching pixels.
  • A mismatched pair can be fixed by mirroring the better one. The Atlas's right leg had a hole its left leg did not; replacing the right leg's art with a horizontally flipped copy of the left gives identical pieces (verified by equal pixel counts).
  • Cut the head out of the CT. Blacking out the CT's art under HD plus a 1px ring makes the head read as a separate part instead of a patch on the torso. Verify with "CT pixels within 1px of HD == 0".
  • S1/S2 are often asymmetric by design. The Atlas's are 22x20 and 21x50 in different places, faithfully inherited from the Radar. Do not force them into a mirrored pair.

Applied to the Atlas (Mech ID 6) on 2026-08-09: fit 0.795 -> 0.956, no cross-piece clipping, assembled footprint 236x321 against the previous 210x321. This is generated art, not authored art; confirm it on a physical MFD before repeating the technique on other chassis.

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.
  • Writing a runtime BMP to a lower-cased path. Several destinations are capitalised on this case-sensitive checkout (Atlas.bmp, Fafnir.bmp); resolve the existing filename first.
  • Saving a doll as indexed or optimised colour instead of 8-bit greyscale. The loader uses the palette index as the pixel value, so anything but an identity greyscale palette renders wrong, usually inverted and mostly transparent. Looking right in an image viewer proves nothing.
  • Applying that greyscale rule to hsh/MFD tiles or hsh/Mechs portraits. Those load through GDI, which honours the palette, and are legitimately a mix of L, P and RGB.
  • Auditing the hsh/hud directory rather than the stems named in texturename[]. Most files in that folder are other HUD art at other sizes, and folder-wide checks drown in false positives.
  • Installing replacement art for an existing chassis without reassembling it first. Re-shading inside a box is safe; a moved piece silently scrambles the doll.
  • Relying on faint edge detail. Alpha is the top nibble of the pixel value, so anything below index 16 is invisible and index 0 is dropped.
  • Activating a mech in coord.cpp without also uncommenting its texturename[] entry, or supplying art for only one of the two displays.
  • Testing piece collisions with bounding boxes. These silhouettes are concave; use pixel masks.