From 99030e4aacd356f8c66aecc7add3e2c4757dff17 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 7 Aug 2026 10:42:03 -0500 Subject: [PATCH] The track plans are the map screen's own drawing The first version of these plans was a scatter of scenery positions - an impression of a track rather than a picture of one. The game already draws the real thing: the map screen in the pod renders the track from above every race, so the plans now reconstruct that instead of approximating it. NavDisplay::DrawStatic walks the static entities, looks up each one's L4GaugeImage by resource id, and draws it through localToWorld x worldToView. navmap.py does the same offline. The pieces that made it possible: - a map instance carries its model's GaugeImage id at +44, beside the position at +48 and the quaternion at +60; - a GaugeImage is a vertex array plus per-LOD polylines through it, in world units - cn3 is an 89x5 wall segment, pit1 a 500x300 pit; - a placement whose model has no gauge image is skipped here exactly as DrawStatic skips it, which is why a card can report fewer placements carrying map art than the track contains. The difference is not subtle. Wiseguy's Wake and Paingod's Passage resolve into twin canyon walls running their length, Brewer's Bane into an L-shaped route through junction chambers, and both arenas into a lattice of obstacles inside a boundary wall. What is still missing is the driving surface: the map draws what lines the route, never the tarmac. tools/pages/navmap.py carries the reader, and the README documents both the instance record and the gauge image stream. Regenerating from the committed generators reproduces the committed page byte for byte. Co-Authored-By: Claude Opus 5 (1M context) --- docs/tracks.html | 167 ++++++++++++++++++------------------ tools/pages/build_tracks.py | 77 +++++++---------- tools/pages/navmap.py | 142 ++++++++++++++++++++++++++++++ tools/pages/tracks.tpl | 31 ++++--- 4 files changed, 277 insertions(+), 140 deletions(-) create mode 100644 tools/pages/navmap.py diff --git a/docs/tracks.html b/docs/tracks.html index 8cdc3cb..d93ce10 100644 --- a/docs/tracks.html +++ b/docs/tracks.html @@ -176,10 +176,10 @@ h1 em { font-style: normal; color: var(--sys); }

Red Planet 4.12 · plans drawn from RPL4.RES

Every track,
seen from above

There are no track maps in the game's files — the console had - pictures of them and those pictures did not survive. So these plans are drawn - from the tracks themselves: every plan below is that map's own scenery, - placed where the game places it, viewed from overhead with height - shown as brightness.

+ pictures of them and those pictures did not survive. But the game draws one every + race: the map screen in the pod renders the track from above. These plans + are that same drawing, reconstructed outside the game — the same + outlines, from the same placements, straight down.

@@ -201,14 +201,14 @@ h1 em { font-style: normal; color: var(--sys); }
- Scenery placement — brighter is higher ground + What the map screen draws — walls, structures, pit Offered for the death race Offered for football
-
+

Paingod's Passage

pain

@@ -217,14 +217,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
255
Cameras
18
Start boxes
12
-
Extent
3256 × 7223
-
Relief
2849
+
Extent
1779 × 7228
+
Relief
240
- +

244 of 255 placements carry map art; the rest the map screen does not draw either

-
+

Blade's Edge

blade

@@ -233,14 +233,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
216
Cameras
21
Start boxes
7
-
Extent
1365 × 6786
-
Relief
466
+
Extent
1335 × 6791
+
Relief
334
- +

203 of 216 placements carry map art; the rest the map screen does not draw either

-
+

Yip's Yahoorama

yip

@@ -249,14 +249,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
127
Cameras
20
Start boxes
17
-
Extent
1451 × 5867
+
Extent
1540 × 5964
Relief
432
- +

114 of 127 placements carry map art; the rest the map screen does not draw either

-
+

Berserker's Bahnzai

otto

@@ -265,14 +265,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
203
Cameras
24
Start boxes
16
-
Extent
1260 × 5773
+
Extent
1329 × 5878
Relief
442
- +

190 of 203 placements carry map art; the rest the map screen does not draw either

-
+

Firestorm's Fury

frstrm

