> */ private $initiatedSlotChanges = []; 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()); } 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)); } } 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(); $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 syncSelectedHotbarSlot() : void{ $this->session->sendDataPacket(MobEquipmentPacket::create( $this->player->getId(), TypeConverter::getInstance()->coreItemStackToNet($this->player->getInventory()->getItemInHand()), $this->player->getInventory()->getHeldItemIndex(), ContainerIds::INVENTORY )); } public function syncCreative() : void{ $items = []; $typeConverter = TypeConverter::getInstance(); if(!$this->player->isSpectator()){ //fill it for all gamemodes except spectator foreach(CreativeInventory::getInstance()->getAll() as $i => $item){ $items[$i] = $typeConverter->coreItemStackToNet($item); } } $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()))); } }