Separate inventory holder info from container & player inventories (#6533)

This PR breaks the cyclic dependency between `Inventory` and its holder, which unblocks a lot of new developments.

### Related issues & PRs
- Fixes #5033
- Removes a blocker for #6147 (which in turn means that async tasks will eventually be able to work with tiles)
- Removes a blocker for #2684

## Changes
### API changes
- `Player->getCurrentWindow()` now returns `?InventoryWindow` instead of `?Inventory`
- `Player->setCurrentWindow()` now accepts `?InventoryWindow` instead of `?Inventory`
- `InventoryWindow` introduced, which is created for each player viewing the inventory, provides decorative information like holder info for `InventoryTransactionEvent`, and is destroyed when the window is closed, eliminating cyclic references
- Added:
  - `player\InventoryWindow`
  - `player\PlayerInventoryWindow` - wraps all permanent inventories of Player with type info for transactions
  - `inventory\Hotbar` - replaces all hotbar usages in `PlayerInventory`
  - `Human->getHotbar()`
  - `Human->getMainHandItem()`, `Human->setMainHandItem()`, `Human->getOffHandItem()`, `Human->setOffHandItem()`
  - `block\utils\AnimatedContainerLike` & `block\utils\AnimatedContainerLikeTrait` (for chests, shulkerboxes, etc)
  - `block\utils\Container` & `block\utils\ContainerTrait` for blocks containing items (chests, etc)
  - `block\utils\MenuAccessor` implemented by all blocks that can open inventory menus
  - `block\utils\MenuAccessorTrait` used by blocks with menus but without inventories (anvils, crafting tables etc)
- Removed:
  - `inventory\DelegateInventory` (only used for ender chests)
  - `inventory\PlayerInventory`,
  - `inventory\PlayerOffHandInventory`,
  - `inventory\PlayerCraftingInventory`,
  - `inventory\PlayerCursorInventory` - these have all been internally replaced by `SimpleInventory` & they will appear as `PlayerInventoryWindow` in transactions (check `getType()` against the `PlayerInventoryWindow::TYPE_*` constants to identify them)
  - `block\inventory\AnimatedBlockInventoryTrait`, (blocks now handle this logic directly using `AnimatedContainer` and `AnimatedContainerTrait`)
  - `block\inventory\BlockInventoryTrait`,
  - `block\inventory\BlockInventory`
- Most `BlockInventory` classes have been transitioned to `InventoryWindow` wrappers
- Tiles now all use `SimpleInventory` internally (no cyclic references) except for `Chest` (which uses `CombinedInventory`, without holder info)
- `InventoryOpenEvent` and `InventoryCloseEvent` now provide `InventoryWindow` instead of `Inventory` (to provide type information)
- `InventoryTransaction` and `SlotChangeAction` now provide `InventoryWindow` instead of `Inventory`
- Renamed `TransactionBuilderInventory` to `SlotChangeActionBuilder`
- `TransactionBuilderInventory->getBuilder()` now accepts `InventoryWindow` instead of `Inventory`
- `DoubleChestInventory` superseded by `CombinedInventory` - this new class allows combining any number of inventories behind a single object; mainly used for double chests but plugins could use it to do lots of fun things

### Impacts to plugins
Plugins can now do the following:
```php
$block = $world->getBlockAt($x, $y, $z);
if($block instanceof MenuAccessor){
    $block->openToUnchecked($player);
}
```
As compared to the old way:
```php
$tile = $world->getTileAt($x, $y, $z);
if($tile instanceof Container){
    $player->setCurrentWindow($tile->getInventory());
}
```

#### Advantages
- No tile access needed
- Works for menu blocks without inventories as well as container blocks
- Less code

### Behavioural changes
Inventories no longer keep permanent cyclic references to their holders.

## Backwards compatibility
This makes significant BC breaks. However, all changes are able to be adapted to and the same amount of information is present on all APIs and events.

## Follow-up
- Implement #6147 
- Support inventory inheritance when copying blocks from one position to another
This commit is contained in:
Dylan T.
2025-09-02 19:23:16 +01:00
committed by GitHub
parent 702733bdde
commit 644f73aa84
91 changed files with 1985 additions and 1608 deletions

View File

@ -23,12 +23,14 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\inventory\window\AnvilInventoryWindow;
use pocketmine\block\utils\Fallable;
use pocketmine\block\utils\FallableTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\HorizontalFacingOption;
use pocketmine\block\utils\HorizontalFacingTrait;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\entity\object\FallingBlock;
@ -39,13 +41,15 @@ use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\utils\Utils;
use pocketmine\world\BlockTransaction;
use pocketmine\world\Position;
use pocketmine\world\sound\AnvilFallSound;
use pocketmine\world\sound\Sound;
use function round;
class Anvil extends Transparent implements Fallable, HorizontalFacing{
class Anvil extends Transparent implements Fallable, HorizontalFacing, MenuAccessor{
use FallableTrait;
use HorizontalFacingTrait;
use MenuAccessorTrait;
public const UNDAMAGED = 0;
public const SLIGHTLY_DAMAGED = 1;
@ -80,12 +84,8 @@ class Anvil extends Transparent implements Fallable, HorizontalFacing{
return SupportType::NONE;
}
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$player->setCurrentWindow(new AnvilInventory($this->position));
}
return true;
protected function newMenu(Player $player, Position $position) : AnvilInventoryWindow{
return new AnvilInventoryWindow($player, $position);
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, Facing $face, Vector3 $clickVector, ?Player $player = null) : bool{

View File

@ -23,19 +23,28 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\tile\Barrel as TileBarrel;
use pocketmine\block\utils\AnimatedContainerLike;
use pocketmine\block\utils\AnimatedContainerLikeTrait;
use pocketmine\block\utils\AnyFacing;
use pocketmine\block\utils\AnyFacingTrait;
use pocketmine\block\utils\Container;
use pocketmine\block\utils\ContainerTrait;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use pocketmine\world\Position;
use pocketmine\world\sound\BarrelCloseSound;
use pocketmine\world\sound\BarrelOpenSound;
use pocketmine\world\sound\Sound;
use function abs;
class Barrel extends Opaque implements AnyFacing{
class Barrel extends Opaque implements AnimatedContainerLike, AnyFacing, Container{
use AnimatedContainerLikeTrait;
use AnyFacingTrait;
use ContainerTrait;
protected bool $open = false;
@ -74,22 +83,23 @@ class Barrel extends Opaque implements AnyFacing{
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$barrel = $this->position->getWorld()->getTile($this->position);
if($barrel instanceof TileBarrel){
if(!$barrel->canOpenWith($item->getCustomName())){
return true;
}
$player->setCurrentWindow($barrel->getInventory());
}
}
return true;
}
public function getFuelTime() : int{
return 300;
}
protected function getOpenSound() : Sound{
return new BarrelOpenSound();
}
protected function getCloseSound() : Sound{
return new BarrelCloseSound();
}
protected function playAnimationVisual(Position $position, bool $isOpen) : void{
$world = $position->getWorld();
$block = $world->getBlock($position);
if($block instanceof Barrel){
$world->setBlock($position, $block->setOpen($isOpen));
}
}
}

View File

@ -23,20 +23,24 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\window\BrewingStandInventoryWindow;
use pocketmine\block\tile\BrewingStand as TileBrewingStand;
use pocketmine\block\utils\BrewingStandSlot;
use pocketmine\block\utils\Container;
use pocketmine\block\utils\ContainerTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\item\Item;
use pocketmine\inventory\Inventory;
use pocketmine\math\Axis;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\Position;
use function array_key_exists;
use function spl_object_id;
class BrewingStand extends Transparent{
class BrewingStand extends Transparent implements Container{
use ContainerTrait;
/**
* @var BrewingStandSlot[]
@ -95,15 +99,8 @@ class BrewingStand extends Transparent{
return $this;
}
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$stand = $this->position->getWorld()->getTile($this->position);
if($stand instanceof TileBrewingStand && $stand->canOpenWith($item->getCustomName())){
$player->setCurrentWindow($stand->getInventory());
}
}
return true;
protected function newMenu(Player $player, Inventory $inventory, Position $position) : BrewingStandInventoryWindow{
return new BrewingStandInventoryWindow($player, $inventory, $position);
}
public function onScheduledUpdate() : void{

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\CampfireInventory;
use pocketmine\block\tile\Campfire as TileCampfire;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\HorizontalFacingOption;
@ -41,6 +40,7 @@ use pocketmine\entity\projectile\SplashPotion;
use pocketmine\event\block\CampfireCookEvent;
use pocketmine\event\entity\EntityDamageByBlockEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\inventory\Inventory;
use pocketmine\item\Durable;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\item\Item;
@ -76,7 +76,7 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
* @deprecated This was added by mistake. It can't be relied on as the inventory won't be initialized if this block
* has never been set in the world.
*/
protected CampfireInventory $inventory;
protected ?Inventory $inventory = null;
/**
* @var int[] slot => ticks
@ -96,7 +96,8 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
$this->inventory = $tile->getInventory();
$this->cookingTimes = $tile->getCookingTimes();
}else{
$this->inventory = new CampfireInventory($this->position);
$this->inventory = null;
$this->cookingTimes = [];
}
return $this;
@ -140,7 +141,7 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
* @deprecated This was added by mistake. It can't be relied on as the inventory won't be initialized if this block
* has never been set in the world.
*/
public function getInventory() : CampfireInventory{
public function getInventory() : ?Inventory{
return $this->inventory;
}
@ -203,10 +204,11 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
return true;
}
if($this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($this->getFurnaceType())->match($item) !== null){
$inventory = $this->inventory;
if($inventory !== null && $this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($this->getFurnaceType())->match($item) !== null){
$ingredient = clone $item;
$ingredient->setCount(1);
if(count($this->inventory->addItem($ingredient)) === 0){
if(count($inventory->addItem($ingredient)) === 0){
$item->pop();
$this->position->getWorld()->addSound($this->position, new ItemFrameAddItemSound());
return true;
@ -241,8 +243,8 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
}
public function onScheduledUpdate() : void{
if($this->lit){
$items = $this->inventory->getContents();
if($this->lit && ($inventory = $this->inventory) !== null){
$items = $inventory->getContents();
$furnaceType = $this->getFurnaceType();
$maxCookDuration = $furnaceType->getCookDurationTicks();
foreach($items as $slot => $item){
@ -260,7 +262,7 @@ class Campfire extends Transparent implements Lightable, HorizontalFacing{
continue;
}
$this->inventory->setItem($slot, VanillaItems::AIR());
$inventory->setItem($slot, VanillaItems::AIR());
$this->setCookingTime($slot, 0);
$this->position->getWorld()->dropItem($this->position->add(0.5, 1, 0.5), $ev->getResult());
}

View File

@ -23,20 +23,17 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\CartographyTableInventory;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\block\inventory\window\CartographyTableInventoryWindow;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\player\Player;
use pocketmine\world\Position;
final class CartographyTable extends Opaque{
final class CartographyTable extends Opaque implements MenuAccessor{
use MenuAccessorTrait;
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player !== null){
$player->setCurrentWindow(new CartographyTableInventory($this->position));
}
return true;
protected function newMenu(Player $player, Position $position) : CartographyTableInventoryWindow{
return new CartographyTableInventoryWindow($player, $position);
}
public function getFuelTime() : int{

View File

@ -23,18 +23,32 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\window\BlockInventoryWindow;
use pocketmine\block\inventory\window\DoubleChestInventoryWindow;
use pocketmine\block\tile\Chest as TileChest;
use pocketmine\block\utils\AnimatedContainerLike;
use pocketmine\block\utils\AnimatedContainerLikeTrait;
use pocketmine\block\utils\Container;
use pocketmine\block\utils\ContainerTrait;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\SupportType;
use pocketmine\event\block\ChestPairEvent;
use pocketmine\item\Item;
use pocketmine\inventory\Inventory;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\BlockEventPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\world\Position;
use pocketmine\world\sound\ChestCloseSound;
use pocketmine\world\sound\ChestOpenSound;
use pocketmine\world\sound\Sound;
class Chest extends Transparent implements HorizontalFacing{
class Chest extends Transparent implements AnimatedContainerLike, Container, HorizontalFacing{
use AnimatedContainerLikeTrait;
use ContainerTrait;
use FacesOppositePlacingPlayerTrait;
protected function recalculateCollisionBoxes() : array{
@ -46,6 +60,25 @@ class Chest extends Transparent implements HorizontalFacing{
return SupportType::NONE;
}
/**
* @phpstan-return array{bool, TileChest}|null
*/
private function locatePair(Position $position) : ?array{
$world = $position->getWorld();
$tile = $world->getTile($position);
if($tile instanceof TileChest){
foreach([false, true] as $clockwise){
$side = Facing::rotateY($this->facing->toFacing(), $clockwise);
$c = $position->getSide($side);
$pair = $world->getTile($c);
if($pair instanceof TileChest && $pair->isPaired() && $pair->getPair() === $tile){
return [$clockwise, $pair];
}
}
}
return null;
}
public function onPostPlace() : void{
$world = $this->position->getWorld();
$tile = $world->getTile($this->position);
@ -70,27 +103,54 @@ class Chest extends Transparent implements HorizontalFacing{
}
}
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$chest = $this->position->getWorld()->getTile($this->position);
if($chest instanceof TileChest){
if(
!$this->getSide(Facing::UP)->isTransparent() ||
(($pair = $chest->getPair()) !== null && !$pair->getBlock()->getSide(Facing::UP)->isTransparent()) ||
!$chest->canOpenWith($item->getCustomName())
){
return true;
}
$player->setCurrentWindow($chest->getInventory());
}
public function isOpeningObstructed() : bool{
if(!$this->getSide(Facing::UP)->isTransparent()){
return true;
}
[, $pair] = $this->locatePair($this->position) ?? [false, null];
return $pair !== null && !$pair->getBlock()->getSide(Facing::UP)->isTransparent();
}
return true;
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
[$pairOnLeft, $pair] = $this->locatePair($position) ?? [false, null];
if($pair === null){
return new BlockInventoryWindow($player, $inventory, $position);
}
[$left, $right] = $pairOnLeft ? [$pair->getPosition(), $position] : [$position, $pair->getPosition()];
//TODO: we should probably construct DoubleChestInventory here directly too using the same logic
//right now it uses some weird logic in TileChest which produces incorrect results
//however I'm not sure if this is currently possible
return new DoubleChestInventoryWindow($player, $inventory, $left, $right);
}
public function getFuelTime() : int{
return 300;
}
protected function getOpenSound() : Sound{
return new ChestOpenSound();
}
protected function getCloseSound() : Sound{
return new ChestCloseSound();
}
protected function playAnimationVisual(Position $position, bool $isOpen) : void{
//event ID is always 1 for a chest
//TODO: we probably shouldn't be sending a packet directly here, but it doesn't fit anywhere into existing systems
$position->getWorld()->broadcastPacketToViewers($position, BlockEventPacket::create(BlockPosition::fromVector3($position), 1, $isOpen ? 1 : 0));
}
protected function doAnimationEffects(bool $isOpen) : void{
$this->playAnimationVisual($this->position, $isOpen);
$this->playAnimationSound($this->position, $isOpen);
$pairInfo = $this->locatePair($this->position);
if($pairInfo !== null){
[, $pair] = $pairInfo;
$this->playAnimationVisual($pair->getPosition(), $isOpen);
$this->playAnimationSound($pair->getPosition(), $isOpen);
}
}
}

View File

@ -23,20 +23,17 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\CraftingTableInventory;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\block\inventory\window\CraftingTableInventoryWindow;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\player\Player;
use pocketmine\world\Position;
class CraftingTable extends Opaque{
class CraftingTable extends Opaque implements MenuAccessor{
use MenuAccessorTrait;
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$player->setCurrentWindow(new CraftingTableInventory($this->position));
}
return true;
protected function newMenu(Player $player, Position $position) : CraftingTableInventoryWindow{
return new CraftingTableInventoryWindow($player, $position);
}
public function getFuelTime() : int{

View File

@ -23,15 +23,17 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\window\EnchantingTableInventoryWindow;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\item\Item;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\Position;
class EnchantingTable extends Transparent{
class EnchantingTable extends Transparent implements MenuAccessor{
use MenuAccessorTrait;
protected function recalculateCollisionBoxes() : array{
return [AxisAlignedBB::one()->trimmedCopy(Facing::UP, 0.25)];
@ -41,13 +43,7 @@ class EnchantingTable extends Transparent{
return SupportType::NONE;
}
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
//TODO lock
$player->setCurrentWindow(new EnchantInventory($this->position));
}
return true;
protected function newMenu(Player $player, Position $position) : EnchantingTableInventoryWindow{
return new EnchantingTableInventoryWindow($player, $position);
}
}

View File

@ -23,18 +23,31 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\EnderChestInventory;
use pocketmine\block\inventory\window\BlockInventoryWindow;
use pocketmine\block\tile\EnderChest as TileEnderChest;
use pocketmine\block\utils\AnimatedContainerLike;
use pocketmine\block\utils\AnimatedContainerLikeTrait;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\item\Item;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\BlockEventPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\player\Player;
use pocketmine\world\Position;
use pocketmine\world\sound\EnderChestCloseSound;
use pocketmine\world\sound\EnderChestOpenSound;
use pocketmine\world\sound\Sound;
class EnderChest extends Transparent implements HorizontalFacing{
class EnderChest extends Transparent implements AnimatedContainerLike, HorizontalFacing{
use AnimatedContainerLikeTrait {
onViewerAdded as private traitOnViewerAdded;
onViewerRemoved as private traitOnViewerRemoved;
}
use MenuAccessorTrait;
use FacesOppositePlacingPlayerTrait;
public function getLightLevel() : int{
@ -50,16 +63,12 @@ class EnderChest extends Transparent implements HorizontalFacing{
return SupportType::NONE;
}
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$enderChest = $this->position->getWorld()->getTile($this->position);
if($enderChest instanceof TileEnderChest && $this->getSide(Facing::UP)->isTransparent()){
$enderChest->setViewerCount($enderChest->getViewerCount() + 1);
$player->setCurrentWindow(new EnderChestInventory($this->position, $player->getEnderInventory()));
}
}
public function isOpeningObstructed() : bool{
return !$this->getSide(Facing::UP)->isTransparent();
}
return true;
protected function newMenu(Player $player, Position $position) : BlockInventoryWindow{
return new BlockInventoryWindow($player, $player->getEnderInventory(), $position);
}
public function getDropsForCompatibleTool(Item $item) : array{
@ -71,4 +80,43 @@ class EnderChest extends Transparent implements HorizontalFacing{
public function isAffectedBySilkTouch() : bool{
return true;
}
protected function getViewerCount() : int{
$enderChest = $this->position->getWorld()->getTile($this->position);
if(!$enderChest instanceof TileEnderChest){
return 0;
}
return $enderChest->getViewerCount();
}
private function updateViewerCount(int $amount) : void{
$enderChest = $this->position->getWorld()->getTile($this->position);
if($enderChest instanceof TileEnderChest){
$enderChest->setViewerCount($enderChest->getViewerCount() + $amount);
}
}
protected function getOpenSound() : Sound{
return new EnderChestOpenSound();
}
protected function getCloseSound() : Sound{
return new EnderChestCloseSound();
}
protected function playAnimationVisual(Position $position, bool $isOpen) : void{
//event ID is always 1 for a chest
//TODO: we probably shouldn't be sending a packet directly here, but it doesn't fit anywhere into existing systems
$position->getWorld()->broadcastPacketToViewers($position, BlockEventPacket::create(BlockPosition::fromVector3($position), 1, $isOpen ? 1 : 0));
}
public function onViewerAdded() : void{
$this->updateViewerCount(1);
$this->traitOnViewerAdded();
}
public function onViewerRemoved() : void{
$this->traitOnViewerRemoved();
$this->updateViewerCount(-1);
}
}

View File

@ -23,20 +23,24 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\window\FurnaceInventoryWindow;
use pocketmine\block\tile\Furnace as TileFurnace;
use pocketmine\block\utils\Container;
use pocketmine\block\utils\ContainerTrait;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\Lightable;
use pocketmine\block\utils\LightableTrait;
use pocketmine\crafting\FurnaceType;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\inventory\Inventory;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\world\Position;
use function mt_rand;
class Furnace extends Opaque implements Lightable, HorizontalFacing{
class Furnace extends Opaque implements Container, Lightable, HorizontalFacing{
use ContainerTrait;
use FacesOppositePlacingPlayerTrait;
use LightableTrait;
@ -60,15 +64,8 @@ class Furnace extends Opaque implements Lightable, HorizontalFacing{
return $this->lit ? 13 : 0;
}
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
$furnace = $this->position->getWorld()->getTile($this->position);
if($furnace instanceof TileFurnace && $furnace->canOpenWith($item->getCustomName())){
$player->setCurrentWindow($furnace->getInventory());
}
}
return true;
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
return new FurnaceInventoryWindow($player, $inventory, $position, $this->furnaceType);
}
public function onScheduledUpdate() : void{

View File

@ -23,19 +23,25 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\tile\Hopper as TileHopper;
use pocketmine\block\inventory\window\HopperInventoryWindow;
use pocketmine\block\utils\Container;
use pocketmine\block\utils\ContainerTrait;
use pocketmine\block\utils\PoweredByRedstone;
use pocketmine\block\utils\PoweredByRedstoneTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\inventory\Inventory;
use pocketmine\item\Item;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use pocketmine\world\Position;
class Hopper extends Transparent implements PoweredByRedstone{
class Hopper extends Transparent implements Container, PoweredByRedstone{
use ContainerTrait;
use PoweredByRedstoneTrait;
private Facing $facing = Facing::DOWN;
@ -81,15 +87,8 @@ class Hopper extends Transparent implements PoweredByRedstone{
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player !== null){
$tile = $this->position->getWorld()->getTile($this->position);
if($tile instanceof TileHopper){ //TODO: find a way to have inventories open on click without this boilerplate in every block
$player->setCurrentWindow($tile->getInventory());
}
return true;
}
return false;
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
return new HopperInventoryWindow($player, $inventory, $position);
}
public function onScheduledUpdate() : void{

View File

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

View File

@ -23,19 +23,34 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\window\BlockInventoryWindow;
use pocketmine\block\tile\ShulkerBox as TileShulkerBox;
use pocketmine\block\utils\AnimatedContainerLike;
use pocketmine\block\utils\AnimatedContainerLikeTrait;
use pocketmine\block\utils\AnyFacing;
use pocketmine\block\utils\AnyFacingTrait;
use pocketmine\block\utils\Container;
use pocketmine\block\utils\ContainerTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\inventory\Inventory;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\BlockEventPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use pocketmine\world\Position;
use pocketmine\world\sound\ShulkerBoxCloseSound;
use pocketmine\world\sound\ShulkerBoxOpenSound;
use pocketmine\world\sound\Sound;
class ShulkerBox extends Opaque implements AnyFacing{
class ShulkerBox extends Opaque implements AnimatedContainerLike, AnyFacing, Container{
use AnimatedContainerLikeTrait;
use AnyFacingTrait;
use ContainerTrait;
protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
//NOOP - we don't read or write facing here, because the tile persists it
@ -95,26 +110,29 @@ class ShulkerBox extends Opaque implements AnyFacing{
return $result;
}
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player){
public function isOpeningObstructed() : bool{
return $this->getSide($this->facing)->isSolid();
}
$shulker = $this->position->getWorld()->getTile($this->position);
if($shulker instanceof TileShulkerBox){
if(
$this->getSide($this->facing)->isSolid() ||
!$shulker->canOpenWith($item->getCustomName())
){
return true;
}
$player->setCurrentWindow($shulker->getInventory());
}
}
return true;
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
return new BlockInventoryWindow($player, $inventory, $position);
}
public function getSupportType(Facing $facing) : SupportType{
return SupportType::NONE;
}
protected function getOpenSound() : Sound{
return new ShulkerBoxOpenSound();
}
protected function getCloseSound() : Sound{
return new ShulkerBoxCloseSound();
}
protected function playAnimationVisual(Position $position, bool $isOpen) : void{
//event ID is always 1 for a chest
//TODO: we probably shouldn't be sending a packet directly here, but it doesn't fit anywhere into existing systems
$position->getWorld()->broadcastPacketToViewers($position, BlockEventPacket::create(BlockPosition::fromVector3($position), 1, $isOpen ? 1 : 0));
}
}

View File

@ -23,20 +23,18 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\SmithingTableInventory;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\block\inventory\window\SmithingTableInventoryWindow;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\world\Position;
final class SmithingTable extends Opaque{
final class SmithingTable extends Opaque implements MenuAccessor{
use MenuAccessorTrait;
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player !== null){
$player->setCurrentWindow(new SmithingTableInventory($this->position));
}
return true;
protected function newMenu(Player $player, Position $position) : InventoryWindow{
return new SmithingTableInventoryWindow($player, $position);
}
public function getFuelTime() : int{

View File

@ -23,24 +23,23 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\StonecutterInventory;
use pocketmine\block\inventory\window\StonecutterInventoryWindow;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\item\Item;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\Position;
class Stonecutter extends Transparent implements HorizontalFacing{
class Stonecutter extends Transparent implements HorizontalFacing, MenuAccessor{
use FacesOppositePlacingPlayerTrait;
use MenuAccessorTrait;
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player !== null){
$player->setCurrentWindow(new StonecutterInventory($this->position));
}
return true;
protected function newMenu(Player $player, Position $position) : StonecutterInventoryWindow{
return new StonecutterInventoryWindow($player, $position);
}
protected function recalculateCollisionBoxes() : array{

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

@ -1,57 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\block\Barrel;
use pocketmine\inventory\SimpleInventory;
use pocketmine\world\Position;
use pocketmine\world\sound\BarrelCloseSound;
use pocketmine\world\sound\BarrelOpenSound;
use pocketmine\world\sound\Sound;
class BarrelInventory extends SimpleInventory implements BlockInventory{
use AnimatedBlockInventoryTrait;
public function __construct(Position $holder){
$this->holder = $holder;
parent::__construct(27);
}
protected function getOpenSound() : Sound{
return new BarrelOpenSound();
}
protected function getCloseSound() : Sound{
return new BarrelCloseSound();
}
protected function animateBlock(bool $isOpen) : void{
$holder = $this->getHolder();
$world = $holder->getWorld();
$block = $world->getBlock($holder);
if($block instanceof Barrel){
$world->setBlock($holder, $block->setOpen($isOpen));
}
}
}

View File

@ -1,34 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\world\Position;
trait BlockInventoryTrait{
protected Position $holder;
public function getHolder() : Position{
return $this->holder;
}
}

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 CartographyTableInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
public function __construct(Position $holder){
$this->holder = $holder;
parent::__construct(2);
}
}

View File

@ -1,56 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\network\mcpe\protocol\BlockEventPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\world\Position;
use pocketmine\world\sound\ChestCloseSound;
use pocketmine\world\sound\ChestOpenSound;
use pocketmine\world\sound\Sound;
class ChestInventory extends SimpleInventory implements BlockInventory{
use AnimatedBlockInventoryTrait;
public function __construct(Position $holder){
$this->holder = $holder;
parent::__construct(27);
}
protected function getOpenSound() : Sound{
return new ChestOpenSound();
}
protected function getCloseSound() : Sound{
return new ChestCloseSound();
}
public function animateBlock(bool $isOpen) : void{
$holder = $this->getHolder();
//event ID is always 1 for a chest
$holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
}
}

View File

@ -1,118 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\BaseInventory;
use pocketmine\inventory\InventoryHolder;
use pocketmine\item\Item;
use pocketmine\world\sound\ChestCloseSound;
use pocketmine\world\sound\ChestOpenSound;
use pocketmine\world\sound\Sound;
class DoubleChestInventory extends BaseInventory implements BlockInventory, InventoryHolder{
use AnimatedBlockInventoryTrait;
public function __construct(
private ChestInventory $left,
private ChestInventory $right
){
$this->holder = $this->left->getHolder();
parent::__construct();
}
public function getInventory() : self{
return $this;
}
public function getSize() : int{
return $this->left->getSize() + $this->right->getSize();
}
public function getItem(int $index) : Item{
return $index < $this->left->getSize() ? $this->left->getItem($index) : $this->right->getItem($index - $this->left->getSize());
}
protected function internalSetItem(int $index, Item $item) : void{
$index < $this->left->getSize() ? $this->left->setItem($index, $item) : $this->right->setItem($index - $this->left->getSize(), $item);
}
public function getContents(bool $includeEmpty = false) : array{
$result = $this->left->getContents($includeEmpty);
$leftSize = $this->left->getSize();
foreach($this->right->getContents($includeEmpty) as $i => $item){
$result[$i + $leftSize] = $item;
}
return $result;
}
protected function internalSetContents(array $items) : void{
$leftSize = $this->left->getSize();
$leftContents = [];
$rightContents = [];
foreach($items as $i => $item){
if($i < $this->left->getSize()){
$leftContents[$i] = $item;
}else{
$rightContents[$i - $leftSize] = $item;
}
}
$this->left->setContents($leftContents);
$this->right->setContents($rightContents);
}
protected function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
$leftSize = $this->left->getSize();
return $slot < $leftSize ?
$this->left->getMatchingItemCount($slot, $test, $checkTags) :
$this->right->getMatchingItemCount($slot - $leftSize, $test, $checkTags);
}
public function isSlotEmpty(int $index) : bool{
$leftSize = $this->left->getSize();
return $index < $leftSize ?
$this->left->isSlotEmpty($index) :
$this->right->isSlotEmpty($index - $leftSize);
}
protected function getOpenSound() : Sound{ return new ChestOpenSound(); }
protected function getCloseSound() : Sound{ return new ChestCloseSound(); }
protected function animateBlock(bool $isOpen) : void{
$this->left->animateBlock($isOpen);
$this->right->animateBlock($isOpen);
}
public function getLeftSide() : ChestInventory{
return $this->left;
}
public function getRightSide() : ChestInventory{
return $this->right;
}
}

View File

@ -1,88 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\event\player\PlayerEnchantingOptionsRequestEvent;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\item\enchantment\EnchantingHelper as Helper;
use pocketmine\item\enchantment\EnchantingOption;
use pocketmine\item\Item;
use pocketmine\world\Position;
use function array_values;
use function count;
class EnchantInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
public const SLOT_INPUT = 0;
public const SLOT_LAPIS = 1;
/**
* @var EnchantingOption[] $options
* @phpstan-var list<EnchantingOption>
*/
private array $options = [];
public function __construct(Position $holder){
$this->holder = $holder;
parent::__construct(2);
}
protected function onSlotChange(int $index, Item $before) : void{
if($index === self::SLOT_INPUT){
foreach($this->viewers as $viewer){
$this->options = [];
$item = $this->getInput();
$options = Helper::generateOptions($this->holder, $item, $viewer->getEnchantmentSeed());
$event = new PlayerEnchantingOptionsRequestEvent($viewer, $this, $options);
$event->call();
if(!$event->isCancelled() && count($event->getOptions()) > 0){
$this->options = array_values($event->getOptions());
$viewer->getNetworkSession()->getInvManager()?->syncEnchantingTableOptions($this->options);
}
}
}
parent::onSlotChange($index, $before);
}
public function getInput() : Item{
return $this->getItem(self::SLOT_INPUT);
}
public function getLapis() : Item{
return $this->getItem(self::SLOT_LAPIS);
}
public function getOutput(int $optionId) : ?Item{
$option = $this->getOption($optionId);
return $option === null ? null : Helper::enchantItem($this->getInput(), $option->getEnchantments());
}
public function getOption(int $optionId) : ?EnchantingOption{
return $this->options[$optionId] ?? null;
}
}

View File

@ -1,88 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\block\tile\EnderChest;
use pocketmine\inventory\DelegateInventory;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\PlayerEnderInventory;
use pocketmine\network\mcpe\protocol\BlockEventPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\player\Player;
use pocketmine\world\Position;
use pocketmine\world\sound\EnderChestCloseSound;
use pocketmine\world\sound\EnderChestOpenSound;
use pocketmine\world\sound\Sound;
/**
* EnderChestInventory is not a real inventory; it's just a gateway to the player's ender inventory.
*/
class EnderChestInventory extends DelegateInventory implements BlockInventory{
use AnimatedBlockInventoryTrait {
onClose as animatedBlockInventoryTrait_onClose;
}
public function __construct(
Position $holder,
private PlayerEnderInventory $inventory
){
parent::__construct($inventory);
$this->holder = $holder;
}
public function getEnderInventory() : PlayerEnderInventory{
return $this->inventory;
}
public function getViewerCount() : int{
$enderChest = $this->getHolder()->getWorld()->getTile($this->getHolder());
if(!$enderChest instanceof EnderChest){
return 0;
}
return $enderChest->getViewerCount();
}
protected function getOpenSound() : Sound{
return new EnderChestOpenSound();
}
protected function getCloseSound() : Sound{
return new EnderChestCloseSound();
}
protected function animateBlock(bool $isOpen) : void{
$holder = $this->getHolder();
//event ID is always 1 for a chest
$holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
}
public function onClose(Player $who) : void{
$this->animatedBlockInventoryTrait_onClose($who);
$enderChest = $this->getHolder()->getWorld()->getTile($this->getHolder());
if($enderChest instanceof EnderChest){
$enderChest->setViewerCount($enderChest->getViewerCount() - 1);
}
}
}

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\block\BlockTypeIds;
use pocketmine\inventory\SimpleInventory;
use pocketmine\item\Item;
use pocketmine\item\ItemTypeIds;
use pocketmine\network\mcpe\protocol\BlockEventPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\world\Position;
use pocketmine\world\sound\ShulkerBoxCloseSound;
use pocketmine\world\sound\ShulkerBoxOpenSound;
use pocketmine\world\sound\Sound;
class ShulkerBoxInventory extends SimpleInventory implements BlockInventory{
use AnimatedBlockInventoryTrait;
public function __construct(Position $holder){
$this->holder = $holder;
parent::__construct(27);
}
protected function getOpenSound() : Sound{
return new ShulkerBoxOpenSound();
}
protected function getCloseSound() : Sound{
return new ShulkerBoxCloseSound();
}
public function canAddItem(Item $item) : bool{
$blockTypeId = ItemTypeIds::toBlockTypeId($item->getTypeId());
if($blockTypeId === BlockTypeIds::SHULKER_BOX || $blockTypeId === BlockTypeIds::DYED_SHULKER_BOX){
return false;
}
return parent::canAddItem($item);
}
protected function animateBlock(bool $isOpen) : void{
$holder = $this->getHolder();
//event ID is always 1 for a chest
$holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0));
}
}

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

@ -21,20 +21,21 @@
declare(strict_types=1);
namespace pocketmine\block\inventory;
namespace pocketmine\block\inventory\window;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\player\Player;
use pocketmine\player\TemporaryInventoryWindow;
use pocketmine\world\Position;
class AnvilInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
final class AnvilInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
public const SLOT_INPUT = 0;
public const SLOT_MATERIAL = 1;
public function __construct(Position $holder){
$this->holder = $holder;
parent::__construct(2);
public function __construct(
Player $viewer,
Position $holder
){
parent::__construct($viewer, new SimpleInventory(2), $holder);
}
}

View File

@ -0,0 +1,59 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\inventory\window;
use pocketmine\block\utils\AnimatedContainerLike;
use pocketmine\inventory\Inventory;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\world\Position;
class BlockInventoryWindow extends InventoryWindow{
public function __construct(
Player $viewer,
Inventory $inventory,
protected Position $holder
){
parent::__construct($viewer, $inventory);
}
public function getHolder() : Position{ return $this->holder; }
public function onOpen() : void{
parent::onOpen();
$block = $this->holder->getWorld()->getBlock($this->holder);
if($block instanceof AnimatedContainerLike){
$block->onViewerAdded();
}
}
public function onClose() : void{
$block = $this->holder->getWorld()->getBlock($this->holder);
if($block instanceof AnimatedContainerLike){
$block->onViewerRemoved();
}
parent::onClose();
}
}

View File

@ -21,22 +21,12 @@
declare(strict_types=1);
namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\world\Position;
class BrewingStandInventory extends SimpleInventory implements BlockInventory{
use BlockInventoryTrait;
namespace pocketmine\block\inventory\window;
final class BrewingStandInventoryWindow extends BlockInventoryWindow{
public const SLOT_INGREDIENT = 0;
public const SLOT_BOTTLE_LEFT = 1;
public const SLOT_BOTTLE_MIDDLE = 2;
public const SLOT_BOTTLE_RIGHT = 3;
public const SLOT_FUEL = 4;
public function __construct(Position $holder, int $size = 5){
$this->holder = $holder;
parent::__construct($size);
}
}

View File

@ -21,20 +21,19 @@
declare(strict_types=1);
namespace pocketmine\block\inventory;
namespace pocketmine\block\inventory\window;
use pocketmine\inventory\SimpleInventory;
use pocketmine\player\Player;
use pocketmine\player\TemporaryInventoryWindow;
use pocketmine\world\Position;
class CampfireInventory extends SimpleInventory implements BlockInventory{
use BlockInventoryTrait;
final class CartographyTableInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
public function __construct(Position $holder){
$this->holder = $holder;
parent::__construct(4);
}
public function getMaxStackSize() : int{
return 1;
public function __construct(
Player $viewer,
Position $holder
){
parent::__construct($viewer, new SimpleInventory(2), $holder);
}
}

View File

@ -21,17 +21,19 @@
declare(strict_types=1);
namespace pocketmine\block\inventory;
namespace pocketmine\block\inventory\window;
use pocketmine\crafting\CraftingGrid;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\player\Player;
use pocketmine\world\Position;
final class CraftingTableInventory extends CraftingGrid implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
final class CraftingTableInventoryWindow extends BlockInventoryWindow{
public function __construct(Position $holder){
$this->holder = $holder;
parent::__construct(CraftingGrid::SIZE_BIG);
public function __construct(
Player $viewer,
Position $holder
){
//TODO: generics would be good for this, since it has special methods
parent::__construct($viewer, new CraftingGrid(CraftingGrid::SIZE_BIG), $holder);
}
}

View File

@ -21,18 +21,24 @@
declare(strict_types=1);
namespace pocketmine\inventory;
namespace pocketmine\block\inventory\window;
use pocketmine\inventory\Inventory;
use pocketmine\player\Player;
use pocketmine\world\Position;
final class DoubleChestInventoryWindow extends BlockInventoryWindow{
class PlayerCursorInventory extends SimpleInventory implements TemporaryInventory{
public function __construct(
protected Player $holder
Player $viewer,
Inventory $inventory,
private Position $left,
private Position $right
){
parent::__construct(1);
parent::__construct($viewer, $inventory, $this->left);
}
public function getHolder() : Player{
return $this->holder;
}
public function getLeft() : Position{ return $this->left; }
public function getRight() : Position{ return $this->right; }
}

View File

@ -0,0 +1,104 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\inventory\window;
use pocketmine\event\player\PlayerEnchantingOptionsRequestEvent;
use pocketmine\inventory\CallbackInventoryListener;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\InventoryListener;
use pocketmine\inventory\SimpleInventory;
use pocketmine\item\enchantment\EnchantingHelper as Helper;
use pocketmine\item\enchantment\EnchantingOption;
use pocketmine\item\Item;
use pocketmine\player\Player;
use pocketmine\world\Position;
use function array_values;
use function count;
final class EnchantingTableInventoryWindow extends BlockInventoryWindow{
public const SLOT_INPUT = 0;
public const SLOT_LAPIS = 1;
/** @var EnchantingOption[] $options */
private array $options = [];
private InventoryListener $listener;
public function __construct(
Player $viewer,
Position $holder
){
parent::__construct($viewer, new SimpleInventory(2), $holder);
/** @phpstan-var \WeakReference<$this> $weakThis */
$weakThis = \WeakReference::create($this);
$this->listener = new CallbackInventoryListener(
onSlotChange: static function(Inventory $_, int $slot) use ($weakThis) : void{ //remaining params unneeded
if($slot === self::SLOT_INPUT && ($strongThis = $weakThis->get()) !== null){
$strongThis->regenerateOptions();
}
},
onContentChange: static function() use ($weakThis) : void{
if(($strongThis = $weakThis->get()) !== null){
$strongThis->regenerateOptions();
}
}
);
$this->inventory->getListeners()->add($this->listener);
}
public function __destruct(){
$this->inventory->getListeners()->remove($this->listener);
}
private function regenerateOptions() : void{
$this->options = [];
$item = $this->getInput();
$options = Helper::generateOptions($this->holder, $item, $this->viewer->getEnchantmentSeed());
$event = new PlayerEnchantingOptionsRequestEvent($this->viewer, $this, $options);
$event->call();
if(!$event->isCancelled() && count($event->getOptions()) > 0){
$this->options = array_values($event->getOptions());
$this->viewer->getNetworkSession()->getInvManager()?->syncEnchantingTableOptions($this->options);
}
}
public function getInput() : Item{
return $this->inventory->getItem(self::SLOT_INPUT);
}
public function getLapis() : Item{
return $this->inventory->getItem(self::SLOT_LAPIS);
}
public function getOutput(int $optionId) : ?Item{
$option = $this->getOption($optionId);
return $option === null ? null : Helper::enchantItem($this->getInput(), $option->getEnchantments());
}
public function getOption(int $optionId) : ?EnchantingOption{
return $this->options[$optionId] ?? null;
}
}

View File

@ -21,51 +21,51 @@
declare(strict_types=1);
namespace pocketmine\block\inventory;
namespace pocketmine\block\inventory\window;
use pocketmine\crafting\FurnaceType;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\Inventory;
use pocketmine\item\Item;
use pocketmine\player\Player;
use pocketmine\world\Position;
class FurnaceInventory extends SimpleInventory implements BlockInventory{
use BlockInventoryTrait;
final class FurnaceInventoryWindow extends BlockInventoryWindow{
public const SLOT_INPUT = 0;
public const SLOT_FUEL = 1;
public const SLOT_RESULT = 2;
public function __construct(
Player $viewer,
Inventory $inventory,
Position $holder,
private FurnaceType $furnaceType
){
$this->holder = $holder;
parent::__construct(3);
parent::__construct($viewer, $inventory, $holder);
}
public function getFurnaceType() : FurnaceType{ return $this->furnaceType; }
public function getResult() : Item{
return $this->getItem(self::SLOT_RESULT);
return $this->inventory->getItem(self::SLOT_RESULT);
}
public function getFuel() : Item{
return $this->getItem(self::SLOT_FUEL);
return $this->inventory->getItem(self::SLOT_FUEL);
}
public function getSmelting() : Item{
return $this->getItem(self::SLOT_INPUT);
return $this->inventory->getItem(self::SLOT_INPUT);
}
public function setResult(Item $item) : void{
$this->setItem(self::SLOT_RESULT, $item);
$this->inventory->setItem(self::SLOT_RESULT, $item);
}
public function setFuel(Item $item) : void{
$this->setItem(self::SLOT_FUEL, $item);
$this->inventory->setItem(self::SLOT_FUEL, $item);
}
public function setSmelting(Item $item) : void{
$this->setItem(self::SLOT_INPUT, $item);
$this->inventory->setItem(self::SLOT_INPUT, $item);
}
}

View File

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

View File

@ -21,21 +21,23 @@
declare(strict_types=1);
namespace pocketmine\block\inventory;
namespace pocketmine\block\inventory\window;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\player\Player;
use pocketmine\player\TemporaryInventoryWindow;
use pocketmine\world\Position;
final class LoomInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
final class LoomInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
public const SLOT_BANNER = 0;
public const SLOT_DYE = 1;
public const SLOT_PATTERN = 2;
public function __construct(Position $holder, int $size = 3){
$this->holder = $holder;
parent::__construct($size);
public function __construct(
Player $viewer,
Position $holder
){
parent::__construct($viewer, new SimpleInventory(3), $holder);
}
}

View File

@ -21,16 +21,15 @@
declare(strict_types=1);
namespace pocketmine\block\inventory;
namespace pocketmine\block\inventory\window;
use pocketmine\inventory\SimpleInventory;
use pocketmine\player\Player;
use pocketmine\player\TemporaryInventoryWindow;
use pocketmine\world\Position;
class HopperInventory extends SimpleInventory implements BlockInventory{
use BlockInventoryTrait;
public function __construct(Position $holder, int $size = 5){
$this->holder = $holder;
parent::__construct($size);
final class SmithingTableInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
public function __construct(Player $viewer, Position $holder){
parent::__construct($viewer, new SimpleInventory(3), $holder);
}
}

View File

@ -21,19 +21,17 @@
declare(strict_types=1);
namespace pocketmine\block\inventory;
namespace pocketmine\block\inventory\window;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\player\Player;
use pocketmine\player\TemporaryInventoryWindow;
use pocketmine\world\Position;
class StonecutterInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
use BlockInventoryTrait;
final class StonecutterInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{
public const SLOT_INPUT = 0;
public function __construct(Position $holder){
$this->holder = $holder;
parent::__construct(1);
public function __construct(Player $viewer, Position $holder){
parent::__construct($viewer, new SimpleInventory(1), $holder);
}
}

View File

@ -23,20 +23,21 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
use pocketmine\block\inventory\BarrelInventory;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\World;
class Barrel extends Spawnable implements Container, Nameable{
class Barrel extends Spawnable implements ContainerTile, Nameable{
use NameableTrait;
use ContainerTrait;
use ContainerTileTrait;
protected BarrelInventory $inventory;
protected Inventory $inventory;
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
$this->inventory = new BarrelInventory($this->position);
$this->inventory = new SimpleInventory(27);
}
public function readSaveData(CompoundTag $nbt) : void{
@ -56,11 +57,11 @@ class Barrel extends Spawnable implements Container, Nameable{
}
}
public function getInventory() : BarrelInventory{
public function getInventory() : Inventory{
return $this->inventory;
}
public function getRealInventory() : BarrelInventory{
public function getRealInventory() : Inventory{
return $this->inventory;
}

View File

@ -23,12 +23,13 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
use pocketmine\block\inventory\BrewingStandInventory;
use pocketmine\block\inventory\window\BrewingStandInventoryWindow;
use pocketmine\crafting\BrewingRecipe;
use pocketmine\event\block\BrewingFuelUseEvent;
use pocketmine\event\block\BrewItemEvent;
use pocketmine\inventory\CallbackInventoryListener;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\math\Vector3;
@ -40,11 +41,11 @@ use pocketmine\world\World;
use function array_map;
use function count;
class BrewingStand extends Spawnable implements Container, Nameable{
class BrewingStand extends Spawnable implements ContainerTile, Nameable{
use NameableTrait {
addAdditionalSpawnData as addNameSpawnData;
}
use ContainerTrait;
use ContainerTileTrait;
public const BREW_TIME_TICKS = 400; // Brew time in ticks
@ -54,7 +55,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{
private const TAG_REMAINING_FUEL_TIME = "Fuel"; //TAG_Byte
private const TAG_REMAINING_FUEL_TIME_PE = "FuelAmount"; //TAG_Short
private BrewingStandInventory $inventory;
private Inventory $inventory;
private int $brewTime = 0;
private int $maxFuelTime = 0;
@ -62,7 +63,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
$this->inventory = new BrewingStandInventory($this->position);
$this->inventory = new SimpleInventory(5);
$this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange(static function(Inventory $unused) use ($world, $pos) : void{
$world->scheduleDelayedBlockUpdate($pos, 1);
}));
@ -112,11 +113,11 @@ class BrewingStand extends Spawnable implements Container, Nameable{
}
}
public function getInventory() : BrewingStandInventory{
public function getInventory() : Inventory{
return $this->inventory;
}
public function getRealInventory() : BrewingStandInventory{
public function getRealInventory() : Inventory{
return $this->inventory;
}
@ -132,7 +133,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{
}
$item->pop();
$this->inventory->setItem(BrewingStandInventory::SLOT_FUEL, $item);
$this->inventory->setItem(BrewingStandInventoryWindow::SLOT_FUEL, $item);
$this->maxFuelTime = $this->remainingFuelTime = $ev->getFuelTime();
}
@ -142,14 +143,14 @@ class BrewingStand extends Spawnable implements Container, Nameable{
* @phpstan-return array<int, BrewingRecipe>
*/
private function getBrewableRecipes() : array{
$ingredient = $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT);
$ingredient = $this->inventory->getItem(BrewingStandInventoryWindow::SLOT_INGREDIENT);
if($ingredient->isNull()){
return [];
}
$recipes = [];
$craftingManager = $this->position->getWorld()->getServer()->getCraftingManager();
foreach([BrewingStandInventory::SLOT_BOTTLE_LEFT, BrewingStandInventory::SLOT_BOTTLE_MIDDLE, BrewingStandInventory::SLOT_BOTTLE_RIGHT] as $slot){
foreach([BrewingStandInventoryWindow::SLOT_BOTTLE_LEFT, BrewingStandInventoryWindow::SLOT_BOTTLE_MIDDLE, BrewingStandInventoryWindow::SLOT_BOTTLE_RIGHT] as $slot){
$input = $this->inventory->getItem($slot);
if($input->isNull()){
continue;
@ -176,8 +177,8 @@ class BrewingStand extends Spawnable implements Container, Nameable{
$ret = false;
$fuel = $this->inventory->getItem(BrewingStandInventory::SLOT_FUEL);
$ingredient = $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT);
$fuel = $this->inventory->getItem(BrewingStandInventoryWindow::SLOT_FUEL);
$ingredient = $this->inventory->getItem(BrewingStandInventoryWindow::SLOT_INGREDIENT);
$recipes = $this->getBrewableRecipes();
$canBrew = count($recipes) !== 0;
@ -219,7 +220,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{
}
$ingredient->pop();
$this->inventory->setItem(BrewingStandInventory::SLOT_INGREDIENT, $ingredient);
$this->inventory->setItem(BrewingStandInventoryWindow::SLOT_INGREDIENT, $ingredient);
$this->brewTime = 0;
}else{

View File

@ -24,9 +24,9 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
use pocketmine\block\Campfire as BlockCampfire;
use pocketmine\block\inventory\CampfireInventory;
use pocketmine\inventory\CallbackInventoryListener;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
@ -34,8 +34,8 @@ use pocketmine\nbt\tag\IntTag;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\world\World;
class Campfire extends Spawnable implements Container{
use ContainerTrait;
class Campfire extends Spawnable implements ContainerTile{
use ContainerTileTrait;
private const TAG_FIRST_INPUT_ITEM = "Item1"; //TAG_Compound
private const TAG_SECOND_INPUT_ITEM = "Item2"; //TAG_Compound
@ -47,13 +47,14 @@ class Campfire extends Spawnable implements Container{
private const TAG_THIRD_COOKING_TIME = "ItemTime3"; //TAG_Int
private const TAG_FOURTH_COOKING_TIME = "ItemTime4"; //TAG_Int
protected CampfireInventory $inventory;
protected Inventory $inventory;
/** @var array<int, int> */
private array $cookingTimes = [];
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
$this->inventory = new CampfireInventory($this->position);
$this->inventory = new SimpleInventory(4);
$this->inventory->setMaxStackSize(1);
$this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange(
static function(Inventory $unused) use ($world, $pos) : void{
$block = $world->getBlock($pos);
@ -64,11 +65,11 @@ class Campfire extends Spawnable implements Container{
);
}
public function getInventory() : CampfireInventory{
public function getInventory() : Inventory{
return $this->inventory;
}
public function getRealInventory() : CampfireInventory{
public function getRealInventory() : Inventory{
return $this->inventory;
}

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\CombinedInventoryProxy;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntTag;
@ -32,11 +33,11 @@ use pocketmine\world\format\Chunk;
use pocketmine\world\World;
use function abs;
class Chest extends Spawnable implements Container, Nameable{
class Chest extends Spawnable implements ContainerTile, Nameable{
use NameableTrait {
addAdditionalSpawnData as addNameSpawnData;
}
use ContainerTrait {
use ContainerTileTrait {
onBlockDestroyedHook as containerTraitBlockDestroyedHook;
}
@ -44,15 +45,15 @@ class Chest extends Spawnable implements Container, Nameable{
public const TAG_PAIRZ = "pairz";
public const TAG_PAIR_LEAD = "pairlead";
protected ChestInventory $inventory;
protected ?DoubleChestInventory $doubleInventory = null;
protected Inventory $inventory;
protected ?CombinedInventoryProxy $doubleInventory = null;
private ?int $pairX = null;
private ?int $pairZ = null;
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
$this->inventory = new ChestInventory($this->position);
$this->inventory = new SimpleInventory(27);
}
public function readSaveData(CompoundTag $nbt) : void{
@ -114,14 +115,14 @@ class Chest extends Spawnable implements Container, Nameable{
$this->containerTraitBlockDestroyedHook();
}
public function getInventory() : ChestInventory|DoubleChestInventory{
public function getInventory() : Inventory|CombinedInventoryProxy{
if($this->isPaired() && $this->doubleInventory === null){
$this->checkPairing();
}
return $this->doubleInventory instanceof DoubleChestInventory ? $this->doubleInventory : $this->inventory;
return $this->doubleInventory ?? $this->inventory;
}
public function getRealInventory() : ChestInventory{
public function getRealInventory() : Inventory{
return $this->inventory;
}
@ -140,9 +141,9 @@ class Chest extends Spawnable implements Container, Nameable{
$this->doubleInventory = $pair->doubleInventory;
}else{
if(($pair->position->x + ($pair->position->z << 15)) > ($this->position->x + ($this->position->z << 15))){ //Order them correctly
$this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($pair->inventory, $this->inventory);
$this->doubleInventory = $pair->doubleInventory = new CombinedInventoryProxy([$pair->inventory, $this->inventory]);
}else{
$this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($this->inventory, $pair->inventory);
$this->doubleInventory = $pair->doubleInventory = new CombinedInventoryProxy([$this->inventory, $pair->inventory]);
}
}
}

View File

@ -37,8 +37,8 @@ use pocketmine\nbt\tag\StringTag;
use pocketmine\world\World;
use function count;
class ChiseledBookshelf extends Tile implements Container{
use ContainerTrait;
class ChiseledBookshelf extends Tile implements ContainerTile{
use ContainerTileTrait;
private const TAG_LAST_INTERACTED_SLOT = "LastInteractedSlot"; //TAG_Int
@ -86,7 +86,7 @@ class ChiseledBookshelf extends Tile implements Container{
}
protected function loadItems(CompoundTag $tag) : void{
if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
if(($inventoryTag = $tag->getTag(ContainerTile::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
$inventory = $this->getRealInventory();
$listeners = $inventory->getListeners()->toArray();
$inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
@ -111,7 +111,7 @@ class ChiseledBookshelf extends Tile implements Container{
$inventory->getListeners()->add(...$listeners);
}
if(($lockTag = $tag->getTag(Container::TAG_LOCK)) instanceof StringTag){
if(($lockTag = $tag->getTag(ContainerTile::TAG_LOCK)) instanceof StringTag){
$this->lock = $lockTag->getValue();
}
}
@ -130,10 +130,10 @@ class ChiseledBookshelf extends Tile implements Container{
}
}
$tag->setTag(Container::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound));
$tag->setTag(ContainerTile::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound));
if($this->lock !== null){
$tag->setString(Container::TAG_LOCK, $this->lock);
$tag->setString(ContainerTile::TAG_LOCK, $this->lock);
}
}
}

View File

@ -26,7 +26,7 @@ namespace pocketmine\block\tile;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\InventoryHolder;
interface Container extends InventoryHolder{
interface ContainerTile extends InventoryHolder{
public const TAG_ITEMS = "Items";
public const TAG_LOCK = "Lock";

View File

@ -34,16 +34,16 @@ use pocketmine\nbt\tag\StringTag;
use pocketmine\world\Position;
/**
* This trait implements most methods in the {@link Container} interface. It should only be used by Tiles.
* This trait implements most methods in the {@link ContainerTile} interface. It should only be used by Tiles.
*/
trait ContainerTrait{
trait ContainerTileTrait{
/** @var string|null */
private $lock = null;
abstract public function getRealInventory() : Inventory;
protected function loadItems(CompoundTag $tag) : void{
if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
if(($inventoryTag = $tag->getTag(ContainerTile::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
$inventory = $this->getRealInventory();
$listeners = $inventory->getListeners()->toArray();
$inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
@ -64,7 +64,7 @@ trait ContainerTrait{
$inventory->getListeners()->add(...$listeners);
}
if(($lockTag = $tag->getTag(Container::TAG_LOCK)) instanceof StringTag){
if(($lockTag = $tag->getTag(ContainerTile::TAG_LOCK)) instanceof StringTag){
$this->lock = $lockTag->getValue();
}
}
@ -75,15 +75,15 @@ trait ContainerTrait{
$items[] = $item->nbtSerialize($slot);
}
$tag->setTag(Container::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound));
$tag->setTag(ContainerTile::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound));
if($this->lock !== null){
$tag->setString(Container::TAG_LOCK, $this->lock);
$tag->setString(ContainerTile::TAG_LOCK, $this->lock);
}
}
/**
* @see Container::canOpenWith()
* @see ContainerTile::canOpenWith()
*/
public function canOpenWith(string $key) : bool{
return $this->lock === null || $this->lock === $key;

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\window\FurnaceInventoryWindow;
use pocketmine\crafting\FurnaceRecipe;
use pocketmine\crafting\FurnaceType;
use pocketmine\event\inventory\FurnaceBurnEvent;
use pocketmine\event\inventory\FurnaceSmeltEvent;
use pocketmine\inventory\CallbackInventoryListener;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
@ -40,22 +41,22 @@ use pocketmine\world\World;
use function array_map;
use function max;
abstract class Furnace extends Spawnable implements Container, Nameable{
abstract class Furnace extends Spawnable implements ContainerTile, Nameable{
use NameableTrait;
use ContainerTrait;
use ContainerTileTrait;
public const TAG_BURN_TIME = "BurnTime";
public const TAG_COOK_TIME = "CookTime";
public const TAG_MAX_TIME = "MaxTime";
protected FurnaceInventory $inventory;
protected Inventory $inventory;
private int $remainingFuelTime = 0;
private int $cookTime = 0;
private int $maxFuelTime = 0;
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
$this->inventory = new FurnaceInventory($this->position, $this->getFurnaceType());
$this->inventory = new SimpleInventory(3);
$this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange(
static function(Inventory $unused) use ($world, $pos) : void{
$world->scheduleDelayedBlockUpdate($pos, 1);
@ -104,11 +105,11 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
}
}
public function getInventory() : FurnaceInventory{
public function getInventory() : Inventory{
return $this->inventory;
}
public function getRealInventory() : FurnaceInventory{
public function getRealInventory() : Inventory{
return $this->getInventory();
}
@ -123,7 +124,7 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
$this->onStartSmelting();
if($this->remainingFuelTime > 0 && $ev->isBurning()){
$this->inventory->setFuel($fuel->getFuelResidue());
$this->inventory->setItem(FurnaceInventoryWindow::SLOT_FUEL, $fuel->getFuelResidue());
}
}
@ -159,9 +160,9 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
$ret = false;
$fuel = $this->inventory->getFuel();
$raw = $this->inventory->getSmelting();
$product = $this->inventory->getResult();
$fuel = $this->inventory->getItem(FurnaceInventoryWindow::SLOT_FUEL);
$raw = $this->inventory->getItem(FurnaceInventoryWindow::SLOT_INPUT);
$product = $this->inventory->getItem(FurnaceInventoryWindow::SLOT_RESULT);
$furnaceType = $this->getFurnaceType();
$smelt = $this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($furnaceType)->match($raw);
@ -184,9 +185,9 @@ abstract class Furnace extends Spawnable implements Container, Nameable{
$ev->call();
if(!$ev->isCancelled()){
$this->inventory->setResult($ev->getResult());
$this->inventory->setItem(FurnaceInventoryWindow::SLOT_RESULT, $ev->getResult());
$raw->pop();
$this->inventory->setSmelting($raw);
$this->inventory->setItem(FurnaceInventoryWindow::SLOT_INPUT, $raw);
}
$this->cookTime -= $furnaceType->getCookDurationTicks();

View File

@ -23,24 +23,25 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
use pocketmine\block\inventory\HopperInventory;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\World;
class Hopper extends Spawnable implements Container, Nameable{
class Hopper extends Spawnable implements ContainerTile, Nameable{
use ContainerTrait;
use ContainerTileTrait;
use NameableTrait;
private const TAG_TRANSFER_COOLDOWN = "TransferCooldown";
private HopperInventory $inventory;
private Inventory $inventory;
private int $transferCooldown = 0;
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
$this->inventory = new HopperInventory($this->position);
$this->inventory = new SimpleInventory(5);
}
public function readSaveData(CompoundTag $nbt) : void{
@ -69,11 +70,11 @@ class Hopper extends Spawnable implements Container, Nameable{
return "Hopper";
}
public function getInventory() : HopperInventory{
public function getInventory() : Inventory{
return $this->inventory;
}
public function getRealInventory() : HopperInventory{
public function getRealInventory() : Inventory{
return $this->inventory;
}
}

View File

@ -23,29 +23,43 @@ declare(strict_types=1);
namespace pocketmine\block\tile;
use pocketmine\block\inventory\ShulkerBoxInventory;
use pocketmine\block\BlockTypeIds;
use pocketmine\data\SavedDataLoadingException;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\transaction\action\validator\CallbackSlotValidator;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\Item;
use pocketmine\item\ItemTypeIds;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\world\World;
class ShulkerBox extends Spawnable implements Container, Nameable{
class ShulkerBox extends Spawnable implements ContainerTile, Nameable{
use NameableTrait {
addAdditionalSpawnData as addNameSpawnData;
}
use ContainerTrait;
use ContainerTileTrait;
public const TAG_FACING = "facing";
protected Facing $facing = Facing::NORTH;
protected ShulkerBoxInventory $inventory;
protected Inventory $inventory;
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
$this->inventory = new ShulkerBoxInventory($this->position);
$this->inventory = new SimpleInventory(27);
$this->inventory->getSlotValidators()->add(new CallbackSlotValidator(static function(Inventory $_, Item $item) : ?TransactionValidationException{ //remaining params not needed
$blockTypeId = ItemTypeIds::toBlockTypeId($item->getTypeId());
if($blockTypeId === BlockTypeIds::SHULKER_BOX || $blockTypeId === BlockTypeIds::DYED_SHULKER_BOX){
return new TransactionValidationException("Shulker box inventory cannot contain shulker boxes");
}
return null;
}));
}
public function readSaveData(CompoundTag $nbt) : void{
@ -96,11 +110,11 @@ class ShulkerBox extends Spawnable implements Container, Nameable{
$this->facing = $facing;
}
public function getInventory() : ShulkerBoxInventory{
public function getInventory() : Inventory{
return $this->inventory;
}
public function getRealInventory() : ShulkerBoxInventory{
public function getRealInventory() : Inventory{
return $this->inventory;
}

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\utils;
/**
* Blocks which have audiovisual behaviour (like chests) and remain in their "open" state for as long as at least 1
* viewer is viewing the menu they provide access to
*/
interface AnimatedContainerLike extends MenuAccessor{
/**
* Do actions when the container block is opened by a player.
* If you have a custom viewer counter (like ender chests), you should increment it here.
*/
public function onViewerAdded() : void;
/**
* Do actions when the container block is closed by a player.
* As above, you should decrement your custom viewer counter here, if you have one.
*/
public function onViewerRemoved() : void;
}

View File

@ -0,0 +1,71 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\utils;
use pocketmine\inventory\InventoryHolder;
use pocketmine\world\Position;
use pocketmine\world\sound\Sound;
use function count;
trait AnimatedContainerLikeTrait{
protected function getViewerCount() : int{
$position = $this->getPosition();
$tile = $position->getWorld()->getTile($position);
if($tile instanceof InventoryHolder){
return count($tile->getInventory()->getViewers());
}
return 0;
}
abstract protected function getOpenSound() : Sound;
abstract protected function getCloseSound() : Sound;
abstract protected function playAnimationVisual(Position $position, bool $isOpen) : void;
protected function playAnimationSound(Position $position, bool $isOpen) : void{
$position->getWorld()->addSound($position->add(0.5, 0.5, 0.5), $isOpen ? $this->getOpenSound() : $this->getCloseSound());
}
abstract protected function getPosition() : Position;
protected function doAnimationEffects(bool $isOpen) : void{
$position = $this->getPosition();
$this->playAnimationVisual($position, $isOpen);
$this->playAnimationSound($position, $isOpen);
}
public function onViewerAdded() : void{
if($this->getViewerCount() === 1){
$this->doAnimationEffects(true);
}
}
public function onViewerRemoved() : void{
if($this->getViewerCount() === 1){
$this->doAnimationEffects(false);
}
}
}

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block\utils;
use pocketmine\block\inventory\BrewingStandInventory;
use pocketmine\block\inventory\window\BrewingStandInventoryWindow;
enum BrewingStandSlot{
case EAST;
@ -35,9 +35,9 @@ enum BrewingStandSlot{
*/
public function getSlotNumber() : int{
return match($this){
self::EAST => BrewingStandInventory::SLOT_BOTTLE_LEFT,
self::NORTHWEST => BrewingStandInventory::SLOT_BOTTLE_MIDDLE,
self::SOUTHWEST => BrewingStandInventory::SLOT_BOTTLE_RIGHT
self::EAST => BrewingStandInventoryWindow::SLOT_BOTTLE_LEFT,
self::NORTHWEST => BrewingStandInventoryWindow::SLOT_BOTTLE_MIDDLE,
self::SOUTHWEST => BrewingStandInventoryWindow::SLOT_BOTTLE_RIGHT
};
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\utils;
use pocketmine\inventory\Inventory;
/**
* Blocks which have an associated inventory of contents
* Default implementation provided by {@see ContainerTrait}
*/
interface Container extends MenuAccessor{
/**
* Returns whether an item with the given key as its custom name can be used to access the container's contents.
*/
public function canOpenWith(string $key) : bool;
/**
* Returns the inventory of this container.
* Note: This may return NULL if the container's tile was missing or incorrect. This is rare, but may occur as a
* result of plugins incorrectly creating blocks, or legacy world data.
*/
public function getInventory() : ?Inventory;
}

View File

@ -0,0 +1,78 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\utils;
use pocketmine\block\Block;
use pocketmine\block\inventory\window\BlockInventoryWindow;
use pocketmine\block\tile\ContainerTile;
use pocketmine\inventory\Inventory;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\world\Position;
trait ContainerTrait{
/**
* @see Block::onInteract()
*/
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player && !$this->isOpeningObstructed() && $this->canOpenWith($item->getCustomName())){
$this->openToUnchecked($player);
}
return true;
}
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
return new BlockInventoryWindow($player, $inventory, $position);
}
public function isOpeningObstructed() : bool{
return false;
}
abstract protected function getPosition() : Position;
protected function getTile() : ?ContainerTile{
$pos = $this->getPosition();
$tile = $pos->getWorld()->getTile($pos);
return $tile instanceof ContainerTile ? $tile : null;
}
public function canOpenWith(string $key) : bool{
//TODO: maybe we can bring the key to the block in readStateFromWorld()?
return $this->getTile()?->canOpenWith($key) ?? false;
}
public function openToUnchecked(Player $player) : bool{
$tile = $this->getTile();
return $tile !== null && $player->setCurrentWindow($this->newMenu($player, $tile->getInventory(), $this->getPosition()));
}
public function getInventory() : ?Inventory{
return $this->getTile()?->getInventory();
}
}

View File

@ -21,16 +21,25 @@
declare(strict_types=1);
namespace pocketmine\inventory;
namespace pocketmine\block\utils;
use pocketmine\crafting\CraftingGrid;
use pocketmine\player\Player;
final class PlayerCraftingInventory extends CraftingGrid implements TemporaryInventory{
/**
* Blocks which open a menu when interacted with
* This could be a container menu, or a menu that otherwise deals with items, such as a crafting menu
*/
interface MenuAccessor{
/**
* Returns whether the block's ability to open the menu is currently obstructed (e.g. by nearby blocks).
*/
public function isOpeningObstructed() : bool;
public function __construct(private Player $holder){
parent::__construct(CraftingGrid::SIZE_SMALL);
}
public function getHolder() : Player{ return $this->holder; }
/**
* Opens the menu to the player.
* Note: No preconditions are checked. Do not check for obstruction or locks here.
*
* Returns true if successful, false otherwise (e.g. event cancelled, container missing)
*/
public function openToUnchecked(Player $player) : bool;
}

View File

@ -0,0 +1,58 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\utils;
use pocketmine\block\Block;
use pocketmine\item\Item;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\world\Position;
trait MenuAccessorTrait{
/**
* @see Block::onInteract()
*/
public function onInteract(Item $item, Facing $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($player instanceof Player && !$this->isOpeningObstructed()){
$this->openToUnchecked($player);
}
return true;
}
abstract protected function newMenu(Player $player, Position $position) : InventoryWindow;
public function isOpeningObstructed() : bool{
return false;
}
abstract protected function getPosition() : Position;
public function openToUnchecked(Player $player) : bool{
return $player->setCurrentWindow($this->newMenu($player, $this->getPosition()));
}
}

View File

@ -56,7 +56,7 @@ class EnchantCommand extends VanillaCommand{
return true;
}
$item = $player->getInventory()->getItemInHand();
$item = $player->getMainHandItem();
if($item->isNull()){
$sender->sendMessage(KnownTranslationFactory::commands_enchant_noItem());
@ -79,7 +79,7 @@ class EnchantCommand extends VanillaCommand{
//this is necessary to deal with enchanted books, which are a different item type than regular books
$enchantedItem = EnchantingHelper::enchantItem($item, [new EnchantmentInstance($enchantment, $level)]);
$player->getInventory()->setItemInHand($enchantedItem);
$player->setMainHandItem($enchantedItem);
self::broadcastCommandMessage($sender, KnownTranslationFactory::commands_enchant_success($player->getName()));
return true;

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

@ -243,10 +243,10 @@ class ExperienceManager{
//TODO: replace this with a more generic equipment getting/setting interface
$equipment = [];
if(($item = $this->entity->getInventory()->getItemInHand()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
if(($item = $this->entity->getMainHandItem()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
$equipment[$mainHandIndex] = $item;
}
if(($item = $this->entity->getOffHandInventory()->getItem(0)) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
if(($item = $this->entity->getOffHandItem()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){
$equipment[$offHandIndex] = $item;
}
foreach($this->entity->getArmorInventory()->getContents() as $k => $armorItem){
@ -263,9 +263,9 @@ class ExperienceManager{
$xpValue -= (int) ceil($repairAmount / 2);
if($k === $mainHandIndex){
$this->entity->getInventory()->setItemInHand($repairItem);
$this->entity->setMainHandItem($repairItem);
}elseif($k === $offHandIndex){
$this->entity->getOffHandInventory()->setItem(0, $repairItem);
$this->entity->setOffHandItem($repairItem);
}else{
$this->entity->getArmorInventory()->setItem($k, $repairItem);
}

View File

@ -32,11 +32,10 @@ use pocketmine\entity\projectile\ProjectileSource;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityExhaustEvent;
use pocketmine\inventory\CallbackInventoryListener;
use pocketmine\inventory\Hotbar;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\InventoryHolder;
use pocketmine\inventory\PlayerEnderInventory;
use pocketmine\inventory\PlayerInventory;
use pocketmine\inventory\PlayerOffHandInventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\item\enchantment\EnchantingHelper;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\item\Item;
@ -101,9 +100,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
public function getNetworkTypeId() : string{ return EntityIds::PLAYER; }
protected PlayerInventory $inventory;
protected PlayerOffHandInventory $offHandInventory;
protected PlayerEnderInventory $enderInventory;
protected Hotbar $hotbar;
protected Inventory $inventory;
protected Inventory $offHandInventory;
protected Inventory $enderInventory;
protected UuidInterface $uuid;
@ -237,13 +237,33 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
return min(100, 7 * $this->xpManager->getXpLevel());
}
public function getInventory() : PlayerInventory{
public function getHotbar() : Hotbar{
return $this->hotbar;
}
public function getInventory() : Inventory{
return $this->inventory;
}
public function getOffHandInventory() : PlayerOffHandInventory{ return $this->offHandInventory; }
public function getMainHandItem() : Item{
return $this->inventory->getItem($this->hotbar->getSelectedIndex());
}
public function getEnderInventory() : PlayerEnderInventory{
public function setMainHandItem(Item $item) : void{
$this->inventory->setItem($this->hotbar->getSelectedIndex(), $item);
}
public function getOffHandItem() : Item{
return $this->offHandInventory->getItem(0);
}
public function setOffHandItem(Item $item) : void{
$this->offHandInventory->setItem(0, $item);
}
public function getOffHandInventory() : Inventory{ return $this->offHandInventory; }
public function getEnderInventory() : Inventory{
return $this->enderInventory;
}
@ -274,25 +294,27 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$this->hungerManager = new HungerManager($this);
$this->xpManager = new ExperienceManager($this);
$this->inventory = new PlayerInventory($this);
$this->inventory = new SimpleInventory(36);
$this->hotbar = new Hotbar();
$syncHeldItem = fn() => NetworkBroadcastUtils::broadcastEntityEvent(
$this->getViewers(),
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
);
$this->inventory->getListeners()->add(new CallbackInventoryListener(
function(Inventory $unused, int $slot, Item $unused2) use ($syncHeldItem) : void{
if($slot === $this->inventory->getHeldItemIndex()){
if($slot === $this->hotbar->getSelectedIndex()){
$syncHeldItem();
}
},
function(Inventory $unused, array $oldItems) use ($syncHeldItem) : void{
if(array_key_exists($this->inventory->getHeldItemIndex(), $oldItems)){
if(array_key_exists($this->hotbar->getSelectedIndex(), $oldItems)){
$syncHeldItem();
}
}
));
$this->offHandInventory = new PlayerOffHandInventory($this);
$this->enderInventory = new PlayerEnderInventory($this);
$this->offHandInventory = new SimpleInventory(1);
$this->enderInventory = new SimpleInventory(27);
$this->initHumanData($nbt);
$inventoryTag = $nbt->getListTag(self::TAG_INVENTORY);
@ -317,7 +339,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
}
$offHand = $nbt->getCompoundTag(self::TAG_OFF_HAND_ITEM);
if($offHand !== null){
$this->offHandInventory->setItem(0, Item::nbtDeserialize($offHand));
$this->setOffHandItem(Item::nbtDeserialize($offHand));
}
$this->offHandInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
$this->getViewers(),
@ -335,8 +357,9 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems);
}
$this->inventory->setHeldItemIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0));
$this->inventory->getHeldItemIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
$this->hotbar->setSelectedIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0));
//TODO: cyclic reference
$this->hotbar->getSelectedIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
$this->getViewers(),
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this)
));
@ -376,7 +399,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$type = $source->getCause();
if($type !== EntityDamageEvent::CAUSE_SUICIDE && $type !== EntityDamageEvent::CAUSE_VOID
&& ($this->inventory->getItemInHand() instanceof Totem || $this->offHandInventory->getItem(0) instanceof Totem)){
&& ($this->getMainHandItem() instanceof Totem || $this->getOffHandItem() instanceof Totem)){
$compensation = $this->getHealth() - $source->getFinalDamage() - 1;
if($compensation <= -1){
@ -398,13 +421,13 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$this->broadcastAnimation(new TotemUseAnimation($this));
$this->broadcastSound(new TotemUseSound());
$hand = $this->inventory->getItemInHand();
$hand = $this->getMainHandItem();
if($hand instanceof Totem){
$hand->pop(); //Plugins could alter max stack size
$this->inventory->setItemInHand($hand);
}elseif(($offHand = $this->offHandInventory->getItem(0)) instanceof Totem){
$this->setMainHandItem($hand);
}elseif(($offHand = $this->getOffHandItem()) instanceof Totem){
$offHand->pop();
$this->offHandInventory->setItem(0, $offHand);
$this->setOffHandItem($offHand);
}
}
}
@ -434,8 +457,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$nbt->setTag(self::TAG_INVENTORY, $inventoryTag);
//Normal inventory
$slotCount = $this->inventory->getSize() + $this->inventory->getHotbarSize();
for($slot = $this->inventory->getHotbarSize(); $slot < $slotCount; ++$slot){
$slotCount = $this->inventory->getSize() + $this->hotbar->getSize();
for($slot = $this->hotbar->getSize(); $slot < $slotCount; ++$slot){
$item = $this->inventory->getItem($slot - 9);
if(!$item->isNull()){
$inventoryTag->push($item->nbtSerialize($slot));
@ -450,9 +473,9 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
}
}
$nbt->setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->inventory->getHeldItemIndex());
$nbt->setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->hotbar->getSelectedIndex());
$offHandItem = $this->offHandInventory->getItem(0);
$offHandItem = $this->getOffHandItem();
if(!$offHandItem->isNull()){
$nbt->setTag(self::TAG_OFF_HAND_ITEM, $offHandItem->nbtSerialize());
}
@ -504,7 +527,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$this->location->pitch,
$this->location->yaw,
$this->location->yaw, //TODO: head yaw
ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($this->getInventory()->getItemInHand())),
ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($this->getMainHandItem())),
GameMode::SURVIVAL,
$this->getAllNetworkData(),
new PropertySyncData([], []),
@ -539,8 +562,8 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
}
protected function onDispose() : void{
$this->hotbar->getSelectedIndexChangeListeners()->clear();
$this->inventory->removeAllViewers();
$this->inventory->getHeldItemIndexChangeListeners()->clear();
$this->offHandInventory->removeAllViewers();
$this->enderInventory->removeAllViewers();
parent::onDispose();
@ -548,9 +571,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
protected function destroyCycles() : void{
unset(
$this->inventory,
$this->offHandInventory,
$this->enderInventory,
$this->hungerManager,
$this->xpManager
);

View File

@ -152,7 +152,7 @@ abstract class Living extends Entity{
$this->effectManager->getEffectAddHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
$this->effectManager->getEffectRemoveHooks()->add(function() : void{ $this->networkPropertiesDirty = true; });
$this->armorInventory = new ArmorInventory($this);
$this->armorInventory = new ArmorInventory();
//TODO: load/save armor inventory contents
$this->armorInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent(
$this->getViewers(),
@ -995,7 +995,6 @@ abstract class Living extends Entity{
protected function destroyCycles() : void{
unset(
$this->armorInventory,
$this->effectManager
);
parent::destroyCycles();

View File

@ -322,7 +322,7 @@ class ItemEntity extends Entity{
$item = $this->getItem();
$playerInventory = match(true){
$player->getOffHandInventory()->getItem(0)->canStackWith($item) && $player->getOffHandInventory()->getAddableItemQuantity($item) > 0 => $player->getOffHandInventory(),
$player->getOffHandItem()->canStackWith($item) && $player->getOffHandInventory()->getAddableItemQuantity($item) > 0 => $player->getOffHandInventory(),
$player->getInventory()->getAddableItemQuantity($item) > 0 => $player->getInventory(),
default => null
};

View File

@ -172,7 +172,7 @@ class Arrow extends Projectile{
$item = VanillaItems::ARROW();
$playerInventory = match(true){
!$player->hasFiniteResources() => null, //arrows are not picked up in creative
$player->getOffHandInventory()->getItem(0)->canStackWith($item) && $player->getOffHandInventory()->canAddItem($item) => $player->getOffHandInventory(),
$player->getOffHandItem()->canStackWith($item) && $player->getOffHandInventory()->canAddItem($item) => $player->getOffHandInventory(),
$player->getInventory()->canAddItem($item) => $player->getInventory(),
default => null
};

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\window\EnchantingTableInventoryWindow;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\event\Event;
use pocketmine\item\enchantment\EnchantingOption;
use pocketmine\player\Player;
use pocketmine\utils\Utils;
@ -44,13 +43,13 @@ class PlayerEnchantingOptionsRequestEvent extends PlayerEvent implements Cancell
*/
public function __construct(
Player $player,
private readonly EnchantInventory $inventory,
private readonly EnchantingTableInventoryWindow $inventory,
private array $options
){
$this->player = $player;
}
public function getInventory() : EnchantInventory{
public function getInventory() : EnchantingTableInventoryWindow{
return $this->inventory;
}

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

@ -102,28 +102,14 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
$listeners = $this->listeners->toArray();
$this->listeners->clear();
$viewers = $this->viewers;
$this->viewers = [];
$this->internalSetContents($items);
$this->listeners->add(...$listeners); //don't directly write, in case listeners were added while operation was in progress
foreach($viewers as $id => $viewer){
$this->viewers[$id] = $viewer;
}
$this->onContentChange($oldContents);
}
/**
* Helper for utility functions which search the inventory.
* TODO: make this abstract instead of providing a slow default implementation (BC break)
*/
protected function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
$item = $this->getItem($slot);
return $item->equals($test, true, $checkTags) ? $item->getCount() : 0;
}
public function contains(Item $item) : bool{
$count = max(1, $item->getCount());
$checkTags = $item->hasNamedTag();
@ -351,7 +337,7 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
*/
public function removeAllViewers() : void{
foreach($this->viewers as $hash => $viewer){
if($viewer->getCurrentWindow() === $this){ //this might not be the case for the player's own inventory
if($viewer->getCurrentWindow()?->getInventory() === $this){ //this might not be the case for the player's own inventory
$viewer->removeCurrentWindow();
}
unset($this->viewers[$hash]);
@ -370,13 +356,6 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
foreach($this->listeners as $listener){
$listener->onSlotChange($this, $index, $before);
}
foreach($this->viewers as $viewer){
$invManager = $viewer->getNetworkSession()->getInvManager();
if($invManager === null){
continue;
}
$invManager->onSlotChange($this, $index);
}
}
/**
@ -387,14 +366,6 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
foreach($this->listeners as $listener){
$listener->onContentChange($this, $itemsBefore);
}
foreach($this->getViewers() as $viewer){
$invManager = $viewer->getNetworkSession()->getInvManager();
if($invManager === null){
continue;
}
$invManager->syncContents($this);
}
}
public function slotExists(int $slot) : bool{

View File

@ -0,0 +1,194 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\inventory;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\utils\AssumptionFailedError;
use function array_fill_keys;
use function array_keys;
use function count;
use function spl_object_id;
/**
* Allows interacting with several separate inventories via a unified interface
* Mainly used for double chests, but could be used for other custom use cases
*/
final class CombinedInventoryProxy extends BaseInventory{
private readonly int $size;
/**
* @var Inventory[]
* @phpstan-var array<int, Inventory>
*/
private array $backingInventories = [];
/**
* @var Inventory[]
* @phpstan-var array<int, Inventory>
*/
private array $slotToInventoryMap = [];
/**
* @var int[]
* @phpstan-var array<int, int>
*/
private array $inventoryToOffsetMap = [];
private InventoryListener $backingInventoryListener;
private bool $modifyingBackingInventory = false;
/**
* @phpstan-param Inventory[] $backingInventories
*/
public function __construct(
array $backingInventories
){
parent::__construct();
foreach($backingInventories as $backingInventory){
$this->backingInventories[spl_object_id($backingInventory)] = $backingInventory;
}
$combinedSize = 0;
foreach($this->backingInventories as $inventory){
$size = $inventory->getSize();
$this->inventoryToOffsetMap[spl_object_id($inventory)] = $combinedSize;
for($slot = 0; $slot < $size; $slot++){
$this->slotToInventoryMap[$combinedSize + $slot] = $inventory;
}
$combinedSize += $size;
}
$this->size = $combinedSize;
$weakThis = \WeakReference::create($this);
$getThis = static fn() => $weakThis->get() ?? throw new AssumptionFailedError("Listener should've been unregistered in __destruct()");
$this->backingInventoryListener = new CallbackInventoryListener(
onSlotChange: static function(Inventory $inventory, int $slot, Item $oldItem) use ($getThis) : void{
$strongThis = $getThis();
if($strongThis->modifyingBackingInventory){
return;
}
$offset = $strongThis->inventoryToOffsetMap[spl_object_id($inventory)];
$strongThis->onSlotChange($offset + $slot, $oldItem);
},
onContentChange: static function(Inventory $inventory, array $oldContents) use ($getThis) : void{
$strongThis = $getThis();
if($strongThis->modifyingBackingInventory){
return;
}
if(count($strongThis->backingInventories) === 1){
$strongThis->onContentChange($oldContents);
}else{
$offset = $strongThis->inventoryToOffsetMap[spl_object_id($inventory)];
for($slot = 0, $limit = $inventory->getSize(); $slot < $limit; $slot++){
$strongThis->onSlotChange($offset + $slot, $oldContents[$slot] ?? VanillaItems::AIR());
}
}
}
);
foreach($this->backingInventories as $inventory){
$inventory->getListeners()->add($this->backingInventoryListener);
}
}
public function __destruct(){
foreach($this->backingInventories as $inventory){
$inventory->getListeners()->remove($this->backingInventoryListener);
}
}
/**
* @phpstan-return array{Inventory, int}
*/
private function getInventory(int $slot) : array{
$inventory = $this->slotToInventoryMap[$slot] ?? throw new \InvalidArgumentException("Invalid combined inventory slot $slot");
$actualSlot = $slot - $this->inventoryToOffsetMap[spl_object_id($inventory)];
return [$inventory, $actualSlot];
}
protected function internalSetItem(int $index, Item $item) : void{
[$inventory, $actualSlot] = $this->getInventory($index);
//Make sure our backing listener doesn't dispatch double updates to our own listeners
$this->modifyingBackingInventory = true;
try{
$inventory->setItem($actualSlot, $item);
}finally{
$this->modifyingBackingInventory = false;
}
}
protected function internalSetContents(array $items) : void{
$contentsByInventory = array_fill_keys(array_keys($this->backingInventories), []);
foreach($items as $i => $item){
[$inventory, $actualSlot] = $this->getInventory($i);
$contentsByInventory[spl_object_id($inventory)][$actualSlot] = $item;
}
foreach($contentsByInventory as $splObjectId => $backingInventoryContents){
$backingInventory = $this->backingInventories[$splObjectId];
//Make sure our backing listener doesn't dispatch double updates to our own listeners
$this->modifyingBackingInventory = true;
try{
$backingInventory->setContents($backingInventoryContents);
}finally{
$this->modifyingBackingInventory = false;
}
}
}
public function getSize() : int{
return $this->size;
}
public function getItem(int $index) : Item{
[$inventory, $actualSlot] = $this->getInventory($index);
return $inventory->getItem($actualSlot);
}
public function getContents(bool $includeEmpty = false) : array{
$result = [];
foreach($this->backingInventories as $inventory){
$offset = $this->inventoryToOffsetMap[spl_object_id($inventory)];
foreach($inventory->getContents($includeEmpty) as $i => $item){
$result[$offset + $i] = $item;
}
}
return $result;
}
public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
[$inventory, $actualSlot] = $this->getInventory($slot);
return $inventory->getMatchingItemCount($actualSlot, $test, $checkTags);
}
public function isSlotEmpty(int $index) : bool{
[$inventory, $actualSlot] = $this->getInventory($index);
return $inventory->isSlotEmpty($actualSlot);
}
}

View File

@ -1,103 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\inventory;
use pocketmine\item\Item;
/**
* An inventory which is backed by another inventory, and acts as a proxy to that inventory.
*/
class DelegateInventory extends BaseInventory{
private InventoryListener $inventoryListener;
private bool $backingInventoryChanging = false;
public function __construct(
private Inventory $backingInventory
){
parent::__construct();
$weakThis = \WeakReference::create($this);
$this->backingInventory->getListeners()->add($this->inventoryListener = new CallbackInventoryListener(
static function(Inventory $unused, int $slot, Item $oldItem) use ($weakThis) : void{
if(($strongThis = $weakThis->get()) !== null){
$strongThis->backingInventoryChanging = true;
try{
$strongThis->onSlotChange($slot, $oldItem);
}finally{
$strongThis->backingInventoryChanging = false;
}
}
},
static function(Inventory $unused, array $oldContents) use ($weakThis) : void{
if(($strongThis = $weakThis->get()) !== null){
$strongThis->backingInventoryChanging = true;
try{
$strongThis->onContentChange($oldContents);
}finally{
$strongThis->backingInventoryChanging = false;
}
}
}
));
}
public function __destruct(){
$this->backingInventory->getListeners()->remove($this->inventoryListener);
}
public function getSize() : int{
return $this->backingInventory->getSize();
}
public function getItem(int $index) : Item{
return $this->backingInventory->getItem($index);
}
protected function internalSetItem(int $index, Item $item) : void{
$this->backingInventory->setItem($index, $item);
}
public function getContents(bool $includeEmpty = false) : array{
return $this->backingInventory->getContents($includeEmpty);
}
protected function internalSetContents(array $items) : void{
$this->backingInventory->setContents($items);
}
public function isSlotEmpty(int $index) : bool{
return $this->backingInventory->isSlotEmpty($index);
}
protected function onSlotChange(int $index, Item $before) : void{
if($this->backingInventoryChanging){
parent::onSlotChange($index, $before);
}
}
protected function onContentChange(array $itemsBefore) : void{
if($this->backingInventoryChanging){
parent::onContentChange($itemsBefore);
}
}
}

View File

@ -23,30 +23,25 @@ declare(strict_types=1);
namespace pocketmine\inventory;
use pocketmine\entity\Human;
use pocketmine\item\Item;
use pocketmine\player\Player;
use pocketmine\utils\ObjectSet;
class PlayerInventory extends SimpleInventory{
protected Human $holder;
protected int $itemInHandIndex = 0;
final class Hotbar{
protected int $selectedIndex = 0;
/**
* @var \Closure[]|ObjectSet
* @phpstan-var ObjectSet<\Closure(int $oldIndex) : void>
*/
protected ObjectSet $heldItemIndexChangeListeners;
protected ObjectSet $selectedIndexChangeListeners;
public function __construct(Human $player){
$this->holder = $player;
$this->heldItemIndexChangeListeners = new ObjectSet();
parent::__construct(36);
public function __construct(
private int $size = 9
){
$this->selectedIndexChangeListeners = new ObjectSet();
}
public function isHotbarSlot(int $slot) : bool{
return $slot >= 0 && $slot < $this->getHotbarSize();
return $slot >= 0 && $slot < $this->getSize();
}
/**
@ -54,25 +49,15 @@ class PlayerInventory extends SimpleInventory{
*/
private function throwIfNotHotbarSlot(int $slot) : void{
if(!$this->isHotbarSlot($slot)){
throw new \InvalidArgumentException("$slot is not a valid hotbar slot index (expected 0 - " . ($this->getHotbarSize() - 1) . ")");
throw new \InvalidArgumentException("$slot is not a valid hotbar slot index (expected 0 - " . ($this->getSize() - 1) . ")");
}
}
/**
* Returns the item in the specified hotbar slot.
*
* @throws \InvalidArgumentException if the hotbar slot index is out of range
*/
public function getHotbarSlotItem(int $hotbarSlot) : Item{
$this->throwIfNotHotbarSlot($hotbarSlot);
return $this->getItem($hotbarSlot);
}
/**
* Returns the hotbar slot number the holder is currently holding.
*/
public function getHeldItemIndex() : int{
return $this->itemInHandIndex;
public function getSelectedIndex() : int{
return $this->selectedIndex;
}
/**
@ -82,13 +67,13 @@ class PlayerInventory extends SimpleInventory{
*
* @throws \InvalidArgumentException if the hotbar slot is out of range
*/
public function setHeldItemIndex(int $hotbarSlot) : void{
public function setSelectedIndex(int $hotbarSlot) : void{
$this->throwIfNotHotbarSlot($hotbarSlot);
$oldIndex = $this->itemInHandIndex;
$this->itemInHandIndex = $hotbarSlot;
$oldIndex = $this->selectedIndex;
$this->selectedIndex = $hotbarSlot;
foreach($this->heldItemIndexChangeListeners as $callback){
foreach($this->selectedIndexChangeListeners as $callback){
$callback($oldIndex);
}
}
@ -97,30 +82,12 @@ class PlayerInventory extends SimpleInventory{
* @return \Closure[]|ObjectSet
* @phpstan-return ObjectSet<\Closure(int $oldIndex) : void>
*/
public function getHeldItemIndexChangeListeners() : ObjectSet{ return $this->heldItemIndexChangeListeners; }
/**
* Returns the currently-held item.
*/
public function getItemInHand() : Item{
return $this->getHotbarSlotItem($this->itemInHandIndex);
}
/**
* Sets the item in the currently-held slot to the specified item.
*/
public function setItemInHand(Item $item) : void{
$this->setItem($this->getHeldItemIndex(), $item);
}
public function getSelectedIndexChangeListeners() : ObjectSet{ return $this->selectedIndexChangeListeners; }
/**
* Returns the number of slots in the hotbar.
*/
public function getHotbarSize() : int{
return 9;
}
public function getHolder() : Human{
return $this->holder;
public function getSize() : int{
return $this->size;
}
}

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,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 pocketmine\utils\Utils;
use function array_values;
@ -57,10 +58,10 @@ class InventoryTransaction{
protected bool $hasExecuted = false;
/**
* @var Inventory[]
* @phpstan-var array<int, Inventory>
* @var InventoryWindow[]
* @phpstan-var array<int, InventoryWindow>
*/
protected array $inventories = [];
protected array $inventoryWindows = [];
/**
* @var InventoryAction[]
@ -85,11 +86,11 @@ class InventoryTransaction{
}
/**
* @return Inventory[]
* @phpstan-return array<int, Inventory>
* @return InventoryWindow[]
* @phpstan-return array<int, InventoryWindow>
*/
public function getInventories() : array{
return $this->inventories;
public function getInventoryWindows() : array{
return $this->inventoryWindows;
}
/**
@ -111,8 +112,8 @@ class InventoryTransaction{
public function addAction(InventoryAction $action) : void{
if(!isset($this->actions[$hash = spl_object_id($action)])){
$this->actions[$hash] = $action;
if($action instanceof SlotChangeAction && !isset($this->inventories[$inventoryId = spl_object_id($action->getInventory())])){
$this->inventories[$inventoryId] = $action->getInventory();
if($action instanceof SlotChangeAction && !isset($this->inventoryWindows[$inventoryId = spl_object_id($action->getInventoryWindow())])){
$this->inventoryWindows[$inventoryId] = $action->getInventoryWindow();
}
}else{
throw new \InvalidArgumentException("Tried to add the same action to a transaction twice");
@ -185,8 +186,8 @@ class InventoryTransaction{
foreach($this->actions as $key => $action){
if($action instanceof SlotChangeAction){
$slotChanges[$h = (spl_object_hash($action->getInventory()) . "@" . $action->getSlot())][] = $action;
$inventories[$h] = $action->getInventory();
$slotChanges[$h = (spl_object_hash($action->getInventoryWindow()) . "@" . $action->getSlot())][] = $action;
$inventories[$h] = $action->getInventoryWindow();
$slots[$h] = $action->getSlot();
}
}
@ -196,10 +197,11 @@ class InventoryTransaction{
continue;
}
$inventory = $inventories[$hash];
$window = $inventories[$hash];
$inventory = $window->getInventory();
$slot = $slots[$hash];
if(!$inventory->slotExists($slot)){ //this can get hit for crafting tables because the validation happens after this compaction
throw new TransactionValidationException("Slot $slot does not exist in inventory " . get_class($inventory));
throw new TransactionValidationException("Slot $slot does not exist in inventory window " . get_class($window));
}
$sourceItem = $inventory->getItem($slot);
@ -214,7 +216,7 @@ class InventoryTransaction{
if(!$targetItem->equalsExact($sourceItem)){
//sometimes we get actions on the crafting grid whose source and target items are the same, so dump them
$this->addAction(new SlotChangeAction($inventory, $slot, $sourceItem, $targetItem));
$this->addAction(new SlotChangeAction($window, $slot, $sourceItem, $targetItem));
}
}
}

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,16 +93,25 @@ final class TransactionBuilderInventory extends BaseInventory{
return $contents;
}
public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{
$slotItem = $this->changedSlots[$slot] ?? null;
if($slotItem !== null){
return $slotItem->equals($test, true, $checkTags) ? $slotItem->getCount() : 0;
}
return $this->inventoryWindow->getInventory()->getMatchingItemCount($slot, $test, $checkTags);
}
/**
* @return SlotChangeAction[]
*/
public function generateActions() : array{
$result = [];
$inventory = $this->inventoryWindow->getInventory();
foreach($this->changedSlots as $index => $newItem){
if($newItem !== null){
$oldItem = $this->actualInventory->getItem($index);
$oldItem = $inventory->getItem($index);
if(!$newItem->equalsExact($oldItem)){
$result[] = new SlotChangeAction($this->actualInventory, $index, $oldItem, $newItem);
$result[] = new SlotChangeAction($this->inventoryWindow, $index, $oldItem, $newItem);
}
}
}

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

@ -27,6 +27,7 @@ use pocketmine\inventory\Inventory;
use pocketmine\inventory\SlotValidatedInventory;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\Item;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
/**
@ -34,7 +35,7 @@ use pocketmine\player\Player;
*/
class SlotChangeAction extends InventoryAction{
public function __construct(
protected Inventory $inventory,
protected InventoryWindow $inventoryWindow,
private int $inventorySlot,
Item $sourceItem,
Item $targetItem
@ -43,10 +44,10 @@ class SlotChangeAction extends InventoryAction{
}
/**
* Returns the inventory involved in this action.
* Returns the inventory window involved in this action.
*/
public function getInventory() : Inventory{
return $this->inventory;
public function getInventoryWindow() : InventoryWindow{
return $this->inventoryWindow;
}
/**
@ -62,21 +63,22 @@ class SlotChangeAction extends InventoryAction{
* @throws TransactionValidationException
*/
public function validate(Player $source) : void{
if(!$this->inventory->slotExists($this->inventorySlot)){
$inventory = $this->inventoryWindow->getInventory();
if(!$inventory->slotExists($this->inventorySlot)){
throw new TransactionValidationException("Slot does not exist");
}
if(!$this->inventory->getItem($this->inventorySlot)->equalsExact($this->sourceItem)){
if(!$inventory->getItem($this->inventorySlot)->equalsExact($this->sourceItem)){
throw new TransactionValidationException("Slot does not contain expected original item");
}
if($this->targetItem->getCount() > $this->targetItem->getMaxStackSize()){
throw new TransactionValidationException("Target item exceeds item type max stack size");
}
if($this->targetItem->getCount() > $this->inventory->getMaxStackSize()){
if($this->targetItem->getCount() > $inventory->getMaxStackSize()){
throw new TransactionValidationException("Target item exceeds inventory max stack size");
}
if($this->inventory instanceof SlotValidatedInventory && !$this->targetItem->isNull()){
foreach($this->inventory->getSlotValidators() as $validator){
$ret = $validator->validate($this->inventory, $this->targetItem, $this->inventorySlot);
if($inventory instanceof SlotValidatedInventory && !$this->targetItem->isNull()){
foreach($inventory->getSlotValidators() as $validator){
$ret = $validator->validate($inventory, $this->targetItem, $this->inventorySlot);
if($ret !== null){
throw new TransactionValidationException("Target item is not accepted by the inventory at slot #" . $this->inventorySlot . ": " . $ret->getMessage(), 0, $ret);
}
@ -88,6 +90,6 @@ class SlotChangeAction extends InventoryAction{
* Sets the item into the target inventory.
*/
public function execute(Player $source) : void{
$this->inventory->setItem($this->inventorySlot, $this->targetItem);
$this->inventoryWindow->getInventory()->setItem($this->inventorySlot, $this->targetItem);
}
}

View File

@ -145,7 +145,7 @@ class Armor extends Durable{
$thisCopy = clone $this;
$new = $thisCopy->pop();
$player->getArmorInventory()->setItem($this->getArmorSlot(), $new);
$player->getInventory()->setItemInHand($existing);
$player->setMainHandItem($existing);
$sound = $new->getMaterial()->getEquipSound();
if($sound !== null){
$player->broadcastSound($sound);

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,20 +23,21 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\inventory\BlockInventory;
use pocketmine\block\inventory\BrewingStandInventory;
use pocketmine\block\inventory\CartographyTableInventory;
use pocketmine\block\inventory\CraftingTableInventory;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\FurnaceInventory;
use pocketmine\block\inventory\HopperInventory;
use pocketmine\block\inventory\LoomInventory;
use pocketmine\block\inventory\SmithingTableInventory;
use pocketmine\block\inventory\StonecutterInventory;
use pocketmine\block\inventory\window\AnvilInventoryWindow;
use pocketmine\block\inventory\window\BlockInventoryWindow;
use pocketmine\block\inventory\window\BrewingStandInventoryWindow;
use pocketmine\block\inventory\window\CartographyTableInventoryWindow;
use pocketmine\block\inventory\window\CraftingTableInventoryWindow;
use pocketmine\block\inventory\window\EnchantingTableInventoryWindow;
use pocketmine\block\inventory\window\FurnaceInventoryWindow;
use pocketmine\block\inventory\window\HopperInventoryWindow;
use pocketmine\block\inventory\window\LoomInventoryWindow;
use pocketmine\block\inventory\window\SmithingTableInventoryWindow;
use pocketmine\block\inventory\window\StonecutterInventoryWindow;
use pocketmine\crafting\FurnaceType;
use pocketmine\data\bedrock\EnchantmentIdMap;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\InventoryListener;
use pocketmine\inventory\transaction\action\SlotChangeAction;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\item\enchantment\EnchantingOption;
@ -62,7 +63,9 @@ use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;
use pocketmine\network\PacketHandlingException;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\player\PlayerInventoryWindow;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\ObjectSet;
use function array_fill_keys;
@ -77,27 +80,27 @@ use function max;
use function spl_object_id;
/**
* @phpstan-type ContainerOpenClosure \Closure(int $id, Inventory $inventory) : (list<ClientboundPacket>|null)
* @phpstan-type ContainerOpenClosure \Closure(int $id, InventoryWindow $window) : (list<ClientboundPacket>|null)
*/
class InventoryManager{
class InventoryManager implements InventoryListener{
/**
* @var InventoryManagerEntry[] spl_object_id(Inventory) => InventoryManagerEntry
* @phpstan-var array<int, InventoryManagerEntry>
*/
private array $inventories = [];
private array $entries = [];
/**
* @var Inventory[] network window ID => Inventory
* @phpstan-var array<int, Inventory>
* @var InventoryWindow[] network window ID => InventoryWindow
* @phpstan-var array<int, InventoryWindow>
*/
private array $networkIdToInventoryMap = [];
private array $networkIdToWindowMap = [];
/**
* @var ComplexInventoryMapEntry[] net slot ID => ComplexWindowMapEntry
* @phpstan-var array<int, ComplexInventoryMapEntry>
* @var ComplexWindowMapEntry[] net slot ID => ComplexWindowMapEntry
* @phpstan-var array<int, ComplexWindowMapEntry>
*/
private array $complexSlotToInventoryMap = [];
private array $complexSlotToWindowMap = [];
private int $lastInventoryNetworkId = ContainerIds::FIRST;
private int $lastWindowNetworkId = ContainerIds::FIRST;
private int $currentWindowType = WindowTypes::CONTAINER;
private int $clientSelectedHotbarSlot = -1;
@ -127,33 +130,52 @@ class InventoryManager{
$this->containerOpenCallbacks = new ObjectSet();
$this->containerOpenCallbacks->add(self::createContainerOpen(...));
$this->add(ContainerIds::INVENTORY, $this->player->getInventory());
$this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory());
$this->add(ContainerIds::ARMOR, $this->player->getArmorInventory());
$this->addComplex(UIInventorySlotOffset::CURSOR, $this->player->getCursorInventory());
$this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $this->player->getCraftingGrid());
foreach($this->player->getPermanentWindows() as $window){
match($window->getType()){
PlayerInventoryWindow::TYPE_INVENTORY => $this->add(ContainerIds::INVENTORY, $window),
PlayerInventoryWindow::TYPE_OFFHAND => $this->add(ContainerIds::OFFHAND, $window),
PlayerInventoryWindow::TYPE_ARMOR => $this->add(ContainerIds::ARMOR, $window),
PlayerInventoryWindow::TYPE_CURSOR => $this->addComplex(UIInventorySlotOffset::CURSOR, $window),
PlayerInventoryWindow::TYPE_CRAFTING => $this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $window),
default => throw new AssumptionFailedError("Unknown permanent window type " . $window->getType())
};
}
$this->player->getInventory()->getHeldItemIndexChangeListeners()->add($this->syncSelectedHotbarSlot(...));
$this->player->getHotbar()->getSelectedIndexChangeListeners()->add($this->syncSelectedHotbarSlot(...));
}
private function associateIdWithInventory(int $id, Inventory $inventory) : void{
$this->networkIdToInventoryMap[$id] = $inventory;
private function associateIdWithInventory(int $id, InventoryWindow $window) : void{
$this->networkIdToWindowMap[$id] = $window;
}
private function getNewWindowId() : int{
$this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % ContainerIds::LAST);
return $this->lastInventoryNetworkId;
$this->lastWindowNetworkId = max(ContainerIds::FIRST, ($this->lastWindowNetworkId + 1) % ContainerIds::LAST);
return $this->lastWindowNetworkId;
}
private function add(int $id, Inventory $inventory) : void{
if(isset($this->inventories[spl_object_id($inventory)])){
throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
private function getEntry(Inventory $inventory) : ?InventoryManagerEntry{
return $this->entries[spl_object_id($inventory)] ?? null;
}
private function getEntryByWindow(InventoryWindow $window) : ?InventoryManagerEntry{
return $this->getEntry($window->getInventory());
}
public function getInventoryWindow(Inventory $inventory) : ?InventoryWindow{
return $this->getEntry($inventory)?->window;
}
private function add(int $id, InventoryWindow $window) : void{
$k = spl_object_id($window->getInventory());
if(isset($this->entries[$k])){
throw new \InvalidArgumentException("Inventory " . get_class($window->getInventory()) . " is already tracked (open in two different windows?)");
}
$this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry($inventory);
$this->associateIdWithInventory($id, $inventory);
$this->entries[$k] = new InventoryManagerEntry($window);
$window->getInventory()->getListeners()->add($this);
$this->associateIdWithInventory($id, $window);
}
private function addDynamic(Inventory $inventory) : int{
private function addDynamic(InventoryWindow $inventory) : int{
$id = $this->getNewWindowId();
$this->add($id, $inventory);
return $id;
@ -163,17 +185,19 @@ class InventoryManager{
* @param int[]|int $slotMap
* @phpstan-param array<int, int>|int $slotMap
*/
private function addComplex(array|int $slotMap, Inventory $inventory) : void{
if(isset($this->inventories[spl_object_id($inventory)])){
throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
private function addComplex(array|int $slotMap, InventoryWindow $window) : void{
$k = spl_object_id($window->getInventory());
if(isset($this->entries[$k])){
throw new \InvalidArgumentException("Inventory " . get_class($window) . " is already tracked");
}
$complexSlotMap = new ComplexInventoryMapEntry($inventory, is_int($slotMap) ? [$slotMap => 0] : $slotMap);
$this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry(
$inventory,
$complexSlotMap = new ComplexWindowMapEntry($window, is_int($slotMap) ? [$slotMap => 0] : $slotMap);
$this->entries[$k] = new InventoryManagerEntry(
$window,
$complexSlotMap
);
$window->getInventory()->getListeners()->add($this);
foreach($complexSlotMap->getSlotMap() as $netSlot => $coreSlot){
$this->complexSlotToInventoryMap[$netSlot] = $complexSlotMap;
$this->complexSlotToWindowMap[$netSlot] = $complexSlotMap;
}
}
@ -181,7 +205,7 @@ class InventoryManager{
* @param int[]|int $slotMap
* @phpstan-param array<int, int>|int $slotMap
*/
private function addComplexDynamic(array|int $slotMap, Inventory $inventory) : int{
private function addComplexDynamic(array|int $slotMap, InventoryWindow $inventory) : int{
$this->addComplex($slotMap, $inventory);
$id = $this->getNewWindowId();
$this->associateIdWithInventory($id, $inventory);
@ -189,54 +213,58 @@ class InventoryManager{
}
private function remove(int $id) : void{
$inventory = $this->networkIdToInventoryMap[$id];
unset($this->networkIdToInventoryMap[$id]);
if($this->getWindowId($inventory) === null){
unset($this->inventories[spl_object_id($inventory)]);
foreach($this->complexSlotToInventoryMap as $netSlot => $entry){
if($entry->getInventory() === $inventory){
unset($this->complexSlotToInventoryMap[$netSlot]);
$window = $this->networkIdToWindowMap[$id];
$inventory = $window->getInventory();
unset($this->networkIdToWindowMap[$id]);
if($this->getWindowId($window) === null){
$inventory->getListeners()->remove($this);
unset($this->entries[spl_object_id($inventory)]);
foreach($this->complexSlotToWindowMap as $netSlot => $entry){
if($entry->getWindow() === $window){
unset($this->complexSlotToWindowMap[$netSlot]);
}
}
}
}
public function getWindowId(Inventory $inventory) : ?int{
return ($id = array_search($inventory, $this->networkIdToInventoryMap, true)) !== false ? $id : null;
public function getWindowId(InventoryWindow $window) : ?int{
return ($id = array_search($window, $this->networkIdToWindowMap, true)) !== false ? $id : null;
}
public function getCurrentWindowId() : int{
return $this->lastInventoryNetworkId;
return $this->lastWindowNetworkId;
}
/**
* @phpstan-return array{Inventory, int}|null
* @phpstan-return array{InventoryWindow, int}|null
*/
public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{
if($windowId === ContainerIds::UI){
$entry = $this->complexSlotToInventoryMap[$netSlotId] ?? null;
$entry = $this->complexSlotToWindowMap[$netSlotId] ?? null;
if($entry === null){
return null;
}
$inventory = $entry->getInventory();
$window = $entry->getWindow();
$coreSlotId = $entry->mapNetToCore($netSlotId);
return $coreSlotId !== null && $inventory->slotExists($coreSlotId) ? [$inventory, $coreSlotId] : null;
return $coreSlotId !== null && $window->getInventory()->slotExists($coreSlotId) ? [$window, $coreSlotId] : null;
}
$inventory = $this->networkIdToInventoryMap[$windowId] ?? null;
if($inventory !== null && $inventory->slotExists($netSlotId)){
return [$inventory, $netSlotId];
$window = $this->networkIdToWindowMap[$windowId] ?? null;
if($window !== null && $window->getInventory()->slotExists($netSlotId)){
return [$window, $netSlotId];
}
return null;
}
private function addPredictedSlotChangeInternal(Inventory $inventory, int $slot, ItemStack $item) : void{
$this->inventories[spl_object_id($inventory)]->predictions[$slot] = $item;
private function addPredictedSlotChangeInternal(InventoryWindow $window, int $slot, ItemStack $item) : void{
//TODO: does this need a null check?
$entry = $this->getEntryByWindow($window) ?? throw new AssumptionFailedError("Assume this should never be null");
$entry->predictions[$slot] = $item;
}
public function addPredictedSlotChange(Inventory $inventory, int $slot, Item $item) : void{
public function addPredictedSlotChange(InventoryWindow $window, int $slot, Item $item) : void{
$typeConverter = $this->session->getTypeConverter();
$itemStack = $typeConverter->coreItemStackToNet($item);
$this->addPredictedSlotChangeInternal($inventory, $slot, $itemStack);
$this->addPredictedSlotChangeInternal($window, $slot, $itemStack);
}
public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
@ -244,7 +272,7 @@ class InventoryManager{
if($action instanceof SlotChangeAction){
//TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
$this->addPredictedSlotChange(
$action->getInventory(),
$action->getInventoryWindow(),
$action->getSlot(),
$action->getTargetItem()
);
@ -275,8 +303,8 @@ class InventoryManager{
continue;
}
[$inventory, $slot] = $info;
$this->addPredictedSlotChangeInternal($inventory, $slot, $action->newItem->getItemStack());
[$window, $slot] = $info;
$this->addPredictedSlotChangeInternal($window, $slot, $action->newItem->getItemStack());
}
}
@ -311,32 +339,32 @@ class InventoryManager{
* @return int[]|null
* @phpstan-return array<int, int>|null
*/
private function createComplexSlotMapping(Inventory $inventory) : ?array{
private function createComplexSlotMapping(InventoryWindow $inventory) : ?array{
//TODO: make this dynamic so plugins can add mappings for stuff not implemented by PM
return match(true){
$inventory instanceof AnvilInventory => UIInventorySlotOffset::ANVIL,
$inventory instanceof EnchantInventory => UIInventorySlotOffset::ENCHANTING_TABLE,
$inventory instanceof LoomInventory => UIInventorySlotOffset::LOOM,
$inventory instanceof StonecutterInventory => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventory::SLOT_INPUT],
$inventory instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT,
$inventory instanceof CartographyTableInventory => UIInventorySlotOffset::CARTOGRAPHY_TABLE,
$inventory instanceof SmithingTableInventory => UIInventorySlotOffset::SMITHING_TABLE,
$inventory instanceof AnvilInventoryWindow => UIInventorySlotOffset::ANVIL,
$inventory instanceof EnchantingTableInventoryWindow => UIInventorySlotOffset::ENCHANTING_TABLE,
$inventory instanceof LoomInventoryWindow => UIInventorySlotOffset::LOOM,
$inventory instanceof StonecutterInventoryWindow => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventoryWindow::SLOT_INPUT],
$inventory instanceof CraftingTableInventoryWindow => UIInventorySlotOffset::CRAFTING3X3_INPUT,
$inventory instanceof CartographyTableInventoryWindow => UIInventorySlotOffset::CARTOGRAPHY_TABLE,
$inventory instanceof SmithingTableInventoryWindow => UIInventorySlotOffset::SMITHING_TABLE,
default => null,
};
}
public function onCurrentWindowChange(Inventory $inventory) : void{
public function onCurrentWindowChange(InventoryWindow $window) : void{
$this->onCurrentWindowRemove();
$this->openWindowDeferred(function() use ($inventory) : void{
if(($slotMap = $this->createComplexSlotMapping($inventory)) !== null){
$windowId = $this->addComplexDynamic($slotMap, $inventory);
$this->openWindowDeferred(function() use ($window) : void{
if(($slotMap = $this->createComplexSlotMapping($window)) !== null){
$windowId = $this->addComplexDynamic($slotMap, $window);
}else{
$windowId = $this->addDynamic($inventory);
$windowId = $this->addDynamic($window);
}
foreach($this->containerOpenCallbacks as $callback){
$pks = $callback($windowId, $inventory);
$pks = $callback($windowId, $window);
if($pks !== null){
$windowType = null;
foreach($pks as $pk){
@ -347,7 +375,7 @@ class InventoryManager{
$this->session->sendDataPacket($pk);
}
$this->currentWindowType = $windowType ?? WindowTypes::CONTAINER;
$this->syncContents($inventory);
$this->syncContents($window);
return;
}
}
@ -362,27 +390,27 @@ class InventoryManager{
* @return ClientboundPacket[]|null
* @phpstan-return list<ClientboundPacket>|null
*/
protected static function createContainerOpen(int $id, Inventory $inv) : ?array{
protected static function createContainerOpen(int $id, InventoryWindow $window) : ?array{
//TODO: we should be using some kind of tagging system to identify the types. Instanceof is flaky especially
//if the class isn't final, not to mention being inflexible.
if($inv instanceof BlockInventory){
$blockPosition = BlockPosition::fromVector3($inv->getHolder());
if($window instanceof BlockInventoryWindow){
$blockPosition = BlockPosition::fromVector3($window->getHolder());
$windowType = match(true){
$inv instanceof LoomInventory => WindowTypes::LOOM,
$inv instanceof FurnaceInventory => match($inv->getFurnaceType()){
$window instanceof LoomInventoryWindow => WindowTypes::LOOM,
$window instanceof FurnaceInventoryWindow => match($window->getFurnaceType()){
FurnaceType::FURNACE => WindowTypes::FURNACE,
FurnaceType::BLAST_FURNACE => WindowTypes::BLAST_FURNACE,
FurnaceType::SMOKER => WindowTypes::SMOKER,
FurnaceType::CAMPFIRE, FurnaceType::SOUL_CAMPFIRE => throw new \LogicException("Campfire inventory cannot be displayed to a player")
},
$inv instanceof EnchantInventory => WindowTypes::ENCHANTMENT,
$inv instanceof BrewingStandInventory => WindowTypes::BREWING_STAND,
$inv instanceof AnvilInventory => WindowTypes::ANVIL,
$inv instanceof HopperInventory => WindowTypes::HOPPER,
$inv instanceof CraftingTableInventory => WindowTypes::WORKBENCH,
$inv instanceof StonecutterInventory => WindowTypes::STONECUTTER,
$inv instanceof CartographyTableInventory => WindowTypes::CARTOGRAPHY,
$inv instanceof SmithingTableInventory => WindowTypes::SMITHING_TABLE,
$window instanceof EnchantingTableInventoryWindow => WindowTypes::ENCHANTMENT,
$window instanceof BrewingStandInventoryWindow => WindowTypes::BREWING_STAND,
$window instanceof AnvilInventoryWindow => WindowTypes::ANVIL,
$window instanceof HopperInventoryWindow => WindowTypes::HOPPER,
$window instanceof CraftingTableInventoryWindow => WindowTypes::WORKBENCH,
$window instanceof StonecutterInventoryWindow => WindowTypes::STONECUTTER,
$window instanceof CartographyTableInventoryWindow => WindowTypes::CARTOGRAPHY,
$window instanceof SmithingTableInventoryWindow => WindowTypes::SMITHING_TABLE,
default => WindowTypes::CONTAINER
};
return [ContainerOpenPacket::blockInv($id, $windowType, $blockPosition)];
@ -395,7 +423,8 @@ class InventoryManager{
$this->openWindowDeferred(function() : void{
$windowId = $this->getNewWindowId();
$this->associateIdWithInventory($windowId, $this->player->getInventory());
$window = $this->getInventoryWindow($this->player->getInventory()) ?? throw new AssumptionFailedError("This should never be null");
$this->associateIdWithInventory($windowId, $window);
$this->currentWindowType = WindowTypes::INVENTORY;
$this->session->sendDataPacket(ContainerOpenPacket::entityInv(
@ -407,25 +436,25 @@ class InventoryManager{
}
public function onCurrentWindowRemove() : void{
if(isset($this->networkIdToInventoryMap[$this->lastInventoryNetworkId])){
$this->remove($this->lastInventoryNetworkId);
$this->session->sendDataPacket(ContainerClosePacket::create($this->lastInventoryNetworkId, $this->currentWindowType, true));
if(isset($this->networkIdToWindowMap[$this->lastWindowNetworkId])){
$this->remove($this->lastWindowNetworkId);
$this->session->sendDataPacket(ContainerClosePacket::create($this->lastWindowNetworkId, $this->currentWindowType, true));
if($this->pendingCloseWindowId !== null){
throw new AssumptionFailedError("We should not have opened a new window while a window was waiting to be closed");
}
$this->pendingCloseWindowId = $this->lastInventoryNetworkId;
$this->pendingCloseWindowId = $this->lastWindowNetworkId;
$this->enchantingTableOptions = [];
}
}
public function onClientRemoveWindow(int $id) : void{
if($id === $this->lastInventoryNetworkId){
if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){
if($id === $this->lastWindowNetworkId){
if(isset($this->networkIdToWindowMap[$id]) && $id !== $this->pendingCloseWindowId){
$this->remove($id);
$this->player->removeCurrentWindow();
}
}else{
$this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastInventoryNetworkId");
$this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastWindowNetworkId");
}
//Always send this, even if no window matches. If we told the client to close a window, it will behave as if it
@ -477,14 +506,25 @@ class InventoryManager{
$this->itemStackExtraDataEqual($left, $right);
}
public function onSlotChange(Inventory $inventory, int $slot) : void{
$inventoryEntry = $this->inventories[spl_object_id($inventory)] ?? null;
public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem) : void{
$window = $this->getInventoryWindow($inventory);
if($window === null){
//this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
//is cleared before removal.
return;
}
$this->requestSyncSlot($window, $slot);
}
public function requestSyncSlot(InventoryWindow $window, int $slot) : void{
$inventoryEntry = $this->getEntryByWindow($window);
if($inventoryEntry === null){
//this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
//is cleared before removal.
return;
}
$currentItem = $this->session->getTypeConverter()->coreItemStackToNet($inventory->getItem($slot));
$currentItem = $this->session->getTypeConverter()->coreItemStackToNet($window->getInventory()->getItem($slot));
$clientSideItem = $inventoryEntry->predictions[$slot] ?? null;
if($clientSideItem === null || !$this->itemStacksEqual($currentItem, $clientSideItem)){
//no prediction or incorrect - do not associate this with the currently active itemstack request
@ -511,7 +551,7 @@ class InventoryManager{
$this->session->sendDataPacket(InventorySlotPacket::create(
$windowId,
$netSlot,
new FullContainerName($this->lastInventoryNetworkId),
new FullContainerName($this->lastWindowNetworkId),
new ItemStackWrapper(0, ItemStack::null()),
new ItemStackWrapper(0, ItemStack::null())
));
@ -520,7 +560,7 @@ class InventoryManager{
$this->session->sendDataPacket(InventorySlotPacket::create(
$windowId,
$netSlot,
new FullContainerName($this->lastInventoryNetworkId),
new FullContainerName($this->lastWindowNetworkId),
new ItemStackWrapper(0, ItemStack::null()),
$itemStackWrapper
));
@ -541,18 +581,15 @@ class InventoryManager{
$this->session->sendDataPacket(InventoryContentPacket::create(
$windowId,
array_fill_keys(array_keys($itemStackWrappers), new ItemStackWrapper(0, ItemStack::null())),
new FullContainerName($this->lastInventoryNetworkId),
new FullContainerName($this->lastWindowNetworkId),
new ItemStackWrapper(0, ItemStack::null())
));
//now send the real contents
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, $itemStackWrappers, new FullContainerName($this->lastInventoryNetworkId), new ItemStackWrapper(0, ItemStack::null())));
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, $itemStackWrappers, new FullContainerName($this->lastWindowNetworkId), new ItemStackWrapper(0, ItemStack::null())));
}
public function syncSlot(Inventory $inventory, int $slot, ItemStack $itemStack) : void{
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
if($entry === null){
throw new \LogicException("Cannot sync an untracked inventory");
}
private function syncSlot(InventoryWindow $window, int $slot, ItemStack $itemStack) : void{
$entry = $this->getEntryByWindow($window) ?? throw new \LogicException("Cannot sync an untracked inventory");
$itemStackInfo = $entry->itemStackInfos[$slot];
if($itemStackInfo === null){
throw new \LogicException("Cannot sync an untracked inventory slot");
@ -561,7 +598,7 @@ class InventoryManager{
$windowId = ContainerIds::UI;
$netSlot = $entry->complexSlotMap->mapCoreToNet($slot) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
}else{
$windowId = $this->getWindowId($inventory) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
$windowId = $this->getWindowId($window) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
$netSlot = $slot;
}
@ -579,8 +616,17 @@ class InventoryManager{
unset($entry->predictions[$slot], $entry->pendingSyncs[$slot]);
}
public function syncContents(Inventory $inventory) : void{
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
public function onContentChange(Inventory $inventory, array $oldContents) : void{
//this can be null when an inventory changed during InventoryCloseEvent, or when a temporary inventory
//is cleared before removal.
$window = $this->getInventoryWindow($inventory);
if($window !== null){
$this->syncContents($window);
}
}
private function syncContents(InventoryWindow $window) : void{
$entry = $this->getEntryByWindow($window);
if($entry === null){
//this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
//is cleared before removal.
@ -589,14 +635,14 @@ class InventoryManager{
if($entry->complexSlotMap !== null){
$windowId = ContainerIds::UI;
}else{
$windowId = $this->getWindowId($inventory);
$windowId = $this->getWindowId($window);
}
if($windowId !== null){
$entry->predictions = [];
$entry->pendingSyncs = [];
$contents = [];
$typeConverter = $this->session->getTypeConverter();
foreach($inventory->getContents(true) as $slot => $item){
foreach($window->getInventory()->getContents(true) as $slot => $item){
$itemStack = $typeConverter->coreItemStackToNet($item);
$info = $this->trackItemStack($entry, $slot, $itemStack, null);
$contents[] = new ItemStackWrapper($info->getStackId(), $itemStack);
@ -616,8 +662,8 @@ class InventoryManager{
}
public function syncAll() : void{
foreach($this->inventories as $entry){
$this->syncContents($entry->inventory);
foreach($this->entries as $entry){
$this->syncContents($entry->window);
}
}
@ -627,8 +673,8 @@ class InventoryManager{
public function syncMismatchedPredictedSlotChanges() : void{
$typeConverter = $this->session->getTypeConverter();
foreach($this->inventories as $entry){
$inventory = $entry->inventory;
foreach($this->entries as $entry){
$inventory = $entry->window->getInventory();
foreach($entry->predictions as $slot => $expectedItem){
if(!$inventory->slotExists($slot) || $entry->itemStackInfos[$slot] === null){
continue; //TODO: size desync ???
@ -646,14 +692,14 @@ class InventoryManager{
public function flushPendingUpdates() : void{
if($this->fullSyncRequested){
$this->fullSyncRequested = false;
$this->session->getLogger()->debug("Full inventory sync requested, sending contents of " . count($this->inventories) . " inventories");
$this->session->getLogger()->debug("Full inventory sync requested, sending contents of " . count($this->entries) . " inventories");
$this->syncAll();
}else{
foreach($this->inventories as $entry){
foreach($this->entries as $entry){
if(count($entry->pendingSyncs) === 0){
continue;
}
$inventory = $entry->inventory;
$inventory = $entry->window;
$this->session->getLogger()->debug("Syncing slots " . implode(", ", array_keys($entry->pendingSyncs)) . " in inventory " . get_class($inventory) . "#" . spl_object_id($inventory));
foreach($entry->pendingSyncs as $slot => $itemStack){
$this->syncSlot($inventory, $slot, $itemStack);
@ -664,7 +710,13 @@ class InventoryManager{
}
public function syncData(Inventory $inventory, int $propertyId, int $value) : void{
$windowId = $this->getWindowId($inventory);
//TODO: the handling of this data has always kinda sucked. Probably ought to route it through InventoryWindow
//somehow, but I'm not sure exactly how that should look.
$window = $this->getInventoryWindow($inventory);
if($window === null){
return;
}
$windowId = $this->getWindowId($window);
if($windowId !== null){
$this->session->sendDataPacket(ContainerSetDataPacket::create($windowId, $propertyId, $value));
}
@ -676,12 +728,9 @@ class InventoryManager{
public function syncSelectedHotbarSlot() : void{
$playerInventory = $this->player->getInventory();
$selected = $playerInventory->getHeldItemIndex();
$selected = $this->player->getHotbar()->getSelectedIndex();
if($selected !== $this->clientSelectedHotbarSlot){
$inventoryEntry = $this->inventories[spl_object_id($playerInventory)] ?? null;
if($inventoryEntry === null){
throw new AssumptionFailedError("Player inventory should always be tracked");
}
$inventoryEntry = $this->getEntry($playerInventory) ?? throw new AssumptionFailedError("Player inventory should always be tracked");
$itemStackInfo = $inventoryEntry->itemStackInfos[$selected] ?? null;
if($itemStackInfo === null){
throw new AssumptionFailedError("Untracked player inventory slot $selected");
@ -689,7 +738,7 @@ class InventoryManager{
$this->session->sendDataPacket(MobEquipmentPacket::create(
$this->player->getId(),
new ItemStackWrapper($itemStackInfo->getStackId(), $this->session->getTypeConverter()->coreItemStackToNet($playerInventory->getItemInHand())),
new ItemStackWrapper($itemStackInfo->getStackId(), $this->session->getTypeConverter()->coreItemStackToNet($playerInventory->getItem($selected))),
$selected,
$selected,
ContainerIds::INVENTORY
@ -740,9 +789,8 @@ class InventoryManager{
return $this->nextItemStackId++;
}
public function getItemStackInfo(Inventory $inventory, int $slot) : ?ItemStackInfo{
$entry = $this->inventories[spl_object_id($inventory)] ?? null;
return $entry?->itemStackInfos[$slot] ?? null;
public function getItemStackInfo(InventoryWindow $window, int $slot) : ?ItemStackInfo{
return $this->getEntryByWindow($window)?->itemStackInfos[$slot] ?? null;
}
private function trackItemStack(InventoryManagerEntry $entry, int $slotId, ItemStack $itemStack, ?int $itemStackRequestId) : ItemStackInfo{

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

@ -102,22 +102,21 @@ final class StandardEntityEventBroadcaster implements EntityEventBroadcaster{
}
public function onMobMainHandItemChange(array $recipients, Human $mob) : void{
//TODO: we could send zero for slot here because remote players don't need to know which slot was selected
$inv = $mob->getInventory();
$item = $mob->getMainHandItem();
$this->sendDataPacket($recipients, MobEquipmentPacket::create(
$mob->getId(),
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($inv->getItemInHand())),
$inv->getHeldItemIndex(),
$inv->getHeldItemIndex(),
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($item)),
0,
0,
ContainerIds::INVENTORY
));
}
public function onMobOffHandItemChange(array $recipients, Human $mob) : void{
$inv = $mob->getOffHandInventory();
$item = $mob->getOffHandItem();
$this->sendDataPacket($recipients, MobEquipmentPacket::create(
$mob->getId(),
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($inv->getItem(0))),
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($item)),
0,
0,
ContainerIds::OFFHAND

View File

@ -307,11 +307,11 @@ class InGamePacketHandler extends PacketHandler{
switch($packet->eventId){
case ActorEvent::EATING_ITEM: //TODO: ignore this and handle it server-side
$item = $this->player->getInventory()->getItemInHand();
$item = $this->player->getMainHandItem();
if($item->isNull()){
return false;
}
$this->player->broadcastAnimation(new ConsumingItemAnimation($this->player, $this->player->getInventory()->getItemInHand()));
$this->player->broadcastAnimation(new ConsumingItemAnimation($this->player, $item));
break;
default:
return false;
@ -358,7 +358,7 @@ class InGamePacketHandler extends PacketHandler{
[$windowId, $slot] = ItemStackContainerIdTranslator::translate($containerInfo->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $netSlot);
$inventoryAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slot);
if($inventoryAndSlot !== null){ //trigger the normal slot sync logic
$this->inventoryManager->onSlotChange($inventoryAndSlot[0], $inventoryAndSlot[1]);
$this->inventoryManager->requestSyncSlot($inventoryAndSlot[0], $inventoryAndSlot[1]);
}
}
}
@ -464,7 +464,8 @@ class InGamePacketHandler extends PacketHandler{
$droppedItem = $sourceSlotItem->pop($droppedCount);
$builder = new TransactionBuilder();
$builder->getInventory($inventory)->setItem($sourceSlot, $sourceSlotItem);
$window = $this->inventoryManager->getInventoryWindow($inventory) ?? throw new AssumptionFailedError("This should never happen");
$builder->getActionBuilder($window)->setItem($sourceSlot, $sourceSlotItem);
$builder->addAction(new DropItemAction($droppedItem));
$transaction = new InventoryTransaction($this->player, $builder->generateActions());

View File

@ -23,16 +23,15 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\inventory\Inventory;
use pocketmine\block\inventory\window\EnchantingTableInventoryWindow;
use pocketmine\inventory\transaction\action\CreateItemAction;
use pocketmine\inventory\transaction\action\DestroyItemAction;
use pocketmine\inventory\transaction\action\DropItemAction;
use pocketmine\inventory\transaction\CraftingTransaction;
use pocketmine\inventory\transaction\EnchantingTransaction;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\inventory\transaction\SlotChangeActionBuilder;
use pocketmine\inventory\transaction\TransactionBuilder;
use pocketmine\inventory\transaction\TransactionBuilderInventory;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use pocketmine\network\mcpe\cache\CraftingDataCache;
@ -56,6 +55,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\SwapStackReque
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\TakeStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
@ -86,25 +86,22 @@ class ItemStackRequestExecutor{
$this->builder = new TransactionBuilder();
}
protected function prettyInventoryAndSlot(Inventory $inventory, int $slot) : string{
if($inventory instanceof TransactionBuilderInventory){
$inventory = $inventory->getActualInventory();
}
protected function prettyWindowAndSlot(InventoryWindow $inventory, int $slot) : string{
return (new \ReflectionClass($inventory))->getShortName() . "#" . spl_object_id($inventory) . ", slot: $slot";
}
/**
* @throws ItemStackRequestProcessException
*/
private function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : void{
$info = $this->inventoryManager->getItemStackInfo($inventory, $slotId);
private function matchItemStack(InventoryWindow $window, int $slotId, int $clientItemStackId) : void{
$info = $this->inventoryManager->getItemStackInfo($window, $slotId);
if($info === null){
throw new AssumptionFailedError("The inventory is tracked and the slot is valid, so this should not be null");
}
if(!($clientItemStackId < 0 ? $info->getRequestId() === $clientItemStackId : $info->getStackId() === $clientItemStackId)){
throw new ItemStackRequestProcessException(
$this->prettyInventoryAndSlot($inventory, $slotId) . ": " .
$this->prettyWindowAndSlot($window, $slotId) . ": " .
"Mismatched expected itemstack, " .
"client expected: $clientItemStackId, server actual: " . $info->getStackId() . ", last modified by request: " . ($info->getRequestId() ?? "none")
);
@ -112,7 +109,7 @@ class ItemStackRequestExecutor{
}
/**
* @phpstan-return array{TransactionBuilderInventory, int}
* @phpstan-return array{SlotChangeActionBuilder, int}
*
* @throws ItemStackRequestProcessException
*/
@ -122,16 +119,17 @@ class ItemStackRequestExecutor{
if($windowAndSlot === null){
throw new ItemStackRequestProcessException("No open inventory matches container UI ID: " . $info->getContainerName()->getContainerId() . ", slot ID: " . $info->getSlotId());
}
[$inventory, $slot] = $windowAndSlot;
[$window, $slot] = $windowAndSlot;
$inventory = $window->getInventory();
if(!$inventory->slotExists($slot)){
throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyInventoryAndSlot($inventory, $slot));
throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyWindowAndSlot($window, $slot));
}
if($info->getStackId() !== $this->request->getRequestId()){ //the itemstack may have been modified by the current request
$this->matchItemStack($inventory, $slot, $info->getStackId());
$this->matchItemStack($window, $slot, $info->getStackId());
}
return [$this->builder->getInventory($inventory), $slot];
return [$this->builder->getActionBuilder($window), $slot];
}
/**
@ -156,12 +154,12 @@ class ItemStackRequestExecutor{
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
if($count < 1){
//this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Cannot take less than 1 items from a stack");
}
$existingItem = $inventory->getItem($slot);
if($existingItem->getCount() < $count){
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount());
throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount());
}
$removed = $existingItem->pop($count);
@ -179,12 +177,12 @@ class ItemStackRequestExecutor{
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
if($count < 1){
//this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Cannot take less than 1 items from a stack");
}
$existingItem = $inventory->getItem($slot);
if(!$existingItem->isNull() && !$existingItem->canStackWith($item)){
throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Can only add items to an empty slot, or a slot containing the same item");
throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Can only add items to an empty slot, or a slot containing the same item");
}
//we can't use the existing item here; it may be an empty stack
@ -342,7 +340,7 @@ class ItemStackRequestExecutor{
$this->setNextCreatedItem($item, true);
}elseif($action instanceof CraftRecipeStackRequestAction){
$window = $this->player->getCurrentWindow();
if($window instanceof EnchantInventory){
if($window instanceof EnchantingTableInventoryWindow){
$optionId = $this->inventoryManager->getEnchantingTableOptionIndex($action->getRecipeId());
if($optionId !== null && ($option = $window->getOption($optionId)) !== null){
$this->specialTransaction = new EnchantingTransaction($this->player, $option, $optionId + 1);
@ -375,7 +373,8 @@ class ItemStackRequestExecutor{
$predictedDamage = $action->getPredictedDurability();
if($usedItem instanceof Durable && $predictedDamage >= 0 && $predictedDamage <= $usedItem->getMaxDurability()){
$usedItem->setDamage($predictedDamage);
$this->inventoryManager->addPredictedSlotChange($inventory, $slot, $usedItem);
$inventoryWindow = $this->inventoryManager->getInventoryWindow($inventory) ?? throw new AssumptionFailedError("The player's inventory should always have an inventory window");
$this->inventoryManager->addPredictedSlotChange($inventoryWindow, $slot, $usedItem);
}
}else{
throw new ItemStackRequestProcessException("Unhandled item stack request action");

View File

@ -31,6 +31,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\FullContainerName;
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponseContainerInfo;
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponseSlotInfo;
use pocketmine\player\InventoryWindow;
use pocketmine\utils\AssumptionFailedError;
final class ItemStackResponseBuilder{
@ -51,20 +52,11 @@ final class ItemStackResponseBuilder{
}
/**
* @phpstan-return array{Inventory, int}
* @phpstan-return array{InventoryWindow, int}
*/
private function getInventoryAndSlot(int $containerInterfaceId, int $slotId) : ?array{
private function locateWindowAndSlot(int $containerInterfaceId, int $slotId) : ?array{
[$windowId, $slotId] = ItemStackContainerIdTranslator::translate($containerInterfaceId, $this->inventoryManager->getCurrentWindowId(), $slotId);
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
if($windowAndSlot === null){
return null;
}
[$inventory, $slot] = $windowAndSlot;
if(!$inventory->slotExists($slot)){
return null;
}
return [$inventory, $slot];
return $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
}
public function build() : ItemStackResponse{
@ -74,18 +66,18 @@ final class ItemStackResponseBuilder{
continue;
}
foreach($slotIds as $slotId){
$inventoryAndSlot = $this->getInventoryAndSlot($containerInterfaceId, $slotId);
if($inventoryAndSlot === null){
$windowAndSlot = $this->locateWindowAndSlot($containerInterfaceId, $slotId);
if($windowAndSlot === null){
//a plugin may have closed the inventory during an event, or the slot may have been invalid
continue;
}
[$inventory, $slot] = $inventoryAndSlot;
[$window, $slot] = $windowAndSlot;
$itemStackInfo = $this->inventoryManager->getItemStackInfo($inventory, $slot);
$itemStackInfo = $this->inventoryManager->getItemStackInfo($window, $slot);
if($itemStackInfo === null){
throw new AssumptionFailedError("ItemStackInfo should never be null for an open inventory");
}
$item = $inventory->getItem($slot);
$item = $window->getInventory()->getItem($slot);
$responseInfosByContainer[$containerInterfaceId][] = new ItemStackResponseSlotInfo(
$slotId,

View File

@ -21,17 +21,30 @@
declare(strict_types=1);
namespace pocketmine\inventory;
namespace pocketmine\player;
use pocketmine\entity\Human;
use pocketmine\inventory\Inventory;
abstract class InventoryWindow{
final class PlayerEnderInventory extends SimpleInventory{
public function __construct(
private Human $holder,
int $size = 27
){
parent::__construct($size);
protected Player $viewer,
protected Inventory $inventory
){}
public function getViewer() : Player{
return $this->viewer;
}
public function getHolder() : Human{ return $this->holder; }
public function getInventory() : Inventory{
return $this->inventory;
}
public function onOpen() : void{
$this->inventory->onOpen($this->viewer);
}
public function onClose() : void{
$this->inventory->onClose($this->viewer);
}
}

View File

@ -87,9 +87,7 @@ use pocketmine\form\FormValidationException;
use pocketmine\inventory\CallbackInventoryListener;
use pocketmine\inventory\CreativeInventory;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\PlayerCraftingInventory;
use pocketmine\inventory\PlayerCursorInventory;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\transaction\action\DropItemAction;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\inventory\transaction\TransactionBuilder;
@ -229,11 +227,11 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
protected bool $authenticated;
protected PlayerInfo $playerInfo;
protected ?Inventory $currentWindow = null;
/** @var Inventory[] */
protected ?InventoryWindow $currentWindow = null;
/** @var PlayerInventoryWindow[] */
protected array $permanentWindows = [];
protected PlayerCursorInventory $cursorInventory;
protected PlayerCraftingInventory $craftingGrid;
protected Inventory $cursorInventory;
protected CraftingGrid $craftingGrid;
protected CreativeInventory $creativeInventory;
protected int $messageCounter = 2;
@ -362,7 +360,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
private function callDummyItemHeldEvent() : void{
$slot = $this->inventory->getHeldItemIndex();
$slot = $this->hotbar->getSelectedIndex();
$event = new PlayerItemHeldEvent($this, $this->inventory->getItem($slot), $slot);
$event->call();
@ -377,7 +375,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->inventory->getListeners()->add(new CallbackInventoryListener(
function(Inventory $unused, int $slot) : void{
if($slot === $this->inventory->getHeldItemIndex()){
if($slot === $this->hotbar->getSelectedIndex()){
$this->setUsingItem(false);
$this->callDummyItemHeldEvent();
@ -1626,10 +1624,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
public function selectHotbarSlot(int $hotbarSlot) : bool{
if(!$this->inventory->isHotbarSlot($hotbarSlot)){ //TODO: exception here?
if(!$this->hotbar->isHotbarSlot($hotbarSlot)){ //TODO: exception here?
return false;
}
if($hotbarSlot === $this->inventory->getHeldItemIndex()){
if($hotbarSlot === $this->hotbar->getSelectedIndex()){
return true;
}
@ -1639,7 +1637,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
return false;
}
$this->inventory->setHeldItemIndex($hotbarSlot);
$this->hotbar->setSelectedIndex($hotbarSlot);
$this->setUsingItem(false);
return true;
@ -1651,7 +1649,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
private function returnItemsFromAction(Item $oldHeldItem, Item $newHeldItem, array $extraReturnedItems) : void{
$heldItemChanged = false;
if(!$newHeldItem->equalsExact($oldHeldItem) && $oldHeldItem->equalsExact($this->inventory->getItemInHand())){
if(!$newHeldItem->equalsExact($oldHeldItem) && $oldHeldItem->equalsExact($this->getMainHandItem())){
//determine if the item was changed in some meaningful way, or just damaged/changed count
//if it was really changed we always need to set it, whether we have finite resources or not
$newReplica = clone $oldHeldItem;
@ -1668,7 +1666,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if($newHeldItem instanceof Durable && $newHeldItem->isBroken()){
$this->broadcastSound(new ItemBreakSound());
}
$this->inventory->setItemInHand($newHeldItem);
$this->setMainHandItem($newHeldItem);
$heldItemChanged = true;
}
}
@ -1678,7 +1676,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
if($heldItemChanged && count($extraReturnedItems) > 0 && $newHeldItem->isNull()){
$this->inventory->setItemInHand(array_shift($extraReturnedItems));
$this->setMainHandItem(array_shift($extraReturnedItems));
}
foreach($this->inventory->addItem(...$extraReturnedItems) as $drop){
//TODO: we can't generate a transaction for this since the items aren't coming from an inventory :(
@ -1700,7 +1698,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
*/
public function useHeldItem() : bool{
$directionVector = $this->getDirectionVector();
$item = $this->inventory->getItemInHand();
$item = $this->getMainHandItem();
$oldItem = clone $item;
$ev = new PlayerItemUseEvent($this, $item, $directionVector);
@ -1734,7 +1732,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* @return bool if the consumption succeeded.
*/
public function consumeHeldItem() : bool{
$slot = $this->inventory->getItemInHand();
$slot = $this->getMainHandItem();
if($slot instanceof ConsumableItem){
$oldItem = clone $slot;
@ -1767,7 +1765,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
*/
public function releaseHeldItem() : bool{
try{
$item = $this->inventory->getItemInHand();
$item = $this->getMainHandItem();
if(!$this->isUsingItem() || $this->hasItemCooldown($item)){
return false;
}
@ -1837,21 +1835,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
private function equipOrAddPickedItem(int $existingSlot, Item $item) : void{
if($existingSlot !== -1){
if($existingSlot < $this->inventory->getHotbarSize()){
$this->inventory->setHeldItemIndex($existingSlot);
if($existingSlot < $this->hotbar->getSize()){
$this->hotbar->setSelectedIndex($existingSlot);
}else{
$this->inventory->swap($this->inventory->getHeldItemIndex(), $existingSlot);
$this->inventory->swap($this->hotbar->getSelectedIndex(), $existingSlot);
}
}else{
$firstEmpty = $this->inventory->firstEmpty();
if($firstEmpty === -1){ //full inventory
$this->inventory->setItemInHand($item);
}elseif($firstEmpty < $this->inventory->getHotbarSize()){
$this->setMainHandItem($item);
}elseif($firstEmpty < $this->hotbar->getSize()){
$this->inventory->setItem($firstEmpty, $item);
$this->inventory->setHeldItemIndex($firstEmpty);
$this->hotbar->setSelectedIndex($firstEmpty);
}else{
$this->inventory->swap($this->inventory->getHeldItemIndex(), $firstEmpty);
$this->inventory->setItemInHand($item);
$this->inventory->swap($this->hotbar->getSelectedIndex(), $firstEmpty);
$this->setMainHandItem($item);
}
}
}
@ -1868,7 +1866,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$target = $this->getWorld()->getBlock($pos);
$ev = new PlayerInteractEvent($this, $this->inventory->getItemInHand(), $target, null, $face, PlayerInteractEvent::LEFT_CLICK_BLOCK);
$ev = new PlayerInteractEvent($this, $this->getMainHandItem(), $target, null, $face, PlayerInteractEvent::LEFT_CLICK_BLOCK);
if($this->isSpectator()){
$ev->cancel();
}
@ -1877,7 +1875,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
return false;
}
$this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
if($target->onAttack($this->inventory->getItemInHand(), $face, $this)){
if($target->onAttack($this->getMainHandItem(), $face, $this)){
return true;
}
@ -1918,7 +1916,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){
$this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
$this->stopBreakBlock($pos);
$item = $this->inventory->getItemInHand();
$item = $this->getMainHandItem();
$oldItem = clone $item;
$returnedItems = [];
if($this->getWorld()->useBreakOn($pos, $item, $this, true, $returnedItems)){
@ -1943,7 +1941,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){
$this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers());
$item = $this->inventory->getItemInHand(); //this is a copy of the real item
$item = $this->getMainHandItem(); //this is a copy of the real item
$oldItem = clone $item;
$returnedItems = [];
if($this->getWorld()->useItemOn($pos, $item, $face, $clickOffset, $this, true, $returnedItems)){
@ -1972,7 +1970,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
return false;
}
$heldItem = $this->inventory->getItemInHand();
$heldItem = $this->getMainHandItem();
$oldItem = clone $heldItem;
$ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_ENTITY_ATTACK, $heldItem->getAttackPoints());
@ -2058,15 +2056,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$ev->call();
$item = $this->inventory->getItemInHand();
$item = $this->getMainHandItem();
$oldItem = clone $item;
if(!$ev->isCancelled()){
if($item->onInteractEntity($this, $entity, $clickPos)){
if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->inventory->getItemInHand())){
if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->getMainHandItem())){
if($item instanceof Durable && $item->isBroken()){
$this->broadcastSound(new ItemBreakSound());
}
$this->inventory->setItemInHand($item);
$this->setMainHandItem($item);
}
}
return $entity->onInteract($this, $clickPos);
@ -2407,7 +2405,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->loadQueue = [];
$this->removeCurrentWindow();
$this->removePermanentInventories();
$this->removePermanentWindows();
$this->perm->getPermissionRecalculationCallbacks()->clear();
@ -2423,8 +2421,6 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
protected function destroyCycles() : void{
$this->networkSession = null;
unset($this->cursorInventory);
unset($this->craftingGrid);
$this->spawnPosition = null;
$this->deathPosition = null;
$this->blockBreakHandler = null;
@ -2504,8 +2500,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->getWorld()->dropItem($this->location, $item);
}
$this->hotbar->setSelectedIndex(0);
$clearInventory = fn(Inventory $inventory) => $inventory->setContents(array_filter($inventory->getContents(), fn(Item $item) => $item->keepOnDeath()));
$this->inventory->setHeldItemIndex(0);
$clearInventory($this->inventory);
$clearInventory($this->armorInventory);
$clearInventory($this->offHandInventory);
@ -2714,15 +2710,19 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
protected function addDefaultWindows() : void{
$this->cursorInventory = new PlayerCursorInventory($this);
$this->craftingGrid = new PlayerCraftingInventory($this);
$this->cursorInventory = new SimpleInventory(1);
$this->craftingGrid = new CraftingGrid(CraftingGrid::SIZE_SMALL);
$this->addPermanentInventories($this->inventory, $this->armorInventory, $this->cursorInventory, $this->offHandInventory, $this->craftingGrid);
//TODO: more windows
$this->addPermanentWindows([
new PlayerInventoryWindow($this, $this->inventory, PlayerInventoryWindow::TYPE_INVENTORY),
new PlayerInventoryWindow($this, $this->armorInventory, PlayerInventoryWindow::TYPE_ARMOR),
new PlayerInventoryWindow($this, $this->cursorInventory, PlayerInventoryWindow::TYPE_CURSOR),
new PlayerInventoryWindow($this, $this->offHandInventory, PlayerInventoryWindow::TYPE_OFFHAND),
new PlayerInventoryWindow($this, $this->craftingGrid, PlayerInventoryWindow::TYPE_CRAFTING),
]);
}
public function getCursorInventory() : PlayerCursorInventory{
public function getCursorInventory() : Inventory{
return $this->cursorInventory;
}
@ -2753,22 +2753,37 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* inventory.
*/
private function doCloseInventory() : void{
$inventories = [$this->craftingGrid, $this->cursorInventory];
if($this->currentWindow instanceof TemporaryInventory){
$inventories[] = $this->currentWindow;
$windowsToClear = [];
$mainInventoryWindow = null;
foreach($this->permanentWindows as $window){
if($window->getType() === PlayerInventoryWindow::TYPE_CRAFTING || $window->getType() === PlayerInventoryWindow::TYPE_CURSOR){
$windowsToClear[] = $window;
}elseif($window->getType() === PlayerInventoryWindow::TYPE_INVENTORY){
$mainInventoryWindow = $window;
}
}
if($mainInventoryWindow === null){
//TODO: in the future this might not be the case, if we implement support for the player closing their
//inventory window outside the protocol layer
//in that case we'd have to create a new ephemeral window here
throw new AssumptionFailedError("This should never be null");
}
if($this->currentWindow instanceof TemporaryInventoryWindow){
$windowsToClear[] = $this->currentWindow;
}
$builder = new TransactionBuilder();
foreach($inventories as $inventory){
$contents = $inventory->getContents();
foreach($windowsToClear as $window){
$contents = $window->getInventory()->getContents();
if(count($contents) > 0){
$drops = $builder->getInventory($this->inventory)->addItem(...$contents);
$drops = $builder->getActionBuilder($mainInventoryWindow)->addItem(...$contents);
foreach($drops as $drop){
$builder->addAction(new DropItemAction($drop));
}
$builder->getInventory($inventory)->clearAll();
$builder->getActionBuilder($window)->clearAll();
}
}
@ -2780,8 +2795,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->logger->debug("Successfully evacuated items from temporary inventories");
}catch(TransactionCancelledException){
$this->logger->debug("Plugin cancelled transaction evacuating items from temporary inventories; items will be destroyed");
foreach($inventories as $inventory){
$inventory->clearAll();
foreach($windowsToClear as $window){
$window->getInventory()->clearAll();
}
}catch(TransactionValidationException $e){
throw new AssumptionFailedError("This server-generated transaction should never be invalid", 0, $e);
@ -2792,18 +2807,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
/**
* Returns the inventory the player is currently viewing. This might be a chest, furnace, or any other container.
*/
public function getCurrentWindow() : ?Inventory{
public function getCurrentWindow() : ?InventoryWindow{
return $this->currentWindow;
}
/**
* Opens an inventory window to the player. Returns if it was successful.
*/
public function setCurrentWindow(Inventory $inventory) : bool{
if($inventory === $this->currentWindow){
public function setCurrentWindow(InventoryWindow $window) : bool{
if($window === $this->currentWindow){
return true;
}
$ev = new InventoryOpenEvent($inventory, $this);
if($window->getViewer() !== $this){
throw new \InvalidArgumentException("Cannot reuse InventoryWindow instances, please create a new one for each player");
}
$ev = new InventoryOpenEvent($window, $this);
$ev->call();
if($ev->isCancelled()){
return false;
@ -2814,10 +2832,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
if(($inventoryManager = $this->getNetworkSession()->getInvManager()) === null){
throw new \InvalidArgumentException("Player cannot open inventories in this state");
}
$this->logger->debug("Opening inventory " . get_class($inventory) . "#" . spl_object_id($inventory));
$inventoryManager->onCurrentWindowChange($inventory);
$inventory->onOpen($this);
$this->currentWindow = $inventory;
$this->logger->debug("Opening inventory window " . get_class($window) . "#" . spl_object_id($window));
$inventoryManager->onCurrentWindowChange($window);
$window->onOpen();
$this->currentWindow = $window;
return true;
}
@ -2825,8 +2843,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->doCloseInventory();
if($this->currentWindow !== null){
$currentWindow = $this->currentWindow;
$this->logger->debug("Closing inventory " . get_class($this->currentWindow) . "#" . spl_object_id($this->currentWindow));
$this->currentWindow->onClose($this);
$this->logger->debug("Closing inventory window " . get_class($this->currentWindow) . "#" . spl_object_id($this->currentWindow));
$this->currentWindow->onClose();
if(($inventoryManager = $this->getNetworkSession()->getInvManager()) !== null){
$inventoryManager->onCurrentWindowRemove();
}
@ -2835,20 +2853,31 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
}
protected function addPermanentInventories(Inventory ...$inventories) : void{
foreach($inventories as $inventory){
$inventory->onOpen($this);
$this->permanentWindows[spl_object_id($inventory)] = $inventory;
/**
* @param PlayerInventoryWindow[] $windows
*/
protected function addPermanentWindows(array $windows) : void{
foreach($windows as $window){
$window->onOpen();
$this->permanentWindows[spl_object_id($window)] = $window;
}
}
protected function removePermanentInventories() : void{
foreach($this->permanentWindows as $inventory){
$inventory->onClose($this);
protected function removePermanentWindows() : void{
foreach($this->permanentWindows as $window){
$window->onClose();
}
$this->permanentWindows = [];
}
/**
* @return PlayerInventoryWindow[]
* @internal
*/
public function getPermanentWindows() : array{
return $this->permanentWindows;
}
/**
* Opens the player's sign editor GUI for the sign at the given position.
*/

View File

@ -0,0 +1,51 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\player;
use pocketmine\inventory\Inventory;
/**
* Window for player-owned inventories. The player can access these at all times.
*/
final class PlayerInventoryWindow extends InventoryWindow{
public const TYPE_INVENTORY = 0;
public const TYPE_OFFHAND = 1;
public const TYPE_ARMOR = 2;
public const TYPE_CURSOR = 3;
public const TYPE_CRAFTING = 4;
public function __construct(
Player $viewer,
Inventory $inventory,
private int $type
){
parent::__construct($viewer, $inventory);
}
/**
* Returns the type of player inventory in this window.
*/
public function getType() : int{ return $this->type; }
}

View File

@ -67,7 +67,7 @@ final class SurvivalBlockBreakHandler{
if(!$this->block->getBreakInfo()->isBreakable()){
return 0.0;
}
$breakTimePerTick = $this->block->getBreakInfo()->getBreakTime($this->player->getInventory()->getItemInHand()) * 20;
$breakTimePerTick = $this->block->getBreakInfo()->getBreakTime($this->player->getMainHandItem()) * 20;
if(!$this->player->isOnGround() && !$this->player->isFlying()){
$breakTimePerTick *= 5;
}

View File

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

View File

@ -0,0 +1,270 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\inventory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use pocketmine\item\Item;
use pocketmine\item\ItemTypeIds;
use pocketmine\item\VanillaItems;
use function array_filter;
final class CombinedInventoryProxyTest extends TestCase{
/**
* @return Inventory[]
* @phpstan-return list<Inventory>
*/
private function createInventories() : array{
$inventory1 = new SimpleInventory(1);
$inventory1->setItem(0, VanillaItems::APPLE());
$inventory2 = new SimpleInventory(1);
$inventory2->setItem(0, VanillaItems::PAPER());
$inventory3 = new SimpleInventory(2);
$inventory3->setItem(1, VanillaItems::BONE());
return [$inventory1, $inventory2, $inventory3];
}
/**
* @param Item[] $items
* @phpstan-param array<int, Item> $items
*/
private function verifyReadItems(array $items) : void{
self::assertSame(ItemTypeIds::APPLE, $items[0]->getTypeId());
self::assertSame(ItemTypeIds::PAPER, $items[1]->getTypeId());
self::assertTrue($items[2]->isNull());
self::assertSame(ItemTypeIds::BONE, $items[3]->getTypeId());
}
/**
* @return Item[]
* @phpstan-return list<Item>
*/
private static function getAltItems() : array{
return [
VanillaItems::AMETHYST_SHARD(),
VanillaItems::AIR(), //null item
VanillaItems::BLAZE_POWDER(),
VanillaItems::BRICK()
];
}
public function testGetItem() : void{
$inventory = new CombinedInventoryProxy($this->createInventories());
$this->verifyReadItems([
$inventory->getItem(0),
$inventory->getItem(1),
$inventory->getItem(2),
$inventory->getItem(3)
]);
$this->expectException(\InvalidArgumentException::class);
$inventory->getItem(4);
}
public function testGetContents() : void{
$inventory = new CombinedInventoryProxy($this->createInventories());
$this->verifyReadItems($inventory->getContents(includeEmpty: true));
$contentsWithoutEmpty = $inventory->getContents(includeEmpty: false);
self::assertFalse(isset($contentsWithoutEmpty[2]), "This index should not be set during this test");
self::assertCount(3, $contentsWithoutEmpty);
$this->verifyReadItems([
$contentsWithoutEmpty[0],
$contentsWithoutEmpty[1],
VanillaItems::AIR(),
$contentsWithoutEmpty[3]
]);
}
/**
* @param Inventory[] $backing
* @param Item[] $altItems
*
* @phpstan-param array<int, Inventory> $backing
* @phpstan-param array<int, Item> $altItems
*/
private function verifyWriteItems(array $backing, array $altItems) : void{
foreach([
0 => [$backing[0], 0],
1 => [$backing[1], 0],
2 => [$backing[2], 0],
3 => [$backing[2], 1]
] as $combinedSlot => [$backingInventory, $backingSlot]){
if(!isset($altItems[$combinedSlot])){
self::assertTrue($backingInventory->isSlotEmpty($backingSlot));
}else{
self::assertSame($altItems[$combinedSlot]->getTypeId(), $backingInventory->getItem($backingSlot)->getTypeId());
}
}
}
public function testSetItem() : void{
$backing = $this->createInventories();
$inventory = new CombinedInventoryProxy($backing);
$altItems = self::getAltItems();
foreach($altItems as $slot => $item){
$inventory->setItem($slot, $item);
}
$this->verifyWriteItems($backing, $altItems);
$this->expectException(\InvalidArgumentException::class);
$inventory->setItem(4, VanillaItems::BRICK());
}
/**
* @phpstan-return \Generator<int, array{array<int, Item>}, void, void>
*/
public static function setContentsProvider() : \Generator{
$altItems = self::getAltItems();
yield [$altItems];
yield [array_filter($altItems, fn(Item $item) => !$item->isNull())];
}
/**
* @param Item[] $altItems
* @phpstan-param array<int, Item> $altItems
*/
#[DataProvider("setContentsProvider")]
public function testSetContents(array $altItems) : void{
$backing = $this->createInventories();
$inventory = new CombinedInventoryProxy($backing);
$inventory->setContents($altItems);
$this->verifyWriteItems($backing, $altItems);
}
public function testGetSize() : void{
self::assertSame(4, (new CombinedInventoryProxy($this->createInventories()))->getSize());
}
public function testGetMatchingItemCount() : void{
$inventory = new CombinedInventoryProxy($this->createInventories());
//we don't need to test the base functionality, only ensure that the correct delegate is called
self::assertSame(1, $inventory->getMatchingItemCount(3, VanillaItems::BONE(), true));
self::assertNotSame(1, $inventory->getMatchingItemCount(3, VanillaItems::PAPER(), true));
}
public function testIsSlotEmpty() : void{
$inventory = new CombinedInventoryProxy($this->createInventories());
self::assertTrue($inventory->isSlotEmpty(2));
self::assertFalse($inventory->isSlotEmpty(0));
self::assertFalse($inventory->isSlotEmpty(1));
self::assertFalse($inventory->isSlotEmpty(3));
}
public function testListenersOnProxySlotUpdate() : void{
$inventory = new CombinedInventoryProxy($this->createInventories());
$numChanges = 0;
$inventory->getListeners()->add(new CallbackInventoryListener(
onSlotChange: function(Inventory $inventory, int $slot, Item $before) use (&$numChanges) : void{
$numChanges++;
},
onContentChange: null
));
$inventory->setItem(0, VanillaItems::DIAMOND_SWORD());
self::assertSame(1, $numChanges, "Inventory listener detected wrong number of changes");
}
public function testListenersOnProxyContentUpdate() : void{
$inventory = new CombinedInventoryProxy($this->createInventories());
$numChanges = 0;
$inventory->getListeners()->add(new CallbackInventoryListener(
onSlotChange: null,
onContentChange: function(Inventory $inventory, array $oldItems) use (&$numChanges) : void{
$numChanges++;
}
));
$inventory->setContents(self::getAltItems());
self::assertSame(1, $numChanges, "Expected onContentChange to be called exactly 1 time");
}
public function testListenersOnBackingSlotUpdate() : void{
$backing = $this->createInventories();
$inventory = new CombinedInventoryProxy($backing);
$slotChangeDetected = null;
$numChanges = 0;
$inventory->getListeners()->add(new CallbackInventoryListener(
onSlotChange: function(Inventory $inventory, int $slot, Item $before) use (&$slotChangeDetected, &$numChanges) : void{
$slotChangeDetected = $slot;
$numChanges++;
},
onContentChange: null
));
$backing[2]->setItem(0, VanillaItems::DIAMOND_SWORD());
self::assertNotNull($slotChangeDetected, "Inventory listener didn't hear about backing inventory update");
self::assertSame(2, $slotChangeDetected, "Inventory listener detected unexpected slot change");
self::assertSame(1, $numChanges, "Inventory listener detected wrong number of changes");
}
/**
* When a combined inventory has multiple backing inventories, content updates of the backing inventories must be
* turned into slot updates on the proxy, to avoid syncing the entire proxy inventory.
*/
public function testListenersOnBackingContentUpdate() : void{
$backing = $this->createInventories();
$inventory = new CombinedInventoryProxy($backing);
$slotChanges = [];
$inventory->getListeners()->add(new CallbackInventoryListener(
onSlotChange: function(Inventory $inventory, int $slot, Item $before) use (&$slotChanges) : void{
$slotChanges[] = $slot;
},
onContentChange: null
));
$backing[2]->setContents([VanillaItems::DIAMOND_SWORD(), VanillaItems::DIAMOND()]);
self::assertCount(2, $slotChanges, "Inventory listener detected wrong number of changes");
self::assertSame([2, 3], $slotChanges, "Incorrect slots updated");
}
/**
* If a combined inventory has only 1 backing inventory, content updates on the backing inventory can be directly
* processed as content updates on the proxy inventory without modification. This allows optimizations when only 1
* backing inventory is used.
* This test verifies that this special case works as expected.
*/
public function testListenersOnSingleBackingContentUpdate() : void{
$backing = new SimpleInventory(2);
$inventory = new CombinedInventoryProxy([$backing]);
$numChanges = 0;
$inventory->getListeners()->add(new CallbackInventoryListener(
onSlotChange: null,
onContentChange: function(Inventory $inventory, array $oldItems) use (&$numChanges) : void{
$numChanges++;
}
));
$inventory->setContents([VanillaItems::DIAMOND_SWORD(), VanillaItems::DIAMOND()]);
self::assertSame(1, $numChanges, "Expected onContentChange to be called exactly 1 time");
}
}