First look: Split up Inventory & InventoryWindow

this unblocks a variety of changes, such as positionless tiles, enhanced APIs on Blocks for inventories, and also eliminates a bunch of cyclic references within the core code.

linked to #5033
This commit is contained in:
Dylan K. Taylor 2024-11-24 21:40:47 +00:00
parent 473bbe64e0
commit 45a4282e8b
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
74 changed files with 827 additions and 933 deletions

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\inventory\AnvilInventoryWindow;
use pocketmine\block\utils\Fallable;
use pocketmine\block\utils\FallableTrait;
use pocketmine\block\utils\HorizontalFacingTrait;
@ -83,7 +83,7 @@ class Anvil extends Transparent implements Fallable{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$player->setCurrentWindow(new AnvilInventory($this->position));
$player->setCurrentWindow(new AnvilInventoryWindow($player, $this->position));
}
return true;

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\BarrelInventoryWindow;
use pocketmine\block\tile\Barrel as TileBarrel;
use pocketmine\block\utils\AnyFacingTrait;
use pocketmine\data\runtime\RuntimeDataDescriber;
@ -81,7 +82,7 @@ class Barrel extends Opaque{
return true;
}
$player->setCurrentWindow($barrel->getInventory());
$player->setCurrentWindow(new BarrelInventoryWindow($player, $barrel->getInventory(), $this->position));
}
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\BrewingStandInventoryWindow;
use pocketmine\block\tile\BrewingStand as TileBrewingStand;
use pocketmine\block\utils\BrewingStandSlot;
use pocketmine\block\utils\SupportType;
@ -99,7 +100,7 @@ class BrewingStand extends Transparent{
if($player instanceof Player){
$stand = $this->position->getWorld()->getTile($this->position);
if($stand instanceof TileBrewingStand && $stand->canOpenWith($item->getCustomName())){
$player->setCurrentWindow($stand->getInventory());
$player->setCurrentWindow(new BrewingStandInventoryWindow($player, $stand->getInventory(), $this->position));
}
}

View File

@ -71,6 +71,13 @@ class Campfire extends Transparent{
protected CampfireInventory $inventory;
public function __construct(BlockIdentifier $idInfo, string $name, BlockTypeInfo $typeInfo){
parent::__construct($idInfo, $name, $typeInfo);
//TODO: this should never have been in the block - it creates problems for setting blocks in different positions
//as inventories aren't designed to be cloned
$this->inventory = new CampfireInventory();
}
/**
* @var int[] slot => ticks
* @phpstan-var array<int, int>
@ -89,7 +96,7 @@ class Campfire extends Transparent{
$this->inventory = $tile->getInventory();
$this->cookingTimes = $tile->getCookingTimes();
}else{
$this->inventory = new CampfireInventory($this->position);
$this->inventory = new CampfireInventory();
}
return $this;

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\CartographyTableInventory;
use pocketmine\block\inventory\CartographyTableInventoryWindow;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
@ -32,7 +32,7 @@ final class CartographyTable extends Opaque{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player !== null){
$player->setCurrentWindow(new CartographyTableInventory($this->position));
$player->setCurrentWindow(new CartographyTableInventoryWindow($player, $this->position));
}
return true;

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\ChestInventoryWindow;
use pocketmine\block\inventory\DoubleChestInventoryWindow;
use pocketmine\block\tile\Chest as TileChest;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\SupportType;
@ -74,8 +76,8 @@ class Chest extends Transparent{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$chest = $this->position->getWorld()->getTile($this->position);
$world = $this->position->getWorld();
$chest = $world->getTile($this->position);
if($chest instanceof TileChest){
if(
!$this->getSide(Facing::UP)->isTransparent() ||
@ -85,7 +87,22 @@ class Chest extends Transparent{
return true;
}
$player->setCurrentWindow($chest->getInventory());
foreach([false, true] as $clockwise){
$sideFacing = Facing::rotateY($this->facing, $clockwise);
$side = $this->position->getSide($sideFacing);
$pair = $world->getTile($side);
if($pair instanceof TileChest && $pair->getPair() === $chest){
[$left, $right] = $clockwise ? [$side, $this->position] : [$this->position, $side];
//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
$window = new DoubleChestInventoryWindow($player, $chest->getInventory(), $left, $right);
break;
}
}
$player->setCurrentWindow($window ?? new ChestInventoryWindow($player, $chest->getInventory(), $this->position));
}
}

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\CraftingTableInventory;
use pocketmine\block\inventory\CraftingTableInventoryWindow;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
@ -32,7 +32,7 @@ class CraftingTable extends Opaque{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$player->setCurrentWindow(new CraftingTableInventory($this->position));
$player->setCurrentWindow(new CraftingTableInventoryWindow($player, $this->position));
}
return true;

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\EnchantingTableInventoryWindow;
use pocketmine\block\utils\SupportType;
use pocketmine\item\Item;
use pocketmine\math\AxisAlignedBB;
@ -48,7 +48,7 @@ class EnchantingTable extends Transparent{
if($player instanceof Player){
//TODO lock
$player->setCurrentWindow(new EnchantInventory($this->position));
$player->setCurrentWindow(new EnchantingTableInventoryWindow($player, $this->position));
}
return true;

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\EnderChestInventory;
use pocketmine\block\inventory\EnderChestInventoryWindow;
use pocketmine\block\tile\EnderChest as TileEnderChest;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\SupportType;
@ -56,8 +56,7 @@ class EnderChest extends Transparent{
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()));
$player->setCurrentWindow(new EnderChestInventoryWindow($player, $player->getEnderInventory(), $this->position));
}
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\FurnaceInventoryWindow;
use pocketmine\block\tile\Furnace as TileFurnace;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\LightableTrait;
@ -61,7 +62,7 @@ class Furnace extends Opaque{
if($player instanceof Player){
$furnace = $this->position->getWorld()->getTile($this->position);
if($furnace instanceof TileFurnace && $furnace->canOpenWith($item->getCustomName())){
$player->setCurrentWindow($furnace->getInventory());
$player->setCurrentWindow(new FurnaceInventoryWindow($player, $furnace->getInventory(), $this->position, $this->furnaceType));
}
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\HopperInventoryWindow;
use pocketmine\block\tile\Hopper as TileHopper;
use pocketmine\block\utils\PoweredByRedstoneTrait;
use pocketmine\block\utils\SupportType;
@ -84,7 +85,7 @@ class Hopper extends Transparent{
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());
$player->setCurrentWindow(new HopperInventoryWindow($player, $tile->getInventory(), $this->position));
}
return true;
}

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\LoomInventory;
use pocketmine\block\inventory\LoomInventoryWindow;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
@ -34,7 +34,7 @@ final class Loom extends Opaque{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player !== null){
$player->setCurrentWindow(new LoomInventory($this->position));
$player->setCurrentWindow(new LoomInventoryWindow($player, $this->position));
return true;
}
return false;

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\ShulkerBoxInventoryWindow;
use pocketmine\block\tile\ShulkerBox as TileShulkerBox;
use pocketmine\block\utils\AnyFacingTrait;
use pocketmine\block\utils\SupportType;
@ -105,7 +106,7 @@ class ShulkerBox extends Opaque{
return true;
}
$player->setCurrentWindow($shulker->getInventory());
$player->setCurrentWindow(new ShulkerBoxInventoryWindow($player, $shulker->getInventory(), $this->position));
}
}

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\SmithingTableInventory;
use pocketmine\block\inventory\SmithingTableInventoryWindow;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
@ -32,7 +32,7 @@ final class SmithingTable extends Opaque{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player !== null){
$player->setCurrentWindow(new SmithingTableInventory($this->position));
$player->setCurrentWindow(new SmithingTableInventoryWindow($player, $this->position));
}
return true;

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\StonecutterInventory;
use pocketmine\block\inventory\StonecutterInventoryWindow;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\item\Item;
@ -37,7 +37,7 @@ class Stonecutter extends Transparent{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player !== null){
$player->setCurrentWindow(new StonecutterInventory($this->position));
$player->setCurrentWindow(new StonecutterInventoryWindow($player, $this->position));
}
return true;
}

View File

@ -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);
}
}

View File

@ -0,0 +1,65 @@
<?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;
use pocketmine\world\sound\Sound;
use function count;
abstract class AnimatedBlockInventoryWindow extends BlockInventoryWindow{
protected function getViewerCount() : int{
return count($this->inventory->getViewers());
}
abstract protected function getOpenSound() : Sound;
abstract protected function getCloseSound() : Sound;
abstract protected function animateBlock(Position $position, bool $isOpen) : void;
protected function playSound(Position $position, bool $isOpen) : void{
$position->getWorld()->addSound($position->add(0.5, 0.5, 0.5), $isOpen ? $this->getOpenSound() : $this->getCloseSound());
}
protected function doBlockEffects(bool $isOpen) : void{
$position = $this->holder;
$this->animateBlock($position, $isOpen);
$this->playSound($position, $isOpen);
}
public function onOpen() : void{
parent::onOpen();
if($this->getViewerCount() === 1){
$this->doBlockEffects(true);
}
}
public function onClose() : void{
if($this->getViewerCount() === 1){
$this->doBlockEffects(false);
}
parent::onClose();
}
}

View File

@ -24,17 +24,18 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
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);
}
}

View File

@ -24,19 +24,12 @@ 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);
}
final class BarrelInventoryWindow extends AnimatedBlockInventoryWindow{
protected function getOpenSound() : Sound{
return new BarrelOpenSound();
@ -46,12 +39,11 @@ class BarrelInventory extends SimpleInventory implements BlockInventory{
return new BarrelCloseSound();
}
protected function animateBlock(bool $isOpen) : void{
$holder = $this->getHolder();
$world = $holder->getWorld();
$block = $world->getBlock($holder);
protected function animateBlock(Position $position, bool $isOpen) : void{
$world = $position->getWorld();
$block = $world->getBlock($position);
if($block instanceof Barrel){
$world->setBlock($holder, $block->setOpen($isOpen));
$world->setBlock($position, $block->setOpen($isOpen));
}
}
}

View 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\inventory;
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; }
}

View File

@ -23,12 +23,10 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\world\Position;
trait BlockInventoryTrait{
protected Position $holder;
public function getHolder() : Position{
return $this->holder;
}
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;
}

View File

@ -24,13 +24,9 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\world\Position;
class CampfireInventory extends SimpleInventory implements BlockInventory{
use BlockInventoryTrait;
public function __construct(Position $holder){
$this->holder = $holder;
final class CampfireInventory extends SimpleInventory{
public function __construct(){
parent::__construct(4);
}

View File

@ -24,14 +24,16 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\player\Player;
use pocketmine\player\TemporaryInventoryWindow;
use pocketmine\world\Position;
final class CartographyTableInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
final class CartographyTableInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
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);
}
}

View File

@ -23,7 +23,6 @@ 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;
@ -31,13 +30,7 @@ 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);
}
class ChestInventoryWindow extends AnimatedBlockInventoryWindow{
protected function getOpenSound() : Sound{
return new ChestOpenSound();
@ -47,10 +40,9 @@ class ChestInventory extends SimpleInventory implements BlockInventory{
return new ChestCloseSound();
}
public function animateBlock(bool $isOpen) : void{
$holder = $this->getHolder();
protected function animateBlock(Position $position, bool $isOpen) : void{
//event ID is always 1 for a chest
$holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
//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));
}
}

View File

@ -24,14 +24,16 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
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);
}
}

View File

@ -24,27 +24,17 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\BaseInventory;
use pocketmine\inventory\InventoryHolder;
use pocketmine\inventory\Inventory;
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;
final class DoubleChestInventory extends BaseInventory{
public function __construct(
private ChestInventory $left,
private ChestInventory $right
private Inventory $left,
private Inventory $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();
}
@ -85,7 +75,7 @@ class DoubleChestInventory extends BaseInventory implements BlockInventory, Inve
$this->right->setContents($rightContents);
}
protected function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
$leftSize = $this->left->getSize();
return $slot < $leftSize ?
$this->left->getMatchingItemCount($slot, $test, $checkTags) :
@ -99,20 +89,11 @@ class DoubleChestInventory extends BaseInventory implements BlockInventory, Inve
$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{
public function getLeftSide() : Inventory{
return $this->left;
}
public function getRightSide() : ChestInventory{
public function getRightSide() : Inventory{
return $this->right;
}
}

View File

@ -23,20 +23,30 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\Inventory;
use pocketmine\player\Player;
use pocketmine\world\Position;
class BrewingStandInventory extends SimpleInventory implements BlockInventory{
use BlockInventoryTrait;
final class DoubleChestInventoryWindow extends ChestInventoryWindow{
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(
Player $viewer,
Inventory $inventory,
private Position $left,
private Position $right
){
parent::__construct($viewer, $inventory, $this->left);
}
public function __construct(Position $holder, int $size = 5){
$this->holder = $holder;
parent::__construct($size);
public function getLeft() : Position{ return $this->left; }
public function getRight() : Position{ return $this->right; }
protected function doBlockEffects(bool $isOpen) : void{
$this->animateBlock($this->left, $isOpen);
$this->animateBlock($this->right, $isOpen);
$this->playSound($this->left, $isOpen);
$this->playSound($this->right, $isOpen);
}
}

View File

@ -1,85 +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 */
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;
}
}

