Pan with the arrows, zoom with plus and minus, [ and ] for the next track. Self-contained HTML with the track data and palette embedded; nothing here ships, and pack-dist.ps1 does not look at it. It follows the engine rather than approximating it. NavDisplay derives metersPerPixel from the zoom and sets LODIndex to it; L4GaugeImage::Draw takes the first LOD whose scale is at least that value and draws nothing once the value runs past the largest, so objects vanish rather than simplify. Both map gauges are here because they disagree - nav is the 448x416 radar screen with LOD following zoom, gps the 125x203 panel whose config pins LOD at 1.0. The HUD reports what is dropped, and is honest that this content barely exercises it: every placement in every track is cn3 with one LOD at scale 1000. The map is not a phosphor screen. Primitives carry palette indices and the palette is whichever the port was configured with - for the pod's secondary port, configure(0,sec,270,0x00ff,clut0,rgb,secpal.pcc). PCC is PCX, so the palette is the last 769 bytes. Walls are grey because index 51 is #4b4b4b; background is index 0, black; a primitive with colour 0 keeps the display's staticColor, 0x3C. Per-file palettes, not a global one - 39 of 40 gauge PCCs differ - so the port's configured palette is the one that counts.
149 lines
5.9 KiB
Python
149 lines
5.9 KiB
Python
"""Build the map viewer: every track, drawn the way the pod's map draws it.
|
|
|
|
This is a dev tool, not something that ships. It exists to answer "what
|
|
does the map screen actually show here?" without booting the game, which
|
|
matters because the answer is not obvious: the engine drops whole objects
|
|
at low zoom rather than simplifying them, and you cannot see that happen
|
|
from a static picture.
|
|
|
|
What it reproduces, from the engine:
|
|
|
|
NavDisplay::CalculateBounds (RP_L4/RPL4GAUG.cpp:1587)
|
|
pixelsPerMeter = (halfWidth<<1) / currentScale
|
|
metersPerPixel = 1 / pixelsPerMeter
|
|
LODIndex = metersPerPixel // N = 1.0 pixels/meter
|
|
|
|
L4GaugeImage::Draw (MUNGA_L4/L4GAUIMA.cpp:908)
|
|
for (i = 0; i < LODCount; ++i)
|
|
if (LOD_value <= LODScales[i]) break;
|
|
if (i < LODCount) draw LODList[i]; // else NOT DRAWN AT ALL
|
|
|
|
That last line is the whole point. LODScales ascend, so index 0 is the
|
|
finest, and when the LOD value runs past the largest scale the object is
|
|
skipped entirely. Zooming out in this viewer makes objects vanish exactly
|
|
where the pod makes them vanish.
|
|
|
|
The GPS gauge is the other case: L4GAUGE.CFG has gps(R,ModeAlwaysActive,
|
|
(125,203),1.0), so its LOD value is pinned at 1.0 by the config and only
|
|
its scale follows the track. Both modes are here - G switches.
|
|
|
|
Usage: build_mapview.py <rpl4tool-listing.txt> <out.html>
|
|
"""
|
|
import json, os, re, struct, sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, os.path.join(HERE, '..', 'pages'))
|
|
import navmap # noqa: E402 (path first)
|
|
|
|
ROOT = 'c:/VWE/RP412'
|
|
RES = open(ROOT + '/assets/RP411/RPL4.RES', 'rb').read()
|
|
LISTING, OUT = sys.argv[1], sys.argv[2]
|
|
TABLE = navmap.resource_table(RES, LISTING)
|
|
|
|
|
|
def gauge_image(addr):
|
|
"""The whole stream, every LOD - not just the finest, which is what
|
|
the page builder wants and exactly what a viewer must not assume."""
|
|
o = addr
|
|
|
|
def i32():
|
|
nonlocal o
|
|
v = struct.unpack_from('<i', RES, o)[0]; o += 4; return v
|
|
|
|
def f32():
|
|
nonlocal o
|
|
v = struct.unpack_from('<f', RES, o)[0]; o += 4; return v
|
|
|
|
vcount = i32()
|
|
if not (0 < vcount < 100000):
|
|
return None
|
|
verts = [[round(f32(), 2) for _ in range(3)] for _ in range(vcount)]
|
|
lodcount = i32()
|
|
if not (0 < lodcount < 64):
|
|
return None
|
|
scales = [f32() for _ in range(lodcount)]
|
|
lods = []
|
|
for lod in range(lodcount):
|
|
pcount = i32()
|
|
if not (0 < pcount < 100000):
|
|
return None
|
|
prims = []
|
|
for _ in range(pcount):
|
|
i32() # type: only primitiveVector
|
|
colour = i32()
|
|
attrs = i32()
|
|
n = i32()
|
|
if not (0 < n <= vcount * 4):
|
|
return None
|
|
prims.append({'c': colour, 'a': attrs,
|
|
'i': [i32() for _ in range(n)]})
|
|
lods.append({'s': scales[lod], 'p': prims})
|
|
return {'v': verts, 'l': lods}
|
|
|
|
|
|
images, bad = {}, 0
|
|
for rid, r in TABLE.items():
|
|
if r['desc'].endswith(': GaugeImage'):
|
|
img = gauge_image(r['addr'])
|
|
if img is None:
|
|
bad += 1
|
|
else:
|
|
img['n'] = r['desc'].split(':')[0]
|
|
images[rid] = img
|
|
print('gauge images: %d (%d unreadable)' % (len(images), bad), file=sys.stderr)
|
|
|
|
CONSOLE = {}
|
|
xml = open(ROOT + '/tools/console-config/RPConfig.xml', encoding='utf-8-sig').read()
|
|
for m in re.finditer(r'<map\s+key="([^"]+)"\s+name="([^"]+)"', xml):
|
|
CONSOLE[m.group(1)] = m.group(2)
|
|
|
|
tracks = []
|
|
for rid, r in sorted(TABLE.items()):
|
|
m = re.match(r'^(\w+): Map stream of (\d+) instances$', r['desc'])
|
|
if not m:
|
|
continue
|
|
key, count = m.group(1), int(m.group(2))
|
|
placed, noart = [], 0
|
|
for gid, pos, q in navmap.instances(RES, r['addr'], r['size'], count,
|
|
set(images)):
|
|
if gid is None:
|
|
noart += 1 # the map screen skips these
|
|
continue
|
|
placed.append([gid,
|
|
round(pos[0], 2), round(pos[1], 2), round(pos[2], 2),
|
|
round(q[0], 5), round(q[1], 5), round(q[2], 5),
|
|
round(q[3], 5)])
|
|
tracks.append({'key': key, 'name': CONSOLE.get(key, key),
|
|
'noart': noart, 'i': placed})
|
|
print(' %-14s %4d placed, %2d without map art'
|
|
% (key, len(placed), noart), file=sys.stderr)
|
|
|
|
# The map is not a phosphor screen. Primitives carry palette indices, and
|
|
# the palette is whatever the port was configured with - for the pod's
|
|
# secondary screen, where the nav display lives, L4GAUGE.CFG says
|
|
# configure(0, sec, 270, 0x00ff, clut0, rgb, secpal.pcc). So the walls
|
|
# (index 51) come out grey, not green. PCC is PCX: 0x0C then 256 RGB
|
|
# triples in the last 769 bytes.
|
|
PALETTE_FILE = ROOT + '/assets/RP411/GAUGE/secpal.pcc'
|
|
raw = open(PALETTE_FILE, 'rb').read()
|
|
if len(raw) < 769 or raw[-769] != 0x0C:
|
|
sys.exit('%s has no VGA palette where PCX keeps one' % PALETTE_FILE)
|
|
base = len(raw) - 768 # absolute: -768+255*3 wraps round to 0
|
|
palette = ['#%02x%02x%02x' % tuple(raw[base + i * 3:base + i * 3 + 3])
|
|
for i in range(256)]
|
|
|
|
# nav(A, ModeAlwaysActive, (448,416), 0x00, 0x3C, ...): the screen is
|
|
# 448x416, the background is colour 0 and static objects default to 0x3C.
|
|
# A primitive whose own colour is 0 keeps the colour already set, which is
|
|
# that default - see L4GaugeImage::Draw.
|
|
data = {'images': images, 'tracks': tracks, 'palette': palette,
|
|
'staticColor': 0x3C, 'background': 0x00, 'screen': [448, 416]}
|
|
blob = json.dumps(data, separators=(',', ':'))
|
|
|
|
read = lambda n: open(os.path.join(HERE, n), encoding='utf-8').read()
|
|
open(OUT, 'w', encoding='utf-8').write(
|
|
read('mapview.tpl').format(css=read('mapview.css'), js=read('mapview.js'),
|
|
data=blob))
|
|
print('wrote %s (%.0f KB, %d tracks)'
|
|
% (OUT, len(blob) / 1024, len(tracks)), file=sys.stderr)
|