"""Render the track reference page from RPL4.RES. Every track's plan view is drawn from the map's own instance stream: each record carries a position at +48 and a quaternion at +60 on a 76-byte stride, so the scenery placements ARE the map, seen from above. Usage: build_tracks.py """ import base64, html, io, os, re, struct, sys from PIL import Image, ImageDraw sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import navmap ROOT = 'c:/VWE/RP412' RES = open(ROOT + '/assets/RP411/RPL4.RES', 'rb').read() NRES = len(RES) LISTING, OUT = sys.argv[1], sys.argv[2] # ------------------------------------------------------- resource id table def resource_table(): rows = [] for line in open(LISTING, encoding='latin1'): m = re.match(r'\s*(\d+)\s+(\d+)?\s*(.*)$', line.rstrip('\n')) if m and m.group(3).strip(): rows.append((int(m.group(1)), m.group(3).strip())) walk = [] o = RES.find(b'StaticAudioStream\x00') - 8 while o + 0x38 <= NRES: name = RES[o + 8:o + 0x28].split(b'\x00')[0].decode('latin1', 'replace') addr, size = struct.unpack_from(' NRES: break walk.append((name, addr, size)) o = addr + size table, i = {}, 0 for rid, desc in rows: if desc == 'Not Used': continue if i < len(walk): name, addr, size = walk[i] table[rid] = {'name': name, 'addr': addr, 'size': size, 'desc': desc} i += 1 return table TABLE = resource_table() # ------------------------------------------------------------ the console CONSOLE = {} xml = open(ROOT + '/tools/console-config/RPConfig.xml', encoding='utf-8-sig').read() for m in re.finditer(r' 1: dr.line(xy, fill=255, width=2, joint='curve') out = Image.merge('RGBA', (Image.new('L', img.size, 255),) * 3 + (img,)) buf = io.BytesIO() out.save(buf, 'PNG', optimize=True) return ('data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode(), iw, ih) # ---------------------------------------------------------------- gather tracks = {} for res in TABLE.values(): m = re.match(r'^(\w+): Map stream of (\d+) instances$', res['desc']) if m: tracks[m.group(1)] = {'key': m.group(1), 'instances': int(m.group(2)), 'addr': res['addr'], 'size': res['size']} for res in TABLE.values(): m = re.match(r'^(\w+): Stream of (\d+) (Cameras|Existance boxes)$', res['desc']) if m and m.group(1) in tracks: field = 'cameras' if m.group(3) == 'Cameras' else 'boxes' tracks[m.group(1)][field] = int(m.group(2)) for key, t in tracks.items(): segs, drawn, skipped = navmap.track_lines( RES, TABLE, t['addr'], t['size'], t['instances']) t['drawn'] = drawn t['skipped'] = skipped t['name'] = CONSOLE.get(key, key) t['race'] = key in RACE t['football'] = key in FOOTBALL t['era'], t['eraLabel'] = era(key) pts = [p for seg in segs for p in seg] if pts: xs = [p[0] for p in pts]; ys = [p[1] for p in pts]; zs = [p[2] for p in pts] t['extent'] = (max(xs) - min(xs), max(ys) - min(ys), max(zs) - min(zs)) t['art'], t['aw'], t['ah'] = plan_view(segs) else: t['extent'] = (0, 0, 0) t['art'] = None # Tracks with identical footprints are almost certainly one built from the # other; say so rather than leaving the reader to notice. twins = {} for key, t in tracks.items(): sig = (t['instances'], round(t['extent'][0]), round(t['extent'][2])) twins.setdefault(sig, []).append(key) ERA_RANK = {'arcade': 0, 'r411': 1, 'later': 2} ORDER = sorted(tracks, key=lambda k: (ERA_RANK[tracks[k]['era']], -tracks[k]['extent'][2])) print('tracks: %d' % len(tracks), file=sys.stderr) # ------------------------------------------------------------------ page def chips(t): out = [] out.append((t['era'], t['eraLabel'])) if t['race']: out.append(('race', 'DEATH RACE')) if t['football']: out.append(('ball', 'FOOTBALL')) if not t['race'] and not t['football']: out.append(('none', 'NOT IN THE MENU')) return ''.join('%s' % (k, v) for k, v in out) cards = [] for key in ORDER: t = tracks[key] sig = (t['instances'], round(t['extent'][0]), round(t['extent'][2])) kin = [k for k in twins.get(sig, []) if k != key] twin = ('

Same footprint and instance count as ' + ', '.join(tracks[k]['name'] for k in kin) + '

') if kin else '' short = ('

%d of %d placements are drawn; the rest carry no ' 'map art or stand alone off the course

' % (t['drawn'], t['instances'])) if t['skipped'] else '' art = ('' % (html.escape(t['name']), t['art'], t['aw'], t['ah'])) if t['art'] else '' cards.append("""
{art}

{name}

{key}

{chips}

Instances
{inst}
Cameras
{cams}
Start boxes
{boxes}
Extent
{ex} × {ez}
Relief
{ey}
{twin}{short}
""".format( tags=t['era'] + (' race' if t['race'] else '') + (' football' if t['football'] else ''), search=html.escape((key + ' ' + t['name']).lower()), art=art, name=html.escape(t['name']), key=html.escape(key), chips=chips(t), inst=t['instances'], cams=t.get('cameras', '—'), boxes=t.get('boxes', '—'), ex='%,d'.replace(',', '') % round(t['extent'][0]), ez='%d' % round(t['extent'][2]), ey='%d' % round(t['extent'][1]), twin=twin, short=short)) arcade_n = sum(1 for t in tracks.values() if t['era'] == 'arcade') r411_n = sum(1 for t in tracks.values() if t['era'] == 'r411') CSS = open(os.path.join(os.path.dirname(__file__), 'tracks.css')).read() JS = open(os.path.join(os.path.dirname(__file__), 'tracks.js')).read() PAGE = open(os.path.join(os.path.dirname(__file__), 'tracks.tpl'), encoding='utf-8').read() open(OUT, 'w', encoding='utf-8').write(PAGE.format( css=CSS, js=JS, cards=''.join(cards), n=len(tracks), arcade=arcade_n, r411=r411_n, later=len(tracks) - arcade_n - r411_n)) print('wrote %s (%d tracks)' % (OUT, len(tracks)), file=sys.stderr)