View File

@ -0,0 +1,110 @@
<?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\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 = [];
/** @phpstan-var \WeakReference<InventoryListener> */
private \WeakReference $listener;
public function __construct(
Player $viewer,
Position $holder
){
parent::__construct($viewer, new SimpleInventory(2), $holder);
/** @phpstan-var \WeakReference<self> $weakThis */
$weakThis = \WeakReference::create($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($listener);
$this->listener = \WeakReference::create($listener);
}
public function __destruct(){
$listener = $this->listener->get();
if($listener !== null){
$this->inventory->getListeners()->remove($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;
}
}

View File

@ -24,45 +24,30 @@ 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;
}
final class EnderChestInventoryWindow extends AnimatedBlockInventoryWindow{
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());
protected function getViewerCount() : int{
$enderChest = $this->holder->getWorld()->getTile($this->getHolder());
if(!$enderChest instanceof EnderChest){
return 0;
}
return $enderChest->getViewerCount();
}
private function updateViewerCount(int $amount) : void{
$enderChest = $this->holder->getWorld()->getTile($this->getHolder());
if($enderChest instanceof EnderChest){
$enderChest->setViewerCount($enderChest->getViewerCount() + $amount);
}
}
protected function getOpenSound() : Sound{
return new EnderChestOpenSound();
}
@ -71,18 +56,18 @@ class EnderChestInventory extends DelegateInventory implements BlockInventory{
return new EnderChestCloseSound();
}
protected function animateBlock(bool $isOpen) : void{
$holder = $this->getHolder();
protected function animateBlock(Position $position, bool $isOpen) : void{
//event ID is always 1 for a chest
$holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
$position->getWorld()->broadcastPacketToViewers($position, BlockEventPacket::create(BlockPosition::fromVector3($position), 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);
}
public function onOpen() : void{
parent::onOpen();
$this->updateViewerCount(1);
}
public function onClose() : void{
parent::onClose();
$this->updateViewerCount(-1);
}
}

View File

@ -24,48 +24,48 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
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);
}
}

