Rework double chest handling, move logic to the Block

This commit is contained in:
Dylan K. Taylor
2025-09-03 16:42:02 +01:00
parent 344d0af01b
commit c61829ee19
4 changed files with 210 additions and 160 deletions

View File

@ -28,12 +28,15 @@ 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\ChestPairHalf;
use pocketmine\block\utils\Container;
use pocketmine\block\utils\ContainerTrait;
use pocketmine\block\utils\FacesOppositePlacingPlayerTrait;
use pocketmine\block\utils\HorizontalFacing;
use pocketmine\block\utils\HorizontalFacingOption;
use pocketmine\block\utils\SupportType;
use pocketmine\event\block\ChestPairEvent;
use pocketmine\inventory\CombinedInventoryProxy;
use pocketmine\inventory\Inventory;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
@ -45,83 +48,159 @@ use pocketmine\world\Position;
use pocketmine\world\sound\ChestCloseSound;
use pocketmine\world\sound\ChestOpenSound;
use pocketmine\world\sound\Sound;
use function assert;
class Chest extends Transparent implements AnimatedContainerLike, Container, HorizontalFacing{
use AnimatedContainerLikeTrait;
use ContainerTrait;
use FacesOppositePlacingPlayerTrait;
protected ?ChestPairHalf $pairHalf = null;
public function getPairHalf() : ?ChestPairHalf{ return $this->pairHalf; }
public function setPairHalf(?ChestPairHalf $pairHalf) : self{
$this->pairHalf = $pairHalf;
return $this;
}
public function readStateFromWorld() : Block{
parent::readStateFromWorld();
$tile = $this->position->getWorld()->getTile($this->position);
$this->pairHalf = null;
if($tile instanceof TileChest && ($pairXZ = $tile->getPairXZ()) !== null){
[$pairX, $pairZ] = $pairXZ;
foreach(ChestPairHalf::cases() as $pairSide){
$pairDirection = $pairSide->getOtherHalfSide($this->facing);
$pairPosition = $this->position->getSide($pairDirection);
if($pairPosition->getFloorX() === $pairX && $pairPosition->getFloorZ() === $pairZ){
$this->pairHalf = $pairSide;
break;
}
}
}
return $this;
}
public function writeStateToWorld() : void{
parent::writeStateToWorld();
$tile = $this->position->getWorld()->getTile($this->position);
assert($tile instanceof TileChest);
//TODO: this should probably use relative coordinates instead of absolute, for portability
if($this->pairHalf !== null){
$pairDirection = $this->pairHalf->getOtherHalfSide($this->facing);
$pairPosition = $this->position->getSide($pairDirection);
$pairXZ = [$pairPosition->getFloorX(), $pairPosition->getFloorZ()];
}else{
$pairXZ = null;
}
$tile->setPairXZ($pairXZ);
}
protected function recalculateCollisionBoxes() : array{
//these are slightly bigger than in PC
return [AxisAlignedBB::one()->contractedCopy(0.025, 0, 0.025)->trimmedCopy(Facing::UP, 0.05)];
$facing = $this->facing->toFacing();
$box = AxisAlignedBB::one()
->squashedCopy(Facing::axis($facing), 0.025)
->trimmedCopy(Facing::UP, 0.05);
$pairSide = $this->pairHalf?->getOtherHalfSide($this->facing);
return [$pairSide !== null ?
$box->trimmedCopy(Facing::opposite($pairSide), 0.025) :
$box->squashedCopy(Facing::axis(Facing::rotateY($facing, true)), 0.025)
];
}
public function getSupportType(Facing $facing) : SupportType{
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;
private function getPossiblePair(ChestPairHalf $pairSide) : ?Chest{
$pair = $this->getSide($pairSide->getOtherHalfSide($this->facing));
return $pair->hasSameTypeId($this) && $pair instanceof Chest && $pair->getFacing() === $this->facing ? $pair : null;
}
public function getOtherHalf() : ?Chest{
return $this->pairHalf !== null && ($pair = $this->getPossiblePair($this->pairHalf)) !== null && $pair->pairHalf === $this->pairHalf->opposite() ? $pair : null;
}
public function onPostPlace() : void{
//Not sure if this vanilla behaviour is intended, but a chest facing north or west will try to pair on the left
//side first, while a chest facing south or east will try the right side first.
$order = match($this->facing){
HorizontalFacingOption::NORTH, HorizontalFacingOption::WEST => [ChestPairHalf::LEFT, ChestPairHalf::RIGHT],
HorizontalFacingOption::SOUTH, HorizontalFacingOption::EAST => [ChestPairHalf::RIGHT, ChestPairHalf::LEFT]
};
$world = $this->position->getWorld();
$tile = $world->getTile($this->position);
if($tile instanceof TileChest){
foreach([false, true] as $clockwise){
$side = Facing::rotateY($this->facing->toFacing(), $clockwise);
$c = $this->getSide($side);
if($c instanceof Chest && $c->hasSameTypeId($this) && $c->facing === $this->facing){
$pair = $world->getTile($c->position);
if($pair instanceof TileChest && !$pair->isPaired()){
[$left, $right] = $clockwise ? [$c, $this] : [$this, $c];
$ev = new ChestPairEvent($left, $right);
$ev->call();
if(!$ev->isCancelled() && $world->getBlock($this->position)->hasSameTypeId($this) && $world->getBlock($c->position)->hasSameTypeId($c)){
$pair->pairWith($tile);
$tile->pairWith($pair);
break;
}
}
foreach($order as $pairSide){
$possiblePair = $this->getPossiblePair($pairSide);
if($possiblePair !== null && $possiblePair->pairHalf === null){
[$left, $right] = $pairSide === ChestPairHalf::LEFT ? [$this, $possiblePair] : [$possiblePair, $this];
$ev = new ChestPairEvent($left, $right);
if(!$ev->isCancelled() && $world->getBlock($this->position)->isSameState($this) && $world->getBlock($possiblePair->position)->isSameState($possiblePair)){
$world->setBlock($this->position, $this->setPairHalf($pairSide));
$world->setBlock($possiblePair->position, $possiblePair->setPairHalf($pairSide->opposite()));
break;
}
}
}
}
public function onNearbyBlockChange() : void{
//TODO: If the pair chunk isn't loaded, a block update of an adjacent block in loaded terrain could cause the
//chest to become unpaired. However, this is not unique to chests (think wall connections). Probably we
//should defer updates in chunks whose neighbours are not loaded?
if($this->pairHalf !== null && $this->getOtherHalf() === null){
$this->position->getWorld()->setBlock($this->position, $this->setPairHalf(null));
}
}
public function isOpeningObstructed() : bool{
if(!$this->getSide(Facing::UP)->isTransparent()){
return true;
foreach([$this, $this->getOtherHalf()] as $chest){
if($chest !== null && !$chest->getSide(Facing::UP)->isTransparent()){
return true;
}
}
[, $pair] = $this->locatePair($this->position) ?? [false, null];
return $pair !== null && !$pair->getBlock()->getSide(Facing::UP)->isTransparent();
return false;
}
protected function getTile() : ?TileChest{
$tile = $this->position->getWorld()->getTile($this->position);
return $tile instanceof TileChest ? $tile : null;
}
public function getInventory() : ?Inventory{
$thisTile = $this->getTile();
if($thisTile === null){
return null;
}
$pairTile = $this->getOtherHalf()?->getTile();
$thisInventory = $thisTile->getRealInventory();
if($pairTile === null){
$thisTile->setDoubleInventory(null);
return $thisInventory;
}
$doubleInventory = $thisTile->getDoubleInventory() ?? $pairTile->getDoubleInventory() ?? null;
if($doubleInventory === null){
$pairInventory = $pairTile->getRealInventory();
[$left, $right] = $this->pairHalf === ChestPairHalf::LEFT ? [$thisInventory, $pairInventory] : [$pairInventory, $thisInventory];
$doubleInventory = new CombinedInventoryProxy([$left, $right]);
$thisTile->setDoubleInventory($doubleInventory);
$pairTile->setDoubleInventory($doubleInventory);
}
return $doubleInventory;
}
protected function newMenu(Player $player, Inventory $inventory, Position $position) : InventoryWindow{
[$pairOnLeft, $pair] = $this->locatePair($position) ?? [false, null];
$pair = $this->getOtherHalf();
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);
[$left, $right] = $this->pairHalf === ChestPairHalf::LEFT ? [$this, $pair] : [$pair, $this];
return new DoubleChestInventoryWindow($player, $inventory, $left->position, $right->position);
}
public function getFuelTime() : int{
@ -146,11 +225,10 @@ class Chest extends Transparent implements AnimatedContainerLike, Container, Hor
$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);
$pair = $this->getOtherHalf();
if($pair !== null){
$this->playAnimationVisual($pair->position, $isOpen);
$this->playAnimationSound($pair->position, $isOpen);
}
}
}

View File

@ -29,7 +29,6 @@ use pocketmine\inventory\SimpleInventory;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\world\format\Chunk;
use pocketmine\world\World;
use function abs;
@ -37,9 +36,7 @@ class Chest extends Spawnable implements ContainerTile, Nameable{
use NameableTrait {
addAdditionalSpawnData as addNameSpawnData;
}
use ContainerTileTrait {
onBlockDestroyedHook as containerTraitBlockDestroyedHook;
}
use ContainerTileTrait;
public const TAG_PAIRX = "pairx";
public const TAG_PAIRZ = "pairz";
@ -48,8 +45,11 @@ class Chest extends Spawnable implements ContainerTile, Nameable{
protected Inventory $inventory;
protected ?CombinedInventoryProxy $doubleInventory = null;
private ?int $pairX = null;
private ?int $pairZ = null;
/**
* @var int[]|null
* @phpstan-var array{int, int}|null
*/
private ?array $pairXZ = null;
public function __construct(World $world, Vector3 $pos){
parent::__construct($world, $pos);
@ -64,10 +64,9 @@ class Chest extends Spawnable implements ContainerTile, Nameable{
($this->position->x === $pairX && abs($this->position->z - $pairZ) === 1) ||
($this->position->z === $pairZ && abs($this->position->x - $pairX) === 1)
){
$this->pairX = $pairX;
$this->pairZ = $pairZ;
$this->pairXZ = [$pairX, $pairZ];
}else{
$this->pairX = $this->pairZ = null;
$this->pairXZ = null;
}
}
$this->loadName($nbt);
@ -75,9 +74,10 @@ class Chest extends Spawnable implements ContainerTile, Nameable{
}
protected function writeSaveData(CompoundTag $nbt) : void{
if($this->isPaired()){
$nbt->setInt(self::TAG_PAIRX, $this->pairX);
$nbt->setInt(self::TAG_PAIRZ, $this->pairZ);
if($this->pairXZ !== null){
[$pairX, $pairZ] = $this->pairXZ;
$nbt->setInt(self::TAG_PAIRX, $pairX);
$nbt->setInt(self::TAG_PAIRZ, $pairZ);
}
$this->saveName($nbt);
$this->saveItems($nbt);
@ -97,12 +97,7 @@ class Chest extends Spawnable implements ContainerTile, Nameable{
$this->inventory->removeAllViewers();
if($this->doubleInventory !== null){
if($this->isPaired() && $this->position->getWorld()->isChunkLoaded($this->pairX >> Chunk::COORD_BIT_SIZE, $this->pairZ >> Chunk::COORD_BIT_SIZE)){
$this->doubleInventory->removeAllViewers();
if(($pair = $this->getPair()) !== null){
$pair->doubleInventory = null;
}
}
$this->doubleInventory->removeAllViewers();
$this->doubleInventory = null;
}
@ -110,15 +105,13 @@ class Chest extends Spawnable implements ContainerTile, Nameable{
}
}
protected function onBlockDestroyedHook() : void{
$this->unpair();
$this->containerTraitBlockDestroyedHook();
public function getDoubleInventory() : ?CombinedInventoryProxy{ return $this->doubleInventory; }
public function setDoubleInventory(?CombinedInventoryProxy $inventory) : void{
$this->doubleInventory = $inventory;
}
public function getInventory() : Inventory|CombinedInventoryProxy{
if($this->isPaired() && $this->doubleInventory === null){
$this->checkPairing();
}
return $this->doubleInventory ?? $this->inventory;
}
@ -126,98 +119,30 @@ class Chest extends Spawnable implements ContainerTile, Nameable{
return $this->inventory;
}
protected function checkPairing() : void{
if($this->isPaired() && !$this->position->getWorld()->isInLoadedTerrain(new Vector3($this->pairX, $this->position->y, $this->pairZ))){
//paired to a tile in an unloaded chunk
$this->doubleInventory = null;
}elseif(($pair = $this->getPair()) instanceof Chest){
if(!$pair->isPaired()){
$pair->createPair($this);
$pair->checkPairing();
}
if($this->doubleInventory === null){
if($pair->doubleInventory !== null){
$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 CombinedInventoryProxy([$pair->inventory, $this->inventory]);
}else{
$this->doubleInventory = $pair->doubleInventory = new CombinedInventoryProxy([$this->inventory, $pair->inventory]);
}
}
}
}else{
$this->doubleInventory = null;
$this->pairX = $this->pairZ = null;
}
}
public function getDefaultName() : string{
return "Chest";
}
public function isPaired() : bool{
return $this->pairX !== null && $this->pairZ !== null;
/**
* @return int[]|null
* @phpstan-return array{int, int}|null
*/
public function getPairXZ() : ?array{
return $this->pairXZ;
}
public function getPair() : ?Chest{
if($this->isPaired()){
$tile = $this->position->getWorld()->getTileAt($this->pairX, $this->position->y, $this->pairZ);
if($tile instanceof Chest){
return $tile;
}
}
return null;
}
public function pairWith(Chest $tile) : bool{
if($this->isPaired() || $tile->isPaired()){
return false;
}
$this->createPair($tile);
$this->clearSpawnCompoundCache();
$tile->clearSpawnCompoundCache();
$this->checkPairing();
return true;
}
private function createPair(Chest $tile) : void{
$this->pairX = $tile->getPosition()->x;
$this->pairZ = $tile->getPosition()->z;
$tile->pairX = $this->getPosition()->x;
$tile->pairZ = $this->getPosition()->z;
}
public function unpair() : bool{
if(!$this->isPaired()){
return false;
}
$tile = $this->getPair();
$this->pairX = $this->pairZ = null;
$this->clearSpawnCompoundCache();
if($tile instanceof Chest){
$tile->pairX = $tile->pairZ = null;
$tile->checkPairing();
$tile->clearSpawnCompoundCache();
}
$this->checkPairing();
return true;
/**
* @param int[]|null $pairXZ
* @phpstan-param array{int, int}|null $pairXZ
*/
public function setPairXZ(?array $pairXZ) : void{
$this->pairXZ = $pairXZ;
}
protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
if($this->isPaired()){
$nbt->setInt(self::TAG_PAIRX, $this->pairX);
$nbt->setInt(self::TAG_PAIRZ, $this->pairZ);
if($this->pairXZ !== null){
$nbt->setInt(self::TAG_PAIRX, $this->pairXZ[0]);
$nbt->setInt(self::TAG_PAIRZ, $this->pairXZ[1]);
}
$this->addNameSpawnData($nbt);

View File

@ -0,0 +1,47 @@
<?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\math\Facing;
enum ChestPairHalf{
/** This is the left half of the chest */
case LEFT;
/** This is the right half of the chest */
case RIGHT;
public function getOtherHalfSide(HorizontalFacingOption $hzFacing) : Facing{
return match($this){
self::RIGHT => Facing::rotateY($hzFacing->toFacing(), clockwise: true),
self::LEFT => Facing::rotateY($hzFacing->toFacing(), clockwise: false)
};
}
public function opposite() : self{
return match($this){
self::LEFT => self::RIGHT,
self::RIGHT => self::LEFT
};
}
}

View File

@ -68,8 +68,8 @@ trait ContainerTrait{
}
public function openToUnchecked(Player $player) : bool{
$tile = $this->getTile();
return $tile !== null && $player->setCurrentWindow($this->newMenu($player, $tile->getInventory(), $this->getPosition()));
$inventory = $this->getInventory();
return $inventory !== null && $player->setCurrentWindow($this->newMenu($player, $inventory, $this->getPosition()));
}
public function getInventory() : ?Inventory{