diff --git a/src/entity/Entity.php b/src/entity/Entity.php index 4a7a4df77..900ebc6d3 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -1599,6 +1599,7 @@ abstract class Entity{ $properties->setLong(EntityMetadataProperties::TARGET_EID, $this->targetId ?? 0); $properties->setString(EntityMetadataProperties::NAMETAG, $this->nameTag); $properties->setString(EntityMetadataProperties::SCORE_TAG, $this->scoreTag); + $properties->setByte(EntityMetadataProperties::COLOR, 0); $properties->setGenericFlag(EntityMetadataFlags::AFFECTED_BY_GRAVITY, true); $properties->setGenericFlag(EntityMetadataFlags::CAN_CLIMB, $this->canClimb); diff --git a/src/entity/ExperienceManager.php b/src/entity/ExperienceManager.php index 457187745..7f4d46511 100644 --- a/src/entity/ExperienceManager.php +++ b/src/entity/ExperienceManager.php @@ -239,6 +239,7 @@ class ExperienceManager{ public function onPickupXp(int $xpValue) : void{ static $mainHandIndex = -1; + static $offHandIndex = -2; //TODO: replace this with a more generic equipment getting/setting interface /** @var Durable[] $equipment */ @@ -247,7 +248,9 @@ class ExperienceManager{ if(($item = $this->entity->getInventory()->getItemInHand()) instanceof Durable and $item->hasEnchantment(VanillaEnchantments::MENDING())){ $equipment[$mainHandIndex] = $item; } - //TODO: check offhand + if(($item = $this->entity->getOffHandInventory()->getItem(0)) instanceof Durable and $item->hasEnchantment(VanillaEnchantments::MENDING())){ + $equipment[$offHandIndex] = $item; + } foreach($this->entity->getArmorInventory()->getContents() as $k => $armorItem){ if($armorItem instanceof Durable and $armorItem->hasEnchantment(VanillaEnchantments::MENDING())){ $equipment[$k] = $armorItem; @@ -263,6 +266,8 @@ class ExperienceManager{ if($k === $mainHandIndex){ $this->entity->getInventory()->setItemInHand($repairItem); + }elseif($k === $offHandIndex){ + $this->entity->getOffHandInventory()->setItem(0, $repairItem); }else{ $this->entity->getArmorInventory()->setItem($k, $repairItem); } diff --git a/src/entity/Human.php b/src/entity/Human.php index 8362671b9..75ac5c056 100644 --- a/src/entity/Human.php +++ b/src/entity/Human.php @@ -34,6 +34,7 @@ use pocketmine\inventory\Inventory; use pocketmine\inventory\InventoryHolder; use pocketmine\inventory\PlayerEnderInventory; use pocketmine\inventory\PlayerInventory; +use pocketmine\inventory\PlayerOffHandInventory; use pocketmine\item\enchantment\VanillaEnchantments; use pocketmine\item\Item; use pocketmine\item\Totem; @@ -73,6 +74,9 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ /** @var PlayerInventory */ protected $inventory; + /** @var PlayerOffHandInventory */ + protected $offHandInventory; + /** @var PlayerEnderInventory */ protected $enderInventory; @@ -193,6 +197,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ return $this->inventory; } + public function getOffHandInventory() : PlayerOffHandInventory{ return $this->offHandInventory; } + public function getEnderInventory() : PlayerEnderInventory{ return $this->enderInventory; } @@ -218,7 +224,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ $this->inventory = new PlayerInventory($this); $syncHeldItem = function() : void{ foreach($this->getViewers() as $viewer){ - $viewer->getNetworkSession()->onMobEquipmentChange($this); + $viewer->getNetworkSession()->onMobMainHandItemChange($this); } }; $this->inventory->getListeners()->add(new CallbackInventoryListener( @@ -233,6 +239,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ } } )); + $this->offHandInventory = new PlayerOffHandInventory($this); $this->enderInventory = new PlayerEnderInventory($this); $this->initHumanData($nbt); @@ -258,6 +265,15 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ $this->armorInventory->getListeners()->add(...$armorListeners); $this->inventory->getListeners()->add(...$inventoryListeners); } + $offHand = $nbt->getCompoundTag("OffHandItem"); + if($offHand !== null){ + $this->offHandInventory->setItem(0, Item::nbtDeserialize($offHand)); + } + $this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(function() : void{ + foreach($this->getViewers() as $viewer){ + $viewer->getNetworkSession()->onMobOffHandItemChange($this); + } + })); $enderChestInventoryTag = $nbt->getListTag("EnderChestInventory"); if($enderChestInventoryTag !== null){ @@ -270,7 +286,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ $this->inventory->setHeldItemIndex($nbt->getInt("SelectedInventorySlot", 0)); $this->inventory->getHeldItemIndexChangeListeners()->add(function(int $oldIndex) : void{ foreach($this->getViewers() as $viewer){ - $viewer->getNetworkSession()->onMobEquipmentChange($this); + $viewer->getNetworkSession()->onMobMainHandItemChange($this); } }); @@ -309,7 +325,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ $type = $source->getCause(); if($type !== EntityDamageEvent::CAUSE_SUICIDE and $type !== EntityDamageEvent::CAUSE_VOID - and $this->inventory->getItemInHand() instanceof Totem){ //TODO: check offhand as well (when it's implemented) + and ($this->inventory->getItemInHand() instanceof Totem || $this->offHandInventory->getItem(0) instanceof Totem)){ $compensation = $this->getHealth() - $source->getFinalDamage() - 1; if($compensation < 0){ @@ -335,6 +351,9 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ if($hand instanceof Totem){ $hand->pop(); //Plugins could alter max stack size $this->inventory->setItemInHand($hand); + }elseif(($offHand = $this->offHandInventory->getItem(0)) instanceof Totem){ + $offHand->pop(); + $this->offHandInventory->setItem(0, $offHand); } } } @@ -342,7 +361,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ public function getDrops() : array{ return array_filter(array_merge( $this->inventory !== null ? array_values($this->inventory->getContents()) : [], - $this->armorInventory !== null ? array_values($this->armorInventory->getContents()) : [] + $this->armorInventory !== null ? array_values($this->armorInventory->getContents()) : [], + $this->offHandInventory !== null ? array_values($this->offHandInventory->getContents()) : [], ), function(Item $item) : bool{ return !$item->hasEnchantment(VanillaEnchantments::VANISHING()); }); } @@ -381,6 +401,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ $nbt->setInt("SelectedInventorySlot", $this->inventory->getHeldItemIndex()); } + $offHandItem = $this->offHandInventory->getItem(0); + if(!$offHandItem->isNull()){ + $nbt->setTag("OffHandItem", $offHandItem->nbtSerialize()); + } if($this->enderInventory !== null){ /** @var CompoundTag[] $items */ @@ -437,6 +461,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ $this->sendData([$player], [EntityMetadataProperties::NAMETAG => new StringMetadataProperty($this->getNameTag())]); $player->getNetworkSession()->onMobArmorChange($this); + $player->getNetworkSession()->onMobOffHandItemChange($this); if(!($this instanceof Player)){ $player->getNetworkSession()->sendDataPacket(PlayerListPacket::remove([PlayerListEntry::createRemovalEntry($this->uuid)])); @@ -466,12 +491,14 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ protected function onDispose() : void{ $this->inventory->removeAllViewers(); $this->inventory->getHeldItemIndexChangeListeners()->clear(); + $this->offHandInventory->removeAllViewers(); $this->enderInventory->removeAllViewers(); parent::onDispose(); } protected function destroyCycles() : void{ $this->inventory = null; + $this->offHandInventory = null; $this->enderInventory = null; $this->hungerManager = null; $this->xpManager = null; diff --git a/src/inventory/PlayerOffhandInventory.php b/src/inventory/PlayerOffhandInventory.php new file mode 100644 index 000000000..89354ffcf --- /dev/null +++ b/src/inventory/PlayerOffhandInventory.php @@ -0,0 +1,38 @@ +holder = $player; + parent::__construct(1); + } + + public function getHolder() : Human{ return $this->holder; } +} diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php index d28e34abc..e162687b6 100644 --- a/src/network/mcpe/InventoryManager.php +++ b/src/network/mcpe/InventoryManager.php @@ -96,6 +96,7 @@ class InventoryManager{ $this->containerOpenCallbacks->add(\Closure::fromCallable([self::class, 'createContainerOpen'])); $this->add(ContainerIds::INVENTORY, $this->player->getInventory()); + $this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory()); $this->add(ContainerIds::ARMOR, $this->player->getArmorInventory()); $this->add(ContainerIds::UI, $this->player->getCursorInventory()); diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index ba0df464b..88a9307a6 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -963,14 +963,18 @@ class NetworkSession{ /** * TODO: expand this to more than just humans - * TODO: offhand */ - public function onMobEquipmentChange(Human $mob) : void{ + public function onMobMainHandItemChange(Human $mob) : void{ //TODO: we could send zero for slot here because remote players don't need to know which slot was selected $inv = $mob->getInventory(); $this->sendDataPacket(MobEquipmentPacket::create($mob->getId(), ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItemInHand())), $inv->getHeldItemIndex(), ContainerIds::INVENTORY)); } + public function onMobOffHandItemChange(Human $mob) : void{ + $inv = $mob->getOffHandInventory(); + $this->sendDataPacket(MobEquipmentPacket::create($mob->getId(), ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItem(0))), 0, ContainerIds::OFFHAND)); + } + public function onMobArmorChange(Living $mob) : void{ $inv = $mob->getArmorInventory(); $converter = TypeConverter::getInstance(); diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index c499258d2..376fa5361 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -463,11 +463,17 @@ class InGamePacketHandler extends PacketHandler{ } public function handleMobEquipment(MobEquipmentPacket $packet) : bool{ - $this->session->getInvManager()->onClientSelectHotbarSlot($packet->hotbarSlot); - if(!$this->player->selectHotbarSlot($packet->hotbarSlot)){ - $this->session->getInvManager()->syncSelectedHotbarSlot(); + if($packet->windowId === ContainerIds::OFFHAND){ + return true; //this happens when we put an item into the offhand } - return true; + if($packet->windowId === ContainerIds::INVENTORY){ + $this->session->getInvManager()->onClientSelectHotbarSlot($packet->hotbarSlot); + if(!$this->player->selectHotbarSlot($packet->hotbarSlot)){ + $this->session->getInvManager()->syncSelectedHotbarSlot(); + } + return true; + } + return false; } public function handleMobArmorEquipment(MobArmorEquipmentPacket $packet) : bool{ diff --git a/src/player/Player.php b/src/player/Player.php index 240e7f0bc..4b1556d72 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -2103,6 +2103,9 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ if($this->armorInventory !== null){ $this->armorInventory->clearAll(); } + if($this->offHandInventory !== null){ + $this->offHandInventory->clearAll(); + } } $this->getWorld()->dropExperience($this->location, $ev->getXpDropAmount()); diff --git a/tests/phpstan/configs/gc-hacks.neon b/tests/phpstan/configs/gc-hacks.neon index e14be2fae..357f0d73d 100644 --- a/tests/phpstan/configs/gc-hacks.neon +++ b/tests/phpstan/configs/gc-hacks.neon @@ -50,6 +50,11 @@ parameters: count: 1 path: ../../../src/entity/Human.php + - + message: "#^Property pocketmine\\\\entity\\\\Human\\:\\:\\$offHandInventory \\(pocketmine\\\\inventory\\\\PlayerOffHandInventory\\) does not accept null\\.$#" + count: 1 + path: ../../../src/entity/Human.php + - message: "#^Property pocketmine\\\\entity\\\\Human\\:\\:\\$xpManager \\(pocketmine\\\\entity\\\\ExperienceManager\\) does not accept null\\.$#" count: 1