The track plans are the course, not the wall markers
Drawing what the map screen draws never was going to give a map. Nearly every placement in every track is one piece, cn3, and its gauge image is two 25x5 bars at x 19.5..44.5 and -44.5..-19.5 - not a wall along the route but a wall across it with a 39 unit gate in the middle. The collision solid agrees exactly. A few hundred of those is a row of ticks. The gate is the point: cn3's origin sits in the opening, so every placement marks somewhere the race passes through. Walking the gates nearest to nearest, from the end furthest out, draws the track itself - Brewer's Bane comes out as its L with the junction chambers, Zaxxis as a circuit, and the small arena as the maze it always was. Guarded, because chaining nearest neighbours across a regular grid invents a maze-like path out of nothing but visit order. Each track is tested first on how many neighbours a gate has within 1.6x the typical spacing: a corridor gives 2, a floor of obstacles gives 4 or more. The separation is not close - seventeen tracks score 1 or 2, the demolition arena scores 8 on an exact 100 unit grid and keeps its wall blocks. Most of the arcade tracks really are near-straight canyon runs, a few hundred units wide and several thousand long. The plans say so now rather than implying otherwise.
This commit is contained in:
+55
-46
File diff suppressed because one or more lines are too long
@@ -86,11 +86,12 @@ def era(key):
|
||||
GAUGE_IDS = {rid for rid, r in TABLE.items() if r['desc'].endswith(': GaugeImage')}
|
||||
|
||||
|
||||
def plan_view(segs, box=520):
|
||||
"""The track's own outlines, straight down, as an alpha mask."""
|
||||
def plan_view(segs, box=520, flat=False):
|
||||
"""The course, straight down, as an alpha mask. `flat` for the route
|
||||
chains, which are (x, z) pairs rather than (x, y, z) world points."""
|
||||
pts = [p for seg in segs for p in seg]
|
||||
xs = [p[0] for p in pts]
|
||||
zs = [p[2] for p in pts]
|
||||
zs = [p[1] if flat else p[2] for p in pts]
|
||||
w = (max(xs) - min(xs)) or 1.0
|
||||
h = (max(zs) - min(zs)) or 1.0
|
||||
scale = (box - 20) / max(w, h)
|
||||
@@ -99,10 +100,11 @@ def plan_view(segs, box=520):
|
||||
img = Image.new('L', (iw, ih), 0)
|
||||
dr = ImageDraw.Draw(img)
|
||||
for seg in segs:
|
||||
xy = [(10 + (p[0] - min(xs)) * scale, ih - 10 - (p[2] - min(zs)) * scale)
|
||||
xy = [(10 + (p[0] - min(xs)) * scale,
|
||||
ih - 10 - ((p[1] if flat else p[2]) - min(zs)) * scale)
|
||||
for p in seg]
|
||||
if len(xy) > 1:
|
||||
dr.line(xy, fill=255, width=2, joint='curve')
|
||||
dr.line(xy, fill=255, width=3 if flat else 2, joint='curve')
|
||||
out = Image.merge('RGBA', (Image.new('L', img.size, 255),) * 3 + (img,))
|
||||
buf = io.BytesIO()
|
||||
out.save(buf, 'PNG', optimize=True)
|
||||
@@ -132,11 +134,20 @@ for key, t in tracks.items():
|
||||
t['race'] = key in RACE
|
||||
t['football'] = key in FOOTBALL
|
||||
t['era'], t['eraLabel'] = era(key)
|
||||
# The plan is the course the gates describe, not the gate markers - see
|
||||
# navmap.route_lines. Where the gates are a floor of obstacles rather
|
||||
# than a course there is no route to draw, so fall back to the wall
|
||||
# blocks themselves rather than invent one.
|
||||
chains, t['kind'] = navmap.route_lines(
|
||||
RES, TABLE, t['addr'], t['size'], t['instances'])
|
||||
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)
|
||||
if t['kind'] == 'route' and chains:
|
||||
t['art'], t['aw'], t['ah'] = plan_view(chains, flat=True)
|
||||
else:
|
||||
t['art'], t['aw'], t['ah'] = plan_view(segs)
|
||||
else:
|
||||
t['extent'] = (0, 0, 0)
|
||||
t['art'] = None
|
||||
|
||||
+69
-2
@@ -14,7 +14,7 @@ GaugeImage stream (MUNGA_L4/L4GAUIMA.cpp):
|
||||
per primitive: int type, int colour, int attributes,
|
||||
int indexCount, int indices[indexCount]
|
||||
"""
|
||||
import re, struct
|
||||
import math, re, statistics, struct
|
||||
|
||||
|
||||
def resource_table(res, listing):
|
||||
@@ -100,6 +100,74 @@ def instances(res, addr, size, count, gauge_ids):
|
||||
return out
|
||||
|
||||
|
||||
def gate_positions(res, table, map_addr, map_size, map_count, snap=20.0):
|
||||
"""Where the course goes, one point per gate.
|
||||
|
||||
cn3 is not a wall along the route - it is a wall ACROSS it, spanning
|
||||
x -44.5..44.5 with a 39 unit opening in the middle, and the collision
|
||||
solid agrees exactly. The piece's own origin sits in that opening, so
|
||||
every placement marks a point the course passes through. Walls stacked
|
||||
for height repeat the same opening, hence the snap."""
|
||||
gauge = {rid for rid, r in table.items() if r['desc'].endswith(': GaugeImage')}
|
||||
seen, out = set(), []
|
||||
for gid, pos, q in instances(res, map_addr, map_size, map_count, gauge):
|
||||
if gid is None:
|
||||
continue
|
||||
key = (round(pos[0] / snap), round(pos[2] / snap))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
out.append((pos[0], pos[2]))
|
||||
return [a for a in out
|
||||
if any(b is not a and math.dist(a, b) < 200 for b in out)]
|
||||
|
||||
|
||||
def route_lines(res, table, map_addr, map_size, map_count):
|
||||
"""-> (chains, kind). Walk the gates in the order the course visits them.
|
||||
|
||||
kind is 'route' when the gates really do form a course and 'field' when
|
||||
they do not. The test is how many neighbours a gate has within 1.6x the
|
||||
typical spacing: a corridor gives each gate the one ahead and the one
|
||||
behind, a floor of obstacles gives it four or more. The two cases are
|
||||
nowhere near each other - seventeen tracks score 1 or 2, and the
|
||||
demolition arena scores 8 on an exact 100 unit grid. That matters,
|
||||
because chaining nearest neighbours across a grid invents a maze-like
|
||||
path out of nothing but the order they happened to be visited in, and
|
||||
a drawing has no business inventing a track layout."""
|
||||
P = gate_positions(res, table, map_addr, map_size, map_count)
|
||||
if len(P) < 3:
|
||||
return [], 'field'
|
||||
spacing = statistics.median(
|
||||
min(math.dist(a, b) for b in P if b is not a) for a in P)
|
||||
if spacing <= 0:
|
||||
return [], 'field'
|
||||
radius = spacing * 1.6
|
||||
degree = statistics.median(
|
||||
sum(1 for b in P if b is not a and math.dist(a, b) <= radius) for a in P)
|
||||
if degree > 2.5:
|
||||
return [], 'field'
|
||||
|
||||
# Nearest neighbour from the end furthest out, restarting when the next
|
||||
# gate is too far to be the next gate. Restarting rather than forcing one
|
||||
# line is what keeps a branch or a separate loop honest.
|
||||
maxlink = max(260.0, spacing * 4)
|
||||
left, chains = set(P), []
|
||||
while left:
|
||||
cx = sum(p[0] for p in left) / len(left)
|
||||
cz = sum(p[1] for p in left) / len(left)
|
||||
cur = max(left, key=lambda p: math.dist(p, (cx, cz)))
|
||||
left.discard(cur)
|
||||
path = [cur]
|
||||
while left:
|
||||
nxt = min(left, key=lambda p: math.dist(path[-1], p))
|
||||
if math.dist(path[-1], nxt) > maxlink:
|
||||
break
|
||||
path.append(nxt)
|
||||
left.discard(nxt)
|
||||
if len(path) > 1:
|
||||
chains.append(path)
|
||||
return chains, 'route'
|
||||
|
||||
|
||||
def spine(seg):
|
||||
"""A track is built almost entirely from one model, cn3: a wall bar
|
||||
drawn as two closed 25x5 rectangles. Five metres of wall thickness is
|
||||
@@ -111,7 +179,6 @@ def spine(seg):
|
||||
p = seg[:-1] if len(seg) >= 5 and seg[0] == seg[-1] else None
|
||||
if not p or len(p) != 4:
|
||||
return [seg]
|
||||
import math
|
||||
edge = [math.hypot(p[(i + 1) % 4][0] - p[i][0],
|
||||
p[(i + 1) % 4][2] - p[i][2]) for i in range(4)]
|
||||
if max(edge) == 0 or min(edge) / max(edge) > 0.5:
|
||||
|
||||
+38
-29
@@ -9,10 +9,11 @@
|
||||
<p class="eyebrow">Red Planet 4.12 · plans drawn from RPL4.RES</p>
|
||||
<h1>Every track,<br><em>seen from above</em></h1>
|
||||
<p class="lede">There are no track maps in the game's files — the console had
|
||||
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. <strong>These plans
|
||||
are that same drawing, reconstructed outside the game</strong> — the same
|
||||
outlines, from the same placements, straight down.</p>
|
||||
pictures of them and those pictures did not survive. But the track itself knows
|
||||
where it goes. Almost every one is built from a single piece repeated: a wall
|
||||
thrown across the route with a gate in the middle. <strong>These plans are the
|
||||
course those gates describe</strong> — every gate is a point the race passes
|
||||
through, walked in the order the course visits them.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -34,7 +35,7 @@
|
||||
|
||||
<main class="wrap">
|
||||
<div class="key">
|
||||
<span><i style="background:var(--ink-mid)"></i>What the map screen draws — walls, structures, pit</span>
|
||||
<span><i style="background:var(--ink-mid)"></i>The course, gate by gate</span>
|
||||
<span><i style="background:var(--sys)"></i>Offered for the death race</span>
|
||||
<span><i style="background:var(--pilot)"></i>Offered for football</span>
|
||||
</div>
|
||||
@@ -43,30 +44,38 @@
|
||||
<p class="empty-state" id="nomatch" hidden>No track matches that.</p>
|
||||
|
||||
<p class="foot">{n} tracks: {arcade} shipped in the 4.10 arcade cabinets, {r411} added
|
||||
in 4.11, {later} built by the community afterwards. Each plan follows
|
||||
<code>NavDisplay::DrawStatic</code>: for every placement in the map's instance
|
||||
stream, look up that model's <code>GaugeImage</code> — 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.</p>
|
||||
<p class="foot">Two things are drawn plainer than the pod draws them, so the plan reads
|
||||
as an outline. Nearly every track is one model repeated — <code>cn3</code>, a
|
||||
wall bar whose gauge image is two closed 25×5 rectangles. Five metres of wall
|
||||
thickness is finer than the plan can resolve, so each bar is collapsed to the
|
||||
centreline between its short edges: one stroke for one wall, instead of two parallel
|
||||
lines and two end caps several hundred times over. Walls stacked to build height land
|
||||
on each other seen from above, and are drawn once. Placements standing alone more than
|
||||
200 units from any other are dropped — fourteen tracks park a single bar at
|
||||
(1200, 0, 0) far off the course, and one stray placement stretches the frame
|
||||
to twelve times the width of the track. A real branch keeps its neighbours and stays:
|
||||
Paingod's second canyon is sixty bars out at x = -400.</p>
|
||||
<p class="foot">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.</p>
|
||||
in 4.11, {later} built by the community afterwards. Drawing what the pod's map screen
|
||||
draws turns out not to give a map. That screen follows
|
||||
<code>NavDisplay::DrawStatic</code>, laying down each placement's
|
||||
<code>GaugeImage</code>, and nearly every placement in every track is the same
|
||||
piece: <code>cn3</code>, whose image is two 25×5 bars at x 19.5–44.5
|
||||
and -44.5–-19.5. That is not a wall running along the route, it is a wall thrown
|
||||
across it with a 39 unit gate in the middle, and the piece's collision solid says the
|
||||
same. A few hundred of those is a row of ticks, not a course.</p>
|
||||
<p class="foot">But the gate is the point: <code>cn3</code>'s own origin sits in the
|
||||
opening, so every placement marks somewhere the race passes through. Walk the gates in
|
||||
the order the course visits them — nearest to nearest, starting from the end
|
||||
furthest out, beginning a new line rather than reaching when the next gate is too far
|
||||
— and the track draws itself. Walls stacked for height repeat the same opening
|
||||
and count once. Placements standing alone more than 200 units from any other are
|
||||
dropped: fourteen tracks park a single piece at (1200, 0, 0) well off the
|
||||
course, and one stray placement stretches the frame to twelve times the width of the
|
||||
track. A real branch keeps its neighbours and stays — Paingod's second canyon is
|
||||
sixty pieces out at x = -400.</p>
|
||||
<p class="foot">One track is drawn the other way. Chaining nearest neighbours across a
|
||||
regular grid invents a maze-like path out of nothing but the order the points happened
|
||||
to be visited in, and a plan has no business inventing a track layout. So each track is
|
||||
tested first: count how many neighbours a gate has within 1.6× the typical
|
||||
spacing. A corridor gives each gate the one ahead and the one behind; a floor of
|
||||
obstacles gives it four or more. Seventeen tracks score 1 or 2 and are drawn as a
|
||||
course. The large arena scores 8 on an exact 100 unit grid, so it keeps its wall blocks
|
||||
instead — and the small arena, which scores 2, really is the maze it looks like.</p>
|
||||
<p class="foot">There is still no driving surface anywhere in the file: the track is
|
||||
walls and gates, never tarmac. Extent and relief are the span of the placed geometry in
|
||||
world units, so a long thin plan is a point-to-point canyon run — most of the
|
||||
arcade tracks are exactly that, a few hundred units wide and several thousand long
|
||||
— and a squat one is a circuit or an arena. Tracks marked NOT IN THE MENU exist
|
||||
in the resource file but the setup screen does not offer them.</p>
|
||||
</main>
|
||||
|
||||
<script>{js}</script>
|
||||
|
||||
Reference in New Issue
Block a user