diff --git a/docs/MAPS.html b/docs/MAPS.html new file mode 100644 index 0000000..791ca98 --- /dev/null +++ b/docs/MAPS.html @@ -0,0 +1,290 @@ + + + + + +BattleTech 4.11 — every map from above + + + + +
+
+

BattleTech 4.11 · drawn from BTL4.RES

+

Every map,
seen from above

+

Each plan is drawn the way the cockpit draws it: every placement in + the map's own stream, the outline its model carries, in the game's own palette. + Nothing here is traced or redrawn — the buttes are circles + because the game draws them as circles.

+
+
+ +
+
+
+ + + +
+
+
+ +
+
+
+
grass from above
+

grass

+

OFFERED IN THE GAME

+
+
Placements
337
+
Drawn
326
+
Extent
6333 × 7000
+
+

tree ×313, hillg3 ×5, hillg1 ×4, hillg2 ×4

+
+
+
dbase from above
+

dbase

+

OFFERED IN THE GAME

+
+
Placements
213
+
Drawn
183
+
Extent
6864 × 7070
+
+

buttec ×21, hummer ×15, butted ×15, butteb ×14, dish ×14

+
+
+
cavern from above
+

cavern

+

OFFERED IN THE GAME

+
+
Placements
182
+
Drawn
107
+
Extent
6864 × 6359
+
+

buttee ×60, dish ×14, bld26 ×8, calp ×6, calpb ×6

+
+
+
polar3 from above
+

polar3

+

OFFERED IN THE GAME

+
+
Placements
341
+
Drawn
104
+
Extent
3000 × 4750
+
+

sr1w ×19, sr1e ×16, sr1n ×13, sr1s ×13, mslg ×10

+
+
+
rav from above
+

rav

+

OFFERED IN THE GAME

+
+
Placements
118
+
Drawn
93
+
Extent
1566 × 2549
+
+

buttea ×27, wall1 ×18, butted ×13, buttee ×10, wall2 ×8

+
+
+
polar4 from above
+

polar4

+

OFFERED IN THE GAME

+
+
Placements
196
+
Drawn
89
+
Extent
2750 × 4750
+
+

sr1w ×12, sr1n ×12, sr1s ×12, hut ×9, sr1e ×9

+
+
+

Nothing this stream places is drawn

+

arena1

+

OFFERED IN THE GAME

+
+
Placements
4
+
Drawn
0
+
Extent
+
+

+
+
+

Nothing this stream places is drawn

+

arena2

+

OFFERED IN THE GAME

+
+
Placements
3
+
Drawn
0
+
Extent
+
+

+
+
+
arenall from above
+

arenall

+

SUPPORTING STREAM

+
+
Placements
213
+
Drawn
62
+
Extent
1375 × 1334
+
+

aw01 ×14, aw02 ×12, ab02 ×5, ad01 ×5, ab08 ×4

+
+
+
arenall2 from above
+

arenall2

+

SUPPORTING STREAM

+
+
Placements
215
+
Drawn
62
+
Extent
1375 × 1334
+
+

aw01 ×14, aw02 ×12, ab02 ×5, ad01 ×5, ab08 ×4

+
+
+
artrucks from above
+

artrucks

+

SUPPORTING STREAM

+
+
Placements
45
+
Drawn
45
+
Extent
1253 × 1272
+
+

mechmovr ×21, coolant ×13, armrmovr ×6, comtruck ×3, hummer ×2

+
+
+

Nothing this stream places is drawn

+

adrop

+

SUPPORTING STREAM

+
+
Placements
4
+
Drawn
0
+
Extent
+
+

+
+
+

Nothing this stream places is drawn

+

pol4sfx

+

SUPPORTING STREAM

+
+
Placements
10
+
Drawn
0
+
Extent
+
+

+
+ +

A map stream is a run of Entity::MakeMessage records. Each + carries its own length at +0, its class at +28, the resource it names at +40, its + instance flags at +44 and its origin at +48 — and BT pads each record to the + next four bytes, which Red Planet does not. The resource named is a Model List; + the outline the display draws is the GaugeImage inside that list, so the join is direct + rather than by name. A model with no GaugeImage is not drawn, which is why several + streams place far more than they draw.

