diff --git a/src/block/Chest.php b/src/block/Chest.php index 18868a606..7abe42eb8 100644 --- a/src/block/Chest.php +++ b/src/block/Chest.php @@ -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); } } } diff --git a/src/block/tile/Chest.php b/src/block/tile/Chest.php index ab03174cf..1bcd4293f 100644 --- a/src/block/tile/Chest.php +++ b/src/block/tile/Chest.php @@ -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); diff --git a/src/block/utils/ChestPairHalf.php b/src/block/utils/ChestPairHalf.php new file mode 100644 index 000000000..b996884b8 --- /dev/null +++ b/src/block/utils/ChestPairHalf.php @@ -0,0 +1,47 @@ + 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 + }; + } +} diff --git a/src/block/utils/ContainerTrait.php b/src/block/utils/ContainerTrait.php index 7b6b05795..cf8bc3801 100644 --- a/src/block/utils/ContainerTrait.php +++ b/src/block/utils/ContainerTrait.php @@ -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{