class.Level.php

  1. <?php
  2. /**
  3.  * class.Level.php
  4.  *
  5.  * Provides an interface to read Level (J2L) files.
  6.  *
  7.  * LICENSE:
  8.  *
  9.  *             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
  10.  *                       Version 2, December 2004
  11.  *
  12.  * Copyright (C) 2009 Stijn Peeters
  13.  *
  14.  * Everyone is permitted to copy and distribute verbatim or modified
  15.  * copies of this license document, and changing it is allowed as long
  16.  * as the name is changed.
  17.  *
  18.  *             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
  19.  * TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
  20.  *
  21.  * 0. You just DO WHAT THE FUCK YOU WANT TO.
  22.  *
  23.  * @package    J2Ov4
  24.  * @author     Stijn Peeters
  25.  * @copyright  Copyright (c) 2009, Stijn Peeters
  26.  * @version    0.5
  27.  * @link       http://www.stijnpeeters.nl/
  28.  * @license    http://sam.zoy.org/wtfpl/ Do What The Fuck You Want To Public License
  29.  */
  30.  
  31. /**
  32.  * Set PHP memory limit and execution time, as preview generation may take a while.
  33.  */
  34. ini_set('memory_limit', '512M');
  35. ini_set('max_execution_time', 60);
  36.  
  37.  /**
  38.  * Level class
  39.  *
  40.  * Reads and interprets Jazz Jackrabbit 2 Level files
  41.  *
  42.  * @package    J2Ov4
  43.  * @author     Stijn Peeters
  44.  * @copyright  Copyright (c) 2009, Stijn Peeters
  45.  * @version    0.5
  46.  * @todo       Manipulating level properties & repacking the level afterwards.
  47.  */
  48. class Level {
  49.         /**
  50.          * @var  int     The handle to the Level tile to read from.
  51.          * @access private
  52.          */
  53.         private $rResource;
  54.         /**
  55.          * @var  mixed  Image Data header bytes (raw pixels).
  56.          * @access private
  57.          */
  58.         private $_Header     = false;
  59.         /**
  60.          * @var  mixed  Level info header bytes.
  61.          * @access private
  62.          */
  63.         private $_LevelInfo  = false;
  64.         /**
  65.          * @var  mixed  Level event data bytes.
  66.          * @access private
  67.          */
  68.         private $_EventData  = false;
  69.         /**
  70.          * @var  mixed  Tile dictionary bytes.
  71.          * @access private
  72.          */
  73.         private $_Dictionary = false;
  74.         /**
  75.          * @var  mixed  Tile cache bytes.
  76.          * @access private
  77.          */
  78.         private $_TileCache  = false;
  79.         /**
  80.          * @var  int     Quick Preview image (GDLib resource).
  81.          * @access private
  82.          */
  83.         private $rQuickPreview;
  84.         /**
  85.          * @var  int     Real Preview image (GDLib resource).
  86.          * @access private
  87.          */
  88.         private $rRealPreview;
  89.         /**
  90.          * @var  array   Level information such as tileset, settings, name, etc
  91.          * @access private
  92.          */
  93.         private $aLevelInfo  = array();
  94.         /**
  95.          * @var  array   Parsed dictionary data to be used in rendering the level.
  96.          * @access private
  97.          */
  98.         private $aWords      = array();
  99.         /**
  100.          * @var  array   The version of the level.
  101.          * @access private
  102.          */
  103.         private $sVersion    = '';
  104.         /**
  105.          * @var  array   Byte offsets for several parts of the level. Available keys:
  106.          *               <ul>
  107.          *                 <li><samp>info_c</samp>: Level Info compressed size.</li>
  108.          *                 <li><samp>info_u</samp>: Level Info uncompressed size.</li>
  109.          *                 <li><samp>evnt_c</samp>: Event data compressed size.</li>
  110.          *                 <li><samp>evnt_u</samp>: Event data uncompressed size.</li>
  111.          *                 <li><samp>dict_c</samp>: Dictionary compressed size.</li>
  112.          *                 <li><samp>dict_u</samp>: Dictionary uncompressed size.</li>
  113.          *                 <li><samp>tile_c</samp>: Tile cache compressed size.</li>
  114.          *                 <li><samp>tile_u</samp>: Tile cache uncompressed size.</li>
  115.          *               </ul>
  116.          * @access private
  117.          */
  118.         private $aOffsets   = array();
  119.         /**
  120.          * @var  boolean Whether debug messages should be printed or not.
  121.          * @access private
  122.          */
  123.         private $bDebug     = false;
  124.         /**
  125.          * @var  boolean Whether "interactive mode" is enabled. "Interactive mode" needs
  126.          *               to be enabled to edit the level.
  127.          */
  128.         private $bInteractiveMode = false;
  129.         /**
  130.          * Size of the Level header
  131.          */
  132.         const SIZE_HEADER   = 262;
  133.         /**
  134.          * Every tile will be this amount pixels large (width & height) in the preview
  135.          */
  136.         const PREVIEW_SCALE = 8;
  137.         /**
  138.          * Max width/height of the image, to prevent out of memory problems
  139.          */
  140.         const MAX_DIMENSION = 10000;
  141.         /**
  142.          * Level header structure
  143.          */
  144.          const LEVL_HEADER = 'A180Copyright/a4Signature/c3PasswordHash/cHideLevel/a32LevelName/sVersion/lFileSize/lCrc/lInfoStreamCompressed/lEventStreamCompressed/lDictStreamCompressed/lCacheStreamCompressed/lInfoStreamUncompressed/lEventStreamUncompressed/lDictStreamUncompressed/lCacheStreamUncompressed';
  145.          const LEVL_HEADER_STRUCT = 'A180/a4/c/c/c/c/a32/s/l/l/l/l/l/l/l/l/l/l';
  146.         /**
  147.          * Level info structure (see php's pack() function for syntax info)
  148.          */
  149.         const LEVL_STRUCT = 'xWTF/
  150.                 vJcsHorizontal/
  151.                 vSecurityEnvelope1/
  152.                 vJcsVertical/
  153.                 CSecurityEnvelope2/
  154.                 CSecEnvAndLayer/
  155.                 CMinimumAmbient/
  156.                 CStartingAmbient/
  157.                 vAnimsUsed/
  158.                 CSplitScreenDivider/
  159.                 CIsItMultiplayer/
  160.                 VStreamSize/
  161.                 a32LevelName/
  162.                 a32Tileset/
  163.                 a32BonusLevel/
  164.                 a32NextLevel/
  165.                 a32SecretLevel/
  166.                 a32MusicFile/
  167.                 A8192HelpStrings/
  168.                 V8LayerProperties/
  169.                 x8LayerUnknown1/
  170.                 C8IsLayerUsed/
  171.                 V8JcsLayerWidth/
  172.                 V8LayerWidth/
  173.                 V8LayerHeight/
  174.                 x16LayerUnknown2/
  175.                 x32LayerUnknown3/
  176.                 x32LayerUnknown4/
  177.                 x24LayerUnknown5/
  178.                 l8LayerXSpeed/
  179.                 l8LayerYSpeed/
  180.                 l8LayerAutoXSpeed/
  181.                 l8LayerAutoYSpeed/
  182.                 x8LayerUnknown6/
  183.                 C3LayerRGB1/
  184.                 C3LayerRGB2/
  185.                 C3LayerRGB3/
  186.                 C3LayerRGB4/
  187.                 C3LayerRGB5/
  188.                 C3LayerRGB6/
  189.                 C3LayerRGB7/
  190.                 C3LayerRGB8/
  191.                 vStaticTiles';
  192.         /**
  193.          * This is the directory the class will look in for cached tileset images
  194.          */
  195.         const CACHE_DIR = '/home/jazzonli/public_html/J2Ov2/upload/downloads/preview/tilesets/';
  196.         /**
  197.          * This is the directory the class will look in for tilesets if no image is found
  198.          */
  199.         const ROOT_DIR  = '/home/jazzonli/public_html/J2Ov2/upload/downloads/';
  200.         /**
  201.          * Magic byte value for TSF tilesets
  202.          */
  203.         const VERSION_TSF = 515;
  204.         /**
  205.          * Magic byte value for 1.23 tilesets
  206.          */
  207.         const VERSION_123 = 514;
  208.         /**
  209.          * Magic value for Next Level setting
  210.          */
  211.         const NEXTLEVEL = 1;
  212.        
  213.         private $bValid = false;
  214.  
  215.         /**
  216.          * Constructor method
  217.          *
  218.          * Sets up the object, checking whether given file path is valid and giving several
  219.          * variables initial values. Then it reads the header via another method.
  220.          *
  221.          * @param   string  $sFilename  The file path pointing to the Level to use.
  222.          * @param   boolean $bDebug     Initial debug mode setting. Defaults to false. If
  223.          *                              enabled, textual debug messages may be shown.
  224.          *
  225.          * @uses    Level::$bDebug     To store debugging settings.
  226.          * @uses    Level::getHeader() To read the Level header subsequently.
  227.          *
  228.          * @access  public
  229.          */
  230.         public function __construct($sFilename, $bDebug = false) {
  231.                 if(!is_readable($sFilename)) {
  232.                         $this->debug('Level file not found');
  233.                         return false;
  234.                 }
  235.                
  236.                 if(filesize($sFilename) == 0) {
  237.                         $this->debug('Level file is zero bytes');
  238.                         return false;
  239.                 }
  240.                
  241.                 $this->bDebug = $bDebug;
  242.                 $this->sPath = $sFilename;
  243.                 $this->rResource = fopen($this->sPath, 'rb');
  244.                 $this->bValid = ($this->rResource !== false);
  245.  
  246.                 $this->rQuickPreview = false;
  247.                 $this->rRealPreview  = false;
  248.        
  249.                 $this->getHeader();
  250.                 $this->debug('Opened level file '.$sFilename);
  251.         }
  252.        
  253.         public function isValid() {
  254.                 return $this->bValid;
  255.         }
  256.        
  257.         /**
  258.          * Retrieve Level header
  259.          *
  260.          * The Level header (first 262 bytes) contains offsets for the actual Level data
  261.          * which is retrieved here, and stored in the offsets array. The header is saved too
  262.          * for later usage.
  263.          *
  264.          * @uses    Level::$aOffsets  To store the offsets found.
  265.          * @uses    Level::$_Header   To store the Level header.
  266.          * @uses    Level::$rResource To read from the Level file.
  267.          *
  268.          * @access  private
  269.          */
  270.         private function getHeader() {
  271.                 $this->_Header = fread($this->rResource, self::SIZE_HEADER);
  272.                 $this->aOffsets = array();
  273.                 list(,$this->aOffsets['info_c']) = unpack('V', substr($this->_Header, 230, 4));
  274.                 list(,$this->aOffsets['info_u']) = unpack('V', substr($this->_Header, 234, 4));
  275.                 list(,$this->aOffsets['evnt_c']) = unpack('V', substr($this->_Header, 238, 4));
  276.                 list(,$this->aOffsets['evnt_u']) = unpack('V', substr($this->_Header, 242, 4));
  277.                 list(,$this->aOffsets['dict_c']) = unpack('V', substr($this->_Header, 246, 4));
  278.                 list(,$this->aOffsets['dict_u']) = unpack('V', substr($this->_Header, 250, 4));
  279.                 list(,$this->aOffsets['tile_c']) = unpack('V', substr($this->_Header, 254, 4));
  280.                 list(,$this->aOffsets['tile_u']) = unpack('V', substr($this->_Header, 258, 4));
  281.                 list(,$this->sVersion)           = unpack('v', substr($this->_Header, 220, 2));
  282.                 if(!strpos($this->_Header, 'MegaGames')) { //crude integrity check
  283.                         $this->debug('Missing or corrupt file header');
  284.                         $this->bValid = false;
  285.                 } else {
  286.                         $this->debug('Succesfully unpacked file header');
  287.                 }
  288.         }
  289.  
  290.         /**
  291.          * Get level info data
  292.          *
  293.          * Uncompresses the first data stream, the level info. The result is stored for
  294.          * later retrieval.
  295.          *
  296.          * @returns string             The raw byte-for-byte level info stream.
  297.          *
  298.          * @uses    Level::$_LevelInfo To store the level info.
  299.          *
  300.          * @access  private
  301.          */
  302.         private function getInfoData() {
  303.                 if(!$this->_LevelInfo) {
  304.                         $this->_LevelInfo = gzuncompress(fread($this->rResource, $this->aOffsets['info_c']));
  305.                         $this->debug('Decompressed level info (size: '.strlen($this->_LevelInfo).')');
  306.                 }
  307.                 return $this->_LevelInfo;
  308.         }
  309.  
  310.         /**
  311.          * Get event data
  312.          *
  313.          * Uncompresses the second data stream, the event data. The result is stored for
  314.          * later retrieval. If the previous stream was not read yet it will be read and
  315.          * uncompressed first.
  316.          *
  317.          * @returns string             The raw byte-for-byte event data stream.
  318.          *
  319.          * @uses    Level::$_EventData To store the event data.
  320.          *
  321.          * @access  private
  322.          */
  323.         private function getEventData() {
  324.                 if(!$this->_LevelInfo) {
  325.                         $this->getInfoData();
  326.                 }
  327.                 if(!$this->_EventData) {
  328.                         $this->_EventData = gzuncompress(fread($this->rResource, $this->aOffsets['evnt_c']));
  329.                         $this->debug('Decompressed event data (size: '.strlen($this->_EventData).')');
  330.                 }
  331.                 return $this->_EventData;
  332.         }
  333.  
  334.         /**
  335.          * Get dictionary
  336.          *
  337.          * Uncompresses the third data stream, the word data. The result is stored for
  338.          * later retrieval. If the previous stream was not read yet it will be read and
  339.          * uncompressed first.
  340.          *
  341.          * @returns string             The raw byte-for-byte dictionary.
  342.          *
  343.          * @uses    Level::$_LevelInfo To store the dictionary.
  344.          *
  345.          * @access  private
  346.          */
  347.         private function getWordData() {
  348.                 if(!$this->_EventData) {
  349.                         $this->getEventData();
  350.                 }
  351.                 if(!$this->_Dictionary) {
  352.                         $this->_Dictionary = gzuncompress(fread($this->rResource, $this->aOffsets['dict_c']));
  353.                         $this->debug('Decompressed tile dictionary (size: '.strlen($this->_Dictionary).')');
  354.                 }
  355.                 return $this->_Dictionary;
  356.         }
  357.  
  358.         /**
  359.          * Get tile cache
  360.          *
  361.          * Uncompresses the fourth data stream, the tile cache. The result is stored for
  362.          * later retrieval. If the previous stream was not read yet it will be read and
  363.          * uncompressed first.
  364.          *
  365.          * @returns string             The raw byte-for-byte tile cache.
  366.          *
  367.          * @uses    Level::$_TileCache To store the tile cache.
  368.          *
  369.          * @access  private
  370.          */
  371.         private function getCacheData() {
  372.                 if(!$this->_Dictionary) {
  373.                         $this->getWordData();
  374.                 }
  375.                 if(!$this->_TileCache) {
  376.                         $this->_TileCache = gzuncompress(fread($this->rResource, $this->aOffsets['tile_c']));
  377.                         fclose($this->rResource);
  378.                         unset($this->rResource);
  379.                          $this->debug('Decompressed tile cache (size: '.strlen($this->_TileCache).')');
  380.                 }
  381.                 return $this->_TileCache;
  382.         }
  383.  
  384.         /**
  385.          * Get words
  386.          *
  387.          * Converts the raw word dictionary into a usable format for parsing the
  388.          * tile cache. Flipped tiles are ignored for now. The result is stored
  389.          * for later retrieval.
  390.          *
  391.          * @returns array              Each value in this array is an array with
  392.          *                             four values, with each value being a
  393.          *                             tile index.
  394.          *
  395.          * @uses    Level::$aOffsets   To determine to which point to read.
  396.          * @uses    Level::$_Dictionary To read the word data from.
  397.          * @uses    Level::$aWords     To store the word array.
  398.          *
  399.          * @access  private
  400.          */
  401.         private function getWords() {
  402.                 if(!$this->_Dictionary) {
  403.                         $this->getWordData();
  404.                 }
  405.                 if($this->aWords) {
  406.                         $this->debug('Returning cached word map');
  407.                         return $this->aWords;
  408.                 }
  409.                 $aLevelData = $this->getLevelInfo();
  410.                 $iTiles = ($this->sVersion == self::VERSION_TSF) ? 4096 : 1024;
  411.                 //map animated tiles (first frame is saved since a static image is generated)
  412.                 $iAnims = $iTiles-$aLevelData['StaticTiles'];
  413.                 $sAnims = substr($this->_LevelInfo, 8813+(7*$iTiles), (137*64));
  414.                 $aAnims = array();
  415.                 for($i=0;$i<$iAnims;$i++) {
  416.                         list(,$iFrame) = unpack('v', substr($sAnims, ($i*137)+9, 2)); //anim description = 137 bytes
  417.                         $aAnims[] = $iFrame;
  418.                 }
  419.                 //map tile types (translucent tiles)
  420.                 $sType = substr($this->_LevelInfo, 8813+(5*$iTiles), $iTiles);
  421.                 $aType = array();
  422.                 for($i=0;$i<$iTiles;$i++) {
  423.                         list(,$aType[]) = unpack('C', substr($sType, $i, 1));
  424.                 }
  425.                 $aWords = array();
  426.                 for($i=0; $i < $this->aOffsets['dict_u']; $i++) {
  427.                         $iWord = floor($i/8);
  428.                         $sWord = substr($this->_Dictionary, ($iWord * 8), 8);
  429.                         for($j=0;$j<4;$j++) {
  430.                                 list(,$iTile) = unpack('v', substr($sWord, ($j*2), 2));
  431.                                 $bFlipped       = false;
  432.                                 $bAnimated      = false;
  433.                                 if($iTile > $iTiles) { //flipped
  434.                                         $iTile -= $iTiles;
  435.                                         $bFlipped = true;
  436.                                 }
  437.                                 if($iTile >= $aLevelData['StaticTiles']) {
  438.                                         $iTile = $aAnims[$iTile-$aLevelData['StaticTiles']];
  439.                                 }
  440.                                 $aWords[$iWord][$j] = array('tile' => $iTile, 'flipped' => $bFlipped, 'animated' => $bAnimated, 'translucent' => ($aType[$iTile]==1));
  441.                         }
  442.                 }
  443.                 $this->aWords = $aWords;
  444.                 return $this->aWords;
  445.         }
  446.        
  447.         /**
  448.          * Build J2L file
  449.          *
  450.          * Builds a new J2L file from the previously uncompressed data.
  451.          *
  452.          * @returns string              The raw file
  453.          *
  454.          * @access  public
  455.          */
  456.         public function repack() {
  457.                 $sJ2L = '';
  458.                 if(!$this->_TileCache) {
  459.                         $this->getCacheData();
  460.                 }
  461.                 $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>');
  462.                 $aHeader = unpack(Level::LEVL_HEADER, $this->_Header);
  463.                 $sCmpLevelInfo  = gzcompress($this->_LevelInfo, 8);
  464.                 $sCmpEventData  = gzcompress($this->_EventData, 8);
  465.                 $sCmpDictionary = gzcompress($this->_Dictionary, 8);
  466.                 $sCmpTileCache  = gzcompress($this->_TileCache, 8);
  467.                 $aHeader['Crc'] = $this->getCRC32(array($sCmpLevelInfo, $sCmpEventData, $sCmpDictionary, $sCmpTileCache));
  468.                 $aHeader['FileSize'] = 262+strlen($sCmpLevelInfo.$sCmpEventData.$sCmpDictionary.$sCmpTileCache);
  469.                 $aHeader['InfoStreamCompressed']    = strlen($sCmpLevelInfo);
  470.                 $aHeader['EventStreamCompressed']   = strlen($sCmpEventData);
  471.                 $aHeader['DictStreamCompressed']    = strlen($sCmpDictionary);
  472.                 $aHeader['CacheStreamCompressed']   = strlen($sCmpTileCache);
  473.                 $aHeader['InfoStreamUncompressed']  = strlen($this->_LevelInfo);
  474.                 $aHeader['EventStreamUncompressed'] = strlen($this->_EventData);
  475.                 $aHeader['DictStreamUncompressed']  = strlen($this->_Dictionary);
  476.                 $aHeader['CacheStreamUncompressed'] = strlen($this->_TileCache);
  477.                 $sJ2L  = Misc::repack(Level::LEVL_HEADER_STRUCT, $aHeader);
  478.                 $sJ2L .= $sCmpLevelInfo;
  479.                 $sJ2L .= $sCmpEventData;
  480.                 $sJ2L .= $sCmpDictionary;
  481.                 $sJ2L .= $sCmpTileCache;
  482.                 return $sJ2L;
  483.         }
  484.        
  485.         public function getCRC32($aData) {
  486.                 $iCRC = 0;
  487.                 for($i=0;$i<4;$i++) {
  488.                         $iCRC = Misc::crc32c($iCRC, $aData[$i], strlen($aData[$i]));
  489.                 }
  490.                 return $iCRC;
  491.         }
  492.        
  493.         /**
  494.          * Edit level info
  495.          *
  496.          * Changes level info such as next level or music file. Currently supports the
  497.          * following options:
  498.          *
  499.          * - <samp>Level::NEXTLEVEL</samp>: Next level setting (32 bytes)
  500.          *
  501.          * If a value is longer or shorter than the allowed length, it will be truncated
  502.          * or expanded.
  503.          *
  504.          * @param   int     $iKey       The field to be edited. See above for allowed
  505.          *                              values.
  506.          * @param   mixed   $mValue     The new value. May be filtered to fit the given
  507.          *                              field.
  508.          *
  509.          * @uses    Level::$_LevelInfo  To alter the level data.
  510.          *
  511.          * @todo    Add more fields that can be edited.
  512.          *
  513.          * @access public
  514.          */
  515.         public function setInfo($iKey, $mValue) {
  516.                 if(!$this->_TileCache) {
  517.                         $this->getCacheData();
  518.                 }
  519.                 switch($iKey) {
  520.                 default:
  521.                         return false;
  522.                 case self::NEXTLEVEL:
  523.                         $this->_LevelInfo = substr($this->_LevelInfo, 0, 114).str_pad($mValue, "\0").substr($this->_LevelInfo, 146);
  524.                         return true;
  525.                 }
  526.         }
  527.        
  528.         /**
  529.          * Get level info
  530.          *
  531.          * Converts the raw level info into a usable array. The result is stored
  532.          * for later retrieval.
  533.          *
  534.          * @returns array              Level info. Keys correspond to those used
  535.          *                             in the J2NSM file format specification,
  536.          *                             ignoring unknown values and storing
  537.          *                             layer-specific data in an array for that
  538.          *                             layer.
  539.          *
  540.          * @uses    Level::LEVL_STRUCT To parse the binary info.
  541.          * @uses    Level::$_LevelInfo To read the level info from.
  542.          * @uses    Level::$aLevelInfo To store the level info array.
  543.          *
  544.          * @access  public
  545.          */
  546.         public function getLevelInfo($mReturn = false) {
  547.                 if(!$this->bValid || empty($this->sPath)) {
  548.                         return false;
  549.                 }
  550.                 if($this->aLevelInfo) {
  551.                         if($mReturn == 'layers') {
  552.                                 return $this->aLevelInfo['layers'];
  553.                         }
  554.                         return $this->aLevelInfo;
  555.                 }
  556.                 if(!$this->_LevelInfo) {
  557.                         $this->getInfoData();
  558.                 }
  559.                 $sFormat = str_replace(array("\n", "\r", ' '), '', self::LEVL_STRUCT);
  560.                 $aLevelInfo = unpack($sFormat, $this->_LevelInfo);
  561.                 for($i=0;$i<16;$i++) { //help strings, 16 of 512 bytes each
  562.                         $aLevelInfo['HelpString'][$i] = trim(substr($aLevelInfo['HelpStrings'], $i*512, 512));
  563.                 }
  564.                 unset($aLevelInfo['HelpStrings']);
  565.                 for($i=1;$i<9;$i++) { //make an array with data for each layer
  566.                         foreach(array('LayerProperties', 'IsLayerUsed', 'JcsLayerWidth', 'LayerWidth', 'LayerHeight', 'LayerXSpeed', 'LayerYSpeed', 'LayerAutoXSpeed', 'LayerAutoYSpeed') as $sLayerKey) {
  567.                                 $aLevelInfo['layers'][$i][$sLayerKey] = $aLevelInfo[$sLayerKey.$i];
  568.                                 unset($aLevelInfo[$sLayerKey.$i]);
  569.                         }
  570.                         //convert RGB (textured mode fade) values to arrays
  571.                         $aLevelInfo['layers'][$i]['rgb'] = array($aLevelInfo['LayerRGB'.$i.'1'], $aLevelInfo['LayerRGB'.$i.'2'], $aLevelInfo['LayerRGB'.$i.'3']);
  572.                         unset($aLevelInfo['LayerRGB'.$i.'1'], $aLevelInfo['LayerRGB'.$i.'2'], $aLevelInfo['LayerRGB'.$i.'3']);
  573.                         //boolean layer properties (bits)
  574.                         $aLevelInfo['layers'][$i]['LayerProperties'] = array(
  575.                                 'TileWidth'             => (($aLevelInfo['layers'][$i]['LayerProperties'] & 1) == 1),
  576.                                 'TileHeight'            => (($aLevelInfo['layers'][$i]['LayerProperties'] & 2) == 2),
  577.                                 'LimitVisibleRegion'    => (($aLevelInfo['layers'][$i]['LayerProperties'] & 4) == 4),
  578.                                 'TextureMode'           => (($aLevelInfo['layers'][$i]['LayerProperties'] & 8) == 8),
  579.                                 'ParallaxStars'         => (($aLevelInfo['layers'][$i]['LayerProperties'] & 16) == 16));
  580.                 }
  581.                 $this->aLevelInfo = $aLevelInfo;
  582.                 if($mReturn == 'layers') {
  583.                         return $this->aLevelInfo['layers'];
  584.                 }
  585.                 return $this->aLevelInfo;
  586.         }
  587.  
  588.         /**
  589.          * Get layer map
  590.          *
  591.          * Parses the binary Tile Cache data to be an array of word indexes for a
  592.          * specific layer.
  593.          *
  594.          * @param   int     $iLayer     The layer to parse. Layers before it are
  595.          *                              ignored.
  596.          *
  597.          * @returns array               The word map, each value being an index
  598.          *                              referencing a word in the dictionary.
  599.          *
  600.          * @uses    Level::getLevelInfo() To retrieve layer size, and henceforth
  601.          *                              skip a layer.
  602.          * @uses    Level::getCacheData() To get the binary data to parse.
  603.          *
  604.          * @see     Level::$_Dictionary Indexes in the returned array point to a
  605.          *                              word in the dictionary.
  606.          *
  607.          * @access  private
  608.          */
  609.         private function getLayerMap($iLayer) {
  610.                 $aLayers = $this->getLevelInfo('layers');
  611.                
  612.                 //calculate offset to read from; we're not interested in layers prior to $iLayer
  613.                 $iOffset = 0;
  614.                 for($i=1;$i<$iLayer;$i++) {
  615.                         if(!$aLayers[$i]['IsLayerUsed']) {
  616.                                 $this->debug('Skipping layer '.$i);
  617.                                 continue;
  618.                         }
  619.                         $iOffset += 2*(ceil($aLayers[$i]['LayerWidth']/4)*$aLayers[$i]['LayerHeight']);
  620.                 }
  621.                 $iWords = ceil($aLayers[$iLayer]['LayerWidth']/4)*$aLayers[$iLayer]['LayerHeight'];
  622.                 $aMap = unpack('v*', substr($this->getCacheData(), $iOffset, ($iWords*2)));
  623.                 return $aMap;
  624.         }
  625.  
  626.         /**
  627.          * Get quick preview image
  628.          *
  629.          * This generates a low-resolution (one pixel per tile) preview image
  630.          * of a layer based on the boolean mask for the used tileset.
  631.          *
  632.          * @returns integer              The image resource, to be used with
  633.          *                               for example imagepng()
  634.          *
  635.          * @param   integer $iLayer      Which layer to render. Defaults to 4.  
  636.          *
  637.          * @uses    Tileset              To retrieve tileset information.
  638.          * @uses    Tileset::getBooleanMask() To retrieve mask information.
  639.          * @uses    Level::getWords()    To parse the tile cache with.
  640.          * @uses    Level::getLevelInfo() To determine layer size.
  641.          * @uses    Level::getLayerMap() To retrieve the tile map for the background layer.
  642.          *
  643.          * @access  public
  644.          */
  645.         public function getQuickPreview($iLayer = 4, $iZoom = 1) {
  646.                 if(!$this->bValid) return false;
  647.  
  648.                 if($this->rQuickPreview) {
  649.                         return $this->rQuickPreview;
  650.                 }
  651.                 //get data; words, level info, tileset mask
  652.                 $aWords = $this->getWords();
  653.                 $aLayers = $this->getLevelInfo();
  654.                 $sTileset = $aLayers['Tileset'];
  655.                 if(!stripos($sTileset, '.j2t')) $sTileset .= '.j2t';
  656.                 $oTileset = new Tileset(self::ROOT_DIR.'tilesets/'.$sTileset);
  657.                 $aMask = $oTileset->getBooleanMask();
  658.                 //create image and fill it with "JCS Blue"; allocate a color for masked tiles (black)
  659.                 $rImage = imagecreatetruecolor($aLayers['layers'][$iLayer]['LayerWidth'], $aLayers['layers'][$iLayer]['LayerHeight']);
  660.                 imagefill($rImage, 1, 1, imagecolorallocate($rImage, 87, 0, 203));
  661.                 $iBlack = imagecolorallocate($rImage, 0, 0, 0);
  662.                 //if the level width is not a multiple of 4, using it for positioning will mess stuff up, so use another value
  663.                 $aMap = $this->getLayerMap($iLayer);
  664.                 $iRealWidth = ceil($aLayers['layers'][$iLayer]['LayerWidth']/4)*4;
  665.                 $i = 0;
  666.                 //loop through the words
  667.                 foreach($aMap as $iWord) {
  668.                         $aWord = $aWords[$iWord];
  669.                         foreach($aWord as $iTile) {
  670.                                 if($aMask[$iTile]) { //if the tile referenced is solid, draw a pixel
  671.                                         imagesetpixel($rImage, ($i % $iRealWidth), floor($i/$iRealWidth), $iBlack);
  672.                                 }
  673.                                 $i++;
  674.                         }
  675.                 }
  676.                 $this->rQuickPreview = $rImage;
  677.                 unset($aWords, $aLayers, $oTileset, $aMask, $rImage);
  678.                 return $this->rQuickPreview;
  679.         }
  680.  
  681.         /**
  682.          * Get real preview image
  683.          *
  684.          * This generates a "real" preview image, scaled, using the actual
  685.          * tileset image. All layers with both x and y speed 1 are rendered
  686.          * on top of each other, in reverse order, so the level looks more
  687.          * or less like what it would like like in JCS.
  688.          *
  689.          * @returns integer              The image resource, to be used with
  690.          *                               for example imagepng()
  691.          *
  692.          * @param   mixed   $mLayer      Which layer to render. Can be either an
  693.          *                               integer or an array; if it is an array,
  694.          *                               each element should be n with 8 > n > 0.
  695.          *                               All layers referenced in the array are
  696.          *                               rendered; this is the default.
  697.          * @param   boolean $bDoBackgroundPass Whether to render the level background or
  698.          *                               not; defaults to <samp>true</samp>.
  699.          *
  700.          * @uses    Tileset              To retrieve tileset information.
  701.          * @uses    Tileset::getTilesetImage() To retrieve the tileset image.
  702.          * @uses    Level::getLayerMap() To retrieve the tile map for the background layer.
  703.          * @uses    Level::CACHE_DIR     To find a cached tileset image.
  704.          * @uses    Level::MAX_DIMENSION To find out the max image size.
  705.          *
  706.          * @access  public
  707.          */
  708.         public function getRealPreview($mLayer = array(7,6,5,4,3,2,1), $bDoBackgroundPass = true) {
  709.                 global $db;
  710.                 $this->debug('Generating fullsize level preview');
  711.                 if(!$this->bValid) {
  712.                          $this->debug('Invalid level file; could not generate preview');
  713.                         return false;
  714.                 }
  715.  
  716.                 if($this->rRealPreview) {
  717.                         $this->debug('Returning cached fullsize level preview');
  718.                         return $this->rRealPreview;
  719.                 }
  720.                 $aLayers  = $this->getLevelInfo();
  721.                 $iWidth   = ($aLayers['layers'][4]['LayerWidth']*32) > self::MAX_DIMENSION ? self::MAX_DIMENSION : ($aLayers['layers'][4]['LayerWidth']*32);
  722.                 $iHeight  = ($aLayers['layers'][4]['LayerHeight']*32) > self::MAX_DIMENSION ? self::MAX_DIMENSION : ($aLayers['layers'][4]['LayerHeight']*32);
  723.                 $rImage   = imagecreatetruecolor($iWidth, $iHeight);
  724.                 //look for cached tileset image, if nonexistant, create it (but dont cache, not our responsibility)
  725.                 $sTileset = $aLayers['Tileset'];
  726.                 if(!stripos($sTileset, '.j2t')) $sTileset .= '.j2t';
  727.                 $oTileset = false;
  728.                 $sPreview = str_ireplace('.j2t', '.png', $sTileset);
  729.                 if(is_readable(self::CACHE_DIR.$sPreview)) {
  730.                         $this->debug('Using cached tileset image for tileset '.$sTileset);
  731.                         $rTileset = imagecreatefrompng(self::CACHE_DIR.$sPreview);
  732.                 } else {
  733.                         $this->debug('Generating new tileset image for tileset '.$sTileset);
  734.                         $oTileset = new Tileset(self::ROOT_DIR.'tilesets/'.$sTileset);
  735.                         if(!$oTileset->bValid) { //if tileset couldnt be loaded, chances are that it's stored with different casing; do an exhaustive dir search
  736.                                 $this->debug('Could not load file '.$sTileset.'; scanning for alternatives');
  737.                                 $aFiles = scandir(self::ROOT_DIR.'tilesets/');
  738.                                 foreach($aFiles as $sName) {
  739.                                         if(strtolower($sName) == strtolower($sTileset)) {
  740.                                                 $oTileset = new Tileset(self::ROOT_DIR.'tilesets/'.$sName);
  741.                                                 break;
  742.                                         }
  743.                                 }
  744.                         }
  745.                         if($oTileset->bValid) {
  746.                                 $rTileset = $oTileset->getTilesetImage();
  747.                         } else {
  748.                                 $this->debug('Could not locate tileset file '.$sTileset.' or any similar filenames, checking database for more alternatives');
  749.                                 //if it still wasn't found and there is a database connection, try looking for the file under a different name in the database
  750.                                 if(is_object($db)) {
  751.                                         $sFilepath = $db->query("
  752.                                                 SELECT f.path
  753.                                                   FROM ".SQL_TBL_FILESLINK." AS l,
  754.                                                        ".SQL_TBL_FILES." AS f
  755.                                                  WHERE l.filename LIKE '".$db->escape($sTileset)."'
  756.                                                    AND l.fileID = f.fileID
  757.                                               ORDER BY (l.filename = '".$db->escape($sTileset)."') DESC", 'firstfield');
  758.                                         if($sFilepath) {
  759.                                                 $oTileset = new Tileset(self::ROOT_DIR.$sFilepath);
  760.                                                 $rTileset = $oTileset->getTilesetImage();
  761.                                         }
  762.                                         if(!$sFilepath || !$oTileset->bValid) {
  763.                                                 $this->debug('Database search gave no results; could not create tileset image for level preview.');
  764.                                                 return false;
  765.                                         }
  766.                                 } else {
  767.                                         $this->debug('No database connection; could not create tileset image for level preview.');
  768.                                         return false;
  769.                                 }
  770.                         }
  771.                 }
  772.                 //render the background first
  773.                 if($bDoBackgroundPass) {
  774.                         $this->renderBackground($rImage, $rTileset);
  775.                 }
  776.                 if(is_array($mLayer)) {
  777.                         foreach($mLayer as $iLayer) {
  778.                                 //render only layers with x and y speed = 1
  779.                                 if($aLayers['layers'][$iLayer]['IsLayerUsed'] && $aLayers['layers'][$iLayer]['LayerXSpeed'] == 65536 && $aLayers['layers'][$iLayer]['LayerXSpeed']==$aLayers['layers'][$iLayer]['LayerYSpeed']) {
  780.                                         $this->debug('Rendering layer '.$iLayer);
  781.                                         $this->renderLayer($iLayer, $rImage, $rTileset);
  782.                                 }
  783.                         }
  784.                 } else {
  785.                         $this->renderLayer($mLayer, $rImage, $rTileset);
  786.                 }
  787.                 $rImage = $this->scaleImage($rImage);
  788.                 $this->rRealPreview = $rImage;
  789.                 imagedestroy($rTileset);
  790.                 unset($aLayers, $oTileset);
  791.                 return $this->rRealPreview;
  792.         }
  793.        
  794.         /**
  795.          * Render background layer
  796.          *
  797.          * This is a separate method, distinct from {@link Level::renderLayer()} because it
  798.          * works slightly differently. The background layer contents are rendered to a
  799.          * separate image, which is then pasted all over the level image, repeating itself
  800.          * until the image is filled. Transparent parts will be "JCS Blue".
  801.          *
  802.          * @param   int     $rImage     The level preview image to copy the background to.
  803.          * @param   int     $rTileset   The tileset image to copy tiles from.
  804.          *
  805.          * @uses    Level::getLevelInfo() To retrieve layer information to parse.
  806.          * @uses    Level::getLayerMap() To retrieve the tile map for the background layer.
  807.          * @uses    Level::getWords()   To map the tile cache to an image.
  808.          *
  809.          * @return  int                 The level preview image.
  810.          *
  811.          * @access  private
  812.          */
  813.         private function renderBackground($rImage, $rTileset) {
  814.                 $aLayers = $this->getLevelInfo('layers');
  815.                 $aBackground = $aLayers[8];
  816.                 //create a separate, temporary image containing only the background, that can be used as a stamp later
  817.                 $rBackground = imagecreatetruecolor($aBackground['LayerWidth']*32, $aBackground['LayerHeight']*32);
  818.                 imagefill($rBackground, 1, 1, imagecolorallocate($rImage, 87, 0, 203)); //JCS blue
  819.                 $aMap       = $this->getLayerMap(8);
  820.                 $aWords     = $this->getWords();
  821.                 $iRealWidth = ceil($aBackground['LayerWidth']/4)*4*32;
  822.                 $i          = 0;
  823.                 foreach($aMap as $iWord) {
  824.                         $aWord = $aWords[$iWord];
  825.                         foreach($aWord as $aTile) {
  826.                                 if($aTile['tile'] != 0) {
  827.                                         if($aTile['flipped']) {
  828.                                                 $rTile = imagecreatetruecolor(32,32);
  829.                                                 imagecopy($rTile, $rTileset, 0, 0, ($aTile['tile']*32) % 320, floor($aTile['tile']/10)*32, 32, 32);
  830.                                                 $rTile = $this->flipTile($rTile);
  831.                                                 imagecopy($rBackground, $rTile, ($i % $iRealWidth), floor($i / $iRealWidth)*32, 0, 0, 32, 32);
  832.                                                 imagedestroy($rTile);
  833.                                         } else {
  834.                                                 imagecopy($rBackground, $rTileset, ($i % $iRealWidth), floor($i / $iRealWidth)*32, ($aTile['tile']*32) % 320, floor($aTile['tile']/10)*32, 32, 32);
  835.                                         }
  836.                                 }
  837.                                 $i += 32;
  838.                         }
  839.                 }
  840.                 $iRepeatX = ceil(($aLayers[4]['LayerWidth']*32)/imagesx($rBackground));
  841.                 $iRepeatY = ceil(($aLayers[4]['LayerHeight']*32)/imagesy($rBackground));
  842.                 for($i=0;$i<$iRepeatY;$i++) {
  843.                         for($j=0;$j<$iRepeatX;$j++) {
  844.                                 imagecopy($rImage, $rBackground, $j*imagesx($rBackground), $i*imagesy($rBackground), 0, 0, imagesx($rBackground), imagesy($rBackground));
  845.                         }
  846.                 }
  847.                 imagedestroy($rBackground);
  848.                 unset($rBackground, $aLayers, $aWords, $aMap);
  849.                 return $rImage;
  850.         }
  851.        
  852.         /**
  853.          * Render a layer
  854.          *
  855.          * Renders a layer to the tileset preview image.
  856.          *
  857.          * @param   int     $iLayer     The layer to render. Defaults to 4.
  858.          * @param   int     $rImage     The level preview image to copy the background to.
  859.          * @param   int     $rTileset   The tileset image to copy tiles from.
  860.          *
  861.          * @uses    Level::getLevelInfo() To retrieve layer information to parse.
  862.          * @uses    Level::getLayerMap() To retrieve the tile map for the background layer.
  863.          * @uses    Level::getWords()   To map the tile cache to an image.
  864.          * @uses    Level::flipTile()   To flip a tile if neeed.
  865.          *
  866.          * @return  int                 The level preview image.
  867.          *
  868.          * @access  private
  869.          */
  870.         private function renderLayer($iLayer = 4, $rImage, $rTileset) {
  871.                 //get data; words, level info, tileset image
  872.                 $aWords   = $this->getWords();
  873.                 $aLayers  = $this->getLevelInfo('layers');
  874.                 $aLayer   = $aLayers[$iLayer];
  875.                 $aMap     = $this->getLayerMap($iLayer);
  876.                 //if the level width is not a multiple of 4, using it for positioning will mess stuff up, so use another value
  877.                 $iRealWidth = ceil($aLayer['LayerWidth']/4)*4*32;
  878.                 $i = 0;
  879.                 //loop through the words
  880.                 foreach($aMap as $iWord) {
  881.                         $aWord = $aWords[$iWord];
  882.                         foreach($aWord as $aTile) {
  883.                                 $iTilePosX = (($aTile['tile']*32) % 320);
  884.                                 $iTilePosY = (floor($aTile['tile']/10)*32);
  885.                                 if($aTile['tile'] != 0) { //imagecopyresampled doesnt work well with transparancy, so use this workaround
  886.                                         $rTile = imagecreatetruecolor(32, 32);
  887.                                         imagecolortransparent($rTile, imagecolorallocate($rTile, 87, 0, 203));
  888.                                         imagealphablending($rTile, false);
  889.                                         imagecopy($rTile, $rTileset, 0, 0, $iTilePosX, $iTilePosY, 32, 32);
  890.                                         if($aTile['flipped']) {
  891.                                                 $rTile = $this->flipTile($rTile);
  892.                                         }
  893.                                         imagecopymerge($rImage, $rTile, ($i % $iRealWidth), floor($i / $iRealWidth)*32, 0, 0, 32, 32, ($aTile['translucent']?66:100));
  894.                                         imagedestroy($rTile);
  895.                                         unset($rTile);
  896.                                 }
  897.                                 $i += 32;
  898.                         }
  899.                 }
  900.                 unset($aWords, $aLayers, $aLayer, $aMap);
  901.                 return $rImage;
  902.         }
  903.        
  904.         /**
  905.          * Scales the preview image
  906.          *
  907.          * Scaling is done only after the complete image has been generated because else
  908.          * ugly aliasing artifacts appear.
  909.          *
  910.          * @param   int     $rImage     The image to manipulate
  911.          *
  912.          * @returns int                 The manipulated image
  913.          *
  914.          * @uses    Level::PREVIEW_SCALE To determine the size of the scaled image. Each
  915.          *                              tile is as wide and high as the value of this
  916.          *                              constant.
  917.          *
  918.          * @access  public
  919.          */
  920.         public function scaleImage($rImage) {
  921.                 if(!$this->bValid) return false;
  922.  
  923.                 if(self::PREVIEW_SCALE == 32) {
  924.                         return $rImage;
  925.                 }
  926.                 $iScaleWidth = floor((imagesx($rImage)/32) * self::PREVIEW_SCALE);
  927.                 $iScaleHeight = floor((imagesy($rImage)/32) * self::PREVIEW_SCALE);
  928.                 $rNew = imagecreatetruecolor($iScaleWidth, $iScaleHeight);
  929.                 imagecopyresampled($rNew, $rImage, 0, 0, 0, 0, $iScaleWidth, $iScaleHeight, imagesx($rImage), imagesy($rImage));
  930.                 imagedestroy($rImage);
  931.                 return $rNew;
  932.         }
  933.        
  934.         /**
  935.          * Flip a tile image
  936.          *
  937.          * This simply flips the image and returns a new image resource with the flipped
  938.          * image. Transparency is preserved. Flipping is done horizontally.
  939.          *
  940.          * @param   int     $rTile      The image resource to flip.
  941.          *
  942.          * @returns int                 The flipped image as an image resource.
  943.          *
  944.          * @access  private
  945.          */
  946.         private function flipTile($rTile) {
  947.                 $rFlipped = imagecreatetruecolor(imagesx($rTile), imagesy($rTile));
  948.                             imagecolortransparent($rFlipped, imagecolorallocate($rFlipped, 87, 0, 203));
  949.                             imagealphablending($rFlipped, false);
  950.                             imagecopyresampled($rFlipped, $rTile, 0, 0, imagesx($rTile)-1, 0, imagesx($rTile), imagesy($rTile), (0-imagesx($rTile)), imagesy($rTile));
  951.                 imagedestroy($rTile);
  952.                 return $rFlipped;
  953.         }
  954.        
  955.         /**
  956.          * Toggle debug
  957.          *
  958.          * Toggles debug mode on or off. When debug mode is enabled, textual error
  959.          * messages may be sent to output.
  960.          *
  961.          * @uses    Level::$bDebug     To read and store debug settings.
  962.          * @see     Level::debug()
  963.          *
  964.          * @access public
  965.          */
  966.         public function toggleDebug() {
  967.                 $this->bDebug = !$this->bDebug;
  968.         }
  969.        
  970.         /**
  971.          * Debug
  972.          *
  973.          * Print debug message if debug mode is enabled.
  974.          *
  975.          * @param   string  $sMessage   The message to print.
  976.          *
  977.          * @uses    Level::$bDebug      Print or not?
  978.          *
  979.          * @access  private
  980.          */
  981.         private function debug($sMessage) {
  982.                 if($this->bDebug) {
  983.                         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>';
  984.                 }
  985.         }
  986. }
  987. ?>