1
0
mirror of https://github.com/Rogiel/star-map synced 2025-12-05 23:02:50 +00:00

Initial commit

This commit is contained in:
2016-07-24 00:04:54 -03:00
commit 3e0132c96e
7 changed files with 978 additions and 0 deletions

51
README.md Normal file
View File

@@ -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!

30
composer.json Normal file
View File

@@ -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"
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* PixForce
*
* @link http://www.pixforce.com.br/
* @copyright Copyright (c) 2016 PixForce (http://www.pixforce.com.br)
* @license Proprietary
*/
namespace Rogiel\StarMap\Entity;
use Rogiel\MPQ\Stream\Parser\BinaryStreamParser;
class DocumentHeader {
const DEFAULT_LOCALE = 'enUS';
private $name = array();
private $shortDescription = array();
private $longDescription = array();
// -----------------------------------------------------------------------------------------------------------------
public function __construct(BinaryStreamParser $parser) {
$parser->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;
}
}

527
src/Entity/MapInfo.php Normal file
View File

@@ -0,0 +1,527 @@
<?php
/**
* PixForce
*
* @link http://www.pixforce.com.br/
* @copyright Copyright (c) 2016 PixForce (http://www.pixforce.com.br)
* @license Proprietary
*/
namespace Rogiel\StarMap\Entity;
use Rogiel\MPQ\Stream\Parser\BinaryStreamParser;
use Rogiel\StarMap\Exception\MapException;
class MapInfo {
/**
* The map info file format version
*
* @var integer
*/
private $version;
private $unknown1;
private $unknown2;
/**
* The full map width
*
* @var integer
*/
private $width;
/**
* The full map height
*
* @var integer
*/
private $height;
/**
* Small map preview type: 0 = None, 1 = Minimap, 2 = Custom
*
* @var integer
*/
private $smallPreviewType;
/**
* (Optional) Small map preview path; relative to root of map archive
*
* @var string
*/
private $smallPreviewPath;
/**
* Large map preview type: 0 = None, 1 = Minimap, 2 = Custom
*
* @var integer
*/
private $largePreviewType;
/**
* (Optional) Large map preview path; relative to root of map archive
*
* @var string
*/
private $largePreviewPath;
private $unknown3;
private $unknown4;
private $unknown5;
private $unknown6;
/**
* The type of fog of war used on the map
*
* @var string
*/
private $fogType;
/**
* The tile set used on the map
*
* @var string
*/
private $tileSet;
/**
* The left bounds for the camera. This value is 7 less than the value shown in the editor.
*
* @var integer
*/
private $cameraLeft;
/**
* The bottom bounds for the camera. This value is 4 less than the value shown in the editor.
*
* @var integer
*/
private $cameraBottom;
/**
* The right bounds for the camera. This value is 7 more than the value shown in the editor.
*
* @var integer
*/
private $cameraRight;
/**
* The top bounds for the camera. This value is 4 more than the value shown in the editor.
*
* @var integer
*/
private $cameraTop;
/**
* The map base height (what is that?). This value is 4096*Base Height in the editor (giving a decimal value).
*
* @var integer
*/
private $baseHeight;
// MIGHT NOT BE ACCURATE AFTER THIS
/**
* Load screen type: 0 = default, 1 = custom
*
* @var integer
*/
private $loadScreenType;
/**
* (Optional) Load screen image path; relative to root of map archive
*
* @var string
*/
private $loadScreenPath;
/**
* Unknown string, usually empty
*
* @var string
*/
private $unknown7;
/**
* Load screen image scaling strategy: 0 = normal, 1 = aspect scaling, 2 = stretch the image.
*
* @var integer
*/
private $loadScreenScaling;
/**
* The text position on the loading screen. One of:
* 0xffffffff = (Default)
* 0 = Top Left
* 1 = Top
* 2 = Top Right
* 3 = Left
* 4 = Center
* 5 = Right
* 6 = Bottom Left
* 7 = Bottom
* 8 = Bottom Right
*
* @var integer
*/
private $textPosition;
/**
* Loading screen text position offset x
*
* @var integer
*/
private $textPositionOffsetX;
/**
* Loading screen text position offset y
*
* @var integer
*/
private $textPositionOffsetY;
/**
* Loading screen text size x
*
* @var integer
*/
private $textPositionSizeX;
/**
* Loading screen text size y
*
* @var integer
*/
private $textPositionSizeY;
/**
* A bit array of flags with the following options (possibly incomplete)
*
* 0x00000001 = Disable Replay Recording
* 0x00000002 = Wait for Key (Loading Screen)
* 0x00000004 = Disable Trigger Preloading
* 0x00000008 = Enable Story Mode Preloading
* 0x00000010 = Use Horizontal Field of View
*
* @var integer
*/
private $dataFlags;
private $unknown8;
private $unknown9;
private $unknown10;
private $unknown11;
public function __construct(BinaryStreamParser $parser) {
$magic = $parser->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;
}
}

99
src/Entity/Minimap.php Normal file
View File

@@ -0,0 +1,99 @@
<?php
/**
* PixForce
*
* @link http://www.pixforce.com.br/
* @copyright Copyright (c) 2016 PixForce (http://www.pixforce.com.br)
* @license Proprietary
*/
namespace Rogiel\StarMap\Entity;
use Rogiel\MPQ\Stream\Block\BlockStream;
use Rogiel\MPQ\Stream\Parser\BinaryStreamParser;
use Rogiel\MPQ\Stream\Stream;
use Rogiel\StarMap\Exception\MapException;
class Minimap {
/**
* @var resource
*/
private $resource;
// -----------------------------------------------------------------------------------------------------------------
public function __construct(Stream $stream) {
$buffer = '';
while (($read = $stream->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;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Copyright (c) 2016, Rogiel Sulzbach
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace Rogiel\StarMap\Exception;
class MapException extends \Exception {
}

132
src/Map.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
/**
* PixForce
*
* @link http://www.pixforce.com.br/
* @copyright Copyright (c) 2016 PixForce (http://www.pixforce.com.br)
* @license Proprietary
*/
namespace Rogiel\StarMap;
use Rogiel\MPQ\MPQFile;
use Rogiel\MPQ\Stream\Parser\BinaryStreamParser;
use Rogiel\StarMap\Entity\DocumentHeader;
use Rogiel\StarMap\Entity\MapInfo;
use Rogiel\StarMap\Entity\Minimap;
use Rogiel\StarMap\Exception\MapException;
class Map {
/**
* @var MPQFile
*/
private $file;
// -----------------------------------------------------------------------------------------------------------------
/**
* The processed MapInfo file
*
* @var MapInfo
*/
private $mapInfo;
/**
* The processed DocumentHeader file
*
* @var DocumentHeader
*/
private $documentHeader;
/**
* The processed Minimap file
*
* @var Minimap
*/
private $minimap;
// -----------------------------------------------------------------------------------------------------------------
/**
* Map constructor.
* @param $file string|MPQFile the map file name or MPQFile instance
* @throws MapException if the file given is not a string or a mpq file
*/
public function __construct($file) {
if(is_string($file)) {
$file = MPQFile::parseFile($file);
}
if(!$file instanceof MPQFile) {
throw new MapException("Invalid map file given");
}
$this->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;
}
// -----------------------------------------------------------------------------------------------------------------
}