From f2281ed661504bcecd17656c19b4160886e14fe5 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:16:05 -0400 Subject: [PATCH] D88.4: propDefs record format spec + N64->PC converter (d88_propdefs.py) The propDefs stream is a flat s32[]; each record's serialized N64 word count is fixed across all 21 levels (verified: the getools C sources tile every propDefs region byte-for-byte against the ROM). Every pointer member is 0 in the file, so records containing pointers grow 4->8B per pointer on PC. d88_propdefs.py rewrites each record to its native PC struct layout: header word (u16+u8+u8) and _mkword half-pairs swapped independently, scalars bswap32'd, pointer slots widened to 8 zero bytes, runtime areas zero-filled. d88_layoutprobe.c (expanded) is the compiler-verified sizeof/offsetof source for PROPDEF_PC_BYTES. Converts + walk-verifies all 21 levels clean. --- .gitignore | 2 +- tools_pc/d88_layoutprobe.c | 181 ++++++++++-------- tools_pc/d88_propdefs.py | 370 +++++++++++++++++++++++++++++++++++++ 3 files changed, 473 insertions(+), 80 deletions(-) create mode 100644 tools_pc/d88_propdefs.py diff --git a/.gitignore b/.gitignore index d008a355..caaffcca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ #Git and VS files .*~ -__pycache__/* +__pycache__/ .vscode/ .[vV][sS]/ diff --git a/tools_pc/d88_layoutprobe.c b/tools_pc/d88_layoutprobe.c index 392ede12..acfb6225 100644 --- a/tools_pc/d88_layoutprobe.c +++ b/tools_pc/d88_layoutprobe.c @@ -1,9 +1,11 @@ /* D88.4: compiler-verified PC layout probe for propDef record types. * - * Prints sizeof (and key offsets) for every PROPDEF_* record struct the - * offline converter (d88_emit.py) must reproduce byte-for-byte, compiled with - * the SAME toolchain + include chain + flags as the PC game build. Run: - * + * Prints sizeof + pointer-member offsets for every PROPDEF_* record struct the + * offline converter (d88_emit.py) must reproduce, compiled with the SAME + * toolchain + include chain + flags as the PC game build. Machine-readable: + * SZ + * PTR + * Run: * export PATH="/c/msys64/mingw64/bin:$PATH" * x86_64-w64-mingw32-gcc -std=gnu11 -DPORT=1 -DPLATFORM_64BIT=1 \ * -D_LANGUAGE_C=1 -DVERSION_US -DLANG_US -DREFRESH_NTSC \ @@ -11,7 +13,6 @@ * -Isrc/libultra/audio -Iport/include -Ibuild-pc/port/include \ * -include versioninfo.h \ * tools_pc/d88_layoutprobe.c -o build-pc/d88_layoutprobe.exe - * ./build-pc/d88_layoutprobe.exe */ #include #include @@ -21,88 +22,110 @@ #define offsetof(type, member) __builtin_offsetof(type, member) #endif -#define P_SZ(t) printf("SZ %-34s %3lu bytes %2lu words\n", #t, \ - (unsigned long)sizeof(t), (unsigned long)sizeof(t) / 4) -#define P_OFF(t, f) printf("OFF %-28s %-16s %3lu\n", #t, #f, \ - (unsigned long)offsetof(t, f)) +#define SZ(t) printf("SZ %-26s %lu\n", #t, (unsigned long)sizeof(t)) +#define PTR(t, f) printf("PTR %-26s %-18s %lu\n", #t, #f, (unsigned long)offsetof(t, f)) int main(void) { - /* --- the base header every record embeds --- */ - P_SZ(PropDefHeaderRecord); - P_OFF(PropDefHeaderRecord, extrascale); - P_OFF(PropDefHeaderRecord, state); - P_OFF(PropDefHeaderRecord, type); + SZ(PropDefHeaderRecord); - /* --- ObjectRecord family (PROP/GLASS/ALARM/RACK/HAT/SAFE/GAS/KEY) --- */ - P_SZ(ObjectRecord); - P_OFF(ObjectRecord, obj); - P_OFF(ObjectRecord, pad); - P_OFF(ObjectRecord, flags); - P_OFF(ObjectRecord, flags2); - P_OFF(ObjectRecord, prop); - P_OFF(ObjectRecord, model); - P_OFF(ObjectRecord, mtx); - P_OFF(ObjectRecord, runtime_pos); - P_OFF(ObjectRecord, runtime_bitflags); - P_OFF(ObjectRecord, ptr_allocated_collisiondata_block); - P_OFF(ObjectRecord, projectile); - P_OFF(ObjectRecord, maxdamage); - P_OFF(ObjectRecord, damage); - P_OFF(ObjectRecord, shadecol); - P_OFF(ObjectRecord, nextcol); + SZ(ObjectRecord); + PTR(ObjectRecord, prop); + PTR(ObjectRecord, model); + PTR(ObjectRecord, ptr_allocated_collisiondata_block); + PTR(ObjectRecord, projectile); + PTR(ObjectRecord, mtx); + PTR(ObjectRecord, maxdamage); + PTR(ObjectRecord, shadecol); - P_SZ(DoorRecord); - P_OFF(DoorRecord, linkedDoorOffset); - P_OFF(DoorRecord, maxFrac); - P_OFF(DoorRecord, doorFlags); - P_OFF(DoorRecord, doorType); - P_OFF(DoorRecord, keyflags); - P_OFF(DoorRecord, autoCloseFrames); - P_OFF(DoorRecord, doorOpenSound); - P_OFF(DoorRecord, frac); - P_OFF(DoorRecord, openstate); - P_OFF(DoorRecord, calculatedopacity); - P_OFF(DoorRecord, TintDist); - P_OFF(DoorRecord, CullDist); - P_OFF(DoorRecord, soundType); - P_OFF(DoorRecord, linkedDoor); - P_OFF(DoorRecord, unkcc); - P_OFF(DoorRecord, bbox); - P_OFF(DoorRecord, openedTime); - P_OFF(DoorRecord, portalNumber); - P_OFF(DoorRecord, openSoundState); - P_OFF(DoorRecord, closeSoundState); + SZ(DoorRecord); + PTR(DoorRecord, prop); + PTR(DoorRecord, model); + PTR(DoorRecord, ptr_allocated_collisiondata_block); + PTR(DoorRecord, projectile); + PTR(DoorRecord, linkedDoorOffset); + PTR(DoorRecord, linkedDoor); + PTR(DoorRecord, unkcc); + PTR(DoorRecord, bbox); + PTR(DoorRecord, openedTime); + PTR(DoorRecord, openSoundState); + PTR(DoorRecord, closeSoundState); - P_SZ(GuardRecord); - P_OFF(GuardRecord, chrnum); - P_OFF(GuardRecord, PadID); - P_OFF(GuardRecord, bitflags); - P_OFF(GuardRecord, HeadID); - P_OFF(GuardRecord, Data); + SZ(GuardRecord); + PTR(GuardRecord, Data); - P_SZ(GlobalDoorScaleRecord); - P_SZ(KeyRecord); - P_OFF(KeyRecord, keyflags); - P_SZ(TintedGlassRecord); - P_SZ(TagObjectRecord); - P_OFF(TagObjectRecord, NextTag); - P_OFF(TagObjectRecord, TaggedObject); + SZ(GlobalDoorScaleRecord); + SZ(KeyRecord); + PTR(KeyRecord, keyflags); + SZ(TintedGlassRecord); - /* --- records whose sizepropdef() arm is a hardcoded literal --- */ - /* Wrapped so the probe still builds if a type is missing; each line that - * fails to compile tells us that type has no PC struct yet. */ -#define TRY_SZ(t) P_SZ(t) - TRY_SZ(CutsceneRecord); - TRY_SZ(RenameObjectRecord); - TRY_SZ(LockDoorRecord); - TRY_SZ(SafeObjectRecord); - TRY_SZ(PhotographObjectRecord); - TRY_SZ(DestroyObjectRecord); - TRY_SZ(CollectObjectRecord); - TRY_SZ(DepositObjectRecord); - TRY_SZ(NULLObjectRecord); - TRY_SZ(CoopyObjectRecord); + SZ(TagObjectRecord); + PTR(TagObjectRecord, ID); + PTR(TagObjectRecord, NextTag); + PTR(TagObjectRecord, TaggedObject); + + SZ(CCTVRecord); + PTR(CCTVRecord, unk84); + PTR(CCTVRecord, unkC4); + PTR(CCTVRecord, unkF8); + + SZ(MonitorRecord); + SZ(MonitorObjRecord); + PTR(MonitorObjRecord, Monitor); + PTR(MonitorObjRecord, OwnerOffset); + PTR(MonitorObjRecord, ImageNum); + + SZ(MultiMonitorObjRecord); + PTR(MultiMonitorObjRecord, Monitor); + PTR(MultiMonitorObjRecord, ImageNums); + + SZ(WeaponObjRecord); + PTR(WeaponObjRecord, weaponnum); + PTR(WeaponObjRecord, dualweapon); + + SZ(AmmoCrateRecord); + PTR(AmmoCrateRecord, ammoType); + + SZ(MultiAmmoCrateRecord); + PTR(MultiAmmoCrateRecord, slots); + + SZ(BodyArmourRecord); + PTR(BodyArmourRecord, initialamount); + + SZ(HatRecord); + SZ(AutogunRecord); + + SZ(GuardAttributeRecord); + PTR(GuardAttributeRecord, chrnum); + + SZ(WatchMenuObjectiveTextRecord); + SZ(MissionObjectiveRecord); + PTR(MissionObjectiveRecord, ObjRefID); + PTR(MissionObjectiveRecord, nextentry); + SZ(DestroyObjectRecord); + SZ(CompleteConditionRecord); + SZ(FailConditionRecord); + SZ(CollectObjectRecord); + SZ(DepositObjectRecord); + SZ(DepositObjectInRoomRecord); + SZ(NULLObjectRecord); + SZ(CoopyObjectRecord); + SZ(GasReleasingRecord); + SZ(EnterRoomRecord); + PTR(EnterRoomRecord, unk8); + + SZ(PhotographObjectRecord); + PTR(PhotographObjectRecord, unk8); + PTR(PhotographObjectRecord, lastprop); + + SZ(RenameObjectRecord); + PTR(RenameObjectRecord, TagID); + PTR(RenameObjectRecord, renobj); + + SZ(LockDoorRecord); + PTR(LockDoorRecord, next); + + SZ(CutsceneRecord); return 0; } diff --git a/tools_pc/d88_propdefs.py b/tools_pc/d88_propdefs.py new file mode 100644 index 00000000..dcc5e049 --- /dev/null +++ b/tools_pc/d88_propdefs.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +"""D88.4: convert the `propDefs` polymorphic record stream of a Usetup*Z stage +file from N64 layout to native PC layout. + +Ground truth (all cross-checked against the retail ROM + the getools-generated +`assets/obseg/setup/Usetup*Z.c` sources, which tile every propDefs region +byte-for-byte): + + * The stream is a flat `s32[]`. Each record starts with a + PropDefHeaderRecord word: [u16 extrascale][u8 state][u8 type]. The `type` + byte selects the record layout and its FIXED serialized word count + (identical across all 21 levels -- see PROPDEF_N64_WORDS below). + * Every pointer member inside a serialized record is 0 in the file + (runtime-populated). On PC those slots widen 4->8 bytes, so records that + contain pointers grow and the walk stride changes. This module emits each + record at its native PC struct size (PROPDEF_PC_BYTES), zero-filling the + runtime area, byte-swapping the meaningful scalar fields, and swapping the + two halves of any `_mkword`-packed field independently. + * The matching runtime change is in loadobjectmodel.c `sizepropdef()` under + `#ifdef PORT`: it must return PROPDEF_PC_BYTES[type] / 4. + +Only `OBJ_COPY_ITEM` (type 34) had a wrong N64 literal in `sizepropdef()`'s +`#if 1` branch (returned 1, real size 3); every other arm already matched. +""" +import struct + +# ---------------------------------------------------------------- constants --- +# serialized N64 word count per PROPDEF type byte (getools / ROM verified). +PROPDEF_N64_WORDS = { + 1: 64, # DOOR + 2: 2, # DOOR_SCALE + 3: 32, # PROP (ObjectRecord) + 4: 33, # KEY + 5: 32, # ALARM (ObjectRecord) + 6: 59, # CCTV + 7: 33, # MAGAZINE / AmmoMag + 8: 34, # COLLECTABLE + 9: 7, # GUARD + 10: 64, # MONITOR (SingleMonitor) + 11: 149, # MULTI_MONITOR + 12: 32, # RACK / HangingMonitor (ObjectRecord) + 13: 54, # AUTOGUN / Drone + 14: 3, # LINK + 17: 32, # HAT (ObjectRecord) + 18: 3, # GUARD_ATTRIBUTE / SetGuardAttribute + 19: 4, # SWITCH / LinkProps + 20: 45, # AMMO / AmmoBox + 21: 34, # ARMOUR + 22: 4, # TAG + 23: 4, # OBJECTIVE_START + 24: 1, # OBJECTIVE_END + 25: 2, # OBJ_DESTROY + 26: 2, # OBJ_COMPLETE_COND + 27: 2, # OBJ_FAIL_COND + 28: 2, # OBJ_COLLECT + 29: 2, # OBJ_DEPOSIT + 30: 4, # OBJ_PHOTOGRAPH + 31: 1, # OBJ_NULL + 32: 4, # OBJ_ENTER_ROOM + 33: 5, # OBJ_DEPOSIT_IN_ROOM + 34: 3, # OBJ_COPY_ITEM (N64 sizepropdef #if 1 wrongly says 1) + 35: 4, # WATCH_MENU_OBJ_TEXT + 36: 32, # GAS_RELEASING (ObjectRecord) + 37: 10, # RENAME + 38: 4, # LOCK_DOOR + 39: 44, # VEHICHLE + 40: 45, # AIRCRAFT + 42: 32, # GLASS (ObjectRecord) + 43: 32, # SAFE (ObjectRecord) + 44: 5, # SAFE_ITEM + 45: 56, # TANK + 46: 7, # CAMERAPOS / Cutscene + 47: 37, # TINTED_GLASS + 48: 1, # END +} + +# ObjectRecord: the shared 32-word N64 prefix of every "object" record. +# N64 word index -> kind. words not listed are plain bswap32 (mostly zero). +# 0 header [u16 extrascale][u8 state][u8 type] +# 1 [s16 obj][s16 pad] +# 4 prop* 5 model* 26 collisiondata* 27 projectile* +OBJ_PTR_WORDS = (4, 5, 26, 27) +OBJ_N64_WORDS = 32 +OBJ_PC_BYTES = 144 + +# Per-type PC struct size (bytes) the converter emits and sizepropdef must +# return / 4. For ObjectRecord-derived types this is 144 + tail growth. +PROPDEF_PC_BYTES = { + 1: 296, # DoorRecord + 2: 8, # GlobalDoorScaleRecord + 3: 144, 5: 144, 12: 144, 17: 144, 36: 144, 42: 144, 43: 144, # ObjectRecord + 4: 152, # KeyRecord + 6: 272, # CCTVRecord + 7: 152, # AmmoCrateRecord + 8: 160, # WeaponObjRecord + 9: 32, # GuardRecord + 10: 288, # MonitorObjRecord + 11: 664, # MultiMonitorObjRecord + 13: 248, # AutogunRecord + 14: 24, # LinkRecord (3w + next* grown, 8-aligned) + 18: 12, # GuardAttributeRecord + 19: 24, # SwitchRecord = LinkRecord + 20: 200, # MultiAmmoCrateRecord + 21: 152, # BodyArmourRecord + 22: 24, # TagObjectRecord + 23: 16, # OBJECTIVE_START (4w, no serialized pointer) + 24: 4, # OBJECTIVE_END + 25: 8, 26: 8, 27: 8, 28: 8, 29: 8, # OBJ_* (2w) + 30: 16, # OBJ_PHOTOGRAPH (4w serialized, lastprop* not serialized) + 31: 4, # OBJ_NULL + 32: 16, # OBJ_ENTER_ROOM + 33: 20, # OBJ_DEPOSIT_IN_ROOM + 34: 12, # OBJ_COPY_ITEM (3w) + 35: 16, # WATCH_MENU_OBJ_TEXT (4w) + 37: 48, # RenameObjectRecord + 38: 32, # LockDoorRecord + 39: 176, # VehichleRecord (guess: 44w + tail; refine if a level needs it) + 40: 180, # AircraftRecord + 44: 24, # SafeObjectRecord fragment (5w + next*), refine on use + 45: 224, # TankRecord + 46: 28, # CutsceneRecord + 47: 168, # TintedGlassRecord + 48: 4, # END +} + +# DoorRecord: N64 words (after the 32-word ObjectRecord prefix) that are +# pointers. linkedDoor@0xc8 unkcc@0xcc openSoundState@0xf4 closeSoundState@0xf8 +DOOR_TAIL_PTR_WORDS = (50, 51, 61, 62) + + +def _bswap32(b): + return b[3:4] + b[2:3] + b[1:2] + b[0:1] + + +def _hdr_word(b): + """[u16 extrascale][u8 state][u8 type] -> swap the u16, keep the 2 bytes.""" + return b[1:2] + b[0:1] + b[2:3] + b[3:4] + + +def _hh_word(b): + """[hi16][lo16] -> byte-swap each half independently.""" + return b[1:2] + b[0:1] + b[3:4] + b[2:3] + + +class PropDefError(Exception): + pass + + +def _emit_object_prefix(out, src, so): + """Convert the 32-word N64 ObjectRecord prefix at src[so:] into the first + 144 PC bytes of `out` (a bytearray already zero-sized to >=144).""" + # cursor walk so pointer slots get 8-byte alignment for free + pc = 0 + for i in range(OBJ_N64_WORDS): + w = src[so + 4 * i: so + 4 * i + 4] + if i in OBJ_PTR_WORDS: + pc = (pc + 7) & ~7 + # 8 zero bytes already present + pc += 8 + elif i == 0: + out[pc:pc + 4] = _hdr_word(w); pc += 4 + elif i == 1: + out[pc:pc + 4] = _hh_word(w); pc += 4 # [s16 obj][s16 pad] + elif i in (30, 31): + out[pc:pc + 4] = w; pc += 4 # shadecol/nextcol rgba: verbatim + else: + out[pc:pc + 4] = _bswap32(w); pc += 4 + assert pc <= OBJ_PC_BYTES, pc + return OBJ_PC_BYTES + + +def convert_record(src, so, type_byte): + """Return the native-PC bytes for one propDef record whose N64 image starts + at src[so:]. Length == PROPDEF_PC_BYTES[type_byte].""" + n64w = PROPDEF_N64_WORDS[type_byte] + pcb = PROPDEF_PC_BYTES[type_byte] + out = bytearray(pcb) + + OBJ_TYPES = {3, 5, 12, 17, 36, 42, 43} + if type_byte in OBJ_TYPES: + _emit_object_prefix(out, src, so) + return bytes(out) + + if type_byte == 1: # DOOR + _emit_object_prefix(out, src, so) + # tail: N64 words 32..63 -> PC bytes 144.. + pc = OBJ_PC_BYTES + for i in range(OBJ_N64_WORDS, n64w): + w = src[so + 4 * i: so + 4 * i + 4] + if i in DOOR_TAIL_PTR_WORDS: + pc = (pc + 7) & ~7 + pc += 8 + elif i == 38: # [u16 doorFlags][u16 doorType] + out[pc:pc + 4] = _hh_word(w); pc += 4 + elif i == 47: # [s8 openstate][s8 unkbd][s16 opacity] + out[pc:pc + 4] = w[0:1] + w[1:2] + w[3:4] + w[2:3]; pc += 4 + elif i == 49: # [s16 CullDist][s8 soundType][s8 fadeTime] + out[pc:pc + 4] = w[1:2] + w[0:1] + w[2:3] + w[3:4]; pc += 4 + else: + out[pc:pc + 4] = _bswap32(w); pc += 4 + assert pc <= pcb, (pc, pcb) + return bytes(out) + + if type_byte in (4, 21, 7): # KEY / ARMOUR / MAGAZINE: ObjectRecord + plain tail + _emit_object_prefix(out, src, so) + pc = OBJ_PC_BYTES + for i in range(OBJ_N64_WORDS, n64w): + w = src[so + 4 * i: so + 4 * i + 4] + out[pc:pc + 4] = _bswap32(w); pc += 4 + return bytes(out) + + if type_byte == 8: # COLLECTABLE / WeaponObjRecord + _emit_object_prefix(out, src, so) + # N64 w32 = [s8 weaponnum][s8 linked][s16 timer]; w33 = dualweapon* + w = src[so + 4 * 32: so + 4 * 32 + 4] + out[144:148] = w[0:1] + w[1:2] + w[3:4] + w[2:3] + # dualweapon* -> zero @152 (already) + return bytes(out) + + if type_byte in (10, 11): # SingleMonitor / MultiMonitor + _emit_object_prefix(out, src, so) + nmon = 1 if type_byte == 10 else 4 + # MonitorRecord blocks are 29 N64 words each, entirely runtime -> zero. + tail_start = OBJ_N64_WORDS + 29 * nmon + pc = OBJ_PC_BYTES + 128 * nmon + for i in range(tail_start, n64w): + w = src[so + 4 * i: so + 4 * i + 4] + if type_byte == 11: + out[pc:pc + 4] = w # ImageNums u8[4]: verbatim + else: + out[pc:pc + 4] = _bswap32(w) # OwnerOffset/OwnerPart/ImageNum + pc += 4 + return bytes(out) + + if type_byte == 6: # CCTV + _emit_object_prefix(out, src, so) + # N64 w32 lookpad; w33..48 Mtxf (runtime zero); w49..58 config + out[144:148] = _bswap32(src[so + 4 * 32: so + 4 * 32 + 4]) + pc = 212 + for i in range(49, n64w): + out[pc:pc + 4] = _bswap32(src[so + 4 * i: so + 4 * i + 4]); pc += 4 + return bytes(out) + + if type_byte == 22: # TAG: [hdr][u16 ID][s16 OffsetToObj] , NextTag*, TaggedObject* + out[0:4] = _hdr_word(src[so:so + 4]) + out[4:8] = _hh_word(src[so + 4:so + 8]) + # words 2,3 = pointers -> zero @8, @16 + return bytes(out) + + if type_byte == 37: # RENAME: hdr + 8 s32 + renobj* + out[0:4] = _hdr_word(src[so:so + 4]) + for i in range(1, 9): + out[4 * i:4 * i + 4] = _bswap32(src[so + 4 * i: so + 4 * i + 4]) + return bytes(out) # renobj* @40 zero + + if type_byte in (14, 19, 38, 44): # LINK / SWITCH / LOCK_DOOR / SAFE_ITEM + out[0:4] = _hdr_word(src[so:so + 4]) + # remaining serialized words: Index1/Index2 are s32 in file (zero or id); + # trailing next* is not serialized. + for i in range(1, n64w): + out[4 * i:4 * i + 4] = _bswap32(src[so + 4 * i: so + 4 * i + 4]) + return bytes(out) + + if type_byte == 18: # SetGuardAttribute: hdr + s32 chrnum + [s16 unk8][s8 unkA][s8 GrenadeProb] + out[0:4] = _hdr_word(src[so:so + 4]) + out[4:8] = _bswap32(src[so + 4:so + 8]) + w = src[so + 8:so + 12] + out[8:12] = w[1:2] + w[0:1] + w[2:3] + w[3:4] + return bytes(out) + + if type_byte == 9: # GUARD: hdr + 5 u16-pair words + Data* + out[0:4] = _hdr_word(src[so:so + 4]) + for i in range(1, 6): + out[4 * i:4 * i + 4] = _hh_word(src[so + 4 * i: so + 4 * i + 4]) + # word 6 = Data* -> zero @24 (already; 20-byte u16 block then 8-align pad@20) + return bytes(out) + + # ---- generic: header-only records (objectives, END, DOOR_SCALE, ...) ---- + # No serialized pointers. header word special, rest plain bswap32. + out[0:4] = _hdr_word(src[so:so + 4]) + for i in range(1, n64w): + seg = src[so + 4 * i: so + 4 * i + 4] + out[4 * i:4 * i + 4] = _bswap32(seg) + return bytes(out) + + +def convert_stream(src, start, end): + """Convert the whole propDefs region src[start:end] (N64) -> PC bytes. + Returns (pc_bytes, n64_len, pc_len). Raises PropDefError on any walk + inconsistency (unknown type / overrun / region size mismatch).""" + out = bytearray() + o = start + n = 0 + while o < end: + if o + 4 > end: + raise PropDefError(f"propDefs: truncated header at {o:#x}") + t = src[o + 3] + if t not in PROPDEF_N64_WORDS: + raise PropDefError(f"propDefs: unknown record type {t} at {o:#x} (rec #{n})") + n64w = PROPDEF_N64_WORDS[t] + if o + 4 * n64w > end: + raise PropDefError(f"propDefs: record type {t} at {o:#x} overruns region end") + out += convert_record(src, o, t) + o += 4 * n64w + n += 1 + if t == 48: # END + break + if o != end: + # trailing alignment padding (a few bytes) is tolerated + pad = end - o + if pad < 0 or pad >= 16 or any(src[o:end]): + raise PropDefError(f"propDefs: walk ended at {o:#x}, region end {end:#x} " + f"(pad={pad})") + out += src[o:end] + return bytes(out), end - start, len(out) + + +if __name__ == "__main__": + import csv, zlib, sys, glob, re + rom = open("data/ge007.ntsc-final.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])) + names = sorted(n for n in fl if n.startswith("Usetup") and n.endswith("Z")) + fails = 0 + for nm in names: + a, s = fl[nm] + if s == 0: + continue + comp = rom[a:a + s] + if comp[:2] != b"\x11\x72": + continue + dec = zlib.decompress(comp[2:], -15) + be = lambda o: struct.unpack_from(">I", dec, o)[0] + pd, intro = be(12), be(8) + try: + out, n64len, pclen = convert_stream(dec, pd, intro) + grow = pclen - n64len + print(f"OK {nm:20} n64={n64len:6d} pc={pclen:6d} +{grow}") + except PropDefError as e: + print(f"FAIL {nm:20} {e}") + fails += 1 + # byte spot-checks on BUNKER1 + a, s = fl["UsetupsevbunkerZ"] + dec = zlib.decompress(rom[a:a + s][2:], -15) + be = lambda o: struct.unpack_from(">I", dec, o)[0] + pd, intro = be(12), be(8) + out, _, _ = convert_stream(dec, pd, intro) + # walk PC output, find the first DOOR (type 1), check obj==138 pad==8 + o = 0 + doors = [] + while o < len(out): + t = out[o + 3] if o + 4 <= len(out) else None + if t not in PROPDEF_PC_BYTES: + break + if t == 1: + obj = struct.unpack_from("