132 lines
3.9 KiB
JavaScript
132 lines
3.9 KiB
JavaScript
|
|
const fs = require("fs");
|
||
|
|
|
||
|
|
function readU32LE(buffer, offset) {
|
||
|
|
return buffer.readUInt32LE(offset);
|
||
|
|
}
|
||
|
|
|
||
|
|
function readU16LE(buffer, offset) {
|
||
|
|
return buffer.readUInt16LE(offset);
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseLsetWdl(data) {
|
||
|
|
const headerSize = readU32LE(data, 0);
|
||
|
|
if (headerSize !== 0x34 || headerSize > data.length) {
|
||
|
|
throw new Error(`unexpected header size ${headerSize}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const headerWords = [];
|
||
|
|
for (let offset = 0; offset < headerSize; offset += 4) {
|
||
|
|
headerWords.push(readU32LE(data, offset));
|
||
|
|
}
|
||
|
|
|
||
|
|
const audioSize = headerWords[1];
|
||
|
|
const sectionSizes = [];
|
||
|
|
for (let offset = 0x08; offset < 0x38; offset += 4) {
|
||
|
|
sectionSizes.push(readU32LE(data, offset));
|
||
|
|
}
|
||
|
|
|
||
|
|
const sections = [];
|
||
|
|
let cursor = headerSize + audioSize;
|
||
|
|
for (let index = 0; index < sectionSizes.length; index += 1) {
|
||
|
|
const size = sectionSizes[index];
|
||
|
|
if (size <= 0 || cursor + size > data.length) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
sections.push({
|
||
|
|
index,
|
||
|
|
name: `post_audio_section_${String(index).padStart(2, "0")}`,
|
||
|
|
offset: cursor,
|
||
|
|
size
|
||
|
|
});
|
||
|
|
cursor += size;
|
||
|
|
}
|
||
|
|
|
||
|
|
return { sections };
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseTypedSection16(data, section, startOffset) {
|
||
|
|
const bytes = data.subarray(section.offset + startOffset, section.offset + section.size);
|
||
|
|
if (bytes.length < 8) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const recordCount = readU32LE(bytes, 0);
|
||
|
|
const payloadBytes = readU32LE(bytes, 4);
|
||
|
|
const headerOffset = 8 + payloadBytes;
|
||
|
|
if (recordCount <= 0 || recordCount > 0x400) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (payloadBytes < 0 || headerOffset + recordCount * 16 > bytes.length) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
let payloadCursor = 8;
|
||
|
|
const records = [];
|
||
|
|
for (let index = 0; index < recordCount; index += 1) {
|
||
|
|
const descriptorOffset = headerOffset + index * 16;
|
||
|
|
const d4Size = readU32LE(bytes, descriptorOffset);
|
||
|
|
const ccSize = readU32LE(bytes, descriptorOffset + 4);
|
||
|
|
const d0Size = readU32LE(bytes, descriptorOffset + 8);
|
||
|
|
const typeId = readU16LE(bytes, descriptorOffset + 12);
|
||
|
|
const variantTypeId = readU16LE(bytes, descriptorOffset + 14);
|
||
|
|
const endOffset = payloadCursor + ccSize + d0Size + d4Size;
|
||
|
|
if (endOffset > headerOffset) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
records.push({ index, typeId, variantTypeId, ccSize, d0Size, d4Size, payloadCursor });
|
||
|
|
payloadCursor = endOffset;
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
sectionName: section.name,
|
||
|
|
startOffset,
|
||
|
|
recordCount,
|
||
|
|
payloadBytes,
|
||
|
|
headerOffset,
|
||
|
|
records
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function summarizeCandidate(candidate, wantedTypes) {
|
||
|
|
return {
|
||
|
|
sectionName: candidate.sectionName,
|
||
|
|
startOffset: `0x${candidate.startOffset.toString(16)}`,
|
||
|
|
recordCount: candidate.recordCount,
|
||
|
|
payloadBytes: `0x${candidate.payloadBytes.toString(16)}`,
|
||
|
|
wanted: candidate.records
|
||
|
|
.filter((record) => wantedTypes.has(record.typeId) || wantedTypes.has(record.variantTypeId))
|
||
|
|
.map((record) => ({
|
||
|
|
index: record.index,
|
||
|
|
typeId: `0x${record.typeId.toString(16)}`,
|
||
|
|
variantTypeId: `0x${record.variantTypeId.toString(16)}`,
|
||
|
|
ccSize: `0x${record.ccSize.toString(16)}`,
|
||
|
|
d0Size: `0x${record.d0Size.toString(16)}`,
|
||
|
|
d4Size: `0x${record.d4Size.toString(16)}`,
|
||
|
|
payloadCursor: `0x${record.payloadCursor.toString(16)}`
|
||
|
|
}))
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function main() {
|
||
|
|
const filePath = process.argv[2];
|
||
|
|
const wantedTypes = new Set(process.argv.slice(3).map((value) => Number.parseInt(value, 16)));
|
||
|
|
const data = fs.readFileSync(filePath);
|
||
|
|
const parsed = parseLsetWdl(data);
|
||
|
|
const candidates = [];
|
||
|
|
|
||
|
|
for (const section of parsed.sections) {
|
||
|
|
for (let startOffset = 0; startOffset < Math.min(section.size, 0x800); startOffset += 4) {
|
||
|
|
const candidate = parseTypedSection16(data, section, startOffset);
|
||
|
|
if (!candidate) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const summary = summarizeCandidate(candidate, wantedTypes);
|
||
|
|
if (summary.wanted.length > 0) {
|
||
|
|
candidates.push(summary);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(JSON.stringify(candidates, null, 2));
|
||
|
|
}
|
||
|
|
|
||
|
|
main();
|