> */ private $initiatedSlotChanges = []; /** @var int */ private $clientSelectedHotbarSlot = -1; public function __construct(Player $player, NetworkSession $session){ $this->player = $player; $this->session = $session; $this->add(ContainerIds::INVENTORY, $this->player->getInventory()); $this->add(ContainerIds::ARMOR, $this->player->getArmorInventory()); $this->add(ContainerIds::UI, $this->player->getCursorInventory()); $this->player->getInventory()->getHeldItemIndexChangeListeners()->add(function() : void{ $this->syncSelectedHotbarSlot(); }); } private function add(int $id, Inventory $inventory) : void{ $this->windowMap[$id] = $inventory; } private function remove(int $id) : void{ unset($this->windowMap[$id], $this->initiatedSlotChanges[$id]); } public function getWindowId(Inventory $inventory) : ?int{ return ($id = array_search($inventory, $this->windowMap, true)) !== false ? $id : null; } public function getCurrentWindowId() : int{ return $this->lastInventoryNetworkId; } public function getWindow(int $windowId) : ?Inventory{ return $this->windowMap[$windowId] ?? null; } public function onTransactionStart(InventoryTransaction $tx) : void{ foreach($tx->getActions() as $action){ if($action instanceof SlotChangeAction and ($windowId = $this->getWindowId($action->getInventory())) !== null){ //in some cases the inventory might not have a window ID, but still be referenced by a transaction (e.g. crafting grid changes), so we can't unconditionally record the change here or we might leak things $this->initiatedSlotChanges[$windowId][$action->getSlot()] = $action->getTargetItem(); } } } public function onCurrentWindowChange(Inventory $inventory) : void{ $this->onCurrentWindowRemove(); $this->add($this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % self::RESERVED_WINDOW_ID_RANGE_START), $inventory); $pk = $this->createContainerOpen($this->lastInventoryNetworkId, $inventory); if($pk !== null){ $this->session->sendDataPacket($pk); $this->syncContents($inventory); }else{ throw new \UnsupportedOperationException("Unsupported inventory type"); } } protected function createContainerOpen(int $id, Inventory $inv) : ?ContainerOpenPacket{ //TODO: allow plugins to inject this if($inv instanceof BlockInventory){ switch(true){ case $inv instanceof FurnaceInventory: //TODO: specialized furnace types return ContainerOpenPacket::blockInvVec3($id, WindowTypes::FURNACE, $inv->getHolder()); case $inv instanceof EnchantInventory: return ContainerOpenPacket::blockInvVec3($id, WindowTypes::ENCHANTMENT, $inv->getHolder()); case $inv instanceof BrewingStandInventory: return ContainerOpenPacket::blockInvVec3($id, WindowTypes::BREWING_STAND, $inv->getHolder()); case $inv instanceof AnvilInventory: return ContainerOpenPacket::blockInvVec3($id, WindowTypes::ANVIL, $inv->getHolder()); case $inv instanceof HopperInventory: return ContainerOpenPacket::blockInvVec3($id, WindowTypes::HOPPER, $inv->getHolder()); default: return ContainerOpenPacket::blockInvVec3($id, WindowTypes::CONTAINER, $inv->getHolder()); } } return null; } public function onCurrentWindowRemove() : void{ if(isset($this->windowMap[$this->lastInventoryNetworkId])){ $this->remove($this->lastInventoryNetworkId); $this->session->sendDataPacket(ContainerClosePacket::create($this->lastInventoryNetworkId, true)); } } public function onClientRemoveWindow(int $id) : void{ if($id >= self::RESERVED_WINDOW_ID_RANGE_START && $id <= self::RESERVED_WINDOW_ID_RANGE_END){ //TODO: HACK! crafting grid & main inventory currently use these fake IDs return; } if($id === $this->lastInventoryNetworkId){ $this->remove($id); $this->player->removeCurrentWindow(); }else{ $this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastInventoryNetworkId"); } } public function syncSlot(Inventory $inventory, int $slot) : void{ $windowId = $this->getWindowId($inventory); if($windowId !== null){ $currentItem = $inventory->getItem($slot); $clientSideItem = $this->initiatedSlotChanges[$windowId][$slot] ?? null; if($clientSideItem === null or !$clientSideItem->equalsExact($currentItem)){ $this->session->sendDataPacket(InventorySlotPacket::create($windowId, $slot, ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem)))); } unset($this->initiatedSlotChanges[$windowId][$slot]); } } public function syncContents(Inventory $inventory) : void{ $windowId = $this->getWindowId($inventory); if($windowId !== null){ unset($this->initiatedSlotChanges[$windowId]); $typeConverter = TypeConverter::getInstance(); if($windowId === ContainerIds::UI){ //TODO: HACK! //Since 1.13, cursor is now part of a larger "UI inventory", and sending contents for this larger inventory does //not work the way it's intended to. Even if it did, it would be necessary to send all 51 slots just to update //this one, which is just not worth it. //This workaround isn't great, but it's at least simple. $this->session->sendDataPacket(InventorySlotPacket::create( $windowId, 0, ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem(0))) )); }else{ $this->session->sendDataPacket(InventoryContentPacket::create($windowId, array_map(function(Item $itemStack) use ($typeConverter) : ItemStackWrapper{ return ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($itemStack)); }, $inventory->getContents(true)))); } } } public function syncAll() : void{ foreach($this->windowMap as $inventory){ $this->syncContents($inventory); } } public function syncData(Inventory $inventory, int $propertyId, int $value) : void{ $windowId = $this->getWindowId($inventory); if($windowId !== null){ $this->session->sendDataPacket(ContainerSetDataPacket::create($windowId, $propertyId, $value)); } } public function onClientSelectHotbarSlot(int $slot) : void{ $this->clientSelectedHotbarSlot = $slot; } public function syncSelectedHotbarSlot() : void{ $selected = $this->player->getInventory()->getHeldItemIndex(); if($selected !== $this->clientSelectedHotbarSlot){ $this->session->sendDataPacket(MobEquipmentPacket::create( $this->player->getId(), ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->player->getInventory()->getItemInHand())), $selected, ContainerIds::INVENTORY )); $this->clientSelectedHotbarSlot = $selected; } } public function syncCreative() : void{ $typeConverter = TypeConverter::getInstance(); $nextEntryId = 1; $this->session->sendDataPacket(CreativeContentPacket::create(array_map(function(Item $item) use($typeConverter, &$nextEntryId) : CreativeContentEntry{ return new CreativeContentEntry($nextEntryId++, $typeConverter->coreItemStackToNet($item)); }, $this->player->isSpectator() ? [] : CreativeInventory::getInstance()->getAll()))); } }