+

Colours are palette indices resolved through btspal.pcc, the + palette L4GAUGE.CFG configures the secondary port with: + configure(0,sec,270,0x003F,clut0,rgb,btspal.pcc). Unlike Red Planet, + every primitive here carries an explicit colour — nothing falls back to a display + default. Seen from above, +X runs right and +Z up.

+

Two of the eight playable maps, arena1 and + arena2, place almost nothing: a sky and a pair of records too short to + carry an origin. Their arena geometry is arenall, which is a stream of its + own. adrop and pol4sfx are likewise supporting streams + — drop points and wind emitters — rather than places you fight in.

+
+ + + + diff --git a/tools/build_maps.py b/tools/build_maps.py new file mode 100644 index 0000000..2d23b72 --- /dev/null +++ b/tools/build_maps.py @@ -0,0 +1,229 @@ +"""Render every map in BTL4.RES the way the cockpit draws them. + +The technique is the one worked out for Red Planet (RP412 +tools/pages/navmap.py) and it transfers because both games are MUNGA: +a map stream is a run of Entity::MakeMessage records, each carrying its +own length, and each naming a Model List whose GaugeImage is the outline +the display draws. + +Where BT differs from RP: + + * The resource file has a real directory. Header is + (labOnly, maxID) at +4 and an offset table at +12; each descriptor + holds rid/rtype at +0, a 32-byte name at +8, and priority, flags, + offset and length at +40. RP412's file inlines the data after each + descriptor instead, so its walk does not apply here. + * rtype 1 is a Model List, 18 a GaugeImage, 14 a map stream. A model's + outline is the rtype-18 member of its list - a direct join, rather + than RP412's match on the model's name. + * Records are advanced by (length + 3) & ~3. RP's are not padded. + * Every primitive carries an explicit palette index; nothing falls back + to a display default the way RP's colour 0 does. + +Colours are indices into btspal.pcc, the palette L4GAUGE.CFG configures +the secondary port with: configure(0,sec,270,0x003F,clut0,rgb,btspal.pcc). +PCC is PCX, so the palette is the last 769 bytes. + +Usage: python tools/build_maps.py [out.html] +""" +import base64, collections, html, io, os, struct, sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +RES = open(os.path.join(ROOT, 'content', 'BTL4.RES'), 'rb').read() +OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, 'docs', 'MAPS.html') + +MODEL_LIST, SOLIDS, MAP_STREAM, GAUGE_IMAGE = 1, 9, 14, 18 + +# ------------------------------------------------------------- directory +labOnly, maxID = struct.unpack_from(' ([(outline id, position, quaternion)], total records, model tally). + + Each record is an Entity::MakeMessage: its own length at +0, flags at + +8, classToCreate at +28, the resource it names at +40, instanceFlags + at +44 and the origin at +48. Records shorter than 76 bytes cannot + carry an origin and are skipped - the arenas hold a couple of those.""" + m = RESOURCES[rid] + count = struct.unpack_from(' end: + break + length = struct.unpack_from(' end: + break + total += 1 + if length >= 76: + target = struct.unpack_from(' lods[lod][0]: + lod += 1 + if lod >= len(lods): + continue + for colour, attrs, idx in lods[lod][1]: + pts = [] + for k in idx: + if not (0 <= k < len(verts)): + continue + v = verts[k] + if attrs & 1: # attributeUnscaled + v = (v[0] * mpp, v[1] * mpp, v[2] * mpp) + g = rotate(q, v) + pts.append((iw / 2 + ((g[0] + pos[0]) - cx) / mpp, + ih / 2 - ((g[2] + pos[2]) - cz) / mpp)) + if len(pts) > 1: + dr.line(pts, fill=PALETTE[colour], width=1) + buf = io.BytesIO() + img.convert('P', palette=Image.ADAPTIVE, colors=64).save( + buf, 'PNG', optimize=True) + return ('data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode(), + iw, ih, max(xs) - min(xs), max(zs) - min(zs)) + + +# The eight the game offers, from tools/mapscan.py. +PLAYABLE = ['cavern', 'grass', 'rav', 'polar3', 'polar4', 'arena1', 'arena2', + 'dbase'] + +maps = [] +for rid, r in RESOURCES.items(): + if r['t'] != MAP_STREAM: + continue + drawn, total, tally = placements(rid) + art = plan(drawn) if drawn else None + maps.append({'name': r['name'], 'rid': rid, 'total': total, + 'drawn': len(drawn), 'tally': tally, 'art': art, + 'playable': r['name'] in PLAYABLE}) + print(' %-10s %4d records, %4d drawn' % (r['name'], total, len(drawn)), + file=sys.stderr) + +maps.sort(key=lambda m: (not m['playable'], -m['drawn'])) + +cards = [] +for m in maps: + if m['art']: + uri, w, h, ex, ez = m['art'] + art = ('%s from '
+               'above' % (uri, w, h, html.escape(m['name']))) + extent = '%d × %d' % (round(ex), round(ez)) + else: + art = '

