diff --git a/MFD-RADAR-MAPPINGS.md b/MFD-RADAR-MAPPINGS.md index 8804d67b..0170d37a 100644 --- a/MFD-RADAR-MAPPINGS.md +++ b/MFD-RADAR-MAPPINGS.md @@ -18,8 +18,17 @@ This guide is about the last two displays, both controlled by `coord.cpp`. The n 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/.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`. +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 @@ -63,6 +72,98 @@ Use `{0,0,0,0}` and `{0,0}` for a zone that has no art. The comment above `offse 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: + +```text +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\.bmp` | `hsh\hud\.bmp` | +| Radar | `CRadar_Device::LoadRadarDamageTexture`, render.cpp:1648 | `hsh\radar\.bmp` | `hsh\radar\hud\.bmp` | +| Cameraship target | `CMR_Device::LoadMRTargetTexture`, render.cpp:1403 | `hsh\.bmp` | `hsh\hud\.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: + +```cpp +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: + +```python +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/.bmp` and `hsh/radar/.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, @@ -390,6 +491,42 @@ Loose `hsh` art is not packed into a `.mw4` resource for this path, so an actual 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 @@ -740,4 +877,17 @@ art; confirm it on a physical MFD before repeating the technique on other chassi - 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. \ No newline at end of file