"""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 math, re, statistics, 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(' (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 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] 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