256 lines
8.0 KiB
JavaScript
256 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 ranges **/
|
||
|
'private const _POS_NAME': [ 0, 40 ],
|
||
|
'private const _POS_AUTHOR': [ 40, 30 ],
|
||
|
'private const _POS_DESC': [ 70, 315 ],
|
||
|
'private const _POS_ID': [ 315, 320 ],
|
||
|
'private const _POS_MOFF': [ 320, 324 ],
|
||
|
|
||
|
/**
|
||
|
* 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 : false;
|
||
|
|
||
|
if ( typeof sgmt === 'string' )
|
||
|
{
|
||
|
sgmt = this.__self.$( sgmt );
|
||
|
}
|
||
|
|
||
|
return String.prototype.substr.apply( ltg_data, sgmt )
|
||
|
.split( '\x00' )[ 0 ];
|
||
|
},
|
||
|
|
||
|
|
||
|
/**
|
||
|
* 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 );
|
||
|
}
|
||
|
} );
|