Skip to content

Instantly share code, notes, and snippets.

@stijnstijn
Created December 1, 2017 02:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stijnstijn/d9caff3373dc61e21b5e9ed29d68ceb1 to your computer and use it in GitHub Desktop.
Save stijnstijn/d9caff3373dc61e21b5e9ed29d68ceb1 to your computer and use it in GitHub Desktop.
json level info
<?php
/**
* level.php
*
* Provides an interface to read Level (J2L) files.
*
* LICENSE:
*
* Copyright (C) 2009 Stijn Peeters
*
* DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
*
* 0. You just DO WHAT THE FUCK YOU WANT TO.
*
* @package J2Ov4
* @author stijn
*/
/**
* Set PHP memory limit and execution time, as preview generation may take a while.
*/
ini_set('memory_limit', '512M');
ini_set('max_execution_time', 60);
/**
* Level class
*
* Reads and interprets Jazz Jackrabbit 2 Level files
*
* @package J2Ov4
* @author Stijn Peeters <mrbartjens@gmail.com>
* @copyright Copyright (c) 2009, Stijn Peeters
* @version 0.5
* @todo Manipulating level properties & repacking the level afterwards.
*/
class Level {
/**
* @var int The handle to the Level tile to read from.
* @access private
*/
private $rResource;
/**
* @var mixed Image Data header bytes (raw pixels).
* @access private
*/
private $_Header = false;
/**
* @var mixed Level info header bytes.
* @access private
*/
private $_LevelInfo = false;
/**
* @var mixed Level event data bytes.
* @access private
*/
private $_EventData = false;
/**
* @var mixed Tile dictionary bytes.
* @access private
*/
private $_Dictionary = false;
/**
* @var mixed Tile cache bytes.
* @access private
*/
private $_TileCache = false;
/**
* @var int Quick Preview image (GDLib resource).
* @access private
*/
private $rQuickPreview;
/**
* @var int Real Preview image (GDLib resource).
* @access private
*/
private $rRealPreview;
/**
* @var array Level information such as tileset, settings, name, etc
* @access private
*/
private $aLevelInfo = array();
/**
* @var array Parsed dictionary data to be used in rendering the level.
* @access private
*/
private $aWords = array();
/**
* @var array The version of the level.
* @access private
*/
private $sVersion = '';
/**
* @var array Byte offsets for several parts of the level. Available keys:
* <ul>
* <li><samp>info_c</samp>: Level Info compressed size.</li>
* <li><samp>info_u</samp>: Level Info uncompressed size.</li>
* <li><samp>evnt_c</samp>: Event data compressed size.</li>
* <li><samp>evnt_u</samp>: Event data uncompressed size.</li>
* <li><samp>dict_c</samp>: Dictionary compressed size.</li>
* <li><samp>dict_u</samp>: Dictionary uncompressed size.</li>
* <li><samp>tile_c</samp>: Tile cache compressed size.</li>
* <li><samp>tile_u</samp>: Tile cache uncompressed size.</li>
* </ul>
* @access private
*/
private $aOffsets = array();
/**
* @var boolean Whether debug messages should be printed or not.
* @access private
*/
private $bDebug = false;
/**
* @var boolean Whether "interactive mode" is enabled. "Interactive mode" needs
* to be enabled to edit the level.
*/
private $bInteractiveMode = false;
/**
* Size of the Level header
*/
const SIZE_HEADER = 262;
/**
* Every tile will be this amount pixels large (width & height) in the preview
*/
const PREVIEW_SCALE = 8;
/**
* Max width/height of the image, to prevent out of memory problems
*/
const MAX_DIMENSION = 9500;
/**
* Level header structure
*/
const LEVL_HEADER = 'A180Copyright/a4Signature/c3PasswordHash/cHideLevel/a32LevelName/sVersion/lFileSize/lCrc/lInfoStreamCompressed/lEventStreamCompressed/lDictStreamCompressed/lCacheStreamCompressed/lInfoStreamUncompressed/lEventStreamUncompressed/lDictStreamUncompressed/lCacheStreamUncompressed';
const LEVL_HEADER_STRUCT = 'A180/a4/c/c/c/c/a32/s/l/l/l/l/l/l/l/l/l/l';
/**
* Level info structure (see php's pack() function for syntax info)
*/
const LEVL_STRUCT = 'xWTF/
vJcsHorizontal/
vSecurityEnvelope1/
vJcsVertical/
CSecurityEnvelope2/
CSecEnvAndLayer/
CMinimumAmbient/
CStartingAmbient/
vAnimsUsed/
CSplitScreenDivider/
CIsItMultiplayer/
VStreamSize/
a32LevelName/
a32Tileset/
a32BonusLevel/
a32NextLevel/
a32SecretLevel/
a32MusicFile/
A8192HelpStrings/
V8LayerProperties/
x8LayerUnknown1/
C8IsLayerUsed/
V8JcsLayerWidth/
V8LayerWidth/
V8LayerHeight/
x16LayerUnknown2/
x32LayerUnknown3/
x32LayerUnknown4/
x24LayerUnknown5/
l8LayerXSpeed/
l8LayerYSpeed/
l8LayerAutoXSpeed/
l8LayerAutoYSpeed/
x8LayerUnknown6/
C3LayerRGB1/
C3LayerRGB2/
C3LayerRGB3/
C3LayerRGB4/
C3LayerRGB5/
C3LayerRGB6/
C3LayerRGB7/
C3LayerRGB8/
vStaticTiles';
/**
* This is the directory the class will look in for cached tileset images
*/
const CACHE_DIR = 'tilesets';
/**
* This is the directory the class will look in for tilesets if no image is found
*/
const ROOT_DIR = 'downloads';
/**
* Magic byte value for TSF tilesets
*/
const VERSION_TSF = 515;
/**
* Magic byte value for 1.23 tilesets
*/
const VERSION_123 = 514;
/**
* Magic value for Next Level setting
*/
const NEXTLEVEL = 1;
private $bValid = false;
/**
* Constructor method
*
* Sets up the object, checking whether given file path is valid and giving several
* variables initial values. Then it reads the header via another method.
*
* @param string $sFilename The file path pointing to the Level to use.
* @param boolean $bDebug Initial debug mode setting. Defaults to false. If
* enabled, textual debug messages may be shown.
*
* @uses Level::$bDebug To store debugging settings.
* @uses Level::getHeader() To read the Level header subsequently.
*
* @access public
*/
public function __construct($sFilename, $bDebug = false) {
if(!is_readable($sFilename)) {
$this->debug('Level file not found');
return false;
}
if(filesize($sFilename) == 0) {
$this->debug('Level file is zero bytes');
return false;
}
$this->bDebug = $bDebug;
$this->sPath = $sFilename;
$this->rResource = fopen($this->sPath, 'rb');
$this->bValid = ($this->rResource !== false);
$this->rQuickPreview = false;
$this->rRealPreview = false;
$this->getHeader();
$this->debug('Opened level file '.$sFilename);
}
public function isValid() {
return $this->bValid;
}
/**
* Retrieve Level header
*
* The Level header (first 262 bytes) contains offsets for the actual Level data
* which is retrieved here, and stored in the offsets array. The header is saved too
* for later usage.
*
* @uses Level::$aOffsets To store the offsets found.
* @uses Level::$_Header To store the Level header.
* @uses Level::$rResource To read from the Level file.
*
* @access private
*/
private function getHeader() {
$this->_Header = fread($this->rResource, self::SIZE_HEADER);
$this->aOffsets = array();
list(,$this->aOffsets['info_c']) = unpack('V', substr($this->_Header, 230, 4));
list(,$this->aOffsets['info_u']) = unpack('V', substr($this->_Header, 234, 4));
list(,$this->aOffsets['evnt_c']) = unpack('V', substr($this->_Header, 238, 4));
list(,$this->aOffsets['evnt_u']) = unpack('V', substr($this->_Header, 242, 4));
list(,$this->aOffsets['dict_c']) = unpack('V', substr($this->_Header, 246, 4));
list(,$this->aOffsets['dict_u']) = unpack('V', substr($this->_Header, 250, 4));
list(,$this->aOffsets['tile_c']) = unpack('V', substr($this->_Header, 254, 4));
list(,$this->aOffsets['tile_u']) = unpack('V', substr($this->_Header, 258, 4));
list(,$this->sVersion) = unpack('v', substr($this->_Header, 220, 2));
if(!strpos($this->_Header, 'MegaGames')) { //crude integrity check
$this->debug('Missing or corrupt file header');
$this->bValid = false;
} else {
$this->debug('Succesfully unpacked file header');
}
}
/**
* Get level info data
*
* Uncompresses the first data stream, the level info. The result is stored for
* later retrieval.
*
* @returns string The raw byte-for-byte level info stream.
*
* @uses Level::$_LevelInfo To store the level info.
*
* @access private
*/
private function getInfoData() {
if(!$this->_LevelInfo) {
$this->_LevelInfo = gzuncompress(fread($this->rResource, $this->aOffsets['info_c']));
$this->debug('Decompressed level info (size: '.strlen($this->_LevelInfo).')');
}
return $this->_LevelInfo;
}
/**
* Get event data
*
* Uncompresses the second data stream, the event data. The result is stored for
* later retrieval. If the previous stream was not read yet it will be read and
* uncompressed first.
*
* @returns string The raw byte-for-byte event data stream.
*
* @uses Level::$_EventData To store the event data.
*
* @access private
*/
private function getEventData() {
if(!$this->_LevelInfo) {
$this->getInfoData();
}
if(!$this->_EventData) {
$this->_EventData = gzuncompress(fread($this->rResource, $this->aOffsets['evnt_c']));
$this->debug('Decompressed event data (size: '.strlen($this->_EventData).')');
}
return $this->_EventData;
}
/**
* Get dictionary
*
* Uncompresses the third data stream, the word data. The result is stored for
* later retrieval. If the previous stream was not read yet it will be read and
* uncompressed first.
*
* @returns string The raw byte-for-byte dictionary.
*
* @uses Level::$_LevelInfo To store the dictionary.
*
* @access private
*/
private function getWordData() {
if(!$this->_EventData) {
$this->getEventData();
}
if(!$this->_Dictionary) {
$this->_Dictionary = gzuncompress(fread($this->rResource, $this->aOffsets['dict_c']));
$this->debug('Decompressed tile dictionary (size: '.strlen($this->_Dictionary).')');
}
return $this->_Dictionary;
}
/**
* Get tile cache
*
* Uncompresses the fourth data stream, the tile cache. The result is stored for
* later retrieval. If the previous stream was not read yet it will be read and
* uncompressed first.
*
* @returns string The raw byte-for-byte tile cache.
*
* @uses Level::$_TileCache To store the tile cache.
*
* @access private
*/
private function getCacheData() {
if(!$this->_Dictionary) {
$this->getWordData();
}
if(!$this->_TileCache) {
$this->_TileCache = gzuncompress(fread($this->rResource, $this->aOffsets['tile_c']));
fclose($this->rResource);
unset($this->rResource);
$this->debug('Decompressed tile cache (size: '.strlen($this->_TileCache).')');
}
return $this->_TileCache;
}
/**
* Get words
*
* Converts the raw word dictionary into a usable format for parsing the
* tile cache. Flipped tiles are ignored for now. The result is stored
* for later retrieval.
*
* @returns array Each value in this array is an array with
* four values, with each value being a
* tile index.
*
* @uses Level::$aOffsets To determine to which point to read.
* @uses Level::$_Dictionary To read the word data from.
* @uses Level::$aWords To store the word array.
*
* @access private
*/
private function getWords() {
if(!$this->_Dictionary) {
$this->getWordData();
}
if($this->aWords) {
$this->debug('Returning cached word map');
return $this->aWords;
}
$aLevelData = $this->getLevelInfo();
$iTiles = ($this->sVersion == self::VERSION_TSF) ? 4096 : 1024;
$iAnimTiles = ($this->sVersion == self::VERSION_TSF) ? 256 : 128;
//map animated tiles (first frame is saved since a static image is generated)
$iAnims = $iTiles - $aLevelData['StaticTiles'];
$sAnims = substr($this->_LevelInfo, 8813+(7*$iTiles), (137*$iAnimTiles));
$aAnims = array();
for($i=0;$i<$iAnims;$i++) {
list(,$iFrame) = unpack('v', substr($sAnims, ($i*137)+9, 2)); //anim description = 137 bytes
$aAnims[] = $iFrame;
}
//map tile types (translucent tiles)
$sType = substr($this->_LevelInfo, 8813+(5*$iTiles), $iTiles);
$aType = array();
for($i=0;$i<$iTiles;$i++) {
list(,$aType[]) = unpack('C', substr($sType, $i, 1));
}
$aWords = array();
for($i=0; $i < $this->aOffsets['dict_u']; $i++) {
$iWord = floor($i/8);
$sWord = substr($this->_Dictionary, ($iWord * 8), 8);
for($j=0;$j<4;$j++) {
list(,$iTile) = unpack('v', substr($sWord, ($j*2), 2));
$bFlipped = false;
$bAnimated = false;
if($iTile > $iTiles) { //flipped
$iTile -= $iTiles;
$bFlipped = true;
}
if($iTile >= $aLevelData['StaticTiles']) {
$iTile = $aAnims[$iTile-$aLevelData['StaticTiles']];
}
$aWords[$iWord][$j] = array('tile' => $iTile, 'flipped' => $bFlipped, 'animated' => $bAnimated, 'translucent' => ($aType[$iTile]==1));
}
}
$this->aWords = $aWords;
return $this->aWords;
}
/**
* Build J2L file
*
* Builds a new J2L file from the previously uncompressed data.
*
* @returns string The raw file
*
* @access public
*/
public function repack() {
$sJ2L = '';
if(!$this->_TileCache) {
$this->getCacheData();
}
$this->debug('Level Info: '.strlen($this->_LevelInfo).' uncompressed, '.strlen(gzcompress($this->_LevelInfo)).' compressed - should be '.$this->aOffsets['info_u'].'/'.$this->aOffsets['info_c'].'<br>'.'Event Data: '.strlen($this->_EventData).' uncompressed, '.strlen(gzcompress($this->_EventData)).' compressed - should be '.$this->aOffsets['evnt_u'].'/'.$this->aOffsets['evnt_c'].'<br>'.'Dictionary: '.strlen($this->_Dictionary).' uncompressed, '.strlen(gzcompress($this->_Dictionary)).' compressed - should be '.$this->aOffsets['dict_u'].'/'.$this->aOffsets['dict_c'].'<br>'.'Tile Cache: '.strlen($this->_TileCache).' uncompressed, '.strlen(gzcompress($this->_TileCache)).' compressed - should be '.$this->aOffsets['tile_u'].'/'.$this->aOffsets['tile_c'].'<br>');
$aHeader = unpack(Level::LEVL_HEADER, $this->_Header);
$sCmpLevelInfo = gzcompress($this->_LevelInfo, 8);
$sCmpEventData = gzcompress($this->_EventData, 8);
$sCmpDictionary = gzcompress($this->_Dictionary, 8);
$sCmpTileCache = gzcompress($this->_TileCache, 8);
$aHeader['Crc'] = $this->getCRC32(array($sCmpLevelInfo, $sCmpEventData, $sCmpDictionary, $sCmpTileCache));
$aHeader['FileSize'] = 262+strlen($sCmpLevelInfo.$sCmpEventData.$sCmpDictionary.$sCmpTileCache);
$aHeader['InfoStreamCompressed'] = strlen($sCmpLevelInfo);
$aHeader['EventStreamCompressed'] = strlen($sCmpEventData);
$aHeader['DictStreamCompressed'] = strlen($sCmpDictionary);
$aHeader['CacheStreamCompressed'] = strlen($sCmpTileCache);
$aHeader['InfoStreamUncompressed'] = strlen($this->_LevelInfo);
$aHeader['EventStreamUncompressed'] = strlen($this->_EventData);
$aHeader['DictStreamUncompressed'] = strlen($this->_Dictionary);
$aHeader['CacheStreamUncompressed'] = strlen($this->_TileCache);
$sJ2L = Misc::repack(Level::LEVL_HEADER_STRUCT, $aHeader);
$sJ2L .= $sCmpLevelInfo;
$sJ2L .= $sCmpEventData;
$sJ2L .= $sCmpDictionary;
$sJ2L .= $sCmpTileCache;
return $sJ2L;
}
public function getCRC32($aData) {
$iCRC = 0;
for($i=0;$i<4;$i++) {
$iCRC = Misc::crc32c($iCRC, $aData[$i], strlen($aData[$i]));
}
return $iCRC;
}
/**
* Edit level info
*
* Changes level info such as next level or music file. Currently supports the
* following options:
*
* - <samp>Level::NEXTLEVEL</samp>: Next level setting (32 bytes)
*
* If a value is longer or shorter than the allowed length, it will be truncated
* or expanded.
*
* @param int $iKey The field to be edited. See above for allowed
* values.
* @param mixed $mValue The new value. May be filtered to fit the given
* field.
*
* @uses Level::$_LevelInfo To alter the level data.
*
* @todo Add more fields that can be edited.
*
* @access public
*/
public function setInfo($iKey, $mValue) {
if(!$this->_TileCache) {
$this->getCacheData();
}
switch($iKey) {
default:
return false;
case self::NEXTLEVEL:
$this->_LevelInfo = substr($this->_LevelInfo, 0, 114).str_pad($mValue, "\0").substr($this->_LevelInfo, 146);
return true;
}
}
/**
* Get level info
*
* Converts the raw level info into a usable array. The result is stored
* for later retrieval.
*
* @returns array Level info. Keys correspond to those used
* in the J2NSM file format specification,
* ignoring unknown values and storing
* layer-specific data in an array for that
* layer.
*
* @uses Level::LEVL_STRUCT To parse the binary info.
* @uses Level::$_LevelInfo To read the level info from.
* @uses Level::$aLevelInfo To store the level info array.
*
* @access public
*/
public function getLevelInfo($mReturn = false) {
if(!$this->bValid || empty($this->sPath)) {
return false;
}
if($this->aLevelInfo) {
if($mReturn == 'layers') {
return $this->aLevelInfo['layers'];
}
return $this->aLevelInfo;
}
if(!$this->_LevelInfo) {
$this->getInfoData();
}
$sFormat = str_replace(array("\n", "\r", ' '), '', self::LEVL_STRUCT);
$aLevelInfo = unpack($sFormat, $this->_LevelInfo);
for($i=0;$i<16;$i++) { //help strings, 16 of 512 bytes each
$aLevelInfo['HelpString'][$i] = trim(substr($aLevelInfo['HelpStrings'], $i*512, 512));
}
unset($aLevelInfo['HelpStrings']);
for($i=1;$i<9;$i++) { //make an array with data for each layer
foreach(array('LayerProperties', 'IsLayerUsed', 'JcsLayerWidth', 'LayerWidth', 'LayerHeight', 'LayerXSpeed', 'LayerYSpeed', 'LayerAutoXSpeed', 'LayerAutoYSpeed') as $sLayerKey) {
$aLevelInfo['layers'][$i][$sLayerKey] = $aLevelInfo[$sLayerKey.$i];
unset($aLevelInfo[$sLayerKey.$i]);
}
//convert RGB (textured mode fade) values to arrays
$aLevelInfo['layers'][$i]['rgb'] = array($aLevelInfo['LayerRGB'.$i.'1'], $aLevelInfo['LayerRGB'.$i.'2'], $aLevelInfo['LayerRGB'.$i.'3']);
unset($aLevelInfo['LayerRGB'.$i.'1'], $aLevelInfo['LayerRGB'.$i.'2'], $aLevelInfo['LayerRGB'.$i.'3']);
//boolean layer properties (bits)
$aLevelInfo['layers'][$i]['LayerProperties'] = array(
'TileWidth' => (($aLevelInfo['layers'][$i]['LayerProperties'] & 1) == 1),
'TileHeight' => (($aLevelInfo['layers'][$i]['LayerProperties'] & 2) == 2),
'LimitVisibleRegion' => (($aLevelInfo['layers'][$i]['LayerProperties'] & 4) == 4),
'TextureMode' => (($aLevelInfo['layers'][$i]['LayerProperties'] & 8) == 8),
'ParallaxStars' => (($aLevelInfo['layers'][$i]['LayerProperties'] & 16) == 16));
}
$this->aLevelInfo = $aLevelInfo;
if($mReturn == 'layers') {
return $this->aLevelInfo['layers'];
}
return $this->aLevelInfo;
}
/**
* Get layer map
*
* Parses the binary Tile Cache data to be an array of word indexes for a
* specific layer.
*
* @param int $iLayer The layer to parse. Layers before it are
* ignored.
*
* @returns array The word map, each value being an index
* referencing a word in the dictionary.
*
* @uses Level::getLevelInfo() To retrieve layer size, and henceforth
* skip a layer.
* @uses Level::getCacheData() To get the binary data to parse.
*
* @see Level::$_Dictionary Indexes in the returned array point to a
* word in the dictionary.
*
* @access private
*/
private function getLayerMap($iLayer) {
$aLayers = $this->getLevelInfo('layers');
//calculate offset to read from; we're not interested in layers prior to $iLayer
$iOffset = 0;
for($i=1;$i<$iLayer;$i++) {
if(!$aLayers[$i]['IsLayerUsed']) {
$this->debug('Skipping layer '.$i);
continue;
}
$iOffset += 2*(ceil($aLayers[$i]['LayerWidth']/4)*$aLayers[$i]['LayerHeight']);
}
$iWords = ceil($aLayers[$iLayer]['LayerWidth']/4)*$aLayers[$iLayer]['LayerHeight'];
$aMap = unpack('v*', substr($this->getCacheData(), $iOffset, ($iWords*2)));
return $aMap;
}
/**
* Get quick preview image
*
* This generates a low-resolution (one pixel per tile) preview image
* of a layer based on the boolean mask for the used tileset.
*
* @returns integer The image resource, to be used with
* for example imagepng()
*
* @param integer $iLayer Which layer to render. Defaults to 4.
*
* @uses Tileset To retrieve tileset information.
* @uses Tileset::getBooleanMask() To retrieve mask information.
* @uses Level::getWords() To parse the tile cache with.
* @uses Level::getLevelInfo() To determine layer size.
* @uses Level::getLayerMap() To retrieve the tile map for the background layer.
*
* @access public
*/
public function getQuickPreview($iLayer = 4, $iZoom = 1) {
if(!$this->bValid) return false;
if($this->rQuickPreview) {
return $this->rQuickPreview;
}
//get data; words, level info, tileset mask
$aWords = $this->getWords();
$aLayers = $this->getLevelInfo();
$sTileset = $aLayers['Tileset'];
if(!stripos($sTileset, '.j2t')) $sTileset .= '.j2t';
$oTileset = new Tileset(self::ROOT_DIR.'tilesets/'.$sTileset);
$aMask = $oTileset->getBooleanMask();
//create image and fill it with "JCS Blue"; allocate a color for masked tiles (black)
$rImage = imagecreatetruecolor($aLayers['layers'][$iLayer]['LayerWidth'], $aLayers['layers'][$iLayer]['LayerHeight']);
imagefill($rImage, 1, 1, imagecolorallocate($rImage, 87, 0, 203));
$iBlack = imagecolorallocate($rImage, 0, 0, 0);
//if the level width is not a multiple of 4, using it for positioning will mess stuff up, so use another value
$aMap = $this->getLayerMap($iLayer);
$iRealWidth = ceil($aLayers['layers'][$iLayer]['LayerWidth']/4)*4;
$i = 0;
//loop through the words
foreach($aMap as $iWord) {
$aWord = $aWords[$iWord];
foreach($aWord as $iTile) {
if($aMask[$iTile]) { //if the tile referenced is solid, draw a pixel
imagesetpixel($rImage, ($i % $iRealWidth), floor($i/$iRealWidth), $iBlack);
}
$i++;
}
}
$this->rQuickPreview = $rImage;
unset($aWords, $aLayers, $oTileset, $aMask, $rImage);
return $this->rQuickPreview;
}
/**
* Get real preview image
*
* This generates a "real" preview image, scaled, using the actual
* tileset image. All layers with both x and y speed 1 are rendered
* on top of each other, in reverse order, so the level looks more
* or less like what it would like like in JCS.
*
* @returns integer The image resource, to be used with
* for example imagepng()
*
* @param mixed $mLayer Which layer to render. Can be either an
* integer or an array; if it is an array,
* each element should be n with 8 > n > 0.
* All layers referenced in the array are
* rendered; this is the default.
* @param boolean $bDoBackgroundPass Whether to render the level background or
* not; defaults to <samp>true</samp>.
*
* @uses Tileset To retrieve tileset information.
* @uses Tileset::getTilesetImage() To retrieve the tileset image.
* @uses Level::getLayerMap() To retrieve the tile map for the background layer.
* @uses Level::CACHE_DIR To find a cached tileset image.
* @uses Level::MAX_DIMENSION To find out the max image size.
*
* @access public
*/
public function getRealPreview($mLayer = array(7,6,5,4,3,2,1), $bDoBackgroundPass = true) {
global $db;
$this->debug('Generating fullsize level preview');
if(!$this->bValid) {
$this->debug('Invalid level file; could not generate preview');
return false;
}
if($this->rRealPreview) {
$this->debug('Returning cached fullsize level preview');
return $this->rRealPreview;
}
$aLayers = $this->getLevelInfo();
$iWidth = ($aLayers['layers'][4]['LayerWidth']*32) > self::MAX_DIMENSION ? self::MAX_DIMENSION : ($aLayers['layers'][4]['LayerWidth']*32);
$iHeight = ($aLayers['layers'][4]['LayerHeight']*32) > self::MAX_DIMENSION ? self::MAX_DIMENSION : ($aLayers['layers'][4]['LayerHeight']*32);
$rImage = imagecreatetruecolor($iWidth, $iHeight);
//look for cached tileset image, if nonexistant, create it (but dont cache, not our responsibility)
$sTileset = trim($aLayers['Tileset']);
if(!stripos($sTileset, '.j2t')) $sTileset .= '.j2t';
$oTileset = false;
$sPreview = trim(str_ireplace('.j2t', '.png', $sTileset));
if(is_file(self::CACHE_DIR.$sPreview) && is_readable(self::CACHE_DIR.$sPreview)) {
$this->debug('Using cached tileset image for tileset '.$sTileset);
$rTileset = imagecreatefrompng(self::CACHE_DIR.$sPreview);
} else {
$this->debug('Generating new tileset image for tileset '.$sTileset);
$oTileset = new Tileset(self::ROOT_DIR.'tilesets/'.$sTileset);
if(!$oTileset->bValid) { //if tileset couldnt be loaded, chances are that it's stored with different casing; do an exhaustive dir search
$this->debug('Could not load file '.$sTileset.'; scanning for alternatives');
$aFiles = scandir(self::ROOT_DIR.'tilesets/');
foreach($aFiles as $sName) {
$sName = trim($sName);
if(strtolower($sName) == strtolower($sTileset)) {
$oTileset = new Tileset(self::ROOT_DIR.'tilesets/'.$sName);
break;
}
}
}
if($oTileset->bValid) {
$rTileset = $oTileset->getTilesetImage();
} else {
$this->debug('Could not locate tileset file '.$sTileset.' or any similar filenames');
return false;
}
}
//render the background first
if($bDoBackgroundPass) {
$this->renderBackground($rImage, $rTileset);
}
if(is_array($mLayer)) {
foreach($mLayer as $iLayer) {
//render only layers with x and y speed = 1
if($aLayers['layers'][$iLayer]['IsLayerUsed'] && $aLayers['layers'][$iLayer]['LayerXSpeed'] == 65536 && $aLayers['layers'][$iLayer]['LayerXSpeed']==$aLayers['layers'][$iLayer]['LayerYSpeed']) {
$this->debug('Rendering layer '.$iLayer);
$this->renderLayer($iLayer, $rImage, $rTileset);
}
}
} else {
$this->renderLayer($mLayer, $rImage, $rTileset);
}
$rImage = $this->scaleImage($rImage);
$this->rRealPreview = $rImage;
imagedestroy($rTileset);
unset($aLayers, $oTileset);
return $this->rRealPreview;
}
/**
* Render background layer
*
* This is a separate method, distinct from {@link Level::renderLayer()} because it
* works slightly differently. The background layer contents are rendered to a
* separate image, which is then pasted all over the level image, repeating itself
* until the image is filled. Transparent parts will be "JCS Blue".
*
* @param int $rImage The level preview image to copy the background to.
* @param int $rTileset The tileset image to copy tiles from.
*
* @uses Level::getLevelInfo() To retrieve layer information to parse.
* @uses Level::getLayerMap() To retrieve the tile map for the background layer.
* @uses Level::getWords() To map the tile cache to an image.
*
* @return int The level preview image.
*
* @access private
*/
private function renderBackground($rImage, $rTileset) {
$aLayers = $this->getLevelInfo('layers');
$aBackground = $aLayers[8];
//create a separate, temporary image containing only the background, that can be used as a stamp later
$rBackground = imagecreatetruecolor($aBackground['LayerWidth']*32, $aBackground['LayerHeight']*32);
imagefill($rBackground, 1, 1, imagecolorallocate($rImage, 87, 0, 203)); //JCS blue
$aMap = $this->getLayerMap(8);
$aWords = $this->getWords();
$iRealWidth = ceil($aBackground['LayerWidth']/4)*4*32;
$i = 0;
foreach($aMap as $iWord) {
$aWord = $aWords[$iWord];
foreach($aWord as $aTile) {
if($aTile['tile'] != 0) {
if($aTile['flipped']) {
$rTile = imagecreatetruecolor(32,32);
imagecopy($rTile, $rTileset, 0, 0, ($aTile['tile']*32) % 320, floor($aTile['tile']/10)*32, 32, 32);
$rTile = $this->flipTile($rTile);
imagecopy($rBackground, $rTile, ($i % $iRealWidth), floor($i / $iRealWidth)*32, 0, 0, 32, 32);
imagedestroy($rTile);
} else {
imagecopy($rBackground, $rTileset, ($i % $iRealWidth), floor($i / $iRealWidth)*32, ($aTile['tile']*32) % 320, floor($aTile['tile']/10)*32, 32, 32);
}
}
$i += 32;
}
}
$iRepeatX = ceil(($aLayers[4]['LayerWidth']*32)/imagesx($rBackground));
$iRepeatY = ceil(($aLayers[4]['LayerHeight']*32)/imagesy($rBackground));
for($i=0;$i<$iRepeatY;$i++) {
for($j=0;$j<$iRepeatX;$j++) {
imagecopy($rImage, $rBackground, $j*imagesx($rBackground), $i*imagesy($rBackground), 0, 0, imagesx($rBackground), imagesy($rBackground));
}
}
imagedestroy($rBackground);
unset($rBackground, $aLayers, $aWords, $aMap);
return $rImage;
}
/**
* Render a layer
*
* Renders a layer to the tileset preview image.
*
* @param int $iLayer The layer to render. Defaults to 4.
* @param int $rImage The level preview image to copy the background to.
* @param int $rTileset The tileset image to copy tiles from.
*
* @uses Level::getLevelInfo() To retrieve layer information to parse.
* @uses Level::getLayerMap() To retrieve the tile map for the background layer.
* @uses Level::getWords() To map the tile cache to an image.
* @uses Level::flipTile() To flip a tile if neeed.
*
* @return int The level preview image.
*
* @access private
*/
private function renderLayer($iLayer = 4, $rImage, $rTileset) {
//get data; words, level info, tileset image
$aWords = $this->getWords();
$aLayers = $this->getLevelInfo('layers');
$aLayer = $aLayers[$iLayer];
$aMap = $this->getLayerMap($iLayer);
//if the level width is not a multiple of 4, using it for positioning will mess stuff up, so use another value
$iRealWidth = ceil($aLayer['LayerWidth']/4)*4*32;
$i = 0;
//loop through the words
foreach($aMap as $iWord) {
$aWord = $aWords[$iWord];
foreach($aWord as $aTile) {
$iTilePosX = (($aTile['tile']*32) % 320);
$iTilePosY = (floor($aTile['tile']/10)*32);
if($aTile['tile'] != 0) { //imagecopyresampled doesnt work well with transparancy, so use this workaround
$rTile = imagecreatetruecolor(32, 32);
imagecolortransparent($rTile, imagecolorallocate($rTile, 87, 0, 203));
imagealphablending($rTile, false);
imagecopy($rTile, $rTileset, 0, 0, $iTilePosX, $iTilePosY, 32, 32);
if($aTile['flipped']) {
$rTile = $this->flipTile($rTile);
}
imagecopymerge($rImage, $rTile, ($i % $iRealWidth), floor($i / $iRealWidth)*32, 0, 0, 32, 32, ($aTile['translucent']?66:100));
imagedestroy($rTile);
unset($rTile);
}
$i += 32;
}
}
unset($aWords, $aLayers, $aLayer, $aMap);
return $rImage;
}
/**
* Scales the preview image
*
* Scaling is done only after the complete image has been generated because else
* ugly aliasing artifacts appear.
*
* @param int $rImage The image to manipulate
*
* @returns int The manipulated image
*
* @uses Level::PREVIEW_SCALE To determine the size of the scaled image. Each
* tile is as wide and high as the value of this
* constant.
*
* @access public
*/
public function scaleImage($rImage) {
if(!$this->bValid) return false;
if(defined('NORESIZE')) return $rImage;
if(self::PREVIEW_SCALE == 32) {
return $rImage;
}
$iScaleWidth = floor((imagesx($rImage)/32) * self::PREVIEW_SCALE);
$iScaleHeight = floor((imagesy($rImage)/32) * self::PREVIEW_SCALE);
$rNew = imagecreatetruecolor($iScaleWidth, $iScaleHeight);
imagecopyresampled($rNew, $rImage, 0, 0, 0, 0, $iScaleWidth, $iScaleHeight, imagesx($rImage), imagesy($rImage));
imagedestroy($rImage);
return $rNew;
}
/**
* Flip a tile image
*
* This simply flips the image and returns a new image resource with the flipped
* image. Transparency is preserved. Flipping is done horizontally.
*
* @param int $rTile The image resource to flip.
*
* @returns int The flipped image as an image resource.
*
* @access private
*/
private function flipTile($rTile) {
$rFlipped = imagecreatetruecolor(imagesx($rTile), imagesy($rTile));
imagecolortransparent($rFlipped, imagecolorallocate($rFlipped, 87, 0, 203));
imagealphablending($rFlipped, false);
imagecopyresampled($rFlipped, $rTile, 0, 0, imagesx($rTile)-1, 0, imagesx($rTile), imagesy($rTile), (0-imagesx($rTile)), imagesy($rTile));
imagedestroy($rTile);
return $rFlipped;
}
/**
* Toggle debug
*
* Toggles debug mode on or off. When debug mode is enabled, textual error
* messages may be sent to output.
*
* @uses Level::$bDebug To read and store debug settings.
* @see Level::debug()
*
* @access public
*/
public function toggleDebug() {
$this->bDebug = !$this->bDebug;
}
/**
* Debug
*
* Print debug message if debug mode is enabled.
*
* @param string $sMessage The message to print.
*
* @uses Level::$bDebug Print or not?
*
* @access private
*/
private function debug($sMessage) {
if($this->bDebug) {
echo '<div style="padding:2px;margin:1px;display:block;width:auto;border:1px solid #000;color:#000;font-family:monospace;background:#CCC;clear:both;">'.$sMessage.'</div>';
}
}
}
$level = new Level('ctfantarctica.j2l');
echo json_encode($level->getLevelInfo());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment