Files
goldeneye-pc-port/tools_pc/d43_invariants.py
T
James Dansereau 73ea03734d PC port: phase 2 — D48 review: re-plan D43 as offline pre-conversion (Plan B)
- D48 finding in PCPortResearch.md §F: d43_convert.py does not emit bytes
  (layout+remap validation only); Plan B = Python emit pass -> data/pcmodels-<region>/
  RZ sidecars served via port-layer hw_address/rom_size table patch (no C
  converter); Plan A kept as fallback with corrected staging guard (D_N64+D_PC<=avail)
- Moved all 23 d43_*.py investigation scripts from gitignored build-pc/ to tracked
  tools_pc/ (spec is now versioned); d43_cover.py path ref updated; converter
  re-run from new location: ALL CLEAN
- HANDOFF.md rewritten as two-session prompt: Session A = independent review of
  Plan B (8 claims, each with falsification criterion -> finding D49), Session B =
  execute (emit pass + round-trip validator, romdata plumbing, one-line hook,
  pixel-assert soak); verified-facts converter spec preserved
- AGENTS.md phase status updated to D48 re-plan
2026-08-22 18:43:04 -04:00

112 lines
4.4 KiB
Python

# D43: full ownership tiling check per file.
# objects: switches[0,4NS) textures[4NS,4NS+12NT) nodes(20B) records(RSZ)
# vtx(nv*16) coll(nc*16) gdl(up to next gdl start or D)
# PointUsage: [puo, min(next owned start)) -- verify even & >0
# then verify union == [0,D) exactly (no gaps/overlaps).
import csv, struct, zlib, os, re
from collections import Counter
ROM = r"data/ge007.ntsc-final.z64"
rom = open(ROM, "rb").read()
rows = list(csv.reader(open(r"scripts/filelist.u.csv")))
RSZ={1:0x10,2:0x1C,4:0x14,8:0x10,9:0x24,10:0x1C,18:0x08,21:0x14,23:0x02,24:0x20}
hdrfiles=set()
for root,dirs,files in os.walk("assets"):
for fn in files:
if fn.lower().endswith("modelfileheader.inc.c"): hdrfiles.add(os.path.join(root,fn))
nsnt={}
for f in sorted(hdrfiles):
for line in open(f):
m=re.search(r"MODELFILEHEADER\((.*)\)\s*;?\s*$",line)
if not m: continue
a=[x.strip() for x in m.group(1).split(",")]
if len(a)<9: continue
try: nsnt[a[0]]=(int(a[4],0),int(a[8],0))
except ValueError: pass
def be16(b,o): return struct.unpack_from(">H",b,o)[0]
def be32(b,o): return struct.unpack_from(">I",b,o)[0]&0xFFFFFF
fails=[]; pu_sizes=Counter(); nfiles=0; gapfiles=[]
for r in rows:
if len(r)<3 or not r[2]: continue
base=r[2].rsplit("/",1)[-1]
if base.endswith(".bin"): base=base[:-4]
if not (base[0] in "CGP" and base.endswith("Z") and "_stan" not in base): continue
try: addr,size=int(r[0]),int(r[1])
except ValueError: continue
key=None
for cand in (base,base[1:],base[:-1],base[1:-1]):
if cand in nsnt: key=cand; break
if not key: fails.append((base,"no header")); continue
NS,NT=nsnt[key]
try: out=zlib.decompress(rom[addr:addr+size][2:],-15)
except Exception: fails.append((base,"decomp")); continue
D=len(out); nfiles+=1
R0=4*NS+12*NT
objs=[] # (start,end,kind)
pu_starts=[]
stack=[R0]; nodeset=set()
while stack:
o=stack.pop()
if o in nodeset or o>=D: fails.append((base,"bad node %X"%o)); break
nodeset.add(o)
op=be16(out,o)&0xff
data=be32(out,o+4); child=be32(out,o+20)
objs.append((o,o+20,"node"))
rsz=RSZ.get(op,0)
if not rsz or data+rsz>D: fails.append((base,"bad rec op%d"%op)); break
objs.append((data,data+rsz,"rec"))
if op==4:
for f_off in (0,4):
v=be32(out,data+f_off)
if v: objs.append((v,None,"gdl"))
v=be32(out,data+0xC); nv=be16(out,data+0x10)
if v: objs.append((v,v+16*nv,"vtx"))
elif op==24:
for f_off in (0,4):
v=be32(out,data+f_off)
if v: objs.append((v,None,"gdl"))
v=be32(out,data+8); nv=be16(out,data+0xC); nc=be16(out,data+0xE)
cv=be32(out,data+0x10); puo=be32(out,data+0x14)
if v: objs.append((v,v+16*nv,"vtx"))
if cv: objs.append((cv,cv+16*nc,"coll"))
if puo: pu_starts.append(puo)
if child: stack.append(child)
else:
# gdl extents: sorted gdl starts, each extends to next gdl start or D
gs=sorted(set(s for s,e,k in objs if k=="gdl"))
fixed=[]
for i,(s,e,k) in enumerate(objs):
if k=="gdl":
end = gs[gs.index(s)+1] if gs.index(s)+1<len(gs) else D
fixed.append((s,end,"gdl"))
else: fixed.append((s,e,k))
objs=fixed
# pointusage: [puo, min next start)
starts=sorted(set(s for s,e,k in objs))
for puo in pu_starts:
nxt=[s for s in starts if s>puo]
end=min(nxt) if nxt else D
sz=end-puo
if sz<=0 or sz%2: fails.append((base,"pu bad size %d @%X"%(sz,puo))); break
objs.append((puo,end,"pu"))
pu_sizes[sz]+=1
else:
# tiling check
ivs=sorted((s,e) for s,e,k in objs)
ok=True; pos=0
for s,e in ivs:
if s<pos: fails.append((base,"overlap @%X (pos %X)"%(s,pos))); ok=False; break
if s>pos: gapfiles.append((base,hex(pos),hex(s))); ok=False; break
pos=e
if ok and pos!=D: fails.append((base,"short end %X vs D=%X"%(pos,D)))
print("files:",nfiles,"fails:",len(fails))
for f in fails[:15]: print(" ",f)
print("gap files:",len(gapfiles))
for g in gapfiles[:10]: print(" ",g)
print("\nPointUsage sizes:",dict(sorted(pu_sizes.items())))