diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 072747848..447280c61 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,8 +1,14 @@ ### Issue description + + ### Steps to reproduce the issue 1. ... diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php index 61ecff1fe..2851195f4 100644 --- a/src/pocketmine/Player.php +++ b/src/pocketmine/Player.php @@ -221,7 +221,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade public static function isValidUserName(string $name) : bool{ $lname = strtolower($name); $len = strlen($name); - return $lname !== "rcon" and $lname !== "console" and $len >= 1 and $len <= 16 and preg_match("[^A-Za-z0-9_]", $name) === 0; + return $lname !== "rcon" and $lname !== "console" and $len >= 1 and $len <= 16 and preg_match("/[^A-Za-z0-9_]/", $name) === 0; } /** @@ -616,16 +616,14 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade } public function sendCommandData(){ - $data = new \stdClass(); - $count = 0; + $data = []; foreach($this->server->getCommandMap()->getCommands() as $command){ - if(($cmdData = $command->generateCustomCommandData($this)) !== null){ - ++$count; - $data->{$command->getName()}->versions[0] = $cmdData; + if(count($cmdData = $command->generateCustomCommandData($this)) > 0){ + $data[$command->getName()]["versions"][0] = $cmdData; } } - if($count > 0){ + if(count($data) > 0){ //TODO: structure checking $pk = new AvailableCommandsPacket(); $pk->commands = json_encode($data); @@ -2145,7 +2143,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade if($this->canInteract($vector->add(0.5, 0.5, 0.5), $this->isCreative() ? 13 : 6) and $this->level->useBreakOn($vector, $item, $this, true)){ if($this->isSurvival()){ - if(!$item->deepEquals($oldItem) or $item->getCount() !== $oldItem->getCount()){ + if(!$item->equals($oldItem) or $item->getCount() !== $oldItem->getCount()){ $this->inventory->setItemInHand($item); $this->inventory->sendHeldItem($this->hasSpawned); } @@ -2420,14 +2418,14 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade if($this->level->useItemOn($blockVector, $item, $packet->face, $packet->fx, $packet->fy, $packet->fz, $this) === true){ return true; } - }elseif(!$this->inventory->getItemInHand()->deepEquals($packet->item)){ + }elseif(!$this->inventory->getItemInHand()->equals($packet->item)){ $this->inventory->sendHeldItem($this); }else{ $item = $this->inventory->getItemInHand(); $oldItem = clone $item; //TODO: Implement adventure mode checks if($this->level->useItemOn($blockVector, $item, $packet->face, $packet->fx, $packet->fy, $packet->fz, $this)){ - if(!$item->deepEquals($oldItem) or $item->getCount() !== $oldItem->getCount()){ + if(!$item->equals($oldItem) or $item->getCount() !== $oldItem->getCount()){ $this->inventory->setItemInHand($item); $this->inventory->sendHeldItem($this->hasSpawned); } @@ -2454,7 +2452,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade if($this->isCreative()){ $item = $this->inventory->getItemInHand(); - }elseif(!$this->inventory->getItemInHand()->deepEquals($packet->item)){ + }elseif(!$this->inventory->getItemInHand()->equals($packet->item)){ $this->inventory->sendHeldItem($this); return true; }else{ @@ -2870,7 +2868,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade break; } - if($transaction->getSourceItem()->deepEquals($transaction->getTargetItem()) and $transaction->getTargetItem()->getCount() === $transaction->getSourceItem()->getCount()){ //No changes! + if($transaction->getSourceItem()->equals($transaction->getTargetItem()) and $transaction->getTargetItem()->getCount() === $transaction->getSourceItem()->getCount()){ //No changes! //No changes, just a local inventory update sent by the client return true; } @@ -2949,7 +2947,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade $item = $packet->input[$y * 3 + $x]; $ingredient = $recipe->getIngredient($x, $y); if($item->getCount() > 0){ - if($ingredient === null or !$ingredient->deepEquals($item, !$ingredient->hasAnyDamageValue(), $ingredient->hasCompoundTag())){ + if($ingredient === null or !$ingredient->equals($item, !$ingredient->hasAnyDamageValue(), $ingredient->hasCompoundTag())){ $canCraft = false; break; } @@ -2965,7 +2963,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade $item = clone $packet->input[$y * 3 + $x]; foreach($needed as $k => $n){ - if($n->deepEquals($item, !$n->hasAnyDamageValue(), $n->hasCompoundTag())){ + if($n->equals($item, !$n->hasAnyDamageValue(), $n->hasCompoundTag())){ $remove = min($n->getCount(), $item->getCount()); $n->setCount($n->getCount() - $remove); $item->setCount($item->getCount() - $remove); @@ -2994,7 +2992,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade $ingredients = $packet->input; $result = $packet->output[0]; - if(!$canCraft or !$recipe->getResult()->deepEquals($result)){ + if(!$canCraft or !$recipe->getResult()->equals($result)){ $this->server->getLogger()->debug("Unmatched recipe " . $recipe->getId() . " from player " . $this->getName() . ": expected " . $recipe->getResult() . ", got " . $result . ", using: " . implode(", ", $ingredients)); $this->inventory->sendContents($this); return true; @@ -3280,7 +3278,7 @@ class Player extends Human implements CommandSender, InventoryHolder, ChunkLoade $pack = $manager->getPackById($packet->packId); if(!($pack instanceof ResourcePack)){ $this->close("", "disconnectionScreen.resourcePack", true); - $this->server->getLogger()->debug("Got a resource pack chunk request for unknown pack with UUID " . $uuid . ", available packs: " . implode(", ", $manager->getPackIdList())); + $this->server->getLogger()->debug("Got a resource pack chunk request for unknown pack with UUID " . $packet->packId . ", available packs: " . implode(", ", $manager->getPackIdList())); return true; } diff --git a/src/pocketmine/Server.php b/src/pocketmine/Server.php index e5be618de..3bf61a76f 100644 --- a/src/pocketmine/Server.php +++ b/src/pocketmine/Server.php @@ -1098,6 +1098,31 @@ class Server{ return true; } + /** + * Searches all levels for the entity with the specified ID. + * Useful for tracking entities across multiple worlds without needing strong references. + * + * @param int $entityId + * @param Level|null $expectedLevel Level to look in first for the target + * + * @return Entity|null + */ + public function findEntity(int $entityId, Level $expectedLevel = null){ + $levels = $this->levels; + if($expectedLevel !== null){ + array_unshift($levels, $expectedLevel); + } + + foreach($levels as $level){ + assert(!$level->isClosed()); + if(($entity = $level->getEntity($entityId)) instanceof Entity){ + return $entity; + } + } + + return null; + } + /** * @param string $variable * @param string $defaultValue diff --git a/src/pocketmine/command/Command.php b/src/pocketmine/command/Command.php index 859bbc823..07347c557 100644 --- a/src/pocketmine/command/Command.php +++ b/src/pocketmine/command/Command.php @@ -32,12 +32,12 @@ use pocketmine\Server; use pocketmine\utils\TextFormat; abstract class Command{ - /** @var \stdClass */ + /** @var array */ private static $defaultDataTemplate = null; /** @var string */ private $name; - /** @var \stdClass */ + /** @var array */ protected $commandData = null; /** @var string */ @@ -87,11 +87,11 @@ abstract class Command{ } /** - * Returns an \stdClass containing command data + * Returns an array containing command data * - * @return \stdClass + * @return array */ - public function getDefaultCommandData() : \stdClass{ + public function getDefaultCommandData() : array{ return $this->commandData; } @@ -101,25 +101,28 @@ abstract class Command{ * * @param Player $player * - * @return \stdClass|null + * @return array */ public function generateCustomCommandData(Player $player){ //TODO: fix command permission filtering on join /*if(!$this->testPermissionSilent($player)){ return null; }*/ - $customData = clone $this->commandData; - $customData->aliases = $this->getAliases(); - /*foreach($customData->overloads as &$overload){ - if(isset($overload->pocketminePermission) and !$player->hasPermission($overload->pocketminePermission)){ - unset($overload); + $customData = $this->commandData; + $customData["aliases"] = $this->getAliases(); + /*foreach($customData["overloads"] as $overloadName => $overload){ + if(isset($overload["pocketminePermission"]) and !$player->hasPermission($overload["pocketminePermission"])){ + unset($customData["overloads"][$overloadName]); } }*/ return $customData; } - public function getOverloads(): \stdClass{ - return $this->commandData->overloads; + /** + * @return array + */ + public function getOverloads(): array{ + return $this->commandData["overloads"]; } /** @@ -142,7 +145,7 @@ abstract class Command{ * @return string */ public function getPermission(){ - return $this->commandData->pocketminePermission ?? null; + return $this->commandData["pocketminePermission"] ?? null; } @@ -151,9 +154,9 @@ abstract class Command{ */ public function setPermission($permission){ if($permission !== null){ - $this->commandData->pocketminePermission = $permission; + $this->commandData["pocketminePermission"] = $permission; }else{ - unset($this->commandData->pocketminePermission); + unset($this->commandData["pocketminePermission"]); } } @@ -239,7 +242,7 @@ abstract class Command{ public function unregister(CommandMap $commandMap){ if($this->allowChangesFrom($commandMap)){ $this->commandMap = null; - $this->activeAliases = $this->commandData->aliases; + $this->activeAliases = $this->commandData["aliases"]; $this->label = $this->nextLabel; return true; @@ -282,7 +285,7 @@ abstract class Command{ * @return string */ public function getDescription(){ - return $this->commandData->description; + return $this->commandData["description"]; } /** @@ -296,7 +299,7 @@ abstract class Command{ * @param string[] $aliases */ public function setAliases(array $aliases){ - $this->commandData->aliases = $aliases; + $this->commandData["aliases"] = $aliases; if(!$this->isRegistered()){ $this->activeAliases = (array) $aliases; } @@ -306,7 +309,7 @@ abstract class Command{ * @param string $description */ public function setDescription($description){ - $this->commandData->description = $description; + $this->commandData["description"] = $description; } /** @@ -323,11 +326,14 @@ abstract class Command{ $this->usageMessage = $usage; } - public static final function generateDefaultData() : \stdClass{ + /** + * @return array + */ + public static final function generateDefaultData() : array{ if(self::$defaultDataTemplate === null){ - self::$defaultDataTemplate = json_decode(file_get_contents(Server::getInstance()->getFilePath() . "src/pocketmine/resources/command_default.json")); + self::$defaultDataTemplate = json_decode(file_get_contents(Server::getInstance()->getFilePath() . "src/pocketmine/resources/command_default.json"), true); } - return clone self::$defaultDataTemplate; + return self::$defaultDataTemplate; } /** diff --git a/src/pocketmine/command/SimpleCommandMap.php b/src/pocketmine/command/SimpleCommandMap.php index c1b54a2bd..48e30d1ac 100644 --- a/src/pocketmine/command/SimpleCommandMap.php +++ b/src/pocketmine/command/SimpleCommandMap.php @@ -177,15 +177,35 @@ class SimpleCommandMap implements CommandMap{ return true; } - public function dispatch(CommandSender $sender, $commandLine){ - $args = explode(" ", $commandLine); + /** + * Returns a command to match the specified command line, or null if no matching command was found. + * This method is intended to provide capability for handling commands with spaces in their name. + * The referenced parameters will be modified accordingly depending on the resulting matched command. + * + * @param string &$commandName + * @param string[] &$args + * + * @return Command|null + */ + public function matchCommand(string &$commandName, array &$args){ + $count = max(count($args), 255); - if(count($args) === 0){ - return false; + for($i = 0; $i < $count; ++$i){ + $commandName .= array_shift($args); + if(($command = $this->getCommand($commandName)) instanceof Command){ + return $command; + } + + $commandName .= " "; } - $sentCommandLabel = strtolower(array_shift($args)); - $target = $this->getCommand($sentCommandLabel); + return null; + } + + public function dispatch(CommandSender $sender, $commandLine){ + $args = explode(" ", $commandLine); + $sentCommandLabel = ""; + $target = $this->matchCommand($sentCommandLabel, $args); if($target === null){ return false; @@ -213,11 +233,7 @@ class SimpleCommandMap implements CommandMap{ } public function getCommand($name){ - if(isset($this->knownCommands[$name])){ - return $this->knownCommands[$name]; - } - - return null; + return $this->knownCommands[$name] ?? null; } /** @@ -235,7 +251,7 @@ class SimpleCommandMap implements CommandMap{ $values = $this->server->getCommandAliases(); foreach($values as $alias => $commandStrings){ - if(strpos($alias, ":") !== false or strpos($alias, " ") !== false){ + if(strpos($alias, ":") !== false){ $this->server->getLogger()->warning($this->server->getLanguage()->translateString("pocketmine.command.alias.illegal", [$alias])); continue; } @@ -243,20 +259,33 @@ class SimpleCommandMap implements CommandMap{ $targets = []; $bad = ""; + $recursive = ""; foreach($commandStrings as $commandString){ $args = explode(" ", $commandString); - $command = $this->getCommand($args[0]); + $commandName = ""; + $command = $this->matchCommand($commandName, $args); + if($command === null){ if(strlen($bad) > 0){ $bad .= ", "; } $bad .= $commandString; + }elseif($commandName === $alias){ + if($recursive !== ""){ + $recursive .= ", "; + } + $recursive .= $commandString; }else{ $targets[] = $commandString; } } + if($recursive !== ""){ + $this->server->getLogger()->warning($this->server->getLanguage()->translateString("pocketmine.command.alias.recursive", [$alias, $recursive])); + continue; + } + if(strlen($bad) > 0){ $this->server->getLogger()->warning($this->server->getLanguage()->translateString("pocketmine.command.alias.notFound", [$alias, $bad])); continue; diff --git a/src/pocketmine/entity/Entity.php b/src/pocketmine/entity/Entity.php index 3c09d906d..d51fa7770 100644 --- a/src/pocketmine/entity/Entity.php +++ b/src/pocketmine/entity/Entity.php @@ -1648,6 +1648,7 @@ abstract class Entity extends Location implements Metadatable{ } $this->namedtag = null; + $this->lastDamageCause = null; } } diff --git a/src/pocketmine/entity/Living.php b/src/pocketmine/entity/Living.php index b8510434f..f8933d39c 100644 --- a/src/pocketmine/entity/Living.php +++ b/src/pocketmine/entity/Living.php @@ -127,13 +127,15 @@ abstract class Living extends Entity implements Damageable{ $e = $source->getChild(); } - if($e->isOnFire() > 0){ - $this->setOnFire(2 * $this->server->getDifficulty()); - } + if($e !== null){ + if($e->isOnFire() > 0){ + $this->setOnFire(2 * $this->server->getDifficulty()); + } - $deltaX = $this->x - $e->x; - $deltaZ = $this->z - $e->z; - $this->knockBack($e, $damage, $deltaX, $deltaZ, $source->getKnockBack()); + $deltaX = $this->x - $e->x; + $deltaZ = $this->z - $e->z; + $this->knockBack($e, $damage, $deltaX, $deltaZ, $source->getKnockBack()); + } } $pk = new EntityEventPacket(); diff --git a/src/pocketmine/entity/Squid.php b/src/pocketmine/entity/Squid.php index 9ad26640b..becc8cfc2 100644 --- a/src/pocketmine/entity/Squid.php +++ b/src/pocketmine/entity/Squid.php @@ -60,7 +60,9 @@ class Squid extends WaterAnimal implements Ageable{ if($source instanceof EntityDamageByEntityEvent){ $this->swimSpeed = mt_rand(150, 350) / 2000; $e = $source->getDamager(); - $this->swimDirection = (new Vector3($this->x - $e->x, $this->y - $e->y, $this->z - $e->z))->normalize(); + if($e !== null){ + $this->swimDirection = (new Vector3($this->x - $e->x, $this->y - $e->y, $this->z - $e->z))->normalize(); + } $pk = new EntityEventPacket(); $pk->eid = $this->getId(); diff --git a/src/pocketmine/event/entity/EntityDamageByBlockEvent.php b/src/pocketmine/event/entity/EntityDamageByBlockEvent.php index bb4075d76..d2c246faa 100644 --- a/src/pocketmine/event/entity/EntityDamageByBlockEvent.php +++ b/src/pocketmine/event/entity/EntityDamageByBlockEvent.php @@ -24,6 +24,9 @@ namespace pocketmine\event\entity; use pocketmine\block\Block; use pocketmine\entity\Entity; +/** + * Called when an entity takes damage from a block. + */ class EntityDamageByBlockEvent extends EntityDamageEvent{ /** @var Block */ diff --git a/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php b/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php index cabeb091a..fad8aaef1 100644 --- a/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php +++ b/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php @@ -23,10 +23,13 @@ namespace pocketmine\event\entity; use pocketmine\entity\Entity; +/** + * Called when an entity takes damage from an entity sourced from another entity, for example being hit by a snowball thrown by a Player. + */ class EntityDamageByChildEntityEvent extends EntityDamageByEntityEvent{ - /** @var Entity */ - private $childEntity; + /** @var int */ + private $childEntityEid; /** @@ -37,15 +40,17 @@ class EntityDamageByChildEntityEvent extends EntityDamageByEntityEvent{ * @param int|int[] $damage */ public function __construct(Entity $damager, Entity $childEntity, Entity $entity, $cause, $damage){ - $this->childEntity = $childEntity; + $this->childEntityEid = $childEntity->getId(); parent::__construct($damager, $entity, $cause, $damage); } /** - * @return Entity + * Returns the entity which caused the damage, or null if the entity has been killed or closed. + * + * @return Entity|null */ public function getChild(){ - return $this->childEntity; + return $this->getEntity()->getLevel()->getServer()->findEntity($this->childEntityEid, $this->getEntity()->getLevel()); } diff --git a/src/pocketmine/event/entity/EntityDamageByEntityEvent.php b/src/pocketmine/event/entity/EntityDamageByEntityEvent.php index ac511a242..ea6228ae1 100644 --- a/src/pocketmine/event/entity/EntityDamageByEntityEvent.php +++ b/src/pocketmine/event/entity/EntityDamageByEntityEvent.php @@ -24,10 +24,13 @@ namespace pocketmine\event\entity; use pocketmine\entity\Effect; use pocketmine\entity\Entity; +/** + * Called when an entity takes damage from another entity. + */ class EntityDamageByEntityEvent extends EntityDamageEvent{ - /** @var Entity */ - private $damager; + /** @var int */ + private $damagerEid; /** @var float */ private $knockBack; @@ -39,7 +42,7 @@ class EntityDamageByEntityEvent extends EntityDamageEvent{ * @param float $knockBack */ public function __construct(Entity $damager, Entity $entity, $cause, $damage, $knockBack = 0.4){ - $this->damager = $damager; + $this->damagerEid = $damager->getId(); $this->knockBack = $knockBack; parent::__construct($entity, $cause, $damage); $this->addAttackerModifiers($damager); @@ -56,10 +59,12 @@ class EntityDamageByEntityEvent extends EntityDamageEvent{ } /** - * @return Entity + * Returns the attacking entity, or null if the attacker has been killed or closed. + * + * @return Entity|null */ public function getDamager(){ - return $this->damager; + return $this->getEntity()->getLevel()->getServer()->findEntity($this->damagerEid, $this->getEntity()->getLevel()); } /** diff --git a/src/pocketmine/event/entity/EntityDamageEvent.php b/src/pocketmine/event/entity/EntityDamageEvent.php index 0b681a7c2..57b77a629 100644 --- a/src/pocketmine/event/entity/EntityDamageEvent.php +++ b/src/pocketmine/event/entity/EntityDamageEvent.php @@ -25,6 +25,9 @@ use pocketmine\entity\Effect; use pocketmine\entity\Entity; use pocketmine\event\Cancellable; +/** + * Called when an entity takes damage. + */ class EntityDamageEvent extends EntityEvent implements Cancellable{ public static $handlerList = null; diff --git a/src/pocketmine/inventory/PlayerInventory.php b/src/pocketmine/inventory/PlayerInventory.php index f8c1cb055..ebc3f5862 100644 --- a/src/pocketmine/inventory/PlayerInventory.php +++ b/src/pocketmine/inventory/PlayerInventory.php @@ -94,8 +94,10 @@ class PlayerInventory extends BaseInventory{ } /** - * @internal This method is intended for use in network interaction with clients only. - * @deprecated Do not change hotbar slot mapping with plugins, this will cause myriad client-sided bugs, especially with desktop GUI clients. + * Links a hotbar slot to the specified slot in the main inventory. -1 links to no slot and will clear the hotbar slot. + * This method is intended for use in network interaction with clients only. + * + * NOTE: Do not change hotbar slot mapping with plugins, this will cause myriad client-sided bugs, especially with desktop GUI clients. * * @param int $hotbarSlot * @param int $inventorySlot diff --git a/src/pocketmine/inventory/SimpleTransactionGroup.php b/src/pocketmine/inventory/SimpleTransactionGroup.php index cd5c23d28..4e8811cf9 100644 --- a/src/pocketmine/inventory/SimpleTransactionGroup.php +++ b/src/pocketmine/inventory/SimpleTransactionGroup.php @@ -98,7 +98,7 @@ class SimpleTransactionGroup implements TransactionGroup{ } $checkSourceItem = $ts->getInventory()->getItem($ts->getSlot()); $sourceItem = $ts->getSourceItem(); - if(!$checkSourceItem->deepEquals($sourceItem) or $sourceItem->getCount() !== $checkSourceItem->getCount()){ + if(!$checkSourceItem->equals($sourceItem) or $sourceItem->getCount() !== $checkSourceItem->getCount()){ return false; } if($sourceItem->getId() !== Item::AIR){ @@ -108,7 +108,7 @@ class SimpleTransactionGroup implements TransactionGroup{ foreach($needItems as $i => $needItem){ foreach($haveItems as $j => $haveItem){ - if($needItem->deepEquals($haveItem)){ + if($needItem->equals($haveItem)){ $amount = min($needItem->getCount(), $haveItem->getCount()); $needItem->setCount($needItem->getCount() - $amount); $haveItem->setCount($haveItem->getCount() - $amount); diff --git a/src/pocketmine/item/Item.php b/src/pocketmine/item/Item.php index cdef7b45b..c5ef917a8 100644 --- a/src/pocketmine/item/Item.php +++ b/src/pocketmine/item/Item.php @@ -351,6 +351,12 @@ class Item implements ItemIds, \JsonSerializable{ } } + /** + * @param int $id + * @param int $meta + * @param int $count + * @param string $name + */ public function __construct(int $id, int $meta = 0, int $count = 1, string $name = "Unknown"){ $this->id = $id & 0xffff; $this->meta = $meta !== -1 ? $meta & 0xffff : -1; @@ -362,6 +368,13 @@ class Item implements ItemIds, \JsonSerializable{ } } + /** + * Sets the Item's NBT + * + * @param CompoundTag|string $tags + * + * @return $this + */ public function setCompoundTag($tags){ if($tags instanceof CompoundTag){ $this->setNamedTag($tags); @@ -374,16 +387,24 @@ class Item implements ItemIds, \JsonSerializable{ } /** + * Returns the serialized NBT of the Item * @return string */ public function getCompoundTag() : string{ return $this->tags; } + /** + * Returns whether this Item has a non-empty NBT. + * @return bool + */ public function hasCompoundTag() : bool{ return $this->tags !== ""; } + /** + * @return bool + */ public function hasCustomBlockData() : bool{ if(!$this->hasCompoundTag()){ return false; @@ -411,6 +432,11 @@ class Item implements ItemIds, \JsonSerializable{ return $this; } + /** + * @param CompoundTag $compound + * + * @return $this + */ public function setCustomBlockData(CompoundTag $compound){ $tags = clone $compound; $tags->setName("BlockEntityTag"); @@ -427,6 +453,9 @@ class Item implements ItemIds, \JsonSerializable{ return $this; } + /** + * @return CompoundTag|null + */ public function getCustomBlockData(){ if(!$this->hasCompoundTag()){ return null; @@ -440,6 +469,9 @@ class Item implements ItemIds, \JsonSerializable{ return null; } + /** + * @return bool + */ public function hasEnchantments() : bool{ if(!$this->hasCompoundTag()){ return false; @@ -457,7 +489,7 @@ class Item implements ItemIds, \JsonSerializable{ } /** - * @param $id + * @param int $id * * @return Enchantment|null */ @@ -534,6 +566,9 @@ class Item implements ItemIds, \JsonSerializable{ return $enchantments; } + /** + * @return bool + */ public function hasCustomName() : bool{ if(!$this->hasCompoundTag()){ return false; @@ -550,6 +585,9 @@ class Item implements ItemIds, \JsonSerializable{ return false; } + /** + * @return string + */ public function getCustomName() : string{ if(!$this->hasCompoundTag()){ return ""; @@ -566,6 +604,11 @@ class Item implements ItemIds, \JsonSerializable{ return ""; } + /** + * @param string $name + * + * @return $this + */ public function setCustomName(string $name){ if($name === ""){ $this->clearCustomName(); @@ -590,6 +633,9 @@ class Item implements ItemIds, \JsonSerializable{ return $this; } + /** + * @return $this + */ public function clearCustomName(){ if(!$this->hasCompoundTag()){ return $this; @@ -621,6 +667,10 @@ class Item implements ItemIds, \JsonSerializable{ return null; } + /** + * Returns a tree of Tag objects representing the Item's NBT + * @return null|CompoundTag + */ public function getNamedTag(){ if(!$this->hasCompoundTag()){ return null; @@ -630,6 +680,12 @@ class Item implements ItemIds, \JsonSerializable{ return $this->cachedNBT = self::parseCompoundTag($this->tags); } + /** + * Sets the Item's NBT from the supplied CompoundTag object. + * @param CompoundTag $tag + * + * @return $this + */ public function setNamedTag(CompoundTag $tag){ if($tag->getCount() === 0){ return $this->clearNamedTag(); @@ -641,37 +697,73 @@ class Item implements ItemIds, \JsonSerializable{ return $this; } + /** + * Removes the Item's NBT. + * @return Item + */ public function clearNamedTag(){ return $this->setCompoundTag(""); } + /** + * @return int + */ public function getCount() : int{ return $this->count; } + /** + * @param int $count + */ public function setCount(int $count){ $this->count = $count; } + /** + * Returns the name of the item, or the custom name if it is set. + * @return string + */ final public function getName() : string{ return $this->hasCustomName() ? $this->getCustomName() : $this->name; } + /** + * @return bool + */ final public function canBePlaced() : bool{ return $this->block !== null and $this->block->canBePlaced(); } + /** + * Returns whether an entity can eat or drink this item. + * @return bool + */ public function canBeConsumed() : bool{ return false; } + /** + * Returns whether this item can be consumed by the supplied Entity. + * @param Entity $entity + * + * @return bool + */ public function canBeConsumedBy(Entity $entity) : bool{ return $this->canBeConsumed(); } + /** + * Called when the item is consumed by an Entity. + * @param Entity $entity + */ public function onConsume(Entity $entity){ + } + /** + * Returns the block corresponding to this Item. + * @return Block + */ public function getBlock() : Block{ if($this->block instanceof Block){ return clone $this->block; @@ -680,22 +772,41 @@ class Item implements ItemIds, \JsonSerializable{ } } + /** + * @return int + */ final public function getId() : int{ return $this->id; } + /** + * @return int + */ final public function getDamage() : int{ return $this->meta; } + /** + * @param int $meta + */ public function setDamage(int $meta){ $this->meta = $meta !== -1 ? $meta & 0xFFFF : -1; } + /** + * Returns whether this item can match any item with an equivalent ID with any meta value. + * Used in crafting recipes which accept multiple variants of the same item, for example crafting tables recipes. + * + * @return bool + */ public function hasAnyDamageValue() : bool{ return $this->meta === -1; } + /** + * Returns the highest amount of this item which will fit into one inventory slot. + * @return int + */ public function getMaxStackSize(){ return 64; } @@ -762,28 +873,75 @@ class Item implements ItemIds, \JsonSerializable{ return 1; } + /** + * Called when a player uses this item on a block. + * + * @param Level $level + * @param Player $player + * @param Block $block + * @param Block $target + * @param int $face + * @param float $fx + * @param float $fy + * @param float $fz + * + * @return bool + */ public function onActivate(Level $level, Player $player, Block $block, Block $target, $face, $fx, $fy, $fz){ return false; } + /** + * Compares an Item to this Item and check if they match. + * + * @param Item $item + * @param bool $checkDamage Whether to verify that the damage values match. + * @param bool $checkCompound Whether to verify that the items' NBT match. + * + * @return bool + */ public final function equals(Item $item, bool $checkDamage = true, bool $checkCompound = true) : bool{ - return $this->id === $item->getId() and ($checkDamage === false or $this->getDamage() === $item->getDamage()) and ($checkCompound === false or $this->getCompoundTag() === $item->getCompoundTag()); - } - - public final function deepEquals(Item $item, bool $checkDamage = true, bool $checkCompound = true) : bool{ - if($this->equals($item, $checkDamage, $checkCompound)){ - return true; - }elseif($item->hasCompoundTag() and $this->hasCompoundTag()){ - return NBT::matchTree($this->getNamedTag(), $item->getNamedTag()); + if($this->id === $item->getId() and ($checkDamage === false or $this->getDamage() === $item->getDamage())){ + if($checkCompound){ + if($item->getCompoundTag() === $this->getCompoundTag()){ + return true; + }elseif($this->hasCompoundTag() and $item->hasCompoundTag()){ + //Serialized NBT didn't match, check the cached object tree. + return NBT::matchTree($this->getNamedTag(), $item->getNamedTag()); + } + }else{ + return true; + } } return false; } - final public function __toString() : string{ - return "Item " . $this->name . " (" . $this->id . ":" . ($this->meta === null ? "?" : $this->meta) . ")x" . $this->count . ($this->hasCompoundTag() ? " tags:0x" . bin2hex($this->getCompoundTag()) : ""); + /** + * @deprecated Use {@link Item#equals} instead, this method will be removed in the future. + * + * @param Item $item + * @param bool $checkDamage + * @param bool $checkCompound + * + * @return bool + */ + public final function deepEquals(Item $item, bool $checkDamage = true, bool $checkCompound = true) : bool{ + return $this->equals($item, $checkDamage, $checkCompound); } + /** + * @return string + */ + final public function __toString() : string{ + return "Item " . $this->name . " (" . $this->id . ":" . ($this->hasAnyDamageValue() ? "?" : $this->meta) . ")x" . $this->count . ($this->hasCompoundTag() ? " tags:0x" . bin2hex($this->getCompoundTag()) : ""); + } + + /** + * Returns an array of item stack properties that can be serialized to json. + * + * @return array + */ final public function jsonSerialize(){ return [ "id" => $this->id, diff --git a/src/pocketmine/lang/locale b/src/pocketmine/lang/locale index a1c4d8f11..b30ca5e3b 160000 --- a/src/pocketmine/lang/locale +++ b/src/pocketmine/lang/locale @@ -1 +1 @@ -Subproject commit a1c4d8f117f27b7da42a9db644e3c252c1fc033a +Subproject commit b30ca5e3bdb65c446e22bf777d1dcb04d78b6f7d diff --git a/src/pocketmine/resourcepacks/ResourcePack.php b/src/pocketmine/resourcepacks/ResourcePack.php index fd7962896..d4eeeb0e6 100644 --- a/src/pocketmine/resourcepacks/ResourcePack.php +++ b/src/pocketmine/resourcepacks/ResourcePack.php @@ -25,15 +25,46 @@ namespace pocketmine\resourcepacks; interface ResourcePack{ + /** + * Returns the human-readable name of the resource pack + * @return string + */ public function getPackName() : string; + /** + * Returns the pack's UUID as a human-readable string + * @return string + */ public function getPackId() : string; + /** + * Returns the size of the pack on disk in bytes. + * @return int + */ public function getPackSize() : int; + /** + * Returns a version number for the pack in the format major.minor.patch + * @return string + */ public function getPackVersion() : string; + /** + * Returns the raw SHA256 sum of the compressed resource pack zip. This is used by clients to validate pack downloads. + * @return string byte-array length 32 bytes + */ public function getSha256() : string; + /** + * Returns a chunk of the resource pack zip as a byte-array for sending to clients. + * + * Note that resource packs must **always** be in zip archive format for sending. + * A folder resource loader may need to perform on-the-fly compression for this purpose. + * + * @param int $start Offset to start reading the chunk from + * @param int $length Maximum length of data to return. + * + * @return string byte-array + */ public function getPackChunk(int $start, int $length) : string; } \ No newline at end of file diff --git a/src/pocketmine/resourcepacks/ResourcePackManager.php b/src/pocketmine/resourcepacks/ResourcePackManager.php index ed47ecb57..125c31ce0 100644 --- a/src/pocketmine/resourcepacks/ResourcePackManager.php +++ b/src/pocketmine/resourcepacks/ResourcePackManager.php @@ -46,6 +46,10 @@ class ResourcePackManager{ /** @var ResourcePack[] */ private $uuidList = []; + /** + * @param Server $server + * @param string $path Path to resource-packs directory. + */ public function __construct(Server $server, string $path){ $this->server = $server; $this->path = $path; @@ -103,6 +107,7 @@ class ResourcePackManager{ } /** + * Returns whether players must accept resource packs in order to join. * @return bool */ public function resourcePacksRequired() : bool{ @@ -110,6 +115,7 @@ class ResourcePackManager{ } /** + * Returns an array of resource packs in use, sorted in order of priority. * @return ResourcePack[] */ public function getResourceStack() : array{ @@ -117,8 +123,9 @@ class ResourcePackManager{ } /** - * @param string $id + * Returns the resource pack matching the specified UUID string, or null if the ID was not recognized. * + * @param string $id * @return ResourcePack|null */ public function getPackById(string $id){ @@ -126,6 +133,7 @@ class ResourcePackManager{ } /** + * Returns an array of pack IDs for packs currently in use. * @return string[] */ public function getPackIdList() : array{ diff --git a/src/pocketmine/resourcepacks/ZippedResourcePack.php b/src/pocketmine/resourcepacks/ZippedResourcePack.php index 40977d5d2..1f49bd8c9 100644 --- a/src/pocketmine/resourcepacks/ZippedResourcePack.php +++ b/src/pocketmine/resourcepacks/ZippedResourcePack.php @@ -25,13 +25,19 @@ namespace pocketmine\resourcepacks; class ZippedResourcePack implements ResourcePack{ + /** + * Performs basic validation checks on a resource pack's manifest.json. + * TODO: add more manifest validation + * + * @param \stdClass $manifest + * @return bool + */ public static function verifyManifest(\stdClass $manifest){ if(!isset($manifest->format_version) or !isset($manifest->header) or !isset($manifest->modules)){ return false; } //Right now we don't care about anything else, only the stuff we're sending to clients. - //TODO: add more manifest validation return isset($manifest->header->description) and isset($manifest->header->name) and @@ -49,7 +55,12 @@ class ZippedResourcePack implements ResourcePack{ /** @var string */ protected $sha256 = null; + /** @var resource */ + protected $fileResource; + /** + * @param string $zipPath Path to the resource pack zip + */ public function __construct(string $zipPath){ $this->path = $zipPath; @@ -74,6 +85,12 @@ class ZippedResourcePack implements ResourcePack{ } $this->manifest = $manifest; + + $this->fileResource = fopen($zipPath, "rb"); + } + + public function __destruct(){ + fclose($this->fileResource); } public function getPackName() : string{ @@ -100,6 +117,10 @@ class ZippedResourcePack implements ResourcePack{ } public function getPackChunk(int $start, int $length) : string{ - return substr(file_get_contents($this->path), $start, $length); + fseek($this->fileResource, $start); + if(feof($this->fileResource)){ + throw new \RuntimeException("Requested a resource pack chunk with invalid start offset"); + } + return fread($this->fileResource, $length); } } \ No newline at end of file diff --git a/src/pocketmine/tile/FlowerPot.php b/src/pocketmine/tile/FlowerPot.php index fa802bc61..a2c4e1611 100644 --- a/src/pocketmine/tile/FlowerPot.php +++ b/src/pocketmine/tile/FlowerPot.php @@ -40,7 +40,7 @@ class FlowerPot extends Spawnable{ parent::__construct($level, $nbt); } - public function canAddItem(Item $item): bool{ + public function canAddItem(Item $item) : bool{ if(!$this->isEmpty()){ return false; } @@ -63,7 +63,7 @@ class FlowerPot extends Spawnable{ } } - public function getItem(): Item{ + public function getItem() : Item{ return Item::get((int) ($this->namedtag["item"] ?? 0), (int) ($this->namedtag["mData"] ?? 0), 1); } @@ -77,11 +77,11 @@ class FlowerPot extends Spawnable{ $this->setItem(Item::get(Item::AIR)); } - public function isEmpty(): bool{ + public function isEmpty() : bool{ return $this->getItem()->getId() === Item::AIR; } - public function getSpawnCompound(): CompoundTag{ + public function getSpawnCompound() : CompoundTag{ return new CompoundTag("", [ new StringTag("id", Tile::FLOWER_POT), new IntTag("x", (int) $this->x), diff --git a/src/pocketmine/tile/Tile.php b/src/pocketmine/tile/Tile.php index 3c51ca6b5..51c3ca9d8 100644 --- a/src/pocketmine/tile/Tile.php +++ b/src/pocketmine/tile/Tile.php @@ -173,11 +173,14 @@ abstract class Tile extends Position{ unset($this->level->updateTiles[$this->id]); if($this->chunk instanceof Chunk){ $this->chunk->removeTile($this); + $this->chunk = null; } if(($level = $this->getLevel()) instanceof Level){ $level->removeTile($this); + $this->setLevel(null); } - $this->level = null; + + $this->namedtag = null; } } diff --git a/src/pocketmine/updater/AutoUpdater.php b/src/pocketmine/updater/AutoUpdater.php index ebf1dcb95..5d206290e 100644 --- a/src/pocketmine/updater/AutoUpdater.php +++ b/src/pocketmine/updater/AutoUpdater.php @@ -31,57 +31,60 @@ class AutoUpdater{ /** @var Server */ protected $server; + /** @var string */ protected $endpoint; + /** @var bool */ protected $hasUpdate = false; + /** @var array|null */ protected $updateInfo = null; + /** + * @param Server $server + * @param string $endpoint + */ public function __construct(Server $server, $endpoint){ $this->server = $server; $this->endpoint = "http://$endpoint/api/"; if($server->getProperty("auto-updater.enabled", true)){ - $this->check(); - if($this->hasUpdate()){ - if($this->server->getProperty("auto-updater.on-update.warn-console", true)){ - $this->showConsoleUpdate(); - } - }elseif($this->server->getProperty("auto-updater.preferred-channel", true)){ - $version = new VersionString(); - if(!$version->isDev() and $this->getChannel() !== "stable"){ - $this->showChannelSuggestionStable(); - }elseif($version->isDev() and $this->getChannel() === "stable"){ - $this->showChannelSuggestionBeta(); - } + $this->doCheck(); + } + } + + /** + * Callback used at the end of the update checking task + * + * @param array $updateInfo + */ + public function checkUpdateCallback(array $updateInfo){ + $this->updateInfo = $updateInfo; + $this->checkUpdate(); + if($this->hasUpdate()){ + if($this->server->getProperty("auto-updater.on-update.warn-console", true)){ + $this->showConsoleUpdate(); + } + }elseif($this->server->getProperty("auto-updater.preferred-channel", true)){ + $version = new VersionString(); + if(!$version->isDev() and $this->getChannel() !== "stable"){ + $this->showChannelSuggestionStable(); + }elseif($version->isDev() and $this->getChannel() === "stable"){ + $this->showChannelSuggestionBeta(); } } } - protected function check(){ - $response = Utils::getURL($this->endpoint . "?channel=" . $this->getChannel(), 4); - $response = json_decode($response, true); - if(!is_array($response)){ - return; - } - - $this->updateInfo = [ - "version" => $response["version"], - "api_version" => $response["api_version"], - "build" => $response["build"], - "date" => $response["date"], - "details_url" => $response["details_url"] ?? null, - "download_url" => $response["download_url"] - ]; - - $this->checkUpdate(); - } - /** + * Returns whether there is an update available. + * * @return bool */ public function hasUpdate(){ return $this->hasUpdate; } + /** + * Posts a warning to the console to tell the user there is an update available + */ public function showConsoleUpdate(){ $logger = $this->server->getLogger(); $newVersion = new VersionString($this->updateInfo["version"]); @@ -94,6 +97,10 @@ class AutoUpdater{ $logger->warning("----- -------------------------- -----"); } + /** + * Shows a warning to a player to tell them there is an update available + * @param Player $player + */ public function showPlayerUpdate(Player $player){ $player->sendMessage(TextFormat::DARK_PURPLE . "The version of PocketMine-MP that this server is running is out of date. Please consider updating to the latest version."); $player->sendMessage(TextFormat::DARK_PURPLE . "Check the console for more details."); @@ -115,14 +122,25 @@ class AutoUpdater{ $logger->info("----- -------------------------- -----"); } + /** + * Returns the last retrieved update data. + * + * @return array|null + */ public function getUpdateInfo(){ return $this->updateInfo; } + /** + * Schedules an AsyncTask to check for an update. + */ public function doCheck(){ - $this->check(); + $this->server->getScheduler()->scheduleAsyncTask(new UpdateCheckTask($this->endpoint, $this->getChannel())); } + /** + * Checks the update information against the current server version to decide if there's an update + */ protected function checkUpdate(){ if($this->updateInfo === null){ return; @@ -138,6 +156,11 @@ class AutoUpdater{ } + /** + * Returns the channel used for update checking (stable, beta, dev) + * + * @return string + */ public function getChannel(){ $channel = strtolower($this->server->getProperty("auto-updater.preferred-channel", "stable")); if($channel !== "stable" and $channel !== "beta" and $channel !== "development"){ @@ -146,4 +169,13 @@ class AutoUpdater{ return $channel; } + + /** + * Returns the host used for update checks. + * + * @return string + */ + public function getEndpoint() : string{ + return $this->endpoint; + } } \ No newline at end of file diff --git a/src/pocketmine/updater/UpdateCheckTask.php b/src/pocketmine/updater/UpdateCheckTask.php new file mode 100644 index 000000000..05923493b --- /dev/null +++ b/src/pocketmine/updater/UpdateCheckTask.php @@ -0,0 +1,82 @@ +endpoint = $endpoint; + $this->channel = $channel; + } + + public function onRun(){ + $this->error = ""; + $response = Utils::getURL($this->endpoint . "?channel=" . $this->channel, 4, [], $this->error); + if($this->error !== ""){ + return; + }else{ + $response = json_decode($response, true); + if(is_array($response)){ + $this->setResult( + [ + "version" => $response["version"], + "api_version" => $response["api_version"], + "build" => $response["build"], + "date" => $response["date"], + "details_url" => $response["details_url"] ?? null, + "download_url" => $response["download_url"] + ], + true + ); + }else{ + $this->error = "Invalid response data"; + } + } + } + + public function onCompletion(Server $server){ + if($this->error !== ""){ + $server->getLogger()->debug("[AutoUpdater] Async update check failed due to \"$this->error\""); + }else{ + $updateInfo = $this->getResult(); + if(is_array($updateInfo)){ + $server->getUpdater()->checkUpdateCallback($updateInfo); + }else{ + $server->getLogger()->debug("[AutoUpdater] Update info error"); + } + + } + } +} \ No newline at end of file diff --git a/src/pocketmine/utils/Utils.php b/src/pocketmine/utils/Utils.php index d64a42701..b8d56b8ea 100644 --- a/src/pocketmine/utils/Utils.php +++ b/src/pocketmine/utils/Utils.php @@ -348,14 +348,16 @@ class Utils{ /** * GETs an URL using cURL + * NOTE: This is a blocking operation and can take a significant amount of time. It is inadvisable to use this method on the main thread. * - * @param $page - * @param int $timeout default 10 - * @param array $extraHeaders + * @param $page + * @param int $timeout default 10 + * @param array $extraHeaders + * @param string &$err Will be set to the output of curl_error(). Use this to retrieve errors that occured during the operation. * - * @return bool|mixed + * @return bool|mixed false if an error occurred, mixed data if successful. */ - public static function getURL($page, $timeout = 10, array $extraHeaders = []){ + public static function getURL($page, $timeout = 10, array $extraHeaders = [], &$err = null){ if(Utils::$online === false){ return false; } @@ -372,6 +374,7 @@ class Utils{ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int) $timeout); curl_setopt($ch, CURLOPT_TIMEOUT, (int) $timeout); $ret = curl_exec($ch); + $err = curl_error($ch); curl_close($ch); return $ret; @@ -379,15 +382,17 @@ class Utils{ /** * POSTs data to an URL + * NOTE: This is a blocking operation and can take a significant amount of time. It is inadvisable to use this method on the main thread. * * @param $page * @param array|string $args * @param int $timeout * @param array $extraHeaders + * @param string &$err Will be set to the output of curl_error(). Use this to retrieve errors that occured during the operation. * - * @return bool|mixed + * @return bool|mixed false if an error occurred, mixed data if successful. */ - public static function postURL($page, $args, $timeout = 10, array $extraHeaders = []){ + public static function postURL($page, $args, $timeout = 10, array $extraHeaders = [], &$err = null){ if(Utils::$online === false){ return false; } @@ -406,6 +411,7 @@ class Utils{ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int) $timeout); curl_setopt($ch, CURLOPT_TIMEOUT, (int) $timeout); $ret = curl_exec($ch); + $err = curl_error($ch); curl_close($ch); return $ret;