159 lines
No EOL
4.9 KiB
JavaScript
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)); |