mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-04 17:06:16 +00:00
Separate inventory holder info from container & player inventories (#6533)
This PR breaks the cyclic dependency between `Inventory` and its holder, which unblocks a lot of new developments. ### Related issues & PRs - Fixes #5033 - Removes a blocker for #6147 (which in turn means that async tasks will eventually be able to work with tiles) - Removes a blocker for #2684 ## Changes ### API changes - `Player->getCurrentWindow()` now returns `?InventoryWindow` instead of `?Inventory` - `Player->setCurrentWindow()` now accepts `?InventoryWindow` instead of `?Inventory` - `InventoryWindow` introduced, which is created for each player viewing the inventory, provides decorative information like holder info for `InventoryTransactionEvent`, and is destroyed when the window is closed, eliminating cyclic references - Added: - `player\InventoryWindow` - `player\PlayerInventoryWindow` - wraps all permanent inventories of Player with type info for transactions - `inventory\Hotbar` - replaces all hotbar usages in `PlayerInventory` - `Human->getHotbar()` - `Human->getMainHandItem()`, `Human->setMainHandItem()`, `Human->getOffHandItem()`, `Human->setOffHandItem()` - `block\utils\AnimatedContainerLike` & `block\utils\AnimatedContainerLikeTrait` (for chests, shulkerboxes, etc) - `block\utils\Container` & `block\utils\ContainerTrait` for blocks containing items (chests, etc) - `block\utils\MenuAccessor` implemented by all blocks that can open inventory menus - `block\utils\MenuAccessorTrait` used by blocks with menus but without inventories (anvils, crafting tables etc) - Removed: - `inventory\DelegateInventory` (only used for ender chests) - `inventory\PlayerInventory`, - `inventory\PlayerOffHandInventory`, - `inventory\PlayerCraftingInventory`, - `inventory\PlayerCursorInventory` - these have all been internally replaced by `SimpleInventory` & they will appear as `PlayerInventoryWindow` in transactions (check `getType()` against the `PlayerInventoryWindow::TYPE_*` constants to identify them) - `block\inventory\AnimatedBlockInventoryTrait`, (blocks now handle this logic directly using `AnimatedContainer` and `AnimatedContainerTrait`) - `block\inventory\BlockInventoryTrait`, - `block\inventory\BlockInventory` - Most `BlockInventory` classes have been transitioned to `InventoryWindow` wrappers - Tiles now all use `SimpleInventory` internally (no cyclic references) except for `Chest` (which uses `CombinedInventory`, without holder info) - `InventoryOpenEvent` and `InventoryCloseEvent` now provide `InventoryWindow` instead of `Inventory` (to provide type information) - `InventoryTransaction` and `SlotChangeAction` now provide `InventoryWindow` instead of `Inventory` - Renamed `TransactionBuilderInventory` to `SlotChangeActionBuilder` - `TransactionBuilderInventory->getBuilder()` now accepts `InventoryWindow` instead of `Inventory` - `DoubleChestInventory` superseded by `CombinedInventory` - this new class allows combining any number of inventories behind a single object; mainly used for double chests but plugins could use it to do lots of fun things ### Impacts to plugins Plugins can now do the following: ```php $block = $world->getBlockAt($x, $y, $z); if($block instanceof MenuAccessor){ $block->openToUnchecked($player); } ``` As compared to the old way: ```php $tile = $world->getTileAt($x, $y, $z); if($tile instanceof Container){ $player->setCurrentWindow($tile->getInventory()); } ``` #### Advantages - No tile access needed - Works for menu blocks without inventories as well as container blocks - Less code ### Behavioural changes Inventories no longer keep permanent cyclic references to their holders. ## Backwards compatibility This makes significant BC breaks. However, all changes are able to be adapted to and the same amount of information is present on all APIs and events. ## Follow-up - Implement #6147 - Support inventory inheritance when copying blocks from one position to another
This commit is contained in:
@ -23,12 +23,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\AnvilInventory;
|
||||
use pocketmine\block\inventory\window\AnvilInventoryWindow;
|
||||
use pocketmine\block\utils\Fallable;
|
||||
use pocketmine\block\utils\FallableTrait;
|
||||
use pocketmine\block\utils\HorizontalFacing;
|
||||
use pocketmine\block\utils\HorizontalFacingOption;
|
||||
use pocketmine\block\utils\HorizontalFacingTrait;
|
||||
use pocketmine\block\utils\MenuAccessor;
|
||||
use pocketmine\block\utils\MenuAccessorTrait;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
use pocketmine\data\runtime\RuntimeDataDescriber;
|
||||
use pocketmine\entity\object\FallingBlock;
|
||||
@ -39,13 +41,15 @@ use pocketmine\math\Vector3;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\BlockTransaction;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\AnvilFallSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
use function round;
|
||||
|
||||
class Anvil extends Transparent implements Fallable, HorizontalFacing{
|
||||
class Anvil extends Transparent implements Fallable, HorizontalFacing, MenuAccessor{
|
||||
use FallableTrait;
|
||||
use HorizontalFacingTrait;
|
||||
use MenuAccessorTrait;
|
||||
|
||||
public const UNDAMAGED = 0;
|
||||
public const SLIGHTLY_DAMAGED = 1;
|
||||
@ -80,12 +84,8 @@ class Anvil extends Transparent implements Fallable, HorizontalFacing{
|
||||
return SupportType::NONE;
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player){
|
||||
$player->setCurrentWindow(new AnvilInventory($this->position));
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Position $position) : AnvilInventoryWindow{
|
||||
return new AnvilInventoryWindow($player, $position);
|
||||
}
|
||||
|
||||
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, Facing $face, Vector3 $clickVector, ?Player $player = null) : bool{
|
||||
|
@ -23,19 +23,28 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\tile\Barrel as TileBarrel;
|
||||
use pocketmine\block\utils\AnimatedContainerLike;
|
||||
use pocketmine\block\utils\AnimatedContainerLikeTrait;
|
||||
use pocketmine\block\utils\AnyFacing;
|
||||
use pocketmine\block\utils\AnyFacingTrait;
|
||||
use pocketmine\block\utils\Container;
|
||||
use pocketmine\block\utils\ContainerTrait;
|
||||
use pocketmine\data\runtime\RuntimeDataDescriber;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\BlockTransaction;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\BarrelCloseSound;
|
||||
use pocketmine\world\sound\BarrelOpenSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
use function abs;
|
||||
|
||||
class Barrel extends Opaque implements AnyFacing{
|
||||
class Barrel extends Opaque implements AnimatedContainerLike, AnyFacing, Container{
|
||||
use AnimatedContainerLikeTrait;
|
||||
use AnyFacingTrait;
|
||||
use ContainerTrait;
|
||||
|
||||
protected bool $open = false;
|
||||
|
||||
@ -74,22 +83,23 @@ class Barrel extends Opaque implements AnyFacing{
|
||||
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player){
|
||||
$barrel = $this->position->getWorld()->getTile($this->position);
|
||||
if($barrel instanceof TileBarrel){
|
||||
if(!$barrel->canOpenWith($item->getCustomName())){
|
||||
return true;
|
||||
}
|
||||
|
||||
$player->setCurrentWindow($barrel->getInventory());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFuelTime() : int{
|
||||
return 300;
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{
|
||||
return new BarrelOpenSound();
|
||||
}
|
||||
|
||||
protected function getCloseSound() : Sound{
|
||||
return new BarrelCloseSound();
|
||||
}
|
||||
|
||||
protected function playAnimationVisual(Position $position, bool $isOpen) : void{
|
||||
$world = $position->getWorld();
|
||||
$block = $world->getBlock($position);
|
||||
if($block instanceof Barrel){
|
||||
$world->setBlock($position, $block->setOpen($isOpen));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,20 +23,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\window\BrewingStandInventoryWindow;
|
||||
use pocketmine\block\tile\BrewingStand as TileBrewingStand;
|
||||
use pocketmine\block\utils\BrewingStandSlot;
|
||||
use pocketmine\block\utils\Container;
|
||||
use pocketmine\block\utils\ContainerTrait;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
use pocketmine\data\runtime\RuntimeDataDescriber;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\math\Axis;
|
||||
use pocketmine\math\AxisAlignedBB;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
use function array_key_exists;
|
||||
use function spl_object_id;
|
||||
|
||||
class BrewingStand extends Transparent{
|
||||
class BrewingStand extends Transparent implements Container{
|
||||
use ContainerTrait;
|
||||
|
||||
/**
|
||||
* @var BrewingStandSlot[]
|
||||
@ -95,15 +99,8 @@ class BrewingStand extends Transparent{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player){
|
||||
$stand = $this->position->getWorld()->getTile($this->position);
|
||||
if($stand instanceof TileBrewingStand && $stand->canOpenWith($item->getCustomName())){
|
||||
$player->setCurrentWindow($stand->getInventory());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Inventory $inventory, Position $position) : BrewingStandInventoryWindow{
|
||||
return new BrewingStandInventoryWindow($player, $inventory, $position);
|
||||
}
|
||||
|
||||
public function onScheduledUpdate() : void{
|
||||
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\CampfireInventory;
|
||||
use pocketmine\block\tile\Campfire as TileCampfire;
|
||||
use pocketmine\block\utils\HorizontalFacing;
|
||||
use pocketmine\block\utils\HorizontalFacingOption;
|
||||
@ -41,6 +40,7 @@ use pocketmine\entity\projectile\SplashPotion;
|
||||
use pocketmine\event\block\CampfireCookEvent;
|
||||
use pocketmine\event\entity\EntityDamageByBlockEvent;
|
||||
use pocketmine\event\entity\EntityDamageEvent;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\item\Durable;
|
||||
use pocketmine\item\enchantment\VanillaEnchantments;
|
||||
use pocketmine\item\Item;
|
||||
@ -76,7 +76,7 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
|
||||
* @deprecated This was added by mistake. It can't be relied on as the inventory won't be initialized if this block
|
||||
* has never been set in the world.
|
||||
*/
|
||||
protected CampfireInventory $inventory;
|
||||
protected ?Inventory $inventory = null;
|
||||
|
||||
/**
|
||||
* @var int[] slot => ticks
|
||||
@ -96,7 +96,8 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
|
||||
$this->inventory = $tile->getInventory();
|
||||
$this->cookingTimes = $tile->getCookingTimes();
|
||||
}else{
|
||||
$this->inventory = new CampfireInventory($this->position);
|
||||
$this->inventory = null;
|
||||
$this->cookingTimes = [];
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -140,7 +141,7 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
|
||||
* @deprecated This was added by mistake. It can't be relied on as the inventory won't be initialized if this block
|
||||
* has never been set in the world.
|
||||
*/
|
||||
public function getInventory() : CampfireInventory{
|
||||
public function getInventory() : ?Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
@ -203,10 +204,11 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
|
||||
return true;
|
||||
}
|
||||
|
||||
if($this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($this->getFurnaceType())->match($item) !== null){
|
||||
$inventory = $this->inventory;
|
||||
if($inventory !== null && $this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($this->getFurnaceType())->match($item) !== null){
|
||||
$ingredient = clone $item;
|
||||
$ingredient->setCount(1);
|
||||
if(count($this->inventory->addItem($ingredient)) === 0){
|
||||
if(count($inventory->addItem($ingredient)) === 0){
|
||||
$item->pop();
|
||||
$this->position->getWorld()->addSound($this->position, new ItemFrameAddItemSound());
|
||||
return true;
|
||||
@ -241,8 +243,8 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
|
||||
}
|
||||
|
||||
public function onScheduledUpdate() : void{
|
||||
if($this->lit){
|
||||
$items = $this->inventory->getContents();
|
||||
if($this->lit && ($inventory = $this->inventory) !== null){
|
||||
$items = $inventory->getContents();
|
||||
$furnaceType = $this->getFurnaceType();
|
||||
$maxCookDuration = $furnaceType->getCookDurationTicks();
|
||||
foreach($items as $slot => $item){
|
||||
@ -260,7 +262,7 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->inventory->setItem($slot, VanillaItems::AIR());
|
||||
$inventory->setItem($slot, VanillaItems::AIR());
|
||||
$this->setCookingTime($slot, 0);
|
||||
$this->position->getWorld()->dropItem($this->position->add(0.5, 1, 0.5), $ev->getResult());
|
||||
}
|
||||
|
@ -23,20 +23,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\CartographyTableInventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\block\inventory\window\CartographyTableInventoryWindow;
|
||||
use pocketmine\block\utils\MenuAccessor;
|
||||
use pocketmine\block\utils\MenuAccessorTrait;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
final class CartographyTable extends Opaque{
|
||||
final class CartographyTable extends Opaque implements MenuAccessor{
|
||||
use MenuAccessorTrait;
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player !== null){
|
||||
$player->setCurrentWindow(new CartographyTableInventory($this->position));
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Position $position) : CartographyTableInventoryWindow{
|
||||
return new CartographyTableInventoryWindow($player, $position);
|
||||
}
|
||||
|
||||
public function getFuelTime() : int{
|
||||
|
@ -23,18 +23,32 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\window\BlockInventoryWindow;
|
||||
use pocketmine\block\inventory\window\DoubleChestInventoryWindow;
|
||||
use pocketmine\block\tile\Chest as TileChest;
|
||||
use pocketmine\block\utils\AnimatedContainerLike;
|
||||
use pocketmine\block\utils\AnimatedContainerLikeTrait;
|
||||
use pocketmine\block\utils\Container;
|
||||
use pocketmine\block\utils\ContainerTrait;
|
||||
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
|
||||
use pocketmine\block\utils\HorizontalFacing;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
use pocketmine\event\block\ChestPairEvent;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\math\AxisAlignedBB;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\BlockEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\ChestCloseSound;
|
||||
use pocketmine\world\sound\ChestOpenSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
|
||||
class Chest extends Transparent implements HorizontalFacing{
|
||||
class Chest extends Transparent implements AnimatedContainerLike, Container, HorizontalFacing{
|
||||
use AnimatedContainerLikeTrait;
|
||||
use ContainerTrait;
|
||||
use FacesOppositePlacingPlayerTrait;
|
||||
|
||||
protected function recalculateCollisionBoxes() : array{
|
||||
@ -46,6 +60,25 @@ class Chest extends Transparent implements HorizontalFacing{
|
||||
return SupportType::NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{bool, TileChest}|null
|
||||
*/
|
||||
private function locatePair(Position $position) : ?array{
|
||||
$world = $position->getWorld();
|
||||
$tile = $world->getTile($position);
|
||||
if($tile instanceof TileChest){
|
||||
foreach([false, true] as $clockwise){
|
||||
$side = Facing::rotateY($this->facing->toFacing(), $clockwise);
|
||||
$c = $position->getSide($side);
|
||||
$pair = $world->getTile($c);
|
||||
if($pair instanceof TileChest && $pair->isPaired() && $pair->getPair() === $tile){
|
||||
return [$clockwise, $pair];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onPostPlace() : void{
|
||||
$world = $this->position->getWorld();
|
||||
$tile = $world->getTile($this->position);
|
||||
@ -70,27 +103,54 @@ class Chest extends Transparent implements HorizontalFacing{
|
||||
}
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player){
|
||||
|
||||
$chest = $this->position->getWorld()->getTile($this->position);
|
||||
if($chest instanceof TileChest){
|
||||
if(
|
||||
!$this->getSide(Facing::UP)->isTransparent() ||
|
||||
(($pair = $chest->getPair()) !== null && !$pair->getBlock()->getSide(Facing::UP)->isTransparent()) ||
|
||||
!$chest->canOpenWith($item->getCustomName())
|
||||
){
|
||||
return true;
|
||||
}
|
||||
|
||||
$player->setCurrentWindow($chest->getInventory());
|
||||
}
|
||||
public function isOpeningObstructed() : bool{
|
||||
if(!$this->getSide(Facing::UP)->isTransparent()){
|
||||
return true;
|
||||
}
|
||||
[, $pair] = $this->locatePair($this->position) ?? [false, null];
|
||||
return $pair !== null && !$pair->getBlock()->getSide(Facing::UP)->isTransparent();
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
|
||||
[$pairOnLeft, $pair] = $this->locatePair($position) ?? [false, null];
|
||||
if($pair === null){
|
||||
return new BlockInventoryWindow($player, $inventory, $position);
|
||||
}
|
||||
[$left, $right] = $pairOnLeft ? [$pair->getPosition(), $position] : [$position, $pair->getPosition()];
|
||||
|
||||
//TODO: we should probably construct DoubleChestInventory here directly too using the same logic
|
||||
//right now it uses some weird logic in TileChest which produces incorrect results
|
||||
//however I'm not sure if this is currently possible
|
||||
return new DoubleChestInventoryWindow($player, $inventory, $left, $right);
|
||||
}
|
||||
|
||||
public function getFuelTime() : int{
|
||||
return 300;
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{
|
||||
return new ChestOpenSound();
|
||||
}
|
||||
|
||||
protected function getCloseSound() : Sound{
|
||||
return new ChestCloseSound();
|
||||
}
|
||||
|
||||
protected function playAnimationVisual(Position $position, bool $isOpen) : void{
|
||||
//event ID is always 1 for a chest
|
||||
//TODO: we probably shouldn't be sending a packet directly here, but it doesn't fit anywhere into existing systems
|
||||
$position->getWorld()->broadcastPacketToViewers($position, BlockEventPacket::create(BlockPosition::fromVector3($position), 1, $isOpen ? 1 : 0));
|
||||
}
|
||||
|
||||
protected function doAnimationEffects(bool $isOpen) : void{
|
||||
$this->playAnimationVisual($this->position, $isOpen);
|
||||
$this->playAnimationSound($this->position, $isOpen);
|
||||
|
||||
$pairInfo = $this->locatePair($this->position);
|
||||
if($pairInfo !== null){
|
||||
[, $pair] = $pairInfo;
|
||||
$this->playAnimationVisual($pair->getPosition(), $isOpen);
|
||||
$this->playAnimationSound($pair->getPosition(), $isOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,20 +23,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\CraftingTableInventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\block\inventory\window\CraftingTableInventoryWindow;
|
||||
use pocketmine\block\utils\MenuAccessor;
|
||||
use pocketmine\block\utils\MenuAccessorTrait;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class CraftingTable extends Opaque{
|
||||
class CraftingTable extends Opaque implements MenuAccessor{
|
||||
use MenuAccessorTrait;
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player){
|
||||
$player->setCurrentWindow(new CraftingTableInventory($this->position));
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Position $position) : CraftingTableInventoryWindow{
|
||||
return new CraftingTableInventoryWindow($player, $position);
|
||||
}
|
||||
|
||||
public function getFuelTime() : int{
|
||||
|
@ -23,15 +23,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\EnchantInventory;
|
||||
use pocketmine\block\inventory\window\EnchantingTableInventoryWindow;
|
||||
use pocketmine\block\utils\MenuAccessor;
|
||||
use pocketmine\block\utils\MenuAccessorTrait;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\AxisAlignedBB;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class EnchantingTable extends Transparent{
|
||||
class EnchantingTable extends Transparent implements MenuAccessor{
|
||||
use MenuAccessorTrait;
|
||||
|
||||
protected function recalculateCollisionBoxes() : array{
|
||||
return [AxisAlignedBB::one()->trimmedCopy(Facing::UP, 0.25)];
|
||||
@ -41,13 +43,7 @@ class EnchantingTable extends Transparent{
|
||||
return SupportType::NONE;
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player){
|
||||
//TODO lock
|
||||
|
||||
$player->setCurrentWindow(new EnchantInventory($this->position));
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Position $position) : EnchantingTableInventoryWindow{
|
||||
return new EnchantingTableInventoryWindow($player, $position);
|
||||
}
|
||||
}
|
||||
|
@ -23,18 +23,31 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\EnderChestInventory;
|
||||
use pocketmine\block\inventory\window\BlockInventoryWindow;
|
||||
use pocketmine\block\tile\EnderChest as TileEnderChest;
|
||||
use pocketmine\block\utils\AnimatedContainerLike;
|
||||
use pocketmine\block\utils\AnimatedContainerLikeTrait;
|
||||
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
|
||||
use pocketmine\block\utils\HorizontalFacing;
|
||||
use pocketmine\block\utils\MenuAccessorTrait;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\AxisAlignedBB;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\BlockEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\EnderChestCloseSound;
|
||||
use pocketmine\world\sound\EnderChestOpenSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
|
||||
class EnderChest extends Transparent implements HorizontalFacing{
|
||||
class EnderChest extends Transparent implements AnimatedContainerLike, HorizontalFacing{
|
||||
use AnimatedContainerLikeTrait {
|
||||
onViewerAdded as private traitOnViewerAdded;
|
||||
onViewerRemoved as private traitOnViewerRemoved;
|
||||
}
|
||||
use MenuAccessorTrait;
|
||||
use FacesOppositePlacingPlayerTrait;
|
||||
|
||||
public function getLightLevel() : int{
|
||||
@ -50,16 +63,12 @@ class EnderChest extends Transparent implements HorizontalFacing{
|
||||
return SupportType::NONE;
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player){
|
||||
$enderChest = $this->position->getWorld()->getTile($this->position);
|
||||
if($enderChest instanceof TileEnderChest && $this->getSide(Facing::UP)->isTransparent()){
|
||||
$enderChest->setViewerCount($enderChest->getViewerCount() + 1);
|
||||
$player->setCurrentWindow(new EnderChestInventory($this->position, $player->getEnderInventory()));
|
||||
}
|
||||
}
|
||||
public function isOpeningObstructed() : bool{
|
||||
return !$this->getSide(Facing::UP)->isTransparent();
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Position $position) : BlockInventoryWindow{
|
||||
return new BlockInventoryWindow($player, $player->getEnderInventory(), $position);
|
||||
}
|
||||
|
||||
public function getDropsForCompatibleTool(Item $item) : array{
|
||||
@ -71,4 +80,43 @@ class EnderChest extends Transparent implements HorizontalFacing{
|
||||
public function isAffectedBySilkTouch() : bool{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getViewerCount() : int{
|
||||
$enderChest = $this->position->getWorld()->getTile($this->position);
|
||||
if(!$enderChest instanceof TileEnderChest){
|
||||
return 0;
|
||||
}
|
||||
return $enderChest->getViewerCount();
|
||||
}
|
||||
|
||||
private function updateViewerCount(int $amount) : void{
|
||||
$enderChest = $this->position->getWorld()->getTile($this->position);
|
||||
if($enderChest instanceof TileEnderChest){
|
||||
$enderChest->setViewerCount($enderChest->getViewerCount() + $amount);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{
|
||||
return new EnderChestOpenSound();
|
||||
}
|
||||
|
||||
protected function getCloseSound() : Sound{
|
||||
return new EnderChestCloseSound();
|
||||
}
|
||||
|
||||
protected function playAnimationVisual(Position $position, bool $isOpen) : void{
|
||||
//event ID is always 1 for a chest
|
||||
//TODO: we probably shouldn't be sending a packet directly here, but it doesn't fit anywhere into existing systems
|
||||
$position->getWorld()->broadcastPacketToViewers($position, BlockEventPacket::create(BlockPosition::fromVector3($position), 1, $isOpen ? 1 : 0));
|
||||
}
|
||||
|
||||
public function onViewerAdded() : void{
|
||||
$this->updateViewerCount(1);
|
||||
$this->traitOnViewerAdded();
|
||||
}
|
||||
|
||||
public function onViewerRemoved() : void{
|
||||
$this->traitOnViewerRemoved();
|
||||
$this->updateViewerCount(-1);
|
||||
}
|
||||
}
|
||||
|
@ -23,20 +23,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\window\FurnaceInventoryWindow;
|
||||
use pocketmine\block\tile\Furnace as TileFurnace;
|
||||
use pocketmine\block\utils\Container;
|
||||
use pocketmine\block\utils\ContainerTrait;
|
||||
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
|
||||
use pocketmine\block\utils\HorizontalFacing;
|
||||
use pocketmine\block\utils\Lightable;
|
||||
use pocketmine\block\utils\LightableTrait;
|
||||
use pocketmine\crafting\FurnaceType;
|
||||
use pocketmine\data\runtime\RuntimeDataDescriber;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
use function mt_rand;
|
||||
|
||||
class Furnace extends Opaque implements Lightable, HorizontalFacing{
|
||||
class Furnace extends Opaque implements Container, Lightable, HorizontalFacing{
|
||||
use ContainerTrait;
|
||||
use FacesOppositePlacingPlayerTrait;
|
||||
use LightableTrait;
|
||||
|
||||
@ -60,15 +64,8 @@ class Furnace extends Opaque implements Lightable, HorizontalFacing{
|
||||
return $this->lit ? 13 : 0;
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player){
|
||||
$furnace = $this->position->getWorld()->getTile($this->position);
|
||||
if($furnace instanceof TileFurnace && $furnace->canOpenWith($item->getCustomName())){
|
||||
$player->setCurrentWindow($furnace->getInventory());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
|
||||
return new FurnaceInventoryWindow($player, $inventory, $position, $this->furnaceType);
|
||||
}
|
||||
|
||||
public function onScheduledUpdate() : void{
|
||||
|
@ -23,19 +23,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\tile\Hopper as TileHopper;
|
||||
use pocketmine\block\inventory\window\HopperInventoryWindow;
|
||||
use pocketmine\block\utils\Container;
|
||||
use pocketmine\block\utils\ContainerTrait;
|
||||
use pocketmine\block\utils\PoweredByRedstone;
|
||||
use pocketmine\block\utils\PoweredByRedstoneTrait;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
use pocketmine\data\runtime\RuntimeDataDescriber;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\AxisAlignedBB;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\BlockTransaction;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class Hopper extends Transparent implements PoweredByRedstone{
|
||||
class Hopper extends Transparent implements Container, PoweredByRedstone{
|
||||
use ContainerTrait;
|
||||
use PoweredByRedstoneTrait;
|
||||
|
||||
private Facing $facing = Facing::DOWN;
|
||||
@ -81,15 +87,8 @@ class Hopper extends Transparent implements PoweredByRedstone{
|
||||
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player !== null){
|
||||
$tile = $this->position->getWorld()->getTile($this->position);
|
||||
if($tile instanceof TileHopper){ //TODO: find a way to have inventories open on click without this boilerplate in every block
|
||||
$player->setCurrentWindow($tile->getInventory());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
|
||||
return new HopperInventoryWindow($player, $inventory, $position);
|
||||
}
|
||||
|
||||
public function onScheduledUpdate() : void{
|
||||
|
@ -23,22 +23,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\LoomInventory;
|
||||
use pocketmine\block\inventory\window\LoomInventoryWindow;
|
||||
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
|
||||
use pocketmine\block\utils\HorizontalFacing;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\block\utils\MenuAccessor;
|
||||
use pocketmine\block\utils\MenuAccessorTrait;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
final class Loom extends Opaque implements HorizontalFacing{
|
||||
final class Loom extends Opaque implements HorizontalFacing, MenuAccessor{
|
||||
use FacesOppositePlacingPlayerTrait;
|
||||
use MenuAccessorTrait;
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player !== null){
|
||||
$player->setCurrentWindow(new LoomInventory($this->position));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
protected function newMenu(Player $player, Position $position) : LoomInventoryWindow{
|
||||
return new LoomInventoryWindow($player, $position);
|
||||
}
|
||||
}
|
||||
|
@ -23,19 +23,34 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\window\BlockInventoryWindow;
|
||||
use pocketmine\block\tile\ShulkerBox as TileShulkerBox;
|
||||
use pocketmine\block\utils\AnimatedContainerLike;
|
||||
use pocketmine\block\utils\AnimatedContainerLikeTrait;
|
||||
use pocketmine\block\utils\AnyFacing;
|
||||
use pocketmine\block\utils\AnyFacingTrait;
|
||||
use pocketmine\block\utils\Container;
|
||||
use pocketmine\block\utils\ContainerTrait;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
use pocketmine\data\runtime\RuntimeDataDescriber;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\BlockEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\BlockTransaction;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\ShulkerBoxCloseSound;
|
||||
use pocketmine\world\sound\ShulkerBoxOpenSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
|
||||
class ShulkerBox extends Opaque implements AnyFacing{
|
||||
class ShulkerBox extends Opaque implements AnimatedContainerLike, AnyFacing, Container{
|
||||
use AnimatedContainerLikeTrait;
|
||||
use AnyFacingTrait;
|
||||
use ContainerTrait;
|
||||
|
||||
protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
|
||||
//NOOP - we don't read or write facing here, because the tile persists it
|
||||
@ -95,26 +110,29 @@ class ShulkerBox extends Opaque implements AnyFacing{
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player){
|
||||
public function isOpeningObstructed() : bool{
|
||||
return $this->getSide($this->facing)->isSolid();
|
||||
}
|
||||
|
||||
$shulker = $this->position->getWorld()->getTile($this->position);
|
||||
if($shulker instanceof TileShulkerBox){
|
||||
if(
|
||||
$this->getSide($this->facing)->isSolid() ||
|
||||
!$shulker->canOpenWith($item->getCustomName())
|
||||
){
|
||||
return true;
|
||||
}
|
||||
|
||||
$player->setCurrentWindow($shulker->getInventory());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
|
||||
return new BlockInventoryWindow($player, $inventory, $position);
|
||||
}
|
||||
|
||||
public function getSupportType(Facing $facing) : SupportType{
|
||||
return SupportType::NONE;
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{
|
||||
return new ShulkerBoxOpenSound();
|
||||
}
|
||||
|
||||
protected function getCloseSound() : Sound{
|
||||
return new ShulkerBoxCloseSound();
|
||||
}
|
||||
|
||||
protected function playAnimationVisual(Position $position, bool $isOpen) : void{
|
||||
//event ID is always 1 for a chest
|
||||
//TODO: we probably shouldn't be sending a packet directly here, but it doesn't fit anywhere into existing systems
|
||||
$position->getWorld()->broadcastPacketToViewers($position, BlockEventPacket::create(BlockPosition::fromVector3($position), 1, $isOpen ? 1 : 0));
|
||||
}
|
||||
}
|
||||
|
@ -23,20 +23,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\SmithingTableInventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\block\inventory\window\SmithingTableInventoryWindow;
|
||||
use pocketmine\block\utils\MenuAccessor;
|
||||
use pocketmine\block\utils\MenuAccessorTrait;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
final class SmithingTable extends Opaque{
|
||||
final class SmithingTable extends Opaque implements MenuAccessor{
|
||||
use MenuAccessorTrait;
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player !== null){
|
||||
$player->setCurrentWindow(new SmithingTableInventory($this->position));
|
||||
}
|
||||
|
||||
return true;
|
||||
protected function newMenu(Player $player, Position $position) : InventoryWindow{
|
||||
return new SmithingTableInventoryWindow($player, $position);
|
||||
}
|
||||
|
||||
public function getFuelTime() : int{
|
||||
|
@ -23,24 +23,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\block\inventory\StonecutterInventory;
|
||||
use pocketmine\block\inventory\window\StonecutterInventoryWindow;
|
||||
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
|
||||
use pocketmine\block\utils\HorizontalFacing;
|
||||
use pocketmine\block\utils\MenuAccessor;
|
||||
use pocketmine\block\utils\MenuAccessorTrait;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\AxisAlignedBB;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class Stonecutter extends Transparent implements HorizontalFacing{
|
||||
class Stonecutter extends Transparent implements HorizontalFacing, MenuAccessor{
|
||||
use FacesOppositePlacingPlayerTrait;
|
||||
use MenuAccessorTrait;
|
||||
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player !== null){
|
||||
$player->setCurrentWindow(new StonecutterInventory($this->position));
|
||||
}
|
||||
return true;
|
||||
protected function newMenu(Player $player, Position $position) : StonecutterInventoryWindow{
|
||||
return new StonecutterInventoryWindow($player, $position);
|
||||
}
|
||||
|
||||
protected function recalculateCollisionBoxes() : array{
|
||||
|
@ -1,67 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\sound\Sound;
|
||||
use function count;
|
||||
|
||||
trait AnimatedBlockInventoryTrait{
|
||||
use BlockInventoryTrait;
|
||||
|
||||
public function getViewerCount() : int{
|
||||
return count($this->getViewers());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Player[]
|
||||
* @phpstan-return array<int, Player>
|
||||
*/
|
||||
abstract public function getViewers() : array;
|
||||
|
||||
abstract protected function getOpenSound() : Sound;
|
||||
|
||||
abstract protected function getCloseSound() : Sound;
|
||||
|
||||
public function onOpen(Player $who) : void{
|
||||
parent::onOpen($who);
|
||||
|
||||
if($this->holder->isValid() && $this->getViewerCount() === 1){
|
||||
//TODO: this crap really shouldn't be managed by the inventory
|
||||
$this->animateBlock(true);
|
||||
$this->holder->getWorld()->addSound($this->holder->add(0.5, 0.5, 0.5), $this->getOpenSound());
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function animateBlock(bool $isOpen) : void;
|
||||
|
||||
public function onClose(Player $who) : void{
|
||||
if($this->holder->isValid() && $this->getViewerCount() === 1){
|
||||
//TODO: this crap really shouldn't be managed by the inventory
|
||||
$this->animateBlock(false);
|
||||
$this->holder->getWorld()->addSound($this->holder->add(0.5, 0.5, 0.5), $this->getCloseSound());
|
||||
}
|
||||
parent::onClose($who);
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\block\Barrel;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\BarrelCloseSound;
|
||||
use pocketmine\world\sound\BarrelOpenSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
|
||||
class BarrelInventory extends SimpleInventory implements BlockInventory{
|
||||
use AnimatedBlockInventoryTrait;
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(27);
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{
|
||||
return new BarrelOpenSound();
|
||||
}
|
||||
|
||||
protected function getCloseSound() : Sound{
|
||||
return new BarrelCloseSound();
|
||||
}
|
||||
|
||||
protected function animateBlock(bool $isOpen) : void{
|
||||
$holder = $this->getHolder();
|
||||
$world = $holder->getWorld();
|
||||
$block = $world->getBlock($holder);
|
||||
if($block instanceof Barrel){
|
||||
$world->setBlock($holder, $block->setOpen($isOpen));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\world\Position;
|
||||
|
||||
trait BlockInventoryTrait{
|
||||
protected Position $holder;
|
||||
|
||||
public function getHolder() : Position{
|
||||
return $this->holder;
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\inventory\TemporaryInventory;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
final class CartographyTableInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
|
||||
use BlockInventoryTrait;
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(2);
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\network\mcpe\protocol\BlockEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\ChestCloseSound;
|
||||
use pocketmine\world\sound\ChestOpenSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
|
||||
class ChestInventory extends SimpleInventory implements BlockInventory{
|
||||
use AnimatedBlockInventoryTrait;
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(27);
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{
|
||||
return new ChestOpenSound();
|
||||
}
|
||||
|
||||
protected function getCloseSound() : Sound{
|
||||
return new ChestCloseSound();
|
||||
}
|
||||
|
||||
public function animateBlock(bool $isOpen) : void{
|
||||
$holder = $this->getHolder();
|
||||
|
||||
//event ID is always 1 for a chest
|
||||
$holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\inventory\BaseInventory;
|
||||
use pocketmine\inventory\InventoryHolder;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\world\sound\ChestCloseSound;
|
||||
use pocketmine\world\sound\ChestOpenSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
|
||||
class DoubleChestInventory extends BaseInventory implements BlockInventory, InventoryHolder{
|
||||
use AnimatedBlockInventoryTrait;
|
||||
|
||||
public function __construct(
|
||||
private ChestInventory $left,
|
||||
private ChestInventory $right
|
||||
){
|
||||
$this->holder = $this->left->getHolder();
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getInventory() : self{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSize() : int{
|
||||
return $this->left->getSize() + $this->right->getSize();
|
||||
}
|
||||
|
||||
public function getItem(int $index) : Item{
|
||||
return $index < $this->left->getSize() ? $this->left->getItem($index) : $this->right->getItem($index - $this->left->getSize());
|
||||
}
|
||||
|
||||
protected function internalSetItem(int $index, Item $item) : void{
|
||||
$index < $this->left->getSize() ? $this->left->setItem($index, $item) : $this->right->setItem($index - $this->left->getSize(), $item);
|
||||
}
|
||||
|
||||
public function getContents(bool $includeEmpty = false) : array{
|
||||
$result = $this->left->getContents($includeEmpty);
|
||||
$leftSize = $this->left->getSize();
|
||||
|
||||
foreach($this->right->getContents($includeEmpty) as $i => $item){
|
||||
$result[$i + $leftSize] = $item;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function internalSetContents(array $items) : void{
|
||||
$leftSize = $this->left->getSize();
|
||||
|
||||
$leftContents = [];
|
||||
$rightContents = [];
|
||||
|
||||
foreach($items as $i => $item){
|
||||
if($i < $this->left->getSize()){
|
||||
$leftContents[$i] = $item;
|
||||
}else{
|
||||
$rightContents[$i - $leftSize] = $item;
|
||||
}
|
||||
}
|
||||
$this->left->setContents($leftContents);
|
||||
$this->right->setContents($rightContents);
|
||||
}
|
||||
|
||||
protected function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
|
||||
$leftSize = $this->left->getSize();
|
||||
return $slot < $leftSize ?
|
||||
$this->left->getMatchingItemCount($slot, $test, $checkTags) :
|
||||
$this->right->getMatchingItemCount($slot - $leftSize, $test, $checkTags);
|
||||
}
|
||||
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
$leftSize = $this->left->getSize();
|
||||
return $index < $leftSize ?
|
||||
$this->left->isSlotEmpty($index) :
|
||||
$this->right->isSlotEmpty($index - $leftSize);
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{ return new ChestOpenSound(); }
|
||||
|
||||
protected function getCloseSound() : Sound{ return new ChestCloseSound(); }
|
||||
|
||||
protected function animateBlock(bool $isOpen) : void{
|
||||
$this->left->animateBlock($isOpen);
|
||||
$this->right->animateBlock($isOpen);
|
||||
}
|
||||
|
||||
public function getLeftSide() : ChestInventory{
|
||||
return $this->left;
|
||||
}
|
||||
|
||||
public function getRightSide() : ChestInventory{
|
||||
return $this->right;
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\event\player\PlayerEnchantingOptionsRequestEvent;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\inventory\TemporaryInventory;
|
||||
use pocketmine\item\enchantment\EnchantingHelper as Helper;
|
||||
use pocketmine\item\enchantment\EnchantingOption;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\world\Position;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
class EnchantInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
|
||||
use BlockInventoryTrait;
|
||||
|
||||
public const SLOT_INPUT = 0;
|
||||
public const SLOT_LAPIS = 1;
|
||||
|
||||
/**
|
||||
* @var EnchantingOption[] $options
|
||||
* @phpstan-var list<EnchantingOption>
|
||||
*/
|
||||
private array $options = [];
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(2);
|
||||
}
|
||||
|
||||
protected function onSlotChange(int $index, Item $before) : void{
|
||||
if($index === self::SLOT_INPUT){
|
||||
foreach($this->viewers as $viewer){
|
||||
$this->options = [];
|
||||
$item = $this->getInput();
|
||||
$options = Helper::generateOptions($this->holder, $item, $viewer->getEnchantmentSeed());
|
||||
|
||||
$event = new PlayerEnchantingOptionsRequestEvent($viewer, $this, $options);
|
||||
$event->call();
|
||||
if(!$event->isCancelled() && count($event->getOptions()) > 0){
|
||||
$this->options = array_values($event->getOptions());
|
||||
$viewer->getNetworkSession()->getInvManager()?->syncEnchantingTableOptions($this->options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parent::onSlotChange($index, $before);
|
||||
}
|
||||
|
||||
public function getInput() : Item{
|
||||
return $this->getItem(self::SLOT_INPUT);
|
||||
}
|
||||
|
||||
public function getLapis() : Item{
|
||||
return $this->getItem(self::SLOT_LAPIS);
|
||||
}
|
||||
|
||||
public function getOutput(int $optionId) : ?Item{
|
||||
$option = $this->getOption($optionId);
|
||||
return $option === null ? null : Helper::enchantItem($this->getInput(), $option->getEnchantments());
|
||||
}
|
||||
|
||||
public function getOption(int $optionId) : ?EnchantingOption{
|
||||
return $this->options[$optionId] ?? null;
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\block\tile\EnderChest;
|
||||
use pocketmine\inventory\DelegateInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\PlayerEnderInventory;
|
||||
use pocketmine\network\mcpe\protocol\BlockEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\EnderChestCloseSound;
|
||||
use pocketmine\world\sound\EnderChestOpenSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
|
||||
/**
|
||||
* EnderChestInventory is not a real inventory; it's just a gateway to the player's ender inventory.
|
||||
*/
|
||||
class EnderChestInventory extends DelegateInventory implements BlockInventory{
|
||||
use AnimatedBlockInventoryTrait {
|
||||
onClose as animatedBlockInventoryTrait_onClose;
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
Position $holder,
|
||||
private PlayerEnderInventory $inventory
|
||||
){
|
||||
parent::__construct($inventory);
|
||||
$this->holder = $holder;
|
||||
}
|
||||
|
||||
public function getEnderInventory() : PlayerEnderInventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function getViewerCount() : int{
|
||||
$enderChest = $this->getHolder()->getWorld()->getTile($this->getHolder());
|
||||
if(!$enderChest instanceof EnderChest){
|
||||
return 0;
|
||||
}
|
||||
return $enderChest->getViewerCount();
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{
|
||||
return new EnderChestOpenSound();
|
||||
}
|
||||
|
||||
protected function getCloseSound() : Sound{
|
||||
return new EnderChestCloseSound();
|
||||
}
|
||||
|
||||
protected function animateBlock(bool $isOpen) : void{
|
||||
$holder = $this->getHolder();
|
||||
|
||||
//event ID is always 1 for a chest
|
||||
$holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
|
||||
}
|
||||
|
||||
public function onClose(Player $who) : void{
|
||||
$this->animatedBlockInventoryTrait_onClose($who);
|
||||
$enderChest = $this->getHolder()->getWorld()->getTile($this->getHolder());
|
||||
if($enderChest instanceof EnderChest){
|
||||
$enderChest->setViewerCount($enderChest->getViewerCount() - 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\block\BlockTypeIds;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\ItemTypeIds;
|
||||
use pocketmine\network\mcpe\protocol\BlockEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\BlockPosition;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\ShulkerBoxCloseSound;
|
||||
use pocketmine\world\sound\ShulkerBoxOpenSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
|
||||
class ShulkerBoxInventory extends SimpleInventory implements BlockInventory{
|
||||
use AnimatedBlockInventoryTrait;
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(27);
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{
|
||||
return new ShulkerBoxOpenSound();
|
||||
}
|
||||
|
||||
protected function getCloseSound() : Sound{
|
||||
return new ShulkerBoxCloseSound();
|
||||
}
|
||||
|
||||
public function canAddItem(Item $item) : bool{
|
||||
$blockTypeId = ItemTypeIds::toBlockTypeId($item->getTypeId());
|
||||
if($blockTypeId === BlockTypeIds::SHULKER_BOX || $blockTypeId === BlockTypeIds::DYED_SHULKER_BOX){
|
||||
return false;
|
||||
}
|
||||
return parent::canAddItem($item);
|
||||
}
|
||||
|
||||
protected function animateBlock(bool $isOpen) : void{
|
||||
$holder = $this->getHolder();
|
||||
|
||||
//event ID is always 1 for a chest
|
||||
$holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
<?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\block\inventory;
|
||||
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\inventory\TemporaryInventory;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
final class SmithingTableInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
|
||||
use BlockInventoryTrait;
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(3);
|
||||
}
|
||||
}
|
@ -21,20 +21,21 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\inventory;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\inventory\TemporaryInventory;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\player\TemporaryInventoryWindow;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class AnvilInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
|
||||
use BlockInventoryTrait;
|
||||
|
||||
final class AnvilInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
|
||||
public const SLOT_INPUT = 0;
|
||||
public const SLOT_MATERIAL = 1;
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(2);
|
||||
public function __construct(
|
||||
Player $viewer,
|
||||
Position $holder
|
||||
){
|
||||
parent::__construct($viewer, new SimpleInventory(2), $holder);
|
||||
}
|
||||
}
|
59
src/block/inventory/window/BlockInventoryWindow.php
Normal file
59
src/block/inventory/window/BlockInventoryWindow.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?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\block\inventory\window;
|
||||
|
||||
use pocketmine\block\utils\AnimatedContainerLike;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class BlockInventoryWindow extends InventoryWindow{
|
||||
|
||||
public function __construct(
|
||||
Player $viewer,
|
||||
Inventory $inventory,
|
||||
protected Position $holder
|
||||
){
|
||||
parent::__construct($viewer, $inventory);
|
||||
}
|
||||
|
||||
public function getHolder() : Position{ return $this->holder; }
|
||||
|
||||
public function onOpen() : void{
|
||||
parent::onOpen();
|
||||
$block = $this->holder->getWorld()->getBlock($this->holder);
|
||||
if($block instanceof AnimatedContainerLike){
|
||||
$block->onViewerAdded();
|
||||
}
|
||||
}
|
||||
|
||||
public function onClose() : void{
|
||||
$block = $this->holder->getWorld()->getBlock($this->holder);
|
||||
if($block instanceof AnimatedContainerLike){
|
||||
$block->onViewerRemoved();
|
||||
}
|
||||
parent::onClose();
|
||||
}
|
||||
}
|
@ -21,22 +21,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\inventory;
|
||||
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class BrewingStandInventory extends SimpleInventory implements BlockInventory{
|
||||
use BlockInventoryTrait;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
final class BrewingStandInventoryWindow extends BlockInventoryWindow{
|
||||
public const SLOT_INGREDIENT = 0;
|
||||
public const SLOT_BOTTLE_LEFT = 1;
|
||||
public const SLOT_BOTTLE_MIDDLE = 2;
|
||||
public const SLOT_BOTTLE_RIGHT = 3;
|
||||
public const SLOT_FUEL = 4;
|
||||
|
||||
public function __construct(Position $holder, int $size = 5){
|
||||
$this->holder = $holder;
|
||||
parent::__construct($size);
|
||||
}
|
||||
}
|
@ -21,20 +21,19 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\inventory;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\player\TemporaryInventoryWindow;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class CampfireInventory extends SimpleInventory implements BlockInventory{
|
||||
use BlockInventoryTrait;
|
||||
final class CartographyTableInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(4);
|
||||
}
|
||||
|
||||
public function getMaxStackSize() : int{
|
||||
return 1;
|
||||
public function __construct(
|
||||
Player $viewer,
|
||||
Position $holder
|
||||
){
|
||||
parent::__construct($viewer, new SimpleInventory(2), $holder);
|
||||
}
|
||||
}
|
@ -21,17 +21,19 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\inventory;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
use pocketmine\crafting\CraftingGrid;
|
||||
use pocketmine\inventory\TemporaryInventory;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
final class CraftingTableInventory extends CraftingGrid implements BlockInventory, TemporaryInventory{
|
||||
use BlockInventoryTrait;
|
||||
final class CraftingTableInventoryWindow extends BlockInventoryWindow{
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(CraftingGrid::SIZE_BIG);
|
||||
public function __construct(
|
||||
Player $viewer,
|
||||
Position $holder
|
||||
){
|
||||
//TODO: generics would be good for this, since it has special methods
|
||||
parent::__construct($viewer, new CraftingGrid(CraftingGrid::SIZE_BIG), $holder);
|
||||
}
|
||||
}
|
@ -21,18 +21,24 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\inventory;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
final class DoubleChestInventoryWindow extends BlockInventoryWindow{
|
||||
|
||||
class PlayerCursorInventory extends SimpleInventory implements TemporaryInventory{
|
||||
public function __construct(
|
||||
protected Player $holder
|
||||
Player $viewer,
|
||||
Inventory $inventory,
|
||||
private Position $left,
|
||||
private Position $right
|
||||
){
|
||||
parent::__construct(1);
|
||||
parent::__construct($viewer, $inventory, $this->left);
|
||||
}
|
||||
|
||||
public function getHolder() : Player{
|
||||
return $this->holder;
|
||||
}
|
||||
public function getLeft() : Position{ return $this->left; }
|
||||
|
||||
public function getRight() : Position{ return $this->right; }
|
||||
}
|
104
src/block/inventory/window/EnchantingTableInventoryWindow.php
Normal file
104
src/block/inventory/window/EnchantingTableInventoryWindow.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?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\block\inventory\window;
|
||||
|
||||
use pocketmine\event\player\PlayerEnchantingOptionsRequestEvent;
|
||||
use pocketmine\inventory\CallbackInventoryListener;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\InventoryListener;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\item\enchantment\EnchantingHelper as Helper;
|
||||
use pocketmine\item\enchantment\EnchantingOption;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
final class EnchantingTableInventoryWindow extends BlockInventoryWindow{
|
||||
public const SLOT_INPUT = 0;
|
||||
public const SLOT_LAPIS = 1;
|
||||
|
||||
/** @var EnchantingOption[] $options */
|
||||
private array $options = [];
|
||||
|
||||
private InventoryListener $listener;
|
||||
|
||||
public function __construct(
|
||||
Player $viewer,
|
||||
Position $holder
|
||||
){
|
||||
parent::__construct($viewer, new SimpleInventory(2), $holder);
|
||||
|
||||
/** @phpstan-var \WeakReference<$this> $weakThis */
|
||||
$weakThis = \WeakReference::create($this);
|
||||
$this->listener = new CallbackInventoryListener(
|
||||
onSlotChange: static function(Inventory $_, int $slot) use ($weakThis) : void{ //remaining params unneeded
|
||||
if($slot === self::SLOT_INPUT && ($strongThis = $weakThis->get()) !== null){
|
||||
$strongThis->regenerateOptions();
|
||||
}
|
||||
},
|
||||
onContentChange: static function() use ($weakThis) : void{
|
||||
if(($strongThis = $weakThis->get()) !== null){
|
||||
$strongThis->regenerateOptions();
|
||||
}
|
||||
}
|
||||
);
|
||||
$this->inventory->getListeners()->add($this->listener);
|
||||
}
|
||||
|
||||
public function __destruct(){
|
||||
$this->inventory->getListeners()->remove($this->listener);
|
||||
}
|
||||
|
||||
private function regenerateOptions() : void{
|
||||
$this->options = [];
|
||||
$item = $this->getInput();
|
||||
$options = Helper::generateOptions($this->holder, $item, $this->viewer->getEnchantmentSeed());
|
||||
|
||||
$event = new PlayerEnchantingOptionsRequestEvent($this->viewer, $this, $options);
|
||||
$event->call();
|
||||
if(!$event->isCancelled() && count($event->getOptions()) > 0){
|
||||
$this->options = array_values($event->getOptions());
|
||||
$this->viewer->getNetworkSession()->getInvManager()?->syncEnchantingTableOptions($this->options);
|
||||
}
|
||||
}
|
||||
|
||||
public function getInput() : Item{
|
||||
return $this->inventory->getItem(self::SLOT_INPUT);
|
||||
}
|
||||
|
||||
public function getLapis() : Item{
|
||||
return $this->inventory->getItem(self::SLOT_LAPIS);
|
||||
}
|
||||
|
||||
public function getOutput(int $optionId) : ?Item{
|
||||
$option = $this->getOption($optionId);
|
||||
return $option === null ? null : Helper::enchantItem($this->getInput(), $option->getEnchantments());
|
||||
}
|
||||
|
||||
public function getOption(int $optionId) : ?EnchantingOption{
|
||||
return $this->options[$optionId] ?? null;
|
||||
}
|
||||
}
|
@ -21,51 +21,51 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\inventory;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
use pocketmine\crafting\FurnaceType;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class FurnaceInventory extends SimpleInventory implements BlockInventory{
|
||||
use BlockInventoryTrait;
|
||||
|
||||
final class FurnaceInventoryWindow extends BlockInventoryWindow{
|
||||
public const SLOT_INPUT = 0;
|
||||
public const SLOT_FUEL = 1;
|
||||
public const SLOT_RESULT = 2;
|
||||
|
||||
public function __construct(
|
||||
Player $viewer,
|
||||
Inventory $inventory,
|
||||
Position $holder,
|
||||
private FurnaceType $furnaceType
|
||||
){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(3);
|
||||
parent::__construct($viewer, $inventory, $holder);
|
||||
}
|
||||
|
||||
public function getFurnaceType() : FurnaceType{ return $this->furnaceType; }
|
||||
|
||||
public function getResult() : Item{
|
||||
return $this->getItem(self::SLOT_RESULT);
|
||||
return $this->inventory->getItem(self::SLOT_RESULT);
|
||||
}
|
||||
|
||||
public function getFuel() : Item{
|
||||
return $this->getItem(self::SLOT_FUEL);
|
||||
return $this->inventory->getItem(self::SLOT_FUEL);
|
||||
}
|
||||
|
||||
public function getSmelting() : Item{
|
||||
return $this->getItem(self::SLOT_INPUT);
|
||||
return $this->inventory->getItem(self::SLOT_INPUT);
|
||||
}
|
||||
|
||||
public function setResult(Item $item) : void{
|
||||
$this->setItem(self::SLOT_RESULT, $item);
|
||||
$this->inventory->setItem(self::SLOT_RESULT, $item);
|
||||
}
|
||||
|
||||
public function setFuel(Item $item) : void{
|
||||
$this->setItem(self::SLOT_FUEL, $item);
|
||||
$this->inventory->setItem(self::SLOT_FUEL, $item);
|
||||
}
|
||||
|
||||
public function setSmelting(Item $item) : void{
|
||||
$this->setItem(self::SLOT_INPUT, $item);
|
||||
$this->inventory->setItem(self::SLOT_INPUT, $item);
|
||||
}
|
||||
}
|
@ -21,10 +21,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\inventory;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
use pocketmine\world\Position;
|
||||
final class HopperInventoryWindow extends BlockInventoryWindow{
|
||||
|
||||
interface BlockInventory{
|
||||
public function getHolder() : Position;
|
||||
}
|
@ -21,21 +21,23 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\inventory;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\inventory\TemporaryInventory;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\player\TemporaryInventoryWindow;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
final class LoomInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
|
||||
use BlockInventoryTrait;
|
||||
final class LoomInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
|
||||
|
||||
public const SLOT_BANNER = 0;
|
||||
public const SLOT_DYE = 1;
|
||||
public const SLOT_PATTERN = 2;
|
||||
|
||||
public function __construct(Position $holder, int $size = 3){
|
||||
$this->holder = $holder;
|
||||
parent::__construct($size);
|
||||
public function __construct(
|
||||
Player $viewer,
|
||||
Position $holder
|
||||
){
|
||||
parent::__construct($viewer, new SimpleInventory(3), $holder);
|
||||
}
|
||||
}
|
@ -21,16 +21,15 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\inventory;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\player\TemporaryInventoryWindow;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class HopperInventory extends SimpleInventory implements BlockInventory{
|
||||
use BlockInventoryTrait;
|
||||
|
||||
public function __construct(Position $holder, int $size = 5){
|
||||
$this->holder = $holder;
|
||||
parent::__construct($size);
|
||||
final class SmithingTableInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
|
||||
public function __construct(Player $viewer, Position $holder){
|
||||
parent::__construct($viewer, new SimpleInventory(3), $holder);
|
||||
}
|
||||
}
|
@ -21,19 +21,17 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\inventory;
|
||||
namespace pocketmine\block\inventory\window;
|
||||
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\inventory\TemporaryInventory;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\player\TemporaryInventoryWindow;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
class StonecutterInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
|
||||
use BlockInventoryTrait;
|
||||
|
||||
final class StonecutterInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
|
||||
public const SLOT_INPUT = 0;
|
||||
|
||||
public function __construct(Position $holder){
|
||||
$this->holder = $holder;
|
||||
parent::__construct(1);
|
||||
public function __construct(Player $viewer, Position $holder){
|
||||
parent::__construct($viewer, new SimpleInventory(1), $holder);
|
||||
}
|
||||
}
|
@ -23,20 +23,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\tile;
|
||||
|
||||
use pocketmine\block\inventory\BarrelInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\world\World;
|
||||
|
||||
class Barrel extends Spawnable implements Container, Nameable{
|
||||
class Barrel extends Spawnable implements ContainerTile, Nameable{
|
||||
use NameableTrait;
|
||||
use ContainerTrait;
|
||||
use ContainerTileTrait;
|
||||
|
||||
protected BarrelInventory $inventory;
|
||||
protected Inventory $inventory;
|
||||
|
||||
public function __construct(World $world, Vector3 $pos){
|
||||
parent::__construct($world, $pos);
|
||||
$this->inventory = new BarrelInventory($this->position);
|
||||
$this->inventory = new SimpleInventory(27);
|
||||
}
|
||||
|
||||
public function readSaveData(CompoundTag $nbt) : void{
|
||||
@ -56,11 +57,11 @@ class Barrel extends Spawnable implements Container, Nameable{
|
||||
}
|
||||
}
|
||||
|
||||
public function getInventory() : BarrelInventory{
|
||||
public function getInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function getRealInventory() : BarrelInventory{
|
||||
public function getRealInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
|
@ -23,12 +23,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\tile;
|
||||
|
||||
use pocketmine\block\inventory\BrewingStandInventory;
|
||||
use pocketmine\block\inventory\window\BrewingStandInventoryWindow;
|
||||
use pocketmine\crafting\BrewingRecipe;
|
||||
use pocketmine\event\block\BrewingFuelUseEvent;
|
||||
use pocketmine\event\block\BrewItemEvent;
|
||||
use pocketmine\inventory\CallbackInventoryListener;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\math\Vector3;
|
||||
@ -40,11 +41,11 @@ use pocketmine\world\World;
|
||||
use function array_map;
|
||||
use function count;
|
||||
|
||||
class BrewingStand extends Spawnable implements Container, Nameable{
|
||||
class BrewingStand extends Spawnable implements ContainerTile, Nameable{
|
||||
use NameableTrait {
|
||||
addAdditionalSpawnData as addNameSpawnData;
|
||||
}
|
||||
use ContainerTrait;
|
||||
use ContainerTileTrait;
|
||||
|
||||
public const BREW_TIME_TICKS = 400; // Brew time in ticks
|
||||
|
||||
@ -54,7 +55,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{
|
||||
private const TAG_REMAINING_FUEL_TIME = "Fuel"; //TAG_Byte
|
||||
private const TAG_REMAINING_FUEL_TIME_PE = "FuelAmount"; //TAG_Short
|
||||
|
||||
private BrewingStandInventory $inventory;
|
||||
private Inventory $inventory;
|
||||
|
||||
private int $brewTime = 0;
|
||||
private int $maxFuelTime = 0;
|
||||
@ -62,7 +63,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{
|
||||
|
||||
public function __construct(World $world, Vector3 $pos){
|
||||
parent::__construct($world, $pos);
|
||||
$this->inventory = new BrewingStandInventory($this->position);
|
||||
$this->inventory = new SimpleInventory(5);
|
||||
$this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange(static function(Inventory $unused) use ($world, $pos) : void{
|
||||
$world->scheduleDelayedBlockUpdate($pos, 1);
|
||||
}));
|
||||
@ -112,11 +113,11 @@ class BrewingStand extends Spawnable implements Container, Nameable{
|
||||
}
|
||||
}
|
||||
|
||||
public function getInventory() : BrewingStandInventory{
|
||||
public function getInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function getRealInventory() : BrewingStandInventory{
|
||||
public function getRealInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
@ -132,7 +133,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{
|
||||
}
|
||||
|
||||
$item->pop();
|
||||
$this->inventory->setItem(BrewingStandInventory::SLOT_FUEL, $item);
|
||||
$this->inventory->setItem(BrewingStandInventoryWindow::SLOT_FUEL, $item);
|
||||
|
||||
$this->maxFuelTime = $this->remainingFuelTime = $ev->getFuelTime();
|
||||
}
|
||||
@ -142,14 +143,14 @@ class BrewingStand extends Spawnable implements Container, Nameable{
|
||||
* @phpstan-return array<int, BrewingRecipe>
|
||||
*/
|
||||
private function getBrewableRecipes() : array{
|
||||
$ingredient = $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT);
|
||||
$ingredient = $this->inventory->getItem(BrewingStandInventoryWindow::SLOT_INGREDIENT);
|
||||
if($ingredient->isNull()){
|
||||
return [];
|
||||
}
|
||||
|
||||
$recipes = [];
|
||||
$craftingManager = $this->position->getWorld()->getServer()->getCraftingManager();
|
||||
foreach([BrewingStandInventory::SLOT_BOTTLE_LEFT, BrewingStandInventory::SLOT_BOTTLE_MIDDLE, BrewingStandInventory::SLOT_BOTTLE_RIGHT] as $slot){
|
||||
foreach([BrewingStandInventoryWindow::SLOT_BOTTLE_LEFT, BrewingStandInventoryWindow::SLOT_BOTTLE_MIDDLE, BrewingStandInventoryWindow::SLOT_BOTTLE_RIGHT] as $slot){
|
||||
$input = $this->inventory->getItem($slot);
|
||||
if($input->isNull()){
|
||||
continue;
|
||||
@ -176,8 +177,8 @@ class BrewingStand extends Spawnable implements Container, Nameable{
|
||||
|
||||
$ret = false;
|
||||
|
||||
$fuel = $this->inventory->getItem(BrewingStandInventory::SLOT_FUEL);
|
||||
$ingredient = $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT);
|
||||
$fuel = $this->inventory->getItem(BrewingStandInventoryWindow::SLOT_FUEL);
|
||||
$ingredient = $this->inventory->getItem(BrewingStandInventoryWindow::SLOT_INGREDIENT);
|
||||
|
||||
$recipes = $this->getBrewableRecipes();
|
||||
$canBrew = count($recipes) !== 0;
|
||||
@ -219,7 +220,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{
|
||||
}
|
||||
|
||||
$ingredient->pop();
|
||||
$this->inventory->setItem(BrewingStandInventory::SLOT_INGREDIENT, $ingredient);
|
||||
$this->inventory->setItem(BrewingStandInventoryWindow::SLOT_INGREDIENT, $ingredient);
|
||||
|
||||
$this->brewTime = 0;
|
||||
}else{
|
||||
|
@ -24,9 +24,9 @@ declare(strict_types=1);
|
||||
namespace pocketmine\block\tile;
|
||||
|
||||
use pocketmine\block\Campfire as BlockCampfire;
|
||||
use pocketmine\block\inventory\CampfireInventory;
|
||||
use pocketmine\inventory\CallbackInventoryListener;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
@ -34,8 +34,8 @@ use pocketmine\nbt\tag\IntTag;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
use pocketmine\world\World;
|
||||
|
||||
class Campfire extends Spawnable implements Container{
|
||||
use ContainerTrait;
|
||||
class Campfire extends Spawnable implements ContainerTile{
|
||||
use ContainerTileTrait;
|
||||
|
||||
private const TAG_FIRST_INPUT_ITEM = "Item1"; //TAG_Compound
|
||||
private const TAG_SECOND_INPUT_ITEM = "Item2"; //TAG_Compound
|
||||
@ -47,13 +47,14 @@ class Campfire extends Spawnable implements Container{
|
||||
private const TAG_THIRD_COOKING_TIME = "ItemTime3"; //TAG_Int
|
||||
private const TAG_FOURTH_COOKING_TIME = "ItemTime4"; //TAG_Int
|
||||
|
||||
protected CampfireInventory $inventory;
|
||||
protected Inventory $inventory;
|
||||
/** @var array<int, int> */
|
||||
private array $cookingTimes = [];
|
||||
|
||||
public function __construct(World $world, Vector3 $pos){
|
||||
parent::__construct($world, $pos);
|
||||
$this->inventory = new CampfireInventory($this->position);
|
||||
$this->inventory = new SimpleInventory(4);
|
||||
$this->inventory->setMaxStackSize(1);
|
||||
$this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange(
|
||||
static function(Inventory $unused) use ($world, $pos) : void{
|
||||
$block = $world->getBlock($pos);
|
||||
@ -64,11 +65,11 @@ class Campfire extends Spawnable implements Container{
|
||||
);
|
||||
}
|
||||
|
||||
public function getInventory() : CampfireInventory{
|
||||
public function getInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function getRealInventory() : CampfireInventory{
|
||||
public function getRealInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\tile;
|
||||
|
||||
use pocketmine\block\inventory\ChestInventory;
|
||||
use pocketmine\block\inventory\DoubleChestInventory;
|
||||
use pocketmine\inventory\CombinedInventoryProxy;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntTag;
|
||||
@ -32,11 +33,11 @@ use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\World;
|
||||
use function abs;
|
||||
|
||||
class Chest extends Spawnable implements Container, Nameable{
|
||||
class Chest extends Spawnable implements ContainerTile, Nameable{
|
||||
use NameableTrait {
|
||||
addAdditionalSpawnData as addNameSpawnData;
|
||||
}
|
||||
use ContainerTrait {
|
||||
use ContainerTileTrait {
|
||||
onBlockDestroyedHook as containerTraitBlockDestroyedHook;
|
||||
}
|
||||
|
||||
@ -44,15 +45,15 @@ class Chest extends Spawnable implements Container, Nameable{
|
||||
public const TAG_PAIRZ = "pairz";
|
||||
public const TAG_PAIR_LEAD = "pairlead";
|
||||
|
||||
protected ChestInventory $inventory;
|
||||
protected ?DoubleChestInventory $doubleInventory = null;
|
||||
protected Inventory $inventory;
|
||||
protected ?CombinedInventoryProxy $doubleInventory = null;
|
||||
|
||||
private ?int $pairX = null;
|
||||
private ?int $pairZ = null;
|
||||
|
||||
public function __construct(World $world, Vector3 $pos){
|
||||
parent::__construct($world, $pos);
|
||||
$this->inventory = new ChestInventory($this->position);
|
||||
$this->inventory = new SimpleInventory(27);
|
||||
}
|
||||
|
||||
public function readSaveData(CompoundTag $nbt) : void{
|
||||
@ -114,14 +115,14 @@ class Chest extends Spawnable implements Container, Nameable{
|
||||
$this->containerTraitBlockDestroyedHook();
|
||||
}
|
||||
|
||||
public function getInventory() : ChestInventory|DoubleChestInventory{
|
||||
public function getInventory() : Inventory|CombinedInventoryProxy{
|
||||
if($this->isPaired() && $this->doubleInventory === null){
|
||||
$this->checkPairing();
|
||||
}
|
||||
return $this->doubleInventory instanceof DoubleChestInventory ? $this->doubleInventory : $this->inventory;
|
||||
return $this->doubleInventory ?? $this->inventory;
|
||||
}
|
||||
|
||||
public function getRealInventory() : ChestInventory{
|
||||
public function getRealInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
@ -140,9 +141,9 @@ class Chest extends Spawnable implements Container, Nameable{
|
||||
$this->doubleInventory = $pair->doubleInventory;
|
||||
}else{
|
||||
if(($pair->position->x + ($pair->position->z << 15)) > ($this->position->x + ($this->position->z << 15))){ //Order them correctly
|
||||
$this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($pair->inventory, $this->inventory);
|
||||
$this->doubleInventory = $pair->doubleInventory = new CombinedInventoryProxy([$pair->inventory, $this->inventory]);
|
||||
}else{
|
||||
$this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($this->inventory, $pair->inventory);
|
||||
$this->doubleInventory = $pair->doubleInventory = new CombinedInventoryProxy([$this->inventory, $pair->inventory]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +37,8 @@ use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\world\World;
|
||||
use function count;
|
||||
|
||||
class ChiseledBookshelf extends Tile implements Container{
|
||||
use ContainerTrait;
|
||||
class ChiseledBookshelf extends Tile implements ContainerTile{
|
||||
use ContainerTileTrait;
|
||||
|
||||
private const TAG_LAST_INTERACTED_SLOT = "LastInteractedSlot"; //TAG_Int
|
||||
|
||||
@ -86,7 +86,7 @@ class ChiseledBookshelf extends Tile implements Container{
|
||||
}
|
||||
|
||||
protected function loadItems(CompoundTag $tag) : void{
|
||||
if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
|
||||
if(($inventoryTag = $tag->getTag(ContainerTile::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
|
||||
$inventory = $this->getRealInventory();
|
||||
$listeners = $inventory->getListeners()->toArray();
|
||||
$inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
|
||||
@ -111,7 +111,7 @@ class ChiseledBookshelf extends Tile implements Container{
|
||||
$inventory->getListeners()->add(...$listeners);
|
||||
}
|
||||
|
||||
if(($lockTag = $tag->getTag(Container::TAG_LOCK)) instanceof StringTag){
|
||||
if(($lockTag = $tag->getTag(ContainerTile::TAG_LOCK)) instanceof StringTag){
|
||||
$this->lock = $lockTag->getValue();
|
||||
}
|
||||
}
|
||||
@ -130,10 +130,10 @@ class ChiseledBookshelf extends Tile implements Container{
|
||||
}
|
||||
}
|
||||
|
||||
$tag->setTag(Container::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound));
|
||||
$tag->setTag(ContainerTile::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound));
|
||||
|
||||
if($this->lock !== null){
|
||||
$tag->setString(Container::TAG_LOCK, $this->lock);
|
||||
$tag->setString(ContainerTile::TAG_LOCK, $this->lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace pocketmine\block\tile;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\InventoryHolder;
|
||||
|
||||
interface Container extends InventoryHolder{
|
||||
interface ContainerTile extends InventoryHolder{
|
||||
public const TAG_ITEMS = "Items";
|
||||
public const TAG_LOCK = "Lock";
|
||||
|
@ -34,16 +34,16 @@ use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
/**
|
||||
* This trait implements most methods in the {@link Container} interface. It should only be used by Tiles.
|
||||
* This trait implements most methods in the {@link ContainerTile} interface. It should only be used by Tiles.
|
||||
*/
|
||||
trait ContainerTrait{
|
||||
trait ContainerTileTrait{
|
||||
/** @var string|null */
|
||||
private $lock = null;
|
||||
|
||||
abstract public function getRealInventory() : Inventory;
|
||||
|
||||
protected function loadItems(CompoundTag $tag) : void{
|
||||
if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
|
||||
if(($inventoryTag = $tag->getTag(ContainerTile::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
|
||||
$inventory = $this->getRealInventory();
|
||||
$listeners = $inventory->getListeners()->toArray();
|
||||
$inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
|
||||
@ -64,7 +64,7 @@ trait ContainerTrait{
|
||||
$inventory->getListeners()->add(...$listeners);
|
||||
}
|
||||
|
||||
if(($lockTag = $tag->getTag(Container::TAG_LOCK)) instanceof StringTag){
|
||||
if(($lockTag = $tag->getTag(ContainerTile::TAG_LOCK)) instanceof StringTag){
|
||||
$this->lock = $lockTag->getValue();
|
||||
}
|
||||
}
|
||||
@ -75,15 +75,15 @@ trait ContainerTrait{
|
||||
$items[] = $item->nbtSerialize($slot);
|
||||
}
|
||||
|
||||
$tag->setTag(Container::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound));
|
||||
$tag->setTag(ContainerTile::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound));
|
||||
|
||||
if($this->lock !== null){
|
||||
$tag->setString(Container::TAG_LOCK, $this->lock);
|
||||
$tag->setString(ContainerTile::TAG_LOCK, $this->lock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Container::canOpenWith()
|
||||
* @see ContainerTile::canOpenWith()
|
||||
*/
|
||||
public function canOpenWith(string $key) : bool{
|
||||
return $this->lock === null || $this->lock === $key;
|
@ -24,13 +24,14 @@ declare(strict_types=1);
|
||||
namespace pocketmine\block\tile;
|
||||
|
||||
use pocketmine\block\Furnace as BlockFurnace;
|
||||
use pocketmine\block\inventory\FurnaceInventory;
|
||||
use pocketmine\block\inventory\window\FurnaceInventoryWindow;
|
||||
use pocketmine\crafting\FurnaceRecipe;
|
||||
use pocketmine\crafting\FurnaceType;
|
||||
use pocketmine\event\inventory\FurnaceBurnEvent;
|
||||
use pocketmine\event\inventory\FurnaceSmeltEvent;
|
||||
use pocketmine\inventory\CallbackInventoryListener;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
@ -40,22 +41,22 @@ use pocketmine\world\World;
|
||||
use function array_map;
|
||||
use function max;
|
||||
|
||||
abstract class Furnace extends Spawnable implements Container, Nameable{
|
||||
abstract class Furnace extends Spawnable implements ContainerTile, Nameable{
|
||||
use NameableTrait;
|
||||
use ContainerTrait;
|
||||
use ContainerTileTrait;
|
||||
|
||||
public const TAG_BURN_TIME = "BurnTime";
|
||||
public const TAG_COOK_TIME = "CookTime";
|
||||
public const TAG_MAX_TIME = "MaxTime";
|
||||
|
||||
protected FurnaceInventory $inventory;
|
||||
protected Inventory $inventory;
|
||||
private int $remainingFuelTime = 0;
|
||||
private int $cookTime = 0;
|
||||
private int $maxFuelTime = 0;
|
||||
|
||||
public function __construct(World $world, Vector3 $pos){
|
||||
parent::__construct($world, $pos);
|
||||
$this->inventory = new FurnaceInventory($this->position, $this->getFurnaceType());
|
||||
$this->inventory = new SimpleInventory(3);
|
||||
$this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange(
|
||||
static function(Inventory $unused) use ($world, $pos) : void{
|
||||
$world->scheduleDelayedBlockUpdate($pos, 1);
|
||||
@ -104,11 +105,11 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
|
||||
}
|
||||
}
|
||||
|
||||
public function getInventory() : FurnaceInventory{
|
||||
public function getInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function getRealInventory() : FurnaceInventory{
|
||||
public function getRealInventory() : Inventory{
|
||||
return $this->getInventory();
|
||||
}
|
||||
|
||||
@ -123,7 +124,7 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
|
||||
$this->onStartSmelting();
|
||||
|
||||
if($this->remainingFuelTime > 0 && $ev->isBurning()){
|
||||
$this->inventory->setFuel($fuel->getFuelResidue());
|
||||
$this->inventory->setItem(FurnaceInventoryWindow::SLOT_FUEL, $fuel->getFuelResidue());
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,9 +160,9 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
|
||||
|
||||
$ret = false;
|
||||
|
||||
$fuel = $this->inventory->getFuel();
|
||||
$raw = $this->inventory->getSmelting();
|
||||
$product = $this->inventory->getResult();
|
||||
$fuel = $this->inventory->getItem(FurnaceInventoryWindow::SLOT_FUEL);
|
||||
$raw = $this->inventory->getItem(FurnaceInventoryWindow::SLOT_INPUT);
|
||||
$product = $this->inventory->getItem(FurnaceInventoryWindow::SLOT_RESULT);
|
||||
|
||||
$furnaceType = $this->getFurnaceType();
|
||||
$smelt = $this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($furnaceType)->match($raw);
|
||||
@ -184,9 +185,9 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
|
||||
$ev->call();
|
||||
|
||||
if(!$ev->isCancelled()){
|
||||
$this->inventory->setResult($ev->getResult());
|
||||
$this->inventory->setItem(FurnaceInventoryWindow::SLOT_RESULT, $ev->getResult());
|
||||
$raw->pop();
|
||||
$this->inventory->setSmelting($raw);
|
||||
$this->inventory->setItem(FurnaceInventoryWindow::SLOT_INPUT, $raw);
|
||||
}
|
||||
|
||||
$this->cookTime -= $furnaceType->getCookDurationTicks();
|
||||
|
@ -23,24 +23,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\tile;
|
||||
|
||||
use pocketmine\block\inventory\HopperInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\world\World;
|
||||
|
||||
class Hopper extends Spawnable implements Container, Nameable{
|
||||
class Hopper extends Spawnable implements ContainerTile, Nameable{
|
||||
|
||||
use ContainerTrait;
|
||||
use ContainerTileTrait;
|
||||
use NameableTrait;
|
||||
|
||||
private const TAG_TRANSFER_COOLDOWN = "TransferCooldown";
|
||||
|
||||
private HopperInventory $inventory;
|
||||
private Inventory $inventory;
|
||||
private int $transferCooldown = 0;
|
||||
|
||||
public function __construct(World $world, Vector3 $pos){
|
||||
parent::__construct($world, $pos);
|
||||
$this->inventory = new HopperInventory($this->position);
|
||||
$this->inventory = new SimpleInventory(5);
|
||||
}
|
||||
|
||||
public function readSaveData(CompoundTag $nbt) : void{
|
||||
@ -69,11 +70,11 @@ class Hopper extends Spawnable implements Container, Nameable{
|
||||
return "Hopper";
|
||||
}
|
||||
|
||||
public function getInventory() : HopperInventory{
|
||||
public function getInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function getRealInventory() : HopperInventory{
|
||||
public function getRealInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
}
|
||||
|
@ -23,29 +23,43 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\tile;
|
||||
|
||||
use pocketmine\block\inventory\ShulkerBoxInventory;
|
||||
use pocketmine\block\BlockTypeIds;
|
||||
use pocketmine\data\SavedDataLoadingException;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\inventory\transaction\action\validator\CallbackSlotValidator;
|
||||
use pocketmine\inventory\transaction\TransactionValidationException;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\ItemTypeIds;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\world\World;
|
||||
|
||||
class ShulkerBox extends Spawnable implements Container, Nameable{
|
||||
class ShulkerBox extends Spawnable implements ContainerTile, Nameable{
|
||||
use NameableTrait {
|
||||
addAdditionalSpawnData as addNameSpawnData;
|
||||
}
|
||||
use ContainerTrait;
|
||||
use ContainerTileTrait;
|
||||
|
||||
public const TAG_FACING = "facing";
|
||||
|
||||
protected Facing $facing = Facing::NORTH;
|
||||
|
||||
protected ShulkerBoxInventory $inventory;
|
||||
protected Inventory $inventory;
|
||||
|
||||
public function __construct(World $world, Vector3 $pos){
|
||||
parent::__construct($world, $pos);
|
||||
$this->inventory = new ShulkerBoxInventory($this->position);
|
||||
$this->inventory = new SimpleInventory(27);
|
||||
|
||||
$this->inventory->getSlotValidators()->add(new CallbackSlotValidator(static function(Inventory $_, Item $item) : ?TransactionValidationException{ //remaining params not needed
|
||||
$blockTypeId = ItemTypeIds::toBlockTypeId($item->getTypeId());
|
||||
if($blockTypeId === BlockTypeIds::SHULKER_BOX || $blockTypeId === BlockTypeIds::DYED_SHULKER_BOX){
|
||||
return new TransactionValidationException("Shulker box inventory cannot contain shulker boxes");
|
||||
}
|
||||
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
|
||||
public function readSaveData(CompoundTag $nbt) : void{
|
||||
@ -96,11 +110,11 @@ class ShulkerBox extends Spawnable implements Container, Nameable{
|
||||
$this->facing = $facing;
|
||||
}
|
||||
|
||||
public function getInventory() : ShulkerBoxInventory{
|
||||
public function getInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function getRealInventory() : ShulkerBoxInventory{
|
||||
public function getRealInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
|
42
src/block/utils/AnimatedContainerLike.php
Normal file
42
src/block/utils/AnimatedContainerLike.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?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\block\utils;
|
||||
|
||||
/**
|
||||
* Blocks which have audiovisual behaviour (like chests) and remain in their "open" state for as long as at least 1
|
||||
* viewer is viewing the menu they provide access to
|
||||
*/
|
||||
interface AnimatedContainerLike extends MenuAccessor{
|
||||
/**
|
||||
* Do actions when the container block is opened by a player.
|
||||
* If you have a custom viewer counter (like ender chests), you should increment it here.
|
||||
*/
|
||||
public function onViewerAdded() : void;
|
||||
|
||||
/**
|
||||
* Do actions when the container block is closed by a player.
|
||||
* As above, you should decrement your custom viewer counter here, if you have one.
|
||||
*/
|
||||
public function onViewerRemoved() : void;
|
||||
}
|
71
src/block/utils/AnimatedContainerLikeTrait.php
Normal file
71
src/block/utils/AnimatedContainerLikeTrait.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?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\block\utils;
|
||||
|
||||
use pocketmine\inventory\InventoryHolder;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\Sound;
|
||||
use function count;
|
||||
|
||||
trait AnimatedContainerLikeTrait{
|
||||
|
||||
protected function getViewerCount() : int{
|
||||
$position = $this->getPosition();
|
||||
$tile = $position->getWorld()->getTile($position);
|
||||
if($tile instanceof InventoryHolder){
|
||||
return count($tile->getInventory()->getViewers());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
abstract protected function getOpenSound() : Sound;
|
||||
|
||||
abstract protected function getCloseSound() : Sound;
|
||||
|
||||
abstract protected function playAnimationVisual(Position $position, bool $isOpen) : void;
|
||||
|
||||
protected function playAnimationSound(Position $position, bool $isOpen) : void{
|
||||
$position->getWorld()->addSound($position->add(0.5, 0.5, 0.5), $isOpen ? $this->getOpenSound() : $this->getCloseSound());
|
||||
}
|
||||
|
||||
abstract protected function getPosition() : Position;
|
||||
|
||||
protected function doAnimationEffects(bool $isOpen) : void{
|
||||
$position = $this->getPosition();
|
||||
$this->playAnimationVisual($position, $isOpen);
|
||||
$this->playAnimationSound($position, $isOpen);
|
||||
}
|
||||
|
||||
public function onViewerAdded() : void{
|
||||
if($this->getViewerCount() === 1){
|
||||
$this->doAnimationEffects(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function onViewerRemoved() : void{
|
||||
if($this->getViewerCount() === 1){
|
||||
$this->doAnimationEffects(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\utils;
|
||||
|
||||
use pocketmine\block\inventory\BrewingStandInventory;
|
||||
use pocketmine\block\inventory\window\BrewingStandInventoryWindow;
|
||||
|
||||
enum BrewingStandSlot{
|
||||
case EAST;
|
||||
@ -35,9 +35,9 @@ enum BrewingStandSlot{
|
||||
*/
|
||||
public function getSlotNumber() : int{
|
||||
return match($this){
|
||||
self::EAST => BrewingStandInventory::SLOT_BOTTLE_LEFT,
|
||||
self::NORTHWEST => BrewingStandInventory::SLOT_BOTTLE_MIDDLE,
|
||||
self::SOUTHWEST => BrewingStandInventory::SLOT_BOTTLE_RIGHT
|
||||
self::EAST => BrewingStandInventoryWindow::SLOT_BOTTLE_LEFT,
|
||||
self::NORTHWEST => BrewingStandInventoryWindow::SLOT_BOTTLE_MIDDLE,
|
||||
self::SOUTHWEST => BrewingStandInventoryWindow::SLOT_BOTTLE_RIGHT
|
||||
};
|
||||
}
|
||||
}
|
||||
|
44
src/block/utils/Container.php
Normal file
44
src/block/utils/Container.php
Normal file
@ -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\block\utils;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
|
||||
/**
|
||||
* Blocks which have an associated inventory of contents
|
||||
* Default implementation provided by {@see ContainerTrait}
|
||||
*/
|
||||
interface Container extends MenuAccessor{
|
||||
/**
|
||||
* Returns whether an item with the given key as its custom name can be used to access the container's contents.
|
||||
*/
|
||||
public function canOpenWith(string $key) : bool;
|
||||
|
||||
/**
|
||||
* Returns the inventory of this container.
|
||||
* Note: This may return NULL if the container's tile was missing or incorrect. This is rare, but may occur as a
|
||||
* result of plugins incorrectly creating blocks, or legacy world data.
|
||||
*/
|
||||
public function getInventory() : ?Inventory;
|
||||
}
|
78
src/block/utils/ContainerTrait.php
Normal file
78
src/block/utils/ContainerTrait.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?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\block\utils;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\block\inventory\window\BlockInventoryWindow;
|
||||
use pocketmine\block\tile\ContainerTile;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
trait ContainerTrait{
|
||||
/**
|
||||
* @see Block::onInteract()
|
||||
*/
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player && !$this->isOpeningObstructed() && $this->canOpenWith($item->getCustomName())){
|
||||
$this->openToUnchecked($player);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
|
||||
return new BlockInventoryWindow($player, $inventory, $position);
|
||||
}
|
||||
|
||||
public function isOpeningObstructed() : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
abstract protected function getPosition() : Position;
|
||||
|
||||
protected function getTile() : ?ContainerTile{
|
||||
$pos = $this->getPosition();
|
||||
$tile = $pos->getWorld()->getTile($pos);
|
||||
return $tile instanceof ContainerTile ? $tile : null;
|
||||
}
|
||||
|
||||
public function canOpenWith(string $key) : bool{
|
||||
//TODO: maybe we can bring the key to the block in readStateFromWorld()?
|
||||
return $this->getTile()?->canOpenWith($key) ?? false;
|
||||
}
|
||||
|
||||
public function openToUnchecked(Player $player) : bool{
|
||||
$tile = $this->getTile();
|
||||
return $tile !== null && $player->setCurrentWindow($this->newMenu($player, $tile->getInventory(), $this->getPosition()));
|
||||
}
|
||||
|
||||
public function getInventory() : ?Inventory{
|
||||
return $this->getTile()?->getInventory();
|
||||
}
|
||||
}
|
@ -21,16 +21,25 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\inventory;
|
||||
namespace pocketmine\block\utils;
|
||||
|
||||
use pocketmine\crafting\CraftingGrid;
|
||||
use pocketmine\player\Player;
|
||||
|
||||
final class PlayerCraftingInventory extends CraftingGrid implements TemporaryInventory{
|
||||
/**
|
||||
* Blocks which open a menu when interacted with
|
||||
* This could be a container menu, or a menu that otherwise deals with items, such as a crafting menu
|
||||
*/
|
||||
interface MenuAccessor{
|
||||
/**
|
||||
* Returns whether the block's ability to open the menu is currently obstructed (e.g. by nearby blocks).
|
||||
*/
|
||||
public function isOpeningObstructed() : bool;
|
||||
|
||||
public function __construct(private Player $holder){
|
||||
parent::__construct(CraftingGrid::SIZE_SMALL);
|
||||
}
|
||||
|
||||
public function getHolder() : Player{ return $this->holder; }
|
||||
/**
|
||||
* Opens the menu to the player.
|
||||
* Note: No preconditions are checked. Do not check for obstruction or locks here.
|
||||
*
|
||||
* Returns true if successful, false otherwise (e.g. event cancelled, container missing)
|
||||
*/
|
||||
public function openToUnchecked(Player $player) : bool;
|
||||
}
|
58
src/block/utils/MenuAccessorTrait.php
Normal file
58
src/block/utils/MenuAccessorTrait.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?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\block\utils;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
trait MenuAccessorTrait{
|
||||
|
||||
/**
|
||||
* @see Block::onInteract()
|
||||
*/
|
||||
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($player instanceof Player && !$this->isOpeningObstructed()){
|
||||
$this->openToUnchecked($player);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract protected function newMenu(Player $player, Position $position) : InventoryWindow;
|
||||
|
||||
public function isOpeningObstructed() : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
abstract protected function getPosition() : Position;
|
||||
|
||||
public function openToUnchecked(Player $player) : bool{
|
||||
return $player->setCurrentWindow($this->newMenu($player, $this->getPosition()));
|
||||
}
|
||||
}
|
@ -56,7 +56,7 @@ class EnchantCommand extends VanillaCommand{
|
||||
return true;
|
||||
}
|
||||
|
||||
$item = $player->getInventory()->getItemInHand();
|
||||
$item = $player->getMainHandItem();
|
||||
|
||||
if($item->isNull()){
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_enchant_noItem());
|
||||
@ -79,7 +79,7 @@ class EnchantCommand extends VanillaCommand{
|
||||
|
||||
//this is necessary to deal with enchanted books, which are a different item type than regular books
|
||||
$enchantedItem = EnchantingHelper::enchantItem($item, [new EnchantmentInstance($enchantment, $level)]);
|
||||
$player->getInventory()->setItemInHand($enchantedItem);
|
||||
$player->setMainHandItem($enchantedItem);
|
||||
|
||||
self::broadcastCommandMessage($sender, KnownTranslationFactory::commands_enchant_success($player->getName()));
|
||||
return true;
|
||||
|
@ -29,7 +29,7 @@ use function max;
|
||||
use function min;
|
||||
use const PHP_INT_MAX;
|
||||
|
||||
abstract class CraftingGrid extends SimpleInventory{
|
||||
class CraftingGrid extends SimpleInventory{
|
||||
public const SIZE_SMALL = 2;
|
||||
public const SIZE_BIG = 3;
|
||||
|
||||
|
@ -243,10 +243,10 @@ class ExperienceManager{
|
||||
//TODO: replace this with a more generic equipment getting/setting interface
|
||||
$equipment = [];
|
||||
|
||||
if(($item = $this->entity->getInventory()->getItemInHand()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
|
||||
if(($item = $this->entity->getMainHandItem()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
|
||||
$equipment[$mainHandIndex] = $item;
|
||||
}
|
||||
if(($item = $this->entity->getOffHandInventory()->getItem(0)) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
|
||||
if(($item = $this->entity->getOffHandItem()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
|
||||
$equipment[$offHandIndex] = $item;
|
||||
}
|
||||
foreach($this->entity->getArmorInventory()->getContents() as $k => $armorItem){
|
||||
@ -263,9 +263,9 @@ class ExperienceManager{
|
||||
$xpValue -= (int) ceil($repairAmount / 2);
|
||||
|
||||
if($k === $mainHandIndex){
|
||||
$this->entity->getInventory()->setItemInHand($repairItem);
|
||||
$this->entity->setMainHandItem($repairItem);
|
||||
}elseif($k === $offHandIndex){
|
||||
$this->entity->getOffHandInventory()->setItem(0, $repairItem);
|
||||
$this->entity->setOffHandItem($repairItem);
|
||||
}else{
|
||||
$this->entity->getArmorInventory()->setItem($k, $repairItem);
|
||||
}
|
||||
|
@ -32,11 +32,10 @@ use pocketmine\entity\projectile\ProjectileSource;
|
||||
use pocketmine\event\entity\EntityDamageEvent;
|
||||
use pocketmine\event\entity\EntityExhaustEvent;
|
||||
use pocketmine\inventory\CallbackInventoryListener;
|
||||
use pocketmine\inventory\Hotbar;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\InventoryHolder;
|
||||
use pocketmine\inventory\PlayerEnderInventory;
|
||||
use pocketmine\inventory\PlayerInventory;
|
||||
use pocketmine\inventory\PlayerOffHandInventory;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\item\enchantment\EnchantingHelper;
|
||||
use pocketmine\item\enchantment\VanillaEnchantments;
|
||||
use pocketmine\item\Item;
|
||||
@ -101,9 +100,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
|
||||
public function getNetworkTypeId() : string{ return EntityIds::PLAYER; }
|
||||
|
||||
protected PlayerInventory $inventory;
|
||||
protected PlayerOffHandInventory $offHandInventory;
|
||||
protected PlayerEnderInventory $enderInventory;
|
||||
protected Hotbar $hotbar;
|
||||
protected Inventory $inventory;
|
||||
protected Inventory $offHandInventory;
|
||||
protected Inventory $enderInventory;
|
||||
|
||||
protected UuidInterface $uuid;
|
||||
|
||||
@ -237,13 +237,33 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
return min(100, 7 * $this->xpManager->getXpLevel());
|
||||
}
|
||||
|
||||
public function getInventory() : PlayerInventory{
|
||||
public function getHotbar() : Hotbar{
|
||||
return $this->hotbar;
|
||||
}
|
||||
|
||||
public function getInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function getOffHandInventory() : PlayerOffHandInventory{ return $this->offHandInventory; }
|
||||
public function getMainHandItem() : Item{
|
||||
return $this->inventory->getItem($this->hotbar->getSelectedIndex());
|
||||
}
|
||||
|
||||
public function getEnderInventory() : PlayerEnderInventory{
|
||||
public function setMainHandItem(Item $item) : void{
|
||||
$this->inventory->setItem($this->hotbar->getSelectedIndex(), $item);
|
||||
}
|
||||
|
||||
public function getOffHandItem() : Item{
|
||||
return $this->offHandInventory->getItem(0);
|
||||
}
|
||||
|
||||
public function setOffHandItem(Item $item) : void{
|
||||
$this->offHandInventory->setItem(0, $item);
|
||||
}
|
||||
|
||||
public function getOffHandInventory() : Inventory{ return $this->offHandInventory; }
|
||||
|
||||
public function getEnderInventory() : Inventory{
|
||||
return $this->enderInventory;
|
||||
}
|
||||
|
||||
@ -274,25 +294,27 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
$this->hungerManager = new HungerManager($this);
|
||||
$this->xpManager = new ExperienceManager($this);
|
||||
|
||||
$this->inventory = new PlayerInventory($this);
|
||||
$this->inventory = new SimpleInventory(36);
|
||||
$this->hotbar = new Hotbar();
|
||||
|
||||
$syncHeldItem = fn() => NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
|
||||
);
|
||||
$this->inventory->getListeners()->add(new CallbackInventoryListener(
|
||||
function(Inventory $unused, int $slot, Item $unused2) use ($syncHeldItem) : void{
|
||||
if($slot === $this->inventory->getHeldItemIndex()){
|
||||
if($slot === $this->hotbar->getSelectedIndex()){
|
||||
$syncHeldItem();
|
||||
}
|
||||
},
|
||||
function(Inventory $unused, array $oldItems) use ($syncHeldItem) : void{
|
||||
if(array_key_exists($this->inventory->getHeldItemIndex(), $oldItems)){
|
||||
if(array_key_exists($this->hotbar->getSelectedIndex(), $oldItems)){
|
||||
$syncHeldItem();
|
||||
}
|
||||
}
|
||||
));
|
||||
$this->offHandInventory = new PlayerOffHandInventory($this);
|
||||
$this->enderInventory = new PlayerEnderInventory($this);
|
||||
$this->offHandInventory = new SimpleInventory(1);
|
||||
$this->enderInventory = new SimpleInventory(27);
|
||||
$this->initHumanData($nbt);
|
||||
|
||||
$inventoryTag = $nbt->getListTag(self::TAG_INVENTORY);
|
||||
@ -317,7 +339,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
}
|
||||
$offHand = $nbt->getCompoundTag(self::TAG_OFF_HAND_ITEM);
|
||||
if($offHand !== null){
|
||||
$this->offHandInventory->setItem(0, Item::nbtDeserialize($offHand));
|
||||
$this->setOffHandItem(Item::nbtDeserialize($offHand));
|
||||
}
|
||||
$this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
@ -335,8 +357,9 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems);
|
||||
}
|
||||
|
||||
$this->inventory->setHeldItemIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0));
|
||||
$this->inventory->getHeldItemIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->hotbar->setSelectedIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0));
|
||||
//TODO: cyclic reference
|
||||
$this->hotbar->getSelectedIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
|
||||
));
|
||||
@ -376,7 +399,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
|
||||
$type = $source->getCause();
|
||||
if($type !== EntityDamageEvent::CAUSE_SUICIDE && $type !== EntityDamageEvent::CAUSE_VOID
|
||||
&& ($this->inventory->getItemInHand() instanceof Totem || $this->offHandInventory->getItem(0) instanceof Totem)){
|
||||
&& ($this->getMainHandItem() instanceof Totem || $this->getOffHandItem() instanceof Totem)){
|
||||
|
||||
$compensation = $this->getHealth() - $source->getFinalDamage() - 1;
|
||||
if($compensation <= -1){
|
||||
@ -398,13 +421,13 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
$this->broadcastAnimation(new TotemUseAnimation($this));
|
||||
$this->broadcastSound(new TotemUseSound());
|
||||
|
||||
$hand = $this->inventory->getItemInHand();
|
||||
$hand = $this->getMainHandItem();
|
||||
if($hand instanceof Totem){
|
||||
$hand->pop(); //Plugins could alter max stack size
|
||||
$this->inventory->setItemInHand($hand);
|
||||
}elseif(($offHand = $this->offHandInventory->getItem(0)) instanceof Totem){
|
||||
$this->setMainHandItem($hand);
|
||||
}elseif(($offHand = $this->getOffHandItem()) instanceof Totem){
|
||||
$offHand->pop();
|
||||
$this->offHandInventory->setItem(0, $offHand);
|
||||
$this->setOffHandItem($offHand);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -434,8 +457,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
$nbt->setTag(self::TAG_INVENTORY, $inventoryTag);
|
||||
|
||||
//Normal inventory
|
||||
$slotCount = $this->inventory->getSize() + $this->inventory->getHotbarSize();
|
||||
for($slot = $this->inventory->getHotbarSize(); $slot < $slotCount; ++$slot){
|
||||
$slotCount = $this->inventory->getSize() + $this->hotbar->getSize();
|
||||
for($slot = $this->hotbar->getSize(); $slot < $slotCount; ++$slot){
|
||||
$item = $this->inventory->getItem($slot - 9);
|
||||
if(!$item->isNull()){
|
||||
$inventoryTag->push($item->nbtSerialize($slot));
|
||||
@ -450,9 +473,9 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
}
|
||||
}
|
||||
|
||||
$nbt->setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->inventory->getHeldItemIndex());
|
||||
$nbt->setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->hotbar->getSelectedIndex());
|
||||
|
||||
$offHandItem = $this->offHandInventory->getItem(0);
|
||||
$offHandItem = $this->getOffHandItem();
|
||||
if(!$offHandItem->isNull()){
|
||||
$nbt->setTag(self::TAG_OFF_HAND_ITEM, $offHandItem->nbtSerialize());
|
||||
}
|
||||
@ -504,7 +527,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
$this->location->pitch,
|
||||
$this->location->yaw,
|
||||
$this->location->yaw, //TODO: head yaw
|
||||
ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($this->getInventory()->getItemInHand())),
|
||||
ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($this->getMainHandItem())),
|
||||
GameMode::SURVIVAL,
|
||||
$this->getAllNetworkData(),
|
||||
new PropertySyncData([], []),
|
||||
@ -539,8 +562,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
}
|
||||
|
||||
protected function onDispose() : void{
|
||||
$this->hotbar->getSelectedIndexChangeListeners()->clear();
|
||||
$this->inventory->removeAllViewers();
|
||||
$this->inventory->getHeldItemIndexChangeListeners()->clear();
|
||||
$this->offHandInventory->removeAllViewers();
|
||||
$this->enderInventory->removeAllViewers();
|
||||
parent::onDispose();
|
||||
@ -548,9 +571,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
|
||||
protected function destroyCycles() : void{
|
||||
unset(
|
||||
$this->inventory,
|
||||
$this->offHandInventory,
|
||||
$this->enderInventory,
|
||||
$this->hungerManager,
|
||||
$this->xpManager
|
||||
);
|
||||
|
@ -152,7 +152,7 @@ abstract class Living extends Entity{
|
||||
$this->effectManager->getEffectAddHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
|
||||
$this->effectManager->getEffectRemoveHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
|
||||
|
||||
$this->armorInventory = new ArmorInventory($this);
|
||||
$this->armorInventory = new ArmorInventory();
|
||||
//TODO: load/save armor inventory contents
|
||||
$this->armorInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
@ -995,7 +995,6 @@ abstract class Living extends Entity{
|
||||
|
||||
protected function destroyCycles() : void{
|
||||
unset(
|
||||
$this->armorInventory,
|
||||
$this->effectManager
|
||||
);
|
||||
parent::destroyCycles();
|
||||
|
@ -322,7 +322,7 @@ class ItemEntity extends Entity{
|
||||
|
||||
$item = $this->getItem();
|
||||
$playerInventory = match(true){
|
||||
$player->getOffHandInventory()->getItem(0)->canStackWith($item) && $player->getOffHandInventory()->getAddableItemQuantity($item) > 0 => $player->getOffHandInventory(),
|
||||
$player->getOffHandItem()->canStackWith($item) && $player->getOffHandInventory()->getAddableItemQuantity($item) > 0 => $player->getOffHandInventory(),
|
||||
$player->getInventory()->getAddableItemQuantity($item) > 0 => $player->getInventory(),
|
||||
default => null
|
||||
};
|
||||
|
@ -172,7 +172,7 @@ class Arrow extends Projectile{
|
||||
$item = VanillaItems::ARROW();
|
||||
$playerInventory = match(true){
|
||||
!$player->hasFiniteResources() => null, //arrows are not picked up in creative
|
||||
$player->getOffHandInventory()->getItem(0)->canStackWith($item) && $player->getOffHandInventory()->canAddItem($item) => $player->getOffHandInventory(),
|
||||
$player->getOffHandItem()->canStackWith($item) && $player->getOffHandInventory()->canAddItem($item) => $player->getOffHandInventory(),
|
||||
$player->getInventory()->canAddItem($item) => $player->getInventory(),
|
||||
default => null
|
||||
};
|
||||
|
@ -23,12 +23,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event\inventory;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
|
||||
class InventoryCloseEvent extends InventoryEvent{
|
||||
public function __construct(
|
||||
Inventory $inventory,
|
||||
InventoryWindow $inventory,
|
||||
private Player $who
|
||||
){
|
||||
parent::__construct($inventory);
|
||||
|
@ -27,15 +27,15 @@ declare(strict_types=1);
|
||||
namespace pocketmine\event\inventory;
|
||||
|
||||
use pocketmine\event\Event;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
|
||||
abstract class InventoryEvent extends Event{
|
||||
public function __construct(
|
||||
protected Inventory $inventory
|
||||
protected InventoryWindow $inventory
|
||||
){}
|
||||
|
||||
public function getInventory() : Inventory{
|
||||
public function getInventory() : InventoryWindow{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
@ -43,6 +43,6 @@ abstract class InventoryEvent extends Event{
|
||||
* @return Player[]
|
||||
*/
|
||||
public function getViewers() : array{
|
||||
return $this->inventory->getViewers();
|
||||
return $this->inventory->getInventory()->getViewers();
|
||||
}
|
||||
}
|
||||
|
@ -25,14 +25,14 @@ namespace pocketmine\event\inventory;
|
||||
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\CancellableTrait;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
|
||||
class InventoryOpenEvent extends InventoryEvent implements Cancellable{
|
||||
use CancellableTrait;
|
||||
|
||||
public function __construct(
|
||||
Inventory $inventory,
|
||||
InventoryWindow $inventory,
|
||||
private Player $who
|
||||
){
|
||||
parent::__construct($inventory);
|
||||
|
@ -23,10 +23,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event\player;
|
||||
|
||||
use pocketmine\block\inventory\EnchantInventory;
|
||||
use pocketmine\block\inventory\window\EnchantingTableInventoryWindow;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\CancellableTrait;
|
||||
use pocketmine\event\Event;
|
||||
use pocketmine\item\enchantment\EnchantingOption;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\Utils;
|
||||
@ -44,13 +43,13 @@ class PlayerEnchantingOptionsRequestEvent extends PlayerEvent implements Cancell
|
||||
*/
|
||||
public function __construct(
|
||||
Player $player,
|
||||
private readonly EnchantInventory $inventory,
|
||||
private readonly EnchantingTableInventoryWindow $inventory,
|
||||
private array $options
|
||||
){
|
||||
$this->player = $player;
|
||||
}
|
||||
|
||||
public function getInventory() : EnchantInventory{
|
||||
public function getInventory() : EnchantingTableInventoryWindow{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,6 @@ declare(strict_types=1);
|
||||
namespace pocketmine\inventory;
|
||||
|
||||
use pocketmine\block\BlockTypeIds;
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\inventory\transaction\action\validator\CallbackSlotValidator;
|
||||
use pocketmine\inventory\transaction\TransactionValidationException;
|
||||
use pocketmine\item\Armor;
|
||||
@ -37,18 +36,12 @@ class ArmorInventory extends SimpleInventory{
|
||||
public const SLOT_LEGS = 2;
|
||||
public const SLOT_FEET = 3;
|
||||
|
||||
public function __construct(
|
||||
protected Living $holder
|
||||
){
|
||||
public function __construct(){
|
||||
parent::__construct(4);
|
||||
|
||||
$this->validators->add(new CallbackSlotValidator(self::validate(...)));
|
||||
}
|
||||
|
||||
public function getHolder() : Living{
|
||||
return $this->holder;
|
||||
}
|
||||
|
||||
public function getHelmet() : Item{
|
||||
return $this->getItem(self::SLOT_HEAD);
|
||||
}
|
||||
|
@ -102,28 +102,14 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
|
||||
|
||||
$listeners = $this->listeners->toArray();
|
||||
$this->listeners->clear();
|
||||
$viewers = $this->viewers;
|
||||
$this->viewers = [];
|
||||
|
||||
$this->internalSetContents($items);
|
||||
|
||||
$this->listeners->add(...$listeners); //don't directly write, in case listeners were added while operation was in progress
|
||||
foreach($viewers as $id => $viewer){
|
||||
$this->viewers[$id] = $viewer;
|
||||
}
|
||||
|
||||
$this->onContentChange($oldContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for utility functions which search the inventory.
|
||||
* TODO: make this abstract instead of providing a slow default implementation (BC break)
|
||||
*/
|
||||
protected function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
|
||||
$item = $this->getItem($slot);
|
||||
return $item->equals($test, true, $checkTags) ? $item->getCount() : 0;
|
||||
}
|
||||
|
||||
public function contains(Item $item) : bool{
|
||||
$count = max(1, $item->getCount());
|
||||
$checkTags = $item->hasNamedTag();
|
||||
@ -351,7 +337,7 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
|
||||
*/
|
||||
public function removeAllViewers() : void{
|
||||
foreach($this->viewers as $hash => $viewer){
|
||||
if($viewer->getCurrentWindow() === $this){ //this might not be the case for the player's own inventory
|
||||
if($viewer->getCurrentWindow()?->getInventory() === $this){ //this might not be the case for the player's own inventory
|
||||
$viewer->removeCurrentWindow();
|
||||
}
|
||||
unset($this->viewers[$hash]);
|
||||
@ -370,13 +356,6 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
|
||||
foreach($this->listeners as $listener){
|
||||
$listener->onSlotChange($this, $index, $before);
|
||||
}
|
||||
foreach($this->viewers as $viewer){
|
||||
$invManager = $viewer->getNetworkSession()->getInvManager();
|
||||
if($invManager === null){
|
||||
continue;
|
||||
}
|
||||
$invManager->onSlotChange($this, $index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -387,14 +366,6 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
|
||||
foreach($this->listeners as $listener){
|
||||
$listener->onContentChange($this, $itemsBefore);
|
||||
}
|
||||
|
||||
foreach($this->getViewers() as $viewer){
|
||||
$invManager = $viewer->getNetworkSession()->getInvManager();
|
||||
if($invManager === null){
|
||||
continue;
|
||||
}
|
||||
$invManager->syncContents($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function slotExists(int $slot) : bool{
|
||||
|
194
src/inventory/CombinedInventoryProxy.php
Normal file
194
src/inventory/CombinedInventoryProxy.php
Normal file
@ -0,0 +1,194 @@
|
||||
<?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\inventory;
|
||||
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function array_fill_keys;
|
||||
use function array_keys;
|
||||
use function count;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* Allows interacting with several separate inventories via a unified interface
|
||||
* Mainly used for double chests, but could be used for other custom use cases
|
||||
*/
|
||||
final class CombinedInventoryProxy extends BaseInventory{
|
||||
|
||||
private readonly int $size;
|
||||
|
||||
/**
|
||||
* @var Inventory[]
|
||||
* @phpstan-var array<int, Inventory>
|
||||
*/
|
||||
private array $backingInventories = [];
|
||||
/**
|
||||
* @var Inventory[]
|
||||
* @phpstan-var array<int, Inventory>
|
||||
*/
|
||||
private array $slotToInventoryMap = [];
|
||||
/**
|
||||
* @var int[]
|
||||
* @phpstan-var array<int, int>
|
||||
*/
|
||||
private array $inventoryToOffsetMap = [];
|
||||
|
||||
private InventoryListener $backingInventoryListener;
|
||||
private bool $modifyingBackingInventory = false;
|
||||
|
||||
/**
|
||||
* @phpstan-param Inventory[] $backingInventories
|
||||
*/
|
||||
public function __construct(
|
||||
array $backingInventories
|
||||
){
|
||||
parent::__construct();
|
||||
foreach($backingInventories as $backingInventory){
|
||||
$this->backingInventories[spl_object_id($backingInventory)] = $backingInventory;
|
||||
}
|
||||
$combinedSize = 0;
|
||||
foreach($this->backingInventories as $inventory){
|
||||
$size = $inventory->getSize();
|
||||
|
||||
$this->inventoryToOffsetMap[spl_object_id($inventory)] = $combinedSize;
|
||||
for($slot = 0; $slot < $size; $slot++){
|
||||
$this->slotToInventoryMap[$combinedSize + $slot] = $inventory;
|
||||
}
|
||||
|
||||
$combinedSize += $size;
|
||||
}
|
||||
$this->size = $combinedSize;
|
||||
|
||||
$weakThis = \WeakReference::create($this);
|
||||
$getThis = static fn() => $weakThis->get() ?? throw new AssumptionFailedError("Listener should've been unregistered in __destruct()");
|
||||
|
||||
$this->backingInventoryListener = new CallbackInventoryListener(
|
||||
onSlotChange: static function(Inventory $inventory, int $slot, Item $oldItem) use ($getThis) : void{
|
||||
$strongThis = $getThis();
|
||||
if($strongThis->modifyingBackingInventory){
|
||||
return;
|
||||
}
|
||||
|
||||
$offset = $strongThis->inventoryToOffsetMap[spl_object_id($inventory)];
|
||||
$strongThis->onSlotChange($offset + $slot, $oldItem);
|
||||
},
|
||||
onContentChange: static function(Inventory $inventory, array $oldContents) use ($getThis) : void{
|
||||
$strongThis = $getThis();
|
||||
if($strongThis->modifyingBackingInventory){
|
||||
return;
|
||||
}
|
||||
|
||||
if(count($strongThis->backingInventories) === 1){
|
||||
$strongThis->onContentChange($oldContents);
|
||||
}else{
|
||||
$offset = $strongThis->inventoryToOffsetMap[spl_object_id($inventory)];
|
||||
for($slot = 0, $limit = $inventory->getSize(); $slot < $limit; $slot++){
|
||||
$strongThis->onSlotChange($offset + $slot, $oldContents[$slot] ?? VanillaItems::AIR());
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
foreach($this->backingInventories as $inventory){
|
||||
$inventory->getListeners()->add($this->backingInventoryListener);
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct(){
|
||||
foreach($this->backingInventories as $inventory){
|
||||
$inventory->getListeners()->remove($this->backingInventoryListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{Inventory, int}
|
||||
*/
|
||||
private function getInventory(int $slot) : array{
|
||||
$inventory = $this->slotToInventoryMap[$slot] ?? throw new \InvalidArgumentException("Invalid combined inventory slot $slot");
|
||||
$actualSlot = $slot - $this->inventoryToOffsetMap[spl_object_id($inventory)];
|
||||
return [$inventory, $actualSlot];
|
||||
}
|
||||
|
||||
protected function internalSetItem(int $index, Item $item) : void{
|
||||
[$inventory, $actualSlot] = $this->getInventory($index);
|
||||
|
||||
//Make sure our backing listener doesn't dispatch double updates to our own listeners
|
||||
$this->modifyingBackingInventory = true;
|
||||
try{
|
||||
$inventory->setItem($actualSlot, $item);
|
||||
}finally{
|
||||
$this->modifyingBackingInventory = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function internalSetContents(array $items) : void{
|
||||
$contentsByInventory = array_fill_keys(array_keys($this->backingInventories), []);
|
||||
foreach($items as $i => $item){
|
||||
[$inventory, $actualSlot] = $this->getInventory($i);
|
||||
$contentsByInventory[spl_object_id($inventory)][$actualSlot] = $item;
|
||||
}
|
||||
foreach($contentsByInventory as $splObjectId => $backingInventoryContents){
|
||||
$backingInventory = $this->backingInventories[$splObjectId];
|
||||
|
||||
//Make sure our backing listener doesn't dispatch double updates to our own listeners
|
||||
$this->modifyingBackingInventory = true;
|
||||
try{
|
||||
$backingInventory->setContents($backingInventoryContents);
|
||||
}finally{
|
||||
$this->modifyingBackingInventory = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getSize() : int{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
public function getItem(int $index) : Item{
|
||||
[$inventory, $actualSlot] = $this->getInventory($index);
|
||||
return $inventory->getItem($actualSlot);
|
||||
}
|
||||
|
||||
public function getContents(bool $includeEmpty = false) : array{
|
||||
$result = [];
|
||||
foreach($this->backingInventories as $inventory){
|
||||
$offset = $this->inventoryToOffsetMap[spl_object_id($inventory)];
|
||||
foreach($inventory->getContents($includeEmpty) as $i => $item){
|
||||
$result[$offset + $i] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
|
||||
[$inventory, $actualSlot] = $this->getInventory($slot);
|
||||
return $inventory->getMatchingItemCount($actualSlot, $test, $checkTags);
|
||||
}
|
||||
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
[$inventory, $actualSlot] = $this->getInventory($index);
|
||||
return $inventory->isSlotEmpty($actualSlot);
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
<?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\inventory;
|
||||
|
||||
use pocketmine\item\Item;
|
||||
|
||||
/**
|
||||
* An inventory which is backed by another inventory, and acts as a proxy to that inventory.
|
||||
*/
|
||||
class DelegateInventory extends BaseInventory{
|
||||
private InventoryListener $inventoryListener;
|
||||
private bool $backingInventoryChanging = false;
|
||||
|
||||
public function __construct(
|
||||
private Inventory $backingInventory
|
||||
){
|
||||
parent::__construct();
|
||||
$weakThis = \WeakReference::create($this);
|
||||
$this->backingInventory->getListeners()->add($this->inventoryListener = new CallbackInventoryListener(
|
||||
static function(Inventory $unused, int $slot, Item $oldItem) use ($weakThis) : void{
|
||||
if(($strongThis = $weakThis->get()) !== null){
|
||||
$strongThis->backingInventoryChanging = true;
|
||||
try{
|
||||
$strongThis->onSlotChange($slot, $oldItem);
|
||||
}finally{
|
||||
$strongThis->backingInventoryChanging = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
static function(Inventory $unused, array $oldContents) use ($weakThis) : void{
|
||||
if(($strongThis = $weakThis->get()) !== null){
|
||||
$strongThis->backingInventoryChanging = true;
|
||||
try{
|
||||
$strongThis->onContentChange($oldContents);
|
||||
}finally{
|
||||
$strongThis->backingInventoryChanging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
public function __destruct(){
|
||||
$this->backingInventory->getListeners()->remove($this->inventoryListener);
|
||||
}
|
||||
|
||||
public function getSize() : int{
|
||||
return $this->backingInventory->getSize();
|
||||
}
|
||||
|
||||
public function getItem(int $index) : Item{
|
||||
return $this->backingInventory->getItem($index);
|
||||
}
|
||||
|
||||
protected function internalSetItem(int $index, Item $item) : void{
|
||||
$this->backingInventory->setItem($index, $item);
|
||||
}
|
||||
|
||||
public function getContents(bool $includeEmpty = false) : array{
|
||||
return $this->backingInventory->getContents($includeEmpty);
|
||||
}
|
||||
|
||||
protected function internalSetContents(array $items) : void{
|
||||
$this->backingInventory->setContents($items);
|
||||
}
|
||||
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
return $this->backingInventory->isSlotEmpty($index);
|
||||
}
|
||||
|
||||
protected function onSlotChange(int $index, Item $before) : void{
|
||||
if($this->backingInventoryChanging){
|
||||
parent::onSlotChange($index, $before);
|
||||
}
|
||||
}
|
||||
|
||||
protected function onContentChange(array $itemsBefore) : void{
|
||||
if($this->backingInventoryChanging){
|
||||
parent::onContentChange($itemsBefore);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,30 +23,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\inventory;
|
||||
|
||||
use pocketmine\entity\Human;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\ObjectSet;
|
||||
|
||||
class PlayerInventory extends SimpleInventory{
|
||||
|
||||
protected Human $holder;
|
||||
protected int $itemInHandIndex = 0;
|
||||
final class Hotbar{
|
||||
protected int $selectedIndex = 0;
|
||||
|
||||
/**
|
||||
* @var \Closure[]|ObjectSet
|
||||
* @phpstan-var ObjectSet<\Closure(int $oldIndex) : void>
|
||||
*/
|
||||
protected ObjectSet $heldItemIndexChangeListeners;
|
||||
protected ObjectSet $selectedIndexChangeListeners;
|
||||
|
||||
public function __construct(Human $player){
|
||||
$this->holder = $player;
|
||||
$this->heldItemIndexChangeListeners = new ObjectSet();
|
||||
parent::__construct(36);
|
||||
public function __construct(
|
||||
private int $size = 9
|
||||
){
|
||||
$this->selectedIndexChangeListeners = new ObjectSet();
|
||||
}
|
||||
|
||||
public function isHotbarSlot(int $slot) : bool{
|
||||
return $slot >= 0 && $slot < $this->getHotbarSize();
|
||||
return $slot >= 0 && $slot < $this->getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,25 +49,15 @@ class PlayerInventory extends SimpleInventory{
|
||||
*/
|
||||
private function throwIfNotHotbarSlot(int $slot) : void{
|
||||
if(!$this->isHotbarSlot($slot)){
|
||||
throw new \InvalidArgumentException("$slot is not a valid hotbar slot index (expected 0 - " . ($this->getHotbarSize() - 1) . ")");
|
||||
throw new \InvalidArgumentException("$slot is not a valid hotbar slot index (expected 0 - " . ($this->getSize() - 1) . ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the item in the specified hotbar slot.
|
||||
*
|
||||
* @throws \InvalidArgumentException if the hotbar slot index is out of range
|
||||
*/
|
||||
public function getHotbarSlotItem(int $hotbarSlot) : Item{
|
||||
$this->throwIfNotHotbarSlot($hotbarSlot);
|
||||
return $this->getItem($hotbarSlot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hotbar slot number the holder is currently holding.
|
||||
*/
|
||||
public function getHeldItemIndex() : int{
|
||||
return $this->itemInHandIndex;
|
||||
public function getSelectedIndex() : int{
|
||||
return $this->selectedIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,13 +67,13 @@ class PlayerInventory extends SimpleInventory{
|
||||
*
|
||||
* @throws \InvalidArgumentException if the hotbar slot is out of range
|
||||
*/
|
||||
public function setHeldItemIndex(int $hotbarSlot) : void{
|
||||
public function setSelectedIndex(int $hotbarSlot) : void{
|
||||
$this->throwIfNotHotbarSlot($hotbarSlot);
|
||||
|
||||
$oldIndex = $this->itemInHandIndex;
|
||||
$this->itemInHandIndex = $hotbarSlot;
|
||||
$oldIndex = $this->selectedIndex;
|
||||
$this->selectedIndex = $hotbarSlot;
|
||||
|
||||
foreach($this->heldItemIndexChangeListeners as $callback){
|
||||
foreach($this->selectedIndexChangeListeners as $callback){
|
||||
$callback($oldIndex);
|
||||
}
|
||||
}
|
||||
@ -97,30 +82,12 @@ class PlayerInventory extends SimpleInventory{
|
||||
* @return \Closure[]|ObjectSet
|
||||
* @phpstan-return ObjectSet<\Closure(int $oldIndex) : void>
|
||||
*/
|
||||
public function getHeldItemIndexChangeListeners() : ObjectSet{ return $this->heldItemIndexChangeListeners; }
|
||||
|
||||
/**
|
||||
* Returns the currently-held item.
|
||||
*/
|
||||
public function getItemInHand() : Item{
|
||||
return $this->getHotbarSlotItem($this->itemInHandIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the item in the currently-held slot to the specified item.
|
||||
*/
|
||||
public function setItemInHand(Item $item) : void{
|
||||
$this->setItem($this->getHeldItemIndex(), $item);
|
||||
}
|
||||
public function getSelectedIndexChangeListeners() : ObjectSet{ return $this->selectedIndexChangeListeners; }
|
||||
|
||||
/**
|
||||
* Returns the number of slots in the hotbar.
|
||||
*/
|
||||
public function getHotbarSize() : int{
|
||||
return 9;
|
||||
}
|
||||
|
||||
public function getHolder() : Human{
|
||||
return $this->holder;
|
||||
public function getSize() : int{
|
||||
return $this->size;
|
||||
}
|
||||
}
|
@ -98,6 +98,13 @@ interface Inventory{
|
||||
*/
|
||||
public function getAddableItemQuantity(Item $item) : int;
|
||||
|
||||
/**
|
||||
* Returns the number of items in the inventory that match the given item.
|
||||
*
|
||||
* @param bool $checkTags If true, the NBT of the items will also be checked and must be the same to be counted.
|
||||
*/
|
||||
public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int;
|
||||
|
||||
/**
|
||||
* Returns whether the total amount of matching items is at least the stack size of the given item. Multiple stacks
|
||||
* of the same item are added together.
|
||||
@ -179,6 +186,11 @@ interface Inventory{
|
||||
*/
|
||||
public function getViewers() : array;
|
||||
|
||||
/**
|
||||
* Tells all Players viewing this inventory to stop viewing it and discard associated windows.
|
||||
*/
|
||||
public function removeAllViewers() : void;
|
||||
|
||||
/**
|
||||
* Called when a player opens this inventory.
|
||||
*/
|
||||
|
@ -1,37 +0,0 @@
|
||||
<?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\inventory;
|
||||
|
||||
use pocketmine\entity\Human;
|
||||
|
||||
final class PlayerOffHandInventory extends SimpleInventory{
|
||||
private Human $holder;
|
||||
|
||||
public function __construct(Human $player){
|
||||
$this->holder = $player;
|
||||
parent::__construct(1);
|
||||
}
|
||||
|
||||
public function getHolder() : Human{ return $this->holder; }
|
||||
}
|
@ -84,7 +84,7 @@ class SimpleInventory extends BaseInventory{
|
||||
}
|
||||
}
|
||||
|
||||
protected function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
|
||||
public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
|
||||
$slotItem = $this->slots[$slot];
|
||||
return $slotItem !== null && $slotItem->equals($test, true, $checkTags) ? $slotItem->getCount() : 0;
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\transaction\action\InventoryAction;
|
||||
use pocketmine\inventory\transaction\action\SlotChangeAction;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\Utils;
|
||||
use function array_values;
|
||||
@ -57,10 +58,10 @@ class InventoryTransaction{
|
||||
protected bool $hasExecuted = false;
|
||||
|
||||
/**
|
||||
* @var Inventory[]
|
||||
* @phpstan-var array<int, Inventory>
|
||||
* @var InventoryWindow[]
|
||||
* @phpstan-var array<int, InventoryWindow>
|
||||
*/
|
||||
protected array $inventories = [];
|
||||
protected array $inventoryWindows = [];
|
||||
|
||||
/**
|
||||
* @var InventoryAction[]
|
||||
@ -85,11 +86,11 @@ class InventoryTransaction{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Inventory[]
|
||||
* @phpstan-return array<int, Inventory>
|
||||
* @return InventoryWindow[]
|
||||
* @phpstan-return array<int, InventoryWindow>
|
||||
*/
|
||||
public function getInventories() : array{
|
||||
return $this->inventories;
|
||||
public function getInventoryWindows() : array{
|
||||
return $this->inventoryWindows;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,8 +112,8 @@ class InventoryTransaction{
|
||||
public function addAction(InventoryAction $action) : void{
|
||||
if(!isset($this->actions[$hash = spl_object_id($action)])){
|
||||
$this->actions[$hash] = $action;
|
||||
if($action instanceof SlotChangeAction && !isset($this->inventories[$inventoryId = spl_object_id($action->getInventory())])){
|
||||
$this->inventories[$inventoryId] = $action->getInventory();
|
||||
if($action instanceof SlotChangeAction && !isset($this->inventoryWindows[$inventoryId = spl_object_id($action->getInventoryWindow())])){
|
||||
$this->inventoryWindows[$inventoryId] = $action->getInventoryWindow();
|
||||
}
|
||||
}else{
|
||||
throw new \InvalidArgumentException("Tried to add the same action to a transaction twice");
|
||||
@ -185,8 +186,8 @@ class InventoryTransaction{
|
||||
|
||||
foreach($this->actions as $key => $action){
|
||||
if($action instanceof SlotChangeAction){
|
||||
$slotChanges[$h = (spl_object_hash($action->getInventory()) . "@" . $action->getSlot())][] = $action;
|
||||
$inventories[$h] = $action->getInventory();
|
||||
$slotChanges[$h = (spl_object_hash($action->getInventoryWindow()) . "@" . $action->getSlot())][] = $action;
|
||||
$inventories[$h] = $action->getInventoryWindow();
|
||||
$slots[$h] = $action->getSlot();
|
||||
}
|
||||
}
|
||||
@ -196,10 +197,11 @@ class InventoryTransaction{
|
||||
continue;
|
||||
}
|
||||
|
||||
$inventory = $inventories[$hash];
|
||||
$window = $inventories[$hash];
|
||||
$inventory = $window->getInventory();
|
||||
$slot = $slots[$hash];
|
||||
if(!$inventory->slotExists($slot)){ //this can get hit for crafting tables because the validation happens after this compaction
|
||||
throw new TransactionValidationException("Slot $slot does not exist in inventory " . get_class($inventory));
|
||||
throw new TransactionValidationException("Slot $slot does not exist in inventory window " . get_class($window));
|
||||
}
|
||||
$sourceItem = $inventory->getItem($slot);
|
||||
|
||||
@ -214,7 +216,7 @@ class InventoryTransaction{
|
||||
|
||||
if(!$targetItem->equalsExact($sourceItem)){
|
||||
//sometimes we get actions on the crafting grid whose source and target items are the same, so dump them
|
||||
$this->addAction(new SlotChangeAction($inventory, $slot, $sourceItem, $targetItem));
|
||||
$this->addAction(new SlotChangeAction($window, $slot, $sourceItem, $targetItem));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\transaction\action\SlotChangeAction;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
|
||||
/**
|
||||
* This class facilitates generating SlotChangeActions to build an inventory transaction.
|
||||
@ -35,7 +36,7 @@ use pocketmine\item\VanillaItems;
|
||||
* This allows you to use the normal Inventory API methods like addItem() and so on to build a transaction, without
|
||||
* modifying the original inventory.
|
||||
*/
|
||||
final class TransactionBuilderInventory extends BaseInventory{
|
||||
final class SlotChangeActionBuilder extends BaseInventory{
|
||||
|
||||
/**
|
||||
* @var \SplFixedArray|(Item|null)[]
|
||||
@ -44,14 +45,14 @@ final class TransactionBuilderInventory extends BaseInventory{
|
||||
private \SplFixedArray $changedSlots;
|
||||
|
||||
public function __construct(
|
||||
private Inventory $actualInventory
|
||||
private InventoryWindow $inventoryWindow
|
||||
){
|
||||
parent::__construct();
|
||||
$this->changedSlots = new \SplFixedArray($this->actualInventory->getSize());
|
||||
$this->changedSlots = new \SplFixedArray($this->inventoryWindow->getInventory()->getSize());
|
||||
}
|
||||
|
||||
public function getActualInventory() : Inventory{
|
||||
return $this->actualInventory;
|
||||
public function getInventoryWindow() : InventoryWindow{
|
||||
return $this->inventoryWindow;
|
||||
}
|
||||
|
||||
protected function internalSetContents(array $items) : void{
|
||||
@ -65,21 +66,21 @@ final class TransactionBuilderInventory extends BaseInventory{
|
||||
}
|
||||
|
||||
protected function internalSetItem(int $index, Item $item) : void{
|
||||
if(!$item->equalsExact($this->actualInventory->getItem($index))){
|
||||
if(!$item->equalsExact($this->inventoryWindow->getInventory()->getItem($index))){
|
||||
$this->changedSlots[$index] = $item->isNull() ? VanillaItems::AIR() : clone $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSize() : int{
|
||||
return $this->actualInventory->getSize();
|
||||
return $this->inventoryWindow->getInventory()->getSize();
|
||||
}
|
||||
|
||||
public function getItem(int $index) : Item{
|
||||
return $this->changedSlots[$index] !== null ? clone $this->changedSlots[$index] : $this->actualInventory->getItem($index);
|
||||
return $this->changedSlots[$index] !== null ? clone $this->changedSlots[$index] : $this->inventoryWindow->getInventory()->getItem($index);
|
||||
}
|
||||
|
||||
public function getContents(bool $includeEmpty = false) : array{
|
||||
$contents = $this->actualInventory->getContents($includeEmpty);
|
||||
$contents = $this->inventoryWindow->getInventory()->getContents($includeEmpty);
|
||||
foreach($this->changedSlots as $index => $item){
|
||||
if($item !== null){
|
||||
if($includeEmpty || !$item->isNull()){
|
||||
@ -92,16 +93,25 @@ final class TransactionBuilderInventory extends BaseInventory{
|
||||
return $contents;
|
||||
}
|
||||
|
||||
public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
|
||||
$slotItem = $this->changedSlots[$slot] ?? null;
|
||||
if($slotItem !== null){
|
||||
return $slotItem->equals($test, true, $checkTags) ? $slotItem->getCount() : 0;
|
||||
}
|
||||
return $this->inventoryWindow->getInventory()->getMatchingItemCount($slot, $test, $checkTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SlotChangeAction[]
|
||||
*/
|
||||
public function generateActions() : array{
|
||||
$result = [];
|
||||
$inventory = $this->inventoryWindow->getInventory();
|
||||
foreach($this->changedSlots as $index => $newItem){
|
||||
if($newItem !== null){
|
||||
$oldItem = $this->actualInventory->getItem($index);
|
||||
$oldItem = $inventory->getItem($index);
|
||||
if(!$newItem->equalsExact($oldItem)){
|
||||
$result[] = new SlotChangeAction($this->actualInventory, $index, $oldItem, $newItem);
|
||||
$result[] = new SlotChangeAction($this->inventoryWindow, $index, $oldItem, $newItem);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,13 +23,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\inventory\transaction;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\transaction\action\InventoryAction;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use function spl_object_id;
|
||||
|
||||
final class TransactionBuilder{
|
||||
|
||||
/** @var TransactionBuilderInventory[] */
|
||||
/** @var SlotChangeActionBuilder[] */
|
||||
private array $inventories = [];
|
||||
|
||||
/** @var InventoryAction[] */
|
||||
@ -39,9 +39,9 @@ final class TransactionBuilder{
|
||||
$this->extraActions[spl_object_id($action)] = $action;
|
||||
}
|
||||
|
||||
public function getInventory(Inventory $inventory) : TransactionBuilderInventory{
|
||||
public function getActionBuilder(InventoryWindow $inventory) : SlotChangeActionBuilder{
|
||||
$id = spl_object_id($inventory);
|
||||
return $this->inventories[$id] ??= new TransactionBuilderInventory($inventory);
|
||||
return $this->inventories[$id] ??= new SlotChangeActionBuilder($inventory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,6 +27,7 @@ use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\SlotValidatedInventory;
|
||||
use pocketmine\inventory\transaction\TransactionValidationException;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
|
||||
/**
|
||||
@ -34,7 +35,7 @@ use pocketmine\player\Player;
|
||||
*/
|
||||
class SlotChangeAction extends InventoryAction{
|
||||
public function __construct(
|
||||
protected Inventory $inventory,
|
||||
protected InventoryWindow $inventoryWindow,
|
||||
private int $inventorySlot,
|
||||
Item $sourceItem,
|
||||
Item $targetItem
|
||||
@ -43,10 +44,10 @@ class SlotChangeAction extends InventoryAction{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inventory involved in this action.
|
||||
* Returns the inventory window involved in this action.
|
||||
*/
|
||||
public function getInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
public function getInventoryWindow() : InventoryWindow{
|
||||
return $this->inventoryWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,21 +63,22 @@ class SlotChangeAction extends InventoryAction{
|
||||
* @throws TransactionValidationException
|
||||
*/
|
||||
public function validate(Player $source) : void{
|
||||
if(!$this->inventory->slotExists($this->inventorySlot)){
|
||||
$inventory = $this->inventoryWindow->getInventory();
|
||||
if(!$inventory->slotExists($this->inventorySlot)){
|
||||
throw new TransactionValidationException("Slot does not exist");
|
||||
}
|
||||
if(!$this->inventory->getItem($this->inventorySlot)->equalsExact($this->sourceItem)){
|
||||
if(!$inventory->getItem($this->inventorySlot)->equalsExact($this->sourceItem)){
|
||||
throw new TransactionValidationException("Slot does not contain expected original item");
|
||||
}
|
||||
if($this->targetItem->getCount() > $this->targetItem->getMaxStackSize()){
|
||||
throw new TransactionValidationException("Target item exceeds item type max stack size");
|
||||
}
|
||||
if($this->targetItem->getCount() > $this->inventory->getMaxStackSize()){
|
||||
if($this->targetItem->getCount() > $inventory->getMaxStackSize()){
|
||||
throw new TransactionValidationException("Target item exceeds inventory max stack size");
|
||||
}
|
||||
if($this->inventory instanceof SlotValidatedInventory && !$this->targetItem->isNull()){
|
||||
foreach($this->inventory->getSlotValidators() as $validator){
|
||||
$ret = $validator->validate($this->inventory, $this->targetItem, $this->inventorySlot);
|
||||
if($inventory instanceof SlotValidatedInventory && !$this->targetItem->isNull()){
|
||||
foreach($inventory->getSlotValidators() as $validator){
|
||||
$ret = $validator->validate($inventory, $this->targetItem, $this->inventorySlot);
|
||||
if($ret !== null){
|
||||
throw new TransactionValidationException("Target item is not accepted by the inventory at slot #" . $this->inventorySlot . ": " . $ret->getMessage(), 0, $ret);
|
||||
}
|
||||
@ -88,6 +90,6 @@ class SlotChangeAction extends InventoryAction{
|
||||
* Sets the item into the target inventory.
|
||||
*/
|
||||
public function execute(Player $source) : void{
|
||||
$this->inventory->setItem($this->inventorySlot, $this->targetItem);
|
||||
$this->inventoryWindow->getInventory()->setItem($this->inventorySlot, $this->targetItem);
|
||||
}
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ class Armor extends Durable{
|
||||
$thisCopy = clone $this;
|
||||
$new = $thisCopy->pop();
|
||||
$player->getArmorInventory()->setItem($this->getArmorSlot(), $new);
|
||||
$player->getInventory()->setItemInHand($existing);
|
||||
$player->setMainHandItem($existing);
|
||||
$sound = $new->getMaterial()->getEquipSound();
|
||||
if($sound !== null){
|
||||
$player->broadcastSound($sound);
|
||||
|
@ -23,9 +23,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
|
||||
final class ComplexInventoryMapEntry{
|
||||
final class ComplexWindowMapEntry{
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
@ -38,7 +38,7 @@ final class ComplexInventoryMapEntry{
|
||||
* @phpstan-param array<int, int> $slotMap
|
||||
*/
|
||||
public function __construct(
|
||||
private Inventory $inventory,
|
||||
private InventoryWindow $inventory,
|
||||
private array $slotMap
|
||||
){
|
||||
foreach($slotMap as $slot => $index){
|
||||
@ -46,7 +46,7 @@ final class ComplexInventoryMapEntry{
|
||||
}
|
||||
}
|
||||
|
||||
public function getInventory() : Inventory{ return $this->inventory; }
|
||||
public function getWindow() : InventoryWindow{ return $this->inventory; }
|
||||
|
||||
/**
|
||||
* @return int[]
|
@ -23,20 +23,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\block\inventory\AnvilInventory;
|
||||
use pocketmine\block\inventory\BlockInventory;
|
||||
use pocketmine\block\inventory\BrewingStandInventory;
|
||||
use pocketmine\block\inventory\CartographyTableInventory;
|
||||
use pocketmine\block\inventory\CraftingTableInventory;
|
||||
use pocketmine\block\inventory\EnchantInventory;
|
||||
use pocketmine\block\inventory\FurnaceInventory;
|
||||
use pocketmine\block\inventory\HopperInventory;
|
||||
use pocketmine\block\inventory\LoomInventory;
|
||||
use pocketmine\block\inventory\SmithingTableInventory;
|
||||
use pocketmine\block\inventory\StonecutterInventory;
|
||||
use pocketmine\block\inventory\window\AnvilInventoryWindow;
|
||||
use pocketmine\block\inventory\window\BlockInventoryWindow;
|
||||
use pocketmine\block\inventory\window\BrewingStandInventoryWindow;
|
||||
use pocketmine\block\inventory\window\CartographyTableInventoryWindow;
|
||||
use pocketmine\block\inventory\window\CraftingTableInventoryWindow;
|
||||
use pocketmine\block\inventory\window\EnchantingTableInventoryWindow;
|
||||
use pocketmine\block\inventory\window\FurnaceInventoryWindow;
|
||||
use pocketmine\block\inventory\window\HopperInventoryWindow;
|
||||
use pocketmine\block\inventory\window\LoomInventoryWindow;
|
||||
use pocketmine\block\inventory\window\SmithingTableInventoryWindow;
|
||||
use pocketmine\block\inventory\window\StonecutterInventoryWindow;
|
||||
use pocketmine\crafting\FurnaceType;
|
||||
use pocketmine\data\bedrock\EnchantmentIdMap;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\InventoryListener;
|
||||
use pocketmine\inventory\transaction\action\SlotChangeAction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\item\enchantment\EnchantingOption;
|
||||
@ -62,7 +63,9 @@ use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;
|
||||
use pocketmine\network\PacketHandlingException;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\player\PlayerInventoryWindow;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\ObjectSet;
|
||||
use function array_fill_keys;
|
||||
@ -77,27 +80,27 @@ use function max;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* @phpstan-type ContainerOpenClosure \Closure(int $id, Inventory $inventory) : (list<ClientboundPacket>|null)
|
||||
* @phpstan-type ContainerOpenClosure \Closure(int $id, InventoryWindow $window) : (list<ClientboundPacket>|null)
|
||||
*/
|
||||
class InventoryManager{
|
||||
class InventoryManager implements InventoryListener{
|
||||
/**
|
||||
* @var InventoryManagerEntry[] spl_object_id(Inventory) => InventoryManagerEntry
|
||||
* @phpstan-var array<int, InventoryManagerEntry>
|
||||
*/
|
||||
private array $inventories = [];
|
||||
private array $entries = [];
|
||||
|
||||
/**
|
||||
* @var Inventory[] network window ID => Inventory
|
||||
* @phpstan-var array<int, Inventory>
|
||||
* @var InventoryWindow[] network window ID => InventoryWindow
|
||||
* @phpstan-var array<int, InventoryWindow>
|
||||
*/
|
||||
private array $networkIdToInventoryMap = [];
|
||||
private array $networkIdToWindowMap = [];
|
||||
/**
|
||||
* @var ComplexInventoryMapEntry[] net slot ID => ComplexWindowMapEntry
|
||||
* @phpstan-var array<int, ComplexInventoryMapEntry>
|
||||
* @var ComplexWindowMapEntry[] net slot ID => ComplexWindowMapEntry
|
||||
* @phpstan-var array<int, ComplexWindowMapEntry>
|
||||
*/
|
||||
private array $complexSlotToInventoryMap = [];
|
||||
private array $complexSlotToWindowMap = [];
|
||||
|
||||
private int $lastInventoryNetworkId = ContainerIds::FIRST;
|
||||
private int $lastWindowNetworkId = ContainerIds::FIRST;
|
||||
private int $currentWindowType = WindowTypes::CONTAINER;
|
||||
|
||||
private int $clientSelectedHotbarSlot = -1;
|
||||
@ -127,33 +130,52 @@ class InventoryManager{
|
||||
$this->containerOpenCallbacks = new ObjectSet();
|
||||
$this->containerOpenCallbacks->add(self::createContainerOpen(...));
|
||||
|
||||
$this->add(ContainerIds::INVENTORY, $this->player->getInventory());
|
||||
$this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory());
|
||||
$this->add(ContainerIds::ARMOR, $this->player->getArmorInventory());
|
||||
$this->addComplex(UIInventorySlotOffset::CURSOR, $this->player->getCursorInventory());
|
||||
$this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $this->player->getCraftingGrid());
|
||||
foreach($this->player->getPermanentWindows() as $window){
|
||||
match($window->getType()){
|
||||
PlayerInventoryWindow::TYPE_INVENTORY => $this->add(ContainerIds::INVENTORY, $window),
|
||||
PlayerInventoryWindow::TYPE_OFFHAND => $this->add(ContainerIds::OFFHAND, $window),
|
||||
PlayerInventoryWindow::TYPE_ARMOR => $this->add(ContainerIds::ARMOR, $window),
|
||||
PlayerInventoryWindow::TYPE_CURSOR => $this->addComplex(UIInventorySlotOffset::CURSOR, $window),
|
||||
PlayerInventoryWindow::TYPE_CRAFTING => $this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $window),
|
||||
default => throw new AssumptionFailedError("Unknown permanent window type " . $window->getType())
|
||||
};
|
||||
}
|
||||
|
||||
$this->player->getInventory()->getHeldItemIndexChangeListeners()->add($this->syncSelectedHotbarSlot(...));
|
||||
$this->player->getHotbar()->getSelectedIndexChangeListeners()->add($this->syncSelectedHotbarSlot(...));
|
||||
}
|
||||
|
||||
private function associateIdWithInventory(int $id, Inventory $inventory) : void{
|
||||
$this->networkIdToInventoryMap[$id] = $inventory;
|
||||
private function associateIdWithInventory(int $id, InventoryWindow $window) : void{
|
||||
$this->networkIdToWindowMap[$id] = $window;
|
||||
}
|
||||
|
||||
private function getNewWindowId() : int{
|
||||
$this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % ContainerIds::LAST);
|
||||
return $this->lastInventoryNetworkId;
|
||||
$this->lastWindowNetworkId = max(ContainerIds::FIRST, ($this->lastWindowNetworkId + 1) % ContainerIds::LAST);
|
||||
return $this->lastWindowNetworkId;
|
||||
}
|
||||
|
||||
private function add(int $id, Inventory $inventory) : void{
|
||||
if(isset($this->inventories[spl_object_id($inventory)])){
|
||||
throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
|
||||
private function getEntry(Inventory $inventory) : ?InventoryManagerEntry{
|
||||
return $this->entries[spl_object_id($inventory)] ?? null;
|
||||
}
|
||||
|
||||
private function getEntryByWindow(InventoryWindow $window) : ?InventoryManagerEntry{
|
||||
return $this->getEntry($window->getInventory());
|
||||
}
|
||||
|
||||
public function getInventoryWindow(Inventory $inventory) : ?InventoryWindow{
|
||||
return $this->getEntry($inventory)?->window;
|
||||
}
|
||||
|
||||
private function add(int $id, InventoryWindow $window) : void{
|
||||
$k = spl_object_id($window->getInventory());
|
||||
if(isset($this->entries[$k])){
|
||||
throw new \InvalidArgumentException("Inventory " . get_class($window->getInventory()) . " is already tracked (open in two different windows?)");
|
||||
}
|
||||
$this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry($inventory);
|
||||
$this->associateIdWithInventory($id, $inventory);
|
||||
$this->entries[$k] = new InventoryManagerEntry($window);
|
||||
$window->getInventory()->getListeners()->add($this);
|
||||
$this->associateIdWithInventory($id, $window);
|
||||
}
|
||||
|
||||
private function addDynamic(Inventory $inventory) : int{
|
||||
private function addDynamic(InventoryWindow $inventory) : int{
|
||||
$id = $this->getNewWindowId();
|
||||
$this->add($id, $inventory);
|
||||
return $id;
|
||||
@ -163,17 +185,19 @@ class InventoryManager{
|
||||
* @param int[]|int $slotMap
|
||||
* @phpstan-param array<int, int>|int $slotMap
|
||||
*/
|
||||
private function addComplex(array|int $slotMap, Inventory $inventory) : void{
|
||||
if(isset($this->inventories[spl_object_id($inventory)])){
|
||||
throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
|
||||
private function addComplex(array|int $slotMap, InventoryWindow $window) : void{
|
||||
$k = spl_object_id($window->getInventory());
|
||||
if(isset($this->entries[$k])){
|
||||
throw new \InvalidArgumentException("Inventory " . get_class($window) . " is already tracked");
|
||||
}
|
||||
$complexSlotMap = new ComplexInventoryMapEntry($inventory, is_int($slotMap) ? [$slotMap => 0] : $slotMap);
|
||||
$this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry(
|
||||
$inventory,
|
||||
$complexSlotMap = new ComplexWindowMapEntry($window, is_int($slotMap) ? [$slotMap => 0] : $slotMap);
|
||||
$this->entries[$k] = new InventoryManagerEntry(
|
||||
$window,
|
||||
$complexSlotMap
|
||||
);
|
||||
$window->getInventory()->getListeners()->add($this);
|
||||
foreach($complexSlotMap->getSlotMap() as $netSlot => $coreSlot){
|
||||
$this->complexSlotToInventoryMap[$netSlot] = $complexSlotMap;
|
||||
$this->complexSlotToWindowMap[$netSlot] = $complexSlotMap;
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,7 +205,7 @@ class InventoryManager{
|
||||
* @param int[]|int $slotMap
|
||||
* @phpstan-param array<int, int>|int $slotMap
|
||||
*/
|
||||
private function addComplexDynamic(array|int $slotMap, Inventory $inventory) : int{
|
||||
private function addComplexDynamic(array|int $slotMap, InventoryWindow $inventory) : int{
|
||||
$this->addComplex($slotMap, $inventory);
|
||||
$id = $this->getNewWindowId();
|
||||
$this->associateIdWithInventory($id, $inventory);
|
||||
@ -189,54 +213,58 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
private function remove(int $id) : void{
|
||||
$inventory = $this->networkIdToInventoryMap[$id];
|
||||
unset($this->networkIdToInventoryMap[$id]);
|
||||
if($this->getWindowId($inventory) === null){
|
||||
unset($this->inventories[spl_object_id($inventory)]);
|
||||
foreach($this->complexSlotToInventoryMap as $netSlot => $entry){
|
||||
if($entry->getInventory() === $inventory){
|
||||
unset($this->complexSlotToInventoryMap[$netSlot]);
|
||||
$window = $this->networkIdToWindowMap[$id];
|
||||
$inventory = $window->getInventory();
|
||||
unset($this->networkIdToWindowMap[$id]);
|
||||
if($this->getWindowId($window) === null){
|
||||
$inventory->getListeners()->remove($this);
|
||||
unset($this->entries[spl_object_id($inventory)]);
|
||||
foreach($this->complexSlotToWindowMap as $netSlot => $entry){
|
||||
if($entry->getWindow() === $window){
|
||||
unset($this->complexSlotToWindowMap[$netSlot]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getWindowId(Inventory $inventory) : ?int{
|
||||
return ($id = array_search($inventory, $this->networkIdToInventoryMap, true)) !== false ? $id : null;
|
||||
public function getWindowId(InventoryWindow $window) : ?int{
|
||||
return ($id = array_search($window, $this->networkIdToWindowMap, true)) !== false ? $id : null;
|
||||
}
|
||||
|
||||
public function getCurrentWindowId() : int{
|
||||
return $this->lastInventoryNetworkId;
|
||||
return $this->lastWindowNetworkId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{Inventory, int}|null
|
||||
* @phpstan-return array{InventoryWindow, int}|null
|
||||
*/
|
||||
public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{
|
||||
if($windowId === ContainerIds::UI){
|
||||
$entry = $this->complexSlotToInventoryMap[$netSlotId] ?? null;
|
||||
$entry = $this->complexSlotToWindowMap[$netSlotId] ?? null;
|
||||
if($entry === null){
|
||||
return null;
|
||||
}
|
||||
$inventory = $entry->getInventory();
|
||||
$window = $entry->getWindow();
|
||||
$coreSlotId = $entry->mapNetToCore($netSlotId);
|
||||
return $coreSlotId !== null && $inventory->slotExists($coreSlotId) ? [$inventory, $coreSlotId] : null;
|
||||
return $coreSlotId !== null && $window->getInventory()->slotExists($coreSlotId) ? [$window, $coreSlotId] : null;
|
||||
}
|
||||
$inventory = $this->networkIdToInventoryMap[$windowId] ?? null;
|
||||
if($inventory !== null && $inventory->slotExists($netSlotId)){
|
||||
return [$inventory, $netSlotId];
|
||||
$window = $this->networkIdToWindowMap[$windowId] ?? null;
|
||||
if($window !== null && $window->getInventory()->slotExists($netSlotId)){
|
||||
return [$window, $netSlotId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function addPredictedSlotChangeInternal(Inventory $inventory, int $slot, ItemStack $item) : void{
|
||||
$this->inventories[spl_object_id($inventory)]->predictions[$slot] = $item;
|
||||
private function addPredictedSlotChangeInternal(InventoryWindow $window, int $slot, ItemStack $item) : void{
|
||||
//TODO: does this need a null check?
|
||||
$entry = $this->getEntryByWindow($window) ?? throw new AssumptionFailedError("Assume this should never be null");
|
||||
$entry->predictions[$slot] = $item;
|
||||
}
|
||||
|
||||
public function addPredictedSlotChange(Inventory $inventory, int $slot, Item $item) : void{
|
||||
public function addPredictedSlotChange(InventoryWindow $window, int $slot, Item $item) : void{
|
||||
$typeConverter = $this->session->getTypeConverter();
|
||||
$itemStack = $typeConverter->coreItemStackToNet($item);
|
||||
$this->addPredictedSlotChangeInternal($inventory, $slot, $itemStack);
|
||||
$this->addPredictedSlotChangeInternal($window, $slot, $itemStack);
|
||||
}
|
||||
|
||||
public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
|
||||
@ -244,7 +272,7 @@ class InventoryManager{
|
||||
if($action instanceof SlotChangeAction){
|
||||
//TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
|
||||
$this->addPredictedSlotChange(
|
||||
$action->getInventory(),
|
||||
$action->getInventoryWindow(),
|
||||
$action->getSlot(),
|
||||
$action->getTargetItem()
|
||||
);
|
||||
@ -275,8 +303,8 @@ class InventoryManager{
|
||||
continue;
|
||||
}
|
||||
|
||||
[$inventory, $slot] = $info;
|
||||
$this->addPredictedSlotChangeInternal($inventory, $slot, $action->newItem->getItemStack());
|
||||
[$window, $slot] = $info;
|
||||
$this->addPredictedSlotChangeInternal($window, $slot, $action->newItem->getItemStack());
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,32 +339,32 @@ class InventoryManager{
|
||||
* @return int[]|null
|
||||
* @phpstan-return array<int, int>|null
|
||||
*/
|
||||
private function createComplexSlotMapping(Inventory $inventory) : ?array{
|
||||
private function createComplexSlotMapping(InventoryWindow $inventory) : ?array{
|
||||
//TODO: make this dynamic so plugins can add mappings for stuff not implemented by PM
|
||||
return match(true){
|
||||
$inventory instanceof AnvilInventory => UIInventorySlotOffset::ANVIL,
|
||||
$inventory instanceof EnchantInventory => UIInventorySlotOffset::ENCHANTING_TABLE,
|
||||
$inventory instanceof LoomInventory => UIInventorySlotOffset::LOOM,
|
||||
$inventory instanceof StonecutterInventory => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventory::SLOT_INPUT],
|
||||
$inventory instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT,
|
||||
$inventory instanceof CartographyTableInventory => UIInventorySlotOffset::CARTOGRAPHY_TABLE,
|
||||
$inventory instanceof SmithingTableInventory => UIInventorySlotOffset::SMITHING_TABLE,
|
||||
$inventory instanceof AnvilInventoryWindow => UIInventorySlotOffset::ANVIL,
|
||||
$inventory instanceof EnchantingTableInventoryWindow => UIInventorySlotOffset::ENCHANTING_TABLE,
|
||||
$inventory instanceof LoomInventoryWindow => UIInventorySlotOffset::LOOM,
|
||||
$inventory instanceof StonecutterInventoryWindow => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventoryWindow::SLOT_INPUT],
|
||||
$inventory instanceof CraftingTableInventoryWindow => UIInventorySlotOffset::CRAFTING3X3_INPUT,
|
||||
$inventory instanceof CartographyTableInventoryWindow => UIInventorySlotOffset::CARTOGRAPHY_TABLE,
|
||||
$inventory instanceof SmithingTableInventoryWindow => UIInventorySlotOffset::SMITHING_TABLE,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function onCurrentWindowChange(Inventory $inventory) : void{
|
||||
public function onCurrentWindowChange(InventoryWindow $window) : void{
|
||||
$this->onCurrentWindowRemove();
|
||||
|
||||
$this->openWindowDeferred(function() use ($inventory) : void{
|
||||
if(($slotMap = $this->createComplexSlotMapping($inventory)) !== null){
|
||||
$windowId = $this->addComplexDynamic($slotMap, $inventory);
|
||||
$this->openWindowDeferred(function() use ($window) : void{
|
||||
if(($slotMap = $this->createComplexSlotMapping($window)) !== null){
|
||||
$windowId = $this->addComplexDynamic($slotMap, $window);
|
||||
}else{
|
||||
$windowId = $this->addDynamic($inventory);
|
||||
$windowId = $this->addDynamic($window);
|
||||
}
|
||||
|
||||
foreach($this->containerOpenCallbacks as $callback){
|
||||
$pks = $callback($windowId, $inventory);
|
||||
$pks = $callback($windowId, $window);
|
||||
if($pks !== null){
|
||||
$windowType = null;
|
||||
foreach($pks as $pk){
|
||||
@ -347,7 +375,7 @@ class InventoryManager{
|
||||
$this->session->sendDataPacket($pk);
|
||||
}
|
||||
$this->currentWindowType = $windowType ?? WindowTypes::CONTAINER;
|
||||
$this->syncContents($inventory);
|
||||
$this->syncContents($window);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -362,27 +390,27 @@ class InventoryManager{
|
||||
* @return ClientboundPacket[]|null
|
||||
* @phpstan-return list<ClientboundPacket>|null
|
||||
*/
|
||||
protected static function createContainerOpen(int $id, Inventory $inv) : ?array{
|
||||
protected static function createContainerOpen(int $id, InventoryWindow $window) : ?array{
|
||||
//TODO: we should be using some kind of tagging system to identify the types. Instanceof is flaky especially
|
||||
//if the class isn't final, not to mention being inflexible.
|
||||
if($inv instanceof BlockInventory){
|
||||
$blockPosition = BlockPosition::fromVector3($inv->getHolder());
|
||||
if($window instanceof BlockInventoryWindow){
|
||||
$blockPosition = BlockPosition::fromVector3($window->getHolder());
|
||||
$windowType = match(true){
|
||||
$inv instanceof LoomInventory => WindowTypes::LOOM,
|
||||
$inv instanceof FurnaceInventory => match($inv->getFurnaceType()){
|
||||
$window instanceof LoomInventoryWindow => WindowTypes::LOOM,
|
||||
$window instanceof FurnaceInventoryWindow => match($window->getFurnaceType()){
|
||||
FurnaceType::FURNACE => WindowTypes::FURNACE,
|
||||
FurnaceType::BLAST_FURNACE => WindowTypes::BLAST_FURNACE,
|
||||
FurnaceType::SMOKER => WindowTypes::SMOKER,
|
||||
FurnaceType::CAMPFIRE, FurnaceType::SOUL_CAMPFIRE => throw new \LogicException("Campfire inventory cannot be displayed to a player")
|
||||
},
|
||||
$inv instanceof EnchantInventory => WindowTypes::ENCHANTMENT,
|
||||
$inv instanceof BrewingStandInventory => WindowTypes::BREWING_STAND,
|
||||
$inv instanceof AnvilInventory => WindowTypes::ANVIL,
|
||||
$inv instanceof HopperInventory => WindowTypes::HOPPER,
|
||||
$inv instanceof CraftingTableInventory => WindowTypes::WORKBENCH,
|
||||
$inv instanceof StonecutterInventory => WindowTypes::STONECUTTER,
|
||||
$inv instanceof CartographyTableInventory => WindowTypes::CARTOGRAPHY,
|
||||
$inv instanceof SmithingTableInventory => WindowTypes::SMITHING_TABLE,
|
||||
$window instanceof EnchantingTableInventoryWindow => WindowTypes::ENCHANTMENT,
|
||||
$window instanceof BrewingStandInventoryWindow => WindowTypes::BREWING_STAND,
|
||||
$window instanceof AnvilInventoryWindow => WindowTypes::ANVIL,
|
||||
$window instanceof HopperInventoryWindow => WindowTypes::HOPPER,
|
||||
$window instanceof CraftingTableInventoryWindow => WindowTypes::WORKBENCH,
|
||||
$window instanceof StonecutterInventoryWindow => WindowTypes::STONECUTTER,
|
||||
$window instanceof CartographyTableInventoryWindow => WindowTypes::CARTOGRAPHY,
|
||||
$window instanceof SmithingTableInventoryWindow => WindowTypes::SMITHING_TABLE,
|
||||
default => WindowTypes::CONTAINER
|
||||
};
|
||||
return [ContainerOpenPacket::blockInv($id, $windowType, $blockPosition)];
|
||||
@ -395,7 +423,8 @@ class InventoryManager{
|
||||
|
||||
$this->openWindowDeferred(function() : void{
|
||||
$windowId = $this->getNewWindowId();
|
||||
$this->associateIdWithInventory($windowId, $this->player->getInventory());
|
||||
$window = $this->getInventoryWindow($this->player->getInventory()) ?? throw new AssumptionFailedError("This should never be null");
|
||||
$this->associateIdWithInventory($windowId, $window);
|
||||
$this->currentWindowType = WindowTypes::INVENTORY;
|
||||
|
||||
$this->session->sendDataPacket(ContainerOpenPacket::entityInv(
|
||||
@ -407,25 +436,25 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
public function onCurrentWindowRemove() : void{
|
||||
if(isset($this->networkIdToInventoryMap[$this->lastInventoryNetworkId])){
|
||||
$this->remove($this->lastInventoryNetworkId);
|
||||
$this->session->sendDataPacket(ContainerClosePacket::create($this->lastInventoryNetworkId, $this->currentWindowType, true));
|
||||
if(isset($this->networkIdToWindowMap[$this->lastWindowNetworkId])){
|
||||
$this->remove($this->lastWindowNetworkId);
|
||||
$this->session->sendDataPacket(ContainerClosePacket::create($this->lastWindowNetworkId, $this->currentWindowType, true));
|
||||
if($this->pendingCloseWindowId !== null){
|
||||
throw new AssumptionFailedError("We should not have opened a new window while a window was waiting to be closed");
|
||||
}
|
||||
$this->pendingCloseWindowId = $this->lastInventoryNetworkId;
|
||||
$this->pendingCloseWindowId = $this->lastWindowNetworkId;
|
||||
$this->enchantingTableOptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
public function onClientRemoveWindow(int $id) : void{
|
||||
if($id === $this->lastInventoryNetworkId){
|
||||
if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){
|
||||
if($id === $this->lastWindowNetworkId){
|
||||
if(isset($this->networkIdToWindowMap[$id]) && $id !== $this->pendingCloseWindowId){
|
||||
$this->remove($id);
|
||||
$this->player->removeCurrentWindow();
|
||||
}
|
||||
}else{
|
||||
$this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastInventoryNetworkId");
|
||||
$this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastWindowNetworkId");
|
||||
}
|
||||
|
||||
//Always send this, even if no window matches. If we told the client to close a window, it will behave as if it
|
||||
@ -477,14 +506,25 @@ class InventoryManager{
|
||||
$this->itemStackExtraDataEqual($left, $right);
|
||||
}
|
||||
|
||||
public function onSlotChange(Inventory $inventory, int $slot) : void{
|
||||
$inventoryEntry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem) : void{
|
||||
$window = $this->getInventoryWindow($inventory);
|
||||
if($window === null){
|
||||
//this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
|
||||
//is cleared before removal.
|
||||
return;
|
||||
}
|
||||
$this->requestSyncSlot($window, $slot);
|
||||
}
|
||||
|
||||
public function requestSyncSlot(InventoryWindow $window, int $slot) : void{
|
||||
$inventoryEntry = $this->getEntryByWindow($window);
|
||||
if($inventoryEntry === null){
|
||||
//this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
|
||||
//is cleared before removal.
|
||||
return;
|
||||
}
|
||||
$currentItem = $this->session->getTypeConverter()->coreItemStackToNet($inventory->getItem($slot));
|
||||
|
||||
$currentItem = $this->session->getTypeConverter()->coreItemStackToNet($window->getInventory()->getItem($slot));
|
||||
$clientSideItem = $inventoryEntry->predictions[$slot] ?? null;
|
||||
if($clientSideItem === null || !$this->itemStacksEqual($currentItem, $clientSideItem)){
|
||||
//no prediction or incorrect - do not associate this with the currently active itemstack request
|
||||
@ -511,7 +551,7 @@ class InventoryManager{
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create(
|
||||
$windowId,
|
||||
$netSlot,
|
||||
new FullContainerName($this->lastInventoryNetworkId),
|
||||
new FullContainerName($this->lastWindowNetworkId),
|
||||
new ItemStackWrapper(0, ItemStack::null()),
|
||||
new ItemStackWrapper(0, ItemStack::null())
|
||||
));
|
||||
@ -520,7 +560,7 @@ class InventoryManager{
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create(
|
||||
$windowId,
|
||||
$netSlot,
|
||||
new FullContainerName($this->lastInventoryNetworkId),
|
||||
new FullContainerName($this->lastWindowNetworkId),
|
||||
new ItemStackWrapper(0, ItemStack::null()),
|
||||
$itemStackWrapper
|
||||
));
|
||||
@ -541,18 +581,15 @@ class InventoryManager{
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create(
|
||||
$windowId,
|
||||
array_fill_keys(array_keys($itemStackWrappers), new ItemStackWrapper(0, ItemStack::null())),
|
||||
new FullContainerName($this->lastInventoryNetworkId),
|
||||
new FullContainerName($this->lastWindowNetworkId),
|
||||
new ItemStackWrapper(0, ItemStack::null())
|
||||
));
|
||||
//now send the real contents
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, $itemStackWrappers, new FullContainerName($this->lastInventoryNetworkId), new ItemStackWrapper(0, ItemStack::null())));
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, $itemStackWrappers, new FullContainerName($this->lastWindowNetworkId), new ItemStackWrapper(0, ItemStack::null())));
|
||||
}
|
||||
|
||||
public function syncSlot(Inventory $inventory, int $slot, ItemStack $itemStack) : void{
|
||||
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
if($entry === null){
|
||||
throw new \LogicException("Cannot sync an untracked inventory");
|
||||
}
|
||||
private function syncSlot(InventoryWindow $window, int $slot, ItemStack $itemStack) : void{
|
||||
$entry = $this->getEntryByWindow($window) ?? throw new \LogicException("Cannot sync an untracked inventory");
|
||||
$itemStackInfo = $entry->itemStackInfos[$slot];
|
||||
if($itemStackInfo === null){
|
||||
throw new \LogicException("Cannot sync an untracked inventory slot");
|
||||
@ -561,7 +598,7 @@ class InventoryManager{
|
||||
$windowId = ContainerIds::UI;
|
||||
$netSlot = $entry->complexSlotMap->mapCoreToNet($slot) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
|
||||
}else{
|
||||
$windowId = $this->getWindowId($inventory) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
|
||||
$windowId = $this->getWindowId($window) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
|
||||
$netSlot = $slot;
|
||||
}
|
||||
|
||||
@ -579,8 +616,17 @@ class InventoryManager{
|
||||
unset($entry->predictions[$slot], $entry->pendingSyncs[$slot]);
|
||||
}
|
||||
|
||||
public function syncContents(Inventory $inventory) : void{
|
||||
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
public function onContentChange(Inventory $inventory, array $oldContents) : void{
|
||||
//this can be null when an inventory changed during InventoryCloseEvent, or when a temporary inventory
|
||||
//is cleared before removal.
|
||||
$window = $this->getInventoryWindow($inventory);
|
||||
if($window !== null){
|
||||
$this->syncContents($window);
|
||||
}
|
||||
}
|
||||
|
||||
private function syncContents(InventoryWindow $window) : void{
|
||||
$entry = $this->getEntryByWindow($window);
|
||||
if($entry === null){
|
||||
//this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
|
||||
//is cleared before removal.
|
||||
@ -589,14 +635,14 @@ class InventoryManager{
|
||||
if($entry->complexSlotMap !== null){
|
||||
$windowId = ContainerIds::UI;
|
||||
}else{
|
||||
$windowId = $this->getWindowId($inventory);
|
||||
$windowId = $this->getWindowId($window);
|
||||
}
|
||||
if($windowId !== null){
|
||||
$entry->predictions = [];
|
||||
$entry->pendingSyncs = [];
|
||||
$contents = [];
|
||||
$typeConverter = $this->session->getTypeConverter();
|
||||
foreach($inventory->getContents(true) as $slot => $item){
|
||||
foreach($window->getInventory()->getContents(true) as $slot => $item){
|
||||
$itemStack = $typeConverter->coreItemStackToNet($item);
|
||||
$info = $this->trackItemStack($entry, $slot, $itemStack, null);
|
||||
$contents[] = new ItemStackWrapper($info->getStackId(), $itemStack);
|
||||
@ -616,8 +662,8 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
public function syncAll() : void{
|
||||
foreach($this->inventories as $entry){
|
||||
$this->syncContents($entry->inventory);
|
||||
foreach($this->entries as $entry){
|
||||
$this->syncContents($entry->window);
|
||||
}
|
||||
}
|
||||
|
||||
@ -627,8 +673,8 @@ class InventoryManager{
|
||||
|
||||
public function syncMismatchedPredictedSlotChanges() : void{
|
||||
$typeConverter = $this->session->getTypeConverter();
|
||||
foreach($this->inventories as $entry){
|
||||
$inventory = $entry->inventory;
|
||||
foreach($this->entries as $entry){
|
||||
$inventory = $entry->window->getInventory();
|
||||
foreach($entry->predictions as $slot => $expectedItem){
|
||||
if(!$inventory->slotExists($slot) || $entry->itemStackInfos[$slot] === null){
|
||||
continue; //TODO: size desync ???
|
||||
@ -646,14 +692,14 @@ class InventoryManager{
|
||||
public function flushPendingUpdates() : void{
|
||||
if($this->fullSyncRequested){
|
||||
$this->fullSyncRequested = false;
|
||||
$this->session->getLogger()->debug("Full inventory sync requested, sending contents of " . count($this->inventories) . " inventories");
|
||||
$this->session->getLogger()->debug("Full inventory sync requested, sending contents of " . count($this->entries) . " inventories");
|
||||
$this->syncAll();
|
||||
}else{
|
||||
foreach($this->inventories as $entry){
|
||||
foreach($this->entries as $entry){
|
||||
if(count($entry->pendingSyncs) === 0){
|
||||
continue;
|
||||
}
|
||||
$inventory = $entry->inventory;
|
||||
$inventory = $entry->window;
|
||||
$this->session->getLogger()->debug("Syncing slots " . implode(", ", array_keys($entry->pendingSyncs)) . " in inventory " . get_class($inventory) . "#" . spl_object_id($inventory));
|
||||
foreach($entry->pendingSyncs as $slot => $itemStack){
|
||||
$this->syncSlot($inventory, $slot, $itemStack);
|
||||
@ -664,7 +710,13 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
public function syncData(Inventory $inventory, int $propertyId, int $value) : void{
|
||||
$windowId = $this->getWindowId($inventory);
|
||||
//TODO: the handling of this data has always kinda sucked. Probably ought to route it through InventoryWindow
|
||||
//somehow, but I'm not sure exactly how that should look.
|
||||
$window = $this->getInventoryWindow($inventory);
|
||||
if($window === null){
|
||||
return;
|
||||
}
|
||||
$windowId = $this->getWindowId($window);
|
||||
if($windowId !== null){
|
||||
$this->session->sendDataPacket(ContainerSetDataPacket::create($windowId, $propertyId, $value));
|
||||
}
|
||||
@ -676,12 +728,9 @@ class InventoryManager{
|
||||
|
||||
public function syncSelectedHotbarSlot() : void{
|
||||
$playerInventory = $this->player->getInventory();
|
||||
$selected = $playerInventory->getHeldItemIndex();
|
||||
$selected = $this->player->getHotbar()->getSelectedIndex();
|
||||
if($selected !== $this->clientSelectedHotbarSlot){
|
||||
$inventoryEntry = $this->inventories[spl_object_id($playerInventory)] ?? null;
|
||||
if($inventoryEntry === null){
|
||||
throw new AssumptionFailedError("Player inventory should always be tracked");
|
||||
}
|
||||
$inventoryEntry = $this->getEntry($playerInventory) ?? throw new AssumptionFailedError("Player inventory should always be tracked");
|
||||
$itemStackInfo = $inventoryEntry->itemStackInfos[$selected] ?? null;
|
||||
if($itemStackInfo === null){
|
||||
throw new AssumptionFailedError("Untracked player inventory slot $selected");
|
||||
@ -689,7 +738,7 @@ class InventoryManager{
|
||||
|
||||
$this->session->sendDataPacket(MobEquipmentPacket::create(
|
||||
$this->player->getId(),
|
||||
new ItemStackWrapper($itemStackInfo->getStackId(), $this->session->getTypeConverter()->coreItemStackToNet($playerInventory->getItemInHand())),
|
||||
new ItemStackWrapper($itemStackInfo->getStackId(), $this->session->getTypeConverter()->coreItemStackToNet($playerInventory->getItem($selected))),
|
||||
$selected,
|
||||
$selected,
|
||||
ContainerIds::INVENTORY
|
||||
@ -740,9 +789,8 @@ class InventoryManager{
|
||||
return $this->nextItemStackId++;
|
||||
}
|
||||
|
||||
public function getItemStackInfo(Inventory $inventory, int $slot) : ?ItemStackInfo{
|
||||
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
|
||||
return $entry?->itemStackInfos[$slot] ?? null;
|
||||
public function getItemStackInfo(InventoryWindow $window, int $slot) : ?ItemStackInfo{
|
||||
return $this->getEntryByWindow($window)?->itemStackInfos[$slot] ?? null;
|
||||
}
|
||||
|
||||
private function trackItemStack(InventoryManagerEntry $entry, int $slotId, ItemStack $itemStack, ?int $itemStackRequestId) : ItemStackInfo{
|
||||
|
@ -23,8 +23,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
|
||||
final class InventoryManagerEntry{
|
||||
/**
|
||||
@ -46,7 +46,7 @@ final class InventoryManagerEntry{
|
||||
public array $pendingSyncs = [];
|
||||
|
||||
public function __construct(
|
||||
public Inventory $inventory,
|
||||
public ?ComplexInventoryMapEntry $complexSlotMap = null
|
||||
public InventoryWindow $window,
|
||||
public ?ComplexWindowMapEntry $complexSlotMap = null
|
||||
){}
|
||||
}
|
||||
|
@ -102,22 +102,21 @@ final class StandardEntityEventBroadcaster implements EntityEventBroadcaster{
|
||||
}
|
||||
|
||||
public function onMobMainHandItemChange(array $recipients, Human $mob) : void{
|
||||
//TODO: we could send zero for slot here because remote players don't need to know which slot was selected
|
||||
$inv = $mob->getInventory();
|
||||
$item = $mob->getMainHandItem();
|
||||
$this->sendDataPacket($recipients, MobEquipmentPacket::create(
|
||||
$mob->getId(),
|
||||
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($inv->getItemInHand())),
|
||||
$inv->getHeldItemIndex(),
|
||||
$inv->getHeldItemIndex(),
|
||||
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($item)),
|
||||
0,
|
||||
0,
|
||||
ContainerIds::INVENTORY
|
||||
));
|
||||
}
|
||||
|
||||
public function onMobOffHandItemChange(array $recipients, Human $mob) : void{
|
||||
$inv = $mob->getOffHandInventory();
|
||||
$item = $mob->getOffHandItem();
|
||||
$this->sendDataPacket($recipients, MobEquipmentPacket::create(
|
||||
$mob->getId(),
|
||||
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($inv->getItem(0))),
|
||||
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($item)),
|
||||
0,
|
||||
0,
|
||||
ContainerIds::OFFHAND
|
||||
|
@ -307,11 +307,11 @@ class InGamePacketHandler extends PacketHandler{
|
||||
|
||||
switch($packet->eventId){
|
||||
case ActorEvent::EATING_ITEM: //TODO: ignore this and handle it server-side
|
||||
$item = $this->player->getInventory()->getItemInHand();
|
||||
$item = $this->player->getMainHandItem();
|
||||
if($item->isNull()){
|
||||
return false;
|
||||
}
|
||||
$this->player->broadcastAnimation(new ConsumingItemAnimation($this->player, $this->player->getInventory()->getItemInHand()));
|
||||
$this->player->broadcastAnimation(new ConsumingItemAnimation($this->player, $item));
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
@ -358,7 +358,7 @@ class InGamePacketHandler extends PacketHandler{
|
||||
[$windowId, $slot] = ItemStackContainerIdTranslator::translate($containerInfo->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $netSlot);
|
||||
$inventoryAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slot);
|
||||
if($inventoryAndSlot !== null){ //trigger the normal slot sync logic
|
||||
$this->inventoryManager->onSlotChange($inventoryAndSlot[0], $inventoryAndSlot[1]);
|
||||
$this->inventoryManager->requestSyncSlot($inventoryAndSlot[0], $inventoryAndSlot[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -464,7 +464,8 @@ class InGamePacketHandler extends PacketHandler{
|
||||
$droppedItem = $sourceSlotItem->pop($droppedCount);
|
||||
|
||||
$builder = new TransactionBuilder();
|
||||
$builder->getInventory($inventory)->setItem($sourceSlot, $sourceSlotItem);
|
||||
$window = $this->inventoryManager->getInventoryWindow($inventory) ?? throw new AssumptionFailedError("This should never happen");
|
||||
$builder->getActionBuilder($window)->setItem($sourceSlot, $sourceSlotItem);
|
||||
$builder->addAction(new DropItemAction($droppedItem));
|
||||
|
||||
$transaction = new InventoryTransaction($this->player, $builder->generateActions());
|
||||
|
@ -23,16 +23,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\handler;
|
||||
|
||||
use pocketmine\block\inventory\EnchantInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\block\inventory\window\EnchantingTableInventoryWindow;
|
||||
use pocketmine\inventory\transaction\action\CreateItemAction;
|
||||
use pocketmine\inventory\transaction\action\DestroyItemAction;
|
||||
use pocketmine\inventory\transaction\action\DropItemAction;
|
||||
use pocketmine\inventory\transaction\CraftingTransaction;
|
||||
use pocketmine\inventory\transaction\EnchantingTransaction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\inventory\transaction\SlotChangeActionBuilder;
|
||||
use pocketmine\inventory\transaction\TransactionBuilder;
|
||||
use pocketmine\inventory\transaction\TransactionBuilderInventory;
|
||||
use pocketmine\item\Durable;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\network\mcpe\cache\CraftingDataCache;
|
||||
@ -56,6 +55,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\SwapStackReque
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\TakeStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Utils;
|
||||
@ -86,25 +86,22 @@ class ItemStackRequestExecutor{
|
||||
$this->builder = new TransactionBuilder();
|
||||
}
|
||||
|
||||
protected function prettyInventoryAndSlot(Inventory $inventory, int $slot) : string{
|
||||
if($inventory instanceof TransactionBuilderInventory){
|
||||
$inventory = $inventory->getActualInventory();
|
||||
}
|
||||
protected function prettyWindowAndSlot(InventoryWindow $inventory, int $slot) : string{
|
||||
return (new \ReflectionClass($inventory))->getShortName() . "#" . spl_object_id($inventory) . ", slot: $slot";
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
private function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : void{
|
||||
$info = $this->inventoryManager->getItemStackInfo($inventory, $slotId);
|
||||
private function matchItemStack(InventoryWindow $window, int $slotId, int $clientItemStackId) : void{
|
||||
$info = $this->inventoryManager->getItemStackInfo($window, $slotId);
|
||||
if($info === null){
|
||||
throw new AssumptionFailedError("The inventory is tracked and the slot is valid, so this should not be null");
|
||||
}
|
||||
|
||||
if(!($clientItemStackId < 0 ? $info->getRequestId() === $clientItemStackId : $info->getStackId() === $clientItemStackId)){
|
||||
throw new ItemStackRequestProcessException(
|
||||
$this->prettyInventoryAndSlot($inventory, $slotId) . ": " .
|
||||
$this->prettyWindowAndSlot($window, $slotId) . ": " .
|
||||
"Mismatched expected itemstack, " .
|
||||
"client expected: $clientItemStackId, server actual: " . $info->getStackId() . ", last modified by request: " . ($info->getRequestId() ?? "none")
|
||||
);
|
||||
@ -112,7 +109,7 @@ class ItemStackRequestExecutor{
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{TransactionBuilderInventory, int}
|
||||
* @phpstan-return array{SlotChangeActionBuilder, int}
|
||||
*
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
@ -122,16 +119,17 @@ class ItemStackRequestExecutor{
|
||||
if($windowAndSlot === null){
|
||||
throw new ItemStackRequestProcessException("No open inventory matches container UI ID: " . $info->getContainerName()->getContainerId() . ", slot ID: " . $info->getSlotId());
|
||||
}
|
||||
[$inventory, $slot] = $windowAndSlot;
|
||||
[$window, $slot] = $windowAndSlot;
|
||||
$inventory = $window->getInventory();
|
||||
if(!$inventory->slotExists($slot)){
|
||||
throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyInventoryAndSlot($inventory, $slot));
|
||||
throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyWindowAndSlot($window, $slot));
|
||||
}
|
||||
|
||||
if($info->getStackId() !== $this->request->getRequestId()){ //the itemstack may have been modified by the current request
|
||||
$this->matchItemStack($inventory, $slot, $info->getStackId());
|
||||
$this->matchItemStack($window, $slot, $info->getStackId());
|
||||
}
|
||||
|
||||
return [$this->builder->getInventory($inventory), $slot];
|
||||
return [$this->builder->getActionBuilder($window), $slot];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,12 +154,12 @@ class ItemStackRequestExecutor{
|
||||
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
|
||||
if($count < 1){
|
||||
//this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
|
||||
throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Cannot take less than 1 items from a stack");
|
||||
}
|
||||
|
||||
$existingItem = $inventory->getItem($slot);
|
||||
if($existingItem->getCount() < $count){
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount());
|
||||
throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount());
|
||||
}
|
||||
|
||||
$removed = $existingItem->pop($count);
|
||||
@ -179,12 +177,12 @@ class ItemStackRequestExecutor{
|
||||
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
|
||||
if($count < 1){
|
||||
//this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
|
||||
throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Cannot take less than 1 items from a stack");
|
||||
}
|
||||
|
||||
$existingItem = $inventory->getItem($slot);
|
||||
if(!$existingItem->isNull() && !$existingItem->canStackWith($item)){
|
||||
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Can only add items to an empty slot, or a slot containing the same item");
|
||||
throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Can only add items to an empty slot, or a slot containing the same item");
|
||||
}
|
||||
|
||||
//we can't use the existing item here; it may be an empty stack
|
||||
@ -342,7 +340,7 @@ class ItemStackRequestExecutor{
|
||||
$this->setNextCreatedItem($item, true);
|
||||
}elseif($action instanceof CraftRecipeStackRequestAction){
|
||||
$window = $this->player->getCurrentWindow();
|
||||
if($window instanceof EnchantInventory){
|
||||
if($window instanceof EnchantingTableInventoryWindow){
|
||||
$optionId = $this->inventoryManager->getEnchantingTableOptionIndex($action->getRecipeId());
|
||||
if($optionId !== null && ($option = $window->getOption($optionId)) !== null){
|
||||
$this->specialTransaction = new EnchantingTransaction($this->player, $option, $optionId + 1);
|
||||
@ -375,7 +373,8 @@ class ItemStackRequestExecutor{
|
||||
$predictedDamage = $action->getPredictedDurability();
|
||||
if($usedItem instanceof Durable && $predictedDamage >= 0 && $predictedDamage <= $usedItem->getMaxDurability()){
|
||||
$usedItem->setDamage($predictedDamage);
|
||||
$this->inventoryManager->addPredictedSlotChange($inventory, $slot, $usedItem);
|
||||
$inventoryWindow = $this->inventoryManager->getInventoryWindow($inventory) ?? throw new AssumptionFailedError("The player's inventory should always have an inventory window");
|
||||
$this->inventoryManager->addPredictedSlotChange($inventoryWindow, $slot, $usedItem);
|
||||
}
|
||||
}else{
|
||||
throw new ItemStackRequestProcessException("Unhandled item stack request action");
|
||||
|
@ -31,6 +31,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\FullContainerName;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponseContainerInfo;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponseSlotInfo;
|
||||
use pocketmine\player\InventoryWindow;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
|
||||
final class ItemStackResponseBuilder{
|
||||
@ -51,20 +52,11 @@ final class ItemStackResponseBuilder{
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{Inventory, int}
|
||||
* @phpstan-return array{InventoryWindow, int}
|
||||
*/
|
||||
private function getInventoryAndSlot(int $containerInterfaceId, int $slotId) : ?array{
|
||||
private function locateWindowAndSlot(int $containerInterfaceId, int $slotId) : ?array{
|
||||
[$windowId, $slotId] = ItemStackContainerIdTranslator::translate($containerInterfaceId, $this->inventoryManager->getCurrentWindowId(), $slotId);
|
||||
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
|
||||
if($windowAndSlot === null){
|
||||
return null;
|
||||
}
|
||||
[$inventory, $slot] = $windowAndSlot;
|
||||
if(!$inventory->slotExists($slot)){
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$inventory, $slot];
|
||||
return $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
|
||||
}
|
||||
|
||||
public function build() : ItemStackResponse{
|
||||
@ -74,18 +66,18 @@ final class ItemStackResponseBuilder{
|
||||
continue;
|
||||
}
|
||||
foreach($slotIds as $slotId){
|
||||
$inventoryAndSlot = $this->getInventoryAndSlot($containerInterfaceId, $slotId);
|
||||
if($inventoryAndSlot === null){
|
||||
$windowAndSlot = $this->locateWindowAndSlot($containerInterfaceId, $slotId);
|
||||
if($windowAndSlot === null){
|
||||
//a plugin may have closed the inventory during an event, or the slot may have been invalid
|
||||
continue;
|
||||
}
|
||||
[$inventory, $slot] = $inventoryAndSlot;
|
||||
[$window, $slot] = $windowAndSlot;
|
||||
|
||||
$itemStackInfo = $this->inventoryManager->getItemStackInfo($inventory, $slot);
|
||||
$itemStackInfo = $this->inventoryManager->getItemStackInfo($window, $slot);
|
||||
if($itemStackInfo === null){
|
||||
throw new AssumptionFailedError("ItemStackInfo should never be null for an open inventory");
|
||||
}
|
||||
$item = $inventory->getItem($slot);
|
||||
$item = $window->getInventory()->getItem($slot);
|
||||
|
||||
$responseInfosByContainer[$containerInterfaceId][] = new ItemStackResponseSlotInfo(
|
||||
$slotId,
|
||||
|
@ -21,17 +21,30 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\inventory;
|
||||
namespace pocketmine\player;
|
||||
|
||||
use pocketmine\entity\Human;
|
||||
use pocketmine\inventory\Inventory;
|
||||
|
||||
abstract class InventoryWindow{
|
||||
|
||||
final class PlayerEnderInventory extends SimpleInventory{
|
||||
public function __construct(
|
||||
private Human $holder,
|
||||
int $size = 27
|
||||
){
|
||||
parent::__construct($size);
|
||||
protected Player $viewer,
|
||||
protected Inventory $inventory
|
||||
){}
|
||||
|
||||
public function getViewer() : Player{
|
||||
return $this->viewer;
|
||||
}
|
||||
|
||||
public function getHolder() : Human{ return $this->holder; }
|
||||
public function getInventory() : Inventory{
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function onOpen() : void{
|
||||
$this->inventory->onOpen($this->viewer);
|
||||
}
|
||||
|
||||
public function onClose() : void{
|
||||
$this->inventory->onClose($this->viewer);
|
||||
}
|
||||
}
|
@ -87,9 +87,7 @@ use pocketmine\form\FormValidationException;
|
||||
use pocketmine\inventory\CallbackInventoryListener;
|
||||
use pocketmine\inventory\CreativeInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\PlayerCraftingInventory;
|
||||
use pocketmine\inventory\PlayerCursorInventory;
|
||||
use pocketmine\inventory\TemporaryInventory;
|
||||
use pocketmine\inventory\SimpleInventory;
|
||||
use pocketmine\inventory\transaction\action\DropItemAction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\inventory\transaction\TransactionBuilder;
|
||||
@ -229,11 +227,11 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
protected bool $authenticated;
|
||||
protected PlayerInfo $playerInfo;
|
||||
|
||||
protected ?Inventory $currentWindow = null;
|
||||
/** @var Inventory[] */
|
||||
protected ?InventoryWindow $currentWindow = null;
|
||||
/** @var PlayerInventoryWindow[] */
|
||||
protected array $permanentWindows = [];
|
||||
protected PlayerCursorInventory $cursorInventory;
|
||||
protected PlayerCraftingInventory $craftingGrid;
|
||||
protected Inventory $cursorInventory;
|
||||
protected CraftingGrid $craftingGrid;
|
||||
protected CreativeInventory $creativeInventory;
|
||||
|
||||
protected int $messageCounter = 2;
|
||||
@ -362,7 +360,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
}
|
||||
|
||||
private function callDummyItemHeldEvent() : void{
|
||||
$slot = $this->inventory->getHeldItemIndex();
|
||||
$slot = $this->hotbar->getSelectedIndex();
|
||||
|
||||
$event = new PlayerItemHeldEvent($this, $this->inventory->getItem($slot), $slot);
|
||||
$event->call();
|
||||
@ -377,7 +375,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
|
||||
$this->inventory->getListeners()->add(new CallbackInventoryListener(
|
||||
function(Inventory $unused, int $slot) : void{
|
||||
if($slot === $this->inventory->getHeldItemIndex()){
|
||||
if($slot === $this->hotbar->getSelectedIndex()){
|
||||
$this->setUsingItem(false);
|
||||
|
||||
$this->callDummyItemHeldEvent();
|
||||
@ -1626,10 +1624,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
}
|
||||
|
||||
public function selectHotbarSlot(int $hotbarSlot) : bool{
|
||||
if(!$this->inventory->isHotbarSlot($hotbarSlot)){ //TODO: exception here?
|
||||
if(!$this->hotbar->isHotbarSlot($hotbarSlot)){ //TODO: exception here?
|
||||
return false;
|
||||
}
|
||||
if($hotbarSlot === $this->inventory->getHeldItemIndex()){
|
||||
if($hotbarSlot === $this->hotbar->getSelectedIndex()){
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1639,7 +1637,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->inventory->setHeldItemIndex($hotbarSlot);
|
||||
$this->hotbar->setSelectedIndex($hotbarSlot);
|
||||
$this->setUsingItem(false);
|
||||
|
||||
return true;
|
||||
@ -1651,7 +1649,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
private function returnItemsFromAction(Item $oldHeldItem, Item $newHeldItem, array $extraReturnedItems) : void{
|
||||
$heldItemChanged = false;
|
||||
|
||||
if(!$newHeldItem->equalsExact($oldHeldItem) && $oldHeldItem->equalsExact($this->inventory->getItemInHand())){
|
||||
if(!$newHeldItem->equalsExact($oldHeldItem) && $oldHeldItem->equalsExact($this->getMainHandItem())){
|
||||
//determine if the item was changed in some meaningful way, or just damaged/changed count
|
||||
//if it was really changed we always need to set it, whether we have finite resources or not
|
||||
$newReplica = clone $oldHeldItem;
|
||||
@ -1668,7 +1666,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
if($newHeldItem instanceof Durable && $newHeldItem->isBroken()){
|
||||
$this->broadcastSound(new ItemBreakSound());
|
||||
}
|
||||
$this->inventory->setItemInHand($newHeldItem);
|
||||
$this->setMainHandItem($newHeldItem);
|
||||
$heldItemChanged = true;
|
||||
}
|
||||
}
|
||||
@ -1678,7 +1676,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
}
|
||||
|
||||
if($heldItemChanged && count($extraReturnedItems) > 0 && $newHeldItem->isNull()){
|
||||
$this->inventory->setItemInHand(array_shift($extraReturnedItems));
|
||||
$this->setMainHandItem(array_shift($extraReturnedItems));
|
||||
}
|
||||
foreach($this->inventory->addItem(...$extraReturnedItems) as $drop){
|
||||
//TODO: we can't generate a transaction for this since the items aren't coming from an inventory :(
|
||||
@ -1700,7 +1698,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
*/
|
||||
public function useHeldItem() : bool{
|
||||
$directionVector = $this->getDirectionVector();
|
||||
$item = $this->inventory->getItemInHand();
|
||||
$item = $this->getMainHandItem();
|
||||
$oldItem = clone $item;
|
||||
|
||||
$ev = new PlayerItemUseEvent($this, $item, $directionVector);
|
||||
@ -1734,7 +1732,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
* @return bool if the consumption succeeded.
|
||||
*/
|
||||
public function consumeHeldItem() : bool{
|
||||
$slot = $this->inventory->getItemInHand();
|
||||
$slot = $this->getMainHandItem();
|
||||
if($slot instanceof ConsumableItem){
|
||||
$oldItem = clone $slot;
|
||||
|
||||
@ -1767,7 +1765,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
*/
|
||||
public function releaseHeldItem() : bool{
|
||||
try{
|
||||
$item = $this->inventory->getItemInHand();
|
||||
$item = $this->getMainHandItem();
|
||||
if(!$this->isUsingItem() || $this->hasItemCooldown($item)){
|
||||
return false;
|
||||
}
|
||||
@ -1837,21 +1835,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
|
||||
private function equipOrAddPickedItem(int $existingSlot, Item $item) : void{
|
||||
if($existingSlot !== -1){
|
||||
if($existingSlot < $this->inventory->getHotbarSize()){
|
||||
$this->inventory->setHeldItemIndex($existingSlot);
|
||||
if($existingSlot < $this->hotbar->getSize()){
|
||||
$this->hotbar->setSelectedIndex($existingSlot);
|
||||
}else{
|
||||
$this->inventory->swap($this->inventory->getHeldItemIndex(), $existingSlot);
|
||||
$this->inventory->swap($this->hotbar->getSelectedIndex(), $existingSlot);
|
||||
}
|
||||
}else{
|
||||
$firstEmpty = $this->inventory->firstEmpty();
|
||||
if($firstEmpty === -1){ //full inventory
|
||||
$this->inventory->setItemInHand($item);
|
||||
}elseif($firstEmpty < $this->inventory->getHotbarSize()){
|
||||
$this->setMainHandItem($item);
|
||||
}elseif($firstEmpty < $this->hotbar->getSize()){
|
||||
$this->inventory->setItem($firstEmpty, $item);
|
||||
$this->inventory->setHeldItemIndex($firstEmpty);
|
||||
$this->hotbar->setSelectedIndex($firstEmpty);
|
||||
}else{
|
||||
$this->inventory->swap($this->inventory->getHeldItemIndex(), $firstEmpty);
|
||||
$this->inventory->setItemInHand($item);
|
||||
$this->inventory->swap($this->hotbar->getSelectedIndex(), $firstEmpty);
|
||||
$this->setMainHandItem($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1868,7 +1866,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
|
||||
$target = $this->getWorld()->getBlock($pos);
|
||||
|
||||
$ev = new PlayerInteractEvent($this, $this->inventory->getItemInHand(), $target, null, $face, PlayerInteractEvent::LEFT_CLICK_BLOCK);
|
||||
$ev = new PlayerInteractEvent($this, $this->getMainHandItem(), $target, null, $face, PlayerInteractEvent::LEFT_CLICK_BLOCK);
|
||||
if($this->isSpectator()){
|
||||
$ev->cancel();
|
||||
}
|
||||
@ -1877,7 +1875,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
return false;
|
||||
}
|
||||
$this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
|
||||
if($target->onAttack($this->inventory->getItemInHand(), $face, $this)){
|
||||
if($target->onAttack($this->getMainHandItem(), $face, $this)){
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1918,7 +1916,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){
|
||||
$this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
|
||||
$this->stopBreakBlock($pos);
|
||||
$item = $this->inventory->getItemInHand();
|
||||
$item = $this->getMainHandItem();
|
||||
$oldItem = clone $item;
|
||||
$returnedItems = [];
|
||||
if($this->getWorld()->useBreakOn($pos, $item, $this, true, $returnedItems)){
|
||||
@ -1943,7 +1941,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
|
||||
if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){
|
||||
$this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
|
||||
$item = $this->inventory->getItemInHand(); //this is a copy of the real item
|
||||
$item = $this->getMainHandItem(); //this is a copy of the real item
|
||||
$oldItem = clone $item;
|
||||
$returnedItems = [];
|
||||
if($this->getWorld()->useItemOn($pos, $item, $face, $clickOffset, $this, true, $returnedItems)){
|
||||
@ -1972,7 +1970,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
return false;
|
||||
}
|
||||
|
||||
$heldItem = $this->inventory->getItemInHand();
|
||||
$heldItem = $this->getMainHandItem();
|
||||
$oldItem = clone $heldItem;
|
||||
|
||||
$ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_ENTITY_ATTACK, $heldItem->getAttackPoints());
|
||||
@ -2058,15 +2056,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
|
||||
$ev->call();
|
||||
|
||||
$item = $this->inventory->getItemInHand();
|
||||
$item = $this->getMainHandItem();
|
||||
$oldItem = clone $item;
|
||||
if(!$ev->isCancelled()){
|
||||
if($item->onInteractEntity($this, $entity, $clickPos)){
|
||||
if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->inventory->getItemInHand())){
|
||||
if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->getMainHandItem())){
|
||||
if($item instanceof Durable && $item->isBroken()){
|
||||
$this->broadcastSound(new ItemBreakSound());
|
||||
}
|
||||
$this->inventory->setItemInHand($item);
|
||||
$this->setMainHandItem($item);
|
||||
}
|
||||
}
|
||||
return $entity->onInteract($this, $clickPos);
|
||||
@ -2407,7 +2405,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
$this->loadQueue = [];
|
||||
|
||||
$this->removeCurrentWindow();
|
||||
$this->removePermanentInventories();
|
||||
$this->removePermanentWindows();
|
||||
|
||||
$this->perm->getPermissionRecalculationCallbacks()->clear();
|
||||
|
||||
@ -2423,8 +2421,6 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
|
||||
protected function destroyCycles() : void{
|
||||
$this->networkSession = null;
|
||||
unset($this->cursorInventory);
|
||||
unset($this->craftingGrid);
|
||||
$this->spawnPosition = null;
|
||||
$this->deathPosition = null;
|
||||
$this->blockBreakHandler = null;
|
||||
@ -2504,8 +2500,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
$this->getWorld()->dropItem($this->location, $item);
|
||||
}
|
||||
|
||||
$this->hotbar->setSelectedIndex(0);
|
||||
$clearInventory = fn(Inventory $inventory) => $inventory->setContents(array_filter($inventory->getContents(), fn(Item $item) => $item->keepOnDeath()));
|
||||
$this->inventory->setHeldItemIndex(0);
|
||||
$clearInventory($this->inventory);
|
||||
$clearInventory($this->armorInventory);
|
||||
$clearInventory($this->offHandInventory);
|
||||
@ -2714,15 +2710,19 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
}
|
||||
|
||||
protected function addDefaultWindows() : void{
|
||||
$this->cursorInventory = new PlayerCursorInventory($this);
|
||||
$this->craftingGrid = new PlayerCraftingInventory($this);
|
||||
$this->cursorInventory = new SimpleInventory(1);
|
||||
$this->craftingGrid = new CraftingGrid(CraftingGrid::SIZE_SMALL);
|
||||
|
||||
$this->addPermanentInventories($this->inventory, $this->armorInventory, $this->cursorInventory, $this->offHandInventory, $this->craftingGrid);
|
||||
|
||||
//TODO: more windows
|
||||
$this->addPermanentWindows([
|
||||
new PlayerInventoryWindow($this, $this->inventory, PlayerInventoryWindow::TYPE_INVENTORY),
|
||||
new PlayerInventoryWindow($this, $this->armorInventory, PlayerInventoryWindow::TYPE_ARMOR),
|
||||
new PlayerInventoryWindow($this, $this->cursorInventory, PlayerInventoryWindow::TYPE_CURSOR),
|
||||
new PlayerInventoryWindow($this, $this->offHandInventory, PlayerInventoryWindow::TYPE_OFFHAND),
|
||||
new PlayerInventoryWindow($this, $this->craftingGrid, PlayerInventoryWindow::TYPE_CRAFTING),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCursorInventory() : PlayerCursorInventory{
|
||||
public function getCursorInventory() : Inventory{
|
||||
return $this->cursorInventory;
|
||||
}
|
||||
|
||||
@ -2753,22 +2753,37 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
* inventory.
|
||||
*/
|
||||
private function doCloseInventory() : void{
|
||||
$inventories = [$this->craftingGrid, $this->cursorInventory];
|
||||
if($this->currentWindow instanceof TemporaryInventory){
|
||||
$inventories[] = $this->currentWindow;
|
||||
$windowsToClear = [];
|
||||
$mainInventoryWindow = null;
|
||||
foreach($this->permanentWindows as $window){
|
||||
if($window->getType() === PlayerInventoryWindow::TYPE_CRAFTING || $window->getType() === PlayerInventoryWindow::TYPE_CURSOR){
|
||||
$windowsToClear[] = $window;
|
||||
}elseif($window->getType() === PlayerInventoryWindow::TYPE_INVENTORY){
|
||||
$mainInventoryWindow = $window;
|
||||
}
|
||||
}
|
||||
if($mainInventoryWindow === null){
|
||||
//TODO: in the future this might not be the case, if we implement support for the player closing their
|
||||
//inventory window outside the protocol layer
|
||||
//in that case we'd have to create a new ephemeral window here
|
||||
throw new AssumptionFailedError("This should never be null");
|
||||
}
|
||||
|
||||
if($this->currentWindow instanceof TemporaryInventoryWindow){
|
||||
$windowsToClear[] = $this->currentWindow;
|
||||
}
|
||||
|
||||
$builder = new TransactionBuilder();
|
||||
foreach($inventories as $inventory){
|
||||
$contents = $inventory->getContents();
|
||||
foreach($windowsToClear as $window){
|
||||
$contents = $window->getInventory()->getContents();
|
||||
|
||||
if(count($contents) > 0){
|
||||
$drops = $builder->getInventory($this->inventory)->addItem(...$contents);
|
||||
$drops = $builder->getActionBuilder($mainInventoryWindow)->addItem(...$contents);
|
||||
foreach($drops as $drop){
|
||||
$builder->addAction(new DropItemAction($drop));
|
||||
}
|
||||
|
||||
$builder->getInventory($inventory)->clearAll();
|
||||
$builder->getActionBuilder($window)->clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2780,8 +2795,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
$this->logger->debug("Successfully evacuated items from temporary inventories");
|
||||
}catch(TransactionCancelledException){
|
||||
$this->logger->debug("Plugin cancelled transaction evacuating items from temporary inventories; items will be destroyed");
|
||||
foreach($inventories as $inventory){
|
||||
$inventory->clearAll();
|
||||
foreach($windowsToClear as $window){
|
||||
$window->getInventory()->clearAll();
|
||||
}
|
||||
}catch(TransactionValidationException $e){
|
||||
throw new AssumptionFailedError("This server-generated transaction should never be invalid", 0, $e);
|
||||
@ -2792,18 +2807,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
/**
|
||||
* Returns the inventory the player is currently viewing. This might be a chest, furnace, or any other container.
|
||||
*/
|
||||
public function getCurrentWindow() : ?Inventory{
|
||||
public function getCurrentWindow() : ?InventoryWindow{
|
||||
return $this->currentWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an inventory window to the player. Returns if it was successful.
|
||||
*/
|
||||
public function setCurrentWindow(Inventory $inventory) : bool{
|
||||
if($inventory === $this->currentWindow){
|
||||
public function setCurrentWindow(InventoryWindow $window) : bool{
|
||||
if($window === $this->currentWindow){
|
||||
return true;
|
||||
}
|
||||
$ev = new InventoryOpenEvent($inventory, $this);
|
||||
if($window->getViewer() !== $this){
|
||||
throw new \InvalidArgumentException("Cannot reuse InventoryWindow instances, please create a new one for each player");
|
||||
}
|
||||
$ev = new InventoryOpenEvent($window, $this);
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
return false;
|
||||
@ -2814,10 +2832,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
if(($inventoryManager = $this->getNetworkSession()->getInvManager()) === null){
|
||||
throw new \InvalidArgumentException("Player cannot open inventories in this state");
|
||||
}
|
||||
$this->logger->debug("Opening inventory " . get_class($inventory) . "#" . spl_object_id($inventory));
|
||||
$inventoryManager->onCurrentWindowChange($inventory);
|
||||
$inventory->onOpen($this);
|
||||
$this->currentWindow = $inventory;
|
||||
$this->logger->debug("Opening inventory window " . get_class($window) . "#" . spl_object_id($window));
|
||||
$inventoryManager->onCurrentWindowChange($window);
|
||||
$window->onOpen();
|
||||
$this->currentWindow = $window;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -2825,8 +2843,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
$this->doCloseInventory();
|
||||
if($this->currentWindow !== null){
|
||||
$currentWindow = $this->currentWindow;
|
||||
$this->logger->debug("Closing inventory " . get_class($this->currentWindow) . "#" . spl_object_id($this->currentWindow));
|
||||
$this->currentWindow->onClose($this);
|
||||
$this->logger->debug("Closing inventory window " . get_class($this->currentWindow) . "#" . spl_object_id($this->currentWindow));
|
||||
$this->currentWindow->onClose();
|
||||
if(($inventoryManager = $this->getNetworkSession()->getInvManager()) !== null){
|
||||
$inventoryManager->onCurrentWindowRemove();
|
||||
}
|
||||
@ -2835,20 +2853,31 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
}
|
||||
}
|
||||
|
||||
protected function addPermanentInventories(Inventory ...$inventories) : void{
|
||||
foreach($inventories as $inventory){
|
||||
$inventory->onOpen($this);
|
||||
$this->permanentWindows[spl_object_id($inventory)] = $inventory;
|
||||
/**
|
||||
* @param PlayerInventoryWindow[] $windows
|
||||
*/
|
||||
protected function addPermanentWindows(array $windows) : void{
|
||||
foreach($windows as $window){
|
||||
$window->onOpen();
|
||||
$this->permanentWindows[spl_object_id($window)] = $window;
|
||||
}
|
||||
}
|
||||
|
||||
protected function removePermanentInventories() : void{
|
||||
foreach($this->permanentWindows as $inventory){
|
||||
$inventory->onClose($this);
|
||||
protected function removePermanentWindows() : void{
|
||||
foreach($this->permanentWindows as $window){
|
||||
$window->onClose();
|
||||
}
|
||||
$this->permanentWindows = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PlayerInventoryWindow[]
|
||||
* @internal
|
||||
*/
|
||||
public function getPermanentWindows() : array{
|
||||
return $this->permanentWindows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the player's sign editor GUI for the sign at the given position.
|
||||
*/
|
||||
|
51
src/player/PlayerInventoryWindow.php
Normal file
51
src/player/PlayerInventoryWindow.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?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\player;
|
||||
|
||||
use pocketmine\inventory\Inventory;
|
||||
|
||||
/**
|
||||
* Window for player-owned inventories. The player can access these at all times.
|
||||
*/
|
||||
final class PlayerInventoryWindow extends InventoryWindow{
|
||||
|
||||
public const TYPE_INVENTORY = 0;
|
||||
public const TYPE_OFFHAND = 1;
|
||||
public const TYPE_ARMOR = 2;
|
||||
public const TYPE_CURSOR = 3;
|
||||
public const TYPE_CRAFTING = 4;
|
||||
|
||||
public function __construct(
|
||||
Player $viewer,
|
||||
Inventory $inventory,
|
||||
private int $type
|
||||
){
|
||||
parent::__construct($viewer, $inventory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of player inventory in this window.
|
||||
*/
|
||||
public function getType() : int{ return $this->type; }
|
||||
}
|
@ -67,7 +67,7 @@ final class SurvivalBlockBreakHandler{
|
||||
if(!$this->block->getBreakInfo()->isBreakable()){
|
||||
return 0.0;
|
||||
}
|
||||
$breakTimePerTick = $this->block->getBreakInfo()->getBreakTime($this->player->getInventory()->getItemInHand()) * 20;
|
||||
$breakTimePerTick = $this->block->getBreakInfo()->getBreakTime($this->player->getMainHandItem()) * 20;
|
||||
if(!$this->player->isOnGround() && !$this->player->isFlying()){
|
||||
$breakTimePerTick *= 5;
|
||||
}
|
||||
|
@ -21,8 +21,8 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\inventory;
|
||||
namespace pocketmine\player;
|
||||
|
||||
interface TemporaryInventory extends Inventory{
|
||||
interface TemporaryInventoryWindow{
|
||||
|
||||
}
|
270
tests/phpunit/inventory/CombinedInventoryProxyTest.php
Normal file
270
tests/phpunit/inventory/CombinedInventoryProxyTest.php
Normal file
@ -0,0 +1,270 @@
|
||||
<?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\inventory;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\ItemTypeIds;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use function array_filter;
|
||||
|
||||
final class CombinedInventoryProxyTest extends TestCase{
|
||||
|
||||
/**
|
||||
* @return Inventory[]
|
||||
* @phpstan-return list<Inventory>
|
||||
*/
|
||||
private function createInventories() : array{
|
||||
$inventory1 = new SimpleInventory(1);
|
||||
$inventory1->setItem(0, VanillaItems::APPLE());
|
||||
$inventory2 = new SimpleInventory(1);
|
||||
$inventory2->setItem(0, VanillaItems::PAPER());
|
||||
$inventory3 = new SimpleInventory(2);
|
||||
$inventory3->setItem(1, VanillaItems::BONE());
|
||||
|
||||
return [$inventory1, $inventory2, $inventory3];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Item[] $items
|
||||
* @phpstan-param array<int, Item> $items
|
||||
*/
|
||||
private function verifyReadItems(array $items) : void{
|
||||
self::assertSame(ItemTypeIds::APPLE, $items[0]->getTypeId());
|
||||
self::assertSame(ItemTypeIds::PAPER, $items[1]->getTypeId());
|
||||
self::assertTrue($items[2]->isNull());
|
||||
self::assertSame(ItemTypeIds::BONE, $items[3]->getTypeId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Item[]
|
||||
* @phpstan-return list<Item>
|
||||
*/
|
||||
private static function getAltItems() : array{
|
||||
return [
|
||||
VanillaItems::AMETHYST_SHARD(),
|
||||
VanillaItems::AIR(), //null item
|
||||
VanillaItems::BLAZE_POWDER(),
|
||||
VanillaItems::BRICK()
|
||||
];
|
||||
}
|
||||
|
||||
public function testGetItem() : void{
|
||||
$inventory = new CombinedInventoryProxy($this->createInventories());
|
||||
|
||||
$this->verifyReadItems([
|
||||
$inventory->getItem(0),
|
||||
$inventory->getItem(1),
|
||||
$inventory->getItem(2),
|
||||
$inventory->getItem(3)
|
||||
]);
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$inventory->getItem(4);
|
||||
}
|
||||
|
||||
public function testGetContents() : void{
|
||||
$inventory = new CombinedInventoryProxy($this->createInventories());
|
||||
|
||||
$this->verifyReadItems($inventory->getContents(includeEmpty: true));
|
||||
|
||||
$contentsWithoutEmpty = $inventory->getContents(includeEmpty: false);
|
||||
self::assertFalse(isset($contentsWithoutEmpty[2]), "This index should not be set during this test");
|
||||
self::assertCount(3, $contentsWithoutEmpty);
|
||||
$this->verifyReadItems([
|
||||
$contentsWithoutEmpty[0],
|
||||
$contentsWithoutEmpty[1],
|
||||
VanillaItems::AIR(),
|
||||
$contentsWithoutEmpty[3]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Inventory[] $backing
|
||||
* @param Item[] $altItems
|
||||
*
|
||||
* @phpstan-param array<int, Inventory> $backing
|
||||
* @phpstan-param array<int, Item> $altItems
|
||||
*/
|
||||
private function verifyWriteItems(array $backing, array $altItems) : void{
|
||||
foreach([
|
||||
0 => [$backing[0], 0],
|
||||
1 => [$backing[1], 0],
|
||||
2 => [$backing[2], 0],
|
||||
3 => [$backing[2], 1]
|
||||
] as $combinedSlot => [$backingInventory, $backingSlot]){
|
||||
if(!isset($altItems[$combinedSlot])){
|
||||
self::assertTrue($backingInventory->isSlotEmpty($backingSlot));
|
||||
}else{
|
||||
self::assertSame($altItems[$combinedSlot]->getTypeId(), $backingInventory->getItem($backingSlot)->getTypeId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testSetItem() : void{
|
||||
$backing = $this->createInventories();
|
||||
$inventory = new CombinedInventoryProxy($backing);
|
||||
|
||||
$altItems = self::getAltItems();
|
||||
foreach($altItems as $slot => $item){
|
||||
$inventory->setItem($slot, $item);
|
||||
}
|
||||
$this->verifyWriteItems($backing, $altItems);
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$inventory->setItem(4, VanillaItems::BRICK());
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return \Generator<int, array{array<int, Item>}, void, void>
|
||||
*/
|
||||
public static function setContentsProvider() : \Generator{
|
||||
$altItems = self::getAltItems();
|
||||
|
||||
yield [$altItems];
|
||||
yield [array_filter($altItems, fn(Item $item) => !$item->isNull())];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Item[] $altItems
|
||||
* @phpstan-param array<int, Item> $altItems
|
||||
*/
|
||||
#[DataProvider("setContentsProvider")]
|
||||
public function testSetContents(array $altItems) : void{
|
||||
$backing = $this->createInventories();
|
||||
$inventory = new CombinedInventoryProxy($backing);
|
||||
$inventory->setContents($altItems);
|
||||
|
||||
$this->verifyWriteItems($backing, $altItems);
|
||||
}
|
||||
|
||||
public function testGetSize() : void{
|
||||
self::assertSame(4, (new CombinedInventoryProxy($this->createInventories()))->getSize());
|
||||
}
|
||||
|
||||
public function testGetMatchingItemCount() : void{
|
||||
$inventory = new CombinedInventoryProxy($this->createInventories());
|
||||
//we don't need to test the base functionality, only ensure that the correct delegate is called
|
||||
self::assertSame(1, $inventory->getMatchingItemCount(3, VanillaItems::BONE(), true));
|
||||
self::assertNotSame(1, $inventory->getMatchingItemCount(3, VanillaItems::PAPER(), true));
|
||||
}
|
||||
|
||||
public function testIsSlotEmpty() : void{
|
||||
$inventory = new CombinedInventoryProxy($this->createInventories());
|
||||
|
||||
self::assertTrue($inventory->isSlotEmpty(2));
|
||||
self::assertFalse($inventory->isSlotEmpty(0));
|
||||
self::assertFalse($inventory->isSlotEmpty(1));
|
||||
self::assertFalse($inventory->isSlotEmpty(3));
|
||||
}
|
||||
|
||||
public function testListenersOnProxySlotUpdate() : void{
|
||||
$inventory = new CombinedInventoryProxy($this->createInventories());
|
||||
|
||||
$numChanges = 0;
|
||||
$inventory->getListeners()->add(new CallbackInventoryListener(
|
||||
onSlotChange: function(Inventory $inventory, int $slot, Item $before) use (&$numChanges) : void{
|
||||
$numChanges++;
|
||||
},
|
||||
onContentChange: null
|
||||
));
|
||||
$inventory->setItem(0, VanillaItems::DIAMOND_SWORD());
|
||||
self::assertSame(1, $numChanges, "Inventory listener detected wrong number of changes");
|
||||
}
|
||||
|
||||
public function testListenersOnProxyContentUpdate() : void{
|
||||
$inventory = new CombinedInventoryProxy($this->createInventories());
|
||||
|
||||
$numChanges = 0;
|
||||
$inventory->getListeners()->add(new CallbackInventoryListener(
|
||||
onSlotChange: null,
|
||||
onContentChange: function(Inventory $inventory, array $oldItems) use (&$numChanges) : void{
|
||||
$numChanges++;
|
||||
}
|
||||
));
|
||||
$inventory->setContents(self::getAltItems());
|
||||
self::assertSame(1, $numChanges, "Expected onContentChange to be called exactly 1 time");
|
||||
}
|
||||
|
||||
public function testListenersOnBackingSlotUpdate() : void{
|
||||
$backing = $this->createInventories();
|
||||
$inventory = new CombinedInventoryProxy($backing);
|
||||
|
||||
$slotChangeDetected = null;
|
||||
$numChanges = 0;
|
||||
$inventory->getListeners()->add(new CallbackInventoryListener(
|
||||
onSlotChange: function(Inventory $inventory, int $slot, Item $before) use (&$slotChangeDetected, &$numChanges) : void{
|
||||
$slotChangeDetected = $slot;
|
||||
$numChanges++;
|
||||
},
|
||||
onContentChange: null
|
||||
));
|
||||
$backing[2]->setItem(0, VanillaItems::DIAMOND_SWORD());
|
||||
self::assertNotNull($slotChangeDetected, "Inventory listener didn't hear about backing inventory update");
|
||||
self::assertSame(2, $slotChangeDetected, "Inventory listener detected unexpected slot change");
|
||||
self::assertSame(1, $numChanges, "Inventory listener detected wrong number of changes");
|
||||
}
|
||||
|
||||
/**
|
||||
* When a combined inventory has multiple backing inventories, content updates of the backing inventories must be
|
||||
* turned into slot updates on the proxy, to avoid syncing the entire proxy inventory.
|
||||
*/
|
||||
public function testListenersOnBackingContentUpdate() : void{
|
||||
$backing = $this->createInventories();
|
||||
$inventory = new CombinedInventoryProxy($backing);
|
||||
|
||||
$slotChanges = [];
|
||||
$inventory->getListeners()->add(new CallbackInventoryListener(
|
||||
onSlotChange: function(Inventory $inventory, int $slot, Item $before) use (&$slotChanges) : void{
|
||||
$slotChanges[] = $slot;
|
||||
},
|
||||
onContentChange: null
|
||||
));
|
||||
$backing[2]->setContents([VanillaItems::DIAMOND_SWORD(), VanillaItems::DIAMOND()]);
|
||||
self::assertCount(2, $slotChanges, "Inventory listener detected wrong number of changes");
|
||||
self::assertSame([2, 3], $slotChanges, "Incorrect slots updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* If a combined inventory has only 1 backing inventory, content updates on the backing inventory can be directly
|
||||
* processed as content updates on the proxy inventory without modification. This allows optimizations when only 1
|
||||
* backing inventory is used.
|
||||
* This test verifies that this special case works as expected.
|
||||
*/
|
||||
public function testListenersOnSingleBackingContentUpdate() : void{
|
||||
$backing = new SimpleInventory(2);
|
||||
$inventory = new CombinedInventoryProxy([$backing]);
|
||||
|
||||
$numChanges = 0;
|
||||
$inventory->getListeners()->add(new CallbackInventoryListener(
|
||||
onSlotChange: null,
|
||||
onContentChange: function(Inventory $inventory, array $oldItems) use (&$numChanges) : void{
|
||||
$numChanges++;
|
||||
}
|
||||
));
|
||||
$inventory->setContents([VanillaItems::DIAMOND_SWORD(), VanillaItems::DIAMOND()]);
|
||||
self::assertSame(1, $numChanges, "Expected onContentChange to be called exactly 1 time");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user