@@ -281,14 +281,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
325
Cameras
20
Start boxes
15
-
Extent
3875 × 5773
-
Relief
3057
+
Extent
1329 × 5878
+
Relief
382
- +

312 of 325 placements carry map art; the rest the map screen does not draw either

-
+

Wiseguy's Wake

wise

@@ -297,14 +297,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
133
Cameras
16
Start boxes
9
-
Extent
1655 × 5608
-
Relief
418
+
Extent
1744 × 5713
+
Relief
386
-

Same footprint and instance count as -- Wiserguy's --

+

Same footprint and instance count as -- Wiserguy's --

120 of 133 placements carry map art; the rest the map screen does not draw either

-
+

Burnt Cookies

burnt

@@ -313,14 +313,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
251
Cameras
17
Start boxes
3
-
Extent
1500 × 5400
-
Relief
1007
+
Extent
1589 × 5447
+
Relief
469
- +

240 of 251 placements carry map art; the rest the map screen does not draw either

-
+

Brewer's Bane

brewers

@@ -329,14 +329,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
607
Cameras
20
Start boxes
20
-
Extent
3100 × 4485
-
Relief
368
+
Extent
3160 × 4532
+
Relief
299
- +

595 of 607 placements carry map art; the rest the map screen does not draw either

-
+

Lyz's Lane

lyzlane

@@ -345,14 +345,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
105
Cameras
12
Start boxes
44
-
Extent
1298 × 4170
+
Extent
1387 × 4175
Relief
313
- +

94 of 105 placements carry map art; the rest the map screen does not draw either

-
+

Freezemoon's Freeway

headoff

@@ -361,14 +361,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
264
Cameras
18
Start boxes
5
-
Extent
1500 × 5000
-
Relief
195
+
Extent
1589 × 5047
+
Relief
247
- +

251 of 264 placements carry map art; the rest the map screen does not draw either

-
+

Freezemoon's Freeway (Football)

headmf

@@ -377,14 +377,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
274
Cameras
18
Start boxes
5
-
Extent
1500 × 5000
-
Relief
195
+
Extent
1589 × 5047
+
Relief
247
- +

263 of 274 placements carry map art; the rest the map screen does not draw either

-
+

-- Tour De Mars --

tourdemars

@@ -393,14 +393,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
607
Cameras
20
Start boxes
20
-
Extent
1400 × 23298
-
Relief
635
+
Extent
1387 × 23303
+
Relief
415
- +

595 of 607 placements carry map art; the rest the map screen does not draw either

-
+

-- Donut --

donut

@@ -409,14 +409,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
325
Cameras
20
Start boxes
15
-
Extent
4275 × 9600
-
Relief
1430
+
Extent
3289 × 9697
+
Relief
129
- +

314 of 325 placements carry map art; the rest the map screen does not draw either

-
+

-- Ares' Armpits --

trough

@@ -425,14 +425,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
633
Cameras
16
Start boxes
9
-
Extent
5130 × 9122
-
Relief
2115
+
Extent
1387 × 9219
+
Relief
540
-

631 of 633 instances decoded

+

582 of 633 placements carry map art; the rest the map screen does not draw either

-
+

-- Zaxxis --

rpweekend2014

@@ -441,14 +441,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
695
Cameras
40
Start boxes
3
-
Extent
3885 × 5915
+
Extent
3389 × 5962
Relief
266
- +

678 of 695 placements carry map art; the rest the map screen does not draw either

-
+

-- Wiserguy's --

wiserguys

@@ -457,14 +457,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
133
Cameras
16
Start boxes
9
-
Extent
1655 × 5608
-
Relief
466
+
Extent
1744 × 5713
+
Relief
435
-

Same footprint and instance count as Wiseguy's Wake

+

Same footprint and instance count as Wiseguy's Wake

119 of 133 placements carry map art; the rest the map screen does not draw either

-
+

-- Arena (Large) --

demoderb

@@ -473,14 +473,14 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
455
Cameras
17
Start boxes
3
-
Extent
2200 × 1900
-
Relief
63
+
Extent
2197 × 1997
+
Relief
0
- +

438 of 455 placements carry map art; the rest the map screen does not draw either

-
+

