Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 65 additions & 19 deletions src/inventory/transaction/InventoryTransaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -135,30 +139,14 @@ private function shuffleActions() : void{
/**
* @param Item[] $needItems
* @param Item[] $haveItems
* @phpstan-param list<Item> $needItems
* @phpstan-param list<Item> $haveItems
* @phpstan-param-out list<Item> $needItems
* @phpstan-param-out list<Item> $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)){
Expand All @@ -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<Item> $needItems
* @phpstan-param-out list<Item> $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.
Expand Down
5 changes: 5 additions & 0 deletions src/inventory/transaction/action/SlotChangeAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);
Expand Down
35 changes: 35 additions & 0 deletions src/item/Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<TAG_String>
private const TAG_CAN_DESTROY = "CanDestroy"; //TAG_List<TAG_String>

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down
44 changes: 44 additions & 0 deletions src/item/ItemLockMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/

declare(strict_types=1);

namespace pocketmine\item;

/**
* Lock options for items in a player's inventory. These options are only respected when the items are in a player's
* inventory. They are ignored when the item is a chest or other container.
*/
enum ItemLockMode{
/**
* Unrestricted item movement (default)
*/
case NONE;
/**
* The item can be moved to any storage slot of the main inventory (including the cursor), but cannot be dropped,
* moved to a container, crafted with, or otherwise removed from the inventory.
*/
case PLAYER_INVENTORY;
/**
* Same as INVENTORY, but additionally prevents the item from being removed from its slot.
*/
case PLAYER_INVENTORY_SLOT;
}
Loading