0x7fff){ //signed short range throw new \InvalidArgumentException("ID must be in range " . -0x8000 . " - " . 0x7fff); } $this->id = $id; $this->meta = $variant !== -1 ? $variant & 0x7FFF : -1; $this->name = $name; } /** * @return bool */ public function hasCustomBlockData() : bool{ return $this->getNamedTag()->hasTag(self::TAG_BLOCK_ENTITY_TAG, CompoundTag::class); } public function clearCustomBlockData(){ $this->getNamedTag()->removeTag(self::TAG_BLOCK_ENTITY_TAG); return $this; } /** * @param CompoundTag $compound * * @return Item */ public function setCustomBlockData(CompoundTag $compound) : Item{ $this->getNamedTag()->setTag(self::TAG_BLOCK_ENTITY_TAG, clone $compound); return $this; } /** * @return CompoundTag|null */ public function getCustomBlockData() : ?CompoundTag{ return $this->getNamedTag()->getCompoundTag(self::TAG_BLOCK_ENTITY_TAG); } /** * @return bool */ public function hasEnchantments() : bool{ return $this->getNamedTag()->hasTag(self::TAG_ENCH, ListTag::class); } /** * @param Enchantment $enchantment * @param int $level * * @return bool */ public function hasEnchantment(Enchantment $enchantment, int $level = -1) : bool{ $ench = $this->getNamedTag()->getListTag(self::TAG_ENCH); if(!($ench instanceof ListTag)){ return false; } $id = $enchantment->getId(); /** @var CompoundTag $entry */ foreach($ench as $entry){ if($entry->getShort("id") === $id and ($level === -1 or $entry->getShort("lvl") === $level)){ return true; } } return false; } /** * @param Enchantment $enchantment * * @return EnchantmentInstance|null */ public function getEnchantment(Enchantment $enchantment) : ?EnchantmentInstance{ $ench = $this->getNamedTag()->getListTag(self::TAG_ENCH); if(!($ench instanceof ListTag)){ return null; } $id = $enchantment->getId(); /** @var CompoundTag $entry */ foreach($ench as $entry){ if($entry->getShort("id") === $id){ $e = Enchantment::get($entry->getShort("id")); if($e !== null){ return new EnchantmentInstance($e, $entry->getShort("lvl")); } } } return null; } /** * @param Enchantment $enchantment * @param int $level * * @return Item */ public function removeEnchantment(Enchantment $enchantment, int $level = -1) : Item{ $ench = $this->getNamedTag()->getListTag(self::TAG_ENCH); if(!($ench instanceof ListTag)){ return $this; } $id = $enchantment->getId(); /** @var CompoundTag $entry */ foreach($ench as $k => $entry){ if($entry->getShort("id") === $id and ($level === -1 or $entry->getShort("lvl") === $level)){ $ench->remove($k); break; } } return $this; } /** * @return Item */ public function removeEnchantments() : Item{ $this->getNamedTag()->removeTag(self::TAG_ENCH); return $this; } /** * @param EnchantmentInstance $enchantment * * @return Item */ public function addEnchantment(EnchantmentInstance $enchantment) : Item{ $found = false; $ench = $this->getNamedTag()->getListTag(self::TAG_ENCH); if(!($ench instanceof ListTag)){ $ench = new ListTag([], NBT::TAG_Compound); }else{ /** @var CompoundTag $entry */ foreach($ench as $k => $entry){ if($entry->getShort("id") === $enchantment->getId()){ $ench->set($k, CompoundTag::create() ->setShort("id", $enchantment->getId()) ->setShort("lvl", $enchantment->getLevel()) ); $found = true; break; } } } if(!$found){ $ench->push(CompoundTag::create() ->setShort("id", $enchantment->getId()) ->setShort("lvl", $enchantment->getLevel()) ); } $this->getNamedTag()->setTag(self::TAG_ENCH, $ench); return $this; } /** * @return EnchantmentInstance[] */ public function getEnchantments() : array{ /** @var EnchantmentInstance[] $enchantments */ $enchantments = []; $ench = $this->getNamedTag()->getListTag(self::TAG_ENCH); if($ench instanceof ListTag){ /** @var CompoundTag $entry */ foreach($ench as $entry){ $e = Enchantment::get($entry->getShort("id")); if($e !== null){ $enchantments[] = new EnchantmentInstance($e, $entry->getShort("lvl")); } } } return $enchantments; } /** * Returns the level of the enchantment on this item with the specified ID, or 0 if the item does not have the * enchantment. * * @param Enchantment $enchantment * * @return int */ public function getEnchantmentLevel(Enchantment $enchantment) : int{ $ench = $this->getNamedTag()->getListTag(self::TAG_ENCH); if($ench !== null){ /** @var CompoundTag $entry */ $enchantmentId = $enchantment->getId(); foreach($ench as $entry){ if($entry->getShort("id") === $enchantmentId){ return $entry->getShort("lvl"); } } } return 0; } /** * @return bool */ public function hasCustomName() : bool{ $display = $this->getNamedTag()->getCompoundTag(self::TAG_DISPLAY); if($display instanceof CompoundTag){ return $display->hasTag(self::TAG_DISPLAY_NAME); } return false; } /** * @return string */ public function getCustomName() : string{ $display = $this->getNamedTag()->getCompoundTag(self::TAG_DISPLAY); if($display instanceof CompoundTag){ return $display->getString(self::TAG_DISPLAY_NAME, ""); } return ""; } /** * @param string $name * * @return Item */ public function setCustomName(string $name) : Item{ if($name === ""){ $this->clearCustomName(); } /** @var CompoundTag $display */ $display = $this->getNamedTag()->getCompoundTag(self::TAG_DISPLAY); if($display === null){ $display = new CompoundTag(); } $display->setString(self::TAG_DISPLAY_NAME, $name); $this->getNamedTag()->setTag(self::TAG_DISPLAY, $display); return $this; } /** * @return Item */ public function clearCustomName() : Item{ $display = $this->getNamedTag()->getCompoundTag(self::TAG_DISPLAY); if($display instanceof CompoundTag){ $display->removeTag(self::TAG_DISPLAY_NAME); if($display->getCount() === 0){ $this->getNamedTag()->removeTag(self::TAG_DISPLAY); }else{ $this->getNamedTag()->setTag(self::TAG_DISPLAY, $display); } } return $this; } /** * @return string[] */ public function getLore() : array{ $display = $this->getNamedTag()->getCompoundTag(self::TAG_DISPLAY); if($display instanceof CompoundTag and ($lore = $display->getListTag(self::TAG_DISPLAY_LORE)) !== null){ return $lore->getAllValues(); } return []; } /** * @param string[] $lines * * @return Item */ public function setLore(array $lines) : Item{ $display = $this->getNamedTag()->getCompoundTag(self::TAG_DISPLAY); if(!($display instanceof CompoundTag)){ $display = new CompoundTag(); } $display->setTag(self::TAG_DISPLAY_LORE, new ListTag(array_map(function(string $str) : StringTag{ return new StringTag($str); }, $lines), NBT::TAG_String)); $this->getNamedTag()->setTag(self::TAG_DISPLAY, $display); return $this; } /** * Returns whether this Item has a non-empty NBT. * @return bool */ public function hasNamedTag() : bool{ return $this->nbt !== null and $this->nbt->count() > 0; } /** * Returns a tree of Tag objects representing the Item's NBT. If the item does not have any NBT, an empty CompoundTag * object is returned to allow the caller to manipulate and apply back to the item. * * @return CompoundTag */ public function getNamedTag() : CompoundTag{ return $this->nbt ?? ($this->nbt = new CompoundTag()); } /** * Sets the Item's NBT from the supplied CompoundTag object. * * @param CompoundTag $tag * * @return Item */ public function setNamedTag(CompoundTag $tag) : Item{ if($tag->getCount() === 0){ return $this->clearNamedTag(); } $this->nbt = clone $tag; return $this; } /** * Removes the Item's NBT. * @return Item */ public function clearNamedTag() : Item{ $this->nbt = null; return $this; } /** * @return int */ public function getCount() : int{ return $this->count; } /** * @param int $count * * @return Item */ public function setCount(int $count) : Item{ $this->count = $count; return $this; } /** * Pops an item from the stack and returns it, decreasing the stack count of this item stack by one. * * @param int $count * * @return Item * @throws \InvalidArgumentException if trying to pop more items than are on the stack */ public function pop(int $count = 1) : Item{ if($count > $this->count){ throw new \InvalidArgumentException("Cannot pop $count items from a stack of $this->count"); } $item = clone $this; $item->count = $count; $this->count -= $count; return $item; } public function isNull() : bool{ return $this->count <= 0 or $this->id === Item::AIR; } /** * 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->getVanillaName(); } /** * Returns the vanilla name of the item, disregarding custom names. * @return string */ public function getVanillaName() : string{ return $this->name; } /** * @return bool */ final public function canBePlaced() : bool{ return $this->getBlock()->canBePlaced(); } /** * Returns the block corresponding to this Item. * @return Block */ public function getBlock() : Block{ return VanillaBlocks::AIR(); } /** * @return int */ final public function getId() : int{ return $this->id; } /** * @return int */ public function getMeta() : int{ return $this->meta; } /** * 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() : int{ return 64; } /** * Returns the time in ticks which the item will fuel a furnace for. * @return int */ public function getFuelTime() : int{ return 0; } /** * Returns how many points of damage this item will deal to an entity when used as a weapon. * @return int */ public function getAttackPoints() : int{ return 1; } /** * Returns how many armor points can be gained by wearing this item. * @return int */ public function getDefensePoints() : int{ return 0; } /** * Returns what type of block-breaking tool this is. Blocks requiring the same tool type as the item will break * faster (except for blocks requiring no tool, which break at the same speed regardless of the tool used) * * @return int */ public function getBlockToolType() : int{ return BlockToolType::NONE; } /** * Returns the harvesting power that this tool has. This affects what blocks it can mine when the tool type matches * the mined block. * This should return 1 for non-tiered tools, and the tool tier for tiered tools. * * @see BlockBreakInfo::getToolHarvestLevel() * * @return int */ public function getBlockToolHarvestLevel() : int{ return 0; } public function getMiningEfficiency(bool $isCorrectTool) : float{ return 1; } /** * Called when a player uses this item on a block. * * @param Player $player * @param Block $blockReplace * @param Block $blockClicked * @param int $face * @param Vector3 $clickVector * * @return ItemUseResult */ public function onActivate(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector) : ItemUseResult{ return ItemUseResult::NONE(); } /** * Called when a player uses the item on air, for example throwing a projectile. * Returns whether the item was changed, for example count decrease or durability change. * * @param Player $player * @param Vector3 $directionVector * * @return ItemUseResult */ public function onClickAir(Player $player, Vector3 $directionVector) : ItemUseResult{ return ItemUseResult::NONE(); } /** * Called when a player is using this item and releases it. Used to handle bow shoot actions. * Returns whether the item was changed, for example count decrease or durability change. * * @param Player $player * * @return ItemUseResult */ public function onReleaseUsing(Player $player) : ItemUseResult{ return ItemUseResult::NONE(); } /** * Called when this item is used to destroy a block. Usually used to update durability. * * @param Block $block * * @return bool */ public function onDestroyBlock(Block $block) : bool{ return false; } /** * Called when this item is used to attack an entity. Usually used to update durability. * * @param Entity $victim * * @return bool */ public function onAttackEntity(Entity $victim) : bool{ return false; } /** * Returns the number of ticks a player must wait before activating this item again. * * @return int */ public function getCooldownTicks() : int{ return 0; } /** * 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 */ final public function equals(Item $item, bool $checkDamage = true, bool $checkCompound = true) : bool{ if($this->id === $item->getId() and (!$checkDamage or $this->getMeta() === $item->getMeta())){ if($checkCompound){ if($this->hasNamedTag() and $item->hasNamedTag()){ //both items have NBT return $this->getNamedTag()->equals($item->getNamedTag()); } return (!$this->hasNamedTag() and !$item->hasNamedTag()); //both items must have no NBT }else{ return true; } } return false; } /** * Returns whether the specified item stack has the same ID, damage, NBT and count as this item stack. * * @param Item $other * * @return bool */ final public function equalsExact(Item $other) : bool{ return $this->equals($other, true, true) and $this->count === $other->count; } /** * @return string */ final public function __toString() : string{ return "Item " . $this->name . " (" . $this->id . ":" . ($this->hasAnyDamageValue() ? "?" : $this->getMeta()) . ")x" . $this->count . ($this->hasNamedTag() ? " tags:" . base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($this->nbt))) : ""); } /** * Returns an array of item stack properties that can be serialized to json. * * @return array */ final public function jsonSerialize() : array{ $data = [ "id" => $this->getId() ]; if($this->getMeta() !== 0){ $data["damage"] = $this->getMeta(); } if($this->getCount() !== 1){ $data["count"] = $this->getCount(); } if($this->hasNamedTag()){ $data["nbt_b64"] = base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($this->getNamedTag()))); } return $data; } /** * Returns an Item from properties created in an array by {@link Item#jsonSerialize} * * @param array $data * * @return Item * @throws NbtDataException * @throws \InvalidArgumentException */ final public static function jsonDeserialize(array $data) : Item{ $nbt = ""; //Backwards compatibility if(isset($data["nbt"])){ $nbt = $data["nbt"]; }elseif(isset($data["nbt_hex"])){ $nbt = hex2bin($data["nbt_hex"]); }elseif(isset($data["nbt_b64"])){ $nbt = base64_decode($data["nbt_b64"], true); } return ItemFactory::get( (int) $data["id"], (int) ($data["damage"] ?? 0), (int) ($data["count"] ?? 1), $nbt !== "" ? (new LittleEndianNbtSerializer())->read($nbt)->getTag() : null ); } /** * Serializes the item to an NBT CompoundTag * * @param int $slot optional, the inventory slot of the item * * @return CompoundTag */ public function nbtSerialize(int $slot = -1) : CompoundTag{ $result = CompoundTag::create() ->setShort("id", $this->id) ->setByte("Count", Binary::signByte($this->count)) ->setShort("Damage", $this->getMeta()); if($this->hasNamedTag()){ $result->setTag("tag", clone $this->getNamedTag()); } if($slot !== -1){ $result->setByte("Slot", $slot); } return $result; } /** * Deserializes an Item from an NBT CompoundTag * * @param CompoundTag $tag * * @return Item */ public static function nbtDeserialize(CompoundTag $tag) : Item{ if(!$tag->hasTag("id") or !$tag->hasTag("Count")){ return ItemFactory::get(0); } $count = Binary::unsignByte($tag->getByte("Count")); $meta = $tag->getShort("Damage", 0); $idTag = $tag->getTag("id"); if($idTag instanceof ShortTag){ $item = ItemFactory::get($idTag->getValue(), $meta, $count); }elseif($idTag instanceof StringTag){ //PC item save format try{ $item = ItemFactory::fromString($idTag->getValue() . ":$meta"); }catch(\InvalidArgumentException $e){ //TODO: improve error handling return ItemFactory::air(); } $item->setCount($count); }else{ throw new \InvalidArgumentException("Item CompoundTag ID must be an instance of StringTag or ShortTag, " . get_class($idTag) . " given"); } $itemNBT = $tag->getCompoundTag("tag"); if($itemNBT !== null){ $item->setNamedTag(clone $itemNBT); } return $item; } public function __clone(){ if($this->nbt !== null){ $this->nbt = clone $this->nbt; } } }