View File

@ -23,8 +23,6 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\world\Position;
final class HopperInventoryWindow extends BlockInventoryWindow{
interface BlockInventory{
public function getHolder() : Position;
}

View File

@ -24,18 +24,20 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
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);
}
}

View File

@ -23,10 +23,6 @@ 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;
@ -34,14 +30,7 @@ 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);
}
final class ShulkerBoxInventoryWindow extends AnimatedBlockInventoryWindow{
protected function getOpenSound() : Sound{
return new ShulkerBoxOpenSound();
}
@ -50,18 +39,8 @@ class ShulkerBoxInventory extends SimpleInventory implements BlockInventory{
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();
protected function animateBlock(Position $position, bool $isOpen) : void{
//event ID is always 1 for a chest
$holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
$position->getWorld()->broadcastPacketToViewers($position, BlockEventPacket::create(BlockPosition::fromVector3($position), 1, $isOpen ? 1 : 0));
}
}

View File

@ -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);
}
}

View File

@ -24,13 +24,12 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
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);
}
}

View File

@ -24,16 +24,14 @@ declare(strict_types=1);
namespace pocketmine\block\inventory;
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);
}
}

View File

@ -23,7 +23,8 @@ 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;
@ -32,11 +33,11 @@ class Barrel extends Spawnable implements Container, Nameable{
use NameableTrait;
use ContainerTrait;
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;
}

