resetHotbar(false); parent::__construct($player, InventoryType::get(InventoryType::PLAYER)); } public function getSize() : int{ return parent::getSize() - 4; //Remove armor slots } public function setSize(int $size){ parent::setSize($size + 4); $this->sendContents($this->getViewers()); } /** * Called when a client equips a hotbar slot. This method should not be used by plugins. * This method will call PlayerItemHeldEvent. * * @param int $hotbarSlot Number of the hotbar slot to equip. * @param int|null $inventorySlot Inventory slot to map to the specified hotbar slot. Supply null to make no change to the link. * * @return bool if the equipment change was successful, false if not. */ public function equipItem(int $hotbarSlot, $inventorySlot = null) : bool{ if($inventorySlot === null){ $inventorySlot = $this->getHotbarSlotIndex($hotbarSlot); } if($hotbarSlot < 0 or $hotbarSlot >= $this->getHotbarSize() or $inventorySlot < -1 or $inventorySlot >= $this->getSize()){ $this->sendContents($this->getHolder()); return false; } if($inventorySlot === -1){ $item = Item::get(Item::AIR, 0, 0); }else{ $item = $this->getItem($inventorySlot); } $this->getHolder()->getLevel()->getServer()->getPluginManager()->callEvent($ev = new PlayerItemHeldEvent($this->getHolder(), $item, $inventorySlot, $hotbarSlot)); if($ev->isCancelled()){ $this->sendContents($this->getHolder()); return false; } $this->setHotbarSlotIndex($hotbarSlot, $inventorySlot); $this->setHeldItemIndex($hotbarSlot, false); return true; } /** * Returns the index of the inventory slot mapped to the specified hotbar slot, or -1 if the hotbar slot does not exist. * @param int $index * * @return int */ public function getHotbarSlotIndex($index){ return $this->hotbar[$index] ?? -1; } /** * 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 */ public function setHotbarSlotIndex($hotbarSlot, $inventorySlot){ if($hotbarSlot < 0 or $hotbarSlot >= $this->getHotbarSize()){ throw new \InvalidArgumentException("Hotbar slot index \"$hotbarSlot\" is out of range"); }elseif($inventorySlot < -1 or $inventorySlot >= $this->getSize()){ throw new \InvalidArgumentException("Inventory slot index \"$inventorySlot\" is out of range"); } if($inventorySlot !== -1 and ($alreadyEquippedIndex = array_search($inventorySlot, $this->hotbar)) !== false){ /* Swap the slots * This assumes that the equipped slot can only be equipped in one other slot * it will not account for ancient bugs where the same slot ended up linked to several hotbar slots. * Such bugs will require a hotbar reset to default. */ $this->hotbar[$alreadyEquippedIndex] = $this->hotbar[$hotbarSlot]; } $this->hotbar[$hotbarSlot] = $inventorySlot; } /** * Returns the item in the slot linked to the specified hotbar slot, or Air if the slot is not linked to any hotbar slot. * @param int $hotbarSlotIndex * * @return Item */ public function getHotbarSlotItem(int $hotbarSlotIndex) : Item{ $inventorySlot = $this->getHotbarSlotIndex($hotbarSlotIndex); if($inventorySlot !== -1){ return $this->getItem($inventorySlot); }else{ return Item::get(Item::AIR, 0, 0); } } /** * Resets hotbar links to their original defaults. * @param bool $send Whether to send changes to the holder. */ public function resetHotbar(bool $send = true){ $this->hotbar = range(0, $this->getHotbarSize() - 1, 1); if($send){ $this->sendContents($this->getHolder()); } } /** * Returns the hotbar slot number the holder is currently holding. * @return int */ public function getHeldItemIndex(){ return $this->itemInHandIndex; } /** * Sets which hotbar slot the player is currently loading. * * @param int $index 0-8 index of the hotbar slot to hold * @param bool $send Whether to send updates back to the inventory holder. This should usually be true for plugin calls. * It should only be false to prevent feedback loops of equipment packets between client and server. */ public function setHeldItemIndex($index, $send = true){ if($index >= 0 and $index < $this->getHotbarSize()){ $this->itemInHandIndex = $index; if($this->getHolder() instanceof Player and $send){ $this->sendHeldItem($this->getHolder()); } $this->sendHeldItem($this->getHolder()->getViewers()); }else{ throw new \InvalidArgumentException("Hotbar slot index \"$index\" is out of range"); } } /** * Returns the currently-held item. * * @return Item */ public function getItemInHand(){ return $this->getHotbarSlotItem($this->itemInHandIndex); } /** * Sets the item in the currently-held slot to the specified item. * @param Item $item * * @return bool */ public function setItemInHand(Item $item){ return $this->setItem($this->getHeldItemSlot(), $item); } /** * Returns the hotbar slot number currently held. * * @return int */ public function getHeldItemSlot(){ return $this->getHotbarSlotIndex($this->itemInHandIndex); } /** * Sets the hotbar slot link of the currently-held hotbar slot. * @deprecated Do not change hotbar slot mapping with plugins, this will cause myriad client-sided bugs, especially with desktop GUI clients. * * @param int $slot */ public function setHeldItemSlot($slot){ if($slot >= -1 and $slot < $this->getSize()){ $this->setHotbarSlotIndex($this->getHeldItemIndex(), $slot); } } /** * Sends the currently-held item to specified targets. * @param Player|Player[] $target */ public function sendHeldItem($target){ $item = $this->getItemInHand(); $pk = new MobEquipmentPacket(); $pk->entityRuntimeId = $this->getHolder()->getId(); $pk->item = $item; $pk->inventorySlot = $this->getHeldItemSlot(); $pk->hotbarSlot = $this->getHeldItemIndex(); $pk->windowId = ContainerIds::INVENTORY; if(!is_array($target)){ $target->dataPacket($pk); if($this->getHeldItemSlot() !== -1 and $target === $this->getHolder()){ $this->sendSlot($this->getHeldItemSlot(), $target); } }else{ $this->getHolder()->getLevel()->getServer()->broadcastPacket($target, $pk); if($this->getHeldItemSlot() !== -1 and in_array($this->getHolder(), $target, true)){ $this->sendSlot($this->getHeldItemSlot(), $this->getHolder()); } } } public function onSlotChange($index, $before){ $holder = $this->getHolder(); if($holder instanceof Player and !$holder->spawned){ return; } if($index >= $this->getSize()){ $this->sendArmorSlot($index, $this->getViewers()); $this->sendArmorSlot($index, $this->getHolder()->getViewers()); }else{ //Do not send armor by accident here. parent::onSlotChange($index, $before); } } /** * Returns the number of slots in the hotbar. * @return int */ public function getHotbarSize(){ return 9; } public function getArmorItem($index){ return $this->getItem($this->getSize() + $index); } public function setArmorItem($index, Item $item){ return $this->setItem($this->getSize() + $index, $item); } public function getHelmet(){ return $this->getItem($this->getSize()); } public function getChestplate(){ return $this->getItem($this->getSize() + 1); } public function getLeggings(){ return $this->getItem($this->getSize() + 2); } public function getBoots(){ return $this->getItem($this->getSize() + 3); } public function setHelmet(Item $helmet){ return $this->setItem($this->getSize(), $helmet); } public function setChestplate(Item $chestplate){ return $this->setItem($this->getSize() + 1, $chestplate); } public function setLeggings(Item $leggings){ return $this->setItem($this->getSize() + 2, $leggings); } public function setBoots(Item $boots){ return $this->setItem($this->getSize() + 3, $boots); } public function setItem(int $index, Item $item) : bool{ if($index < 0 or $index >= $this->size){ return false; }elseif($item->getId() === 0 or $item->getCount() <= 0){ return $this->clear($index); } if($index >= $this->getSize()){ //Armor change Server::getInstance()->getPluginManager()->callEvent($ev = new EntityArmorChangeEvent($this->getHolder(), $this->getItem($index), $item, $index)); if($ev->isCancelled() and $this->getHolder() instanceof Human){ $this->sendArmorSlot($index, $this->getViewers()); return false; } $item = $ev->getNewItem(); }else{ Server::getInstance()->getPluginManager()->callEvent($ev = new EntityInventoryChangeEvent($this->getHolder(), $this->getItem($index), $item, $index)); if($ev->isCancelled()){ $this->sendSlot($index, $this->getViewers()); return false; } $item = $ev->getNewItem(); } $old = $this->getItem($index); $this->slots[$index] = clone $item; $this->onSlotChange($index, $old); return true; } public function clear(int $index) : bool{ if(isset($this->slots[$index])){ $item = Item::get(Item::AIR, 0, 0); $old = $this->slots[$index]; if($index >= $this->getSize() and $index < $this->size){ //Armor change Server::getInstance()->getPluginManager()->callEvent($ev = new EntityArmorChangeEvent($this->getHolder(), $old, $item, $index)); if($ev->isCancelled()){ if($index >= $this->size){ $this->sendArmorSlot($index, $this->getViewers()); }else{ $this->sendSlot($index, $this->getViewers()); } return false; } $item = $ev->getNewItem(); }else{ Server::getInstance()->getPluginManager()->callEvent($ev = new EntityInventoryChangeEvent($this->getHolder(), $old, $item, $index)); if($ev->isCancelled()){ if($index >= $this->size){ $this->sendArmorSlot($index, $this->getViewers()); }else{ $this->sendSlot($index, $this->getViewers()); } return false; } $item = $ev->getNewItem(); } if($item->getId() !== Item::AIR){ $this->slots[$index] = clone $item; }else{ unset($this->slots[$index]); } $this->onSlotChange($index, $old); } return true; } /** * @return Item[] */ public function getArmorContents(){ $armor = []; for($i = 0; $i < 4; ++$i){ $armor[$i] = $this->getItem($this->getSize() + $i); } return $armor; } public function clearAll(){ $limit = $this->getSize() + 4; for($index = 0; $index < $limit; ++$index){ $this->clear($index); } } /** * @param Player|Player[] $target */ public function sendArmorContents($target){ if($target instanceof Player){ $target = [$target]; } $armor = $this->getArmorContents(); $pk = new MobArmorEquipmentPacket(); $pk->entityRuntimeId = $this->getHolder()->getId(); $pk->slots = $armor; $pk->encode(); foreach($target as $player){ if($player === $this->getHolder()){ $pk2 = new InventoryContentPacket(); $pk2->windowId = ContainerIds::ARMOR; $pk2->items = $armor; $player->dataPacket($pk2); }else{ $player->dataPacket($pk); } } } /** * @param Item[] $items */ public function setArmorContents(array $items){ for($i = 0; $i < 4; ++$i){ if(!isset($items[$i]) or !($items[$i] instanceof Item)){ $items[$i] = Item::get(Item::AIR, 0, 0); } if($items[$i]->getId() === Item::AIR){ $this->clear($this->getSize() + $i); }else{ $this->setItem($this->getSize() + $i, $items[$i]); } } } /** * @param int $index * @param Player|Player[] $target */ public function sendArmorSlot($index, $target){ if($target instanceof Player){ $target = [$target]; } $armor = $this->getArmorContents(); $pk = new MobArmorEquipmentPacket(); $pk->entityRuntimeId = $this->getHolder()->getId(); $pk->slots = $armor; $pk->encode(); foreach($target as $player){ if($player === $this->getHolder()){ /** @var Player $player */ $pk2 = new InventorySlotPacket(); $pk2->windowId = ContainerIds::ARMOR; $pk2->slotIndex = $index - $this->getSize(); $pk2->item = $this->getItem($index); $player->dataPacket($pk2); }else{ $player->dataPacket($pk); } } } /** * @param Player|Player[] $target */ public function sendContents($target){ if($target instanceof Player){ $target = [$target]; } $pk = new InventoryContentPacket(); for($i = 0; $i < $this->getSize(); ++$i){ //Do not send armor by error here $pk->items[$i] = $this->getItem($i); } foreach($target as $player){ if(($id = $player->getWindowId($this)) === -1 or $player->spawned !== true){ $this->close($player); continue; } $pk->windowId = $id; $player->dataPacket(clone $pk); if($player === $this->getHolder()){ $pk = new PlayerHotbarPacket(); $pk->slots = array_map(function(int $i){ return $i + $this->getHotbarSize(); }, $this->hotbar); $pk->selectedSlot = $this->getHeldItemIndex(); $pk->windowId = ContainerIds::INVENTORY; $player->dataPacket($pk); } } } public function sendCreativeContents(){ $pk = new InventoryContentPacket(); $pk->windowId = ContainerIds::CREATIVE; if($this->getHolder()->getGamemode() === Player::CREATIVE){ foreach(Item::getCreativeItems() as $i => $item){ $pk->items[$i] = clone $item; } } $this->getHolder()->dataPacket($pk); } /** * @param int $index * @param Player|Player[] $target */ public function sendSlot($index, $target){ if($target instanceof Player){ $target = [$target]; } $pk = new InventorySlotPacket(); $pk->slotIndex = $index; $pk->item = clone $this->getItem($index); foreach($target as $player){ if($player === $this->getHolder()){ /** @var Player $player */ $pk->windowId = ContainerIds::INVENTORY; $player->dataPacket(clone $pk); }else{ if(($id = $player->getWindowId($this)) === -1){ $this->close($player); continue; } $pk->windowId = $id; $player->dataPacket(clone $pk); } } } /** * @return Human|Player */ public function getHolder(){ return parent::getHolder(); } }