diff --git a/.github/workflows/team-pr-auto-approve.yml b/.github/workflows/team-pr-auto-approve.yml index ad2812383..8f40b16e3 100644 --- a/.github/workflows/team-pr-auto-approve.yml +++ b/.github/workflows/team-pr-auto-approve.yml @@ -14,8 +14,8 @@ on: - ready_for_review jobs: - approve: - name: Auto approve + dispatch: + name: Request approval runs-on: ubuntu-latest steps: diff --git a/src/entity/Entity.php b/src/entity/Entity.php index b52d0e49b..e10f82668 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -35,6 +35,7 @@ use pocketmine\event\entity\EntityMotionEvent; use pocketmine\event\entity\EntityRegainHealthEvent; use pocketmine\event\entity\EntitySpawnEvent; use pocketmine\event\entity\EntityTeleportEvent; +use pocketmine\item\Item; use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; use pocketmine\math\Vector2; @@ -1560,6 +1561,13 @@ abstract class Entity{ $this->hasSpawned = []; } + /** + * Returns the item that players will equip when middle-clicking on this entity. + */ + public function getPickedItem() : ?Item{ + return null; + } + /** * Flags the entity to be removed from the world on the next tick. */ diff --git a/src/entity/Squid.php b/src/entity/Squid.php index bc47925f3..7d6d9bbb3 100644 --- a/src/entity/Squid.php +++ b/src/entity/Squid.php @@ -26,6 +26,7 @@ namespace pocketmine\entity; use pocketmine\entity\animation\SquidInkCloudAnimation; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; +use pocketmine\item\Item; use pocketmine\item\VanillaItems; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; @@ -124,4 +125,8 @@ class Squid extends WaterAnimal{ VanillaItems::INK_SAC()->setCount(mt_rand(1, 3)) ]; } + + public function getPickedItem() : ?Item{ + return VanillaItems::SQUID_SPAWN_EGG(); + } } diff --git a/src/entity/Villager.php b/src/entity/Villager.php index caca9a148..cbb06d1e6 100644 --- a/src/entity/Villager.php +++ b/src/entity/Villager.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace pocketmine\entity; +use pocketmine\item\Item; +use pocketmine\item\VanillaItems; use pocketmine\nbt\tag\CompoundTag; use pocketmine\network\mcpe\protocol\types\entity\EntityIds; use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection; @@ -87,6 +89,10 @@ class Villager extends Living implements Ageable{ return $this->baby; } + public function getPickedItem() : ?Item{ + return VanillaItems::VILLAGER_SPAWN_EGG(); + } + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ parent::syncNetworkData($properties); $properties->setGenericFlag(EntityMetadataFlags::BABY, $this->baby); diff --git a/src/entity/Zombie.php b/src/entity/Zombie.php index 16ca4c0b0..e8ee1c448 100644 --- a/src/entity/Zombie.php +++ b/src/entity/Zombie.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\entity; +use pocketmine\item\Item; use pocketmine\item\VanillaItems; use pocketmine\network\mcpe\protocol\types\entity\EntityIds; use function mt_rand; @@ -65,4 +66,8 @@ class Zombie extends Living{ //TODO: check for equipment and whether it's a baby return 5; } + + public function getPickedItem() : ?Item{ + return VanillaItems::ZOMBIE_SPAWN_EGG(); + } } diff --git a/src/entity/object/FallingBlock.php b/src/entity/object/FallingBlock.php index f1cc37332..5285ebbbc 100644 --- a/src/entity/object/FallingBlock.php +++ b/src/entity/object/FallingBlock.php @@ -35,6 +35,7 @@ use pocketmine\entity\Location; use pocketmine\event\entity\EntityBlockChangeEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; +use pocketmine\item\Item; use pocketmine\math\Vector3; use pocketmine\nbt\tag\ByteTag; use pocketmine\nbt\tag\CompoundTag; @@ -194,6 +195,10 @@ class FallingBlock extends Entity{ return $nbt; } + public function getPickedItem() : ?Item{ + return $this->block->asItem(); + } + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ parent::syncNetworkData($properties); diff --git a/src/entity/object/Painting.php b/src/entity/object/Painting.php index 5b5217e62..78a006d77 100644 --- a/src/entity/object/Painting.php +++ b/src/entity/object/Painting.php @@ -28,6 +28,7 @@ use pocketmine\entity\Entity; use pocketmine\entity\EntitySizeInfo; use pocketmine\entity\Location; use pocketmine\event\entity\EntityDamageByEntityEvent; +use pocketmine\item\Item; use pocketmine\item\VanillaItems; use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; @@ -165,6 +166,10 @@ class Painting extends Entity{ )); } + public function getPickedItem() : ?Item{ + return VanillaItems::PAINTING(); + } + /** * Returns the painting motive (which image is displayed on the painting) */ diff --git a/src/entity/object/PrimedTNT.php b/src/entity/object/PrimedTNT.php index d08b9f1bb..29cbf59b9 100644 --- a/src/entity/object/PrimedTNT.php +++ b/src/entity/object/PrimedTNT.php @@ -23,11 +23,13 @@ declare(strict_types=1); namespace pocketmine\entity\object; +use pocketmine\block\VanillaBlocks; use pocketmine\entity\Entity; use pocketmine\entity\EntitySizeInfo; use pocketmine\entity\Explosive; use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\entity\EntityPreExplodeEvent; +use pocketmine\item\Item; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; use pocketmine\network\mcpe\protocol\types\entity\EntityIds; @@ -127,6 +129,10 @@ class PrimedTNT extends Entity implements Explosive{ } } + public function getPickedItem() : ?Item{ + return VanillaBlocks::TNT()->setWorksUnderwater($this->worksUnderwater)->asItem(); + } + protected function syncNetworkData(EntityMetadataCollection $properties) : void{ parent::syncNetworkData($properties); diff --git a/src/event/player/PlayerEntityPickEvent.php b/src/event/player/PlayerEntityPickEvent.php new file mode 100644 index 000000000..3c742d6c4 --- /dev/null +++ b/src/event/player/PlayerEntityPickEvent.php @@ -0,0 +1,53 @@ +player = $player; + } + + public function getEntity() : Entity{ + return $this->entityClicked; + } + + public function getResultItem() : Item{ + return $this->resultItem; + } +} diff --git a/src/event/player/PlayerInteractEvent.php b/src/event/player/PlayerInteractEvent.php index 6119d5e99..46daf7081 100644 --- a/src/event/player/PlayerInteractEvent.php +++ b/src/event/player/PlayerInteractEvent.php @@ -42,6 +42,9 @@ class PlayerInteractEvent extends PlayerEvent implements Cancellable{ protected Vector3 $touchVector; + protected bool $useItem = true; + protected bool $useBlock = true; + public function __construct( Player $player, protected Item $item, @@ -73,4 +76,28 @@ class PlayerInteractEvent extends PlayerEvent implements Cancellable{ public function getFace() : int{ return $this->blockFace; } + + /** + * Returns whether the item may react to the interaction. If disabled, items such as spawn eggs will not activate. + * This does NOT prevent blocks from being placed - it makes the item behave as if the player is sneaking. + */ + public function useItem() : bool{ return $this->useItem; } + + /** + * Sets whether the used item may react to the interaction. If false, items such as spawn eggs will not activate. + * This does NOT prevent blocks from being placed - it makes the item behave as if the player is sneaking. + */ + public function setUseItem(bool $useItem) : void{ $this->useItem = $useItem; } + + /** + * Returns whether the block may react to the interaction. If false, doors, fence gates and trapdoors will not + * respond, containers will not open, etc. + */ + public function useBlock() : bool{ return $this->useBlock; } + + /** + * Sets whether the block may react to the interaction. If false, doors, fence gates and trapdoors will not + * respond, containers will not open, etc. + */ + public function setUseBlock(bool $useBlock) : void{ $this->useBlock = $useBlock; } } diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index c92db3133..e74eb87c6 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -493,15 +493,18 @@ class InGamePacketHandler extends PacketHandler{ $blockPos = $data->getBlockPosition(); $vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ()); - if(!$this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos)){ - $this->onFailedBlockAction($vBlockPos, $data->getFace()); - } + $this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos); + //always sync this in case plugins caused a different result than the client expected + //we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating + //more information up the stack. For now I think this is good enough. + //if only the client would tell us what blocks it thinks changed... + $this->syncBlocksNearby($vBlockPos, $data->getFace()); return true; case UseItemTransactionData::ACTION_BREAK_BLOCK: $blockPos = $data->getBlockPosition(); $vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ()); if(!$this->player->breakBlock($vBlockPos)){ - $this->onFailedBlockAction($vBlockPos, null); + $this->syncBlocksNearby($vBlockPos, null); } return true; case UseItemTransactionData::ACTION_CLICK_AIR: @@ -529,9 +532,9 @@ class InGamePacketHandler extends PacketHandler{ } /** - * Internal function used to execute rollbacks when an action fails on a block. + * Syncs blocks nearby to ensure that the client and server agree on the world's blocks after a block interaction. */ - private function onFailedBlockAction(Vector3 $blockPos, ?int $face) : void{ + private function syncBlocksNearby(Vector3 $blockPos, ?int $face) : void{ if($blockPos->distanceSquared($this->player->getLocation()) < 10000){ $blocks = $blockPos->sidesArray(); if($face !== null){ @@ -668,7 +671,7 @@ class InGamePacketHandler extends PacketHandler{ } public function handleActorPickRequest(ActorPickRequestPacket $packet) : bool{ - return false; //TODO + return $this->player->pickEntity($packet->actorUniqueId); } public function handlePlayerAction(PlayerActionPacket $packet) : bool{ @@ -682,7 +685,7 @@ class InGamePacketHandler extends PacketHandler{ case PlayerAction::START_BREAK: self::validateFacing($face); if(!$this->player->attackBlock($pos, $face)){ - $this->onFailedBlockAction($pos, $face); + $this->syncBlocksNearby($pos, $face); } break; @@ -998,7 +1001,7 @@ class InGamePacketHandler extends PacketHandler{ $lectern = $world->getBlockAt($pos->getX(), $pos->getY(), $pos->getZ()); if($lectern instanceof Lectern && $this->player->canInteract($lectern->getPosition(), 15)){ if(!$lectern->onPageTurn($packet->page)){ - $this->onFailedBlockAction($lectern->getPosition(), null); + $this->syncBlocksNearby($lectern->getPosition(), null); } return true; } diff --git a/src/player/Player.php b/src/player/Player.php index 3a6997ef7..96f53470f 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -58,6 +58,7 @@ use pocketmine\event\player\PlayerDisplayNameChangeEvent; use pocketmine\event\player\PlayerDropItemEvent; use pocketmine\event\player\PlayerEmoteEvent; use pocketmine\event\player\PlayerEntityInteractEvent; +use pocketmine\event\player\PlayerEntityPickEvent; use pocketmine\event\player\PlayerExhaustEvent; use pocketmine\event\player\PlayerGameModeChangeEvent; use pocketmine\event\player\PlayerInteractEvent; @@ -1714,29 +1715,58 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $ev->call(); if(!$ev->isCancelled()){ - if($existingSlot !== -1){ - if($existingSlot < $this->inventory->getHotbarSize()){ - $this->inventory->setHeldItemIndex($existingSlot); - }else{ - $this->inventory->swap($this->inventory->getHeldItemIndex(), $existingSlot); - } - }else{ - $firstEmpty = $this->inventory->firstEmpty(); - if($firstEmpty === -1){ //full inventory - $this->inventory->setItemInHand($item); - }elseif($firstEmpty < $this->inventory->getHotbarSize()){ - $this->inventory->setItem($firstEmpty, $item); - $this->inventory->setHeldItemIndex($firstEmpty); - }else{ - $this->inventory->swap($this->inventory->getHeldItemIndex(), $firstEmpty); - $this->inventory->setItemInHand($item); - } - } + $this->equipOrAddPickedItem($existingSlot, $item); } return true; } + public function pickEntity(int $entityId) : bool{ + $entity = $this->getWorld()->getEntity($entityId); + if($entity === null){ + return true; + } + + $item = $entity->getPickedItem(); + if($item === null){ + return true; + } + + $ev = new PlayerEntityPickEvent($this, $entity, $item); + $existingSlot = $this->inventory->first($item); + if($existingSlot === -1 && ($this->hasFiniteResources() || $this->isSpectator())){ + $ev->cancel(); + } + $ev->call(); + + if(!$ev->isCancelled()){ + $this->equipOrAddPickedItem($existingSlot, $item); + } + + return true; + } + + private function equipOrAddPickedItem(int $existingSlot, Item $item) : void{ + if($existingSlot !== -1){ + if($existingSlot < $this->inventory->getHotbarSize()){ + $this->inventory->setHeldItemIndex($existingSlot); + }else{ + $this->inventory->swap($this->inventory->getHeldItemIndex(), $existingSlot); + } + }else{ + $firstEmpty = $this->inventory->firstEmpty(); + if($firstEmpty === -1){ //full inventory + $this->inventory->setItemInHand($item); + }elseif($firstEmpty < $this->inventory->getHotbarSize()){ + $this->inventory->setItem($firstEmpty, $item); + $this->inventory->setHeldItemIndex($firstEmpty); + }else{ + $this->inventory->swap($this->inventory->getHeldItemIndex(), $firstEmpty); + $this->inventory->setItemInHand($item); + } + } + } + /** * Performs a left-click (attack) action on the block. * diff --git a/src/world/World.php b/src/world/World.php index fbfa05b71..702410191 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -2173,19 +2173,25 @@ class World implements ChunkManager{ if($player !== null){ $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK); + if($player->isSneaking()){ + $ev->setUseItem(false); + $ev->setUseBlock($item->isNull()); //opening doors is still possible when sneaking if using an empty hand + } if($player->isSpectator()){ $ev->cancel(); //set it to cancelled so plugins can bypass this } $ev->call(); if(!$ev->isCancelled()){ - if((!$player->isSneaking() || $item->isNull()) && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){ + if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){ return true; } - $result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems); - if($result !== ItemUseResult::NONE){ - return $result === ItemUseResult::SUCCESS; + if($ev->useItem()){ + $result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems); + if($result !== ItemUseResult::NONE){ + return $result === ItemUseResult::SUCCESS; + } } }else{ return false; diff --git a/start.sh b/start.sh index 4d4787114..8f458b4e6 100755 --- a/start.sh +++ b/start.sh @@ -44,6 +44,27 @@ fi LOOPS=0 +handle_exit_code() { + local exitcode=$1 + if [ "$exitcode" -eq 134 ] || [ "$exitcode" -eq 139 ]; then #SIGABRT/SIGSEGV + echo "" + echo "ERROR: The server process was killed due to a critical error (code $exitcode) which could indicate a problem with PHP." + echo "Updating your PHP binary is recommended." + echo "If this keeps happening, please open a bug report." + echo "" + elif [ "$exitcode" -eq 143 ]; then #SIGKILL, maybe user intervention + echo "" + echo "WARNING: Server was forcibly killed!" + echo "If you didn't kill the server manually, this probably means the server used too much memory and was killed by the system's OOM Killer." + echo "Please ensure your system has enough available RAM." + echo "" + elif [ "$exitcode" -ne 0 ] && [ "$exitcode" -ne 137 ]; then #normal exit / SIGTERM + echo "" + echo "WARNING: Server did not shut down correctly! (code $exitcode)" + echo "" + fi +} + set +e if [ "$DO_LOOP" == "yes" ]; then @@ -52,11 +73,15 @@ if [ "$DO_LOOP" == "yes" ]; then echo "Restarted $LOOPS times" fi "$PHP_BINARY" "$POCKETMINE_FILE" "$@" + handle_exit_code $? echo "To escape the loop, press CTRL+C now. Otherwise, wait 5 seconds for the server to restart." echo "" sleep 5 ((LOOPS++)) done else - exec "$PHP_BINARY" "$POCKETMINE_FILE" "$@" + "$PHP_BINARY" "$POCKETMINE_FILE" "$@" + exitcode=$? + handle_exit_code $exitcode + exit $exitcode fi