/** * Handles the loading of LTG files * * Copyright (C) 2012 Mike Gerwitz * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * * LTG files contain the game tiles and additional metadata. Specifically, the * structure of the header is: * - name (string), 40 bytes * - author (string), 30 bytes * - description (string), 245 bytes * - id (string), 5 bytes, "LTG1\0" * - mask offset (32-bit integer), 4 bytes (little-endian) * ~ (see TLTGREC struct in LTANK.H of the original game sources) * * Immediately following the header is the game tile set (a bitmap), immediately * after which we find the mask bitmap (at the mask offset). * * In the original game (written in C), the loading of this file into the * necessary data structures was trivial and highly performant. In the case of * ECMAScript, we are left to string parsing. With the string in memory, we will * cut out the necessary segments. * * At that point, we can easily convert the binary bitmap data into usable * images by creating new Image objects in memory and assigning the `src' * attribute to "data:image/bmp;base64,B", where B is the base64-encoded bitmap. * (To help visualize the data, one can open the LTG file in his/her favorite * text editor and search for "BM" (0x424D), which is the header field used to * identify the file as a bitmap image.) */ /** * Loads tiles and metadata from LTG files */ ltjs.LtgLoader = Class( 'LtgLoader', { /** various data segment byte offsets and lengths **/ 'private const _POS_NAME': [ 0, 40 ], 'private const _POS_AUTHOR': [ 40, 30 ], 'private const _POS_DESC': [ 70, 245 ], 'private const _POS_ID': [ 315, 5 ], 'private const _POS_MOFF': [ 320, 4 ], /** * Beginning of game bitmap (one byte past the header) * @type {number} */ 'private const _OFFSET_HEADER_END': 324, /** * Load LTG file from memory and return the raw data * * @param {string} ltg_data binary LTG data * * @return {Object} LTG metadata and bitmaps (sans mask offset) */ 'public fromString': function( ltg_data ) { var mask_offset = this._getMaskOffsetFromData( ltg_data ); return { name: this._getNameFromData( ltg_data ), author: this._getAuthorFromData( ltg_data ), desc: this._getDescFromData( ltg_data ), id: this._getIdFromData( ltg_data ), tiles: this._getBitmapDataUrl( this._getGameBitmap( ltg_data, mask_offset ) ), mask: this._getBitmapDataUrl( this._getMaskBitmap( ltg_data, mask_offset ) ) }; }, /** * Retrieve the requested portion of the given data, optionally stripping * null bytes * * @param {string} ltg_data source LTG data * @param {string} sgmt name of segment to retrieve (constant) * @param {=boolean} stripnull whether to strip null bytes (default true) * * @return {string} requested segment */ 'private _getDataSegment': function( ltg_data, sgmt, stripnull ) { // strip null bytes by default stripnull = ( stripnull === undefined ) ? true : !!stripnull; if ( typeof sgmt === 'string' ) { sgmt = this.__self.$( sgmt ); } var data = String.prototype.substr.apply( ltg_data, sgmt ); return ( stripnull ) ? data.split( '\x00' )[ 0 ] : data; }, /** * Retrieve LTG name from given LTG data * * @param {string} ltg_data raw LTG data * * @return {string} LTG name, null bytes stripped */ 'private _getNameFromData': function( ltg_data ) { return this._getDataSegment( ltg_data, '_POS_NAME' ); }, /** * Retrieve author name from the given LTG data * * @param {string} ltg_data raw LTG data * * @return {string} LTG author, null bytes stripped */ 'private _getAuthorFromData': function( ltg_data ) { return this._getDataSegment( ltg_data, '_POS_AUTHOR' ); }, /** * Retrieve description from the given LTG data * * @param {string} ltg_data raw LTG data * * @return {string} LTG description, null bytes stripped */ 'private _getDescFromData': function( ltg_data ) { return this._getDataSegment( ltg_data, '_POS_DESC' ); }, /** * Retrieve id from the given LTG data * * @param {string} ltg_data raw LTG data * * @return {string} LTG id, null bytes stripped */ 'private _getIdFromData': function( ltg_data ) { return this._getDataSegment( ltg_data, '_POS_ID' ); }, /** * Retrieve mask bitmap offset (relative to beginning of file) from the * given LTG data * * The mask is stored as a 32-bit integer, little-endian. * * @param {string} ltg_data raw LTG data * * @return {number} LTG mask offset in bytes */ 'private _getMaskOffsetFromData': function( ltg_data ) { // grab the data and don't bother stripping off the null bytes (it would // function the same with them stripped, but let's avoid the confusion // since we are supposed to be working with a 32-bit value) var data = this._getDataSegment( ltg_data, '_POS_MOFF', false ), i = data.length, offset = 0; // convert the DWORD entry (little-endian format, 32-bit) into an // integer that we can work with while ( i-- ) { offset += ( data.charCodeAt( i ) << ( 8 * i ) ); } return offset; }, /** * Return data URL for the given bitmap data * * The data URL may be used with any image element in place of an external * resource. It consists of a "data:" prefix, MIME type and the * base64-encoded data. * * @param {string} data binary bitmap data * * @return {string} data URL corresponding to the given bitmap data */ 'private _getBitmapDataUrl': function( data ) { return 'data:image/bmp;base64,' + btoa( data ); }, /** * Extracts the game bitmap from the given LTG data * * While the beginning offset of the game bitmap is static, the end is * determined by the mask offset. The game bitmap would be displayed * properly even if we read to the end of the file, but that is incorrect * and poor practice. * * @param {string} ltg_data raw LTG data * @param {number} mask_offset mask bitmap offset in bytes * * @return {string} game bitmap data */ 'private _getGameBitmap': function( ltg_data, mask_offset ) { var bmp_offset = this.__self.$( '_OFFSET_HEADER_END' ); // return the bitmap data up until the mask offset return ltg_data.substr( bmp_offset, ( mask_offset - bmp_offset ) ); }, /** * Extracts the mask bitmap from the given LTG data * * The mask bitmap position must be provided and consists of the remainder * of the file. * * @param {string} ltg_data raw LTG data * @param {number} mask_offset mask bitmap offset in bytes * * @return {string} mask bitmap data */ 'private _getMaskBitmap': function( ltg_data, mask_offset ) { // the mask bitmap accounts for the remainder of the data return ltg_data.substr( mask_offset ); } } );