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.
194 lines
7.0 KiB
Python
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
|