View File

@ -23,12 +23,13 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
use pocketmine\block\inventory\BrewingStandInventory;
use pocketmine\block\inventory\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;
@ -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{

View File

@ -53,7 +53,7 @@ class Campfire extends Spawnable implements Container{
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
$this->inventory = new CampfireInventory($this->position);
$this->inventory = new CampfireInventory();
$this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange(
static function(Inventory $unused) use ($world, $pos) : void{
$block = $world->getBlock($pos);

View File

@ -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\Inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntTag;
@ -44,7 +45,7 @@ class Chest extends Spawnable implements Container, Nameable{
public const TAG_PAIRZ = "pairz";
public const TAG_PAIR_LEAD = "pairlead";
protected ChestInventory $inventory;
protected Inventory $inventory;
protected ?DoubleChestInventory $doubleInventory = null;
private ?int $pairX = null;
@ -52,7 +53,7 @@ class Chest extends Spawnable implements Container, Nameable{
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|DoubleChestInventory{
if($this->isPaired() && $this->doubleInventory === null){
$this->checkPairing();
}
return $this->doubleInventory instanceof DoubleChestInventory ? $this->doubleInventory : $this->inventory;
}
public function getRealInventory() : ChestInventory{
public function getRealInventory() : Inventory{
return $this->inventory;
}

View File

@ -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\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;
@ -48,14 +49,14 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
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();

View File

@ -23,7 +23,8 @@ 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;
@ -35,12 +36,12 @@ class Hopper extends Spawnable implements Container, Nameable{
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;
}
}

View File

@ -23,8 +23,13 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
use pocketmine\block\inventory\ShulkerBoxInventory;
use pocketmine\block\BlockTypeIds;
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;
@ -40,11 +45,20 @@ class ShulkerBox extends Spawnable implements Container, Nameable{
protected int $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{
@ -93,11 +107,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;
}

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block\utils;
use pocketmine\block\inventory\BrewingStandInventory;
use pocketmine\block\inventory\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
};
}
}

View File

@ -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;

View File

@ -35,9 +35,7 @@ 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;
@ -102,9 +100,9 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
public function getNetworkTypeId() : string{ return EntityIds::PLAYER; }
protected Hotbar $hotbar;
protected PlayerInventory $inventory;
protected PlayerOffHandInventory $offHandInventory;
protected PlayerEnderInventory $enderInventory;
protected Inventory $inventory;
protected Inventory $offHandInventory;
protected Inventory $enderInventory;
protected UuidInterface $uuid;
@ -234,13 +232,13 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
return $this->hotbar;
}
public function getInventory() : PlayerInventory{
public function getInventory() : Inventory{
return $this->inventory;
}
public function getOffHandInventory() : PlayerOffHandInventory{ return $this->offHandInventory; }
public function getOffHandInventory() : Inventory{ return $this->offHandInventory; }
public function getEnderInventory() : PlayerEnderInventory{
public function getEnderInventory() : Inventory{
return $this->enderInventory;
}
@ -271,7 +269,7 @@ 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($this->inventory);
$syncHeldItem = fn() => NetworkBroadcastUtils::broadcastEntityEvent(
@ -290,8 +288,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
}
}
));
$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);
@ -335,6 +333,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
}
$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)
@ -546,9 +545,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
protected function destroyCycles() : void{
unset(
$this->inventory,
$this->offHandInventory,
$this->enderInventory,
$this->hungerManager,
$this->xpManager
);

View File

@ -145,7 +145,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(),
@ -928,7 +928,6 @@ abstract class Living extends Entity{
protected function destroyCycles() : void{
unset(
$this->armorInventory,
$this->effectManager
);
parent::destroyCycles();

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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);

View File

@ -23,10 +23,9 @@ declare(strict_types=1);
namespace pocketmine\event\player;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\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;
}

View File

@ -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);
}

