From 699a85a5d67216af8abd4dc35f5356ac62587a3c Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 7 Dec 2024 18:51:25 +0000 Subject: [PATCH] Replace DoubleChestInventory with a more generic CombinedInventory this could be used for a bunch of different things aside from double chests since the DoubleChestInventory no longer references anything specific about chests, I figured it was time to generalize this. --- src/block/Chest.php | 1 - src/block/inventory/DoubleChestInventory.php | 99 ---------- src/block/tile/Chest.php | 12 +- src/inventory/CombinedInventory.php | 135 +++++++++++++ .../inventory/CombinedInventoryTest.php | 180 ++++++++++++++++++ 5 files changed, 321 insertions(+), 106 deletions(-) delete mode 100644 src/block/inventory/DoubleChestInventory.php create mode 100644 src/inventory/CombinedInventory.php create mode 100644 tests/phpunit/inventory/CombinedInventoryTest.php diff --git a/src/block/Chest.php b/src/block/Chest.php index 26ba16a15..13712cab9 100644 --- a/src/block/Chest.php +++ b/src/block/Chest.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace pocketmine\block; -use pocketmine\block\inventory\DoubleChestInventory; use pocketmine\block\inventory\window\BlockInventoryWindow; use pocketmine\block\inventory\window\DoubleChestInventoryWindow; use pocketmine\block\tile\Chest as TileChest; diff --git a/src/block/inventory/DoubleChestInventory.php b/src/block/inventory/DoubleChestInventory.php deleted file mode 100644 index 5c11d1a7a..000000000 --- a/src/block/inventory/DoubleChestInventory.php +++ /dev/null @@ -1,99 +0,0 @@ -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); - } - - public 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); - } - - public function getLeftSide() : Inventory{ - return $this->left; - } - - public function getRightSide() : Inventory{ - return $this->right; - } -} diff --git a/src/block/tile/Chest.php b/src/block/tile/Chest.php index b66e849c7..1c21a650f 100644 --- a/src/block/tile/Chest.php +++ b/src/block/tile/Chest.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace pocketmine\block\tile; -use pocketmine\block\inventory\DoubleChestInventory; +use pocketmine\inventory\CombinedInventory; use pocketmine\inventory\Inventory; use pocketmine\inventory\SimpleInventory; use pocketmine\math\Vector3; @@ -46,7 +46,7 @@ class Chest extends Spawnable implements ContainerTile, Nameable{ public const TAG_PAIR_LEAD = "pairlead"; protected Inventory $inventory; - protected ?DoubleChestInventory $doubleInventory = null; + protected ?CombinedInventory $doubleInventory = null; private ?int $pairX = null; private ?int $pairZ = null; @@ -113,11 +113,11 @@ class Chest extends Spawnable implements ContainerTile, Nameable{ $this->containerTraitBlockDestroyedHook(); } - public function getInventory() : Inventory|DoubleChestInventory{ + public function getInventory() : Inventory|CombinedInventory{ 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() : Inventory{ @@ -139,9 +139,9 @@ class Chest extends Spawnable implements ContainerTile, 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 CombinedInventory([$pair->inventory, $this->inventory]); }else{ - $this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($this->inventory, $pair->inventory); + $this->doubleInventory = $pair->doubleInventory = new CombinedInventory([$this->inventory, $pair->inventory]); } } } diff --git a/src/inventory/CombinedInventory.php b/src/inventory/CombinedInventory.php new file mode 100644 index 000000000..922f1bc0d --- /dev/null +++ b/src/inventory/CombinedInventory.php @@ -0,0 +1,135 @@ + + */ + private array $backingInventories = []; + /** + * @var Inventory[] + * @phpstan-var array + */ + private array $slotToInventoryMap = []; + /** + * @var int[] + * @phpstan-var array + */ + private array $inventoryToOffsetMap = []; + + /** + * @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; + } + + /** + * @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); + $inventory->setItem($actualSlot, $item); + } + + 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]; + $backingInventory->setContents($backingInventoryContents); + } + } + + 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); + } +} diff --git a/tests/phpunit/inventory/CombinedInventoryTest.php b/tests/phpunit/inventory/CombinedInventoryTest.php new file mode 100644 index 000000000..44c65b0e0 --- /dev/null +++ b/tests/phpunit/inventory/CombinedInventoryTest.php @@ -0,0 +1,180 @@ + + */ + 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 $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 + */ + private static function getAltItems() : array{ + return [ + VanillaItems::AMETHYST_SHARD(), + VanillaItems::AIR(), //null item + VanillaItems::BLAZE_POWDER(), + VanillaItems::BRICK() + ]; + } + + public function testGetItem() : void{ + $inventory = new CombinedInventory($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 CombinedInventory($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 $backing + * @phpstan-param array $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 CombinedInventory($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}, void, void> + */ + public static function setContentsProvider() : \Generator{ + $altItems = self::getAltItems(); + + yield [$altItems]; + yield [array_filter($altItems, fn(Item $item) => !$item->isNull())]; + } + + /** + * @dataProvider setContentsProvider + * @param Item[] $altItems + * @phpstan-param array $altItems + */ + public function testSetContents(array $altItems) : void{ + $backing = $this->createInventories(); + $inventory = new CombinedInventory($backing); + $inventory->setContents($altItems); + + $this->verifyWriteItems($backing, $altItems); + } + + public function testGetSize() : void{ + self::assertSame(4, (new CombinedInventory($this->createInventories()))->getSize()); + } + + public function testGetMatchingItemCount() : void{ + $inventory = new CombinedInventory($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 CombinedInventory($this->createInventories()); + + self::assertTrue($inventory->isSlotEmpty(2)); + self::assertFalse($inventory->isSlotEmpty(0)); + self::assertFalse($inventory->isSlotEmpty(1)); + self::assertFalse($inventory->isSlotEmpty(3)); + } +}