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