import path from 'node:path'; function readU32LE(buffer, offset) { return buffer.readUInt32LE(offset); } function readU16LE(buffer, offset) { return buffer.readUInt16LE(offset); } const ALLOWED_LANE_WORDS = new Set([0x20, 0x22, 0x30]); const PSX_SCREEN_SCALE = 2; function uniqueSorted(values) { return [...new Set(values)].sort((left, right) => left - right); } export function parseLsetWdl(buffer, filePath) { if (buffer.length < 0x34) { throw new Error(`File too small for LSET header: ${filePath}`); } const headerSize = readU32LE(buffer, 0); if (headerSize < 0x34 || headerSize % 4 !== 0 || headerSize > buffer.length) { throw new Error(`Unexpected header size 0x${headerSize.toString(16)} in ${filePath}`); } const headerWords = []; for (let offset = 0; offset < headerSize; offset += 4) { headerWords.push(readU32LE(buffer, offset)); } const audioSize = readU32LE(buffer, 4); const postAudioStart = headerSize + audioSize; const sectionSizes = []; for (let offset = 0x08; offset < 0x38 && offset + 4 <= buffer.length; offset += 4) { sectionSizes.push(readU32LE(buffer, offset)); } const sections = []; let sectionCursor = postAudioStart; for (let index = 0; index < sectionSizes.length; index += 1) { const size = sectionSizes[index]; if (size <= 0 || sectionCursor + size > buffer.length) { break; } sections.push({ name: `post_audio_section_${String(index).padStart(2, '0')}`, offset: sectionCursor, size, buffer: buffer.subarray(sectionCursor, sectionCursor + size), }); sectionCursor += size; } const boundaryCandidates = uniqueSorted( headerWords .slice(2) .filter((value) => value > postAudioStart && value < buffer.length) ); if (boundaryCandidates.length < 4) { throw new Error( `Expected at least 4 post-audio boundaries, found ${boundaryCandidates.length} in ${filePath}` ); } const selectedBoundaries = boundaryCandidates.slice(0, 4); const regions = []; const regionStarts = [postAudioStart, ...selectedBoundaries]; const regionEnds = [...selectedBoundaries, buffer.length]; regions.push({ name: 'audio_or_spu_blob', offset: headerSize, size: audioSize, buffer: buffer.subarray(headerSize, postAudioStart), }); for (let index = 0; index < regionStarts.length; index += 1) { const offset = regionStarts[index]; const end = regionEnds[index]; regions.push({ name: `post_audio_region_${String(index).padStart(2, '0')}`, offset, size: end - offset, buffer: buffer.subarray(offset, end), }); } return { filePath, fileName: path.basename(filePath), buffer, headerSize, audioSize, postAudioStart, headerWords, sectionSizes, sections, boundaryCandidates, regions, }; } function isPlausibleRecord(words) { const [typeWord, xWord, yWord, zWord, selectorWord, laneWord] = words; if (typeWord < 0x20 || typeWord > 0x1ff) { return false; } if ((xWord | yWord | zWord) === 0) { return false; } if (laneWord === 0 || laneWord > 0x1fff) { return false; } if (selectorWord > 0x03ff) { return false; } return true; } function isStructuredCandidate(words) { const [typeWord, xWord, yWord, zWord, selectorWord, laneWord] = words; if (typeWord >= 0x200) { return false; } if (xWord === 0 && yWord === 0) { return false; } if (xWord >= 0x4000 || yWord >= 0x4000) { return false; } if (zWord > 0x20 || selectorWord > 0x04) { return false; } if (!ALLOWED_LANE_WORDS.has(laneWord)) { return false; } return true; } function buildRecord(words, source, offset, rawWords = words) { if (!isPlausibleRecord(words)) { return null; } const [typeWord, xWord, yWord, zWord, selectorWord, laneWord] = words; const screenX = (yWord - xWord) * PSX_SCREEN_SCALE; const screenY = ((2 * zWord) - Math.floor((xWord + yWord) / 2)) * PSX_SCREEN_SCALE; return { index: -1, source, offset, words, rawWords, typeWord, xWord, yWord, zWord, selectorWord, laneWord, screenX, screenY, sourceFamily: null, sourceRole: null, recordSide: null, rowIndex: -1, }; } function decodeRecord(buffer, offset, source) { const words = []; for (let cursor = 0; cursor < 12; cursor += 2) { words.push(readU16LE(buffer, offset + cursor)); } return buildRecord(words, source, offset, words); } function makeAsciiPreview(buffer, length = 64) { const slice = buffer.subarray(0, Math.min(length, buffer.length)); let text = ''; for (const value of slice) { text += value >= 0x20 && value <= 0x7e ? String.fromCharCode(value) : '.'; } return text; } function scanOffsetTableCandidates(buffer, maxBase = 0x200) { const candidates = []; const limit = Math.min(maxBase, Math.max(0, buffer.length - 8)); for (let base = 0; base <= limit; base += 2) { const count = readU16LE(buffer, base); if (count <= 0 || count >= 0x200) { continue; } const tableEnd = base + 2 + count * 2; if (tableEnd > buffer.length) { continue; } let previous = -1; let monotonic = true; const firstOffsets = []; for (let index = 0; index < count; index += 1) { const offset = readU16LE(buffer, base + 2 + index * 2); if (index < 8) { firstOffsets.push(offset); } if (offset < previous || offset >= buffer.length) { monotonic = false; break; } previous = offset; } if (monotonic) { candidates.push({ base, count, firstOffsets }); } } return candidates; } function scanPlausible12ByteRecordStarts(buffer, maxBase = 0x200) { const starts = []; const limit = Math.min(maxBase, Math.max(0, buffer.length - 12)); for (let base = 0; base <= limit; base += 2) { const words = []; for (let cursor = 0; cursor < 12; cursor += 2) { words.push(readU16LE(buffer, base + cursor)); } if (isPlausibleRecord(words)) { starts.push({ base, words }); } } return starts; } function buildPreviewRows(buffer, rowWordWidth = 8, rowCount = 24) { const rows = []; const maxRows = Math.min(rowCount, Math.floor(buffer.length / (rowWordWidth * 2))); for (let rowIndex = 0; rowIndex < maxRows; rowIndex += 1) { const offset = rowIndex * rowWordWidth * 2; const words = []; for (let wordIndex = 0; wordIndex < rowWordWidth; wordIndex += 1) { words.push(readU16LE(buffer, offset + wordIndex * 2)); } const bytes = buffer.subarray(offset, offset + rowWordWidth * 2); rows.push({ rowIndex, offset, words, ascii: makeAsciiPreview(bytes, bytes.length), }); } return rows; } export function summarizeRegion02(region) { const firstU32 = []; const firstU16 = []; for (let offset = 0; offset + 4 <= region.buffer.length && firstU32.length < 8; offset += 4) { firstU32.push(readU32LE(region.buffer, offset)); } for (let offset = 0; offset + 2 <= region.buffer.length && firstU16.length < 16; offset += 2) { firstU16.push(readU16LE(region.buffer, offset)); } const offsetTableCandidates = scanOffsetTableCandidates(region.buffer); const plausible12ByteRecordStarts = scanPlausible12ByteRecordStarts(region.buffer); return { offset: region.offset, size: region.size, firstU32, firstU16, asciiPreview: makeAsciiPreview(region.buffer, 96), previewRows: buildPreviewRows(region.buffer), offsetTableCandidates: offsetTableCandidates.slice(0, 16), plausible12ByteRecordStarts: plausible12ByteRecordStarts.slice(0, 16), note: offsetTableCandidates.length === 0 && plausible12ByteRecordStarts.length === 0 ? 'Leading region-02 bytes do not look like a count-prefixed offset table or direct 12-byte placement rows.' : 'Region-02 exposes candidate structure and should be correlated against live loader-installed subordinate slices.', }; } export function parseRegion00Records(region) { const rowCount = region.buffer.length >= 4 ? readU32LE(region.buffer, 0) : 0; const records = []; for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) { const rowBase = 4 + rowIndex * 24; if (rowBase + 24 > region.buffer.length) { break; } const rowWords = []; for (let wordIndex = 0; wordIndex < 12; wordIndex += 1) { rowWords.push(readU16LE(region.buffer, rowBase + wordIndex * 2)); } const leftRawWords = rowWords.slice(0, 6); const rightRawWords = rowWords.slice(6, 12); const leftWords = [rowWords[4], rowWords[5], rowWords[0], rowWords[1], rowWords[2], rowWords[3]]; const rightWords = [rowWords[10], rowWords[11], rowWords[6], rowWords[7], rowWords[8], rowWords[9]]; for (const [recordSide, wordSet, rawWordSet, sourceByteOffset] of [ ['left', leftWords, leftRawWords, 0], ['right', rightWords, rightRawWords, 12], ]) { const record = buildRecord(wordSet, 'region00', rowBase + sourceByteOffset, rawWordSet); if (!record) { continue; } record.sourceFamily = 'section0_dispatch_roots'; record.sourceRole = 'root-dispatch'; record.rowIndex = rowIndex; record.recordSide = recordSide; records.push(record); } } records.forEach((record, index) => { record.index = index; record.absoluteOffset = region.offset + record.offset; }); return { source: 'region00', recordStartOffset: 4, records, }; } function detectStructured12ByteStream(buffer) { let bestCandidate = null; for (let headerOffset = 0; headerOffset + 16 <= buffer.length; headerOffset += 4) { const count = readU32LE(buffer, headerOffset); const recordStartOffset = headerOffset + 4; const maxPossibleCount = Math.floor((buffer.length - recordStartOffset) / 12); if (count === 0 || count > maxPossibleCount) { continue; } let prefixStructuredCount = 0; for (let index = 0; index < count; index += 1) { const recordOffset = recordStartOffset + index * 12; const words = []; for (let cursor = 0; cursor < 12; cursor += 2) { words.push(readU16LE(buffer, recordOffset + cursor)); } if (!isStructuredCandidate(words)) { break; } prefixStructuredCount += 1; } if (prefixStructuredCount < 16) { continue; } if ( !bestCandidate || prefixStructuredCount > bestCandidate.prefixStructuredCount || (prefixStructuredCount === bestCandidate.prefixStructuredCount && headerOffset < bestCandidate.headerOffset) ) { bestCandidate = { headerOffset, recordStartOffset, count, prefixStructuredCount, }; } } return bestCandidate; } export function parseRegion01Records(region) { const records = []; const stream = detectStructured12ByteStream(region.buffer); if (stream) { for (let index = 0; index < stream.count; index += 1) { const recordOffset = stream.recordStartOffset + index * 12; if (recordOffset + 12 > region.buffer.length) { break; } const record = decodeRecord(region.buffer, recordOffset, 'region01'); if (!record || !isStructuredCandidate(record.words)) { continue; } record.sourceFamily = 'section0_constructor_placements'; record.sourceRole = 'constructor-placement'; record.rowIndex = index; record.recordSide = null; records.push(record); } } else { for (let rowOffset = 0; rowOffset + 24 <= region.buffer.length; rowOffset += 24) { const left = decodeRecord(region.buffer, rowOffset, 'region01-left'); const right = decodeRecord(region.buffer, rowOffset + 12, 'region01-right'); if (left && isStructuredCandidate(left.words)) { left.sourceFamily = 'section0_constructor_placements'; left.sourceRole = 'constructor-placement'; left.rowIndex = Math.floor(rowOffset / 24); left.recordSide = 'left'; records.push(left); } if (right && isStructuredCandidate(right.words)) { right.sourceFamily = 'section0_constructor_placements'; right.sourceRole = 'constructor-placement'; right.rowIndex = Math.floor(rowOffset / 24); right.recordSide = 'right'; records.push(right); } } } records.forEach((record, index) => { record.index = index; record.absoluteOffset = region.offset + record.offset; }); return { source: 'region01', recordStartOffset: stream?.recordStartOffset ?? 0, streamHeaderOffset: stream?.headerOffset ?? null, streamRecordCount: stream?.count ?? null, streamStructuredPrefixCount: stream?.prefixStructuredCount ?? null, records, }; }