Files
RP412/tools/pages/navmap.py
T
Cyd 5135476088 Track plans read as outlines, not hatching
Nearly every track is one model repeated: cn3, a wall bar whose gauge
image is two closed 25x5 rectangles. Five metres of wall thickness is
finer than the plan can resolve, so each bar was landing as two parallel
lines plus two end caps - several hundred times over, which is the
hatching that swamped the arenas. Collapse a thin closed quad to the
centreline between its short edges: one stroke for one wall.

Walls stacked to build height coincide seen from above, so draw each
distinct wall once. And drop placements standing alone more than 200
units from any other - fourteen tracks park a single bar at (1200,0,0)
well off the course, and that one placement stretched the frame to
twelve times the width of the track. The four tracks without it are
exactly the four that always framed correctly. A real branch keeps its
neighbours and stays: Paingod's second canyon is sixty bars out at
x=-400.
2026-08-07 11:05:16 -05:00

194 lines
7.0 KiB
Python

"""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('<II', res, o + 0x30)
if addr != o + 0x38 or addr + size > 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('<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 = []
for _ in range(vcount):
verts.append((f32(), f32(), f32()))
lods = i32()
if not (0 < lods < 64):
return None
scales = [f32() for _ in range(lods)]
lines = []
for lod in range(lods):
pcount = i32()
if not (0 < pcount < 100000):
return None
prims = []
for _ in range(pcount):
ptype = i32(); i32(); i32() # type, colour, attributes
n = i32()
if not (0 < n <= vcount * 4):
return None
idx = [i32() for _ in range(n)]
prims.append(idx)
if lod == 0: # finest detail
lines = prims
if o > 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('<i', res, o + 44)[0]
out.append((gid if gid in gauge_ids else None, pos, q))
o += 76
else:
o += 4
return out
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
below the map's own resolution, so drawing the rectangle puts two
parallel lines and two end caps where the wall is one line - and a few
hundred bars of that is the hatching that swamps the plan. Collapse a
thin closed quad to the centreline joining its two short edges: the
same wall, drawn as the single stroke it reads as."""
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:
return [seg] # not a bar - leave it alone
lo = min(range(4), key=lambda i: edge[i])
a, b = p[lo], p[(lo + 1) % 4]
c, d = p[(lo + 2) % 4], p[(lo + 3) % 4]
mid = lambda u, v: tuple((u[k] + v[k]) / 2 for k in range(3))
return [[mid(a, b), mid(c, d)]]
def rotate(q, p):
"""Quaternion (x,y,z,w) applied to a point."""
qx, qy, qz, qw = q
x, y, z = p
tx = 2.0 * (qy * z - qz * y)
ty = 2.0 * (qz * x - qx * z)
tz = 2.0 * (qx * y - qy * x)
return (x + qw * tx + qy * tz - qz * ty,
y + qw * ty + qz * tx - qx * tz,
z + qw * tz + qx * ty - qy * tx)
def track_lines(res, table, map_addr, map_size, map_count):
"""Every drawn outline in the track, in world space, flattened to XZ."""
gauge = {}
for rid, r in table.items():
if r['desc'].endswith(': GaugeImage'):
gauge[rid] = r
segs = []
drawn = skipped = 0
placed = instances(res, map_addr, map_size, map_count, set(gauge))
# Fourteen of the eighteen tracks carry a single bar parked at exactly
# (1200, 0, 0), well off the course; the four that don't are the four
# that always framed correctly. One stray placement drags the bounding
# box out to twelve times the width of the course and squeezes the
# track into a sliver, so drop placements that stand alone. A real
# branch - Paingod's second canyon is sixty bars out at x=-400 - has
# neighbours and stays.
def isolated(i):
x, _, z = placed[i][1]
for j, (g, p, _) in enumerate(placed):
if j != i and g is not None and (p[0] - x) ** 2 + (p[2] - z) ** 2 < 200 ** 2:
return False
return True
for i, (gid, pos, q) in enumerate(placed):
if gid is None:
skipped += 1
continue
if isolated(i):
skipped += 1
continue
r = gauge[gid]
img = read_gauge_image(res, r['addr'], r['size'])
if img is None:
skipped += 1
continue
verts, prims = img
drawn += 1
for idx in prims:
pts = []
for k in idx:
if 0 <= k < len(verts):
wx, wy, wz = rotate(q, verts[k])
pts.append((wx + pos[0], wy + pos[1], wz + pos[2]))
if len(pts) > 1:
segs.extend(spine(pts))
# Walls are stacked to build height. Seen from above those copies land
# on each other exactly, so draw each distinct wall once.
seen, out = set(), []
for s in segs:
key = tuple(round(v, 1) for p in s for v in (p[0], p[2]))
if key not in seen:
seen.add(key)
out.append(s)
return out, drawn, skipped