diff --git a/.gitignore b/.gitignore index 72426657a..59aae3fba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -data/* +data/players/* +data/maps/* *.log *.bat server.properties diff --git a/classes/ChunkParser.class.php b/classes/ChunkParser.class.php new file mode 100644 index 000000000..2fe293f07 --- /dev/null +++ b/classes/ChunkParser.class.php @@ -0,0 +1,27 @@ +client = $client; + $this->map = $this->client->mapParser; + $this->floor = method_exists($this->map, "getFloor"); + $this->column = method_exists($this->map, "getColumn"); + //$this->biome = method_exists($this->map, "getBiome"); + //include("misc/materials.php"); + //$this->material = $material; + //include("misc/biomes.php"); + //$this->biomes = $biomes; + } + + public function getBiome($x, $z){ + $x = (int) $x; + $z = (int) $z; + if($this->biome === true){ + return $this->map->getBiome($x, $z); + }else{ + return 0; + } + } + + /*public function getBiomeName($x, $z){ + $biome = $this->getBiome($x, $z); + return isset($this->biomes[$biome]) ? $this->biomes[$biome]:"Unknown"; + }*/ + + public function getBlockName($x, $y, $z){ + $block = $this->getBlock($x, $y, $z); + return isset($this->material[$block[0]]) ? $this->material[$block[0]]:"Unknown"; + } + + public function getFloor($x, $z, $startY = -1){ + $x = (int) $x; + $z = (int) $z; + if($this->floor === true){ + $map = $this->map->getFloor($x, $z, $startY); + return $map; + }else{ + $startY = ((int) $startY) > -1 ? ((int) $startY):HEIGHT_LIMIT - 1; + for($y = $startY; $y > 0; --$y){ + $block = $this->getBlock($x, $y, $z); + if(!isset($this->material["nosolid"][$block[0]])){ + break; + } + } + return array($y, $block[0], $block[1]); + } + } + + public function changeBlock($x, $y, $z, $block, $metadata = 0){ + $x = (int) $x; + $y = (int) $y; + $z = (int) $z; + return $this->map->changeBlock($x, $y, $z, $block, $metadata); + } + + public function getBlock($x, $y, $z){ + $x = (int) $x; + $y = (int) $y; + $z = (int) $z; + return $this->map->getBlock($x, $y, $z); + } + + public function getColumn($x, $z){ + $x = (int) $x; + $z = (int) $z; + if($this->column === true){ + return $this->map->getColumn($x, $z); + }else{ + $zone = $this->getZone($x,0,$z,$x,HEIGHT_LIMIT,$z); + $data = array(); + foreach($zone as $x => $a){ + foreach($a as $y => $b){ + foreach($b as $z => $block){ + $data[$y] = $block; + } + } + } + return $data; + } + } + + public function getEllipse($x, $y, $z, $rX = 4, $rZ = 4, $rY = 4){ + $x = (int) $x; + $y = (int) $y; + $z = (int) $z; + $rY = abs((int) $rX); + $rY = abs((int) $rZ); + $rY = abs((int) $rY); + return $this->getZone($x-$rX,max(0,$y-$rY),$z-$rZ,$x+$rX,$y+$rY,$z+$rZ); + } + + public function getSphere($x, $y, $z, $r=4){ + $x = (int) $x; + $y = (int) $y; + $z = (int) $z; + $r = abs((int) $r); + return $this->getZone($x-$r,max(0,$y-$r),$z-$r,$x+$r,$y+$r,$z+$r); + } + + public function getZone($x1, $y1, $z1, $x2, $y2, $z2){ + $x1 = (int) $x1; + $y1 = (int) $y1; + $z1 = (int) $z1; + $x2 = (int) $x2; + $y2 = (int) $y2; + $z2 = (int) $z2; + if($x1>$x2 or $y1>$y2 or $z1>$z2){ + return array(); + } + $blocks = array(); + for($x=$x1;$x<=$x2;++$x){ + $blocks[$x] = array(); + for($z=$z1;$z<=$z2;++$z){ + $blocks[$x][$z] = array(); + for($y=$y1;$y<=$y2;++$y){ + $blocks[$x][$z][$y] = $this->getBlock($x,$y,$z); + } + } + } + return $blocks; + } + +} \ No newline at end of file diff --git a/classes/MinecraftInterface.class.php b/classes/MinecraftInterface.class.php index ddaa1b054..8714ff342 100644 --- a/classes/MinecraftInterface.class.php +++ b/classes/MinecraftInterface.class.php @@ -55,7 +55,7 @@ class MinecraftInterface{ } protected function writeDump($pid, $raw, $data, $origin = "client", $ip = "", $port = 0){ - if(LOG === true and DEBUG >= 2){ + if(LOG === true and DEBUG >= 3){ $p = "[".(microtime(true) - $this->start)."] [".((($origin === "client" and $this->client === true) or ($origin === "server" and $this->client === false)) ? "CLIENT->SERVER":"SERVER->CLIENT")." ".$ip.":".$port."]: ".(isset($data["id"]) ? "MC Packet ".$this->dataName[$pid]:$this->name[$pid])." (0x".Utils::strTohex(chr($pid)).") [length ".strlen($raw)."]".PHP_EOL; $p .= Utils::hexdump($raw); if(is_array($data)){ diff --git a/classes/NBT.class.php b/classes/NBT.class.php new file mode 100644 index 000000000..6c4ee9b0a --- /dev/null +++ b/classes/NBT.class.php @@ -0,0 +1,102 @@ + + * @version 1.0 + * MODIFIED BY @shoghicp + * + * Dependencies: + * PHP 4.3+ (5.3+ recommended) + */ + +class NBT { + public $root = array(); + + const TAG_END = 0; + const TAG_BYTE = 1; + const TAG_SHORT = 2; + const TAG_INT = 3; + const TAG_LONG = 4; + const TAG_FLOAT = 5; + const TAG_DOUBLE = 6; + const TAG_BYTE_ARRAY = 7; + const TAG_STRING = 8; + const TAG_LIST = 9; + const TAG_COMPOUND = 10; + + public function loadFile($filename) { + if(is_file($filename)) { + $fp = fopen($filename, "rb"); + }else{ + trigger_error("First parameter must be a filename", E_USER_WARNING); + return false; + } + switch(basename($filename, ".dat")){ + case "level": + $version = Utils::readInt(strrev(fread($fp, 4))); + $lenght = Utils::readInt(strrev(fread($fp, 4))); + break; + case "entities": + fread($fp, 12); + break; + } + $this->traverseTag($fp, $this->root); + return end($this->root); + } + + public function traverseTag($fp, &$tree) { + if(feof($fp)) { + return false; + } + $tagType = $this->readType($fp, self::TAG_BYTE); // Read type byte. + if($tagType == self::TAG_END) { + return false; + } else { + $tagName = $this->readType($fp, self::TAG_STRING); + $tagData = $this->readType($fp, $tagType); + $tree[] = array("type"=>$tagType, "name"=>$tagName, "value"=>$tagData); + return true; + } + } + + public function readType($fp, $tagType) { + switch($tagType) { + case self::TAG_BYTE: // Signed byte (8 bit) + return Utils::readByte(fread($fp, 1)); + case self::TAG_SHORT: // Signed short (16 bit, big endian) + return Utils::readShort(strrev(fread($fp, 2))); + case self::TAG_INT: // Signed integer (32 bit, big endian) + return Utils::readInt(strrev(fread($fp, 4))); + case self::TAG_LONG: // Signed long (64 bit, big endian) + return Utils::readLong(strrev(fread($fp, 8))); + case self::TAG_FLOAT: // Floating point value (32 bit, big endian, IEEE 754-2008) + return Utils::readFloat(strrev(fread($fp, 4))); + case self::TAG_DOUBLE: // Double value (64 bit, big endian, IEEE 754-2008) + return Utils::readDouble(strrev(fread($fp, 8))); + case self::TAG_BYTE_ARRAY: // Byte array + $arrayLength = $this->readType($fp, self::TAG_INT); + $array = array(); + for($i = 0; $i < $arrayLength; $i++) $array[] = $this->readType($fp, self::TAG_BYTE); + return $array; + case self::TAG_STRING: // String + if(!$stringLength = $this->readType($fp, self::TAG_SHORT)) return ""; + $string = fread($fp, $stringLength); // Read in number of bytes specified by string length, and decode from utf8. + return $string; + case self::TAG_LIST: // List + $tagID = $this->readType($fp, self::TAG_BYTE); + $listLength = $this->readType($fp, self::TAG_INT); + $list = array("type"=>$tagID, "value"=>array()); + for($i = 0; $i < $listLength; $i++) { + if(feof($fp)) break; + $list["value"][] = $this->readType($fp, $tagID); + } + return $list; + case self::TAG_COMPOUND: // Compound + $tree = array(); + while($this->traverseTag($fp, $tree)); + return $tree; + } + } +} +?> \ No newline at end of file diff --git a/classes/PocketMinecraftServer.class.php b/classes/PocketMinecraftServer.class.php index 0e3c5b876..b7d4f722e 100644 --- a/classes/PocketMinecraftServer.class.php +++ b/classes/PocketMinecraftServer.class.php @@ -28,7 +28,7 @@ the Free Software Foundation, either version 3 of the License, or require_once("classes/Session.class.php"); class PocketMinecraftServer{ - var $seed, $protocol, $gamemode, $name, $maxClients, $clients, $eidCnt, $custom, $description, $motd, $timePerSecond, $responses, $spawn, $entities; + var $seed, $protocol, $gamemode, $name, $maxClients, $clients, $eidCnt, $custom, $description, $motd, $timePerSecond, $responses, $spawn, $entities, $mapDir, $mapParser, $map, $level, $tileEntities; private $database, $interface, $cnt, $events, $version, $serverType, $lastTick; function __construct($name, $gamemode = 1, $seed = false, $protocol = CURRENT_PROTOCOL, $port = 19132, $serverID = false, $version = CURRENT_VERSION){ $this->port = (int) $port; @@ -38,6 +38,12 @@ class PocketMinecraftServer{ $this->gamemode = (int) $gamemode; $this->version = (int) $version; $this->name = $name; + $this->mapDir = false; + $this->mapParser = false; + $this->map = false; + $this->level = false; + $this->tileEntities = array(); + $this->entities = array(); $this->custom = array(); $this->cnt = 1; $this->eidCnt = 1; @@ -61,11 +67,8 @@ class PocketMinecraftServer{ console("[INFO] Server Name: ".$this->name); console("[INFO] Server GUID: ".$this->serverID); console("[INFO] Protocol Version: ".$this->protocol); - console("[INFO] Seed: ".$this->seed); - console("[INFO] Gamemode: ".($this->gamemode === 0 ? "survival":"creative")); console("[INFO] Max Clients: ".$this->maxClients); $this->stop = false; - console("[INFO] Server started!"); } public function loadEvents(){ @@ -120,7 +123,7 @@ class PocketMinecraftServer{ console("[DEBUG] Memory usage: ".$info["memory_usage"]." (Peak ".$info["memory_peak_usage"]."), Entities: ".$info["entities"].", Events: ".$info["events"].", Actions: ".$info["actions"].", Garbage: ".$info["garbage"], true, true, 2); } return $info; - } + } public function close($reason = "stop"){ $this->chat(false, "Stopping server..."); @@ -157,12 +160,31 @@ class PocketMinecraftServer{ } } + private function loadMap(){ + $this->level = unserialize(file_get_contents($this->mapDir."level.dat")); + console("[INFO] Map: ".$this->level["LevelName"]); + $this->seed = $this->level["RandomSeed"]; + console("[INFO] Seed: ".$this->seed); + console("[INFO] Gamemode: ".($this->gamemode === 0 ? "survival":"creative")); + console("[DEBUG] Loading entities..."); + $entities = unserialize(file_get_contents($this->mapDir."entities.dat")); + foreach($entities as $entity){ + $this->entities[$this->eidCnt] = new Entity($this->eidCnt, ENTITY_MOB, $entity["id"], $this); + $this->entities[$this->eidCnt]->setPosition($entity["Pos"][0], $entity["Pos"][1], $entity["Pos"][2], $entity["Rotation"][0], $entity["Rotation"][1]); + $this->entities[$this->eidCnt]->setHealth($entity["Health"]); + ++$this->eidCnt; + } + console("[DEBUG] Loaded ".count($this->entities)." Entities", true, true, 2); + } + public function start(){ declare(ticks=15); register_tick_function(array($this, "tick")); $this->event("onTick", "tickerFunction", true); $this->event("onReceivedPacket", "packetHandler", true); register_shutdown_function(array($this, "close")); + $this->loadMap(); + console("[INFO] Server started!"); $this->process(); } diff --git a/classes/Utils.class.php b/classes/Utils.class.php index 0db81e1c5..7cb999ac5 100644 --- a/classes/Utils.class.php +++ b/classes/Utils.class.php @@ -34,7 +34,6 @@ if(!defined("HEX2BIN")){ define("BIG_ENDIAN", 0x00); define("LITTLE_ENDIAN", 0x01); define("ENDIANNESS", (pack("d", 1) === "\77\360\0\0\0\0\0\0" ? BIG_ENDIAN:LITTLE_ENDIAN)); -console("[DEBUG] Endianness: ".(ENDIANNESS === LITTLE_ENDIAN ? "Little Endian":"Big Endian"), true, true, 2); class Utils{ diff --git a/common/default.properties b/common/default.properties index 1da7adcbb..d9be679a5 100644 --- a/common/default.properties +++ b/common/default.properties @@ -3,14 +3,15 @@ server-name=PHP Server description= This is a Work in Progress custom server. motd=Welcome to PHP Server port=19132 -gamemode=1 protocol=CURRENT -seed=false -server-id=false -server-type=normal -max-players=20 -time-per-second=10 white-list=false debug=1 +max-players=20 +server-type=normal +time-per-second=10 +gamemode=1 +seed=false +level-name=false +server-id=false spawn=128.5;100;128.5 regenerate-config=true \ No newline at end of file diff --git a/common/dependencies.php b/common/dependencies.php index de718675e..6f092688c 100644 --- a/common/dependencies.php +++ b/common/dependencies.php @@ -71,6 +71,9 @@ require_once("classes/Utils.class.php"); require_once("classes/UDPSocket.class.php"); require_once("classes/Packet.class.php"); require_once("classes/Entity.class.php"); +require_once("classes/MapInterface.class.php"); +require_once("classes/ChunkParser.class.php"); +require_once("classes/NBT.class.php"); require_once("classes/SerializedPacketHandler.class.php"); require_once("classes/CustomPacketHandler.class.php"); require_once("classes/MinecraftInterface.class.php"); diff --git a/common/functions.php b/common/functions.php index 1d08c7b52..029ba2ddd 100644 --- a/common/functions.php +++ b/common/functions.php @@ -26,6 +26,31 @@ the Free Software Foundation, either version 3 of the License, or */ +function parseNBTData($data){ + $x = array(); + if(isset($data["value"])){ + return parseNBTData($data["value"]); + } + foreach($data as $d){ + if(!isset($d["value"]) and is_array($d) and count($d) == 1){ + return parseNBTData(array_pop($d)); + }elseif(!isset($d["value"]) and is_array($d)){ + $x[] = parseNBTData($d); + }elseif(is_array($d["value"]) and isset($d["name"])){ + $x[$d["name"]] = parseNBTData($d["value"]); + }elseif(is_array($d["value"]) and $d["type"] == 10){ + return parseNBTData($d["value"]); + }elseif($d["name"] != ""){ + $x[$d["name"]] = $d["value"]; + } + } + if(count($x) == 0){ + $x = $data; + } + return $x; +} + + function arg($name, $default){ global $arguments, $argv; if(!isset($arguments)){ diff --git a/data/How To Import Maps.txt b/data/How To Import Maps.txt new file mode 100644 index 000000000..1f6370c78 --- /dev/null +++ b/data/How To Import Maps.txt @@ -0,0 +1,2 @@ +To import a Pocket Edition Map, drop to the folder "maps" +the chunk.dat, level.dat and entities.dat from the savegame file. \ No newline at end of file diff --git a/server.php b/server.php index a7da922e3..756178e82 100644 --- a/server.php +++ b/server.php @@ -29,23 +29,25 @@ require_once("common/dependencies.php"); require_once("classes/PocketMinecraftServer.class.php"); file_put_contents("packets.log", ""); + if(!file_exists(FILE_PATH."white-list.txt")){ - console("[WARNING] No white-list.txt found, creating blank file"); + console("[NOTICE] No white-list.txt found, creating blank file"); file_put_contents(FILE_PATH."white-list.txt", ""); } if(!file_exists(FILE_PATH."banned-ips.txt")){ - console("[WARNING] No banned-ips.txt found, creating blank file"); + console("[NOTICE] No banned-ips.txt found, creating blank file"); file_put_contents(FILE_PATH."banned-ips.txt", ""); } if(!file_exists(FILE_PATH."server.properties")){ - console("[WARNING] No server.properties found, using default settings"); + console("[NOTICE] No server.properties found, using default settings"); copy(FILE_PATH."common/default.properties", FILE_PATH."server.properties"); } -@mkdir(FILE_PATH."data/entities/", 0777, true); -@mkdir(FILE_PATH."data/players/", 0777); +@mkdir(FILE_PATH."data/players/", 0777, true); +@mkdir(FILE_PATH."data/maps/", 0777); + $prop = file_get_contents(FILE_PATH."server.properties"); $prop = explode("\n", str_replace("\r", "", $prop)); @@ -75,6 +77,10 @@ foreach($prop as $line){ $v = trim($v); $v = $v == "false" ? false:(preg_match("/[^0-9\-]/", $v) > 0 ? Utils::readInt(substr(md5($v, true), 0, 4)):$v); break; + case "level-name": + $v = trim($v); + $v = $v == "false" ? false:$v; + break; case "spawn": $v = explode(";", $v); $v = array("x" => floatval($v[0]), "y" => floatval($v[1]), "z" => floatval($v[2])); @@ -89,6 +95,39 @@ foreach($prop as $line){ define("DEBUG", $config["debug"]); $server = new PocketMinecraftServer($config["server-name"], $config["gamemode"], $config["seed"], $config["protocol"], $config["port"], $config["server-id"]); + +if(file_exists(FILE_PATH."data/maps/level.dat")){ + console("[NOTICE] Detected unimported map data. Importing..."); + $nbt = new NBT(); + $level = parseNBTData($nbt->loadFile(FILE_PATH."data/maps/level.dat")); + console("[DEBUG] Importing map \"".$level["LevelName"]."\" gamemode ".$level["GameType"]." with seed ".$level["RandomSeed"], true, true, 2); + unset($level["Player"]); + $lvName = $level["LevelName"]."/"; + @mkdir(FILE_PATH."data/maps/".$lvName, 0777); + file_put_contents(FILE_PATH."data/maps/".$lvName."level.dat", serialize($level)); + $entities = parseNBTData($nbt->loadFile(FILE_PATH."data/maps/entities.dat")); + file_put_contents(FILE_PATH."data/maps/".$lvName."entities.dat", serialize($entities["Entities"])); + if(!isset($entities["TileEntities"])){ + $entities["TileEntities"] = array(); + } + file_put_contents(FILE_PATH."data/maps/".$lvName."tileEntities.dat", serialize($entities["TileEntities"])); + console("[DEBUG] Imported ".count($entities["Entities"])." Entities and ".count($entities["TileEntities"])." TileEntities", true, true, 2); + rename(FILE_PATH."data/maps/chunks.dat", FILE_PATH."data/maps/".$lvName."chunks.dat"); + unlink(FILE_PATH."data/maps/level.dat"); + @unlink(FILE_PATH."data/maps/level.dat_old"); + unlink(FILE_PATH."data/maps/entities.dat"); + if($config["level-name"] === false){ + console("[INFO] Setting default level to \"".$level["LevelName"]."\""); + $config["level-name"] = $level["LevelName"]; + $config["gamemode"] = $level["GameType"]; + $server->gamemode = $config["gamemode"]; + $server->seed = $level["RandomSeed"]; + $config["spawn"] = array("x" => $level["SpawnX"], "y" => $level["SpawnY"], "z" => $level["SpawnZ"]); + $config["regenerate-config"] = true; + } + console("[INFO] Map \"".$level["LevelName"]."\" importing done!"); + unset($level, $entities, $nbt); +} $server->setType($config["server-type"]); $server->timePerSecond = $config["time-per-second"]; $server->maxClients = $config["max-players"]; @@ -96,6 +135,7 @@ $server->description = $config["description"]; $server->motd = $config["motd"]; $server->spawn = $config["spawn"]; $server->whitelist = $config["white-list"]; +$server->mapDir = FILE_PATH."data/maps/".$config["level-name"]."/"; $server->reloadConfig(); if($config["regenerate-config"] == true){