View File

@ -107,15 +107,6 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
$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();
@ -345,7 +336,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]);

View File

@ -85,6 +85,10 @@ class DelegateInventory extends BaseInventory{
$this->backingInventory->setContents($items);
}
public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
return $this->backingInventory->getMatchingItemCount($slot, $test, $checkTags);
}
public function isSlotEmpty(int $index) : bool{
return $this->backingInventory->isSlotEmpty($index);
}

View File

@ -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.
*/

View File

@ -1,36 +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\crafting\CraftingGrid;
use pocketmine\player\Player;
final class PlayerCraftingInventory extends CraftingGrid implements TemporaryInventory{
public function __construct(private Player $holder){
parent::__construct(CraftingGrid::SIZE_SMALL);
}
public function getHolder() : Player{ return $this->holder; }
}

View File

@ -1,38 +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\player\Player;
class PlayerCursorInventory extends SimpleInventory implements TemporaryInventory{
public function __construct(
protected Player $holder
){
parent::__construct(1);
}
public function getHolder() : Player{
return $this->holder;
}
}

View File

@ -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 PlayerEnderInventory extends SimpleInventory{
public function __construct(
private Human $holder,
int $size = 27
){
parent::__construct($size);
}
public function getHolder() : Human{ return $this->holder; }
}

View File

@ -1,40 +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;
class PlayerInventory extends SimpleInventory{
protected Human $holder;
public function __construct(Human $player){
$this->holder = $player;
parent::__construct(36);
}
public function getHolder() : Human{
return $this->holder;
}
}

View File

@ -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; }
}

View File

@ -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;
}

View File

