PSX Research
This commit is contained in:
parent
2f243976b6
commit
8d34c85c22
13 changed files with 1720 additions and 8 deletions
|
|
@ -184,6 +184,14 @@ For the generic family band now dominating `LSET1/L0` failures (`0x003e`, `0x004
|
|||
|
||||
The chosen bundle and clamped frame index, plus binding-diversity metrics, are preserved in output metadata so failures stay auditable.
|
||||
|
||||
There is now one opt-in experimental binding mode for current map-0 research:
|
||||
|
||||
- `runtime-map0-masked-proxy`
|
||||
|
||||
That mode reads `.cache/runtime-map0-correlation.json`, takes the live `headerWord11` field from the current map-0 type rows, masks it to `0x0fffff`, and remaps a type only when that masked value lands within a small tolerance of a scanned raw bundle offset with matching kind/mode. All non-matching types still fall back to the raw slot rule.
|
||||
|
||||
This is still a probe rule, not claimed final executable truth. It exists to turn the new RAM-backed map-0 correlation into a small, auditable extraction improvement without pretending the full late `DAT_800758d8` bank parse is solved.
|
||||
|
||||
When debug labels are enabled for a map render, labels now identify unique rendered resources rather than per-instance placements. The stable label key is currently `bundle offset + clamped frame + resolved palette`. Validation atlas sheets still use progressive cell indices.
|
||||
|
||||
## Rendering Rule
|
||||
|
|
@ -231,6 +239,7 @@ Supported options:
|
|||
- `--source <relative-path>`
|
||||
- `--wdl <absolute-or-relative-file>`
|
||||
- `--disc-root <path>`
|
||||
- `--binding-mode <raw|runtime-map0-masked-proxy>`
|
||||
- `--map-source <auto|combined|layered|constructors|roots|region01|region00>`
|
||||
- `--out-name <stem>`
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ function parseArgs(argv) {
|
|||
'Crusader - No Remorse (USA) GPU RAM 2.bin'
|
||||
),
|
||||
mapSource: 'auto',
|
||||
bindingMode: 'raw',
|
||||
sceneScope: 'probe',
|
||||
validationBundles: [],
|
||||
};
|
||||
|
|
@ -52,6 +53,11 @@ function parseArgs(argv) {
|
|||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--binding-mode') {
|
||||
options.bindingMode = next;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--scene-scope') {
|
||||
options.sceneScope = next;
|
||||
index += 1;
|
||||
|
|
@ -97,6 +103,7 @@ function printHelp() {
|
|||
' --source <relative path> WDL path relative to the PSX disc root',
|
||||
' --wdl <file> Direct WDL path',
|
||||
' --disc-root <path> PSX asset root, defaults to STATIC_PSX in the sibling workspace',
|
||||
' --binding-mode <raw|runtime-map0-masked-proxy> Raw slot binding by default; optional map-0 runtime proxy uses .cache/runtime-map0-correlation.json when present',
|
||||
' --scene-scope <probe|full> Probe is supported; full is intentionally disabled until raw floor and full-map decode is recovered',
|
||||
' --gpu-ram-dump <path> PSX GPU RAM dump used for live mode-1 palette extraction',
|
||||
' --validation-bundles <csv> Comma-separated bundle absolute offsets (hex or decimal) for bundle palette-sweep validation sheets',
|
||||
|
|
@ -133,6 +140,7 @@ async function main() {
|
|||
wdlPath,
|
||||
sourceRelPath: options.source,
|
||||
mapSource: options.mapSource,
|
||||
bindingMode: options.bindingMode,
|
||||
sceneScope: options.sceneScope,
|
||||
gpuRamDumpPath: options.gpuRamDump,
|
||||
validationBundles: options.validationBundles,
|
||||
|
|
|
|||
|
|
@ -119,6 +119,132 @@ function chooseBundleForType(bundles, typeWord) {
|
|||
return null;
|
||||
}
|
||||
|
||||
async function loadJsonIfExists(filePath) {
|
||||
try {
|
||||
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildRuntimeMap0MaskedBindings(correlation, bundles, tolerance = 0x200) {
|
||||
const byType = new Map();
|
||||
const diagnostics = [];
|
||||
const provisionalBindings = [];
|
||||
|
||||
if (!correlation || correlation.currentMapId !== 0 || !Array.isArray(correlation.combined)) {
|
||||
return { byType, diagnostics };
|
||||
}
|
||||
|
||||
for (const row of correlation.combined) {
|
||||
if (!row || !Number.isInteger(row.typeId) || !Number.isInteger(row.rawRecordCount) || row.rawRecordCount <= 0) {
|
||||
continue;
|
||||
}
|
||||
if ((row.headerKind !== 4 && row.headerKind !== 5) || (row.headerWord3 !== 1 && row.headerWord3 !== 2)) {
|
||||
continue;
|
||||
}
|
||||
if (!Number.isInteger(row.headerWord11) || row.headerWord11 === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const maskedAbsoluteOffset = row.headerWord11 & 0x0fffff;
|
||||
let bestMatch = null;
|
||||
for (const bundle of bundles) {
|
||||
if (bundle.kind !== row.headerKind || bundle.mode !== row.headerWord3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absoluteOffsetDelta = Math.abs(bundle.absoluteOffset - maskedAbsoluteOffset);
|
||||
if (!bestMatch || absoluteOffsetDelta < bestMatch.absoluteOffsetDelta) {
|
||||
bestMatch = {
|
||||
bundle,
|
||||
absoluteOffsetDelta,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!bestMatch || bestMatch.absoluteOffsetDelta > tolerance) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const binding = {
|
||||
typeId: row.typeId,
|
||||
bundle: bestMatch.bundle,
|
||||
mappingSource: 'runtime-map0-masked-header-offset-proxy',
|
||||
runtimeBinding: {
|
||||
maskedAbsoluteOffset,
|
||||
absoluteOffsetDelta: bestMatch.absoluteOffsetDelta,
|
||||
headerKind: row.headerKind,
|
||||
headerMode: row.headerWord3,
|
||||
headerWord4: row.headerWord4,
|
||||
headerWord8: row.headerWord8,
|
||||
headerWord10: row.headerWord10,
|
||||
visibleCount: row.visibleCount ?? 0,
|
||||
rawRecordCount: row.rawRecordCount,
|
||||
},
|
||||
};
|
||||
|
||||
provisionalBindings.push(binding);
|
||||
}
|
||||
|
||||
const bundleBuckets = new Map();
|
||||
for (const binding of provisionalBindings) {
|
||||
const bucket = bundleBuckets.get(binding.bundle.absoluteOffset) ?? [];
|
||||
bucket.push(binding);
|
||||
bundleBuckets.set(binding.bundle.absoluteOffset, bucket);
|
||||
}
|
||||
|
||||
for (const binding of provisionalBindings) {
|
||||
const bucket = bundleBuckets.get(binding.bundle.absoluteOffset) ?? [];
|
||||
const isCrowdedLargeSingleFrameBundle =
|
||||
bucket.length >= 4 &&
|
||||
binding.bundle.frameCount === 1 &&
|
||||
binding.bundle.width >= 96 &&
|
||||
binding.bundle.height >= 48;
|
||||
|
||||
if (isCrowdedLargeSingleFrameBundle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byType.set(binding.typeId, binding);
|
||||
diagnostics.push({
|
||||
typeId: binding.typeId,
|
||||
bundleSlot: binding.bundle.slot,
|
||||
bundleAbsoluteOffset: binding.bundle.absoluteOffset,
|
||||
maskedAbsoluteOffset: binding.runtimeBinding.maskedAbsoluteOffset,
|
||||
absoluteOffsetDelta: binding.runtimeBinding.absoluteOffsetDelta,
|
||||
headerKind: binding.runtimeBinding.headerKind,
|
||||
headerMode: binding.runtimeBinding.headerMode,
|
||||
visibleCount: binding.runtimeBinding.visibleCount,
|
||||
rawRecordCount: binding.runtimeBinding.rawRecordCount,
|
||||
crowdedBundleTypeCount: bucket.length,
|
||||
});
|
||||
}
|
||||
|
||||
diagnostics.sort((left, right) => left.typeId - right.typeId);
|
||||
return { byType, diagnostics };
|
||||
}
|
||||
|
||||
function chooseBundleBinding(record, bundles, options = {}) {
|
||||
if (options.bindingMode === 'runtime-map0-masked-proxy') {
|
||||
const runtimeBinding = options.runtimeMap0Bindings?.get(record.typeWord) ?? null;
|
||||
if (runtimeBinding?.bundle) {
|
||||
return runtimeBinding;
|
||||
}
|
||||
}
|
||||
|
||||
const rawTypeBundle = chooseBundleForType(bundles, record.typeWord);
|
||||
if (!rawTypeBundle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
bundle: rawTypeBundle,
|
||||
mappingSource: 'raw-typeword-bundle-slot-diagnostic',
|
||||
runtimeBinding: null,
|
||||
};
|
||||
}
|
||||
|
||||
function describeMapScope(recordSet) {
|
||||
if (recordSet.source === 'combined') {
|
||||
return 'layered object-projection probe from both loader-sized section-0 constructor-placement and root-dispatch records in post_audio_section_00';
|
||||
|
|
@ -738,9 +864,9 @@ async function buildSceneItems(region04, records, bundles, options = {}) {
|
|||
continue;
|
||||
}
|
||||
|
||||
const rawTypeBundle = chooseBundleForType(bundles, record.typeWord);
|
||||
const bundle = rawTypeBundle;
|
||||
if (!bundle) {
|
||||
const binding = chooseBundleBinding(record, bundles, options);
|
||||
const bundle = binding?.bundle ?? null;
|
||||
if (!bundle || !binding) {
|
||||
continue;
|
||||
}
|
||||
if (nonMapFacingBundleOffsets.has(bundle.absoluteOffset)) {
|
||||
|
|
@ -788,9 +914,13 @@ async function buildSceneItems(region04, records, bundles, options = {}) {
|
|||
defaultPaletteIndex: bundle.defaultPaletteIndex ?? null,
|
||||
resolvedPaletteIndex: bundle.resolvedPaletteIndex ?? null,
|
||||
paletteFormula: bundle.paletteFormula ?? null,
|
||||
mappingSource: 'raw-typeword-bundle-slot-diagnostic',
|
||||
mappingSource: binding.mappingSource,
|
||||
templateTypeId: null,
|
||||
donorTypeId: null,
|
||||
runtimeBindingMaskedAbsoluteOffset: binding.runtimeBinding?.maskedAbsoluteOffset ?? null,
|
||||
runtimeBindingOffsetDelta: binding.runtimeBinding?.absoluteOffsetDelta ?? null,
|
||||
runtimeBindingVisibleCount: binding.runtimeBinding?.visibleCount ?? null,
|
||||
runtimeBindingRawRecordCount: binding.runtimeBinding?.rawRecordCount ?? null,
|
||||
rawWords: record.rawWords ?? record.words,
|
||||
flipped: (record.laneWord & 0x0002) !== 0,
|
||||
width: sprite.width,
|
||||
|
|
@ -903,10 +1033,17 @@ export async function exportMap(options) {
|
|||
{ mode1RuntimePalette }
|
||||
);
|
||||
|
||||
const runtimeMap0Correlation = options.bindingMode === 'runtime-map0-masked-proxy'
|
||||
? await loadJsonIfExists(path.join(cacheBaseRoot, 'runtime-map0-correlation.json'))
|
||||
: null;
|
||||
const runtimeMap0BindingResult = buildRuntimeMap0MaskedBindings(runtimeMap0Correlation, bundles);
|
||||
|
||||
const { items: sceneItems, skippedRecords } = await buildSceneItems(region04, recordSet.records, bundles, {
|
||||
paletteSets,
|
||||
bindingMode: options.bindingMode,
|
||||
mode1RuntimePalette,
|
||||
mode1PaletteBank,
|
||||
runtimeMap0Bindings: runtimeMap0BindingResult.byType,
|
||||
});
|
||||
const bindingDiversity = summarizeBindingDiversity(sceneItems);
|
||||
const authoredLayerSummary = summarizeAuthoredLayers(recordSet.records);
|
||||
|
|
@ -949,7 +1086,11 @@ export async function exportMap(options) {
|
|||
bundleCount: bundles.length,
|
||||
bundleSource: bundles[0]?.bundleSource ?? 'none',
|
||||
gpuRamDumpPath: options.gpuRamDumpPath ?? null,
|
||||
artBindingSource: 'raw-typeword-bundle-slot-diagnostic',
|
||||
artBindingSource: options.bindingMode === 'runtime-map0-masked-proxy'
|
||||
? 'runtime-map0-masked-header-offset-proxy-with-raw-fallback'
|
||||
: 'raw-typeword-bundle-slot-diagnostic',
|
||||
runtimeMap0BindingTypeCount: runtimeMap0BindingResult.diagnostics.length,
|
||||
runtimeMap0BindingTypes: runtimeMap0BindingResult.diagnostics,
|
||||
activeHeaderOverrideCandidateCount: activeHeaderOverrideCandidates.length,
|
||||
bestActiveHeaderOverrideCandidate: activeHeaderOverrideCandidates[0]
|
||||
? {
|
||||
|
|
@ -975,7 +1116,9 @@ export async function exportMap(options) {
|
|||
'The root-dispatch lane is now rendered as a second authored layer, but runtime-driven control mutations and dynamic effect spawns are still out of scope.',
|
||||
'Known non-map-facing portrait/talk root types `0x0042` and `0x0049`, plus the known portrait bundle `0x000D84F4`, are excluded from probe rendering.',
|
||||
'Viewer-derived sidecars, donor mappings, and cached scene references are intentionally disabled in this standalone exporter.',
|
||||
'Current bundle selection is still diagnostic-only until the late DAT_800758d8 art bank is parsed directly.',
|
||||
options.bindingMode === 'runtime-map0-masked-proxy'
|
||||
? 'Current bundle selection uses an experimental map-0 runtime masked-offset proxy where live headerWord11 low bits land near a scanned raw bundle; all non-matching types still fall back to the raw slot diagnostic.'
|
||||
: 'Current bundle selection is still diagnostic-only until the late DAT_800758d8 art bank is parsed directly.',
|
||||
'No floor or tile layer is decoded directly yet; post_audio_region_02 and the decompressed level-state lane remain unresolved.',
|
||||
]),
|
||||
'Palette routing remains partly heuristic when authored token and default bank evidence are both absent.',
|
||||
|
|
@ -1023,6 +1166,10 @@ export async function exportMap(options) {
|
|||
mappingSource: item.mappingSource,
|
||||
templateTypeId: item.templateTypeId,
|
||||
donorTypeId: item.donorTypeId,
|
||||
runtimeBindingMaskedAbsoluteOffset: item.runtimeBindingMaskedAbsoluteOffset,
|
||||
runtimeBindingOffsetDelta: item.runtimeBindingOffsetDelta,
|
||||
runtimeBindingVisibleCount: item.runtimeBindingVisibleCount,
|
||||
runtimeBindingRawRecordCount: item.runtimeBindingRawRecordCount,
|
||||
token06HighByte: item.paletteDiagnostics?.token06HighByte ?? null,
|
||||
token0cHighByte: item.paletteDiagnostics?.token0cHighByte ?? null,
|
||||
expectedPaletteToken: item.paletteDiagnostics?.expectedPaletteToken ?? null,
|
||||
|
|
|
|||
79
psx-map-exporter/tmp_correlate_runtime_map0.mjs
Normal file
79
psx-map-exporter/tmp_correlate_runtime_map0.mjs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const projectRoot = path.resolve('..');
|
||||
const runtimePath = path.resolve(projectRoot, 'psx-map-exporter', '.cache', 'runtime-snapshot-report.json');
|
||||
const recordsPath = path.resolve(projectRoot, 'psx-map-exporter', '.cache', 'ctor_stream_probe', 'records.json');
|
||||
|
||||
const runtime = JSON.parse(fs.readFileSync(runtimePath, 'utf8'));
|
||||
const records = JSON.parse(fs.readFileSync(recordsPath, 'utf8'));
|
||||
|
||||
const visibleObjects = runtime.sampleVisibleObjects ?? [];
|
||||
const visibleTypeMap = new Map();
|
||||
for (const object of visibleObjects) {
|
||||
const key = object.typeId;
|
||||
const entry = visibleTypeMap.get(key) ?? {
|
||||
typeId: key,
|
||||
count: 0,
|
||||
routeWords: new Set(),
|
||||
selectorIndexes: new Set(),
|
||||
latchedTokens: new Set(),
|
||||
artResourcePtrs: new Set(),
|
||||
authoredRecordPtrs: [],
|
||||
};
|
||||
entry.count += 1;
|
||||
entry.routeWords.add(object.routeWord);
|
||||
entry.selectorIndexes.add(object.selectorIndex);
|
||||
entry.latchedTokens.add(object.latchedToken);
|
||||
entry.artResourcePtrs.add(object.artResourcePtr);
|
||||
if (entry.authoredRecordPtrs.length < 8) {
|
||||
entry.authoredRecordPtrs.push(object.authoredRecordPtr);
|
||||
}
|
||||
visibleTypeMap.set(key, entry);
|
||||
}
|
||||
|
||||
const typeRows = new Map((runtime.sampleTypeRows ?? []).map((row) => [row.typeId, row]));
|
||||
const recordTypeCounts = new Map();
|
||||
const recordLaneByType = new Map();
|
||||
for (const record of records.records ?? []) {
|
||||
recordTypeCounts.set(record.typeWord, (recordTypeCounts.get(record.typeWord) ?? 0) + 1);
|
||||
const laneSet = recordLaneByType.get(record.typeWord) ?? new Set();
|
||||
laneSet.add(record.laneWord);
|
||||
recordLaneByType.set(record.typeWord, laneSet);
|
||||
}
|
||||
|
||||
const combined = [...new Set([...recordTypeCounts.keys(), ...visibleTypeMap.keys()])]
|
||||
.sort((left, right) => left - right)
|
||||
.map((typeId) => {
|
||||
const visible = visibleTypeMap.get(typeId);
|
||||
const row = typeRows.get(typeId) ?? null;
|
||||
return {
|
||||
typeId,
|
||||
rawRecordCount: recordTypeCounts.get(typeId) ?? 0,
|
||||
rawLaneWords: [...(recordLaneByType.get(typeId) ?? new Set())].sort((a, b) => a - b),
|
||||
visibleCount: visible?.count ?? 0,
|
||||
visibleRouteWords: visible ? [...visible.routeWords].sort((a, b) => a - b) : [],
|
||||
visibleSelectorIndexes: visible ? [...visible.selectorIndexes].sort((a, b) => a - b) : [],
|
||||
visibleLatchedTokens: visible ? [...visible.latchedTokens].sort((a, b) => a - b) : [],
|
||||
visibleArtResourcePtrs: visible ? [...visible.artResourcePtrs] : [],
|
||||
sampleAuthoredRecordPtrs: visible?.authoredRecordPtrs ?? [],
|
||||
activeHeader: row?.activeHeader ?? 0,
|
||||
builtResource: row?.builtResource ?? 0,
|
||||
headerKind: row?.headerWords?.[0] ?? null,
|
||||
headerWord3: row?.headerWords?.[3] ?? null,
|
||||
headerWord4: row?.headerWords?.[4] ?? null,
|
||||
headerWord8: row?.headerWords?.[8] ?? null,
|
||||
headerWord10: row?.headerWords?.[10] ?? null,
|
||||
headerWord11: row?.headerWords?.[11] ?? null,
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.rawRecordCount !== 0 || entry.visibleCount !== 0);
|
||||
|
||||
const outputPath = path.resolve(projectRoot, 'psx-map-exporter', '.cache', 'runtime-map0-correlation.json');
|
||||
fs.writeFileSync(outputPath, JSON.stringify({
|
||||
currentMapId: runtime.currentMapId,
|
||||
visibleObjectCount: runtime.visibleObjectCount,
|
||||
combined,
|
||||
}, null, 2));
|
||||
|
||||
console.log(JSON.stringify({ outputPath }, null, 2));
|
||||
159
psx-map-exporter/tmp_dump_runtime_snapshot.mjs
Normal file
159
psx-map-exporter/tmp_dump_runtime_snapshot.mjs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
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));
|
||||
Loading…
Add table
Add a link
Reference in a new issue