Nothing this stream places is drawn

' + extent = '—' + top = ', '.join('%s ×%d' % (html.escape(n), c) + for n, c in m['tally'].most_common(5)) or '—' + cards.append(""" +
+
{art}
+

{name}

+

{label}

+
+
Placements
{total}
+
Drawn
{drawn}
+
Extent
{extent}
+
+

{top}

+
""".format( + tags='playable' if m['playable'] else 'aux', + search=html.escape(m['name'].lower()), art=art, + name=html.escape(m['name']), + label='OFFERED IN THE GAME' if m['playable'] else 'SUPPORTING STREAM', + total=m['total'], drawn=m['drawn'], extent=extent, top=top)) + +PAGE = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'maps.tpl'), encoding='utf-8').read() +os.makedirs(os.path.dirname(OUT), exist_ok=True) +open(OUT, 'w', encoding='utf-8').write(PAGE.format( + cards=''.join(cards), n=len(maps), + playable=sum(1 for m in maps if m['playable']))) +print('wrote %s (%d maps)' % (OUT, len(maps)), file=sys.stderr) diff --git a/tools/maps.tpl b/tools/maps.tpl new file mode 100644 index 0000000..99bba14 --- /dev/null +++ b/tools/maps.tpl @@ -0,0 +1,147 @@ + + + + + +BattleTech 4.11 — every map from above + + + + +
+
+

BattleTech 4.11 · drawn from BTL4.RES

+

Every map,
seen from above

+

Each plan is drawn the way the cockpit draws it: every placement in + the map's own stream, the outline its model carries, in the game's own palette. + Nothing here is traced or redrawn — the buttes are circles + because the game draws them as circles.

+
+
+ +
+
+
+ + + +
+
+
+ +
+
{cards}
+ +

A map stream is a run of Entity::MakeMessage records. Each + carries its own length at +0, its class at +28, the resource it names at +40, its + instance flags at +44 and its origin at +48 — and BT pads each record to the + next four bytes, which Red Planet does not. The resource named is a Model List; + the outline the display draws is the GaugeImage inside that list, so the join is direct + rather than by name. A model with no GaugeImage is not drawn, which is why several + streams place far more than they draw.

+

Colours are palette indices resolved through btspal.pcc, the + palette L4GAUGE.CFG configures the secondary port with: + configure(0,sec,270,0x003F,clut0,rgb,btspal.pcc). Unlike Red Planet, + every primitive here carries an explicit colour — nothing falls back to a display + default. Seen from above, +X runs right and +Z up.

+

Two of the eight playable maps, arena1 and + arena2, place almost nothing: a sky and a pair of records too short to + carry an origin. Their arena geometry is arenall, which is a stream of its + own. adrop and pol4sfx are likewise supporting streams + — drop points and wind emitters — rather than places you fight in.

+
+ + + +