diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php index cf1d5245b..1e1fb8226 100644 --- a/src/pocketmine/Player.php +++ b/src/pocketmine/Player.php @@ -89,6 +89,7 @@ use pocketmine\nbt\tag\Int; use pocketmine\nbt\tag\String; use pocketmine\network\protocol\AdventureSettingsPacket; use pocketmine\network\protocol\AnimatePacket; +use pocketmine\network\protocol\BatchPacket; use pocketmine\network\protocol\DataPacket; use pocketmine\network\protocol\DisconnectPacket; use pocketmine\network\protocol\EntityEventPacket; @@ -180,7 +181,6 @@ class Player extends Human implements CommandSender, InventoryHolder, IPlayer{ public $usedChunks = []; protected $loadQueue = []; - protected $chunkACK = []; protected $nextChunkOrderRun = 5; /** @var Player[] */ @@ -557,49 +557,24 @@ class Player extends Human implements CommandSender, InventoryHolder, IPlayer{ } } - /** - * @param int $identifier - * - * @return bool - */ - public function checkACK($identifier){ - return !isset($this->needACK[$identifier]); - } - - public function handleACK($identifier){ - unset($this->needACK[$identifier]); - if(isset($this->chunkACK[$identifier])){ - $index = $this->chunkACK[$identifier]; - unset($this->chunkACK[$identifier]); - if(isset($this->usedChunks[$index])){ - $this->usedChunks[$index] = true; - $X = null; - $Z = null; - Level::getXZ($index, $X, $Z); - - foreach($this->level->getChunkEntities($X, $Z) as $entity){ - if($entity !== $this and !$entity->closed and !$entity->dead){ - $entity->spawnTo($this); - } - } - } - } - } - public function sendChunk($x, $z, $payload){ if($this->connected === false){ return; } + $this->usedChunks[Level::chunkHash($x, $z)] = true; + $pk = new FullChunkDataPacket(); $pk->chunkX = $x; $pk->chunkZ = $z; $pk->data = $payload; - $cnt = $this->dataPacket($pk, true); - if($cnt === false or $cnt === true){ - return; + $this->dataPacket($pk); + + foreach($this->level->getChunkEntities($x, $z) as $entity){ + if($entity !== $this and !$entity->closed and !$entity->dead){ + $entity->spawnTo($this); + } } - $this->chunkACK[$cnt] = Level::chunkHash($x, $z); } protected function sendNextChunk(){ @@ -1391,6 +1366,12 @@ class Player extends Human implements CommandSender, InventoryHolder, IPlayer{ return; } + if($packet->pid() === ProtocolInfo::BATCH_PACKET){ + /** @var BatchPacket $packet */ + $this->server->getNetwork()->processBatch($packet, $this); + return; + } + $this->server->getPluginManager()->callEvent($ev = new DataPacketReceiveEvent($this, $packet)); if($ev->isCancelled()){ return; @@ -1408,10 +1389,8 @@ class Player extends Human implements CommandSender, InventoryHolder, IPlayer{ $this->iusername = strtolower($this->username); $this->loginData = ["clientId" => $packet->clientId, "loginData" => $packet->loginData]; - if(count($this->server->getOnlinePlayers()) > $this->server->getMaxPlayers()){ - if($this->kick("server full") === true){ - return; - } + if(count($this->server->getOnlinePlayers()) > $this->server->getMaxPlayers() and $this->kick("server full")){ + return; } if($packet->protocol1 !== ProtocolInfo::CURRENT_PROTOCOL){ if($packet->protocol1 < ProtocolInfo::CURRENT_PROTOCOL){ diff --git a/src/pocketmine/Server.php b/src/pocketmine/Server.php index 57fccd1b2..387fec66e 100644 --- a/src/pocketmine/Server.php +++ b/src/pocketmine/Server.php @@ -75,6 +75,9 @@ use pocketmine\nbt\tag\Int; use pocketmine\nbt\tag\Long; use pocketmine\nbt\tag\Short; use pocketmine\nbt\tag\String; +use pocketmine\network\CompressBatchedTask; +use pocketmine\network\Network; +use pocketmine\network\protocol\BatchPacket; use pocketmine\network\protocol\DataPacket; use pocketmine\network\query\QueryHandler; use pocketmine\network\RakLibInterface; @@ -191,10 +194,11 @@ class Server{ /** @var LevelMetadataStore */ private $levelMetadata; - /** @var SourceInterface[] */ - private $interfaces = []; - /** @var RakLibInterface */ - private $mainInterface; + /** @var Network */ + private $network; + + private $networkCompressionAsync = true; + private $networkCompressionLevel = 7; private $serverID; @@ -217,6 +221,8 @@ class Server{ /** @var Player[] */ private $players = []; + private $identifiers = []; + /** @var Level[] */ private $levels = []; @@ -595,67 +601,54 @@ class Server{ return round((array_sum($this->useAverage) / count($this->useAverage)) * 100, 2); } + /** + * @deprecated + * + * @param $address + * @param int $timeout + */ + public function blockAddress($address, $timeout = 300){ + $this->network->blockAddress($address, $timeout); + } + + /** + * @deprecated + * + * @param $address + * @param $port + * @param $payload + */ + public function sendPacket($address, $port, $payload){ + $this->network->sendPacket($address, $port, $payload); + } + + /** + * @deprecated + * * @return SourceInterface[] */ public function getInterfaces(){ - return $this->interfaces; + return $this->network->getInterfaces(); } /** + * @deprecated + * * @param SourceInterface $interface */ public function addInterface(SourceInterface $interface){ - $this->interfaces[spl_object_hash($interface)] = $interface; + $this->network->registerInterface($interface); } /** + * @deprecated + * * @param SourceInterface $interface */ public function removeInterface(SourceInterface $interface){ $interface->shutdown(); - unset($this->interfaces[spl_object_hash($interface)]); - } - - /** - * @param string $address - * @param int $port - * @param string $payload - */ - public function sendPacket($address, $port, $payload){ - $this->mainInterface->putRaw($address, $port, $payload); - } - - /** - * Blocks an IP address from the main interface. Setting timeout to -1 will block it forever - * - * @param string $address - * @param int $timeout - */ - public function blockAddress($address, $timeout = 300){ - $this->mainInterface->blockAddress($address, $timeout); - } - - /** - * @param string $address - * @param int $port - * @param string $payload - */ - public function handlePacket($address, $port, $payload){ - try{ - if(strlen($payload) > 2 and substr($payload, 0, 2) === "\xfe\xfd" and $this->queryHandler instanceof QueryHandler){ - $this->queryHandler->handle($address, $port, $payload); - } - }catch(\Exception $e){ - if(\pocketmine\DEBUG > 1){ - if($this->logger instanceof MainLogger){ - $this->logger->logException($e); - } - } - - $this->blockAddress($address, 600); - } - //TODO: add raw packet events + $this->network->unregisterInterface($interface); } /** @@ -893,6 +886,7 @@ class Server{ foreach($this->players as $identifier => $p){ if($player === $p){ unset($this->players[$identifier]); + unset($this->identifiers[spl_object_hash($player)]); break; } } @@ -1529,6 +1523,14 @@ class Server{ ServerScheduler::$WORKERS = $this->getProperty("settings.async-workers", ServerScheduler::$WORKERS); + if($this->getProperty("network.batch-threshold", 256) >= 0){ + Network::$BATCH_THRESHOLD = (int) $this->getProperty("network.batch-threshold", 256); + }else{ + Network::$BATCH_THRESHOLD = -1; + } + $this->networkCompressionLevel = $this->getProperty("network.compression-level", 7); + $this->networkCompressionAsync = $this->getProperty("network.async-compression", true); + $this->scheduler = new ServerScheduler(); if($this->getConfigBoolean("enable-rcon", false) === true){ @@ -1588,7 +1590,10 @@ class Server{ define("BOOTUP_RANDOM", @Utils::getRandomBytes(16)); $this->serverID = Binary::readLong(substr(Utils::getUniqueID(true, $this->getIp() . $this->getPort()), 0, 8)); - $this->addInterface($this->mainInterface = new RakLibInterface($this)); + $this->network = new Network($this); + $this->network->setName($this->getMotd()); + $this->network->registerInterface(new RakLibInterface($this)); + $this->logger->info("This server is running " . $this->getName() . " version " . ($version->isDev() ? TextFormat::YELLOW : "") . $version->get(true) . TextFormat::WHITE . " \"" . $this->getCodename() . "\" (API " . $this->getApiVersion() . ")"); $this->logger->info($this->getName() . " is distributed under the LGPL License"); @@ -1751,6 +1756,11 @@ class Server{ public static function broadcastPacket(array $players, DataPacket $packet){ $packet->encode(); $packet->isEncoded = true; + if(Network::$BATCH_THRESHOLD >= 0 and strlen($packet->buffer) >= Network::$BATCH_THRESHOLD){ + Server::getInstance()->batchPackets($players, [$packet->buffer]); + return; + } + foreach($players as $player){ $player->dataPacket($packet); } @@ -1759,6 +1769,53 @@ class Server{ } } + /** + * Broadcasts a list of packets in a batch to a list of players + * + * @param Player[] $players + * @param DataPacket[]|string $packets + */ + public function batchPackets(array $players, array $packets){ + $str = ""; + + foreach($packets as $p){ + if(is_object($p)){ + $p->encode(); + $str .= $p->buffer; + }else{ + $str .= $p; + } + } + + $targets = []; + foreach($players as $p){ + $targets[] = $this->identifiers[spl_object_hash($p)]; + } + + if($this->networkCompressionAsync){ + $task = new CompressBatchedTask(); + $task->targets = $targets; + $task->data = $str; + $task->level = $this->networkCompressionLevel; + $this->getScheduler()->scheduleAsyncTask($task); + }else{ + $this->broadcastPacketsCallback(zlib_encode($str, ZLIB_ENCODING_DEFLATE, $this->networkCompressionLevel), $targets); + } + } + + public function broadcastPacketsCallback($data, array $identifiers){ + $pk = new BatchPacket(); + $pk->payload = $data; + $pk->encode(); + $pk->isEncoded = true; + + foreach($identifiers as $i){ + if(isset($this->players[$i])){ + $this->players[$i]->dataPacket($pk); + } + } + } + /** * @param int $type @@ -1902,7 +1959,7 @@ class Server{ $this->rcon->stop(); } - if($this->getProperty("settings.upnp-forwarding", false) === true){ + if($this->getProperty("network.upnp-forwarding", false) === true){ $this->logger->info("[UPnP] Removing port forward..."); UPnP::RemovePortForward($this->getPort()); } @@ -1930,8 +1987,9 @@ class Server{ $this->console->kill(); - foreach($this->interfaces as $interface){ + foreach($this->network->getInterfaces() as $interface){ $interface->shutdown(); + $this->network->unregisterInterface($interface); } }catch(\Exception $e){ $this->logger->emergency("Crashed while crashing, killing process"); @@ -1960,7 +2018,7 @@ class Server{ } - if($this->getProperty("settings.upnp-forwarding", false) == true){ + if($this->getProperty("network.upnp-forwarding", false) == true){ $this->logger->info("[UPnP] Trying to port forward..."); UPnP::PortForward($this->getPort()); } @@ -2112,6 +2170,7 @@ class Server{ public function addPlayer($identifier, Player $player){ $this->players[$identifier] = $player; + $this->identifiers[spl_object_hash($player)] = $identifier; } private function checkTickUpdates($currentTick){ @@ -2137,6 +2196,7 @@ class Server{ $player->save(); }elseif(!$player->isConnected()){ unset($this->players[$index]); + unset($this->identifiers[spl_object_hash($player)]); } } @@ -2178,7 +2238,7 @@ class Server{ "version" => $version->get(true), "build" => $version->getBuild(), "mc_version" => \pocketmine\MINECRAFT_VERSION, - "protocol" => network\protocol\Info::CURRENT_PROTOCOL, + "protocol" => \pocketmine\network\protocol\Info::CURRENT_PROTOCOL, "online" => count($this->players), "max" => $this->getMaxPlayers(), "plugins" => $plist, @@ -2188,7 +2248,7 @@ class Server{ } public function getNetwork(){ - return $this->mainInterface; + return $this->network; } private function titleTick(){ @@ -2203,10 +2263,12 @@ class Server{ $this->getPocketMineVersion() . " | Online " . count($this->players) . "/" . $this->getMaxPlayers() . " | Memory " . $usage . - " | U " . round($this->mainInterface->getUploadUsage() / 1024, 2) . - " D " . round($this->mainInterface->getDownloadUsage() / 1024, 2) . + " | U " . round($this->network->getUpload() / 1024, 2) . + " D " . round($this->network->getDownload() / 1024, 2) . " kB/s | TPS " . $this->getTicksPerSecond() . " | Load " . $this->getTickUsage() . "%\x07"; + + $this->network->resetStatistics(); } public function getMemoryUsage($advanced = false){ @@ -2250,6 +2312,31 @@ class Server{ } + /** + * @param string $address + * @param int $port + * @param string $payload + * + * TODO: move this to Network + */ + public function handlePacket($address, $port, $payload){ + try{ + if(strlen($payload) > 2 and substr($payload, 0, 2) === "\xfe\xfd" and $this->queryHandler instanceof QueryHandler){ + $this->queryHandler->handle($address, $port, $payload); + } + }catch(\Exception $e){ + if(\pocketmine\DEBUG > 1){ + if($this->logger instanceof MainLogger){ + $this->logger->logException($e); + } + } + + $this->blockAddress($address, 600); + } + //TODO: add raw packet events + } + + /** * Tries to execute a server tick */ @@ -2266,9 +2353,7 @@ class Server{ $this->checkConsole(); Timings::$connectionTimer->startTiming(); - foreach($this->interfaces as $interface){ - $interface->process(); - } + $this->network->processInterfaces(); Timings::$connectionTimer->stopTiming(); Timings::$schedulerTimer->startTiming(); diff --git a/src/pocketmine/block/Door.php b/src/pocketmine/block/Door.php index 67d6ab388..d1ef50ac8 100644 --- a/src/pocketmine/block/Door.php +++ b/src/pocketmine/block/Door.php @@ -23,6 +23,7 @@ namespace pocketmine\block; use pocketmine\item\Item; use pocketmine\level\Level; +use pocketmine\level\sound\DoorSound; use pocketmine\math\AxisAlignedBB; use pocketmine\network\protocol\LevelEventPacket; use pocketmine\Player; @@ -275,14 +276,8 @@ abstract class Door extends Transparent{ if($player instanceof Player){ unset($players[$player->getId()]); } - $pk = new LevelEventPacket(); - $pk->x = $this->x; - $pk->y = $this->y; - $pk->z = $this->z; - $pk->evid = 1003; - $pk->data = 0; - Server::broadcastPacket($players, $pk); + $this->level->addSound(new DoorSound($this)); return true; } @@ -294,13 +289,7 @@ abstract class Door extends Transparent{ if($player instanceof Player){ unset($players[$player->getId()]); } - $pk = new LevelEventPacket(); - $pk->x = $this->x; - $pk->y = $this->y; - $pk->z = $this->z; - $pk->evid = 1003; - $pk->data = 0; - Server::broadcastPacket($players, $pk); + $this->level->addSound(new DoorSound($this)); } return true; diff --git a/src/pocketmine/level/Level.php b/src/pocketmine/level/Level.php index 13ae68f96..cd48b94c5 100644 --- a/src/pocketmine/level/Level.php +++ b/src/pocketmine/level/Level.php @@ -380,9 +380,7 @@ class Level implements ChunkManager, Metadatable{ if(!is_array($pk)){ Server::broadcastPacket($players, $pk); }else{ - foreach($pk as $p){ - Server::broadcastPacket($players, $p); - } + $this->server->batchPackets($players, $pk); } } } @@ -398,9 +396,7 @@ class Level implements ChunkManager, Metadatable{ if(!is_array($pk)){ Server::broadcastPacket($players, $pk); }else{ - foreach($pk as $p){ - Server::broadcastPacket($players, $p); - } + $this->server->batchPackets($players, $pk); } } } diff --git a/src/pocketmine/level/format/anvil/ChunkRequestTask.php b/src/pocketmine/level/format/anvil/ChunkRequestTask.php index d609e9648..1145a1db2 100644 --- a/src/pocketmine/level/format/anvil/ChunkRequestTask.php +++ b/src/pocketmine/level/format/anvil/ChunkRequestTask.php @@ -34,7 +34,6 @@ class ChunkRequestTask extends AsyncTask{ protected $levelId; protected $chunkX; protected $chunkZ; - protected $compressionLevel; /** @var \pocketmine\level\format\ChunkSection[] */ protected $sections; @@ -69,8 +68,6 @@ class ChunkRequestTask extends AsyncTask{ $this->tiles = $tiles; - $this->compressionLevel = Level::$COMPRESSION_LEVEL; - } public function onRun(){ @@ -102,9 +99,9 @@ class ChunkRequestTask extends AsyncTask{ $biomeColors = pack("N*", ...$this->biomeColors); - $ordered = zlib_encode($orderedIds . $orderedData . $orderedSkyLight . $orderedLight . $this->biomeIds . $biomeColors . $this->tiles, ZLIB_ENCODING_DEFLATE, $this->compressionLevel); + $ordered = $orderedIds . $orderedData . $orderedSkyLight . $orderedLight . $this->biomeIds . $biomeColors . $this->tiles; - $this->setResult($ordered); + $this->setResult($ordered, false); } public function getColumn(&$data, $x, $z){ diff --git a/src/pocketmine/level/format/leveldb/LevelDB.php b/src/pocketmine/level/format/leveldb/LevelDB.php index 243d26b9f..dac41d6fd 100644 --- a/src/pocketmine/level/format/leveldb/LevelDB.php +++ b/src/pocketmine/level/format/leveldb/LevelDB.php @@ -148,15 +148,13 @@ class LevelDB extends BaseLevelProvider{ $biomeColors = pack("N*", ...$chunk->getBiomeColorArray()); - $ordered = zlib_encode( - $chunk->getBlockIdArray() . + $ordered = $chunk->getBlockIdArray() . $chunk->getBlockDataArray() . $chunk->getBlockSkyLightArray() . $chunk->getBlockLightArray() . $chunk->getBiomeIdArray() . $biomeColors . - $tiles - , ZLIB_ENCODING_DEFLATE, Level::$COMPRESSION_LEVEL); + $tiles; $this->getLevel()->chunkRequestCallback($x, $z, $ordered); diff --git a/src/pocketmine/level/format/mcregion/McRegion.php b/src/pocketmine/level/format/mcregion/McRegion.php index d198bbee9..3bee7fa63 100644 --- a/src/pocketmine/level/format/mcregion/McRegion.php +++ b/src/pocketmine/level/format/mcregion/McRegion.php @@ -129,15 +129,13 @@ class McRegion extends BaseLevelProvider{ $biomeColors = pack("N*", ...$chunk->getBiomeColorArray()); - $ordered = zlib_encode( - $chunk->getBlockIdArray() . + $ordered = $chunk->getBlockIdArray() . $chunk->getBlockDataArray() . $chunk->getBlockSkyLightArray() . $chunk->getBlockLightArray() . $chunk->getBiomeIdArray() . $biomeColors . - $tiles - , ZLIB_ENCODING_DEFLATE, Level::$COMPRESSION_LEVEL); + $tiles; $this->getLevel()->chunkRequestCallback($x, $z, $ordered); diff --git a/src/pocketmine/network/AdvancedSourceInterface.php b/src/pocketmine/network/AdvancedSourceInterface.php new file mode 100644 index 000000000..7e49b54cd --- /dev/null +++ b/src/pocketmine/network/AdvancedSourceInterface.php @@ -0,0 +1,47 @@ +final = zlib_encode($this->data, ZLIB_ENCODING_DEFLATE, $this->level); + }catch(\Exception $e){ + + } + } + + public function onCompletion(Server $server){ + $server->broadcastPacketsCallback($this->final, $this->targets); + } +} \ No newline at end of file diff --git a/src/pocketmine/network/Network.php b/src/pocketmine/network/Network.php new file mode 100644 index 000000000..f04a0b46d --- /dev/null +++ b/src/pocketmine/network/Network.php @@ -0,0 +1,283 @@ +registerPackets(); + + $this->server = $server; + + } + + public function addStatistics($upload, $download){ + $this->upload += $upload; + $this->download += $download; + } + + public function getUpload(){ + return $this->upload; + } + + public function getDownload(){ + return $this->download; + } + + public function resetStatistics(){ + $this->upload = 0; + $this->download = 0; + } + + /** + * @return SourceInterface[] + */ + public function getInterfaces(){ + return $this->interfaces; + } + + public function processInterfaces(){ + foreach($this->interfaces as $interface){ + $interface->process(); + } + } + + /** + * @param SourceInterface $interface + */ + public function registerInterface(SourceInterface $interface){ + $this->interfaces[$hash = spl_object_hash($interface)] = $interface; + if($interface instanceof AdvancedSourceInterface){ + $this->advancedInterfaces[$hash] = $interface; + $interface->setNetwork($this); + } + $interface->setName($this->name); + } + + /** + * @param SourceInterface $interface + */ + public function unregisterInterface(SourceInterface $interface){ + unset($this->interfaces[$hash = spl_object_hash($interface)], + $this->advancedInterfaces[$hash]); + } + + /** + * Sets the server name shown on each interface Query + * + * @param string $name + */ + public function setName($name){ + $this->name = (string) $name; + foreach($this->interfaces as $interface){ + $interface->setName($this->name); + } + } + + public function getName(){ + return $this->name; + } + + /** + * @param int $id 0-255 + * @param DataPacket $class + */ + public function registerPacket($id, $class){ + $this->packetPool[$id] = new $class; + } + + public function getServer(){ + return $this->server; + } + + public function processBatch(BatchPacket $packet, Player $p){ + $str = zlib_decode($packet->payload, 1024 * 1024 * 64); //Max 64MB + $len = strlen($str); + $offset = 0; + while($offset < $len){ + if(($packetId = $this->getPacket(ord($str{$offset++}))) !== null){ + $packetId->buffer = $str; + $packet->decode(); + $p->handleDataPacket($packet); + } + } + } + + /** + * @param $id + * + * @return DataPacket + */ + public function getPacket($id){ + /** @var DataPacket $class */ + $class = $this->packetPool[$id]; + if($class !== null){ + return clone $class; + } + return null; + } + + + /** + * @param string $address + * @param int $port + * @param string $payload + */ + public function sendPacket($address, $port, $payload){ + foreach($this->advancedInterfaces as $interface){ + $interface->sendRawPacket($address, $port, $payload); + } + } + + /** + * Blocks an IP address from the main interface. Setting timeout to -1 will block it forever + * + * @param string $address + * @param int $timeout + */ + public function blockAddress($address, $timeout = 300){ + foreach($this->advancedInterfaces as $interface){ + $interface->blockAddress($address, $timeout); + } + } + + private function registerPackets(){ + $this->packetPool = new \SplFixedArray(256); + + $this->registerPacket(ProtocolInfo::LOGIN_PACKET, LoginPacket::class); + $this->registerPacket(ProtocolInfo::PLAY_STATUS_PACKET, PlayStatusPacket::class); + $this->registerPacket(ProtocolInfo::DISCONNECT_PACKET, DisconnectPacket::class); + $this->registerPacket(ProtocolInfo::TEXT_PACKET, TextPacket::class); + $this->registerPacket(ProtocolInfo::SET_TIME_PACKET, SetTimePacket::class); + $this->registerPacket(ProtocolInfo::START_GAME_PACKET, StartGamePacket::class); + $this->registerPacket(ProtocolInfo::ADD_MOB_PACKET, AddMobPacket::class); + $this->registerPacket(ProtocolInfo::ADD_PLAYER_PACKET, AddPlayerPacket::class); + $this->registerPacket(ProtocolInfo::REMOVE_PLAYER_PACKET, RemovePlayerPacket::class); + $this->registerPacket(ProtocolInfo::ADD_ENTITY_PACKET, AddEntityPacket::class); + $this->registerPacket(ProtocolInfo::REMOVE_ENTITY_PACKET, RemoveEntityPacket::class); + $this->registerPacket(ProtocolInfo::ADD_ITEM_ENTITY_PACKET, AddItemEntityPacket::class); + $this->registerPacket(ProtocolInfo::TAKE_ITEM_ENTITY_PACKET, TakeItemEntityPacket::class); + $this->registerPacket(ProtocolInfo::MOVE_ENTITY_PACKET, MoveEntityPacket::class); + $this->registerPacket(ProtocolInfo::MOVE_PLAYER_PACKET, MovePlayerPacket::class); + $this->registerPacket(ProtocolInfo::REMOVE_BLOCK_PACKET, RemoveBlockPacket::class); + $this->registerPacket(ProtocolInfo::UPDATE_BLOCK_PACKET, UpdateBlockPacket::class); + $this->registerPacket(ProtocolInfo::ADD_PAINTING_PACKET, AddPaintingPacket::class); + $this->registerPacket(ProtocolInfo::EXPLODE_PACKET, ExplodePacket::class); + $this->registerPacket(ProtocolInfo::LEVEL_EVENT_PACKET, LevelEventPacket::class); + $this->registerPacket(ProtocolInfo::TILE_EVENT_PACKET, TileEventPacket::class); + $this->registerPacket(ProtocolInfo::ENTITY_EVENT_PACKET, EntityEventPacket::class); + $this->registerPacket(ProtocolInfo::PLAYER_EQUIPMENT_PACKET, PlayerEquipmentPacket::class); + $this->registerPacket(ProtocolInfo::PLAYER_ARMOR_EQUIPMENT_PACKET, PlayerArmorEquipmentPacket::class); + $this->registerPacket(ProtocolInfo::INTERACT_PACKET, InteractPacket::class); + $this->registerPacket(ProtocolInfo::USE_ITEM_PACKET, UseItemPacket::class); + $this->registerPacket(ProtocolInfo::PLAYER_ACTION_PACKET, PlayerActionPacket::class); + $this->registerPacket(ProtocolInfo::HURT_ARMOR_PACKET, HurtArmorPacket::class); + $this->registerPacket(ProtocolInfo::SET_ENTITY_DATA_PACKET, SetEntityDataPacket::class); + $this->registerPacket(ProtocolInfo::SET_ENTITY_MOTION_PACKET, SetEntityMotionPacket::class); + $this->registerPacket(ProtocolInfo::SET_HEALTH_PACKET, SetHealthPacket::class); + $this->registerPacket(ProtocolInfo::SET_SPAWN_POSITION_PACKET, SetSpawnPositionPacket::class); + $this->registerPacket(ProtocolInfo::ANIMATE_PACKET, AnimatePacket::class); + $this->registerPacket(ProtocolInfo::RESPAWN_PACKET, RespawnPacket::class); + $this->registerPacket(ProtocolInfo::DROP_ITEM_PACKET, DropItemPacket::class); + $this->registerPacket(ProtocolInfo::CONTAINER_OPEN_PACKET, ContainerOpenPacket::class); + $this->registerPacket(ProtocolInfo::CONTAINER_CLOSE_PACKET, ContainerClosePacket::class); + $this->registerPacket(ProtocolInfo::CONTAINER_SET_SLOT_PACKET, ContainerSetSlotPacket::class); + $this->registerPacket(ProtocolInfo::CONTAINER_SET_DATA_PACKET, ContainerSetDataPacket::class); + $this->registerPacket(ProtocolInfo::CONTAINER_SET_CONTENT_PACKET, ContainerSetContentPacket::class); + $this->registerPacket(ProtocolInfo::ADVENTURE_SETTINGS_PACKET, AdventureSettingsPacket::class); + $this->registerPacket(ProtocolInfo::TILE_ENTITY_DATA_PACKET, TileEntityDataPacket::class); + $this->registerPacket(ProtocolInfo::SET_DIFFICULTY_PACKET, SetDifficultyPacket::class); + $this->registerPacket(ProtocolInfo::BATCH_PACKET, BatchPacket::class); + } +} diff --git a/src/pocketmine/network/RakLibInterface.php b/src/pocketmine/network/RakLibInterface.php index 1813ef3b3..99354219a 100644 --- a/src/pocketmine/network/RakLibInterface.php +++ b/src/pocketmine/network/RakLibInterface.php @@ -19,9 +19,6 @@ * */ -/** - * Network-related classes - */ namespace pocketmine\network; use pocketmine\event\player\PlayerCreationEvent; @@ -32,6 +29,7 @@ use pocketmine\network\protocol\AddPaintingPacket; use pocketmine\network\protocol\AddPlayerPacket; use pocketmine\network\protocol\AdventureSettingsPacket; use pocketmine\network\protocol\AnimatePacket; +use pocketmine\network\protocol\BatchPacket; use pocketmine\network\protocol\ContainerClosePacket; use pocketmine\network\protocol\ContainerOpenPacket; use pocketmine\network\protocol\ContainerSetContentPacket; @@ -81,7 +79,7 @@ use raklib\server\RakLibServer; use raklib\server\ServerHandler; use raklib\server\ServerInstance; -class RakLibInterface implements ServerInstance, SourceInterface{ +class RakLibInterface implements ServerInstance, AdvancedSourceInterface{ /** @var \SplFixedArray */ private $packetPool; @@ -89,6 +87,9 @@ class RakLibInterface implements ServerInstance, SourceInterface{ /** @var Server */ private $server; + /** @var Network */ + private $network; + /** @var RakLibServer */ private $rakLib; @@ -104,31 +105,42 @@ class RakLibInterface implements ServerInstance, SourceInterface{ /** @var ServerHandler */ private $interface; - private $upload = 0; - private $download = 0; + /** @var string[][] */ + private $batchedPackets = []; public function __construct(Server $server){ - $this->registerPackets(); - $this->server = $server; $this->identifiers = new \SplObjectStorage(); $this->rakLib = new RakLibServer($this->server->getLogger(), $this->server->getLoader(), $this->server->getPort(), $this->server->getIp() === "" ? "0.0.0.0" : $this->server->getIp()); $this->interface = new ServerHandler($this->rakLib, $this); - $this->setName($this->server->getMotd()); + } + + public function setNetwork(Network $network){ + $this->network = $network; } public function doTick(){ if(!$this->rakLib->isTerminated()){ + $this->sendBatchedPackets(); $this->interface->sendTick(); }else{ $info = $this->rakLib->getTerminationInfo(); - $this->server->removeInterface($this); + $this->network->unregisterInterface($this); \ExceptionHandler::handler(E_ERROR, "RakLib Thread crashed [".$info["scope"]."]: " . (isset($info["message"]) ? $info["message"] : ""), $info["file"], $info["line"]); } } + private function sendBatchedPackets(){ + foreach($this->batchedPackets as $i => $p){ + if($this->batchedPackets[$i] !== ""){ + $this->server->batchPackets([$this->players[$i]], [$p]); + $this->batchedPackets[$i] = ""; + } + } + } + public function process(){ $work = false; if($this->interface->handlePacket()){ @@ -147,6 +159,7 @@ class RakLibInterface implements ServerInstance, SourceInterface{ $player = $this->players[$identifier]; $this->identifiers->detach($player); unset($this->players[$identifier]); + unset($this->batchedPackets[$identifier]); unset($this->identifiersACK[$identifier]); $player->close(TextFormat::YELLOW . $player->getName() . " has left the game", $reason); } @@ -155,6 +168,7 @@ class RakLibInterface implements ServerInstance, SourceInterface{ public function close(Player $player, $reason = "unknown reason"){ if(isset($this->identifiers[$player])){ unset($this->players[$this->identifiers[$player]]); + unset($this->batchedPackets[$identifier]); unset($this->identifiersACK[$this->identifiers[$player]]); $this->interface->closeSession($this->identifiers[$player], $reason); $this->identifiers->detach($player); @@ -177,6 +191,7 @@ class RakLibInterface implements ServerInstance, SourceInterface{ $player = new $class($this, $ev->getClientId(), $ev->getAddress(), $ev->getPort()); $this->players[$identifier] = $player; $this->identifiersACK[$identifier] = 0; + $this->batchedPackets[$identifier] = ""; $this->identifiers->attach($player, $identifier); $this->server->addPlayer($identifier, $player); } @@ -209,7 +224,7 @@ class RakLibInterface implements ServerInstance, SourceInterface{ $this->server->handlePacket($address, $port, $payload); } - public function putRaw($address, $port, $payload){ + public function sendRawPacket($address, $port, $payload){ $this->interface->sendRaw($address, $port, $payload); } @@ -230,19 +245,10 @@ class RakLibInterface implements ServerInstance, SourceInterface{ public function handleOption($name, $value){ if($name === "bandwidth"){ $v = unserialize($value); - $this->upload = $v["up"]; - $this->download = $v["down"]; + $this->network->addStatistics($v["up"], $v["down"]); } } - public function getUploadUsage(){ - return $this->upload; - } - - public function getDownloadUsage(){ - return $this->download; - } - public function putPacket(Player $player, DataPacket $packet, $needACK = false, $immediate = false){ if(isset($this->identifiers[$player])){ $identifier = $this->identifiers[$player]; @@ -259,6 +265,13 @@ class RakLibInterface implements ServerInstance, SourceInterface{ $pk = $packet->__encapsulatedPacket; } + if(!$needACK and $packet->pid() !== ProtocolInfo::BATCH_PACKET + and Network::$BATCH_THRESHOLD >= 0 + and strlen($packet->buffer) >= Network::$BATCH_THRESHOLD){ + $this->batchedPackets[$this->identifiers[$player]] .= $packet->buffer; + return; + } + if($pk === null){ $pk = new EncapsulatedPacket(); $pk->buffer = $packet->buffer; @@ -276,76 +289,10 @@ class RakLibInterface implements ServerInstance, SourceInterface{ return null; } - public function registerPacket($id, $class){ - $this->packetPool[$id] = $class; - } - - /** - * @param $id - * - * @return DataPacket - */ - public function getPacketFromPool($id){ - /** @var DataPacket $class */ - $class = $this->packetPool[$id]; - if($class !== null){ - return new $class; - } - return null; - } - - private function registerPackets(){ - $this->packetPool = new \SplFixedArray(256); - - $this->registerPacket(ProtocolInfo::LOGIN_PACKET, LoginPacket::class); - $this->registerPacket(ProtocolInfo::PLAY_STATUS_PACKET, PlayStatusPacket::class); - $this->registerPacket(ProtocolInfo::DISCONNECT_PACKET, DisconnectPacket::class); - $this->registerPacket(ProtocolInfo::TEXT_PACKET, TextPacket::class); - $this->registerPacket(ProtocolInfo::SET_TIME_PACKET, SetTimePacket::class); - $this->registerPacket(ProtocolInfo::START_GAME_PACKET, StartGamePacket::class); - $this->registerPacket(ProtocolInfo::ADD_MOB_PACKET, AddMobPacket::class); - $this->registerPacket(ProtocolInfo::ADD_PLAYER_PACKET, AddPlayerPacket::class); - $this->registerPacket(ProtocolInfo::REMOVE_PLAYER_PACKET, RemovePlayerPacket::class); - $this->registerPacket(ProtocolInfo::ADD_ENTITY_PACKET, AddEntityPacket::class); - $this->registerPacket(ProtocolInfo::REMOVE_ENTITY_PACKET, RemoveEntityPacket::class); - $this->registerPacket(ProtocolInfo::ADD_ITEM_ENTITY_PACKET, AddItemEntityPacket::class); - $this->registerPacket(ProtocolInfo::TAKE_ITEM_ENTITY_PACKET, TakeItemEntityPacket::class); - $this->registerPacket(ProtocolInfo::MOVE_ENTITY_PACKET, MoveEntityPacket::class); - $this->registerPacket(ProtocolInfo::MOVE_PLAYER_PACKET, MovePlayerPacket::class); - $this->registerPacket(ProtocolInfo::REMOVE_BLOCK_PACKET, RemoveBlockPacket::class); - $this->registerPacket(ProtocolInfo::UPDATE_BLOCK_PACKET, UpdateBlockPacket::class); - $this->registerPacket(ProtocolInfo::ADD_PAINTING_PACKET, AddPaintingPacket::class); - $this->registerPacket(ProtocolInfo::EXPLODE_PACKET, ExplodePacket::class); - $this->registerPacket(ProtocolInfo::LEVEL_EVENT_PACKET, LevelEventPacket::class); - $this->registerPacket(ProtocolInfo::TILE_EVENT_PACKET, TileEventPacket::class); - $this->registerPacket(ProtocolInfo::ENTITY_EVENT_PACKET, EntityEventPacket::class); - $this->registerPacket(ProtocolInfo::PLAYER_EQUIPMENT_PACKET, PlayerEquipmentPacket::class); - $this->registerPacket(ProtocolInfo::PLAYER_ARMOR_EQUIPMENT_PACKET, PlayerArmorEquipmentPacket::class); - $this->registerPacket(ProtocolInfo::INTERACT_PACKET, InteractPacket::class); - $this->registerPacket(ProtocolInfo::USE_ITEM_PACKET, UseItemPacket::class); - $this->registerPacket(ProtocolInfo::PLAYER_ACTION_PACKET, PlayerActionPacket::class); - $this->registerPacket(ProtocolInfo::HURT_ARMOR_PACKET, HurtArmorPacket::class); - $this->registerPacket(ProtocolInfo::SET_ENTITY_DATA_PACKET, SetEntityDataPacket::class); - $this->registerPacket(ProtocolInfo::SET_ENTITY_MOTION_PACKET, SetEntityMotionPacket::class); - $this->registerPacket(ProtocolInfo::SET_HEALTH_PACKET, SetHealthPacket::class); - $this->registerPacket(ProtocolInfo::SET_SPAWN_POSITION_PACKET, SetSpawnPositionPacket::class); - $this->registerPacket(ProtocolInfo::ANIMATE_PACKET, AnimatePacket::class); - $this->registerPacket(ProtocolInfo::RESPAWN_PACKET, RespawnPacket::class); - $this->registerPacket(ProtocolInfo::DROP_ITEM_PACKET, DropItemPacket::class); - $this->registerPacket(ProtocolInfo::CONTAINER_OPEN_PACKET, ContainerOpenPacket::class); - $this->registerPacket(ProtocolInfo::CONTAINER_CLOSE_PACKET, ContainerClosePacket::class); - $this->registerPacket(ProtocolInfo::CONTAINER_SET_SLOT_PACKET, ContainerSetSlotPacket::class); - $this->registerPacket(ProtocolInfo::CONTAINER_SET_DATA_PACKET, ContainerSetDataPacket::class); - $this->registerPacket(ProtocolInfo::CONTAINER_SET_CONTENT_PACKET, ContainerSetContentPacket::class); - $this->registerPacket(ProtocolInfo::ADVENTURE_SETTINGS_PACKET, AdventureSettingsPacket::class); - $this->registerPacket(ProtocolInfo::TILE_ENTITY_DATA_PACKET, TileEntityDataPacket::class); - $this->registerPacket(ProtocolInfo::SET_DIFFICULTY_PACKET, SetDifficultyPacket::class); - } - private function getPacket($buffer){ $pid = ord($buffer{0}); - if(($data = $this->getPacketFromPool($pid)) === null){ + if(($data = $this->network->getPacket($pid)) === null){ $data = new UnknownPacket(); $data->packetID = $pid; } diff --git a/src/pocketmine/network/protocol/BatchPacket.php b/src/pocketmine/network/protocol/BatchPacket.php new file mode 100644 index 000000000..513837878 --- /dev/null +++ b/src/pocketmine/network/protocol/BatchPacket.php @@ -0,0 +1,48 @@ + + + +class BatchPacket extends DataPacket{ + public static $pool = []; + public static $next = 0; + + public $payload; + + public function pid(){ + return Info::BATCH_PACKET; + } + + public function decode(){ + $size = $this->getInt(); + $this->payload = $this->get($size); + } + + public function encode(){ + $this->reset(); + $this->putInt(strlen($this->payload)); + $this->put($this->payload); + } + +} \ No newline at end of file diff --git a/src/pocketmine/network/protocol/FullChunkDataPacket.php b/src/pocketmine/network/protocol/FullChunkDataPacket.php index 685ac20f7..5b70e43e0 100644 --- a/src/pocketmine/network/protocol/FullChunkDataPacket.php +++ b/src/pocketmine/network/protocol/FullChunkDataPacket.php @@ -44,6 +44,7 @@ class FullChunkDataPacket extends DataPacket{ $this->reset(); $this->putInt($this->chunkX); $this->putInt($this->chunkZ); + $this->putInt(strlen($this->data)); $this->put($this->data); } diff --git a/src/pocketmine/network/protocol/Info.php b/src/pocketmine/network/protocol/Info.php index b8ca5024d..db45b7c56 100644 --- a/src/pocketmine/network/protocol/Info.php +++ b/src/pocketmine/network/protocol/Info.php @@ -91,5 +91,6 @@ interface Info{ //const PLAYER_INPUT_PACKET = 0xaf; const FULL_CHUNK_DATA_PACKET = 0xb0; const SET_DIFFICULTY_PACKET = 0xb1; + const BATCH_PACKET = 0xb2; } diff --git a/src/pocketmine/resources/pocketmine.yml b/src/pocketmine/resources/pocketmine.yml index 65c7f852f..c03810d32 100644 --- a/src/pocketmine/resources/pocketmine.yml +++ b/src/pocketmine/resources/pocketmine.yml @@ -1,6 +1,7 @@ # Main configuration file for PocketMine-MP # These settings are the ones that cannot be included in server.properties # Some of these settings are safe, others can break your server if modified incorrectly +# New settings/defaults won't appear automatically on this file when upgrading. settings: shutdown-message: "Server closed" @@ -10,13 +11,22 @@ settings: deprecated-verbose: true #Enable plugin and core profiling by default enable-profiling: false - advanced-cache: false - upnp-forwarding: false #Sends anonymous statistics to create usage reports send-usage: true - #Number of AsyncTask workers - #WARNING: This will increase global memory usage, but it won't be listed in the total. - async-workers: 1 + #Number of AsyncTask workers. + #Used for plugin asynchronous tasks, compression and web communication. + async-workers: 2 + +network: + #Threshold for batching packets, in bytes. Only these packets will be compressed + #Set to 0 to compress everything, -1 to disable. + batch-threshold: 256 + #Compression level used when sending batched packets. Higher = more CPU, less bandwidth usage + compression-level: 7 + #Use AsyncTasks for compression. Adds half/one tick delay, less CPU load on main thread + async-compression: true + #Experimental, only for Windows. Tries to use UPnP to automatically port forward + upnp-forwarding: false debug: #If > 1, it will show debug messages in the console @@ -33,17 +43,15 @@ level-settings: chunk-sending: #Amount of chunks sent to players per tick - per-tick: 4 - #Compression level used when sending chunks. Higher = more CPU, less bandwidth usage - compression-level: 7 + per-tick: 8 #Amount of chunks sent around each player max-chunks: 256 chunk-ticking: #Max amount of chunks processed each tick - per-tick: 80 + per-tick: 24 #Radius of chunks around a player to tick - tick-radius: 4 + tick-radius: 3 #NOTE: This is currently not implemented light-updates: false clear-tick-list: false @@ -54,7 +62,7 @@ chunk-generation: #Using this with fast generators is recommended use-async: true #Max. amount of chunks to generate per tick, only for use-async: false - per-tick: 1 + per-tick: 3 #Max. amount of chunks to populate per tick populations-per-tick: 1 @@ -78,7 +86,7 @@ auto-report: enabled: true send-code: true send-settings: true - send-phpinfo: true + send-phpinfo: false host: crash.pocketmine.net auto-updater: diff --git a/src/pocketmine/scheduler/AsyncTask.php b/src/pocketmine/scheduler/AsyncTask.php index c363b712a..5297395c3 100644 --- a/src/pocketmine/scheduler/AsyncTask.php +++ b/src/pocketmine/scheduler/AsyncTask.php @@ -31,6 +31,7 @@ use pocketmine\Server; abstract class AsyncTask extends \Collectable{ private $result = null; + private $serialized = false; /** @var int */ private $taskId = null; @@ -55,7 +56,7 @@ abstract class AsyncTask extends \Collectable{ * @return mixed */ public function getResult(){ - return unserialize($this->result); + return $this->serialized ? unserialize($this->result) : $this->result; } /** @@ -67,9 +68,11 @@ abstract class AsyncTask extends \Collectable{ /** * @param mixed $result + * @param bool $serialize */ - public function setResult($result){ - $this->result = serialize($result); + public function setResult($result, $serialize = true){ + $this->result = $serialize ? serialize($result) : $result; + $this->serialized = $serialize; } public function setTaskId($taskId){