124 lines
4.1 KiB
JavaScript
124 lines
4.1 KiB
JavaScript
const fs = require("fs");
|
|
|
|
class PopnChart {
|
|
|
|
//offsetKeysounds indicates that any keysound index references
|
|
//within the chart may need to be decremented by one to account
|
|
//for the bgtrack not being at the start of the 2dx container.
|
|
|
|
constructor(filename, offsetKeysounds=false) {
|
|
this.filename = filename;
|
|
this.data = fs.readFileSync(filename);
|
|
|
|
//Check if the chart is newstyle or old>
|
|
//Needed to know event lengths.
|
|
let newFormat = this.checkFormat();
|
|
|
|
this.events = [];
|
|
|
|
//This loop reads through the entire file,
|
|
//rather than ending on an endofsong event.
|
|
let offset = 0;
|
|
while (offset < this.data.length) {
|
|
const eventOffset = this.data.readInt32LE(offset);
|
|
offset += 5;
|
|
const eventFlag = this.data.readInt8(offset);
|
|
offset += 1;
|
|
|
|
let eventParam = 0;
|
|
let eventValue = 0;
|
|
|
|
//In regular events, param and value are 1 byte.
|
|
//However on keysound events, the first 4 bits
|
|
//are used for the param, while the proceeding
|
|
//12 bits are used for the value.
|
|
let joined = this.data.slice(offset, offset+2);
|
|
offset += 2;
|
|
if (eventFlag === 2 || eventFlag === 7) {
|
|
//Endianness needs flipped.
|
|
//This is a terrible way of doing this, I think.
|
|
joined.swap16();
|
|
const hx = joined.toString("hex");
|
|
|
|
eventParam = parseInt(hx.slice(1, 4), 16);
|
|
eventValue = parseInt(hx.slice(0, 1), 16);
|
|
} else {
|
|
eventParam = joined.readUInt8(0);
|
|
eventValue = joined.readUInt8(1);
|
|
}
|
|
|
|
//Long note data isn't needed for GSTs, however it's here.
|
|
if (newFormat) {
|
|
const longNoteData = this.data.readInt32LE(offset);
|
|
offset += 4;
|
|
}
|
|
|
|
this.events.push([eventOffset, eventFlag, eventParam, eventValue]);
|
|
}
|
|
|
|
this.bpm = 0;
|
|
this.bpmTransitions = [];
|
|
|
|
this.playEvents = [];
|
|
this.uniqueKeysounds = [];
|
|
|
|
this.notecount = 0;
|
|
|
|
const sampleColumns = [0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
|
|
for (const event of this.events) {
|
|
let [offset, eventType, param, value] = event;
|
|
|
|
if (eventType == 7 || eventType == 2) {
|
|
if (this.uniqueKeysounds.indexOf(param) == -1) {
|
|
this.uniqueKeysounds.push(param);
|
|
}
|
|
}
|
|
|
|
switch (eventType) {
|
|
case 1:
|
|
//Playable note event.
|
|
//This if is overzealous, just trying to stop BG tracks from being played twice.
|
|
if (sampleColumns[param] != 0) {
|
|
this.playEvents.push([offset, sampleColumns[param]]);
|
|
}
|
|
this.notecount += 1;
|
|
break;
|
|
case 2:
|
|
//Sample change event.
|
|
if (offsetKeysounds) {
|
|
param -= 1;
|
|
}
|
|
sampleColumns[value] = param;
|
|
break;
|
|
case 3:
|
|
//BG track start event.
|
|
this.playEvents.push([offset, 0]);
|
|
break;
|
|
case 4:
|
|
//BPM change event.
|
|
this.bpm = param;
|
|
this.bpmTransitions.push(param);
|
|
break;
|
|
case 7:
|
|
//BG sample event.
|
|
if (offsetKeysounds) {
|
|
param -= 1;
|
|
}
|
|
this.playEvents.push([offset, param]);
|
|
}
|
|
}
|
|
}
|
|
|
|
checkFormat() {
|
|
if (this.data.readInt8(16) == 69) {
|
|
return true;
|
|
} else if (this.data.readInt8(12) == 69) {
|
|
return false;
|
|
} else {
|
|
throw "Chart format not supported.";
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = PopnChart; |