@maniajs/gbxparser
Version:
ManiaPlanet GBX File Parser.
436 lines (359 loc) • 13.4 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _bufferReader = require('buffer-reader');
var _bufferReader2 = _interopRequireDefault(_bufferReader);
var _events = require('events');
var _fs = require('fs');
var fs = _interopRequireWildcard(_fs);
var _map = require('./map');
var _map2 = _interopRequireDefault(_map);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
/**
* Map Parser
* @class MapParser
* @type {MapParser}
*
* @property {BufferReader} parser
* @property {{thumb:boolean}} options
*/
var MapParser = function (_EventEmitter) {
_inherits(MapParser, _EventEmitter);
/**
* Parse Map File.
* @param {string|Buffer} fd Could be a file (string) or a buffer.
* @param {{thumb:boolean, body:boolean}} [options] Provide extra options, Turn thumb off/on.. Turn body parsing off/on.
*/
function MapParser(fd, options) {
_classCallCheck(this, MapParser);
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(MapParser).call(this));
_this.map = new _map2.default();
options = options || {};
options.thumb = typeof options.thumb === 'undefined' ? true : options.thumb; // Parse thumb by default!
options.body = typeof options.body === 'undefined' ? true : options.body; // Parse thumb by default!
_this.options = options;
if (fd instanceof Buffer) {
_this.buffer = fd;
} else if (typeof fd === 'string') {
_this.file = fd;
} else {
throw new Error('Please provide a buffer or file path!');
}
return _this;
}
/**
* Parse the Map file.
* After completing it will emit events on the Parser itself.
*
* @returns {Promise}
*/
_createClass(MapParser, [{
key: 'parse',
value: function parse() {
var _this2 = this;
return new Promise(function (resolve, reject) {
return new Promise(function (resolve, reject) {
if (!_this2.buffer && _this2.file) {
fs.readFile(_this2.file, function (err, data) {
if (err) return reject(err);
return resolve(data);
});
} else {
return resolve(_this2.buffer);
}
}).then(function (buffer) {
_this2.parser = new _bufferReader2.default(buffer);
try {
_this2.parser.move(9);
var classId = _this2.parser.nextUInt32LE();
if (classId !== (0x3 << 24 | 0x43 << 12)) {
// engineid << 24, classid << 12.
return reject(new Error('Map is not a valid map class!'));
}
return _this2._parseHeader();
} catch (err) {
return reject(err);
}
}).then(function () {
// Parse body
if (_this2.options.body) {
return _this2._parseBody();
}
return Promise.resolve();
}).then(function () {
// Done, return resolving with the map object.
return resolve(_this2.map);
}).catch(function (err) {
return reject(err);
});
});
}
/**
* Parse Header.
* @returns {Promise}
* @private
*/
}, {
key: '_parseHeader',
value: function _parseHeader() {
var _this3 = this;
var headerLength = this.parser.nextUInt32LE();
var chunkCount = this.parser.nextUInt32LE();
var headerChunks = {};
// Parse chunks (extract from header).
for (var chunkNr = 0; chunkNr < chunkCount; chunkNr++) {
headerChunks[this.parser.nextUInt32LE()] = this.parser.nextUInt32LE() & ~0x80000000;
}
return Object.keys(headerChunks).reduce(function (promise, chunkId) {
return promise.then(function () {
return _this3._parseChunk(chunkId, headerChunks[chunkId]);
});
}, Promise.resolve());
}
/**
* Parse Body (data section).
* @returns {Promise}
* @private
*/
}, {
key: '_parseBody',
value: function _parseBody() {
return Promise.resolve(); // Not implemented.
var instanceCount = this.parser.nextUInt32LE();
var externalInstanceCount = this.parser.nextUInt32LE();
if (externalInstanceCount !== 0) {
return Promise.reject('Body parsing failed, external instances and dependencies not implemented in the parser!');
}
// TODO: Implement body parsing.
// TODO: LZO decompression.
}
/**
* Parse Chunk (will have the switch in it to forward to internal methods)
*
* @param {string} id String format with decimal of chunkid.
* @param {number} size Size in bytes of chunk.
* @returns {Promise}
* @private
*/
}, {
key: '_parseChunk',
value: function _parseChunk(id, size) {
// Clear Lookback
this.parser.resetLookBackStrings();
switch (id) {
// ======== HEADER =========
case '50606082':
// 0x03043002
return this._parseC1(size);
case '50606083':
// 0x03043003
return this._parseC2(size);
case '50606084':
// 0x03043004
return this._parseC3(size);
case '50606085':
// 0x03043005
return this._parseC4(size);
case '50606087':
// 0x03043007
return this._parseC5(size);
case '50606088':
// 0x03043008
return this._parseC6(size);
// ========= DATA ===========
default:
try {
this.parser.move(size); // Skip by default (when not found).
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
}
// Chunk Parsers...........
/**
* Parse First Chunk, time and attribute information in it.
* Part name 'basic'.
* Chunk '50606082' (0x03043002)
*
* @param size
* @returns {Promise}
* @private
*/
}, {
key: '_parseC1',
value: function _parseC1(size) {
// Header
try {
var version = this.parser.nextUInt8();
if (typeof this.debug === 'function') this.debug('Chunk (0x03043002), Version: ' + version);
// times, price, lap, etc.
this.parser.move(4);
this.map.time.bronze = this.parser.nextUInt32LE();
this.map.time.silver = this.parser.nextUInt32LE();
this.map.time.gold = this.parser.nextUInt32LE();
this.map.time.author = this.parser.nextUInt32LE();
this.map.price = this.parser.nextUInt32LE();
this.map.isMultilap = this.parser.nextUInt32LE() === 1;
this.map.type = this.parser.nextUInt32LE();
this.parser.move(4);
this.map.authorScore = this.parser.nextUInt32LE();
this.map.editor = this.parser.nextUInt32LE() === 1 ? 'simple' : 'advanced';
this.parser.move(4);
this.map.checkpoints = this.parser.nextUInt32LE();
this.map.laps = this.parser.nextUInt32LE();
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
/**
* Parse Second Chunk, extended map informations, mostly general/technical.
* Part name 'basic'.
* Chunk '50606083' (0x03043003)
*
* @param size
* @returns {Promise}
* @private
*/
}, {
key: '_parseC2',
value: function _parseC2(size) {
// Header
try {
var version = this.parser.nextUInt8();
if (typeof this.debug === 'function') this.debug('Chunk (0x03043003), Version: ' + version);
this.map.uid = this.parser.nextLookBackString();
this.map.environment = this.parser.nextLookBackString();
this.map.author.login = this.parser.nextLookBackString();
this.map.name = this.parser.nextGbxString();
this.parser.move(5);
this.parser.nextGbxString(); // I don't want that on my baguette!
this.map.mood = this.parser.nextLookBackString();
this.map.decorationEnvironment.id = this.parser.nextLookBackString();
this.map.decorationEnvironment.author = this.parser.nextLookBackString();
this.parser.move(4 * 4 + 16);
this.map.mapType = this.parser.nextGbxString();
this.map.style = this.parser.nextGbxString();
this.parser.move(9);
this.map.title = this.parser.nextLookBackString();
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
/**
* Parse Chunk 3. (skip)
* Chunk '50606084' (0x03043004)
*
* @param size
* @returns {Promise}
* @private
*/
}, {
key: '_parseC3',
value: function _parseC3(size) {
// Header
try {
var version = this.parser.nextUInt8();
if (typeof this.debug === 'function') this.debug('Chunk (0x03043004), Version: ' + version);
this.parser.move(size - 1);
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
/**
* Parse Chunk 4. Header XML
* Chunk '50606085' (0x03043005)
*
* @param size
* @returns {Promise}
* @private
*/
}, {
key: '_parseC4',
value: function _parseC4(size) {
// Header
try {
if (typeof this.debug === 'function') this.debug('Chunk (0x03043005)');
this.map.xml = this.parser.nextGbxString();
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
/**
* Parse Chunk 5. Thumb + Comment
* Chunk '50606087' (0x03043007)
*
* @param size
* @returns {Promise}
* @private
*/
}, {
key: '_parseC5',
value: function _parseC5(size) {
// Header
try {
if (typeof this.debug === 'function') this.debug('Chunk (0x03043007)');
if (this.parser.nextUInt32LE() === 1) {
// Has Thumb.
var thumbSize = this.parser.nextUInt32LE();
this.parser.move(15); // Begin thumb xml tag
if (this.options.thumb) {
this.map.thumb = this.parser.nextBuffer(thumbSize);
} else {
this.parser.move(thumbSize);
}
this.parser.move(16); // </Thumbnail.jpg>
this.parser.move(10); // <Comments>
var commentSize = this.parser.nextUInt32LE();
this.map.comment = '';
if (commentSize > 0) {
this.map.comment = this.parser.nextString(commentSize);
}
this.parser.move(11); // </Comments>
} else {
this.parser.move(size - 4);
}
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
/**
* Parse Chunk 6. Author information.
* Chunk '50606088' (0x03043008)
*
* @param size
* @returns {Promise}
* @private
*/
}, {
key: '_parseC6',
value: function _parseC6(size) {
// Header
try {
var version = this.parser.nextUInt32LE();
if (typeof this.debug === 'function') this.debug('Chunk (0x03043008), Version: ' + version);
this.map.author.version = this.parser.nextUInt32LE();
this.map.author.login = this.parser.nextGbxString();
this.map.author.nickname = this.parser.nextGbxString();
this.map.author.zone = this.parser.nextGbxString();
this.map.author.extra = this.parser.nextGbxString();
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
}
}]);
return MapParser;
}(_events.EventEmitter);
exports.default = MapParser;