diff --git a/src/inventory/BaseInventory.php b/src/inventory/BaseInventory.php index 5700923ac..e593893ce 100644 --- a/src/inventory/BaseInventory.php +++ b/src/inventory/BaseInventory.php @@ -55,6 +55,25 @@ abstract class BaseInventory implements Inventory{ return $this->maxStackSize; } + public function setMaxStackSize(int $size) : void{ + $this->maxStackSize = $size; + } + + abstract protected function internalSetItem(int $index, Item $item) : void; + + public function setItem(int $index, Item $item) : void{ + if($item->isNull()){ + $item = VanillaItems::AIR(); + }else{ + $item = clone $item; + } + + $oldItem = $this->getItem($index); + + $this->internalSetItem($index, $item); + $this->onSlotChange($index, $oldItem); + } + /** * @param Item[] $items */ @@ -85,21 +104,6 @@ abstract class BaseInventory implements Inventory{ $this->onContentChange($oldContents); } - abstract protected function internalSetItem(int $index, Item $item) : void; - - public function setItem(int $index, Item $item) : void{ - if($item->isNull()){ - $item = VanillaItems::AIR(); - }else{ - $item = clone $item; - } - - $oldItem = $this->getItem($index); - - $this->internalSetItem($index, $item); - $this->onSlotChange($index, $oldItem); - } - public function contains(Item $item) : bool{ $count = max(1, $item->getCount()); $checkDamage = !$item->hasAnyDamageValue(); @@ -128,18 +132,6 @@ abstract class BaseInventory implements Inventory{ return $slots; } - - public function remove(Item $item) : void{ - $checkDamage = !$item->hasAnyDamageValue(); - $checkTags = $item->hasNamedTag(); - - 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(); @@ -253,6 +245,17 @@ abstract class BaseInventory implements Inventory{ return $slot; } + public function remove(Item $item) : void{ + $checkDamage = !$item->hasAnyDamageValue(); + $checkTags = $item->hasNamedTag(); + + foreach($this->getContents() as $index => $i){ + if($item->equals($i, $checkDamage, $checkTags)){ + $this->clear($index); + } + } + } + public function removeItem(Item ...$slots) : array{ /** @var Item[] $itemSlots */ /** @var Item[] $slots */ @@ -323,10 +326,6 @@ abstract class BaseInventory implements Inventory{ } } - public function setMaxStackSize(int $size) : void{ - $this->maxStackSize = $size; - } - public function onOpen(Player $who) : void{ $this->viewers[spl_object_id($who)] = $who; } diff --git a/src/inventory/Inventory.php b/src/inventory/Inventory.php index 8eef250b1..cbb09ed24 100644 --- a/src/inventory/Inventory.php +++ b/src/inventory/Inventory.php @@ -46,6 +46,16 @@ interface Inventory{ */ public function setItem(int $index, Item $item) : void; + /** + * @return Item[] + */ + public function getContents(bool $includeEmpty = false) : array; + + /** + * @param Item[] $items + */ + public function setContents(array $items) : void; + /** * Stores the given Items in the inventory. This will try to fill * existing stacks and empty slots as well as it can. @@ -66,24 +76,6 @@ interface Inventory{ */ public function getAddableItemQuantity(Item $item) : int; - /** - * Removes the given Item from the inventory. - * It will return the Items that couldn't be removed. - * - * @return Item[] - */ - public function removeItem(Item ...$slots) : array; - - /** - * @return Item[] - */ - public function getContents(bool $includeEmpty = false) : array; - - /** - * @param Item[] $items - */ - public function setContents(array $items) : void; - /** * Checks if the inventory contains any Item with the same material data. * It will check id, amount, and metadata (if not null) @@ -121,6 +113,14 @@ interface Inventory{ */ public function remove(Item $item) : void; + /** + * Removes the given Item from the inventory. + * It will return the Items that couldn't be removed. + * + * @return Item[] + */ + public function removeItem(Item ...$slots) : array; + /** * Will clear a specific slot */ diff --git a/src/inventory/SimpleInventory.php b/src/inventory/SimpleInventory.php index 28b7d8b47..3f44a3994 100644 --- a/src/inventory/SimpleInventory.php +++ b/src/inventory/SimpleInventory.php @@ -52,6 +52,10 @@ class SimpleInventory extends BaseInventory{ return $this->slots[$index] !== null ? clone $this->slots[$index] : VanillaItems::AIR(); } + protected function internalSetItem(int $index, Item $item) : void{ + $this->slots[$index] = $item->isNull() ? null : $item; + } + /** * @return Item[] */ @@ -78,8 +82,4 @@ class SimpleInventory extends BaseInventory{ } } } - - protected function internalSetItem(int $index, Item $item) : void{ - $this->slots[$index] = $item->isNull() ? null : $item; - } } diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php index 6cd8e5019..dc7a9cc2e 100644 --- a/src/network/mcpe/InventoryManager.php +++ b/src/network/mcpe/InventoryManager.php @@ -59,7 +59,9 @@ use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\ObjectSet; use function array_map; use function array_search; +use function get_class; use function max; +use function spl_object_id; /** * @phpstan-type ContainerOpenClosure \Closure(int $id, Inventory $inventory) : (list|null) @@ -304,6 +306,28 @@ class InventoryManager{ } } + public function syncMismatchedPredictedSlotChanges() : void{ + foreach($this->initiatedSlotChanges as $windowId => $slots){ + if(!isset($this->windowMap[$windowId])){ + continue; + } + $inventory = $this->windowMap[$windowId]; + + foreach($slots as $slot => $expectedItem){ + if(!$inventory->slotExists($slot)){ + continue; //TODO: size desync ??? + } + $actualItem = $inventory->getItem($slot); + if(!$actualItem->equalsExact($expectedItem)){ + $this->session->getLogger()->debug("Detected prediction mismatch in inventory " . get_class($inventory) . "#" . spl_object_id($inventory) . " slot $slot"); + $this->syncSlot($inventory, $slot); + } + } + } + + $this->initiatedSlotChanges = []; + } + public function syncData(Inventory $inventory, int $propertyId, int $value) : void{ $windowId = $this->getWindowId($inventory); if($windowId !== null){ diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index f2e0eafb4..896ed7d00 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -234,9 +234,13 @@ class InGamePacketHandler extends PacketHandler{ $packetHandled = true; $useItemTransaction = $packet->getItemInteractionData(); - if($useItemTransaction !== null && !$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){ - $packetHandled = false; - $this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")"); + if($useItemTransaction !== null){ + if(!$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){ + $packetHandled = false; + $this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")"); + }else{ + $this->inventoryManager->syncMismatchedPredictedSlotChanges(); + } } $blockActions = $packet->getBlockActions(); @@ -304,6 +308,8 @@ class InGamePacketHandler extends PacketHandler{ if(!$result){ $this->inventoryManager->syncAll(); + }else{ + $this->inventoryManager->syncMismatchedPredictedSlotChanges(); } return $result; } diff --git a/src/player/Player.php b/src/player/Player.php index 2a0e4191f..3a05428be 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -313,6 +313,16 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $this->setNameTag($this->username); } + private function callDummyItemHeldEvent() : void{ + $slot = $this->inventory->getHeldItemIndex(); + + $event = new PlayerItemHeldEvent($this, $this->inventory->getItem($slot), $slot); + $event->call(); + //TODO: this event is actually cancellable, but cancelling it here has no meaningful result, so we + //just ignore it. We fire this only because the content of the held slot changed, not because the + //held slot index changed. We can't prevent that from here, and nor would it be sensible to. + } + protected function initEntity(CompoundTag $nbt) : void{ parent::initEntity($nbt); $this->addDefaultWindows(); @@ -321,10 +331,16 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ function(Inventory $unused, int $slot) : void{ if($slot === $this->inventory->getHeldItemIndex()){ $this->setUsingItem(false); + + $this->callDummyItemHeldEvent(); } }, - function() : void{ + function(Inventory $unused, array $changedSlots) : void{ $this->setUsingItem(false); + $heldSlot = $this->inventory->getHeldItemIndex(); + if(isset($changedSlots[$heldSlot])){ + $this->callDummyItemHeldEvent(); + } } ));