mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
PC port: phase 2 — Plan B executed (D50): offline model sidecars + port plumbing; texCopyGdls w1 fix
- tools_pc/d43_emit.py: full emit pass, 512/512 round-trip validated; data/pcmodels-<region>/pcmodels.bin + manifest.csv (gitignored, regenerate) - port/src/pcmodels.c(+h): cart-extension reservation, sidecar copy, one-shot file_resource_table patch; romdata.c/h extended - objecthandler_2.c: #ifdef PORT hook — pcmodelsPatchTable() + poolRemaining=0 reset (closes D48.3/R6 reload hazard) - language.c/textrelated.c: runtime C fixups for lang-bank BE offset tables (romdataFixupLangBank) and font re-layout (romdataFontPcSize/FixupFont) - front.c: legal-screen uninitialized-pointer UB seed (N64 survived by luck) - tex.c (D50.6): texCopyGdls copied only w0 of each 16B Gfx slot on PC — compaction scratch kept stale mempool w1s -> garbage G_SETTIMG -> OOB in import_texture_rgba16. Full-slot copy under #ifdef PORT; byte-proven vs PlegalpageZ sidecar (w0s match, w1s stale). Model GDLs now execute.
This commit is contained in:
1 parent
8f7ab8ec69
commit
330b2bae46
11 files changed
+1588
-3
No files matched your search
@@ -0,0 +1,46 @@
|
||||
#ifndef PORT_PCMODELS_H
|
||||
#define PORT_PCMODELS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* D50 (docs/PCPortResearch.md): PC-layout model sidecars — Plan B (D48/D49).
|
||||
*
|
||||
* N64 ROM model files are 32-bit-pointer big-endian images the PC build
|
||||
* cannot read in place (D37/D43). tools_pc/d43_emit.py converts all of them
|
||||
* offline to PC-layout RZ sidecars:
|
||||
* data/pcmodels-<region>/pcmodels.bin single concatenated image
|
||||
* data/pcmodels-<region>/manifest.csv name,offset,size (decimal)
|
||||
*
|
||||
* romdataInit reserves romSize + pcmodelsReserveSize() at CART_BASE and
|
||||
* pcmodelsLoadSidecars() copies the sidecar image to [CART_BASE+romSize, ...).
|
||||
* pcmodelsPatchTable() — called once from load_object_fill_header, after
|
||||
* obInit() has run — redirects every model entry's hw_address into the
|
||||
* extension region and sets rom_size to the PC compressed size, so the
|
||||
* existing romCopy/decompressdata load path serves PC bytes unmodified.
|
||||
*/
|
||||
|
||||
/* Bytes to reserve immediately after the ROM (0 = no sidecar image found;
|
||||
* the game boots but model loads will fail). romImg is the validated ROM
|
||||
* image; the country byte at +0x3E selects the region directory. */
|
||||
uint32_t pcmodelsReserveSize(const uint8_t *romImg);
|
||||
|
||||
/* Copy the sidecar image to cartBase+romSize and parse the manifest.
|
||||
* Returns 1 on success. */
|
||||
int pcmodelsLoadSidecars(uintptr_t cartBase, uint32_t romSize);
|
||||
|
||||
/* One-shot patch of file_resource_table + resource_lookup_data_array for
|
||||
* every manifest row. Must run after obInit() (obInit derives rom_size from
|
||||
* adjacent hw_address deltas). No-op if the sidecar image is absent. */
|
||||
void pcmodelsPatchTable(void);
|
||||
|
||||
uint32_t pcmodelsTotalSize(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PORT_PCMODELS_H */
|
||||
@@ -61,6 +61,25 @@ void romdataFixupAnimationData(u8 *blob, u32 blobSize,
|
||||
*/
|
||||
void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize);
|
||||
|
||||
/*
|
||||
* D50 (docs/PCPortResearch.md): decode a language bank's big-endian offset
|
||||
* table in place ([u32 offsets x N][text]; see romdata.c). blobSize is the
|
||||
* decompressed bank size (resource_lookup_data_array[].poolRemaining).
|
||||
*/
|
||||
void romdataFixupLangBank(u8 *blob, u32 blobSize);
|
||||
|
||||
/*
|
||||
* D50 (docs/PCPortResearch.md): re-lay out a font segment (struct font:
|
||||
* kerning[169] + chars[94] + glyph pixel data) from the N64 ROM layout to
|
||||
* the PC C layout, shifting the pixel block below the expanded char array.
|
||||
* src/n64Size locate the N64 image (ROM segment); the caller allocates
|
||||
* romdataFontPcSize() bytes, romCopies n64Size of them, then calls
|
||||
* romdataFixupFont(). pixeldata fields are left as relative offsets —
|
||||
* load_font_tables()'s `pixeldata += base` loop promotes them.
|
||||
*/
|
||||
u32 romdataFontPcSize(const u8 *src, u32 n64Size);
|
||||
void romdataFixupFont(u8 *blob, u32 n64Size);
|
||||
|
||||
/*
|
||||
* D37 (docs/PCPortResearch.md): the libaudio SFX/instrument bank segments
|
||||
* (ALBankFile trees: ALBank -> ALInstrument -> ALSound -> ALWaveTable /
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* PC-layout model sidecars (D50 / Plan B, D48/D49).
|
||||
*
|
||||
* Serves the offline-converted model files produced by tools_pc/d43_emit.py:
|
||||
* data/pcmodels-<region>/pcmodels.bin single concatenated RZ image
|
||||
* data/pcmodels-<region>/manifest.csv name,offset,size (decimal)
|
||||
*
|
||||
* The ROM is mapped at CART_BASE (romdata.c); the sidecar image is placed in
|
||||
* a reservation extension at [CART_BASE+romSize, ...). romCopy() on PC is a
|
||||
* host memcpy gated only by romdataCartAddrValid(), so once every model
|
||||
* entry's hw_address points into the extension region and rom_size holds the
|
||||
* PC compressed size, the existing load_resource()/decompressdata() path
|
||||
* serves PC-layout bytes unmodified (and sets poolRemaining = D_PC).
|
||||
*
|
||||
* The table patch must run AFTER obInit(): obInit derives rom_size from
|
||||
* adjacent-entry hw_address deltas (ob.c), so patching earlier would be
|
||||
* overwritten. load_object_fill_header calls pcmodelsPatchTable() on every
|
||||
* model load; it is a one-shot no-op after the first.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* D38: <stdlib.h>/<string.h> resolve to the decomp's N64 stubs via the
|
||||
* include path; declare the host functions this file uses. */
|
||||
extern void *malloc(unsigned long long size);
|
||||
extern int snprintf(char *str, unsigned long long maxsize, const char *format, ...);
|
||||
|
||||
#include "platform.h"
|
||||
#include "system.h"
|
||||
#include "fs.h"
|
||||
#include "pcmodels.h"
|
||||
|
||||
/* Order matters: <ultra64.h> must finish first — its PR/ucode.h anchor pulls
|
||||
* in pc_protos.h, which itself includes game/ob.h. Including ob.h before
|
||||
* ultra64.h leaves FILELOADMETHOD undefined mid-parse (circular guard). */
|
||||
#include <ultra64.h> /* u8/u32/s32 + full PR chain */
|
||||
#include "game/ob.h" /* fileentry, resource_lookup_data_entry */
|
||||
|
||||
/* Defined in src/game/ob.c (the table is included there from
|
||||
* assets/obseg/file_resource_table.inc.c). Declared here rather than in ob.h
|
||||
* so the game header stays untouched. */
|
||||
extern struct fileentry file_resource_table[];
|
||||
extern resource_lookup_data_entry resource_lookup_data_array[];
|
||||
extern s32 file_entry_max;
|
||||
|
||||
#define PCMODES_CART_BASE 0x10000000u
|
||||
#define PCMODES_MAX_FILES 1024
|
||||
#define PCMODES_NAME_LEN 64
|
||||
|
||||
struct pcmodelsRow {
|
||||
char name[PCMODES_NAME_LEN];
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
};
|
||||
|
||||
static uint32_t s_total = 0;
|
||||
static int s_loaded = 0;
|
||||
static int s_patched = 0;
|
||||
static uintptr_t s_base = 0; /* cart address of the sidecar image */
|
||||
static struct pcmodelsRow s_rows[PCMODES_MAX_FILES];
|
||||
static int s_rowCount = 0;
|
||||
static char s_binPath[1024] = "";
|
||||
static char s_manPath[1024] = "";
|
||||
|
||||
static const char *pcmodelsRegionForCountry(u8 country)
|
||||
{
|
||||
switch (country) {
|
||||
case 'E': return "ntsc-final";
|
||||
case 'P': return "pal-final";
|
||||
case 'J': return "jpn-final";
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Try $S/, $E/, ./ for <rel>; on success copy the resolved path into out. */
|
||||
static int pcmodelsFindDataFile(const char *rel, char *out, size_t outsz)
|
||||
{
|
||||
static const char *locs[3] = { "$S/", "$E/", "./" };
|
||||
int i;
|
||||
for (i = 0; i < 3; i++) {
|
||||
char pathbuf[1024];
|
||||
snprintf(pathbuf, sizeof(pathbuf), "%s%s", locs[i], rel);
|
||||
if (fsExists(sysResolvePath(pathbuf))) {
|
||||
snprintf(out, outsz, "%s", sysResolvePath(pathbuf));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t pcmodelsReserveSize(const uint8_t *romImg)
|
||||
{
|
||||
const char *region = pcmodelsRegionForCountry(romImg[0x3E]);
|
||||
char rel[256];
|
||||
|
||||
if (!region)
|
||||
return 0;
|
||||
|
||||
snprintf(rel, sizeof(rel), "pcmodels-%s/pcmodels.bin", region);
|
||||
if (!pcmodelsFindDataFile(rel, s_binPath, sizeof(s_binPath))) {
|
||||
sysLogPrintf(LOG_WARNING,
|
||||
"pcmodels: %s not found — model loads will fail; run "
|
||||
"\"python tools_pc/d43_emit.py\" (see docs/HANDOFF.md)",
|
||||
rel);
|
||||
return 0;
|
||||
}
|
||||
|
||||
snprintf(rel, sizeof(rel), "pcmodels-%s/manifest.csv", region);
|
||||
if (!pcmodelsFindDataFile(rel, s_manPath, sizeof(s_manPath))) {
|
||||
sysLogPrintf(LOG_ERROR, "pcmodels: manifest.csv missing next to %s",
|
||||
s_binPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
FSFile *f = fsOpen(s_binPath, "rb");
|
||||
if (!f) {
|
||||
sysLogPrintf(LOG_ERROR, "pcmodels: cannot open %s", s_binPath);
|
||||
return 0;
|
||||
}
|
||||
uint32_t size = (uint32_t)fsSize(f);
|
||||
fsClose(f);
|
||||
s_total = size;
|
||||
return size;
|
||||
}
|
||||
|
||||
int pcmodelsLoadSidecars(uintptr_t cartBase, uint32_t romSize)
|
||||
{
|
||||
FSFile *f;
|
||||
int ok;
|
||||
|
||||
if (!s_total)
|
||||
return 0;
|
||||
|
||||
f = fsOpen(s_binPath, "rb");
|
||||
if (!f)
|
||||
return 0;
|
||||
s_base = cartBase + romSize;
|
||||
ok = fsRead(f, (void *)s_base, (int32_t)s_total) == (int32_t)s_total;
|
||||
fsClose(f);
|
||||
if (!ok) {
|
||||
sysLogPrintf(LOG_ERROR, "pcmodels: short read of %s", s_binPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Parse the manifest (one-shot data for pcmodelsPatchTable). */
|
||||
f = fsOpen(s_manPath, "rb");
|
||||
if (!f) {
|
||||
sysLogPrintf(LOG_ERROR, "pcmodels: cannot open %s", s_manPath);
|
||||
return 0;
|
||||
}
|
||||
int32_t msize = fsSize(f);
|
||||
char *buf = (char *)malloc((unsigned long long)(msize > 0 ? msize + 1 : 1));
|
||||
if (!buf) {
|
||||
fsClose(f);
|
||||
return 0;
|
||||
}
|
||||
int32_t nread = fsRead(f, buf, msize);
|
||||
fsClose(f);
|
||||
if (nread <= 0) {
|
||||
free(buf);
|
||||
sysLogPrintf(LOG_ERROR, "pcmodels: empty manifest %s", s_manPath);
|
||||
return 0;
|
||||
}
|
||||
buf[nread] = 0;
|
||||
|
||||
for (char *line = strtok(buf, "\r\n"); line;
|
||||
line = strtok(NULL, "\r\n")) {
|
||||
char *c1 = strchr(line, ',');
|
||||
char *c2;
|
||||
if (!c1 || !strcmp(line, "name,offset,size"))
|
||||
continue;
|
||||
*c1 = 0;
|
||||
c2 = strchr(c1 + 1, ',');
|
||||
if (!c2)
|
||||
continue;
|
||||
*c2 = 0;
|
||||
if (s_rowCount >= PCMODES_MAX_FILES) {
|
||||
sysLogPrintf(LOG_ERROR, "pcmodels: manifest exceeds %d rows",
|
||||
PCMODES_MAX_FILES);
|
||||
break;
|
||||
}
|
||||
struct pcmodelsRow *r = &s_rows[s_rowCount];
|
||||
snprintf(r->name, sizeof(r->name), "%s", line);
|
||||
r->offset = (uint32_t)strtol(c1 + 1, NULL, 10);
|
||||
r->size = (uint32_t)strtol(c2 + 1, NULL, 10);
|
||||
s_rowCount++;
|
||||
}
|
||||
free(buf);
|
||||
|
||||
if (!s_rowCount) {
|
||||
sysLogPrintf(LOG_ERROR, "pcmodels: no rows parsed from %s", s_manPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
s_loaded = 1;
|
||||
sysLogPrintf(LOG_INFO, "pcmodels: %d sidecars (%u bytes) at 0x%llX",
|
||||
s_rowCount, s_total, (unsigned long long)s_base);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void pcmodelsPatchTable(void)
|
||||
{
|
||||
int i, t, hit;
|
||||
|
||||
if (s_patched || !s_loaded)
|
||||
return;
|
||||
s_patched = 1;
|
||||
|
||||
hit = 0;
|
||||
for (i = 0; i < s_rowCount; i++) {
|
||||
const struct pcmodelsRow *r = &s_rows[i];
|
||||
for (t = 1; t < file_entry_max; t++) {
|
||||
if (file_resource_table[t].filename &&
|
||||
strcmp(file_resource_table[t].filename, r->name) == 0) {
|
||||
file_resource_table[t].hw_address =
|
||||
(u8 *)(s_base + r->offset);
|
||||
resource_lookup_data_array[t].rom_size = r->size;
|
||||
hit++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hit != s_rowCount)
|
||||
sysLogPrintf(LOG_WARNING,
|
||||
"pcmodels: patched %d/%d manifest rows (table mismatch?)",
|
||||
hit, s_rowCount);
|
||||
else
|
||||
sysLogPrintf(LOG_INFO, "pcmodels: table patched (%d model entries)",
|
||||
hit);
|
||||
}
|
||||
|
||||
uint32_t pcmodelsTotalSize(void)
|
||||
{
|
||||
return s_loaded ? s_total : 0;
|
||||
}
|
||||
+119
-3
@@ -37,6 +37,12 @@ extern int snprintf(char *str, unsigned long long maxsize, const char *format, .
|
||||
#include "system.h"
|
||||
#include "fs.h"
|
||||
#include "romdata.h"
|
||||
#include "pcmodels.h"
|
||||
|
||||
/* D50: struct font/fontchar layouts for the font-segment re-layout. Order
|
||||
* matters (see pcmodels.c): ultra64.h must finish before bondtypes.h. */
|
||||
#include <ultra64.h>
|
||||
#include "bondtypes.h"
|
||||
|
||||
#define CART_BASE 0x10000000u
|
||||
|
||||
@@ -151,16 +157,24 @@ int romdataInit(void)
|
||||
continue;
|
||||
}
|
||||
|
||||
/* D50: reserve room for the PC model sidecar image right after the
|
||||
* ROM ([CART_BASE+romSize, ...)); it is loaded below and patched into
|
||||
* file_resource_table after obInit. */
|
||||
u32 sideTotal = pcmodelsReserveSize(img);
|
||||
|
||||
/* Map at the cart address so absolute asset symbols are live. */
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
void *at = VirtualAlloc((LPVOID)CART_BASE, romSize,
|
||||
void *at = VirtualAlloc((LPVOID)CART_BASE, romSize + sideTotal,
|
||||
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
if (at == (void *)(uintptr_t)CART_BASE) {
|
||||
memcpy(at, img, romSize);
|
||||
free(img);
|
||||
mappedAtCartBase = 1;
|
||||
sysLogPrintf(LOG_INFO, "romdataInit: %s (%u bytes) mapped at "
|
||||
"0x%08X (cart base)", tok, romSize, CART_BASE);
|
||||
"0x%08X (cart base)%s",
|
||||
tok, romSize, CART_BASE,
|
||||
sideTotal ? ", + model sidecars" : "");
|
||||
pcmodelsLoadSidecars(CART_BASE, romSize);
|
||||
return 0;
|
||||
}
|
||||
if (at)
|
||||
@@ -216,7 +230,8 @@ int romdataCartAddrValid(u32 addr, u32 size)
|
||||
if (addr < CART_BASE)
|
||||
return 0;
|
||||
u32 off = addr - CART_BASE;
|
||||
return off + size <= romSize;
|
||||
/* D50: the model sidecar image extends the valid cart region. */
|
||||
return off + size <= romSize + pcmodelsTotalSize();
|
||||
}
|
||||
|
||||
const void *romdataMapVa(u32 va)
|
||||
@@ -259,6 +274,10 @@ static u32 romdataBswap32(u32 v)
|
||||
((v & 0x00FF0000u) >> 8) | ((v & 0xFF000000u) >> 24);
|
||||
}
|
||||
|
||||
/* The port/shim <stddef.h> is the N64 stub for C TUs (no offsetof); this is
|
||||
* the equivalent null-cast idiom. */
|
||||
#define ROMDATA_OFFSEXP(T, M) ((u32)&(((T *)0)->M))
|
||||
|
||||
static u16 romdataBswap16(u16 v)
|
||||
{
|
||||
return (u16)((v << 8) | (v >> 8));
|
||||
@@ -353,6 +372,103 @@ void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* D50 (docs/PCPortResearch.md): a language bank is [u32 offsets x N][text].
|
||||
* Offsets are big-endian and point into the text region; langGet() reads them
|
||||
* little-endian on PC and would build garbage pointers. Scan from the front
|
||||
* and bswap word i while bswap32(word_i) is a plausible offset (strictly past
|
||||
* its own position, before the end of the blob). Zero entries stay zero
|
||||
* ("string not loaded" — langGet returns NULL). The scan stops at the first
|
||||
* text word: printable ASCII byte-swaps to >= 0x20000000, always >= blobSize
|
||||
* (banks are < 64 KB). All-zero placeholder banks (e.g. LameE) bswap nothing.
|
||||
*/
|
||||
void romdataFixupLangBank(u8 *blob, u32 blobSize)
|
||||
{
|
||||
u32 i = 0;
|
||||
while (i * 4 + 4 <= blobSize) {
|
||||
u32 v = romdataBswap32(*(const u32 *)(blob + i * 4));
|
||||
if (v != 0 && (v <= i * 4 || v >= blobSize))
|
||||
break;
|
||||
*(u32 *)(blob + i * 4) = v;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* D50 (docs/PCPortResearch.md): font segments (Zurich Bold, Bank Gothic)
|
||||
* serialize `struct font` in the N64 layout — big-endian scalars, 4-byte
|
||||
* pointers:
|
||||
* [s32 kerning x 169][fontchar x 94 @ stride 24][glyph pixel data]
|
||||
* where fontchar = {s32 index, baseline, height, width, kerningindex;
|
||||
* u32 pixeldata-offset-from-font-base}. The PC C layout is
|
||||
* {5 s32; u64 pixeldata} @ stride sizeof(fontchar) with chars at
|
||||
* ROMDATA_OFFSEXP(struct font, chars) — and that region overlaps the start of the
|
||||
* pixel data in ROM (chars end 0xB74, pixels start 0xB80, PC chars end
|
||||
* 0xE78), so the re-layout must also shift the pixel data down.
|
||||
*
|
||||
* Transform: bswap kerning in place; memmove the pixel block from
|
||||
* [pixStart, n64Size) to [pcCharsEnd, ...); convert each char entry
|
||||
* (backward — PC stride 32 > N64 stride 24, dst always past src) with
|
||||
* pixeldata stored as a zero-extended RELATIVE offset into the new image,
|
||||
* so load_font_tables()'s existing `pixeldata += base` loop promotes it
|
||||
* (N64 pattern, D37-style). The caller must allocate romdataFontPcSize()
|
||||
* bytes and romCopy only n64Size of them.
|
||||
*/
|
||||
/* src is the N64-layout image (the ROM segment); n64Size its size. */
|
||||
u32 romdataFontPcSize(const u8 *src, u32 n64Size)
|
||||
{
|
||||
enum { N64_COFF = 169 * 4, N64_CSTRIDE = 24, NCHARS = 94 };
|
||||
u32 i, n, pixStart = 0;
|
||||
|
||||
if (n64Size <= N64_COFF)
|
||||
return n64Size;
|
||||
n = (n64Size - N64_COFF) / N64_CSTRIDE;
|
||||
if (n > NCHARS)
|
||||
n = NCHARS;
|
||||
for (i = 0; i < n; i++) {
|
||||
u32 o = romdataBswap32(*(const u32 *)(src + N64_COFF + N64_CSTRIDE * i + 20));
|
||||
if (o != 0 && (pixStart == 0 || o < pixStart))
|
||||
pixStart = o;
|
||||
}
|
||||
return (u32)(ROMDATA_OFFSEXP(struct font, chars) + NCHARS * sizeof(struct fontchar))
|
||||
+ n64Size - pixStart;
|
||||
}
|
||||
|
||||
void romdataFixupFont(u8 *blob, u32 n64Size)
|
||||
{
|
||||
enum { N64_KERN = 169, N64_COFF = 169 * 4, N64_CSTRIDE = 24, NCHARS = 94 };
|
||||
u32 i, n, pixStart = 0, pcPixOff;
|
||||
|
||||
for (i = 0; i < N64_KERN; i++)
|
||||
*(u32 *)(blob + 4 * i) = romdataBswap32(*(const u32 *)(blob + 4 * i));
|
||||
|
||||
n = (n64Size - N64_COFF) / N64_CSTRIDE;
|
||||
if (n > NCHARS)
|
||||
n = NCHARS;
|
||||
for (i = 0; i < n; i++) {
|
||||
u32 o = romdataBswap32(*(const u32 *)(blob + N64_COFF + N64_CSTRIDE * i + 20));
|
||||
if (o != 0 && (pixStart == 0 || o < pixStart))
|
||||
pixStart = o;
|
||||
}
|
||||
|
||||
pcPixOff = (u32)(ROMDATA_OFFSEXP(struct font, chars) + NCHARS * sizeof(struct fontchar));
|
||||
if (pixStart != 0 && pixStart < n64Size)
|
||||
memmove(blob + pcPixOff, blob + pixStart, n64Size - pixStart);
|
||||
|
||||
for (i = n; i-- > 0;) {
|
||||
const u8 *s = blob + N64_COFF + N64_CSTRIDE * i;
|
||||
u8 *d = blob + ROMDATA_OFFSEXP(struct font, chars) + sizeof(struct fontchar) * i;
|
||||
u32 k, o;
|
||||
for (k = 0; k < 5; k++)
|
||||
*(u32 *)(d + 4 * k) = romdataBswap32(*(const u32 *)(s + 4 * k));
|
||||
o = romdataBswap32(*(const u32 *)(s + 20));
|
||||
if (o != 0 && pixStart != 0 && o >= pixStart)
|
||||
o = pcPixOff + (o - pixStart);
|
||||
*(u32 *)(d + 20) = o; /* zero-extended relative offset */
|
||||
*(u32 *)(d + 24) = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* D37 (docs/PCPortResearch.md): convert a ROM-serialized libaudio bank tree
|
||||
* (ALBankFile -> ALBank -> ALInstrument -> ALSound -> ALWaveTable /
|
||||
|
||||
@@ -1506,7 +1506,18 @@ Gfx *constructor_menu00_legalscreen(Gfx *DL)
|
||||
s32 i;
|
||||
u8 *txt;
|
||||
Mtxf tmpmtx;
|
||||
#ifdef PORT
|
||||
/* PC port (D50): on N64 this pointer is used uninitialized at the lookat
|
||||
* call below — the R4300 register happens to hold a readable address and
|
||||
* the result is zeroed by `* 0.0f` anyway. The x86-64 compiler folds the
|
||||
* UB read to NULL (cvtsi2ssl 0x4) and faults. Seeding it with the array
|
||||
* the function assigns a few lines later keeps the value deterministic;
|
||||
* v_pos is s32 so `v_pos * 0.0f` is ±0.0f either way — identical lookat.
|
||||
* N64 build untouched (no -DPORT). */
|
||||
struct legal_screen_text *legal_text_ptr = legalpage_text_array;
|
||||
#else
|
||||
struct legal_screen_text *legal_text_ptr;
|
||||
#endif
|
||||
|
||||
renderdata = legalscreen_MRD;
|
||||
|
||||
|
||||
@@ -4,6 +4,20 @@
|
||||
#include "language.h"
|
||||
#include "ob.h"
|
||||
|
||||
#ifdef PORT
|
||||
/* D50: language banks carry a big-endian offset table; decode it in place
|
||||
* after each load (see romdataFixupLangBank). */
|
||||
#include "romdata.h"
|
||||
extern resource_lookup_data_entry resource_lookup_data_array[]; /* ob.c */
|
||||
|
||||
static void langFixupLoadedBank(char *name, void *p)
|
||||
{
|
||||
s32 idx = fileGetIndex(name);
|
||||
if (idx >= 0 && resource_lookup_data_array[idx].poolRemaining != 0)
|
||||
romdataFixupLangBank((u8 *)p, resource_lookup_data_array[idx].poolRemaining);
|
||||
}
|
||||
#endif
|
||||
|
||||
// bss
|
||||
//CODE.bss:8008C640
|
||||
s32 g_LangBanks[45];
|
||||
@@ -249,6 +263,15 @@ void langInit(void) {
|
||||
g_LangBanks[LMPWEAPONS] = _fileNameLoadToBank(LnameX_lookuptable[LMPWEAPONS][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT);
|
||||
g_LangBanks[LOPTIONS] = _fileNameLoadToBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT);
|
||||
g_LangBanks[LMISC] = _fileNameLoadToBank(LnameX_lookuptable[LMISC][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT);
|
||||
#ifdef PORT
|
||||
langFixupLoadedBank(LnameX_lookuptable[LGUN][j_text_trigger], (void *)g_LangBanks[LGUN]);
|
||||
langFixupLoadedBank(LnameX_lookuptable[LTITLE][j_text_trigger], (void *)g_LangBanks[LTITLE]);
|
||||
langFixupLoadedBank(LnameX_lookuptable[LMPMENU][j_text_trigger], (void *)g_LangBanks[LMPMENU]);
|
||||
langFixupLoadedBank(LnameX_lookuptable[LPROPOBJ][j_text_trigger], (void *)g_LangBanks[LPROPOBJ]);
|
||||
langFixupLoadedBank(LnameX_lookuptable[LMPWEAPONS][j_text_trigger], (void *)g_LangBanks[LMPWEAPONS]);
|
||||
langFixupLoadedBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], (void *)g_LangBanks[LOPTIONS]);
|
||||
langFixupLoadedBank(LnameX_lookuptable[LMISC][j_text_trigger], (void *)g_LangBanks[LMISC]);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -354,12 +377,18 @@ struct jpncharpixels *langGetJpnCharPixels(s32 codepoint)
|
||||
void langLoadToAddr(u32 id)
|
||||
{
|
||||
g_LangBanks[id] = _fileNameLoadToBank(LnameX_lookuptable[id][j_text_trigger],1,0x100,MEMPOOL_STAGE);
|
||||
#ifdef PORT
|
||||
langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], (void *)g_LangBanks[id]);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void langLoadToBank(int id,u8 *target,int size)
|
||||
{
|
||||
g_LangBanks[id] = _fileNameLoadToAddr(LnameX_lookuptable[id][j_text_trigger],1,target,size);
|
||||
#ifdef PORT
|
||||
langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], (void *)g_LangBanks[id]);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
#include "quaternion.h"
|
||||
#include "tex.h"
|
||||
|
||||
#ifdef PORT
|
||||
#include "pcmodels.h" /* D50: PC-layout model sidecars (Plan B, D48/D49) */
|
||||
extern resource_lookup_data_entry resource_lookup_data_array[]; /* ob.c */
|
||||
#endif
|
||||
|
||||
|
||||
/***
|
||||
* Perfect Dark:
|
||||
@@ -90,6 +95,22 @@ void load_object_fill_header(struct ModelFileHeader *objheader, u8 *name, u8* ds
|
||||
{
|
||||
void *filedata;
|
||||
|
||||
#ifdef PORT
|
||||
/* PC port (D50, Plan B D48/D49): serve PC-layout model sidecars from the
|
||||
* cart extension region. One-shot table patch (safe to call repeatedly);
|
||||
* by first model load obInit() has definitely run (boss.c:179). Must run
|
||||
* before _fileNameLoadTo* below reads hw_address/rom_size. */
|
||||
pcmodelsPatchTable();
|
||||
if (dst == 0) {
|
||||
/* D48.3: a stale poolRemaining from an earlier load of this file
|
||||
* would under-allocate the fresh bank chunk (P_old < round8(C_pc)+8)
|
||||
* and load_resource would fail with poolRemaining=0. Resetting to 0
|
||||
* forces the full-bank allocation; the bump allocator ends at the
|
||||
* same cursor either way (mempAddEntryOfSizeToBank shrinks it back). */
|
||||
resource_lookup_data_array[fileGetIndex((char *)name)].poolRemaining = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (dst != 0)
|
||||
{
|
||||
filedata = _fileNameLoadToAddr(name, 0, dst, size);
|
||||
|
||||
@@ -1072,7 +1072,16 @@ void texCopyGdls(Gfx *arg0, Gfx *arg1, s32 arg2)
|
||||
|
||||
while (arg2--)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* PC port (D50): on x86-64 the trailing `long long` union member spans
|
||||
* only the first 8 of 16 slot bytes; assigning it leaves w1 stale.
|
||||
* texLoadFromGdl() then reads the half-copied scratch copy and
|
||||
* propagates garbage w1s into the final GDLs (seg_addr -> OOB read in
|
||||
* import_texture_rgba16). Copy the whole 16-byte slot. */
|
||||
*arg1 = *arg0;
|
||||
#else
|
||||
arg1->force_structure_alignment = arg0->force_structure_alignment;
|
||||
#endif
|
||||
arg1--;
|
||||
arg0--;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
#include "bondtypes.h"
|
||||
#include "game/language.h"
|
||||
|
||||
#ifdef PORT
|
||||
/* D50: font segments are re-laid out from the N64 ROM layout to the PC C
|
||||
* layout (see romdataFixupFont). */
|
||||
#include "romdata.h"
|
||||
#endif
|
||||
|
||||
#define SPACE_WIDTH 5
|
||||
|
||||
#define M_COLOR_R(x) (u8)(x >> 0x18)
|
||||
@@ -108,22 +114,46 @@ void load_font_tables(void)
|
||||
text_s = 0;
|
||||
text_t = 0;
|
||||
|
||||
len = (romptr_t)&_fontbankgothicSegmentEnd - (romptr_t)&_fontbankgothicSegmentStart;
|
||||
#ifdef PORT
|
||||
{
|
||||
u32 n64len = len;
|
||||
ptrFontBankGothic = (struct font *)mempAllocBytesInBank(romdataFontPcSize((const u8 *)&_fontbankgothicSegmentRomStart, n64len), MEMPOOL_STAGE);
|
||||
ptrFontBankGothicChars = ptrFontBankGothic->chars;
|
||||
|
||||
romCopy(ptrFontBankGothic, (void *) &_fontbankgothicSegmentRomStart, n64len);
|
||||
romdataFixupFont((u8 *)ptrFontBankGothic, n64len);
|
||||
}
|
||||
#else
|
||||
len = (romptr_t)&_fontbankgothicSegmentEnd - (romptr_t)&_fontbankgothicSegmentStart;
|
||||
ptrFontBankGothic = (struct font *)mempAllocBytesInBank(len, MEMPOOL_STAGE);
|
||||
ptrFontBankGothicChars = ptrFontBankGothic->chars;
|
||||
|
||||
romCopy(ptrFontBankGothic, (void *) &_fontbankgothicSegmentRomStart, len);
|
||||
#endif
|
||||
|
||||
// Convert pointers
|
||||
for (i = 0; i < 94; i++) {
|
||||
ptrFontBankGothicChars[i].pixeldata += (uintptr_t)ptrFontBankGothic;
|
||||
}
|
||||
|
||||
len = (romptr_t)&_fontzurichboldSegmentEnd - (romptr_t)&_fontzurichboldSegmentStart;
|
||||
#ifdef PORT
|
||||
{
|
||||
u32 n64len = len;
|
||||
ptrFontZurichBold = (struct font *)mempAllocBytesInBank(romdataFontPcSize((const u8 *)&_fontzurichboldSegmentRomStart, n64len), MEMPOOL_STAGE);
|
||||
ptrFontZurichBoldChars = ptrFontZurichBold->chars;
|
||||
|
||||
romCopy(ptrFontZurichBold, (void *) &_fontzurichboldSegmentRomStart, n64len);
|
||||
romdataFixupFont((u8 *)ptrFontZurichBold, n64len);
|
||||
}
|
||||
#else
|
||||
len = (romptr_t)&_fontzurichboldSegmentEnd - (romptr_t)&_fontzurichboldSegmentStart;
|
||||
ptrFontZurichBold = (struct font *)mempAllocBytesInBank(len, MEMPOOL_STAGE);
|
||||
ptrFontZurichBoldChars = ptrFontZurichBold->chars;
|
||||
|
||||
romCopy(ptrFontZurichBold, (void *) &_fontzurichboldSegmentRomStart, len);
|
||||
#endif
|
||||
|
||||
// Convert pointers
|
||||
for (i = 0; i < 94; i++) {
|
||||
|
||||
@@ -0,0 +1,956 @@
|
||||
#!/usr/bin/env python3
|
||||
"""D50 / Plan B (D48/D49): offline emit pass — convert all N64 model files to
|
||||
PC layout and write RZ-compressed sidecars + manifest.csv.
|
||||
|
||||
Per file:
|
||||
1. decompress the N64 image from the ROM (filelist.u.csv row)
|
||||
2. build mutable node map; run the EXACT modelIterateDisplayLists visit
|
||||
simulation (d43_gdlorder logic: LOD/SWITCH rewire, BSP splice) -> gdl_seq
|
||||
3. layout pass (d43_convert placement): [switches NS*8][texconfigs NT*12]
|
||||
[DFS nodes 48B + records PC_REC + vtx arrays][zero-vtx][GDLs packed in
|
||||
gdl_seq order, 16B per N64 slot] -> D_PC, region map
|
||||
4. emit pass: byte-exact field tables (bswap32/16, promoted ptrs -> u64
|
||||
zero-ext 0x05xxxxxx VMA, GDL Primary/Secondary raw VMAs, BaseAddr=0)
|
||||
5. round-trip validation: re-parse the emitted image and check every field
|
||||
against the N64 source (inverse remap), plus region tiling (no gaps)
|
||||
6. compress: 0x11 0x72 + raw deflate (level 6) -> C_PC
|
||||
|
||||
Cross-checks (all must pass):
|
||||
- per-file dst!=0 buffers: round8(C)+8 <= buf AND D+round8(C) <= buf
|
||||
(inflate overlap safety); N64 sanity version of the same checks
|
||||
- G* hand weapons worst-case vs 0xF000
|
||||
- chains (cast / title / bondview): cumulative P_final bound
|
||||
B_pc + 2*(R_share_N64 - B_n64) per slot, worst over candidate sets
|
||||
- totals: sum round8(C), max C, max D
|
||||
|
||||
Output: data/pcmodels-<region>/pcmodels.bin (single concatenated image,
|
||||
sidecars at 16-aligned offsets) + manifest.csv (name,offset,size decimal;
|
||||
file_resource_table.inc.c order). The port layer (port/src/pcmodels.c)
|
||||
copies pcmodels.bin to [CART_BASE+romSize, ...) and patches
|
||||
file_resource_table[i].hw_address / rom_size per manifest row.
|
||||
"""
|
||||
import csv, struct, zlib, os, re, sys, bisect
|
||||
from collections import Counter
|
||||
|
||||
# ---------------------------------------------------------------- config ---
|
||||
REGION = "ntsc-final"
|
||||
CHECK_ONLY = False
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == "--check-only":
|
||||
CHECK_ONLY = True
|
||||
elif sys.argv[1] in ("ntsc-final", "pal-final", "jpn-final"):
|
||||
REGION = sys.argv[1]
|
||||
|
||||
ROM_PATH = f"data/ge007.{REGION}.z64"
|
||||
OUT_DIR = f"data/pcmodels-{REGION}"
|
||||
TABLE = "assets/obseg/file_resource_table.inc.c"
|
||||
FILELIST = "scripts/filelist.u.csv"
|
||||
|
||||
rom = open(ROM_PATH, "rb").read()
|
||||
rows = list(csv.reader(open(FILELIST)))
|
||||
|
||||
# nsnt from assets/**/modelFileHeader.inc.c (case-insensitive match)
|
||||
nsnt = {}
|
||||
for root, dirs, files in os.walk("assets"):
|
||||
for fn in files:
|
||||
if fn.lower().endswith("modelfileheader.inc.c"):
|
||||
for line in open(os.path.join(root, fn)):
|
||||
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
|
||||
|
||||
# file_resource_table order (manifest order) + which entries are model files
|
||||
table_names = []
|
||||
for line in open(TABLE):
|
||||
m = re.search(r'\{\s*\w+\s*,\s*"([^"]+)"', line)
|
||||
if m:
|
||||
table_names.append(m.group(1))
|
||||
|
||||
def is_model_name(n):
|
||||
return n and n[0] in "CGP" and n.endswith("Z") and "_stan" not in n
|
||||
|
||||
# filelist lookup by basename (with .bin stripped)
|
||||
fl_by_base = {}
|
||||
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]
|
||||
fl_by_base[base] = (int(r[0]), int(r[1]))
|
||||
|
||||
def find_row(name):
|
||||
for cand in (name, name[1:], name[:-1], name[1:-1]):
|
||||
if cand in fl_by_base:
|
||||
return cand, fl_by_base[cand]
|
||||
return None, None
|
||||
|
||||
# ---------------------------------------------------------------- helpers ---
|
||||
MASK = 0xFFFFFF
|
||||
def be16(b, o): return struct.unpack_from(">h", b, o)[0]
|
||||
def bu16(b, o): return struct.unpack_from(">H", b, o)[0]
|
||||
def be32o(b, o): return struct.unpack_from(">I", b, o)[0] & MASK
|
||||
def be32r(b, o): return struct.unpack_from(">I", b, o)[0]
|
||||
|
||||
N64_REC = {1: 0x1C, 2: 0x1C, 3: 0x1C, 4: 0x14, 8: 0x10, 9: 0x24, 10: 0x1C,
|
||||
12: 0x28, 13: 0x20, 15: 0x1C, 17: 0x20, 18: 0x08, 21: 0x14,
|
||||
22: 0x10, 23: 0x02, 24: 0x20}
|
||||
PC_REC = {1: 24, 2: 40, 3: 40, 4: 40, 8: 24, 9: 48, 10: 28, 12: 48,
|
||||
13: 48, 15: 28, 18: 16, 21: 20, 22: 32, 23: 2, 24: 64}
|
||||
PC_NODE = 48
|
||||
# GDL w1 remap set: only these opcodes carry file-relative seg-5 addresses in
|
||||
# w1. Verified against the ROM (all 512 files): G_VTX w1 is 0x04xxxxxx (seg 4 =
|
||||
# runtime vtx buffer, game-set — leave) or 0x05xxxxxx (file-relative — remap);
|
||||
# G_SETTIMG w1 is 0x05xxxxxx (embedded image blob — remap). G_LOADBLOCK (0xF3)
|
||||
# w1=0x07xxxxxx is ignored by port/fast3d (fixed dpLoadBlock) — leave. All
|
||||
# other opcodes carry index/mode data in w1 (GE redefined the immediate range:
|
||||
# 0xB1 is a triangle command with 8-bit indices, NOT G_DL) — leave.
|
||||
ADDR_OPS = {0x04, 0xFD}
|
||||
|
||||
def round8(x): return (x + 7) & ~7
|
||||
def round16(x): return (x + 15) & ~15
|
||||
def round64(x): return (x + 63) & ~63
|
||||
|
||||
# ------------------------------------------------- node map + visit sim ----
|
||||
class N:
|
||||
__slots__ = ("op", "data", "parent", "next", "prev", "child")
|
||||
|
||||
def build_nodes(src, D, NS, NT):
|
||||
"""Mutable node map. Closure MUST match d43_convert.walk() exactly:
|
||||
op8/op18 follow their Affects/Controls pointer INSTEAD of child (their
|
||||
Child/Next fields are not tree links and lead into garbage)."""
|
||||
R0 = 4 * NS + 12 * NT
|
||||
nodes = {}
|
||||
stack = [R0]
|
||||
while stack:
|
||||
o = stack.pop()
|
||||
if o in nodes or o >= D:
|
||||
continue
|
||||
n = N()
|
||||
n.op = bu16(src, o) & 0xff
|
||||
n.data = be32o(src, o + 4)
|
||||
n.parent = be32o(src, o + 8)
|
||||
n.next = be32o(src, o + 0xC)
|
||||
n.prev = be32o(src, o + 0x10)
|
||||
n.child = be32o(src, o + 0x14)
|
||||
nodes[o] = n
|
||||
if n.op == 8:
|
||||
q = be32o(src, n.data + 8)
|
||||
if q:
|
||||
stack.append(q)
|
||||
elif n.op == 18:
|
||||
q = be32o(src, n.data + 0)
|
||||
if q:
|
||||
stack.append(q)
|
||||
else:
|
||||
if n.child:
|
||||
stack.append(n.child)
|
||||
if n.next:
|
||||
stack.append(n.next)
|
||||
return nodes, R0
|
||||
|
||||
def gdl_of(src, n):
|
||||
if n.op in (4, 24):
|
||||
return be32o(src, n.data + 0), be32o(src, n.data + 4)
|
||||
if n.op == 22:
|
||||
return be32o(src, n.data + 8), 0
|
||||
return 0, 0
|
||||
|
||||
def visit_seq(nodes, src, R0):
|
||||
"""EXACT modelIterateDisplayLists GDL visit order (mutable walk)."""
|
||||
seq = []
|
||||
node = R0
|
||||
prev_node = None
|
||||
prev_gdl = 0
|
||||
guard = 0
|
||||
while node and guard < 200000:
|
||||
guard += 1
|
||||
n = nodes[node]
|
||||
gdl = 0
|
||||
if n.op in (4, 22, 24):
|
||||
p, s = gdl_of(src, n)
|
||||
if node != prev_node:
|
||||
gdl = p
|
||||
elif s and s != prev_gdl:
|
||||
gdl = s
|
||||
elif n.op == 8: # LOD rewire
|
||||
aff = be32o(src, n.data + 8)
|
||||
n.child = aff
|
||||
elif n.op == 18: # SWITCH rewire
|
||||
ctl = be32o(src, n.data + 0)
|
||||
n.child = ctl
|
||||
elif n.op == 9: # BSP splice (visible=TRUE)
|
||||
lc, rc = be32o(src, n.data + 0x18), be32o(src, n.data + 0x1C)
|
||||
node1, node2 = lc, rc
|
||||
if node1:
|
||||
n.child = node1
|
||||
nodes[node1].prev = 0
|
||||
loop = node1
|
||||
while nodes[loop].next and nodes[loop].next != node2:
|
||||
loop = nodes[loop].next
|
||||
nodes[loop].next = node2
|
||||
if node2:
|
||||
nodes[node2].prev = loop
|
||||
loop = node2
|
||||
while nodes[loop].next and nodes[loop].next != node1:
|
||||
loop = nodes[loop].next
|
||||
nodes[loop].next = 0
|
||||
else:
|
||||
n.child = node2
|
||||
if node2:
|
||||
nodes[node2].prev = 0
|
||||
if gdl:
|
||||
seq.append(gdl)
|
||||
prev_node = node
|
||||
prev_gdl = gdl
|
||||
continue
|
||||
prev_node = None
|
||||
if n.child:
|
||||
node = n.child
|
||||
else:
|
||||
while node:
|
||||
nn = nodes[node].next
|
||||
if nn:
|
||||
node = nn
|
||||
break
|
||||
node = nodes[node].parent
|
||||
return seq
|
||||
|
||||
def placement_order(nodes, src, R0):
|
||||
"""d43_convert walk(): next-first DFS stack (node placement order only;
|
||||
GDL packing uses visit_seq). Returns [(n64_off, op)] in placement order."""
|
||||
seen = set()
|
||||
stack = [R0]
|
||||
out = []
|
||||
while stack:
|
||||
o = stack.pop()
|
||||
if o in seen or o >= len(src):
|
||||
continue
|
||||
seen.add(o)
|
||||
n = nodes[o]
|
||||
out.append((o, n.op))
|
||||
child, nxt = n.child, n.next
|
||||
if n.op == 8:
|
||||
a = be32o(src, n.data + 8)
|
||||
if a:
|
||||
stack.append(a)
|
||||
elif n.op == 18:
|
||||
c = be32o(src, n.data + 0)
|
||||
if c:
|
||||
stack.append(c)
|
||||
else:
|
||||
if child:
|
||||
stack.append(child)
|
||||
if nxt:
|
||||
stack.append(nxt)
|
||||
return out
|
||||
|
||||
def gdl_end(src, D, g):
|
||||
o = g
|
||||
while o + 8 <= D:
|
||||
if (be32r(src, o) >> 24) == 0xB8:
|
||||
return o + 8
|
||||
o += 8
|
||||
return D
|
||||
|
||||
# ------------------------------------------------------------- per file ----
|
||||
errors = []
|
||||
stats = Counter()
|
||||
results = {} # name -> dict
|
||||
|
||||
def process(name):
|
||||
key, (addr, size) = find_row(name)
|
||||
if not key:
|
||||
errors.append(f"{name}: no filelist row")
|
||||
return
|
||||
src = zlib.decompress(rom[addr:addr + size][2:], -15)
|
||||
D = len(src)
|
||||
# nsnt is keyed by header symbol (e.g. "armourguard"), not file base
|
||||
hkey = None
|
||||
for cand in (key, key[1:], key[:-1], key[1:-1]):
|
||||
if cand in nsnt:
|
||||
hkey = cand
|
||||
break
|
||||
if not hkey:
|
||||
errors.append(f"{name}: no header data")
|
||||
return
|
||||
NS, NT = nsnt[hkey]
|
||||
|
||||
nodes, R0 = build_nodes(src, D, NS, NT)
|
||||
if not nodes:
|
||||
errors.append(f"{name}: empty tree")
|
||||
return
|
||||
seq = visit_seq(nodes, src, R0) # GDL packing order
|
||||
placed = placement_order(nodes, src, R0) # object placement order
|
||||
|
||||
# ---- layout pass (d43_convert verbatim) ----
|
||||
regions = [] # (old_off, old_size, new_off)
|
||||
def add_region(old, osz, cur):
|
||||
regions.append((old, osz, cur))
|
||||
return cur + osz
|
||||
|
||||
dstpos = 0
|
||||
# switch table: u64 node VMAs on PC (R0_pc = 8*NS + 12*NT)
|
||||
for i in range(NS):
|
||||
dstpos = add_region(4 * i, 8, dstpos)
|
||||
for i in range(NT):
|
||||
dstpos = add_region(4 * NS + 12 * i, 12, dstpos)
|
||||
|
||||
node_newoff = {}
|
||||
rec_newoff = {}
|
||||
vtx_regions = []
|
||||
zero_vtx = []
|
||||
for (no, op) in placed:
|
||||
data = be32o(src, no + 4)
|
||||
# PC ModelNode is 48 bytes (5 x u64 pointers), not the N64 24
|
||||
dstpos = add_region(no, 48, dstpos)
|
||||
node_newoff[no] = dstpos - 48
|
||||
if op not in N64_REC:
|
||||
errors.append(f"{name}: unknown opcode {op} at {no:#x}")
|
||||
continue
|
||||
nsz, psz = N64_REC[op], PC_REC[op]
|
||||
rec_newoff[data] = dstpos
|
||||
# allocate the PC footprint (emit writes psz bytes; nsz would let the
|
||||
# next region start inside this record's struct)
|
||||
dstpos = add_region(data, psz, dstpos)
|
||||
if op == 4:
|
||||
nv = bu16(src, data + 0x10); vo = be32o(src, data + 0xC)
|
||||
if vo:
|
||||
sz = 16 * nv if nv else None
|
||||
if sz is None:
|
||||
zero_vtx.append((vo,))
|
||||
else:
|
||||
vtx_regions.append((vo, nv))
|
||||
if nv:
|
||||
dstpos = add_region(vo, sz, dstpos)
|
||||
elif op == 24:
|
||||
nv = be16(src, data + 0xC); ncv = be16(src, data + 0xE)
|
||||
vo = be32o(src, data + 8); cvo = be32o(src, data + 0x10)
|
||||
puo = be32o(src, data + 0x14)
|
||||
if nv and vo:
|
||||
vtx_regions.append((vo, nv)); dstpos = add_region(vo, 16 * nv, dstpos)
|
||||
if ncv and cvo:
|
||||
op24_is_collision[cvo] = True
|
||||
vtx_regions.append((cvo, ncv)); dstpos = add_region(cvo, 16 * ncv, dstpos)
|
||||
if nv and puo:
|
||||
dstpos = add_region(puo, 2 * nv, dstpos)
|
||||
elif op == 22:
|
||||
nv = struct.unpack_from(">i", src, data)[0]; vo = be32o(src, data + 4)
|
||||
if nv and vo:
|
||||
vtx_regions.append((vo, nv)); dstpos = add_region(vo, 16 * nv, dstpos)
|
||||
|
||||
# All record-referenced GDLs (needed before zero-vtx/blob layout so the
|
||||
# "next object" boundaries include them).
|
||||
ref_gdls = []
|
||||
for (no, op) in placed:
|
||||
data = be32o(src, no + 4)
|
||||
if op == 4 or op == 24:
|
||||
for off in (0, 4):
|
||||
q = be32o(src, data + off)
|
||||
if q and q not in ref_gdls:
|
||||
ref_gdls.append(q)
|
||||
elif op == 22:
|
||||
q = be32o(src, data + 8)
|
||||
if q and q not in ref_gdls:
|
||||
ref_gdls.append(q)
|
||||
visited_set = set(seq)
|
||||
unvisited = sorted(g for g in ref_gdls if g not in visited_set)
|
||||
all_gdls = unvisited + list(seq) # pack order
|
||||
|
||||
obj_offs_now = sorted(set(no for no, _ in placed) | set(rec_newoff.keys()) |
|
||||
set(v[0] for v in vtx_regions) | set(all_gdls))
|
||||
for (vo,) in zero_vtx:
|
||||
nxt = min((o for o in obj_offs_now if o > vo), default=D)
|
||||
sz = nxt - vo
|
||||
if sz <= 0 or sz % 16:
|
||||
errors.append(f"{name}: zero-vtx array {vo:#x} bad size {sz:#x}")
|
||||
else:
|
||||
vtx_regions.append((vo, sz // 16))
|
||||
dstpos = add_region(vo, sz, dstpos)
|
||||
|
||||
# Embedded image blobs: G_SETTIMG seg-5 targets (P* logo/legal files only).
|
||||
# texLoadFromGdl copies G_SETTIMG slots verbatim, so the remapped w1 must
|
||||
# point at the relocated pixels. Each span runs from its target to the next
|
||||
# placed object; overlapping spans merge (PlegalpageZ's 5 images share one
|
||||
# unclaimed span). Bytes are copied raw — the RDP reads them as-is.
|
||||
blob_targets = []
|
||||
for g in all_gdls:
|
||||
o = g
|
||||
while o + 8 <= D:
|
||||
w0 = be32r(src, o); c = w0 >> 24
|
||||
if c == 0xB8:
|
||||
break
|
||||
if c == 0xFD and (be32r(src, o + 4) >> 24) == 5:
|
||||
blob_targets.append(be32r(src, o + 4) & MASK)
|
||||
o += 8
|
||||
placed_offs = set(o for o, _, _ in regions)
|
||||
intervals = []
|
||||
for t in sorted(set(blob_targets)):
|
||||
end = min((o for o in placed_offs if o > t), default=D)
|
||||
intervals.append([t, end])
|
||||
merged = []
|
||||
for iv in intervals:
|
||||
if merged and iv[0] <= merged[-1][1]:
|
||||
merged[-1][1] = max(merged[-1][1], iv[1])
|
||||
else:
|
||||
merged.append(iv)
|
||||
for (t, e) in merged:
|
||||
dstpos = add_region(t, e - t, dstpos)
|
||||
|
||||
# GDLs last, tight-packed 16B slots. Visited GDLs go in VISIT order (the
|
||||
# compaction span arithmetic requires it). Referenced-but-unvisited GDLs
|
||||
# (e.g. PexplosionbitZ: compaction skips the file entirely when iterate
|
||||
# visits zero GDLs) are packed just before the visited block so they stay
|
||||
# outside [off_first, D_PC) and are never span-read; if a runtime BSP
|
||||
# visible=FALSE state renders them, they are still valid 16B slots.
|
||||
|
||||
gdl_newoff = {}
|
||||
total_slots = 0
|
||||
def pack_gdl(g):
|
||||
nonlocal dstpos, total_slots
|
||||
end = gdl_end(src, D, g)
|
||||
nslots = (end - g) // 8
|
||||
if (end - g) % 8:
|
||||
errors.append(f"{name}: GDL {g:#x} not 8B aligned end")
|
||||
gdl_newoff[g] = dstpos
|
||||
dstpos += 16 * nslots
|
||||
total_slots += nslots
|
||||
# region size = NEW footprint (16B slots); the old 8B bytes are not a
|
||||
# 1:1 byte map, so inv_remap must never be called inside a GDL region
|
||||
# (GDL pointers go through gdl_newoff, not put_ptr).
|
||||
regions.append((g, 16 * nslots, gdl_newoff[g]))
|
||||
for g in unvisited:
|
||||
pack_gdl(g)
|
||||
for g in seq:
|
||||
pack_gdl(g)
|
||||
|
||||
D_PC = dstpos
|
||||
G_PC = min(gdl_newoff.values()) if gdl_newoff else D_PC
|
||||
g1_n64 = min(seq) if seq else D
|
||||
|
||||
# ---- P_final estimate (D45/D46 marker-expansion worst case) ----
|
||||
# Runtime footprint after compaction + texLoadFromGdl expansion:
|
||||
# P_off = g1_visited_new + 16*S_visited + 16*sum(K_t-1 over visited markers)
|
||||
# <= D_PC + 16*sum(K_t-1 over ALL referenced-GDL markers)
|
||||
# K_BOUND: exact worst-case output slots per marker type (tex.c analysis,
|
||||
# verified over all files by d43_lutscan.py; unknown types default to 44).
|
||||
# Markers are counted over N64 spans [g, next_gdl_or_EOF) of every
|
||||
# referenced GDL (superset of what the PC runtime processes: unvisited
|
||||
# GDLs and inter-GDL junk are never span-read on PC).
|
||||
K_BOUND = {0: 38, 1: 46, 2: 36, 3: 18, 4: 15}
|
||||
kslack = 0
|
||||
gsorted = sorted(set(all_gdls))
|
||||
for i, gg in enumerate(gsorted):
|
||||
e = gsorted[i + 1] if i + 1 < len(gsorted) else D
|
||||
j = gg
|
||||
while j + 8 <= e:
|
||||
w0 = be32r(src, j)
|
||||
if (w0 >> 24) == 0xC0:
|
||||
kslack += K_BOUND.get(w0 & 7, 44) - 1
|
||||
j += 8
|
||||
P_est = D_PC + 16 * kslack
|
||||
|
||||
# ---- remap machinery ----
|
||||
regions.sort()
|
||||
rold = [x[0] for x in regions]
|
||||
def remap(off):
|
||||
if off == 0:
|
||||
return 0
|
||||
i = bisect.bisect_right(rold, off) - 1
|
||||
if i < 0:
|
||||
return None
|
||||
o, osz, n = regions[i]
|
||||
if o <= off < o + osz:
|
||||
return n + (off - o)
|
||||
return None
|
||||
|
||||
# NOTE: `regions` is sorted by OLD offset above; new offsets follow DFS
|
||||
# placement order and are NOT sorted, so inv_remap needs its own index.
|
||||
rsorted_new = sorted(regions, key=lambda x: x[2])
|
||||
rnew_offs = [x[2] for x in rsorted_new]
|
||||
def inv_remap(off):
|
||||
"""PC offset -> N64 offset (for round-trip validation)."""
|
||||
i = bisect.bisect_right(rnew_offs, off) - 1
|
||||
if i < 0:
|
||||
return None
|
||||
o, osz, n = rsorted_new[i]
|
||||
if n <= off < n + osz:
|
||||
return o + (off - n)
|
||||
return None
|
||||
|
||||
# ---- emit pass ----
|
||||
buf = bytearray(D_PC)
|
||||
def put_u16(o, v): buf[o:o + 2] = struct.pack("<H", v & 0xFFFF)
|
||||
def put_s16(o, v): buf[o:o + 2] = struct.pack("<H", v & 0xFFFF) # bytes identical to <h
|
||||
def put_u32(o, v): buf[o:o + 4] = struct.pack("<I", v & 0xFFFFFFFF)
|
||||
def put_f32(o, doff): buf[o:o + 4] = src[doff + 4:doff:-1] # bswap32 (4 bytes!)
|
||||
def put_ptr(o, old_off):
|
||||
if not old_off:
|
||||
return
|
||||
n = remap(old_off)
|
||||
if n is None:
|
||||
errors.append(f"{name}: ptr {old_off:#x} not in region map")
|
||||
n = 0
|
||||
buf[o:o + 4] = struct.pack("<I", 0x05000000 | (n & MASK))
|
||||
buf[o + 4:o + 8] = b"\x00" * 4
|
||||
|
||||
# switches: NS x u64 (VMA of node)
|
||||
for i in range(NS):
|
||||
put_ptr(8 * i, be32o(src, 4 * i))
|
||||
# texconfigs: NT x 12B
|
||||
for i in range(NT):
|
||||
doff = 4 * NS + 12 * i
|
||||
npos = 8 * NS + 12 * i
|
||||
tid = be32r(src, doff)
|
||||
put_u32(npos, tid) # bswap32 -> LE (seg-5 VMA included:
|
||||
buf[npos + 4:npos + 12] = src[doff + 4:doff + 12] # game skips >= MAX_TEXTURES on both platforms)
|
||||
if (tid >> 24) == 5 and remap(tid & MASK) is None:
|
||||
errors.append(f"{name}: texconfig seg-5 target {tid & MASK:#x} not in a blob region")
|
||||
|
||||
# nodes + records
|
||||
for (no, op) in placed:
|
||||
npos = node_newoff[no]
|
||||
put_u16(npos, bu16(src, no)) # Opcode
|
||||
put_ptr(npos + 8, be32o(src, no + 4)) # Data
|
||||
put_ptr(npos + 16, be32o(src, no + 8)) # Parent
|
||||
put_ptr(npos + 24, be32o(src, no + 0xC)) # Next
|
||||
put_ptr(npos + 32, be32o(src, no + 0x10)) # Prev
|
||||
put_ptr(npos + 40, be32o(src, no + 0x14)) # Child
|
||||
|
||||
d = be32o(src, no + 4)
|
||||
r = rec_newoff[d]
|
||||
if op == 1: # HeaderRecord
|
||||
put_u16(r, bu16(src, d)); put_s16(r + 2, be16(src, d + 2))
|
||||
put_ptr(r + 8, be32o(src, d + 4))
|
||||
put_u16(r + 16, bu16(src, d + 8)); put_u16(r + 18, bu16(src, d + 0xA))
|
||||
put_u16(r + 20, bu16(src, d + 0xC))
|
||||
elif op == 2 or op == 3: # GroupRecord / OP03
|
||||
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
|
||||
put_u16(r + 12, bu16(src, d + 0xC))
|
||||
for i in range(3): put_s16(r + 14 + 2 * i, be16(src, d + 0xE + 2 * i))
|
||||
put_ptr(r + 24, be32o(src, d + 0x14))
|
||||
put_f32(r + 32, d + 0x18)
|
||||
elif op == 4: # DisplayListRecord
|
||||
p = be32o(src, d); s = be32o(src, d + 4)
|
||||
if p: buf[r:r + 4] = struct.pack("<I", 0x05000000 | (gdl_newoff[p] & MASK))
|
||||
if s: buf[r + 8:r + 12] = struct.pack("<I", 0x05000000 | (gdl_newoff[s] & MASK))
|
||||
put_ptr(r + 24, be32o(src, d + 0xC))
|
||||
put_u16(r + 32, bu16(src, d + 0x10))
|
||||
buf[r + 34] = src[d + 0x12] # ModelType s8 raw
|
||||
elif op == 8: # LODRecord
|
||||
put_f32(r, d); put_f32(r + 4, d + 4)
|
||||
put_ptr(r + 8, be32o(src, d + 8))
|
||||
put_u16(r + 16, bu16(src, d + 0xC))
|
||||
elif op == 9: # BSPRecord
|
||||
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
|
||||
for i in range(3): put_f32(r + 12 + 4 * i, d + 0xC + 4 * i)
|
||||
put_ptr(r + 24, be32o(src, d + 0x18))
|
||||
put_ptr(r + 32, be32o(src, d + 0x1C))
|
||||
put_u16(r + 42, bu16(src, d + 0x22))
|
||||
elif op == 10: # BoundingBoxRecord
|
||||
put_u32(r, be32r(src, d))
|
||||
for i in range(6): put_f32(r + 4 + 4 * i, d + 4 + 4 * i)
|
||||
elif op == 12: # GunfireRecord
|
||||
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
|
||||
for i in range(3): put_f32(r + 12 + 4 * i, d + 0xC + 4 * i)
|
||||
put_ptr(r + 24, be32o(src, d + 0x18))
|
||||
put_f32(r + 32, d + 0x1C)
|
||||
put_u16(r + 36, bu16(src, d + 0x20))
|
||||
elif op == 13: # ShadowRecord
|
||||
for i in range(2): put_f32(r + 4 * i, d + 4 * i)
|
||||
for i in range(2): put_f32(r + 8 + 4 * i, d + 8 + 4 * i)
|
||||
put_ptr(r + 16, be32o(src, d + 0x10))
|
||||
put_ptr(r + 24, be32o(src, d + 0x14))
|
||||
put_f32(r + 32, d + 0x18)
|
||||
elif op == 15: # InterlinkageRecord
|
||||
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
|
||||
for i in range(3): put_f32(r + 12 + 4 * i, d + 0xC + 4 * i)
|
||||
put_f32(r + 24, d + 0x18)
|
||||
elif op == 18: # SwitchRecord
|
||||
put_ptr(r, be32o(src, d))
|
||||
put_u16(r + 8, bu16(src, d + 4))
|
||||
elif op == 21: # GroupSimpleRecord
|
||||
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
|
||||
put_s16(r + 12, be16(src, d + 0xC)); put_u16(r + 14, bu16(src, d + 0xE))
|
||||
put_f32(r + 16, d + 0x10)
|
||||
elif op == 22: # DisplayListPrimaryRecord
|
||||
put_u32(r, be32r(src, d)) # numVertices s32 bswap
|
||||
put_ptr(r + 8, be32o(src, d + 4))
|
||||
p = be32o(src, d + 8)
|
||||
if p: buf[r + 16:r + 20] = struct.pack("<I", 0x05000000 | (gdl_newoff[p] & MASK))
|
||||
elif op == 23: # HeadPlaceholderRecord
|
||||
put_u16(r, bu16(src, d))
|
||||
elif op == 24: # DisplayList_CollisionRecord
|
||||
p = be32o(src, d); s = be32o(src, d + 4)
|
||||
if p: buf[r:r + 4] = struct.pack("<I", 0x05000000 | (gdl_newoff[p] & MASK))
|
||||
if s: buf[r + 8:r + 12] = struct.pack("<I", 0x05000000 | (gdl_newoff[s] & MASK))
|
||||
put_ptr(r + 16, be32o(src, d + 8))
|
||||
put_s16(r + 24, be16(src, d + 0xC)); put_s16(r + 26, be16(src, d + 0xE))
|
||||
put_ptr(r + 32, be32o(src, d + 0x10))
|
||||
put_ptr(r + 40, be32o(src, d + 0x14))
|
||||
put_s16(r + 48, be16(src, d + 0x18)); put_u16(r + 50, bu16(src, d + 0x1A))
|
||||
|
||||
# vertex arrays
|
||||
def emit_main_vtx(vo, nv, npos):
|
||||
for i in range(nv):
|
||||
o = vo + 16 * i; q = npos + 16 * i
|
||||
put_s16(q, be16(src, o)); put_s16(q + 2, be16(src, o + 2))
|
||||
put_s16(q + 4, be16(src, o + 4)); put_s16(q + 6, be16(src, o + 6))
|
||||
put_s16(q + 8, be16(src, o + 8)); put_s16(q + 10, be16(src, o + 0xA))
|
||||
buf[q + 12:q + 16] = src[o + 0xC:o + 0x10]
|
||||
|
||||
for (vo, nv) in vtx_regions:
|
||||
npos = remap(vo)
|
||||
if op24_is_collision.get(vo):
|
||||
cvo_npos = npos
|
||||
for i in range(nv):
|
||||
o = vo + 16 * i; q = cvo_npos + 16 * i
|
||||
put_s16(q, be16(src, o)); put_s16(q + 2, be16(src, o + 2))
|
||||
put_s16(q + 4, be16(src, o + 4)); put_s16(q + 6, be16(src, o + 6))
|
||||
v = be32r(src, o + 8)
|
||||
if v == 0:
|
||||
pass
|
||||
elif (v >> 24) == 5:
|
||||
n = remap(v & MASK)
|
||||
if n is None:
|
||||
errors.append(f"{name}: LinkedTo {v:#x} not in map")
|
||||
n = 0
|
||||
put_u32(q + 8, 0x05000000 | (n & MASK))
|
||||
else:
|
||||
errors.append(f"{name}: LinkedTo {v:#x} neither null nor seg-5")
|
||||
put_s16(q + 12, be16(src, o + 0xC)); put_s16(q + 14, be16(src, o + 0xE))
|
||||
else:
|
||||
emit_main_vtx(vo, nv, npos)
|
||||
|
||||
# GDLs (pack order: unvisited first, then visit order), 16B LE slots
|
||||
for g in all_gdls:
|
||||
end = gdl_end(src, D, g)
|
||||
npos = gdl_newoff[g]
|
||||
j = 0
|
||||
o = g
|
||||
while o + 8 <= end:
|
||||
w0 = be32r(src, o); w1 = be32r(src, o + 4)
|
||||
q = npos + 16 * j
|
||||
# PC Gfx slot = {u64 words.w0; u64 words.w1} (Gwords uses uintptr_t).
|
||||
# Both the game (tex.c: (u8)(words.w0>>24), words.w1 & 0xfff) and
|
||||
# fast3d (opcode = words.w0>>24 unmasked, seg_addr(words.w1)) read
|
||||
# N64 w0 from the LOW dword of bytes[0..8) and N64 w1 from the LOW
|
||||
# dword of bytes[8..16). High dwords must be zero (gSP* macros
|
||||
# zero-extend their 32-bit patterns into the u64 fields).
|
||||
c = w0 >> 24
|
||||
if c in ADDR_OPS and (w1 >> 24) == 5:
|
||||
n = remap(w1 & MASK)
|
||||
if n is None:
|
||||
errors.append(f"{name}: GDL {g:#x} op{c:#x} w1 {w1:#x} not in map")
|
||||
n = 0
|
||||
w1 = 0x05000000 | (n & MASK)
|
||||
buf[q:q + 8] = struct.pack("<II", w0, 0) # words.w0 = w0
|
||||
buf[q + 8:q + 16] = struct.pack("<II", w1, 0) # words.w1 = w1
|
||||
if c == 0xB8:
|
||||
break
|
||||
j += 1
|
||||
o += 8
|
||||
|
||||
# ---- round-trip validation ----
|
||||
validate(name, src, D, NS, NT, nodes, R0, placed, all_gdls, buf, D_PC,
|
||||
regions, node_newoff, rec_newoff, gdl_newoff, remap, inv_remap)
|
||||
|
||||
# ---- compress ----
|
||||
co = zlib.compressobj(6, zlib.DEFLATED, -15)
|
||||
comp = b"\x11\x72" + co.compress(bytes(buf)) + co.flush()
|
||||
C_PC = len(comp)
|
||||
|
||||
results[name] = dict(C=C_PC, D=D_PC, Dn64=D, S=total_slots,
|
||||
Bpc=D_PC - 16 * total_slots, Bn64=g1_n64,
|
||||
Pest=P_est,
|
||||
comp=comp if not CHECK_ONLY else None,
|
||||
NS=NS, NT=NT)
|
||||
|
||||
# collision-vertex set (filled during layout; emit pass needs it)
|
||||
op24_is_collision = {}
|
||||
|
||||
def validate(name, src, D, NS, NT, nodes, R0, placed, all_gdls, buf, D_PC,
|
||||
regions, node_newoff, rec_newoff, gdl_newoff, remap, inv_remap):
|
||||
def err(m): errors.append(f"{name}: RT {m}")
|
||||
|
||||
# tiling: regions cover [0, D_PC) exactly
|
||||
rs = sorted(regions, key=lambda x: x[2])
|
||||
pos = 0
|
||||
for o, osz, n in rs:
|
||||
if n != pos or n + osz > D_PC:
|
||||
err(f"tiling gap/overlap at {pos:#x} (region {o:#x}->{n:#x})")
|
||||
return
|
||||
pos += osz
|
||||
if pos != D_PC:
|
||||
err(f"tiling ends at {pos:#x} != D_PC {D_PC:#x}")
|
||||
return
|
||||
|
||||
# node fields
|
||||
for (no, op) in placed:
|
||||
npos = node_newoff[no]
|
||||
if bu16(src, no) != struct.unpack_from("<H", buf, npos)[0]:
|
||||
err(f"node {no:#x} opcode mismatch")
|
||||
for f, pf in ((4, 8), (8, 16), (0xC, 24), (0x10, 32), (0x14, 40)):
|
||||
old = be32o(src, no + f)
|
||||
v = struct.unpack_from("<Q", buf, npos + pf)[0]
|
||||
if old == 0:
|
||||
if v != 0: err(f"node {no:#x}+{f:#x}: expected null")
|
||||
else:
|
||||
want = remap(old)
|
||||
if (v >> 24) != 5 or (v & MASK) != want:
|
||||
err(f"node {no:#x}+{f:#x}: {v:#x} != 0x05|{want and hex(want)}")
|
||||
|
||||
# record fields (spot-check every pointer + scalar via inverse remap)
|
||||
ptrfields = {1: [4], 2: [0x14], 3: [0x14], 8: [8], 9: [0x18, 0x1C],
|
||||
12: [0x18], 13: [0x10, 0x14], 18: [0], 24: [8, 0x10, 0x14],
|
||||
4: [0xC], 22: [4]}
|
||||
for (no, op) in placed:
|
||||
d = be32o(src, no + 4)
|
||||
r = rec_newoff[d]
|
||||
for f in ptrfields.get(op, []):
|
||||
old = be32o(src, d + f)
|
||||
v = struct.unpack_from("<Q", buf, r + PC_PTR_OFF[op][f])[0]
|
||||
if old == 0:
|
||||
if v != 0: err(f"rec{op} {d:#x}+{f:#x}: expected null")
|
||||
else:
|
||||
want = remap(old)
|
||||
if (v >> 24) != 5 or (v & MASK) != want:
|
||||
err(f"rec{op} {d:#x}+{f:#x}: {v:#x}")
|
||||
|
||||
# GDL slots (all packed GDLs)
|
||||
for g in all_gdls:
|
||||
end = gdl_end(src, D, g)
|
||||
npos = gdl_newoff[g]
|
||||
j = 0; o = g
|
||||
while o + 8 <= end:
|
||||
w0 = be32r(src, o); w1 = be32r(src, o + 4)
|
||||
q = npos + 16 * j
|
||||
if struct.unpack_from("<I", buf, q)[0] != w0:
|
||||
err(f"gdl {g:#x} slot {j}: w0 mismatch")
|
||||
c = w0 >> 24
|
||||
if struct.unpack_from("<I", buf, q + 4)[0] != 0:
|
||||
err(f"gdl {g:#x} slot {j}: pad[4..8) nonzero")
|
||||
w1p = struct.unpack_from("<I", buf, q + 8)[0]
|
||||
if c in ADDR_OPS and (w1 >> 24) == 5:
|
||||
want = remap(w1 & MASK)
|
||||
if w1p != (0x05000000 | want):
|
||||
err(f"gdl {g:#x} slot {j}: w1 {w1p:#x} != 0x05|{hex(want)}")
|
||||
elif w1p != w1:
|
||||
err(f"gdl {g:#x} slot {j}: w1 passthrough mismatch")
|
||||
if struct.unpack_from("<I", buf, q + 12)[0] != 0:
|
||||
err(f"gdl {g:#x} slot {j}: pad[12..16) nonzero")
|
||||
if c == 0xB8:
|
||||
break
|
||||
j += 1; o += 8
|
||||
|
||||
# PC pointer offsets per (op, n64 field) — for round-trip validation
|
||||
PC_PTR_OFF = {
|
||||
1: {4: 8}, 2: {0x14: 24}, 3: {0x14: 24}, 8: {8: 8},
|
||||
9: {0x18: 24, 0x1C: 32}, 12: {0x18: 24}, 13: {0x10: 16, 0x14: 24},
|
||||
18: {0: 0}, 24: {8: 16, 0x10: 32, 0x14: 40}, 4: {0xC: 24}, 22: {4: 8},
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------- main ---
|
||||
model_names = [n for n in table_names if is_model_name(n)]
|
||||
stats["table_models"] = len(model_names)
|
||||
|
||||
for name in model_names:
|
||||
# reset per-file collision set before layout (populated below)
|
||||
op24_is_collision.clear()
|
||||
process(name)
|
||||
|
||||
if errors:
|
||||
print(f"\n{len(errors)} ERRORS:")
|
||||
for e in errors[:40]:
|
||||
print(" ", e)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"files converted: {len(results)} / {stats['table_models']}")
|
||||
tot = sum(round8(r["C"]) for r in results.values())
|
||||
print(f"sum round8(C_pc) = {tot} max C = {hex(max(r['C'] for r in results.values()))}"
|
||||
f" max D = {hex(max(r['D'] for r in results.values()))}")
|
||||
|
||||
# ------------------------------------------------------- cross-checks ------
|
||||
DIRECT = {
|
||||
"Csuit_lf_handZ": 0x18000,
|
||||
"GtriggerZ": 0x17000, "GwatchlaserZ": 0x17000,
|
||||
"PwalletbondZ": 0x17000,
|
||||
"PlegalpageZ": 0x3C000, "PnintendologoZ": 0x3C000, "PgoldeneyelogoZ": 0x3C000,
|
||||
}
|
||||
WEAPON_BUF = 0xF000
|
||||
|
||||
fail = 0
|
||||
def chk(cond, msg):
|
||||
global fail
|
||||
tag = "OK " if cond else "FAIL"
|
||||
if not cond:
|
||||
fail += 1
|
||||
print(f"{tag} {msg}")
|
||||
|
||||
print("\n== single-file dst!=0 buffers ==")
|
||||
for name, b in DIRECT.items():
|
||||
r = results.get(name)
|
||||
if not r:
|
||||
chk(False, f"{name}: missing"); continue
|
||||
ok = (round8(r["C"]) + 8 <= b and r["D"] + round8(r["C"]) <= b
|
||||
and r["Pest"] <= b)
|
||||
chk(ok, f"{name:20s} C={r['C']:>6} D={r['D']:>6} round8(C)+8={round8(r['C'])+8:#7x} "
|
||||
f"D+r8C={r['D']+round8(r['C']):#7x} P_est={r['Pest']:>#7x} vs {b:#x}")
|
||||
|
||||
# gun.c used_to_load_1st_person_model_on_demand: every item model except
|
||||
# ITEM_SUIT_LF_HAND (Csuit_lf_handZ, 0x18000) and ITEM_TRIGGER/ITEM_WATCHLASER
|
||||
# (GtriggerZ/GwatchlaserZ, 0x17000 — both in DIRECT above) loads into the
|
||||
# D_80032464 weapon buffer. G* files are exactly the item models.
|
||||
GUN_BUF_FILES = [n for n in results
|
||||
if n.startswith("G") and n.endswith("Z")
|
||||
and n not in ("GtriggerZ", "GwatchlaserZ")]
|
||||
worst_w = None
|
||||
for n in GUN_BUF_FILES:
|
||||
r = results[n]
|
||||
if worst_w is None or r["Pest"] > worst_w[0]:
|
||||
worst_w = (r["Pest"], n, r)
|
||||
chk(worst_w[0] <= WEAPON_BUF and worst_w[2]["D"] + round8(worst_w[2]["C"]) <= WEAPON_BUF
|
||||
and round8(worst_w[2]["C"]) + 8 <= WEAPON_BUF,
|
||||
f"worst gun-buffer item {worst_w[1]}: P_est={worst_w[0]:#x}, D+r8C={worst_w[2]['D']+round8(worst_w[2]['C']):#x} "
|
||||
f"r8C+8={round8(worst_w[2]['C'])+8:#x} vs {WEAPON_BUF:#x}")
|
||||
|
||||
def chain_check(label, slots, buf_pc):
|
||||
"""Chain fit: the game advances bufferPtr by each file's post-load
|
||||
poolRemaining (>= its actual footprint P_off), so the chain works iff
|
||||
sum(P_off_i) <= R_pc (prefix argument: A_j >= P_off_j)
|
||||
with P_off_i bounded by P_est_i (D45/D46 marker-expansion worst case).
|
||||
Slots are independent (any body+head+weapon can be drawn), so the worst
|
||||
combination is the sum of per-slot maxima. Each slot's DMA/decompress
|
||||
constraints are checked against a lower bound on its remaining buffer.
|
||||
"""
|
||||
per_slot = []
|
||||
for sl, cands in slots:
|
||||
best = None
|
||||
for n in cands:
|
||||
r = results.get(n)
|
||||
if not r:
|
||||
continue
|
||||
if best is None or r["Pest"] > best[0]:
|
||||
best = (r["Pest"], n, r)
|
||||
if best is None:
|
||||
chk(False, f"{label}/{sl}: no candidates"); continue
|
||||
per_slot.append((sl, best[1], best[2]))
|
||||
total = sum(r["Pest"] for _, _, r in per_slot)
|
||||
chk(total <= buf_pc,
|
||||
f"{label}: worst combination {total:#x} vs buffer {buf_pc:#x} "
|
||||
+ " ".join(f"{sl}:{n}(P_est={r['Pest']:#x})" for sl, n, r in per_slot))
|
||||
rem = buf_pc
|
||||
for sl, n, r in per_slot:
|
||||
ok = round8(r["C"]) + 8 <= rem and r["D"] + round8(r["C"]) <= rem
|
||||
chk(ok, f"{label}/{sl}: worst {n} C={r['C']:#x} D={r['D']:#x} "
|
||||
f"round8(C)+8={round8(r['C'])+8:#x} D+r8C={r['D']+round8(r['C']):#x} "
|
||||
f"vs rem-lower={rem:#x}")
|
||||
rem -= round64(r["Pest"])
|
||||
|
||||
# candidate sets
|
||||
heads = [n for n in results if n.startswith("Chead")]
|
||||
bodies = [n for n in results if n.startswith("C") and n.endswith("Z")
|
||||
and not n.startswith("Chead") and n != "Csuit_lf_handZ"]
|
||||
# cast screen (front.c random_rifles_in_intro / random_pistols_in_intro):
|
||||
CAST_WEAPONS = ["PchrkalashZ", "Pchrm16Z", "Pchrfnp90Z", "PchrautoshotZ",
|
||||
"PchrgrenadelaunchZ", "PchrsniperrifleZ",
|
||||
"PchrwppkZ", "PchrwppksilZ", "PchrskorpionZ", "PchruziZ",
|
||||
"Pchrtt33Z", "PchrrugerZ", "PchrlaserZ", "PchrgoldenZ"]
|
||||
# bondview2 held prop (player.c getPropForHeldItem):
|
||||
BONDVIEW_PROPS = ["PchrknifeZ", "PchrwppkZ", "PchrskorpionZ", "PchrkalashZ",
|
||||
"PchruziZ", "Pchrmp5kZ", "PchrspectreZ", "Pchrm16Z",
|
||||
"Pchrfnp90Z", "PchrshotgunZ", "PchrgrenadelaunchZ",
|
||||
"PchrgrenadeZ", "PchrremotemineZ", "PchrproximitymineZ",
|
||||
"PchrtimedmineZ", "PchrrugerZ", "PchrlaserZ",
|
||||
"PchrsniperrifleZ", "Pchrmp5ksilZ", "Pchrtt33Z",
|
||||
"PchrwppksilZ", "PchrthrowknifeZ", "PchrautoshotZ",
|
||||
"PchrrocketlaunchZ", "PchrgoldenZ", "PflagZ"]
|
||||
for n in CAST_WEAPONS + BONDVIEW_PROPS:
|
||||
chk(n in results, f"candidate present: {n}")
|
||||
|
||||
print("\n== cast screen chain (front.c:7811/7819/7869; PC 0x25000, N64 0x18160) ==")
|
||||
chain_check("cast", [("body", bodies), ("head", heads), ("weapon", CAST_WEAPONS)],
|
||||
0x25000)
|
||||
|
||||
print("\n== title gunbarrel chain (title.c:489/495/517; avail 0x24D00 both sides) ==")
|
||||
# fixed files (c_item_entries indexed by BODIES enum; BODY_Male_Karl=HEAD_START
|
||||
# is an alias so the effective count is 80, matching the 80 record includes):
|
||||
# BODY_Brosnan_Tuxedo=5 -> CdjbondZ
|
||||
# BODY_Male_Pierce_Bond_Tuxedo=78 -> CheadbrosnanZ (used as the head slot)
|
||||
# PitemZ_entries[PROP_CHRWPPK] -> PchrwppkZ
|
||||
t1 = ["CdjbondZ"]
|
||||
t2 = ["CheadbrosnanZ"]
|
||||
t3 = ["PchrwppkZ"]
|
||||
chain_check("title", [("brosnan", t1), ("bond", t2), ("wppk", t3)],
|
||||
0x24D00)
|
||||
|
||||
print("\n== bondview2 chain (bondview2.c:490/502/571; PC 0x23000, N64 0x14820) ==")
|
||||
chain_check("bondview", [("body", bodies), ("head", heads), ("prop", BONDVIEW_PROPS)],
|
||||
0x23000)
|
||||
|
||||
print("\n== stage bank (dst==0 fresh loads) ==")
|
||||
maxr = max(round8(r["C"]) + 8 for r in results.values())
|
||||
chk(maxr <= 0x24C400, f"max round8(C)+8 = {maxr:#x} vs STAGE bank 0x24C400")
|
||||
|
||||
# N64 sanity: same overlap check with N64 sizes (the game worked on N64)
|
||||
print("\n== N64 sanity (overlap model) ==")
|
||||
n64_fail = 0
|
||||
for name, r in results.items():
|
||||
# recompute C_n64 cheaply? we only stored Dn64; approximate with stored comp? no.
|
||||
pass
|
||||
# (N64 compressed sizes: recomputed below from ROM for the DIRECT set only)
|
||||
for name, b in DIRECT.items():
|
||||
key, (addr, size) = find_row(name)
|
||||
Cn = size - 2
|
||||
Dn = results[name]["Dn64"]
|
||||
ok = round8(Cn) + 8 <= b and Dn + round8(Cn) <= b
|
||||
if not ok:
|
||||
n64_fail += 1
|
||||
print(f" N64 {name}: round8(C)+8={round8(Cn)+8:#x} D+r8C={Dn+round8(Cn):#x} vs {b:#x}")
|
||||
print(f"N64 overlap-model violations: {n64_fail} (0 expected; if >0 the overlap "
|
||||
f"constraint is looser than modeled — review before trusting PC checks)")
|
||||
|
||||
# ---------------------------------------------------------------- output ---
|
||||
if CHECK_ONLY:
|
||||
print("\ncheck-only: no sidecars written")
|
||||
else:
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
manifest = [] # (name, offset, size) in table order
|
||||
off = 0
|
||||
chunks = []
|
||||
for name in model_names:
|
||||
r = results[name]
|
||||
start = (off + 15) & ~15
|
||||
if start > off:
|
||||
chunks.append(b"\x00" * (start - off))
|
||||
off = start
|
||||
manifest.append((name, off, r["C"]))
|
||||
chunks.append(r["comp"])
|
||||
off += r["C"]
|
||||
total = (off + 15) & ~15
|
||||
if off % 16:
|
||||
chunks.append(b"\x00" * (16 - (off % 16)))
|
||||
with open(os.path.join(OUT_DIR, "pcmodels.bin"), "wb") as f:
|
||||
for c in chunks:
|
||||
f.write(c)
|
||||
# decimal fields: the C parser uses strtol(..., 10)
|
||||
with open(os.path.join(OUT_DIR, "manifest.csv"), "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["name", "offset", "size"])
|
||||
for name, o, s in manifest:
|
||||
w.writerow([name, o, s])
|
||||
print(f"\nwrote {len(manifest)} sidecars to {OUT_DIR}/pcmodels.bin")
|
||||
print(f"total reservation needed: {total:#x} bytes "
|
||||
f"(romSize + this must fit the extended VirtualAlloc)")
|
||||
|
||||
if fail:
|
||||
print(f"\n{fail} CHECK FAILURES")
|
||||
sys.exit(1)
|
||||
print("\nALL CHECKS PASSED")
|
||||
@@ -0,0 +1,112 @@
|
||||
/* D43 Plan B: compiler-verified PC layout probe for model-file records.
|
||||
*
|
||||
* Prints sizeof/offsetof for every struct the offline emit pass (d43_emit.py)
|
||||
* must reproduce byte-for-byte, compiled with the SAME toolchain + includes as
|
||||
* the game build (x86_64-w64-mingw32-gcc, port/shim first). Run:
|
||||
* x86_64-w64-mingw32-gcc -DPORT -Iport/shim -I. -Iinclude -Iinclude/PR \
|
||||
* -Isrc -Isrc/game -Isrc/libultra -Isrc/libultra/audio -Iport/include \
|
||||
* tools_pc/d43_layoutprobe.c -o build-pc/d43_layoutprobe.exe
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <ultra64.h>
|
||||
#include <bondtypes.h>
|
||||
|
||||
/* The decomp's include/stddef.h is an empty guard (no offsetof); use the GCC
|
||||
* builtin so this probe works in a C TU with the project include chain. */
|
||||
#ifndef offsetof
|
||||
#define offsetof(type, member) __builtin_offsetof(type, member)
|
||||
#endif
|
||||
|
||||
#define P_SZ(t) printf("SZ %-40s %lu\n", #t, (unsigned long)sizeof(t))
|
||||
#define P_OFF(t, f) printf("OFF %-32s %-12s %lu\n", #t, #f, (unsigned long)offsetof(t, f))
|
||||
|
||||
int main(void)
|
||||
{
|
||||
P_SZ(ModelNode);
|
||||
P_OFF(ModelNode, Opcode); P_OFF(ModelNode, Data); P_OFF(ModelNode, Parent);
|
||||
P_OFF(ModelNode, Next); P_OFF(ModelNode, Prev); P_OFF(ModelNode, Child);
|
||||
|
||||
P_SZ(Vertex);
|
||||
P_OFF(Vertex, coord); P_OFF(Vertex, index); P_OFF(Vertex, s);
|
||||
P_OFF(Vertex, CollisionRelatedIndex); P_OFF(Vertex, CollisionReserved);
|
||||
|
||||
P_SZ(ModelFileHeader);
|
||||
P_OFF(ModelFileHeader, RootNode); P_OFF(ModelFileHeader, Switches);
|
||||
P_OFF(ModelFileHeader, numSwitches); P_OFF(ModelFileHeader, numtextures);
|
||||
P_OFF(ModelFileHeader, Textures);
|
||||
|
||||
P_SZ(ModelFileTextures);
|
||||
P_OFF(ModelFileTextures, TextureID); P_OFF(ModelFileTextures, Width);
|
||||
P_OFF(ModelFileTextures, RenderDepth); P_OFF(ModelFileTextures, sflags);
|
||||
|
||||
P_SZ(ModelRoData_HeaderRecord);
|
||||
P_OFF(ModelRoData_HeaderRecord, AnimPart); P_OFF(ModelRoData_HeaderRecord, MatrixIndex);
|
||||
P_OFF(ModelRoData_HeaderRecord, FirstGroup); P_OFF(ModelRoData_HeaderRecord, Group1);
|
||||
P_OFF(ModelRoData_HeaderRecord, RwDataIndex);
|
||||
|
||||
P_SZ(ModelRoData_GroupRecord);
|
||||
P_OFF(ModelRoData_GroupRecord, Origin); P_OFF(ModelRoData_GroupRecord, JointID);
|
||||
P_OFF(ModelRoData_GroupRecord, MatrixID0); P_OFF(ModelRoData_GroupRecord, ChildGroup);
|
||||
P_OFF(ModelRoData_GroupRecord, BoundingVolumeRadius);
|
||||
|
||||
P_SZ(ModelRoData_DisplayListRecord);
|
||||
P_OFF(ModelRoData_DisplayListRecord, Primary); P_OFF(ModelRoData_DisplayListRecord, Secondary);
|
||||
P_OFF(ModelRoData_DisplayListRecord, BaseAddr); P_OFF(ModelRoData_DisplayListRecord, Vertices);
|
||||
P_OFF(ModelRoData_DisplayListRecord, numVertices); P_OFF(ModelRoData_DisplayListRecord, ModelType);
|
||||
|
||||
P_SZ(ModelRoData_LODRecord);
|
||||
P_OFF(ModelRoData_LODRecord, MinDistance); P_OFF(ModelRoData_LODRecord, MaxDistance);
|
||||
P_OFF(ModelRoData_LODRecord, Affects); P_OFF(ModelRoData_LODRecord, RwDataIndex);
|
||||
|
||||
P_SZ(ModelRoData_BSPRecord);
|
||||
P_OFF(ModelRoData_BSPRecord, Point); P_OFF(ModelRoData_BSPRecord, Vector);
|
||||
P_OFF(ModelRoData_BSPRecord, leftChild); P_OFF(ModelRoData_BSPRecord, rightChild);
|
||||
P_OFF(ModelRoData_BSPRecord, RwDataIndex);
|
||||
|
||||
P_SZ(ModelRoData_BoundingBoxRecord);
|
||||
P_OFF(ModelRoData_BoundingBoxRecord, ModelNumber); P_OFF(ModelRoData_BoundingBoxRecord, Bounds);
|
||||
|
||||
P_SZ(ModelRoData_GunfireRecord);
|
||||
P_OFF(ModelRoData_GunfireRecord, Offset); P_OFF(ModelRoData_GunfireRecord, Size);
|
||||
P_OFF(ModelRoData_GunfireRecord, Image); P_OFF(ModelRoData_GunfireRecord, Scale);
|
||||
P_OFF(ModelRoData_GunfireRecord, RwDataIndex);
|
||||
|
||||
P_SZ(ModelRoData_ShadowRecord);
|
||||
P_OFF(ModelRoData_ShadowRecord, pos); P_OFF(ModelRoData_ShadowRecord, size);
|
||||
P_OFF(ModelRoData_ShadowRecord, image); P_OFF(ModelRoData_ShadowRecord, Header);
|
||||
P_OFF(ModelRoData_ShadowRecord, Scale);
|
||||
|
||||
P_SZ(ModelRoData_InterlinkageRecord);
|
||||
P_OFF(ModelRoData_InterlinkageRecord, pos); P_OFF(ModelRoData_InterlinkageRecord, pos2);
|
||||
P_OFF(ModelRoData_InterlinkageRecord, Scale);
|
||||
|
||||
P_SZ(ModelRoData_SwitchRecord);
|
||||
P_OFF(ModelRoData_SwitchRecord, Controls); P_OFF(ModelRoData_SwitchRecord, RwDataIndex);
|
||||
|
||||
P_SZ(ModelRoData_GroupSimpleRecord);
|
||||
P_OFF(ModelRoData_GroupSimpleRecord, Origin); P_OFF(ModelRoData_GroupSimpleRecord, Group1);
|
||||
P_OFF(ModelRoData_GroupSimpleRecord, Group2); P_OFF(ModelRoData_GroupSimpleRecord, BoundingVolumeRadius);
|
||||
|
||||
P_SZ(ModelRoData_DisplayListPrimaryRecord);
|
||||
P_OFF(ModelRoData_DisplayListPrimaryRecord, numVertices);
|
||||
P_OFF(ModelRoData_DisplayListPrimaryRecord, Vertices);
|
||||
P_OFF(ModelRoData_DisplayListPrimaryRecord, Primary);
|
||||
P_OFF(ModelRoData_DisplayListPrimaryRecord, BaseAddr);
|
||||
|
||||
P_SZ(ModelRoData_HeadPlaceholderRecord);
|
||||
P_OFF(ModelRoData_HeadPlaceholderRecord, RwDataIndex);
|
||||
|
||||
P_SZ(ModelRoData_DisplayList_CollisionRecord);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, Primary);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, Secondary);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, Vertices);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, numVertices);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, numCollisionVertices);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, CollisionVertices);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, PointUsage);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, ModelType);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, RwDataIndex);
|
||||
P_OFF(ModelRoData_DisplayList_CollisionRecord, BaseAddr);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user