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.