1
0
Fork 0
lasertank-js/lib/LtgLoader.js

259 lines
8.0 KiB
JavaScript

/**
* 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 <http://www.gnu.org/licenses/>.
*
*
* 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 );
}
} );