The tracks were never built from one model

A map record is an Entity::MakeMessage (MUNGA/ENTITY3.h): classToCreate,
owningPlayerID, resourceID, instanceFlags, localOrigin. The origin ends
the 76-byte case, which puts classToCreate at +28, resourceID at +40 and
instanceFlags at +44.

I had been reading +44. That is instanceFlags, and it is 524 on every
scenery record - and 524 happens to be cn3's GaugeImage. So every track
resolved to cn3 repeated a few hundred times, consistently and wrongly,
and every conclusion drawn from that followed: the "one wall bar with a
gate", the ticks, the claim that the LOD machinery is never exercised.

The id at +40 varies per placement. The tracks use cn1, cn3, cn4, cn5,
cn7, br1, br3, ft1, cq1, cq2, md3, md4, the pits, and the score zones
sc50/sc50a/sc500a that gave the amber boxes at each end. A record names
the model's Model List; the GaugeImage is filed under the same model
name, so the name is the join - and models with no gauge image (oao,
snAwork, pz1) are skipped here exactly as DrawStatic skips them.

Found by following the map loader: InterestManager::LoadMapStream reads
the stream as MakeMessages and names the map entity classes, one of which
is 95 in these records - CulturalIconClassID.
This commit is contained in:
Cyd
2026-08-07 14:45:20 -05:00
parent 86bf6a934b
commit 9f0a77cc16
4 changed files with 137 additions and 99 deletions
+6 -2
View File
@@ -97,6 +97,10 @@ xml = open(ROOT + '/tools/console-config/RPConfig.xml', encoding='utf-8-sig').re
for m in re.finditer(r'<map\s+key="([^"]+)"\s+name="([^"]+)"', xml):
CONSOLE[m.group(1)] = m.group(2)
# A record names the model's Model List; its GaugeImage is filed under the
# same model name. navmap.gauge_lookup does that join.
LOOKUP = {k: v for k, v in navmap.gauge_lookup(TABLE).items() if v in images}
tracks = []
for rid, r in sorted(TABLE.items()):
m = re.match(r'^(\w+): Map stream of (\d+) instances$', r['desc'])
@@ -105,9 +109,9 @@ for rid, r in sorted(TABLE.items()):
key, count = m.group(1), int(m.group(2))
placed, noart = [], 0
for gid, pos, q in navmap.instances(RES, r['addr'], r['size'], count,
set(images)):
LOOKUP):
if gid is None:
noart += 1 # the map screen skips these
noart += 1 # no gauge image: the map skips these too
continue
placed.append([gid,
round(pos[0], 2), round(pos[1], 2), round(pos[2], 2),
File diff suppressed because one or more lines are too long
+44 -10
View File
@@ -82,10 +82,31 @@ def read_gauge_image(res, addr, size):
return verts, lines
def instances(res, addr, size, count, gauge_ids):
def gauge_lookup(table):
"""-> {resource id in a map record: that model's GaugeImage id}.
A record names the entity's *Model List*, not its gauge image. Both are
filed under the model's own name - "cn3: Model List of 3 elements" and
"cn3: GaugeImage" - so the name is the join. DrawStatic looks the image
up by the entity's resource id and skips the entity when there is none,
which is why plenty of models resolve to nothing here: oao, snAwork and
pz1 have Model Lists and no gauge image, and the map does not draw them
either."""
by_name = {}
for rid, r in table.items():
if r['desc'].endswith(': GaugeImage'):
by_name[r['desc'].split(':')[0]] = rid
out = {}
for rid, r in table.items():
name = r['desc'].split(':')[0]
if name in by_name:
out[rid] = by_name[name]
return out
def instances(res, addr, size, count, lookup):
"""Map instance records are variable length and say so: the first int
of a record is its own length in bytes. Resource id at +44, position
at +48, quaternion at +60.
of a record is its own length in bytes.
Most records are 76 bytes - a scenery placement - but every track also
carries eight of 140, two of 80 and one of 336 (560 on Paingod's).
@@ -96,17 +117,29 @@ def instances(res, addr, size, count, gauge_ids):
makes all eighteen tracks parse to exactly their declared count with
no bytes left over.
A record whose +44 is not a gauge image is not drawn, exactly as
DrawStatic skips an entity with no gauge image."""
The record is an Entity::MakeMessage (MUNGA/ENTITY3.h): classToCreate,
owningPlayerID, resourceID, instanceFlags, then localOrigin. With the
origin ending the 76-byte case that puts classToCreate at +28,
resourceID at +40 and instanceFlags at +44.
Reading +44 as the resource id was wrong and quietly convincing:
instanceFlags is 524 on every scenery record, and 524 happens to be
cn3's GaugeImage - so every track came out built from one model
repeated. It is not. The id at +40 varies per placement, and the
tracks use cn1, cn4, cn5, cn7, br1, ft1, cq2, the score zones and
plenty more.
An entity whose model has no gauge image is not drawn, exactly as
DrawStatic skips it."""
out, o, end = [], addr + 4, addr + size
while len(out) < count and o + 8 <= end:
length = struct.unpack_from('<i', res, o)[0]
if not (40 <= length <= 1024) or o + length > end:
break
gid = struct.unpack_from('<i', res, o + 44)[0]
resource = struct.unpack_from('<i', res, o + 40)[0]
pos = struct.unpack_from('<3f', res, o + 48)
q = struct.unpack_from('<4f', res, o + 60)
out.append((gid if gid in gauge_ids else None, pos, q))
out.append((lookup.get(resource), pos, q))
o += length
return out
@@ -119,9 +152,9 @@ def gate_positions(res, table, map_addr, map_size, map_count, snap=20.0):
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')}
lookup = gauge_lookup(table)
seen, out = set(), []
for gid, pos, q in instances(res, map_addr, map_size, map_count, gauge):
for gid, pos, q in instances(res, map_addr, map_size, map_count, lookup):
if gid is None:
continue
key = (round(pos[0] / snap), round(pos[2] / snap))
@@ -221,7 +254,8 @@ def track_lines(res, table, map_addr, map_size, map_count):
gauge[rid] = r
segs = []
drawn = skipped = 0
placed = instances(res, map_addr, map_size, map_count, set(gauge))
placed = instances(res, map_addr, map_size, map_count,
gauge_lookup(table))
# 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