-- Arena (Small) --

arena

@@ -489,23 +489,26 @@ h1 em { font-style: normal; color: var(--sys); }
Instances
695
Cameras
17
Start boxes
3
-
Extent
1775 × 1750
-
Relief
80
+
Extent
1844 × 1855
+
Relief
55
- +

678 of 695 placements carry map art; the rest the map screen does not draw either

-

18 tracks: 9 shipped in the 4.10 arcade cabinets, 2 added in 4.11, 7 built by the community afterwards. Each plan - is the map's instance stream — the scenery the game places — read out of - assets/RP411/RPL4.RES. Records are 76 bytes carrying a position and a - unit quaternion, so a placement validates itself and a mis-read cannot pass as a - building. Extent and relief are the span of those placements in world units, not a - measured track length: a long thin plan is a point-to-point run, a squat one a - circuit or an arena.

-

What is not here: the driving surface. Only placed objects are - stored this way, so a plan shows what lines the route rather than the route itself. +

18 tracks: 9 shipped in the 4.10 arcade cabinets, 2 added + in 4.11, 7 built by the community afterwards. Each plan follows + NavDisplay::DrawStatic: for every placement in the map's instance + stream, look up that model's GaugeImage — a small set of outlines + in world units, the same ones the pod's map screen draws — and lay it down + rotated and positioned. A placement whose model has no gauge image is skipped here + exactly as the map screen skips it, which is why a card can say fewer placements + carry map art than the track contains.

+

So the walls are the canyon walls and the chambers are real chambers, + but there is still no driving surface: the map draws what lines the route, never the + tarmac. Extent and relief are the span of the drawn outlines in world units, so a + long thin plan is a point-to-point run and a squat one a circuit or an arena. Tracks marked NOT IN THE MENU exist in the resource file but the setup screen does not offer them.

diff --git a/tools/pages/build_tracks.py b/tools/pages/build_tracks.py index 0659a36..197fe8c 100644 --- a/tools/pages/build_tracks.py +++ b/tools/pages/build_tracks.py @@ -9,6 +9,9 @@ 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) @@ -75,49 +78,31 @@ def era(key): if key in RP411: return ('r411', 'ADDED 4.11') return ('later', 'COMMUNITY') -# ------------------------------------------------------- instance stream -def placements(addr, size, count): - """Records are 76 bytes: a class id at +0, position at +48, unit - quaternion at +60. A few records are longer, so resync on a bad - header rather than trusting the stride.""" - o, end, out, guard = addr + 4, addr + size, [], 0 - while len(out) < count and o + 76 <= end and guard < 40000: - guard += 1 - cls = struct.unpack_from(' 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) @@ -139,16 +124,19 @@ for res in TABLE.values(): tracks[m.group(1)][field] = int(m.group(2)) for key, t in tracks.items(): - pts = placements(t['addr'], t['size'], t['instances']) - t['parsed'] = len(pts) + 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(pts) + t['art'], t['aw'], t['ah'] = plan_view(segs) else: t['extent'] = (0, 0, 0) t['art'] = None @@ -183,8 +171,9 @@ for key in ORDER: 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 instances decoded

' - % (t['parsed'], t['instances'])) if t['parsed'] != t['instances'] else '' + short = ('

%d of %d placements carry map art; the rest the ' + 'map screen does not draw either

