#!/usr/bin/env python3 """ make_generic_doll.py - build a generic MFD/Radar damage paper doll. The six chassis imported from V4H have no damage art. V4H shipped Mad Cat copies under six names, which renders an aligned doll that lies about the mech. Instead this builds one honest generic doll from art the game already owns. Source: `hsh/mfd_texture.bmp`, the MFD sprite sheet, which contains a generic mech already separated into components, together with a validated 11-zone mapping in `huddamage.cpp` (`sm2_texture` / `sm2_offset`, lines 472 and 487) -- the same zone order `coord.cpp` uses. `RenderAux4SmallMech` draws it, so the geometry below is proven in game rather than measured by us. Output is the exploded runtime BMP: every zone appears exactly once, and no two source rectangles touch, so each `texuv` isolates one component. The game reassembles the figure from the `offset` values. python3 make_generic_doll.py [-o OUTDIR] """ import argparse, os, sys from PIL import Image, ImageDraw HSH = "/home/rich/Repositories/firestorm/Gameleap/mw4/hsh" # huddamage.cpp:472 -- source rects into mfd_texture.bmp, in coord.cpp zone order SM2_TEXTURE = [ (172, 0, 231, 140, "LL", "left leg"), (109, 0, 168, 140, "RL", "right leg"), (30, 144, 67, 246, "LA", "left arm"), (0, 36, 36, 128, "RA", "right arm"), (37, 24, 66, 101, "RT", "right torso"), (0, 132, 28, 209, "LT", "left torso"), (67, 33, 99, 125, "CT", "center torso"), (None, None, None, None, "CTR", "center torso rear"), (67, 0, 99, 31, "HD", "head"), (None, None, None, None, "S1", "special 1"), (None, None, None, None, "S2", "special 2"), ] # huddamage.cpp:487 -- assembled positions (the *3 is already applied here) SM2_OFFSET = [ (29 * 3, 35 * 3), (6 * 3, 35 * 3), (43 * 3, 11 * 3), (0 * 3, 11 * 3), (12 * 3, 7 * 3), (33 * 3, 7 * 3), (22 * 3, 10 * 3), None, (22 * 3, 0 * 3), None, None, ] # The native figure is 166x245. Real dolls assemble to roughly 330x335 (Atlas, # Assassin II), so 1.5 lands in the right range and keeps the arithmetic exact: # the generic centre torso becomes 48x138 against Atlas's 48x174. # # Radar is authored on a 410 working canvas against the MFD's 340, and real rows # follow that -- Assassin II's radar rects run about 1.27x its MFD ones -- so the # Radar doll is scaled up to match. The runtime halves Radar values at draw time. MFD_SCALE = 1.5 RADAR_SCALE = 1.8 CANVAS = 512 def even(v): """Coordinates are authored even; runtime Radar halves them with integer /2.""" return int(round(v / 2.0)) * 2 def components(scale): sheet = Image.open(os.path.join(HSH, "mfd_texture.bmp")).convert("L") out = [] for (rect, off) in zip(SM2_TEXTURE, SM2_OFFSET): x0, y0, x1, y1, zone, label = rect if x0 is None: out.append((zone, label, None, None, None)) continue piece = sheet.crop((x0, y0, x1, y1)) w = even((x1 - x0) * scale) h = even((y1 - y0) * scale) piece = piece.resize((w, h), Image.LANCZOS) out.append((zone, label, piece, (even(off[0] * scale), even(off[1] * scale)), (w, h))) return out def layout(comps, outline=False): """Place every piece in the 512 canvas without touching. -> (image, {zone: rect})""" img = Image.new("L", (CANVAS, CANVAS), 0) draw = ImageDraw.Draw(img) rects = {} x, y, row_h, gap = 4, 4, 0, 8 for zone, _label, piece, _off, size in comps: if piece is None: rects[zone] = (0, 0, 0, 0) continue w, h = size if x + w + gap > CANVAS: x = 4 y += row_h + gap row_h = 0 img.paste(piece, (x, y)) if outline: draw.rectangle([x, y, x + w - 1, y + h - 1], outline=255, width=2) rects[zone] = (x, y, x + w, y + h) x += w + gap row_h = max(row_h, h) return img, rects def row(values, width=3): return "{" + ",".join("{" + ",".join(f"{v:{width}d}" for v in t) + "}" for t in values) + "}" def main(): ap = argparse.ArgumentParser() ap.add_argument("-o", "--outdir", default="/home/rich/Repositories/FS_Build_V4H_extracted") args = ap.parse_args() comps = components(MFD_SCALE) order = [c[0] for c in comps] written = [] tables = {} for kind, sub, outlined, scale in (("MFD", "hsh/hud", False, MFD_SCALE), ("Radar", "hsh/radar/hud", True, RADAR_SCALE)): comps = components(scale) img, rects = layout(comps, outline=outlined) d = os.path.join(args.outdir, sub) os.makedirs(d, exist_ok=True) p = os.path.join(d, "generic.bmp") img.save(p) written.append(p) texuv = [rects[z] for z in order] offset = [(c[3] if c[3] else (0, 0)) for c in comps] offset = [o if rects[z] != (0, 0, 0, 0) else (0, 0) for z, o in zip(order, offset)] tables[kind] = (texuv, offset) doc = os.path.join(args.outdir, "hsh", "GENERIC-DOLL-COORDS.txt") with open(doc, "w", newline="\r\n") as fh: fh.write(TEXT.format( scale=MFD_SCALE, rscale=RADAR_SCALE, zones=" ".join(f"{i}:{z}" for i, z in enumerate(order)), mfd_texuv=row(tables["MFD"][0]), mfd_offset=row(tables["MFD"][1]), rad_texuv=row(tables["Radar"][0]), rad_offset=row(tables["Radar"][1]), )) written.append(doc) for p in written: print("wrote", p) TEXT = """GENERIC MECH DAMAGE PAPER DOLL - coordinates for coord.cpp ========================================================= WHAT THIS IS A generic 'Mech damage doll for chassis that have no bespoke MFD/Radar art. Six imported V4H chassis (champion, dasher, griffin, jenner2c, marauder, thunderbolt) ship with none. V4H filled the gap with pixel-identical copies of the Mad Cat doll under six names, which renders an aligned display that misrepresents the mech. This is the honest placeholder instead. WHERE THE ART CAME FROM Not drawn from scratch. The game already owns a generic 'Mech, separated into components, inside hsh/mfd_texture.bmp (the MFD sprite sheet), with a validated eleven-zone mapping in huddamage.cpp: sm2_texture[][4] line 472 source rectangles sm2_offset[][2] line 487 assembled positions drawn by RenderAux4SmallMech(). Those tables use the same zone order as coord.cpp, so the geometry below is proven in game, not measured by us. Components were scaled {scale}x for the MFD and {rscale}x for the Radar. The native figure is 166x245 and real dolls assemble to roughly 330x335, so this lands in the right range; the generic centre torso ends up 48x138 against the Atlas's 48x174. The Radar is larger because it is authored on a 410 working canvas against the MFD's 340, and the runtime halves Radar values at draw. FILES hsh/hud/generic.bmp 512x512 external MFD doll hsh/radar/hud/generic.bmp 512x512 Radar doll, components outlined 2px white per the Radar pipeline Both are the EXPLODED runtime view: each zone appears exactly once and no two source rectangles touch, so a texuv isolates one component. The game reassembles the figure from the offsets. ZONE ORDER (index: zone) {zones} CTR, S1 and S2 are all-zero, and that is normal rather than a shortcut. Across the 65 shipped rows CTR is zero in ALL 65, S1 in 31 and S2 in 49. Neither draw loop guards zero rectangles - a degenerate quad simply renders nothing - so absent zones are safe. The generic figure has no rear silhouette and no special hardpoints. ONE FILE SERVES ALL SIX The BMP name comes only from `texturename[]` in huddamage.cpp; both displays use it (`LoadDamageTexture` and `LoadRadarDamageTexture` are both called with `texturename[m_MechID]`). Nothing in the mech data files names it. So six entries pointing at "hud\\\\generic" share one pair of images - do not make per-chassis copies. (`m_HudMap` is unrelated: that is the mission map.) -------------------------------------------------------------------------- HOW TO INSTALL -------------------------------------------------------------------------- 1. Copy the art hsh/hud/generic.bmp -> Gameleap/mw4/hsh/hud/generic.bmp hsh/radar/hud/generic.bmp -> Gameleap/mw4/hsh/radar/hud/generic.bmp Loose hsh art is not packed into a .mw4, so no resource rebuild is needed. 2. huddamage.cpp - add one texturename[] entry per new chassis, all pointing at the same generic art. The array is sized [LastMechID+1], so LastMechID in MechLabHeaders.h must also rise from 64 to 70. "hud\\\\generic", // M_Champion 65 "hud\\\\generic", // M_Jenner2c 66 "hud\\\\generic", // M_Dasher 67 "hud\\\\generic", // M_Marauder 68 "hud\\\\generic", // M_Thunderbolt 69 "hud\\\\generic", // M_Griffin 70 Keep V4H's append order (65-70). Inserting alphabetically would renumber every existing chassis, and Mech IDs are positional across code, tables and shell scripts. 3. coord.cpp - widen all four arrays from [65] to [71] and append the SAME row six times to each, once per new chassis. All six share one doll, so all six rows are identical. texuv2 / offset2 are the external MFD. texuv3 / offset3 are the Radar, which the runtime divides by two at draw time - store the full-size values here, do not pre-divide. texuv2 (MFD source rectangles) {mfd_texuv} offset2 (MFD exploded positions) {mfd_offset} texuv3 (Radar source rectangles) {rad_texuv} offset3 (Radar exploded positions) {rad_offset} 4. Rebuild. DXRasterizer.cpp includes coord.cpp directly, so a coordinate change needs a Release/Profile rebuild and redeploy of MW4.exe. The art alone does not. -------------------------------------------------------------------------- NOTES AND LIMITS -------------------------------------------------------------------------- * Dasher's four commented-out rows were REMOVED from coord.cpp on 2026-08-08. They were unique authored Dasher geometry rather than a copy, but the art they mapped has never existed, and leaving them invited someone to enable them against wrong art. Recoverable from git (last touched in deafc2b0) if real Dasher art is ever produced. The matching commented `"hud\\\\dasher"` entry at huddamage.cpp:81 is still there and should go with them. * The generic cannot represent chassis-specific zones. Marauder's cage and Dasher's S1/S2 will not appear. * This is a placeholder. The point of a generic silhouette over a borrowed one is that it reads as "no art yet" rather than as the wrong mech. Replace it per chassis when real 1024x1024 renders exist; the authoring pipelines are in MFD-RADAR-MAPPINGS.md. * PRE-EXISTING BUGS, unrelated to this work but found while checking it. Three chassis share another mech's rows while having their own art. Overlaying each row on its own art shows: Rifleman (id 49, uses Mad Cat's rows) - BROKEN. CT lands in empty space beside the mech, S1/S2 float in empty corners, and the leg boxes run well past the feet. Battlemaster (id 9, uses Atlas's rows) - BROKEN. CT sits over a left-side arm piece and the leg boxes are narrow centre strips while the real legs fall outside them. Templar (id 54, uses Archer's rows) - fine. Its art was evidently authored to that layout; only the leg boxes clip slightly. Rifleman and Battlemaster need their own measured rows, or art authored to the borrowed layout. """ if __name__ == "__main__": main()