Crusader_Decomp/psx-map-exporter/tmp_dump_runtime_snapshot.mjs
2026-04-13 16:50:28 +02:00

159 lines
No EOL
4.9 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
const RAM_BASE = 0x80000000;
const TYPE_TABLE_BASE = 0x800758c4;
const TYPE_ROW_STRIDE = 0x18;
const MAIN_VISIBLE_LIST = 0x8006ad5c;
const MAIN_VISIBLE_READ_INDEX = 0x80067690;
const MAIN_VISIBLE_WRITE_INDEX = 0x800676bc;
const CURRENT_MAP_ID = 0x80067728;
const NEXT_MAP_ID = 0x800678d0;
const projectRoot = path.resolve('..');
const ramDumpPath = path.resolve(projectRoot, 'binary', 'Crusader - No Remorse (USA) Main Memory Dump.bin');
const ram = fs.readFileSync(ramDumpPath);
const outputPath = path.resolve(projectRoot, 'psx-map-exporter', '.cache', 'runtime-snapshot-report.json');
function vaToOffset(address) {
const offset = address - RAM_BASE;
if (offset < 0 || offset >= ram.length) {
throw new RangeError(`Address out of dump range: 0x${address.toString(16)}`);
}
return offset;
}
function canRead(address, size = 4) {
const offset = address - RAM_BASE;
return offset >= 0 && offset + size <= ram.length;
}
function readU32(address) {
return ram.readUInt32LE(vaToOffset(address));
}
function readS32(address) {
return ram.readInt32LE(vaToOffset(address));
}
function readU16(address) {
return ram.readUInt16LE(vaToOffset(address));
}
function readS16(address) {
return ram.readInt16LE(vaToOffset(address));
}
function readHeaderWords(address, wordCount = 10) {
if (!canRead(address, wordCount * 4)) {
return null;
}
return Array.from({ length: wordCount }, (_, index) => readU32(address + index * 4));
}
function parseTypeRows(limit = 0x100) {
const rows = [];
for (let typeId = 0; typeId < limit; typeId += 1) {
const rowBase = TYPE_TABLE_BASE + typeId * TYPE_ROW_STRIDE;
const installCount = readU32(rowBase + 0x00);
const builtResource = readU32(rowBase + 0x04);
const stateScript = readU32(rowBase + 0x08);
const simpleComponent = readU32(rowBase + 0x0c);
const extents = readU32(rowBase + 0x10);
const activeHeader = readU32(rowBase + 0x14);
if ((installCount | builtResource | stateScript | simpleComponent | extents | activeHeader) === 0) {
continue;
}
const headerWords = activeHeader !== 0 ? readHeaderWords(activeHeader, 12) : null;
rows.push({
typeId,
rowBase,
installCount,
builtResource,
stateScript,
simpleComponent,
extents,
activeHeader,
headerWords,
});
}
return rows;
}
function parseVisibleObjects(limit = 256) {
const readIndex = readU32(MAIN_VISIBLE_READ_INDEX);
const writeIndex = readU32(MAIN_VISIBLE_WRITE_INDEX);
const count = Math.max(0, Math.min(limit, writeIndex));
const objects = [];
for (let index = 0; index < count; index += 1) {
const objectPtr = readU32(MAIN_VISIBLE_LIST + index * 4);
if (!canRead(objectPtr, 0xa4)) {
continue;
}
objects.push({
index,
objectPtr,
typeId: readU16(objectPtr + 0x18),
routeWord: readU16(objectPtr + 0x1c),
stateFlags: readU16(objectPtr + 0x1e),
screenLeft: readS16(objectPtr + 0x20),
screenTop: readS16(objectPtr + 0x22),
screenRight: readS16(objectPtr + 0x24),
screenBottom: readS16(objectPtr + 0x26),
worldX: readS32(objectPtr + 0x3c),
worldY: readS32(objectPtr + 0x40),
worldZ: readS32(objectPtr + 0x44),
velocityX: readS32(objectPtr + 0x60),
velocityY: readS32(objectPtr + 0x64),
velocityZ: readS32(objectPtr + 0x68),
programPtr: readU32(objectPtr + 0x08),
artResourcePtr: readU32(objectPtr + 0x10),
companionExtentsPtr: readU32(objectPtr + 0x84),
stateScriptPtr: readU32(objectPtr + 0x88),
scriptBasePtr: readU32(objectPtr + 0x8c),
scriptReadPtr: readU32(objectPtr + 0x90),
latchedToken: readU16(objectPtr + 0x94),
scriptCountdown: readU16(objectPtr + 0x96),
selectorIndex: readU16(objectPtr + 0x9e),
authoredRecordPtr: readU32(objectPtr + 0xa0),
});
}
return {
readIndex,
writeIndex,
count,
objects,
};
}
const typeRows = parseTypeRows();
const visible = parseVisibleObjects();
const byType = new Map();
for (const object of visible.objects) {
byType.set(object.typeId, (byType.get(object.typeId) ?? 0) + 1);
}
const summary = {
ramDumpPath,
ramSize: ram.length,
currentMapId: readU32(CURRENT_MAP_ID),
nextMapId: readU32(NEXT_MAP_ID),
mainVisibleReadIndex: visible.readIndex,
mainVisibleWriteIndex: visible.writeIndex,
visibleObjectCount: visible.objects.length,
nonZeroTypeRowCount: typeRows.length,
visibleTypeCounts: [...byType.entries()]
.map(([typeId, count]) => ({ typeId, count }))
.sort((left, right) => right.count - left.count || left.typeId - right.typeId),
sampleTypeRows: typeRows.slice(0, 64),
sampleVisibleObjects: visible.objects.slice(0, 128),
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(summary, null, 2));
console.log(JSON.stringify({ outputPath }, null, 2));