*/ protected $canPlaceOn = []; /** * @var string[] * @phpstan-var array */ protected $canDestroy; /** * Constructs a new Item type. This constructor should ONLY be used when constructing a new item TYPE to register * into the index. * * NOTE: This should NOT BE USED for creating items to set into an inventory. Use {@link ItemFactory#get} for that * purpose. */ public function __construct(ItemIdentifier $identifier, string $name = "Unknown"){ $this->identifier = $identifier; $this->name = $name; $this->canPlaceOn = []; $this->canDestroy = []; $this->nbt = new CompoundTag(); } public function hasCustomBlockData() : bool{ return $this->blockEntityTag !== null; } /** * @return $this */ public function clearCustomBlockData(){ $this->blockEntityTag = null; return $this; } /** * @return $this */ public function setCustomBlockData(CompoundTag $compound) : Item{ $this->blockEntityTag = clone $compound; return $this; } public function getCustomBlockData() : ?CompoundTag{ return $this->blockEntityTag; } public function hasCustomName() : bool{ return $this->customName !== ""; } public function getCustomName() : string{ return $this->customName; } /** * @return $this */ public function setCustomName(string $name) : Item{ Utils::checkUTF8($name); $this->customName = $name; return $this; } /** * @return $this */ public function clearCustomName() : Item{ $this->setCustomName(""); return $this; } /** * @return string[] */ public function getLore() : array{ return $this->lore; } /** * @param string[] $lines * * @return $this */ public function setLore(array $lines) : Item{ foreach($lines as $line){ if(!is_string($line)){ throw new \TypeError("Expected string[], but found " . gettype($line) . " in given array"); } Utils::checkUTF8($line); } $this->lore = $lines; return $this; } /** * @return string[] * @phpstan-return array */ public function getCanPlaceOn() : array{ return $this->canPlaceOn; } /** * @param string[] $canPlaceOn */ public function setCanPlaceOn(array $canPlaceOn) : void{ $this->canPlaceOn = []; foreach($canPlaceOn as $value){ $this->canPlaceOn[$value] = $value; } } /** * @return string[] * @phpstan-return array */ public function getCanDestroy() : array{ return $this->canDestroy; } /** * @param string[] $canDestroy */ public function setCanDestroy(array $canDestroy) : void{ $this->canDestroy = []; foreach($canDestroy as $value){ $this->canDestroy[$value] = $value; } } /** * Returns whether this Item has a non-empty NBT. */ public function hasNamedTag() : bool{ return $this->getNamedTag()->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. */ public function getNamedTag() : CompoundTag{ $this->serializeCompoundTag($this->nbt); return $this->nbt; } /** * Sets the Item's NBT from the supplied CompoundTag object. * * @return $this * @throws NbtException */ public function setNamedTag(CompoundTag $tag) : Item{ if($tag->getCount() === 0){ return $this->clearNamedTag(); } $this->nbt = clone $tag; $this->deserializeCompoundTag($this->nbt); return $this; } /** * Removes the Item's NBT. * @return $this * @throws NbtException */ public function clearNamedTag() : Item{ $this->nbt = new CompoundTag(); $this->deserializeCompoundTag($this->nbt); return $this; } /** * @throws NbtException */ protected function deserializeCompoundTag(CompoundTag $tag) : void{ $this->customName = ""; $this->lore = []; $display = $tag->getCompoundTag(self::TAG_DISPLAY); if($display !== null){ $this->customName = $display->getString(self::TAG_DISPLAY_NAME, $this->customName); $lore = $display->getListTag(self::TAG_DISPLAY_LORE); if($lore !== null && $lore->getTagType() === NBT::TAG_String){ /** @var StringTag $t */ foreach($lore as $t){ $this->lore[] = $t->getValue(); } } } $this->removeEnchantments(); $enchantments = $tag->getListTag(self::TAG_ENCH); if($enchantments !== null && $enchantments->getTagType() === NBT::TAG_Compound){ /** @var CompoundTag $enchantment */ foreach($enchantments as $enchantment){ $magicNumber = $enchantment->getShort("id", -1); $level = $enchantment->getShort("lvl", 0); if($level <= 0){ continue; } $type = EnchantmentIdMap::getInstance()->fromId($magicNumber); if($type !== null){ $this->addEnchantment(new EnchantmentInstance($type, $level)); } } } $this->blockEntityTag = $tag->getCompoundTag(self::TAG_BLOCK_ENTITY_TAG); $this->canPlaceOn = []; $canPlaceOn = $tag->getListTag("CanPlaceOn"); if($canPlaceOn !== null && $canPlaceOn->getTagType() === NBT::TAG_String){ /** @var StringTag $entry */ foreach($canPlaceOn as $entry){ $this->canPlaceOn[$entry->getValue()] = $entry->getValue(); } } $this->canDestroy = []; $canDestroy = $tag->getListTag("CanDestroy"); if($canDestroy !== null && $canDestroy->getTagType() === NBT::TAG_String){ /** @var StringTag $entry */ foreach($canDestroy as $entry){ $this->canDestroy[$entry->getValue()] = $entry->getValue(); } } } protected function serializeCompoundTag(CompoundTag $tag) : void{ $display = $tag->getCompoundTag(self::TAG_DISPLAY) ?? new CompoundTag(); $this->hasCustomName() ? $display->setString(self::TAG_DISPLAY_NAME, $this->getCustomName()) : $display->removeTag(self::TAG_DISPLAY_NAME); if(count($this->lore) > 0){ $loreTag = new ListTag(); foreach($this->lore as $line){ $loreTag->push(new StringTag($line)); } $display->setTag(self::TAG_DISPLAY_LORE, $loreTag); }else{ $display->removeTag(self::TAG_DISPLAY_LORE); } $display->count() > 0 ? $tag->setTag(self::TAG_DISPLAY, $display) : $tag->removeTag(self::TAG_DISPLAY); if($this->hasEnchantments()){ $ench = new ListTag(); foreach($this->getEnchantments() as $enchantmentInstance){ $ench->push(CompoundTag::create() ->setShort("id", EnchantmentIdMap::getInstance()->toId($enchantmentInstance->getType())) ->setShort("lvl", $enchantmentInstance->getLevel()) ); } $tag->setTag(self::TAG_ENCH, $ench); }else{ $tag->removeTag(self::TAG_ENCH); } ($blockData = $this->getCustomBlockData()) !== null ? $tag->setTag(self::TAG_BLOCK_ENTITY_TAG, clone $blockData) : $tag->removeTag(self::TAG_BLOCK_ENTITY_TAG); if(count($this->canPlaceOn) > 0){ $canPlaceOn = new ListTag(); foreach($this->canPlaceOn as $item){ $canPlaceOn->push(new StringTag($item)); } $tag->setTag("CanPlaceOn", $canPlaceOn); }else{ $tag->removeTag("CanPlaceOn"); } if(count($this->canDestroy) > 0){ $canDestroy = new ListTag(); foreach($this->canDestroy as $item){ $canDestroy->push(new StringTag($item)); } $tag->setTag("CanDestroy", $canDestroy); }else{ $tag->removeTag("CanDestroy"); } } public function getCount() : int{ return $this->count; } /** * @return $this */ 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. * * @return static A clone of this itemstack containing the amount of items that were removed from this stack. * @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 || $this->getId() === ItemIds::AIR; } /** * Returns the name of the item, or the custom name if it is set. */ final public function getName() : string{ return $this->hasCustomName() ? $this->getCustomName() : $this->getVanillaName(); } /** * Returns the vanilla name of the item, disregarding custom names. */ public function getVanillaName() : string{ return $this->name; } final public function canBePlaced() : bool{ return $this->getBlock()->canBePlaced(); } /** * Returns the block corresponding to this Item. */ public function getBlock(?int $clickedFace = null) : Block{ return VanillaBlocks::AIR(); } final public function getTypeId() : int{ //don't use Item::getMeta(), since it might be overridden for non-type information (e.g. durability) return ($this->identifier->getId() << 16) | $this->identifier->getMeta(); } public function getId() : int{ return $this->identifier->getId(); } public function getMeta() : int{ return $this->identifier->getMeta(); } /** * 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. */ public function hasAnyDamageValue() : bool{ return $this->identifier->getMeta() === -1; } /** * Returns the highest amount of this item which will fit into one inventory slot. */ public function getMaxStackSize() : int{ return 64; } /** * Returns the time in ticks which the item will fuel a furnace for. */ public function getFuelTime() : int{ return 0; } /** * Returns an item after burning fuel */ public function getFuelResidue() : Item{ $item = clone $this; $item->pop(); return $item; } /** * Returns how many points of damage this item will deal to an entity when used as a weapon. */ public function getAttackPoints() : int{ return 1; } /** * Returns how many armor points can be gained by wearing this item. */ 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) */ 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() */ public function getBlockToolHarvestLevel() : int{ return 0; } public function getMiningEfficiency(bool $isCorrectTool) : float{ return 1; } /** * Called when a player uses this item on a block. */ public function onInteractBlock(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. */ 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. */ public function onReleaseUsing(Player $player) : ItemUseResult{ return ItemUseResult::NONE(); } /** * Called when this item is used to destroy a block. Usually used to update durability. */ public function onDestroyBlock(Block $block) : bool{ return false; } /** * Called when this item is used to attack an entity. Usually used to update durability. */ public function onAttackEntity(Entity $victim) : bool{ return false; } /** * Returns the number of ticks a player must wait before activating this item again. */ public function getCooldownTicks() : int{ return 0; } /** * Compares an Item to this Item and check if they match. * * @param bool $checkDamage Whether to verify that the damage values match. * @param bool $checkCompound Whether to verify that the items' NBT match. */ final public function equals(Item $item, bool $checkDamage = true, bool $checkCompound = true) : bool{ return $this->getId() === $item->getId() && (!$checkDamage || $this->getMeta() === $item->getMeta()) && (!$checkCompound || $this->getNamedTag()->equals($item->getNamedTag())); } /** * Returns whether this item could stack with the given item (ignoring stack size and count). */ final public function canStackWith(Item $other) : bool{ return $this->equals($other, true, true); } /** * Returns whether the specified item stack has the same ID, damage, NBT and count as this item stack. */ final public function equalsExact(Item $other) : bool{ return $this->equals($other, true, true) && $this->count === $other->count; } final public function __toString() : string{ return "Item " . $this->name . " (" . $this->getId() . ":" . ($this->hasAnyDamageValue() ? "?" : $this->getMeta()) . ")x" . $this->count . ($this->hasNamedTag() ? " tags:0x" . base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($this->getNamedTag()))) : ""); } /** * Returns an array of item stack properties that can be serialized to json. * * @return mixed[] * @phpstan-return array{id: int, damage?: int, count?: int, nbt_b64?: string} */ 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 mixed[] $data * @phpstan-param array{ * id: int, * damage?: int, * count?: int, * nbt?: string, * nbt_hex?: string, * nbt_b64?: string * } $data * * @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::getInstance()->get( (int) $data["id"], (int) ($data["damage"] ?? 0), (int) ($data["count"] ?? 1), $nbt !== "" ? (new LittleEndianNbtSerializer())->read($nbt)->mustGetCompoundTag() : null ); } /** * Serializes the item to an NBT CompoundTag * * @param int $slot optional, the inventory slot of the item */ public function nbtSerialize(int $slot = -1) : CompoundTag{ $result = CompoundTag::create() ->setShort("id", $this->getId()) ->setByte("Count", Binary::signByte($this->count)) ->setShort("Damage", $this->getMeta()); $tag = $this->getNamedTag(); if($tag->count() > 0){ $result->setTag("tag", $tag); } if($slot !== -1){ $result->setByte("Slot", $slot); } return $result; } /** * Deserializes an Item from an NBT CompoundTag * @throws NbtException * @throws SavedDataLoadingException */ public static function nbtDeserialize(CompoundTag $tag) : Item{ if($tag->getTag("id") === null || $tag->getTag("Count") === null){ return VanillaItems::AIR(); } $count = Binary::unsignByte($tag->getByte("Count")); $meta = $tag->getShort("Damage", 0); $idTag = $tag->getTag("id"); if($idTag instanceof ShortTag){ $item = ItemFactory::getInstance()->get($idTag->getValue(), $meta, $count); }elseif($idTag instanceof StringTag){ //PC item save format try{ $item = LegacyStringToItemParser::getInstance()->parse($idTag->getValue() . ":$meta"); }catch(LegacyStringToItemParserException $e){ //TODO: improve error handling return VanillaItems::AIR(); } $item->setCount($count); }else{ throw new SavedDataLoadingException("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(){ $this->nbt = clone $this->nbt; if($this->blockEntityTag !== null){ $this->blockEntityTag = clone $this->blockEntityTag; } } }