Finally deal with APIs on container and menu blocks

Two main interfaces are introduced:
- Container (chest, furnace, etc)
- MenuAccessor (includes Containers but also anvils, crafting tables etc. which are not containers)

Containers have inventories, locks, and everything from MenuAccessor
MenuAccessors have obstruction checks, and openToUnchecked() for plugins to use

I opted not to include precondition checks in openTo() because it's possible
to replicate those using the provided APIs if desired.
This commit is contained in:
Dylan K. Taylor
2025-08-30 23:38:07 +01:00
parent 496ab808a8
commit 3c6b0993cc
21 changed files with 211 additions and 152 deletions

View File

@ -29,7 +29,8 @@ use pocketmine\block\utils\FallableTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\HorizontalFacingOption;
use pocketmine\block\utils\HorizontalFacingTrait;
use pocketmine\block\utils\InventoryMenuTrait;
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;
@ -45,10 +46,10 @@ 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 InventoryMenuTrait;
use MenuAccessorTrait;
public const UNDAMAGED = 0;
public const SLIGHTLY_DAMAGED = 1;
@ -83,7 +84,7 @@ class Anvil extends Transparent implements Fallable, HorizontalFacing{
return SupportType::NONE;
}
protected function newWindow(Player $player, Position $position) : AnvilInventoryWindow{
protected function newMenu(Player $player, Position $position) : AnvilInventoryWindow{
return new AnvilInventoryWindow($player, $position);
}

View File

@ -23,10 +23,11 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\utils\AnimatedContainer;
use pocketmine\block\utils\AnimatedContainerTrait;
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;
@ -40,8 +41,8 @@ use pocketmine\world\sound\BarrelOpenSound;
use pocketmine\world\sound\Sound;
use function abs;
class Barrel extends Opaque implements AnimatedContainer, AnyFacing{
use AnimatedContainerTrait;
class Barrel extends Opaque implements AnimatedContainerLike, AnyFacing, Container{
use AnimatedContainerLikeTrait;
use AnyFacingTrait;
use ContainerTrait;
@ -86,15 +87,15 @@ class Barrel extends Opaque implements AnimatedContainer, AnyFacing{
return 300;
}
protected function getContainerOpenSound() : Sound{
protected function getOpenSound() : Sound{
return new BarrelOpenSound();
}
protected function getContainerCloseSound() : Sound{
protected function getCloseSound() : Sound{
return new BarrelCloseSound();
}
protected function doContainerAnimation(Position $position, bool $isOpen) : void{
protected function playAnimationVisual(Position $position, bool $isOpen) : void{
$world = $position->getWorld();
$block = $world->getBlock($position);
if($block instanceof Barrel){

View File

@ -26,6 +26,7 @@ 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;
@ -38,7 +39,7 @@ 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;
/**
@ -98,7 +99,7 @@ class BrewingStand extends Transparent{
return $this;
}
protected function newWindow(Player $player, Inventory $inventory, Position $position) : BrewingStandInventoryWindow{
protected function newMenu(Player $player, Inventory $inventory, Position $position) : BrewingStandInventoryWindow{
return new BrewingStandInventoryWindow($player, $inventory, $position);
}

View File

@ -24,14 +24,15 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\window\CartographyTableInventoryWindow;
use pocketmine\block\utils\InventoryMenuTrait;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\player\Player;
use pocketmine\world\Position;
final class CartographyTable extends Opaque{
use InventoryMenuTrait;
final class CartographyTable extends Opaque implements MenuAccessor{
use MenuAccessorTrait;
protected function newWindow(Player $player, Position $position) : CartographyTableInventoryWindow{
protected function newMenu(Player $player, Position $position) : CartographyTableInventoryWindow{
return new CartographyTableInventoryWindow($player, $position);
}

View File

@ -26,8 +26,9 @@ 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\AnimatedContainer;
use pocketmine\block\utils\AnimatedContainerTrait;
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;
@ -45,8 +46,8 @@ use pocketmine\world\sound\ChestCloseSound;
use pocketmine\world\sound\ChestOpenSound;
use pocketmine\world\sound\Sound;
class Chest extends Transparent implements AnimatedContainer, HorizontalFacing{
use AnimatedContainerTrait;
class Chest extends Transparent implements AnimatedContainerLike, Container, HorizontalFacing{
use AnimatedContainerLikeTrait;
use ContainerTrait;
use FacesOppositePlacingPlayerTrait;
@ -102,7 +103,7 @@ class Chest extends Transparent implements AnimatedContainer, HorizontalFacing{
}
}
protected function isOpeningObstructed() : bool{
public function isOpeningObstructed() : bool{
if(!$this->getSide(Facing::UP)->isTransparent()){
return true;
}
@ -110,7 +111,7 @@ class Chest extends Transparent implements AnimatedContainer, HorizontalFacing{
return $pair !== null && !$pair->getBlock()->getSide(Facing::UP)->isTransparent();
}
protected function newWindow(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
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);
@ -127,29 +128,29 @@ class Chest extends Transparent implements AnimatedContainer, HorizontalFacing{
return 300;
}
protected function getContainerOpenSound() : Sound{
protected function getOpenSound() : Sound{
return new ChestOpenSound();
}
protected function getContainerCloseSound() : Sound{
protected function getCloseSound() : Sound{
return new ChestCloseSound();
}
protected function doContainerAnimation(Position $position, bool $isOpen) : void{
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 doContainerEffects(bool $isOpen) : void{
$this->doContainerAnimation($this->position, $isOpen);
$this->playContainerSound($this->position, $isOpen);
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->doContainerAnimation($pair->getPosition(), $isOpen);
$this->playContainerSound($pair->getPosition(), $isOpen);
$this->playAnimationVisual($pair->getPosition(), $isOpen);
$this->playAnimationSound($pair->getPosition(), $isOpen);
}
}
}

View File

@ -24,14 +24,15 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\window\CraftingTableInventoryWindow;
use pocketmine\block\utils\InventoryMenuTrait;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\player\Player;
use pocketmine\world\Position;
class CraftingTable extends Opaque{
use InventoryMenuTrait;
class CraftingTable extends Opaque implements MenuAccessor{
use MenuAccessorTrait;
protected function newWindow(Player $player, Position $position) : CraftingTableInventoryWindow{
protected function newMenu(Player $player, Position $position) : CraftingTableInventoryWindow{
return new CraftingTableInventoryWindow($player, $position);
}

View File

@ -24,15 +24,16 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\window\EnchantingTableInventoryWindow;
use pocketmine\block\utils\InventoryMenuTrait;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\player\Player;
use pocketmine\world\Position;
class EnchantingTable extends Transparent{
use InventoryMenuTrait;
class EnchantingTable extends Transparent implements MenuAccessor{
use MenuAccessorTrait;
protected function recalculateCollisionBoxes() : array{
return [AxisAlignedBB::one()->trimmedCopy(Facing::UP, 0.25)];
@ -42,7 +43,7 @@ class EnchantingTable extends Transparent{
return SupportType::NONE;
}
protected function newWindow(Player $player, Position $position) : EnchantingTableInventoryWindow{
protected function newMenu(Player $player, Position $position) : EnchantingTableInventoryWindow{
return new EnchantingTableInventoryWindow($player, $position);
}
}

View File

@ -25,11 +25,11 @@ namespace pocketmine\block;
use pocketmine\block\inventory\window\BlockInventoryWindow;
use pocketmine\block\tile\EnderChest as TileEnderChest;
use pocketmine\block\utils\AnimatedContainer;
use pocketmine\block\utils\AnimatedContainerTrait;
use pocketmine\block\utils\AnimatedContainerLike;
use pocketmine\block\utils\AnimatedContainerLikeTrait;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\InventoryMenuTrait;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\item\Item;
use pocketmine\math\AxisAlignedBB;
@ -42,12 +42,12 @@ use pocketmine\world\sound\EnderChestCloseSound;
use pocketmine\world\sound\EnderChestOpenSound;
use pocketmine\world\sound\Sound;
class EnderChest extends Transparent implements AnimatedContainer, HorizontalFacing{
use AnimatedContainerTrait {
onContainerOpen as private traitOnContainerOpen;
onContainerClose as private traitOnContainerClose;
class EnderChest extends Transparent implements AnimatedContainerLike, HorizontalFacing{
use AnimatedContainerLikeTrait {
onViewerAdded as private traitOnViewerAdded;
onViewerRemoved as private traitOnViewerRemoved;
}
use InventoryMenuTrait;
use MenuAccessorTrait;
use FacesOppositePlacingPlayerTrait;
public function getLightLevel() : int{
@ -67,7 +67,7 @@ class EnderChest extends Transparent implements AnimatedContainer, HorizontalFac
return !$this->getSide(Facing::UP)->isTransparent();
}
protected function newWindow(Player $player, Position $position) : BlockInventoryWindow{
protected function newMenu(Player $player, Position $position) : BlockInventoryWindow{
return new BlockInventoryWindow($player, $player->getEnderInventory(), $position);
}
@ -81,7 +81,7 @@ class EnderChest extends Transparent implements AnimatedContainer, HorizontalFac
return true;
}
protected function getContainerViewerCount() : int{
protected function getViewerCount() : int{
$enderChest = $this->position->getWorld()->getTile($this->position);
if(!$enderChest instanceof TileEnderChest){
return 0;
@ -89,34 +89,34 @@ class EnderChest extends Transparent implements AnimatedContainer, HorizontalFac
return $enderChest->getViewerCount();
}
private function updateContainerViewerCount(int $amount) : void{
private function updateViewerCount(int $amount) : void{
$enderChest = $this->position->getWorld()->getTile($this->position);
if($enderChest instanceof TileEnderChest){
$enderChest->setViewerCount($enderChest->getViewerCount() + $amount);
}
}
protected function getContainerOpenSound() : Sound{
protected function getOpenSound() : Sound{
return new EnderChestOpenSound();
}
protected function getContainerCloseSound() : Sound{
protected function getCloseSound() : Sound{
return new EnderChestCloseSound();
}
protected function doContainerAnimation(Position $position, bool $isOpen) : void{
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 onContainerOpen() : void{
$this->updateContainerViewerCount(1);
$this->traitOnContainerOpen();
public function onViewerAdded() : void{
$this->updateViewerCount(1);
$this->traitOnViewerAdded();
}
public function onContainerClose() : void{
$this->traitOnContainerClose();
$this->updateContainerViewerCount(-1);
public function onViewerRemoved() : void{
$this->traitOnViewerRemoved();
$this->updateViewerCount(-1);
}
}

View File

@ -25,6 +25,7 @@ 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;
@ -38,7 +39,7 @@ 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;
@ -63,7 +64,7 @@ class Furnace extends Opaque implements Lightable, HorizontalFacing{
return $this->lit ? 13 : 0;
}
protected function newWindow(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
return new FurnaceInventoryWindow($player, $inventory, $position, $this->furnaceType);
}

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\block;
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;
@ -39,7 +40,7 @@ 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;
@ -86,7 +87,7 @@ class Hopper extends Transparent implements PoweredByRedstone{
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
protected function newWindow(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
return new HopperInventoryWindow($player, $inventory, $position);
}

View File

@ -26,15 +26,16 @@ namespace pocketmine\block;
use pocketmine\block\inventory\window\LoomInventoryWindow;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\InventoryMenuTrait;
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 InventoryMenuTrait;
use MenuAccessorTrait;
protected function newWindow(Player $player, Position $position) : LoomInventoryWindow{
protected function newMenu(Player $player, Position $position) : LoomInventoryWindow{
return new LoomInventoryWindow($player, $position);
}
}

View File

@ -25,10 +25,11 @@ namespace pocketmine\block;
use pocketmine\block\inventory\window\BlockInventoryWindow;
use pocketmine\block\tile\ShulkerBox as TileShulkerBox;
use pocketmine\block\utils\AnimatedContainer;
use pocketmine\block\utils\AnimatedContainerTrait;
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;
@ -46,8 +47,8 @@ use pocketmine\world\sound\ShulkerBoxCloseSound;
use pocketmine\world\sound\ShulkerBoxOpenSound;
use pocketmine\world\sound\Sound;
class ShulkerBox extends Opaque implements AnimatedContainer, AnyFacing{
use AnimatedContainerTrait;
class ShulkerBox extends Opaque implements AnimatedContainerLike, AnyFacing, Container{
use AnimatedContainerLikeTrait;
use AnyFacingTrait;
use ContainerTrait;
@ -109,11 +110,11 @@ class ShulkerBox extends Opaque implements AnimatedContainer, AnyFacing{
return $result;
}
protected function isOpeningObstructed() : bool{
public function isOpeningObstructed() : bool{
return $this->getSide($this->facing)->isSolid();
}
protected function newWindow(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
return new BlockInventoryWindow($player, $inventory, $position);
}
@ -121,15 +122,15 @@ class ShulkerBox extends Opaque implements AnimatedContainer, AnyFacing{
return SupportType::NONE;
}
protected function getContainerOpenSound() : Sound{
protected function getOpenSound() : Sound{
return new ShulkerBoxOpenSound();
}
protected function getContainerCloseSound() : Sound{
protected function getCloseSound() : Sound{
return new ShulkerBoxCloseSound();
}
protected function doContainerAnimation(Position $position, bool $isOpen) : void{
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

@ -24,15 +24,16 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\inventory\window\SmithingTableInventoryWindow;
use pocketmine\block\utils\InventoryMenuTrait;
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{
use InventoryMenuTrait;
final class SmithingTable extends Opaque implements MenuAccessor{
use MenuAccessorTrait;
protected function newWindow(Player $player, Position $position) : InventoryWindow{
protected function newMenu(Player $player, Position $position) : InventoryWindow{
return new SmithingTableInventoryWindow($player, $position);
}

View File

@ -26,18 +26,19 @@ namespace pocketmine\block;
use pocketmine\block\inventory\window\StonecutterInventoryWindow;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\InventoryMenuTrait;
use pocketmine\block\utils\MenuAccessor;
use pocketmine\block\utils\MenuAccessorTrait;
use pocketmine\block\utils\SupportType;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\player\Player;
use pocketmine\world\Position;
class Stonecutter extends Transparent implements HorizontalFacing{
class Stonecutter extends Transparent implements HorizontalFacing, MenuAccessor{
use FacesOppositePlacingPlayerTrait;
use InventoryMenuTrait;
use MenuAccessorTrait;
protected function newWindow(Player $player, Position $position) : StonecutterInventoryWindow{
protected function newMenu(Player $player, Position $position) : StonecutterInventoryWindow{
return new StonecutterInventoryWindow($player, $position);
}

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\block\inventory\window;
use pocketmine\block\utils\AnimatedContainer;
use pocketmine\block\utils\AnimatedContainerLike;
use pocketmine\inventory\Inventory;
use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
@ -44,15 +44,15 @@ class BlockInventoryWindow extends InventoryWindow{
public function onOpen() : void{
parent::onOpen();
$block = $this->holder->getWorld()->getBlock($this->holder);
if($block instanceof AnimatedContainer){
$block->onContainerOpen();
if($block instanceof AnimatedContainerLike){
$block->onViewerAdded();
}
}
public function onClose() : void{
$block = $this->holder->getWorld()->getBlock($this->holder);
if($block instanceof AnimatedContainer){
$block->onContainerClose();
if($block instanceof AnimatedContainerLike){
$block->onViewerRemoved();
}
parent::onClose();
}

View File

@ -23,16 +23,20 @@ declare(strict_types=1);
namespace pocketmine\block\utils;
interface AnimatedContainer{
/**
* 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 onContainerOpen() : void;
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 onContainerClose() : void;
public function onViewerRemoved() : void;
}

View File

@ -28,9 +28,9 @@ use pocketmine\world\Position;
use pocketmine\world\sound\Sound;
use function count;
trait AnimatedContainerTrait{
trait AnimatedContainerLikeTrait{
protected function getContainerViewerCount() : int{
protected function getViewerCount() : int{
$position = $this->getPosition();
$tile = $position->getWorld()->getTile($position);
if($tile instanceof InventoryHolder){
@ -39,33 +39,33 @@ trait AnimatedContainerTrait{
return 0;
}
abstract protected function getContainerOpenSound() : Sound;
abstract protected function getOpenSound() : Sound;
abstract protected function getContainerCloseSound() : Sound;
abstract protected function getCloseSound() : Sound;
abstract protected function doContainerAnimation(Position $position, bool $isOpen) : void;
abstract protected function playAnimationVisual(Position $position, bool $isOpen) : void;
protected function playContainerSound(Position $position, bool $isOpen) : void{
$position->getWorld()->addSound($position->add(0.5, 0.5, 0.5), $isOpen ? $this->getContainerOpenSound() : $this->getContainerCloseSound());
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 doContainerEffects(bool $isOpen) : void{
protected function doAnimationEffects(bool $isOpen) : void{
$position = $this->getPosition();
$this->doContainerAnimation($position, $isOpen);
$this->playContainerSound($position, $isOpen);
$this->playAnimationVisual($position, $isOpen);
$this->playAnimationSound($position, $isOpen);
}
public function onContainerOpen() : void{
if($this->getContainerViewerCount() === 1){
$this->doContainerEffects(true);
public function onViewerAdded() : void{
if($this->getViewerCount() === 1){
$this->doAnimationEffects(true);
}
}
public function onContainerClose() : void{
if($this->getContainerViewerCount() === 1){
$this->doContainerEffects(false);
public function onViewerRemoved() : void{
if($this->getViewerCount() === 1){
$this->doAnimationEffects(false);
}
}
}

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

@ -39,34 +39,34 @@ 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->openTo($player, ignoreObstruction: false, ignoreLock: false);
if($player instanceof Player && !$this->isOpeningObstructed() && $this->canOpenWith($item->getCustomName())){
$this->openToUnchecked($player);
}
return true;
}
protected function isOpeningObstructed() : bool{
return false;
}
protected function newWindow(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
return new BlockInventoryWindow($player, $inventory, $position);
}
public function openTo(Player $player, bool $ignoreObstruction, bool $ignoreLock) : ContainerOpenResult{
public function isOpeningObstructed() : bool{
return false;
}
public function canOpenWith(string $key) : bool{
//TODO: maybe we can bring the key to the block in readStateFromWorld()?
$tile = $this->position->getWorld()->getTile($this->position);
if(!$tile instanceof ContainerTile){
return ContainerOpenResult::CONTAINER_NOT_FOUND;
}
if(!$ignoreLock && !$tile->canOpenWith($player->getHotbar()->getHeldItem()->getCustomName())){
return ContainerOpenResult::INCORRECT_KEY;
}
if(!$ignoreObstruction && $this->isOpeningObstructed()){
return ContainerOpenResult::OBSTRUCTED;
}
$window = $this->newWindow($player, $tile->getInventory(), $this->position);
$player->setCurrentWindow($window);
return ContainerOpenResult::SUCCESS;
return $tile instanceof ContainerTile && $tile->canOpenWith($key);
}
public function openToUnchecked(Player $player) : bool{
$tile = $this->position->getWorld()->getTile($this->position);
return $tile instanceof ContainerTile && $player->setCurrentWindow($this->newMenu($player, $tile->getInventory(), $this->position));
}
public function getInventory() : ?Inventory{
$tile = $this->position->getWorld()->getTile($this->position);
return $tile instanceof ContainerTile ? $tile->getInventory() : null;
}
}

View File

@ -23,21 +23,23 @@ declare(strict_types=1);
namespace pocketmine\block\utils;
enum ContainerOpenResult{
use pocketmine\player\Player;
/**
* 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{
/**
* Opening the container succeeded and the player is now viewing the contents
* Returns whether the block's ability to open the menu is currently obstructed (e.g. by nearby blocks).
*/
case SUCCESS;
public function isOpeningObstructed() : bool;
/**
* No container tile (and therefore no content) was found on the block's position
* 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)
*/
case CONTAINER_NOT_FOUND;
/**
* The container's opening is obstructed (e.g a block on top of a chest's lid)
*/
case OBSTRUCTED;
/**
* The container is locked and the used item doesn't have the correct custom name to unlock it
*/
case INCORRECT_KEY;
public function openToUnchecked(Player $player) : bool;
}

View File

@ -31,30 +31,26 @@ use pocketmine\player\InventoryWindow;
use pocketmine\player\Player;
use pocketmine\world\Position;
trait InventoryMenuTrait{
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->openTo($player, ignoreObstruction: false);
if($player instanceof Player && !$this->isOpeningObstructed()){
$this->openToUnchecked($player);
}
return true;
}
abstract protected function newWindow(Player $player, Position $position) : InventoryWindow;
abstract protected function newMenu(Player $player, Position $position) : InventoryWindow;
protected function isOpeningObstructed() : bool{
public function isOpeningObstructed() : bool{
return false;
}
public function openTo(Player $player, bool $ignoreObstruction) : bool{
if(!$ignoreObstruction && $this->isOpeningObstructed()){
return false;
}
$player->setCurrentWindow($this->newWindow($player, $this->position));
return true;
public function openToUnchecked(Player $player) : bool{
return $player->setCurrentWindow($this->newMenu($player, $this->position));
}
}