*/ protected $slots; /** @var Player[] */ protected $viewers = []; /** @var InventoryEventProcessor */ protected $eventProcessor; /** * @param Item[] $items */ public function __construct(array $items = [], int $size = null, string $title = null){ $this->slots = new \SplFixedArray($size ?? $this->getDefaultSize()); $this->title = $title ?? $this->getName(); $this->setContents($items, false); } abstract public function getName() : string; public function getTitle() : string{ return $this->title; } /** * Returns the size of the inventory. */ public function getSize() : int{ return $this->slots->getSize(); } /** * Sets the new size of the inventory. * WARNING: If the size is smaller, any items past the new size will be lost. * * @return void */ public function setSize(int $size){ $this->slots->setSize($size); } abstract public function getDefaultSize() : int; public function getMaxStackSize() : int{ return $this->maxStackSize; } public function getItem(int $index) : Item{ return $this->slots[$index] !== null ? clone $this->slots[$index] : ItemFactory::get(Item::AIR, 0, 0); } /** * @return Item[] */ public function getContents(bool $includeEmpty = false) : array{ $contents = []; $air = null; foreach($this->slots as $i => $slot){ if($slot !== null){ $contents[$i] = clone $slot; }elseif($includeEmpty){ $contents[$i] = $air ?? ($air = ItemFactory::get(Item::AIR, 0, 0)); } } return $contents; } /** * @param Item[] $items */ public function setContents(array $items, bool $send = true) : void{ if(count($items) > $this->getSize()){ $items = array_slice($items, 0, $this->getSize(), true); } for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ if(!isset($items[$i])){ if($this->slots[$i] !== null){ $this->clear($i, false); } }else{ if(!$this->setItem($i, $items[$i], false)){ $this->clear($i, false); } } } if($send){ $this->sendContents($this->getViewers()); } } /** * Drops the contents of the inventory into the specified Level at the specified position and clears the inventory * contents. */ public function dropContents(Level $level, Vector3 $position) : void{ foreach($this->getContents() as $item){ $level->dropItem($position, $item); } $this->clearAll(); } public function setItem(int $index, Item $item, bool $send = true) : bool{ if($item->isNull()){ $item = ItemFactory::get(Item::AIR, 0, 0); }else{ $item = clone $item; } $oldItem = $this->getItem($index); if($this->eventProcessor !== null){ $newItem = $this->eventProcessor->onSlotChange($this, $index, $oldItem, $item); if($newItem === null){ return false; } }else{ $newItem = $item; } $this->slots[$index] = $newItem->isNull() ? null : $newItem; $this->onSlotChange($index, $oldItem, $send); return true; } public function contains(Item $item) : bool{ $count = max(1, $item->getCount()); $checkDamage = !$item->hasAnyDamageValue(); $checkTags = $item->hasCompoundTag(); foreach($this->getContents() as $i){ if($item->equals($i, $checkDamage, $checkTags)){ $count -= $i->getCount(); if($count <= 0){ return true; } } } return false; } public function all(Item $item) : array{ $slots = []; $checkDamage = !$item->hasAnyDamageValue(); $checkTags = $item->hasCompoundTag(); foreach($this->getContents() as $index => $i){ if($item->equals($i, $checkDamage, $checkTags)){ $slots[$index] = $i; } } return $slots; } public function remove(Item $item) : void{ $checkDamage = !$item->hasAnyDamageValue(); $checkTags = $item->hasCompoundTag(); foreach($this->getContents() as $index => $i){ if($item->equals($i, $checkDamage, $checkTags)){ $this->clear($index); } } } public function first(Item $item, bool $exact = false) : int{ $count = $exact ? $item->getCount() : max(1, $item->getCount()); $checkDamage = $exact || !$item->hasAnyDamageValue(); $checkTags = $exact || $item->hasCompoundTag(); foreach($this->getContents() as $index => $i){ if($item->equals($i, $checkDamage, $checkTags) and ($i->getCount() === $count or (!$exact and $i->getCount() > $count))){ return $index; } } return -1; } public function firstEmpty() : int{ foreach($this->slots as $i => $slot){ if($slot === null or $slot->isNull()){ return $i; } } return -1; } public function isSlotEmpty(int $index) : bool{ return $this->slots[$index] === null or $this->slots[$index]->isNull(); } public function canAddItem(Item $item) : bool{ $count = $item->getCount(); for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ $slot = $this->getItem($i); if($item->equals($slot)){ if(($diff = $slot->getMaxStackSize() - $slot->getCount()) > 0){ $count -= $diff; } }elseif($slot->isNull()){ $count -= $this->getMaxStackSize(); } if($count <= 0){ return true; } } return false; } public function addItem(Item ...$slots) : array{ /** @var Item[] $itemSlots */ /** @var Item[] $slots */ $itemSlots = []; foreach($slots as $slot){ if(!$slot->isNull()){ $itemSlots[] = clone $slot; } } $emptySlots = []; for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ $item = $this->getItem($i); if($item->isNull()){ $emptySlots[] = $i; } foreach($itemSlots as $index => $slot){ if($slot->equals($item) and $item->getCount() < $item->getMaxStackSize()){ $amount = min($item->getMaxStackSize() - $item->getCount(), $slot->getCount(), $this->getMaxStackSize()); if($amount > 0){ $slot->setCount($slot->getCount() - $amount); $item->setCount($item->getCount() + $amount); $this->setItem($i, $item); if($slot->getCount() <= 0){ unset($itemSlots[$index]); } } } } if(count($itemSlots) === 0){ break; } } if(count($itemSlots) > 0 and count($emptySlots) > 0){ foreach($emptySlots as $slotIndex){ //This loop only gets the first item, then goes to the next empty slot foreach($itemSlots as $index => $slot){ $amount = min($slot->getMaxStackSize(), $slot->getCount(), $this->getMaxStackSize()); $slot->setCount($slot->getCount() - $amount); $item = clone $slot; $item->setCount($amount); $this->setItem($slotIndex, $item); if($slot->getCount() <= 0){ unset($itemSlots[$index]); } break; } } } return $itemSlots; } public function removeItem(Item ...$slots) : array{ /** @var Item[] $itemSlots */ /** @var Item[] $slots */ $itemSlots = []; foreach($slots as $slot){ if(!$slot->isNull()){ $itemSlots[] = clone $slot; } } for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ $item = $this->getItem($i); if($item->isNull()){ continue; } foreach($itemSlots as $index => $slot){ if($slot->equals($item, !$slot->hasAnyDamageValue(), $slot->hasCompoundTag())){ $amount = min($item->getCount(), $slot->getCount()); $slot->setCount($slot->getCount() - $amount); $item->setCount($item->getCount() - $amount); $this->setItem($i, $item); if($slot->getCount() <= 0){ unset($itemSlots[$index]); } } } if(count($itemSlots) === 0){ break; } } return $itemSlots; } public function clear(int $index, bool $send = true) : bool{ return $this->setItem($index, ItemFactory::get(Item::AIR, 0, 0), $send); } public function clearAll(bool $send = true) : void{ for($i = 0, $size = $this->getSize(); $i < $size; ++$i){ $this->clear($i, false); } if($send){ $this->sendContents($this->getViewers()); } } public function swap(int $slot1, int $slot2) : void{ $i1 = $this->getItem($slot1); $i2 = $this->getItem($slot2); $this->setItem($slot1, $i2); $this->setItem($slot2, $i1); } /** * @return Player[] */ public function getViewers() : array{ return $this->viewers; } /** * Removes the inventory window from all players currently viewing it. * * @param bool $force Force removal of permanent windows such as the player's own inventory. Used internally. */ public function removeAllViewers(bool $force = false) : void{ foreach($this->viewers as $hash => $viewer){ $viewer->removeWindow($this, $force); unset($this->viewers[$hash]); } } public function setMaxStackSize(int $size) : void{ $this->maxStackSize = $size; } public function open(Player $who) : bool{ $ev = new InventoryOpenEvent($this, $who); $ev->call(); if($ev->isCancelled()){ return false; } $this->onOpen($who); return true; } public function close(Player $who) : void{ $this->onClose($who); } public function onOpen(Player $who) : void{ $this->viewers[spl_object_hash($who)] = $who; } public function onClose(Player $who) : void{ unset($this->viewers[spl_object_hash($who)]); } public function onSlotChange(int $index, Item $before, bool $send) : void{ if($send){ $this->sendSlot($index, $this->getViewers()); } } /** * @param Player|Player[] $target */ public function sendContents($target) : void{ if($target instanceof Player){ $target = [$target]; } $pk = new InventoryContentPacket(); $pk->items = $this->getContents(true); foreach($target as $player){ if(($id = $player->getWindowId($this)) === ContainerIds::NONE){ $this->close($player); continue; } $pk->windowId = $id; $player->dataPacket($pk); } } /** * @param Player|Player[] $target */ public function sendSlot(int $index, $target) : void{ if($target instanceof Player){ $target = [$target]; } $pk = new InventorySlotPacket(); $pk->inventorySlot = $index; $pk->item = $this->getItem($index); foreach($target as $player){ if(($id = $player->getWindowId($this)) === ContainerIds::NONE){ $this->close($player); continue; } $pk->windowId = $id; $player->dataPacket($pk); } } public function slotExists(int $slot) : bool{ return $slot >= 0 and $slot < $this->slots->getSize(); } public function getEventProcessor() : ?InventoryEventProcessor{ return $this->eventProcessor; } public function setEventProcessor(?InventoryEventProcessor $eventProcessor) : void{ $this->eventProcessor = $eventProcessor; } }