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.
This commit is contained in:
Dylan K. Taylor 2024-12-07 18:51:25 +00:00
parent 4906f5bec2
commit 699a85a5d6
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
5 changed files with 321 additions and 106 deletions

View File

@ -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;

View File

@ -1,99 +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\Inventory;
use pocketmine\item\Item;
final class DoubleChestInventory extends BaseInventory{
public function __construct(
private Inventory $left,
private Inventory $right
){
parent::__construct();
}
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);
}
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;
}
}

View File

@ -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]);
}
}
}

View File

@ -0,0 +1,135 @@
<?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 function array_fill_keys;
use function array_keys;
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 CombinedInventory 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 = [];
/**
* @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);
}
}

View File

@ -0,0 +1,180 @@
<?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\ItemTypeIds;
use pocketmine\item\VanillaItems;
use function array_filter;
final class CombinedInventoryTest extends \PHPUnit\Framework\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 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<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 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<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())];
}
/**
* @dataProvider setContentsProvider
* @param Item[] $altItems
* @phpstan-param array<int, Item> $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));
}
}