diff --git a/src/inventory/transaction/InventoryTransaction.php b/src/inventory/transaction/InventoryTransaction.php index 47290e4015..8c0adc0b0f 100644 --- a/src/inventory/transaction/InventoryTransaction.php +++ b/src/inventory/transaction/InventoryTransaction.php @@ -25,12 +25,16 @@ use pocketmine\event\inventory\InventoryTransactionEvent; use pocketmine\inventory\Inventory; +use pocketmine\inventory\PlayerCursorInventory; +use pocketmine\inventory\PlayerInventory; use pocketmine\inventory\transaction\action\InventoryAction; use pocketmine\inventory\transaction\action\SlotChangeAction; use pocketmine\item\Item; +use pocketmine\item\ItemLockMode; use pocketmine\player\Player; use pocketmine\utils\Utils; use function array_keys; +use function array_push; use function array_values; use function assert; use function count; @@ -135,30 +139,14 @@ private function shuffleActions() : void{ /** * @param Item[] $needItems * @param Item[] $haveItems + * @phpstan-param list $needItems + * @phpstan-param list $haveItems * @phpstan-param-out list $needItems * @phpstan-param-out list $haveItems * * @throws TransactionValidationException */ - protected function matchItems(array &$needItems, array &$haveItems) : void{ - $needItems = []; - $haveItems = []; - foreach($this->actions as $key => $action){ - if(!$action->getTargetItem()->isNull()){ - $needItems[] = $action->getTargetItem(); - } - - try{ - $action->validate($this->source); - }catch(TransactionValidationException $e){ - throw new TransactionValidationException(get_class($action) . "#" . spl_object_id($action) . ": " . $e->getMessage(), 0, $e); - } - - if(!$action->getSourceItem()->isNull()){ - $haveItems[] = $action->getSourceItem(); - } - } - + public function computeDiff(array &$needItems, array &$haveItems) : void{ foreach($needItems as $i => $needItem){ foreach($haveItems as $j => $haveItem){ if($needItem->canStackWith($haveItem)){ @@ -179,6 +167,64 @@ protected function matchItems(array &$needItems, array &$haveItems) : void{ $haveItems = array_values($haveItems); } + /** + * @param Item[] $needItems + * @param Item[] $haveItems + * @phpstan-param-out list $needItems + * @phpstan-param-out list $haveItems + * + * @throws TransactionValidationException + */ + protected function matchItems(array &$needItems, array &$haveItems) : void{ + $needItems = []; + $haveItems = []; + + $boundLockedNeedItems = []; + $boundLockedHaveItems = []; + foreach($this->actions as $key => $action){ + $checkBoundLockedItem = $action instanceof SlotChangeAction && + (($inventory = $action->getInventory()) instanceof PlayerInventory || $inventory instanceof PlayerCursorInventory); + $targetItem = $action->getTargetItem(); + if(!$targetItem->isNull()){ + if($checkBoundLockedItem && $targetItem->getLockMode() !== ItemLockMode::NONE){ + $boundLockedNeedItems[] = $targetItem; + }else{ + $needItems[] = $targetItem; + } + } + + try{ + $action->validate($this->source); + }catch(TransactionValidationException $e){ + throw new TransactionValidationException(get_class($action) . "#" . spl_object_id($action) . ": " . $e->getMessage(), 0, $e); + } + + $sourceItem = $action->getSourceItem(); + if(!$sourceItem->isNull()){ + if($checkBoundLockedItem && $sourceItem->getLockMode() !== ItemLockMode::NONE){ + $boundLockedHaveItems[] = $sourceItem; + }else{ + $haveItems[] = $sourceItem; + } + } + } + + $this->computeDiff($needItems, $haveItems); + + if(count($boundLockedNeedItems) > 0 || count($boundLockedHaveItems) > 0){ + //try to balance bound locked items with items already bound to the player's inventory + $this->computeDiff($boundLockedNeedItems, $boundLockedHaveItems); + if(count($boundLockedHaveItems) > 0){ + throw new TransactionValidationException("Player tried to remove locked items from their inventory"); + } + + //check if there are unbound locked items the player moved into its inventory + //this allows moving locked items from e.g. a chest -> player's inventory, but not the other way round + $this->computeDiff($boundLockedNeedItems, $haveItems); + array_push($needItems, ...$boundLockedNeedItems); + } + } + /** * Iterates over SlotChangeActions in this transaction and compacts any which refer to the same slot in the same * inventory so they can be correctly handled. diff --git a/src/inventory/transaction/action/SlotChangeAction.php b/src/inventory/transaction/action/SlotChangeAction.php index 3c9b8e5cf7..44f39cbabe 100644 --- a/src/inventory/transaction/action/SlotChangeAction.php +++ b/src/inventory/transaction/action/SlotChangeAction.php @@ -24,9 +24,11 @@ namespace pocketmine\inventory\transaction\action; use pocketmine\inventory\Inventory; +use pocketmine\inventory\PlayerInventory; use pocketmine\inventory\SlotValidatedInventory; use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\item\Item; +use pocketmine\item\ItemLockMode; use pocketmine\player\Player; /** @@ -74,6 +76,9 @@ public function validate(Player $source) : void{ if($this->targetItem->getCount() > $this->inventory->getMaxStackSize()){ throw new TransactionValidationException("Target item exceeds inventory max stack size"); } + if($this->sourceItem->getLockMode() === ItemLockMode::PLAYER_INVENTORY_SLOT && $this->inventory instanceof PlayerInventory){ + throw new TransactionValidationException("Source item is locked in slot"); + } if($this->inventory instanceof SlotValidatedInventory && !$this->targetItem->isNull()){ foreach($this->inventory->getSlotValidators() as $validator){ $ret = $validator->validate($this->inventory, $this->targetItem, $this->inventorySlot); diff --git a/src/item/Item.php b/src/item/Item.php index 205f15e130..3840320f3c 100644 --- a/src/item/Item.php +++ b/src/item/Item.php @@ -66,12 +66,16 @@ class Item implements \JsonSerializable{ public const TAG_DISPLAY = "display"; public const TAG_BLOCK_ENTITY_TAG = "BlockEntityTag"; + public const TAG_ITEM_LOCK = "minecraft:item_lock"; public const TAG_DISPLAY_NAME = "Name"; public const TAG_DISPLAY_LORE = "Lore"; public const TAG_KEEP_ON_DEATH = "minecraft:keep_on_death"; + private const VALUE_ITEM_LOCK_IN_SLOT = 1; + private const VALUE_ITEM_LOCK_IN_INVENTORY = 2; + private const TAG_CAN_PLACE_ON = "CanPlaceOn"; //TAG_List private const TAG_CAN_DESTROY = "CanDestroy"; //TAG_List @@ -100,6 +104,8 @@ class Item implements \JsonSerializable{ protected bool $keepOnDeath = false; + protected ItemLockMode $lockMode = ItemLockMode::NONE; + /** * Constructs a new Item type. This constructor should ONLY be used when constructing a new item TYPE to register * into the index. @@ -227,6 +233,21 @@ public function setCanDestroy(array $canDestroy) : void{ } } + /** + * Returns how the movement of this item will be restricted when in a player's inventory. + */ + public function getLockMode() : ItemLockMode{ + return $this->lockMode; + } + + /** + * Sets how the movement of this item will be restricted when in a player's inventory. + */ + public function setLockMode(ItemLockMode $lockMode) : self{ + $this->lockMode = $lockMode; + return $this; + } + /** * Returns whether players will retain this item on death. If a non-player dies it will be excluded from the drops. */ @@ -338,6 +359,12 @@ protected function deserializeCompoundTag(CompoundTag $tag) : void{ } $this->keepOnDeath = $tag->getByte(self::TAG_KEEP_ON_DEATH, 0) !== 0; + + $this->lockMode = match($tag->getByte(self::TAG_ITEM_LOCK, 0)){ + self::VALUE_ITEM_LOCK_IN_SLOT => ItemLockMode::PLAYER_INVENTORY_SLOT, + self::VALUE_ITEM_LOCK_IN_INVENTORY => ItemLockMode::PLAYER_INVENTORY, + default => ItemLockMode::NONE + }; } protected function serializeCompoundTag(CompoundTag $tag) : void{ @@ -406,6 +433,14 @@ protected function serializeCompoundTag(CompoundTag $tag) : void{ }else{ $tag->removeTag(self::TAG_KEEP_ON_DEATH); } + if($this->lockMode !== ItemLockMode::NONE){ + $tag->setByte(self::TAG_ITEM_LOCK, match($this->lockMode){ + ItemLockMode::PLAYER_INVENTORY_SLOT => self::VALUE_ITEM_LOCK_IN_SLOT, + ItemLockMode::PLAYER_INVENTORY => self::VALUE_ITEM_LOCK_IN_INVENTORY, + }); + }else{ + $tag->removeTag(self::TAG_ITEM_LOCK); + } } public function getCount() : int{ diff --git a/src/item/ItemLockMode.php b/src/item/ItemLockMode.php new file mode 100644 index 0000000000..b67bcece97 --- /dev/null +++ b/src/item/ItemLockMode.php @@ -0,0 +1,44 @@ +