@ -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 function array_keys;
use function array_values;
@ -57,8 +58,8 @@ use function spl_object_id;
class InventoryTransaction{
protected bool $hasExecuted = false;
/** @var Inventory[] */
protected array $inventories = [];
/** @var InventoryWindow[] */
protected array $inventoryWindows = [];
/** @var InventoryAction[] */
protected array $actions = [];
@ -80,10 +81,10 @@ class InventoryTransaction{
}
/**
* @return Inventory[]
* @return InventoryWindow[]
*/
public function getInventories() : array{
return $this->inventories;
public function getInventoryWindows() : array{
return $this->inventoryWindows;
}
/**
@ -124,9 +125,9 @@ class InventoryTransaction{
* @internal This method should not be used by plugins, it's used to add tracked inventories for InventoryActions
* involving inventories.
*/
public function addInventory(Inventory $inventory) : void{
if(!isset($this->inventories[$hash = spl_object_id($inventory)])){
$this->inventories[$hash] = $inventory;
public function addInventoryWindow(InventoryWindow $inventoryWindow) : void{
if(!isset($this->inventoryWindows[$hash = spl_object_id($inventoryWindow)])){
$this->inventoryWindows[$hash] = $inventoryWindow;
}
}
@ -190,15 +191,15 @@ class InventoryTransaction{
protected function squashDuplicateSlotChanges() : void{
/** @var SlotChangeAction[][] $slotChanges */
$slotChanges = [];
/** @var Inventory[] $inventories */
/** @var InventoryWindow[] $inventories */
$inventories = [];
/** @var int[] $slots */
$slots = [];
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();
}
}
@ -208,10 +209,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);
@ -226,7 +228,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));
}
}
}

View File

