D125: fix d88_emit.py:374 8-byte slice / 4-byte literal (Aztec C3 crash)

emit_pad()'s widened-stan zero-fill assigned a 4-byte literal to an
8-byte bytearray slice. Python slice-assign with a short RHS silently
deletes the surplus bytes, so `out` shrank 4 B per pad+boundpad; once
len(out) dropped below a later leaf write's target offset, the write
clamped to empty-at-end and inserted there, drifting/truncating the
boundpad plink-name blob (Aztec pad33 "p138d2" -> "8d2") -> stanPackId
reject -> NULL stan -> door model=NULL -> crash propobj.c:13601.

Fix: 8 NUL bytes (only width-mismatched out[ write in the file).
All 21 Usetup*Z sidecars regenerated. Aztec (-level_28) now PASSES
(was C3 CRASH); Bunker1/Silo unregressed; d125_check.py 21/21 MATCH.
Sweep 12 -> 13/21.

Bunker2 (-level_27) still crashes: separate DOOR-tail linkedDoor
layout bug in d88_propdefs.py (C3r) -- linkedDoor is resolved from
linkedDoorOffset (read-before-write int id, D123 pattern). Folded
into the converter write-audit.

M-16 probes reverted; d125_check.py kept as the offline regression guard.
Generalisable quirk added to PORT-LEARNINGS.md §C.
This commit is contained in:
James Dansereau committed 2026-08-29 19:12:52 -04:00
1 parent 710d0b12b5
commit 060a7252e5
6 files changed
+279 -71

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""D125 diagnostic: does the propDefs blob in the *emitted* pccg sidecar
(after RZ round-trip) byte-match tools_pc/d88_propdefs.convert_stream()?
Tests the M-14 claim that the RAM propDefs is "zeros/garbage after record 0".
If this script says MATCH, the corruption is downstream of the sidecar
(runtime rebase / bank alloc / sizepropdef walk); if MISMATCH, it is in
d88_emit.py's emit/placement or the RZ recompress.
"""
import csv, struct, zlib, sys
from d88_propdefs import convert_stream, PROPDEF_PC_BYTES
REGION = "ntsc-final"
rom = open(f"data/ge007.{REGION}.z64", "rb").read()
fl = {}
for r in csv.reader(open("scripts/filelist.u.csv")):
if len(r) >= 3 and r[2]:
b = r[2].rsplit("/", 1)[-1]
b = b[:-4] if b.endswith(".bin") else b
fl[b] = (int(r[0]), int(r[1]))
man = {}
for r in csv.reader(open(f"data/pccg-{REGION}/manifest.csv")):
if r and r[0].startswith("Usetup"):
man[r[0]] = (int(r[1]), int(r[2]))
sidebin = open(f"data/pccg-{REGION}/pccg.bin", "rb").read()
be = lambda b, o: struct.unpack_from(">I", b, o)[0]
targets = sys.argv[1:] or sorted(man)
for nm in targets:
a, s = fl[nm]
dec_n64 = zlib.decompress(rom[a:a + s][2:], -15)
pd_n64, intro_n64 = be(dec_n64, 12), be(dec_n64, 8)
want, n64len, pclen = convert_stream(dec_n64, pd_n64, intro_n64)
off, size = man[nm]
dec_pc = zlib.decompress(sidebin[off:off + size][2:], -15)
# PC header: 8-byte slots, low 4 = LE offset
pd_pc = struct.unpack_from("<I", dec_pc, 8 * 3)[0] # propDefs = field 3
intro_pc = struct.unpack_from("<I", dec_pc, 8 * 2)[0] # intro = field 2
got = dec_pc[pd_pc:pd_pc + len(want)]
if got == want:
print(f"MATCH {nm:18} pd@{pd_pc:#x} len={len(want)}")
else:
# first differing byte + which record it lands in
d = next((i for i in range(min(len(got), len(want))) if got[i] != want[i]), None)
rec, o = 0, 0
while o < (d or 0):
t = want[o + 3]
o += PROPDEF_PC_BYTES.get(t, 4)
rec += 1
print(f"MISMATCH {nm:18} pd@{pd_pc:#x} want={len(want)} got={len(got)} "
f"first diff @+{d} (~record {rec}, off {o:#x}); "
f"gotlen_vs_intro: {intro_pc - pd_pc} vs pclen {pclen}")
+4 -1
View File
@@ -371,7 +371,10 @@ def convert_usetup(name, src):
out[dst_o + 0x2c:dst_o + 0x30] = b"\x00\x00\x00\x00"
# stan: dead-on-load (init_pathtable_something always sets it before
# use, D88 same class as D79) -- zero it, never read from file.
out[dst_o + 0x30:dst_o + 0x38] = b"\x00\x00\x00\x00"
# NOTE: this slice is 8 bytes wide (widened stan ptr); the RHS MUST be
# 8 NUL bytes -- a 4-byte literal here makes `out` SHRINK by 4 per pad
# via Python slice-assign semantics, drifting every later region (D125).
out[dst_o + 0x30:dst_o + 0x38] = b"\x00\x00\x00\x00\x00\x00\x00\x00"
if has_bbox:
for k in range(6):
v = be32(src, src_o + 0x2c + 4 * k)