class.Tileset.php

  1. <?php
  2. /**
  3.  * class.Tileset.php
  4.  *
  5.  * Provides an interface to read tileset (J2T) 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    1.0
  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.  * Tileset class
  33.  *
  34.  * Reads and interprets Jazz Jackrabbit 2 Tileset files
  35.  *
  36.  * @package    J2Ov4
  37.  * @author     Stijn Peeters
  38.  * @copyright  Copyright (c) 2009, Stijn Peeters
  39.  * @version    1.0
  40.  */
  41. class Tileset {
  42.         /**
  43.          * @var  int     The handle to the tileset tile to read from.
  44.          * @access private
  45.          */
  46.         private $rResource;
  47.         /**
  48.          * @var  mixed   Tileset info header bytes.
  49.          * @access private
  50.          */
  51.         private $_TilesetInfo   = false;
  52.         /**
  53.          * @var  mixed   Image Data bytes (raw pixels).
  54.          * @access private
  55.          */
  56.         private $_ImageData     = false;
  57.         /**
  58.          * @var  mixed   Transparency mask data bytes (raw bitmask).
  59.          * @access private
  60.          */
  61.         private $_TransparencyData = false;
  62.         /**
  63.          * @var  mixed   Mask data bytes (raw bitmask).
  64.          * @access private
  65.          */
  66.         private $_MaskData      = false;
  67.         /**
  68.          * @var  mixed   The handle to the tileset tile to read from.
  69.          * @access private
  70.          */
  71.         private $_Header        = false;
  72.         /**
  73.          * @var  int     The tileset image (GDLib resource).
  74.          * @access private
  75.          */
  76.         private $rTilesetImage;
  77.         /**
  78.          * @var  int     The mask image (GDLib resource).
  79.          * @access private
  80.          */
  81.         private $rMaskImage;
  82.         /**
  83.          * @var  array   Tileset palette, each index is an array(r, g, b)
  84.          * @access private
  85.          */
  86.         private $aPalette       = array();
  87.         /**
  88.          * @var  boolean Magic variable to check whether tileset can be loaded or not.
  89.          * @access public
  90.          */
  91.         public  $bValid         = false;
  92.         /**
  93.          * @var  array   Byte offsets for several parts of the tileset. Available keys:
  94.          *               <ul>
  95.          *                 <li><samp>info_c</samp>: Tileset Info compressed size.</li>
  96.          *                 <li><samp>info_u</samp>: Tileset Info uncompressed size.</li>
  97.          *                 <li><samp>data_c</samp>: Image data compressed size.</li>
  98.          *                 <li><samp>data_u</samp>: Image data uncompressed size.</li>
  99.          *                 <li><samp>tiles</samp>: Amount of tiles in the tileset.</li>
  100.          *               </ul>
  101.          * @access private
  102.          */
  103.         private $aOffsets       = array();
  104.         /**
  105.          * @var  array   Used to keep track of which tiles have already been rendered by
  106.          *               tileset preview rendering methods.
  107.          * @access private
  108.          */
  109.         private $aCache         = array();
  110.         /**
  111.          * @var  int     Maximum amount of tiles to render/parse. Defaults to 1024, which
  112.          *               is the limit for 1.23 tilesets.
  113.          * @access private
  114.          */
  115.         private $iMaxTiles      = 1024;
  116.         /**
  117.          * Size of the Tileset header
  118.          */
  119.         const SIZE_HEADER = 262;
  120.         /**
  121.          * Magic byte value for TSF tilesets
  122.          */
  123.         const VERSION_TSF = 513;
  124.         /**
  125.          * Magic byte value for 1.23 tilesets
  126.          */
  127.         const VERSION_123 = 512;
  128.        
  129.         /**
  130.          * Amount of pixels in a tile that need to be masked for the tile itself to be
  131.          * considered fully masked for the boolean masking function. Percentages;
  132.          * giving this constant a value of 25 will mean 25% of the pixels in a tile
  133.          * need to be solid for it to be considered masked.
  134.          */
  135.         const MASK_TRESHOLD = 1;
  136.        
  137.         /**
  138.          * Constructor method
  139.          *
  140.          * Sets up the object, checking whether given file path is valid and giving several
  141.          * variables initial values. Then it reads the header via another method.
  142.          *
  143.          * @param   string  $sFilename  The file path pointing to the tileset to use.
  144.          *
  145.          * @uses    Tileset::getHeader() To read the tileset header subsequently.
  146.          *
  147.          * @access  public
  148.          */
  149.         public function __construct($sFilename) {
  150.                 if(!is_readable($sFilename)) {
  151.                         //try some alternative file names
  152.                         $aFilename = explode('/', $sFilename);
  153.                         $sTempName = array_pop($aFilename);
  154.                         $sPath = implode('/', $aFilename);
  155.                         if(is_readable($sPath.'/'.strtoupper($sTempName))) {
  156.                                 $sFilename = $sPath.'/'.strtoupper($sTempName);
  157.                         } elseif(is_readable($sPath.'/'.strtolower($sTempName))) {
  158.                                 $sFilename = $sPath.'/'.strtolower($sTempName);
  159.                         } elseif(is_readable($sPath.'/'.ucfirst($sTempName))) {
  160.                                 $sFilename = $sPath.'/'.ucfirst($sTempName);
  161.                         } else {
  162.                                 return false;
  163.                         }
  164.                 }
  165.                
  166.                
  167.                 $this->sPath = $sFilename;
  168.                 $this->rResource = fopen($this->sPath, 'rb');
  169.                 $this->bValid = ($this->rResource !== false);
  170.  
  171.                 $this->aPalette      = false;
  172.                 $this->rTilesetImage = false;
  173.                 $this->rMaskImage    = false;
  174.                
  175.                 $this->aCache = array();
  176.  
  177.  
  178.  
  179.                 $this->getHeader();
  180.         }
  181.        
  182.         /**
  183.          * Retrieve tileset header
  184.          *
  185.          * The tileset header (first 262 bytes) contains offsets for the actual tileset data
  186.          * which is retrieved here, and stored in the offsets array. The header is saved too
  187.          * for later usage. The palette is then analyzed and also stored. It also determines
  188.          * whether the tileset is a TSF or 1.23 tileset and sets some variables based on that
  189.          * such as the amount of tiles to look for.
  190.          *
  191.          * @uses    Tileset::$aOffsets  To store the offsets found.
  192.          * @uses    Tileset::$_Header   To store the tileset header.
  193.          * @uses    Tileset::getPalette() To analyze the palette.
  194.          * @uses    Tileset::$iMaxTiles To store the (version-specific) max amount of tiles.
  195.          * @uses    Tileset::$rResource To read from the tileset file.
  196.          *
  197.          * @access  private
  198.          */
  199.         private function getHeader() {
  200.                 $this->_Header = fread($this->rResource, self::SIZE_HEADER);
  201.                 list(,$this->sVersion) = unpack('v', substr($this->_Header,220,2));
  202.                 $this->iMaxTiles = ($this->sVersion == self::VERSION_TSF) ? 4096: 1024;
  203.                 $this->aOffsets = array();
  204.                 list(,$this->aOffsets['info_c']) = unpack('V',substr($this->_Header,230,4));
  205.                 list(,$this->aOffsets['info_u']) = unpack('V',substr($this->_Header,234,4));
  206.                 list(,$this->aOffsets['data_c']) = unpack('V',substr($this->_Header,238,4));
  207.                 list(,$this->aOffsets['data_u']) = unpack('V',substr($this->_Header,242,4));
  208.                 list(,$this->aOffsets['tran_c']) = unpack('V',substr($this->_Header,246,4));
  209.                 list(,$this->aOffsets['tran_u']) = unpack('V',substr($this->_Header,250,4));
  210.                 list(,$this->aOffsets['mask_c']) = unpack('V',substr($this->_Header,254,4));
  211.                 list(,$this->aOffsets['mask_u']) = unpack('V',substr($this->_Header,258,4));
  212.                 $this->getPalette();
  213.                 list(,$this->aOffsets['tiles']) = unpack('V',substr($this->_TilesetInfo,1024,4));
  214.         }
  215.        
  216.         /**
  217.          * Get tileset name
  218.          *
  219.          * Determines the tileset's name as it shows up in JCS.
  220.          *
  221.          * @returns string              The tileset name. White spaces and NULs are trimmed off.
  222.          *
  223.          * @access  public
  224.          */
  225.         public function getName() {
  226.                 return trim(substr($this->_Header,188,32));
  227.         }
  228.  
  229.         /**
  230.          * Get number of tiles
  231.          *
  232.          * Get the number of tiles in the tileset
  233.          *
  234.          * @returns int                 The number of tiles.
  235.          *
  236.          * @access  public
  237.          */
  238.         public function getNumTiles() {
  239.                 return $this->aOffsets['tiles'];
  240.         }
  241.        
  242.         /**
  243.          * Uncompresses the part of the tileset file containing information on what tile goes where.
  244.          * This retrieval is only done once and always after the header is uncompressed.
  245.          *
  246.          * @returns string             The raw byte-for-byte image data.
  247.          *
  248.          * @uses    Tileset::$_ImageData To store the raw image data.
  249.          *
  250.          * @access  private
  251.          */
  252.         private function getImageData() {
  253.                 if($this->_ImageData === false) {
  254.                         $this->_ImageData = gzuncompress(fread($this->rResource, $this->aOffsets['data_c']));
  255.                 }
  256.                 return $this->_ImageData;
  257.         }
  258.  
  259.         /**
  260.          * Generate tileset image
  261.          *
  262.          * Goes through the tile indexes one by one and draws that tile to the tileset image
  263.          * pixel for pixel. If a tile is a duplicate of another tile, that other tile is copied
  264.          * instead. The image is then saved so it does not need to be generated again if the method
  265.          * is called again. If the image data is not uncompressed yet the method to do so is
  266.          * called.
  267.          *
  268.          * @returns integer             The image resource (GDLib) containing the full tileset
  269.          *                              image.
  270.          *
  271.          * @uses    Tileset::$_TilesetInfo To get the offsets within the image data
  272.          *                              for each tile.
  273.          * @uses    Tileset::getImageData() To retrieve the raw image data.
  274.          * @uses    Tileset::$aPalette  To get the correct color values for the tile pixels
  275.          * @uses    Tileset::$aCache    To keep track of which unique tiles have been drawn;
  276.          *                              if a tile exist in this array (as a key) and is seen
  277.          *                              again, it is copied rather than redrawn.
  278.          * @uses    Tileset::$rTilesetImage To store the generated image.
  279.          * @uses    Tileset::$iMaxTiles To determine how much data to read (version-specific).
  280.          * @uses    Tileset::$aOffsets  To find out how big the image data is (depends on version).
  281.          * @uses    Tileset::$rResource To read from the tileset file.
  282.          *
  283.          * @access  public
  284.          */
  285.         public function getTilesetImage() {
  286.                 if(!$this->bValid) return false;
  287.  
  288.                 $iImage = imagecreatetruecolor(320,(($this->aOffsets['tiles']/10)*32));
  289.                 imagefill($iImage, 1, 1, imagecolorallocate($iImage, 87, 0, 203)); //"JCS blue"
  290.                 //Every byte in the Map array points to a tile index in $_ImageData (1024 bytes per tile)
  291.                 $aMap = unpack('V*', substr($this->_TilesetInfo, 1024+4+2*$this->iMaxTiles, ($this->iMaxTiles*4)));
  292.                 //loop through the tiles in the map
  293.                 foreach($aMap as $iTile => $iIndex) {
  294.                         if($iIndex == 0) { //empty tile
  295.                                 continue;
  296.                         }
  297.                         $iTile -= 1; //else a "ghost" empty tile is drawn on the first row
  298.                         $x = (($iTile % 10)*32);
  299.                         $y = (floor($iTile/10)*32);
  300.                                 if(array_key_exists($iIndex, $this->aCache)) { //tile already drawn?
  301.                                 imagecopy($iImage, $iImage, $x, $y, (($this->aCache[$iIndex]%10)*32), (floor($this->aCache[$iIndex]/10)*32), 32, 32);
  302.                         } else {
  303.                                 for($iPixel=0;$iPixel<1024;$iPixel++) { //1024 pixels per tile
  304.                                         //the byte value is the palette index for this tile
  305.                                         $iColor = ord(substr($this->getImageData(), $iIndex+$iPixel, 1));
  306.                                         $nx = $x+($iPixel%32);
  307.                                         $ny = $y+floor($iPixel/32);
  308.                                         if($iColor > 0) {
  309.                                                 $rColor = imagecolorallocate($iImage, $this->aPalette[$iColor][0], $this->aPalette[$iColor][1], $this->aPalette[$iColor][2]);
  310.                                                 imagesetpixel($iImage, $nx, $ny, $rColor);
  311.                                         }
  312.                                 }
  313.                         //save tile to cache
  314.                         $this->aCache[$iIndex] = $iTile;
  315.                         }
  316.                 }
  317.                 //save the image
  318.                 $this->rTilesetImage = $iImage;
  319.                 return $this->rTilesetImage;
  320.         }
  321.  
  322.         /**
  323.          * Uncompresses the part of the tileset file containing information on what part
  324.          * of a tile is masked. This retrieval is only done once and always after both
  325.          * the header and the image data are uncompressed. If this is not done yet
  326.          * methods to do so are called first.
  327.          *
  328.          * @returns string             The raw bit-for-bit mask data.
  329.          *
  330.          * @uses    Tileset::$getImageData() To retrieve the raw image data.
  331.          * @uses    Tileset::$_TransparencyData To store the raw transparency data. This
  332.          *                             data is not yet used anywhere in the script.
  333.          * @uses    Tileset::$_MaskData To store the raw mask data.
  334.          *
  335.          * @access  private
  336.          */
  337.         private function getMaskData() {
  338.                 if(!$this->_ImageData) {
  339.                         $this->getImageData();
  340.                 }
  341.                 if(!$this->_MaskData) {
  342.                         $this->_TransparencyData = gzuncompress(fread($this->rResource, $this->aOffsets['tran_c']));
  343.                         $this->_MaskData = gzuncompress(fread($this->rResource, $this->aOffsets['mask_c']));
  344.                 }
  345.                 return $this->_MaskData;
  346.         }
  347.        
  348.         /**
  349.          * Generate mask image
  350.          *
  351.          * Generates a GDLib image resource representing the tileset mask. This is a
  352.          * black-and-white image, except the background is made "JCS Blue".
  353.          *
  354.          * @returns integer             The image resource (GDLib) containing the mask
  355.          *                              image.
  356.          *
  357.          * @uses    Tileset::$_TilesetInfo To get the offsets within the mask data
  358.          *                              for each tile.
  359.          * @uses    Tileset::getMaskData() To retrieve the raw mask data.
  360.          * @uses    Tileset::$rMaskImage To store the generated image.
  361.          * @uses    Tileset::$iMaxTiles To determine how much data to read (version-specific).
  362.          * @uses    Tileset::$aOffsets  To find out how big the image data is (depends on version).
  363.          * @uses    Tileset::$rResource To read from the tileset file.
  364.          *
  365.          * @access  public
  366.          */
  367.         public function getMaskImage() {
  368.                 if(!$this->bValid) return false;
  369.  
  370.                 $iPic = imagecreatetruecolor(320,(($this->aOffsets['tiles']/10)*32));
  371.                 imagefill($iPic, 1, 1, imagecolorallocate($iPic, 87, 0, 203)); //"JCS blue"
  372.                 $aMap = unpack('V*', substr($this->_TilesetInfo, 1024+4+18*$this->iMaxTiles, ($this->iMaxTiles*4)));
  373.                 $iBlack = imagecolorallocate($iPic, 0, 0, 0);
  374.                 foreach($aMap as $iTile => $iIndex) {
  375.                         if($iIndex == 0) { //empty tile
  376.                                 continue;
  377.                         }
  378.                         $iTile -= 1; //else a "ghost" empty tile is drawn on the first row
  379.                         $x = (($iTile % 10)*32);
  380.                         $y = (floor($iTile/10)*32);
  381.                         for($iPixel=0;$iPixel<128;$iPixel++) { //128 bytes per tile
  382.                                 $iByte = ord(substr($this->getMaskData(), $iIndex+$iPixel, 1));
  383.                                         //bit by bit... if bit = 1 then draw a pixel, if 0 then transparent
  384.                                 for($i=0;$i<8;$i++) {
  385.                                         $iValue = ($iByte & pow(2, $i)); //bit value
  386.                                         $iBit = (($iPixel * 8) + $i); //bit index, for position
  387.                                         $nx = $x + ($iBit % 32);
  388.                                         $ny = $y + floor($iBit/32);
  389.                                         if($iValue != 0) {
  390.                                                 imagesetpixel($iPic, $nx, $ny, $iBlack);
  391.                                         }
  392.                                 }
  393.                         }
  394.                 }
  395.                 $this->rMaskImage = $iPic;
  396.                 return $this->rMaskImage;
  397.         }
  398.        
  399.         /**
  400.          * Generate boolean mask array
  401.          *
  402.          * This method generated an array with one value for each tile (in sequential
  403.          * order). This can be used, for example, to generate level preview images
  404.          * without generating a tile-for-tile preview, which is very CPU intensive.
  405.          *
  406.          * @returns array               Array with one value per tile. The value is
  407.          *                              either <samp>true</samp> or <samp>false</samp>;
  408.          *                              when the value is <samp>true</samp>, this
  409.          *                              means the tile should be considered "masked".
  410.          *
  411.          * @uses    Tileset::MASK_TRESHOLD To determine what percentage of pixels in a
  412.          *                              tile need to be solid for it to be considered
  413.          *                              masked.
  414.          * @uses    Tileset::$_TilesetInfo To get the offsets within the mask data
  415.          *                              for each tile.
  416.          * @uses    Tileset::getMaskData() To retrieve the raw mask data.
  417.          *
  418.          * @access  public
  419.          */
  420.         public function getBooleanMask() {
  421.                 if(!$this->bValid) return false;
  422.  
  423.                 $aBitMask = array();
  424.                 $aMap = unpack('V*', substr($this->_TilesetInfo, 1024+4+18*$this->iMaxTiles, ($this->iMaxTiles*4)));
  425.                 $i=0;
  426.                 $iMaskTreshold = (32640/(100/self::MASK_TRESHOLD)); //32640 = max value for the 128 bytes
  427.                 foreach($aMap as $iTile => $iIndex) {
  428.                         $iMaskTotal = 0;
  429.                         for($iPixel=0;$iPixel<128;$iPixel++) { //128 bytes per tile
  430.                                 $iMaskTotal += ord(substr($this->getMaskData(), $iIndex+$iPixel, 1));
  431.                         }
  432.                         $aBitMask[$i] = ($iMaskTotal>=$iMaskTreshold) ? true : false;
  433.                         $i++;
  434.                 }
  435.                 return $aBitMask;
  436.         }
  437.        
  438.         /**
  439.          * Get tileset palette
  440.          *
  441.          * Palettes are stored as 4 bytes per entry, rgb0. This method goes through this raw data,
  442.          * read from the tileset file, and stores every entry in an array for later use. The array
  443.          * is saved so subsequent calls don't need to regenerate the palette. The palette entries
  444.          * are stored as an array(r,g,b). The whole Tileset Info struct is saved by this method,
  445.          * and will be used by other methods. This method is called automatically upon object
  446.          * initialization to make all this data available immediately.
  447.          *
  448.          * @returns array               The palette array.
  449.          *
  450.          * @uses    Tileset::$_TilesetInfo To store the tileset info struct.
  451.          * @uses    Tileset::$aPalette  To store the palette array.
  452.          * @uses    Tileset::$aOffsets  To find out how big the info struct is (depends on version).
  453.          * @uses    Tileset::$rResource To read from the tileset file.
  454.          *
  455.          * @access  public
  456.          */
  457.         public function getPalette() {
  458.                 if(!$this->bValid) return false;
  459.  
  460.                 if($this->_TilesetInfo === false) {
  461.                         $this->_TilesetInfo = gzuncompress(fread($this->rResource, $this->aOffsets['info_c']));
  462.                         for($i=0; $i<256; $i++) {
  463.                                 $aPalette[] = array(
  464.                                         ord($this->_TilesetInfo[$i*4+0]),
  465.                                         ord($this->_TilesetInfo[$i*4+1]),
  466.                                         ord($this->_TilesetInfo[$i*4+2]));
  467.                         }
  468.                         $this->aPalette = $aPalette;
  469.                 }
  470.                 return $this->aPalette;
  471.         }
  472. }