@ -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,6 +93,14 @@ 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[]
*/
@ -99,9 +108,9 @@ final class TransactionBuilderInventory extends BaseInventory{
$result = [];
foreach($this->changedSlots as $index => $newItem){
if($newItem !== null){
$oldItem = $this->actualInventory->getItem($index);
$oldItem = $this->inventoryWindow->getInventory()->getItem($index);
if(!$newItem->equalsExact($oldItem)){
$result[] = new SlotChangeAction($this->actualInventory, $index, $oldItem, $newItem);
$result[] = new SlotChangeAction($this->inventoryWindow, $index, $oldItem, $newItem);
}
}
}

View File

@ -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);
}
/**

View File

@ -28,6 +28,7 @@ use pocketmine\inventory\SlotValidatedInventory;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\Item;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
/**
@ -35,7 +36,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
@ -44,10 +45,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;
}
/**
@ -63,21 +64,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);
}
@ -89,13 +91,13 @@ class SlotChangeAction extends InventoryAction{
* Adds this action's target inventory to the transaction's inventory list.
*/
public function onAddToTransaction(InventoryTransaction $transaction) : void{
$transaction->addInventory($this->inventory);
$transaction->addInventoryWindow($this->inventoryWindow);
}
/**
* 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);
}
}

View File

@ -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[]

View File

@ -23,17 +23,17 @@ 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\AnvilInventoryWindow;
use pocketmine\block\inventory\BlockInventoryWindow;
use pocketmine\block\inventory\BrewingStandInventoryWindow;
use pocketmine\block\inventory\CartographyTableInventoryWindow;
use pocketmine\block\inventory\CraftingTableInventoryWindow;
use pocketmine\block\inventory\EnchantingTableInventoryWindow;
use pocketmine\block\inventory\FurnaceInventoryWindow;
use pocketmine\block\inventory\HopperInventoryWindow;
use pocketmine\block\inventory\LoomInventoryWindow;
use pocketmine\block\inventory\SmithingTableInventoryWindow;
use pocketmine\block\inventory\StonecutterInventoryWindow;
use pocketmine\crafting\FurnaceType;
use pocketmine\data\bedrock\EnchantmentIdMap;
use pocketmine\inventory\Inventory;
@ -63,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;
@ -78,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 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;
@ -128,34 +130,48 @@ class InventoryManager implements InventoryListener{
$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->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;
}
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) . " is already tracked");
}
$this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry($inventory);
$inventory->getListeners()->add($this);
$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;
@ -165,18 +181,19 @@ class InventoryManager implements InventoryListener{
* @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
);
$inventory->getListeners()->add($this);
$window->getInventory()->getListeners()->add($this);
foreach($complexSlotMap->getSlotMap() as $netSlot => $coreSlot){
$this->complexSlotToInventoryMap[$netSlot] = $complexSlotMap;
$this->complexSlotToWindowMap[$netSlot] = $complexSlotMap;
}
}
@ -184,7 +201,7 @@ class InventoryManager implements InventoryListener{
* @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);
@ -192,49 +209,52 @@ class InventoryManager implements InventoryListener{
}
private function remove(int $id) : void{
$inventory = $this->networkIdToInventoryMap[$id];
unset($this->networkIdToInventoryMap[$id]);
if($this->getWindowId($inventory) === null){
$window = $this->networkIdToWindowMap[$id];
$inventory = $window->getInventory();
unset($this->networkIdToWindowMap[$id]);
if($this->getWindowId($window) === null){
$inventory->getListeners()->remove($this);
unset($this->inventories[spl_object_id($inventory)]);
foreach($this->complexSlotToInventoryMap as $netSlot => $entry){
if($entry->getInventory() === $inventory){
unset($this->complexSlotToInventoryMap[$netSlot]);
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 addPredictedSlotChange(Inventory $inventory, int $slot, ItemStack $item) : void{
$this->inventories[spl_object_id($inventory)]->predictions[$slot] = $item;
private function addPredictedSlotChange(InventoryWindow $window, int $slot, ItemStack $item) : void{
//TODO: does this need a null check?
$entry = $this->getEntry($window->getInventory()) ?? throw new AssumptionFailedError("Assume this should never be null");
$entry->predictions[$slot] = $item;
}
public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
@ -243,7 +263,7 @@ class InventoryManager implements InventoryListener{
if($action instanceof SlotChangeAction){
//TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
$itemStack = $typeConverter->coreItemStackToNet($action->getTargetItem());
$this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $itemStack);
$this->addPredictedSlotChange($action->getInventoryWindow(), $action->getSlot(), $itemStack);
}
}
}
@ -271,8 +291,8 @@ class InventoryManager implements InventoryListener{
continue;
}
[$inventory, $slot] = $info;
$this->addPredictedSlotChange($inventory, $slot, $action->newItem->getItemStack());
[$window, $slot] = $info;
$this->addPredictedSlotChange($window, $slot, $action->newItem->getItemStack());
}
}
@ -307,32 +327,32 @@ class InventoryManager implements InventoryListener{
* @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){
@ -343,7 +363,7 @@ class InventoryManager implements InventoryListener{
$this->session->sendDataPacket($pk);
}
$this->currentWindowType = $windowType ?? WindowTypes::CONTAINER;
$this->syncContents($inventory);
$this->syncContents($window);
return;
}
}
@ -358,27 +378,27 @@ class InventoryManager implements InventoryListener{
* @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)];
@ -391,7 +411,8 @@ class InventoryManager implements InventoryListener{
$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(
@ -403,25 +424,25 @@ class InventoryManager implements InventoryListener{
}
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
@ -474,12 +495,24 @@ class InventoryManager implements InventoryListener{
}
public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem) : void{
$inventoryEntry = $this->inventories[spl_object_id($inventory)] ?? null;
$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{
$inventory = $window->getInventory();
$inventoryEntry = $this->getEntry($inventory);
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));
$clientSideItem = $inventoryEntry->predictions[$slot] ?? null;
if($clientSideItem === null || !$this->itemStacksEqual($currentItem, $clientSideItem)){
@ -507,7 +540,7 @@ class InventoryManager implements InventoryListener{
$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())
));
@ -516,7 +549,7 @@ class InventoryManager implements InventoryListener{
$this->session->sendDataPacket(InventorySlotPacket::create(
$windowId,
$netSlot,
new FullContainerName($this->lastInventoryNetworkId),
new FullContainerName($this->lastWindowNetworkId),
new ItemStackWrapper(0, ItemStack::null()),
$itemStackWrapper
));
@ -537,18 +570,15 @@ class InventoryManager implements InventoryListener{
$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->getEntry($window->getInventory()) ?? throw new \LogicException("Cannot sync an untracked inventory");
$itemStackInfo = $entry->itemStackInfos[$slot];
if($itemStackInfo === null){
throw new \LogicException("Cannot sync an untracked inventory slot");
@ -557,7 +587,7 @@ class InventoryManager implements InventoryListener{
$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;
}
@ -576,11 +606,16 @@ class InventoryManager implements InventoryListener{
}
public function onContentChange(Inventory $inventory, array $oldContents) : void{
$this->syncContents($inventory);
//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);
}
}
public function syncContents(Inventory $inventory) : void{
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
private function syncContents(InventoryWindow $window) : void{
$entry = $this->getEntry($window->getInventory());
if($entry === null){
//this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
//is cleared before removal.
@ -589,14 +624,14 @@ class InventoryManager implements InventoryListener{
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);
@ -617,8 +652,8 @@ class InventoryManager implements InventoryListener{
}
public function syncAll() : void{
foreach($this->inventories as $entry){
$this->syncContents($entry->inventory);
foreach($this->entries as $entry){
$this->syncContents($entry->window);
}
}
@ -628,8 +663,8 @@ class InventoryManager implements InventoryListener{
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 ???
@ -647,14 +682,14 @@ class InventoryManager implements InventoryListener{
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);
@ -665,7 +700,13 @@ class InventoryManager implements InventoryListener{
}
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));
}
@ -679,10 +720,7 @@ class InventoryManager implements InventoryListener{
$playerInventory = $this->player->getInventory();
$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");
@ -741,8 +779,7 @@ class InventoryManager implements InventoryListener{
}
public function getItemStackInfo(Inventory $inventory, int $slot) : ?ItemStackInfo{
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
return $entry?->itemStackInfos[$slot] ?? null;
return $this->getEntry($inventory)?->itemStackInfos[$slot] ?? null;
}
private function trackItemStack(InventoryManagerEntry $entry, int $slotId, ItemStack $itemStack, ?int $itemStackRequestId) : ItemStackInfo{

View File

@ -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
){}
}

View File

@ -101,6 +101,7 @@ use pocketmine\network\mcpe\protocol\types\PlayerBlockActionStopBreak;
use pocketmine\network\mcpe\protocol\types\PlayerBlockActionWithBlockInfo;
use pocketmine\network\PacketHandlingException;
use pocketmine\player\Player;
use pocketmine\player\PlayerInventoryWindow;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Limits;
use pocketmine\utils\TextFormat;
@ -355,7 +356,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]);
}
}
}
@ -461,7 +462,8 @@ class InGamePacketHandler extends PacketHandler{
$droppedItem = $sourceSlotItem->pop($droppedCount);
$builder = new TransactionBuilder();
$builder->getInventory($inventory)->setItem($sourceSlot, $sourceSlotItem);
//TODO: this probably shouldn't be creating an ephemeral window here - it works, but no idea what side effects it might have on the permanent window
$builder->getActionBuilder(new PlayerInventoryWindow($this->player, $inventory, PlayerInventoryWindow::TYPE_INVENTORY))->setItem($sourceSlot, $sourceSlotItem);
$builder->addAction(new DropItemAction($droppedItem));
$transaction = new InventoryTransaction($this->player, $builder->generateActions());

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\EnchantingTableInventoryWindow;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\transaction\action\CreateItemAction;
use pocketmine\inventory\transaction\action\DestroyItemAction;
@ -31,8 +31,8 @@ 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\Item;
use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
@ -52,6 +52,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 function array_key_first;
@ -81,25 +82,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->getInventory(), $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")
);
@ -107,7 +105,7 @@ class ItemStackRequestExecutor{
}
/**
* @phpstan-return array{TransactionBuilderInventory, int}
* @phpstan-return array{SlotChangeActionBuilder, int}
*
* @throws ItemStackRequestProcessException
*/
@ -117,16 +115,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];
}
/**
@ -151,12 +150,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);
@ -174,12 +173,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
@ -336,7 +335,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);

View File

@ -59,7 +59,8 @@ final class ItemStackResponseBuilder{
if($windowAndSlot === null){
return null;
}
[$inventory, $slot] = $windowAndSlot;
[$window, $slot] = $windowAndSlot;
$inventory = $window->getInventory();
if(!$inventory->slotExists($slot)){
return null;
}

View File

@ -85,9 +85,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;
@ -218,11 +216,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;
@ -2318,7 +2316,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->loadQueue = [];
$this->removeCurrentWindow();
$this->removePermanentInventories();
$this->removePermanentWindows();
$this->perm->getPermissionRecalculationCallbacks()->clear();
@ -2334,8 +2332,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->blockBreakHandler = null;
parent::destroyCycles();
@ -2589,15 +2585,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;
}
@ -2628,22 +2628,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();
}
}
@ -2655,8 +2670,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);
@ -2667,18 +2682,18 @@ 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);
$ev = new InventoryOpenEvent($window, $this);
$ev->call();
if($ev->isCancelled()){
return false;
@ -2689,10 +2704,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 " . get_class($window) . "#" . spl_object_id($window));
$inventoryManager->onCurrentWindowChange($window);
$window->onOpen();
$this->currentWindow = $window;
return true;
}
@ -2701,7 +2716,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
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->currentWindow->onClose();
if(($inventoryManager = $this->getNetworkSession()->getInvManager()) !== null){
$inventoryManager->onCurrentWindowRemove();
}
@ -2710,20 +2725,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.
* TODO: add support for editing the rear side of the sign (not currently supported due to technical limitations)

View File

@ -21,8 +21,8 @@
declare(strict_types=1);
namespace pocketmine\inventory;
namespace pocketmine\player;
interface TemporaryInventory extends Inventory{
interface TemporaryInventoryWindow{
}