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));