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:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user