' % (t['drawn'], t['instances']) + ) if t['skipped'] else '' art = ('' % (html.escape(t['name']), t['art'], t['aw'], t['ah'])) if t['art'] else '' diff --git a/tools/pages/navmap.py b/tools/pages/navmap.py new file mode 100644 index 0000000..f36124a --- /dev/null +++ b/tools/pages/navmap.py @@ -0,0 +1,142 @@ +"""Recreate the map display's own overhead track drawing. + +NavDisplay::DrawStatic (RP_L4/RPL4GAUG.cpp) walks the static entities, looks +up each one's L4GaugeImage by resource id, and draws it through +localToWorld x worldToView. An entity with no gauge image is skipped. This +does the same thing offline: same outlines, same placements, straight down. + +GaugeImage stream (MUNGA_L4/L4GAUIMA.cpp): + int vertexCount + Point3D vertices[vertexCount] + int LODCount + Scalar LODScales[LODCount] + per LOD: int primitiveCount + per primitive: int type, int colour, int attributes, + int indexCount, int indices[indexCount] +""" +import re, struct + + +def resource_table(res, listing): + 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, n = [], res.find(b'StaticAudioStream\x00') - 8, len(res) + while o + 0x38 <= n: + name = res[o + 8:o + 0x28].split(b'\x00')[0].decode('latin1', 'replace') + addr, size = struct.unpack_from(' n: + 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): + nm, addr, size = walk[i] + table[rid] = {'name': nm, 'addr': addr, 'size': size, 'desc': desc} + i += 1 + return table + + +def read_gauge_image(res, addr, size): + """-> (vertices, [polyline of (x,y,z) ...]) using the finest LOD.""" + o = addr + def i32(): + nonlocal o + v = struct.unpack_from(' addr + size: + return None + return verts, lines + + +def instances(res, addr, size, count, gauge_ids): + """Map instance records: 76 bytes, model gauge-image id at +44, + position at +48, unit quaternion at +60. The quaternion validates the + record; a record whose +44 is not a gauge image is simply not drawn, + exactly as DrawStatic skips it.""" + out, o, end = [], addr + 4, addr + size + while len(out) < count and o + 76 <= end: + pos = struct.unpack_from('<3f', res, o + 48) + q = struct.unpack_from('<4f', res, o + 60) + if abs(sum(v * v for v in q) - 1.0) < 0.02 and all(abs(v) < 1e5 for v in pos): + gid = struct.unpack_from(' 1: + segs.append(pts) + return segs, drawn, skipped diff --git a/tools/pages/tracks.tpl b/tools/pages/tracks.tpl index d440f7d..438fbb8 100644 --- a/tools/pages/tracks.tpl +++ b/tools/pages/tracks.tpl @@ -9,10 +9,10 @@

Red Planet 4.12 · plans drawn from RPL4.RES

Every track,
seen from above

There are no track maps in the game's files — the console had - pictures of them and those pictures did not survive. So these plans are drawn - from the tracks themselves: every plan below is that map's own scenery, - placed where the game places it, viewed from overhead with height - shown as brightness.

+ pictures of them and those pictures did not survive. But the game draws one every + race: the map screen in the pod renders the track from above. These plans + are that same drawing, reconstructed outside the game — the same + outlines, from the same placements, straight down.

@@ -34,7 +34,7 @@
- Scenery placement — brighter is higher ground + What the map screen draws — walls, structures, pit Offered for the death race Offered for football
@@ -42,15 +42,18 @@
{cards}
-

{n} tracks: {arcade} shipped in the 4.10 arcade cabinets, {r411} added in 4.11, {later} built by the community afterwards. Each plan - is the map's instance stream — the scenery the game places — read out of - assets/RP411/RPL4.RES. Records are 76 bytes carrying a position and a - unit quaternion, so a placement validates itself and a mis-read cannot pass as a - building. Extent and relief are the span of those placements in world units, not a - measured track length: a long thin plan is a point-to-point run, a squat one a - circuit or an arena.

-

What is not here: the driving surface. Only placed objects are - stored this way, so a plan shows what lines the route rather than the route itself. +

{n} tracks: {arcade} shipped in the 4.10 arcade cabinets, {r411} added + in 4.11, {later} built by the community afterwards. Each plan follows + NavDisplay::DrawStatic: for every placement in the map's instance + stream, look up that model's GaugeImage — a small set of outlines + in world units, the same ones the pod's map screen draws — and lay it down + rotated and positioned. A placement whose model has no gauge image is skipped here + exactly as the map screen skips it, which is why a card can say fewer placements + carry map art than the track contains.

+

So the walls are the canyon walls and the chambers are real chambers, + but there is still no driving surface: the map draws what lines the route, never the + tarmac. Extent and relief are the span of the drawn outlines in world units, so a + long thin plan is a point-to-point run and a squat one a circuit or an arena. Tracks marked NOT IN THE MENU exist in the resource file but the setup screen does not offer them.