270 lines
No EOL
6.2 KiB
JavaScript
270 lines
No EOL
6.2 KiB
JavaScript
const MAP_START_MARKER = 0x33;
|
|
|
|
class P5File {
|
|
/** @type {String} */ data
|
|
/** @type {File} */ file
|
|
/** @type {String} */ name
|
|
/** @type {Number} */ size
|
|
/** @type {String} */ type
|
|
/** @type {String?} */ subtype
|
|
}
|
|
|
|
class BinUtils {
|
|
/**
|
|
*
|
|
* @param {DataView} view
|
|
*/
|
|
constructor(view) {
|
|
this.offset = 0;
|
|
this.view = view;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Number} offset
|
|
*/
|
|
seek(offset) {
|
|
this.offset = offset;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {Number}
|
|
*/
|
|
readInt() {
|
|
const out = this.view.getInt32(this.offset);
|
|
this.offset += 4;
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {Number}
|
|
*/
|
|
readShort() {
|
|
const out = this.view.getInt16(this.offset);
|
|
this.offset += 2;
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {Number}
|
|
*/
|
|
readByte() {
|
|
const out = this.view.getInt8(this.offset);
|
|
this.offset += 1;
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {Number}
|
|
*/
|
|
peekInt() {
|
|
return this.view.getInt32(this.offset);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {Number}
|
|
*/
|
|
peekShort() {
|
|
return this.view.getInt16(this.offset);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @returns {Number}
|
|
*/
|
|
peekByte() {
|
|
return this.view.getInt8(this.offset);
|
|
}
|
|
}
|
|
|
|
class Point {
|
|
/** @type {Number} */ x;
|
|
/** @type {Number} */ y;
|
|
|
|
/**
|
|
*
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
constructor(x, y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
}
|
|
|
|
class AABB {
|
|
/** @type {Point} */ topLeft;
|
|
/** @type {Point} */ bottomRight;
|
|
}
|
|
|
|
class TrackData {
|
|
/** @type {Point} */ playerStartPos;
|
|
/** @type {Point} */ finishPos;
|
|
/** @type {Number} */ pointsCount;
|
|
/** @type {Point[]} */ mapPoints;
|
|
/** @type {AABB?} */ _trackAABB = null;
|
|
|
|
/**
|
|
* @returns {AABB}
|
|
*/
|
|
getAABB() {
|
|
if (this._trackAABB === null) {
|
|
let lx, mx = lx = 0;
|
|
let ly, my = ly = 0;
|
|
|
|
this.mapPoints.forEach(pt => {
|
|
if (pt.x < lx) {
|
|
lx = pt.x;
|
|
} else if (pt.x > mx) {
|
|
mx = pt.x;
|
|
}
|
|
|
|
if (pt.y < ly) {
|
|
ly = pt.y;
|
|
} else if (pt.y > my) {
|
|
my = pt.y;
|
|
}
|
|
});
|
|
|
|
this._trackAABB = new AABB();
|
|
this._trackAABB.topLeft = new Point(lx, my);
|
|
this._trackAABB.bottomRight = new Point(mx, ly);
|
|
}
|
|
|
|
return this._trackAABB;
|
|
}
|
|
}
|
|
|
|
class TrackName {
|
|
/** @type {Number} */ offset;
|
|
/** @type {String} */ name;
|
|
/** @type {TrackData} */ trackData;
|
|
|
|
/**
|
|
*
|
|
* @param {String} name
|
|
* @param {Number} offset
|
|
*/
|
|
constructor(name, offset) {
|
|
this.name = name;
|
|
this.offset = offset;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {TrackData} trackData
|
|
*/
|
|
setTrackData(trackData) {
|
|
this.trackData = trackData;
|
|
}
|
|
}
|
|
|
|
class Level {
|
|
/** @type {Number} */ count;
|
|
/** @type {TrackName[]} */ tracks;
|
|
|
|
/**
|
|
*
|
|
* @param {Number} tracksCount
|
|
*/
|
|
constructor(tracksCount) {
|
|
this.count = tracksCount;
|
|
this.tracks = Array(tracksCount);
|
|
}
|
|
}
|
|
|
|
class MRGFile {
|
|
/** @type {Level[]} */ levels;
|
|
|
|
constructor() {
|
|
this.levels = Array(3);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Number} levelNum
|
|
* @param {Number} trackNum
|
|
* @returns {TrackName}
|
|
*/
|
|
getTrack(levelNum, trackNum) {
|
|
return this.levels[levelNum].tracks[trackNum];
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Uint8Array} bytes
|
|
* @returns MRGFile
|
|
*/
|
|
static parse(bytes) {
|
|
const view = new DataView(bytes.buffer, 0);
|
|
const reader = new BinUtils(view);
|
|
let mrgFile = new MRGFile();
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const tn = reader.readInt();
|
|
|
|
const level = new Level(tn);
|
|
for (let j = 0; j < tn; j++) {
|
|
const trackOffset = reader.readInt();
|
|
let titleBytes = [];
|
|
while (reader.peekByte() !== 0) {
|
|
let byte = reader.readByte();
|
|
titleBytes.push(byte);
|
|
}
|
|
reader.readByte();
|
|
let trackName = String.fromCharCode(...titleBytes);
|
|
|
|
level.tracks[j] = new TrackName(trackName, trackOffset);
|
|
}
|
|
mrgFile.levels[i] = level;
|
|
}
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
for (let j = 0; j < mrgFile.levels[i].count; j++) {
|
|
let track = mrgFile.levels[i].tracks[j];
|
|
reader.seek(track.offset);
|
|
if (reader.peekByte() !== MAP_START_MARKER) {
|
|
console.warn(`Corrupted map data at offset ${track.offset}`);
|
|
continue;
|
|
}
|
|
reader.readByte();
|
|
|
|
let trackData = new TrackData();
|
|
trackData.playerStartPos = new Point(
|
|
reader.readInt() >> 16 << 3, reader.readInt() >> 16 << 3
|
|
);
|
|
trackData.finishPos = new Point(
|
|
reader.readInt() >> 16 << 3, reader.readInt() >> 16 << 3
|
|
);
|
|
trackData.pointsCount = reader.readShort();
|
|
trackData.mapPoints = Array(trackData.pointsCount);
|
|
trackData.mapPoints[0] = new Point(
|
|
reader.readInt(), reader.readInt()
|
|
);
|
|
for (let i = 1; i < trackData.pointsCount; i++) {
|
|
let ppx = reader.readByte();
|
|
let ppy = 0;
|
|
if (ppx === -1) {
|
|
ppx = reader.readInt();
|
|
ppy = reader.readInt();
|
|
} else {
|
|
ppx += trackData.mapPoints[i-1].x;
|
|
ppy = trackData.mapPoints[i-1].y + reader.readByte();
|
|
}
|
|
trackData.mapPoints[i] = new Point(
|
|
ppx, ppy
|
|
);
|
|
}
|
|
|
|
track.setTrackData(trackData);
|
|
}
|
|
}
|
|
|
|
return mrgFile;
|
|
}
|
|
} |