A map record is an Entity::MakeMessage (MUNGA/ENTITY3.h): classToCreate, owningPlayerID, resourceID, instanceFlags, localOrigin. The origin ends the 76-byte case, which puts classToCreate at +28, resourceID at +40 and instanceFlags at +44. I had been reading +44. That is instanceFlags, and it is 524 on every scenery record - and 524 happens to be cn3's GaugeImage. So every track resolved to cn3 repeated a few hundred times, consistently and wrongly, and every conclusion drawn from that followed: the "one wall bar with a gate", the ticks, the claim that the LOD machinery is never exercised. The id at +40 varies per placement. The tracks use cn1, cn3, cn4, cn5, cn7, br1, br3, ft1, cq1, cq2, md3, md4, the pits, and the score zones sc50/sc50a/sc500a that gave the amber boxes at each end. A record names the model's Model List; the GaugeImage is filed under the same model name, so the name is the join - and models with no gauge image (oao, snAwork, pz1) are skipped here exactly as DrawStatic skips them. Found by following the map loader: InterestManager::LoadMapStream reads the stream as MakeMessages and names the map entity classes, one of which is 95 in these records - CulturalIconClassID.
153 lines
6.1 KiB
Python
153 lines
6.1 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)
|
|
|
|
# A record names the model's Model List; its GaugeImage is filed under the
|
|
# same model name. navmap.gauge_lookup does that join.
|
|
LOOKUP = {k: v for k, v in navmap.gauge_lookup(TABLE).items() if v in images}
|
|
|
|
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,
|
|
LOOKUP):
|
|
if gid is None:
|
|
noart += 1 # no gauge image: the map skips these too
|
|
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)
|