-
-
Save stijnstijn/d9caff3373dc61e21b5e9ed29d68ceb1 to your computer and use it in GitHub Desktop.
json level info
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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