commit 3e0132c96e71127b0508dfe0c3bd992c2c3d46f2 Author: Rogiel Sulzbach Date: Sun Jul 24 00:04:54 2016 -0300 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c355ab --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Star Map + +This library allows you to read StarCraft II map files from PHP. + +A object-oriented API is provided to browse through the metadata and the minimap image. + +## Features +* Read .SC2Map files from all public game versions +* **Minimap**: Allows to read the embeded minimap image + +## Installation + +The recommended way of installing this library is using Composer. + + composer require "rogiel/star-map" + +This library uses [php-mpq](https://github.com/Rogiel/php-mpq) to parse and extract compressed information inside the map file. + +## Example + +```php +use Rogiel\StarMap\Map; + +// Parse the map +$map = new Map('Ruins of Seras.SC2Map'); + +// Get the map name in multiple locales +$documentHeader = $map->getDocumentHeader(); + +echo sprintf('Map name (English): %s', $documentHeader->getName()).PHP_EOL; // english is default +echo sprintf('Map name (French): %s', $documentHeader->getName('frFR')).PHP_EOL; + +// Get the map size +$mapInfo = $map->getMapInfo(); +$x = $mapInfo->getWidth(); +$y = $mapInfo->getHeight(); +echo sprintf('Map size: %sx%s', $x, $y).PHP_EOL; + +// Export Minimap image as a PNG +$map->getMinimap()->toPNG('Minimap.png'); +``` + +The output to the snippet above is the following: + +``` +Map name (English): Ruins of Seras +Map name (French): Ruines de Seras +Map size: 224x192 +``` + +Have fun! \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4ee116e --- /dev/null +++ b/composer.json @@ -0,0 +1,30 @@ +{ + "name": "rogiel/star-map", + "type": "library", + "description": "A StarCraft II map parser in PHP", + "keywords": ["StarCraft II", "Map parsing", "Gaming", "Blizzard"], + "homepage": "https://rogiel.com/portfolio/star-map", + "license": "BSD-2.0", + "authors": [{ + "name": "Rogiel Sulzbach", + "email": "rogiel@rogiel.com", + "homepage": "https://rogiel.com" + }], + "autoload": { + "psr-4": { + "Rogiel\\StarMap\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Rogiel\\StarMap\\Tests\\": "tests/" + } + }, + "require": { + "php": ">=5.5", + "rogiel/mpq": "^0.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^4.8" + } +} diff --git a/src/Entity/DocumentHeader.php b/src/Entity/DocumentHeader.php new file mode 100644 index 0000000..74f9796 --- /dev/null +++ b/src/Entity/DocumentHeader.php @@ -0,0 +1,105 @@ +readBytes(44); + + $numDeps = $parser->readByte(); + $parser->readBytes(3); + while ($numDeps > 0) { + while ($parser->readByte() !== 0); + $numDeps--; + } + $numAttribs = $parser->readUInt32(); + $attribs = array(); + while ($numAttribs > 0) { + $keyLen = $parser->readUInt16(); + $key = $parser->readBytes($keyLen); + $locale = hex2bin(dechex($parser->readUInt32())); + $valueLen = $parser->readUInt16(); + $value = $parser->readBytes($valueLen); + $attribs[$key][$locale] = $value; + $numAttribs--; + } + + if(isset($attribs['DocInfo/Name'])) { + $this->name = $attribs['DocInfo/Name']; + } + if(isset($attribs['DocInfo/DescShort'])) { + $this->shortDescription = $attribs['DocInfo/DescShort']; + } + if(isset($attribs['DocInfo/DescLong'])) { + $this->longDescription = $attribs['DocInfo/DescLong']; + } + } + + // ----------------------------------------------------------------------------------------------------------------- + + /** + * @return string|null + */ + public function hasLocale($locale) { + return $this->getName($locale) != NULL; + } + + /** + * @return array + */ + public function getLocales() { + return array_keys($this->name); + } + + // ----------------------------------------------------------------------------------------------------------------- + + /** + * @return string|null + */ + public function getName($locale = DocumentHeader::DEFAULT_LOCALE) { + if(isset($this->name[$locale])) { + return $this->name[$locale]; + } + return NULL; + } + + /** + * @return string|null + */ + public function getShortDescription($locale = DocumentHeader::DEFAULT_LOCALE) { + if(isset($this->shortDescription[$locale])) { + return $this->shortDescription[$locale]; + } + return NULL; + } + + /** + * @return string|null + */ + public function getLongDescription($locale = DocumentHeader::DEFAULT_LOCALE) { + if(isset($this->longDescription[$locale])) { + return $this->longDescription[$locale]; + } + return NULL; + } + + +} \ No newline at end of file diff --git a/src/Entity/MapInfo.php b/src/Entity/MapInfo.php new file mode 100644 index 0000000..1ed30c5 --- /dev/null +++ b/src/Entity/MapInfo.php @@ -0,0 +1,527 @@ +readBytes(4); + if($magic !== 'IpaM') { + throw new MapException('Invalid MapInfo magic header'); + } + + $this->version = $parser->readUInt32(); + if ($this->version >= 0x18) { + $this->unknown1 = $parser->readUInt32(); + $this->unknown2 = $parser->readUInt32(); + } + + $this->width = $parser->readUInt32(); + $this->height = $parser->readUInt32(); + + $this->smallPreviewType = $parser->readUInt32(); + if ($this->smallPreviewType == 2) { + $this->smallPreviewPath = $parser->readCString(); + } + + $this->largePreviewType = $parser->readUInt32(); + if ($this->largePreviewType == 2) { + $this->largePreviewPath = $parser->readCString(); + } + + if ($this->version >= 0x1f) { + $this->unknown3 = $parser->readCString(); + } + + if ($this->version >= 0x26) { + $this->unknown4 = $parser->readCString(); + } + + if ($this->version >= 0x1f) { + $this->unknown5 = $parser->readUInt32(); + } + + $this->unknown6 = $parser->readUInt32(); + $this->fogType = $parser->readCString(); + $this->tileSet = $parser->readCString(); + $this->cameraLeft = $parser->readUInt32(); + $this->cameraBottom = $parser->readUInt32(); + $this->cameraRight = $parser->readUInt32(); + $this->cameraTop = $parser->readUInt32(); + $this->baseHeight = $parser->readUInt32() / 4096; + + // ------------------------------------------------------------------------------------------------------------- + + $this->loadScreenType = $parser->readUInt32(); + $this->loadScreenPath = $parser->readCString(); + $this->unknown7 = $parser->readBytes($parser->readUInt16()); + $this->loadScreenScaling = $parser->readUInt32(); + $this->textPosition = $parser->readUInt32(); + $this->textPositionOffsetX = $parser->readUInt32(); + $this->textPositionOffsetY = $parser->readUInt32(); + $this->textPositionSizeX = $parser->readUInt32(); + $this->textPositionSizeY = $parser->readUInt32(); + $this->dataFlags = $parser->readUInt32(); + $this->unknown8 = $parser->readUInt32(); + + if ($this->version >= 0x19) { + $this->unknown9 = $parser->readBytes(8); + } + if ($this->version >= 0x1f) { + $this->unknown10 = $parser->readBytes(9); + } + if ($this->version >= 0x20) { + $this->unknown11 = $parser->readBytes(4); + } + + // there are more fields, but the implementation of them have been ommited + } + + // ----------------------------------------------------------------------------------------------------------------- + + /** + * @return int + */ + public function getVersion() { + return $this->version; + } + + /** + * @return mixed + */ + public function getUnknown1() { + return $this->unknown1; + } + + /** + * @return mixed + */ + public function getUnknown2() { + return $this->unknown2; + } + + /** + * @return int + */ + public function getWidth() { + return $this->width; + } + + /** + * @return int + */ + public function getHeight() { + return $this->height; + } + + /** + * @return int + */ + public function getSmallPreviewType() { + return $this->smallPreviewType; + } + + /** + * @return string + */ + public function getSmallPreviewPath() { + return $this->smallPreviewPath; + } + + /** + * @return int + */ + public function getLargePreviewType() { + return $this->largePreviewType; + } + + /** + * @return string + */ + public function getLargePreviewPath() { + return $this->largePreviewPath; + } + + /** + * @return string + */ + public function getUnknown3() { + return $this->unknown3; + } + + /** + * @return string + */ + public function getUnknown4() { + return $this->unknown4; + } + + /** + * @return mixed + */ + public function getUnknown5() { + return $this->unknown5; + } + + /** + * @return mixed + */ + public function getUnknown6() { + return $this->unknown6; + } + + /** + * @return string + */ + public function getFogType() { + return $this->fogType; + } + + /** + * @return string + */ + public function getTileSet() { + return $this->tileSet; + } + + /** + * @return int + */ + public function getCameraLeft() { + return $this->cameraLeft; + } + + /** + * @return int + */ + public function getCameraBottom() { + return $this->cameraBottom; + } + + /** + * @return int + */ + public function getCameraRight() { + return $this->cameraRight; + } + + /** + * @return int + */ + public function getCameraTop() { + return $this->cameraTop; + } + + /** + * @return int + */ + public function getBaseHeight() { + return $this->baseHeight; + } + + /** + * @return int + */ + public function getLoadScreenType() { + return $this->loadScreenType; + } + + /** + * @return string + */ + public function getLoadScreenPath() { + return $this->loadScreenPath; + } + + /** + * @return string + */ + public function getUnknown7() { + return $this->unknown7; + } + + /** + * @return int + */ + public function getLoadScreenScaling() { + return $this->loadScreenScaling; + } + + /** + * @return int + */ + public function getTextPosition() { + return $this->textPosition; + } + + /** + * @return int + */ + public function getTextPositionOffsetX() { + return $this->textPositionOffsetX; + } + + /** + * @return int + */ + public function getTextPositionOffsetY() { + return $this->textPositionOffsetY; + } + + /** + * @return int + */ + public function getTextPositionSizeX() { + return $this->textPositionSizeX; + } + + /** + * @return int + */ + public function getTextPositionSizeY() { + return $this->textPositionSizeY; + } + + /** + * @return int + */ + public function getDataFlags() { + return $this->dataFlags; + } + + /** + * @return mixed + */ + public function getUnknown8() { + return $this->unknown8; + } + + /** + * @return string + */ + public function getUnknown9() { + return $this->unknown9; + } + + /** + * @return string + */ + public function getUnknown10() { + return $this->unknown10; + } + + /** + * @return string + */ + public function getUnknown11() { + return $this->unknown11; + } + +} \ No newline at end of file diff --git a/src/Entity/Minimap.php b/src/Entity/Minimap.php new file mode 100644 index 0000000..a773e39 --- /dev/null +++ b/src/Entity/Minimap.php @@ -0,0 +1,99 @@ +readBytes(10240))) { + $buffer .= $read; + } + + $this->resource = self::createImageResourceFromTGA($buffer); + $buffer = NULL; + } + + function __destruct() { + imagedestroy($this->resource); + } + + // ----------------------------------------------------------------------------------------------------------------- + + /** + * @param string $filename + * @param int $compressionLevel + * @param int $filters see imagepng documentation for details on this field + * + * @return bool + */ + public function toPNG($filename, $compressionLevel = 0, $filters = 0) { + return imagepng($this->resource, $filename, $compressionLevel, $filters); + } + + /** + * @param string $filename + * @param int $quality + * + * @return bool + * + */ + public function toJPG($filename, $quality = 75) { + return imagejpeg($this->resource, $filename, $quality); + } + + // ----------------------------------------------------------------------------------------------------------------- + + /** + * @param $data + * @param int $return_array + * + * @return array|resource + */ + private static function createImageResourceFromTGA($data, $return_array = 0) { + $pointer = 18; + $x = 0; + $y = 0; + $w = base_convert(bin2hex(strrev(substr($data, 12, 2))), 16, 10); + $h = base_convert(bin2hex(strrev(substr($data, 14, 2))), 16, 10); + $img = imagecreatetruecolor($w, $h); + + while ($pointer < strlen($data)) { + imagesetpixel($img, $x, $y, base_convert(bin2hex(strrev(substr($data, $pointer, 3))), 16, 10)); + $x++; + + if ($x == $w) { + $y++; + $x = 0; + } + + $pointer += 3; + } + + if ($return_array) + return array($img, $w, $h); + else + return $img; + } + + +} \ No newline at end of file diff --git a/src/Exception/MapException.php b/src/Exception/MapException.php new file mode 100644 index 0000000..6da4045 --- /dev/null +++ b/src/Exception/MapException.php @@ -0,0 +1,34 @@ +file = $file; + $this->file->parse(); + } + + // ----------------------------------------------------------------------------------------------------------------- + + /** + * Gets the MapInfo parsed structure + * + * @return MapInfo + * @throws MapException + */ + public function getMapInfo() { + if($this->mapInfo != NULL) { + return $this->mapInfo; + } + + $stream = $this->file->openStream('MapInfo'); + if($stream == NULL) { + throw new MapException("MapInfo file not found on map MPQ file."); + } + + $parser = new BinaryStreamParser($stream); + $this->mapInfo = new MapInfo($parser); + return $this->mapInfo; + } + + /** + * Gets the MapInfo parsed structure + * + * @return DocumentHeader + * @throws MapException + */ + public function getDocumentHeader($locale = 'enUS') { + if($this->documentHeader != NULL) { + return $this->documentHeader; + } + + $stream = $this->file->openStream('DocumentHeader'); + if($stream == NULL) { + throw new MapException("DocumentHeader file not found on map MPQ file."); + } + + $parser = new BinaryStreamParser($stream); + $this->documentHeader = new DocumentHeader($parser); + return $this->documentHeader; + } + + /** + * @return Minimap + * @throws MapException + */ + public function getMinimap() { + if($this->minimap != NULL) { + return $this->minimap; + } + + $stream = $this->file->openStream('Minimap.tga'); + if($stream == NULL) { + throw new MapException("Minimap.tga file not found on map MPQ file."); + } + + $this->minimap = new Minimap($stream); + return $this->minimap; + } + + // ----------------------------------------------------------------------------------------------------------------- + +} \ No newline at end of file