diff --git a/src/block/Anvil.php b/src/block/Anvil.php index 0a1a4707005..e2455da9be6 100644 --- a/src/block/Anvil.php +++ b/src/block/Anvil.php @@ -23,7 +23,7 @@ namespace pocketmine\block; -use pocketmine\block\inventory\AnvilInventory; +use pocketmine\block\inventory\window\AnvilInventoryWindow; use pocketmine\block\utils\Fallable; use pocketmine\block\utils\FallableTrait; use pocketmine\block\utils\HorizontalFacingTrait; @@ -83,7 +83,7 @@ public function getSupportType(int $facing) : SupportType{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($player instanceof Player){ - $player->setCurrentWindow(new AnvilInventory($this->position)); + $player->setCurrentWindow(new AnvilInventoryWindow($player, $this)); } return true; diff --git a/src/block/Barrel.php b/src/block/Barrel.php index 0f0499ab93b..999f82db47a 100644 --- a/src/block/Barrel.php +++ b/src/block/Barrel.php @@ -23,7 +23,10 @@ namespace pocketmine\block; +use pocketmine\block\inventory\window\BlockInventoryWindow; use pocketmine\block\tile\Barrel as TileBarrel; +use pocketmine\block\utils\AnimatedContainer; +use pocketmine\block\utils\AnimatedContainerTrait; use pocketmine\block\utils\AnyFacingTrait; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; @@ -31,9 +34,14 @@ use pocketmine\math\Vector3; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; +use pocketmine\world\Position; +use pocketmine\world\sound\BarrelCloseSound; +use pocketmine\world\sound\BarrelOpenSound; +use pocketmine\world\sound\Sound; use function abs; -class Barrel extends Opaque{ +class Barrel extends Opaque implements AnimatedContainer{ + use AnimatedContainerTrait; use AnyFacingTrait; protected bool $open = false; @@ -81,7 +89,7 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player return true; } - $player->setCurrentWindow($barrel->getInventory()); + $player->setCurrentWindow(new BlockInventoryWindow($player, $barrel->getInventory(), $this)); } } @@ -91,4 +99,20 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player public function getFuelTime() : int{ return 300; } + + protected function getContainerOpenSound() : Sound{ + return new BarrelOpenSound(); + } + + protected function getContainerCloseSound() : Sound{ + return new BarrelCloseSound(); + } + + protected function doContainerAnimation(Position $position, bool $isOpen) : void{ + $world = $position->getWorld(); + $block = $world->getBlock($position); + if($block instanceof Barrel){ + $world->setBlock($position, $block->setOpen($isOpen)); + } + } } diff --git a/src/block/Block.php b/src/block/Block.php index 89fe3926584..aabb6966566 100644 --- a/src/block/Block.php +++ b/src/block/Block.php @@ -36,6 +36,7 @@ use pocketmine\data\runtime\RuntimeDataWriter; use pocketmine\entity\Entity; use pocketmine\entity\projectile\Projectile; +use pocketmine\inventory\Inventory; use pocketmine\item\enchantment\AvailableEnchantmentRegistry; use pocketmine\item\enchantment\ItemEnchantmentTagRegistry; use pocketmine\item\enchantment\ItemEnchantmentTags; @@ -515,6 +516,15 @@ public function onScheduledUpdate() : void{ } + /** + * Called by the World when a change is detected in a container's inventory at the block's position. + * Use this to do visual updates on the block if needed. + * Don't do any expensive logic in here. It will be called every time a slot of the inventory changes. + */ + public function onContainerUpdate(Inventory $inventory) : void{ + + } + /** * Do actions when interacted by Item. Returns if it has done anything * diff --git a/src/block/BrewingStand.php b/src/block/BrewingStand.php index 03969316494..c43747e40de 100644 --- a/src/block/BrewingStand.php +++ b/src/block/BrewingStand.php @@ -23,10 +23,12 @@ 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\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; +use pocketmine\inventory\Inventory; use pocketmine\item\Item; use pocketmine\math\Axis; use pocketmine\math\AxisAlignedBB; @@ -99,7 +101,7 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player if($player instanceof Player){ $stand = $this->position->getWorld()->getTile($this->position); if($stand instanceof TileBrewingStand && $stand->canOpenWith($item->getCustomName())){ - $player->setCurrentWindow($stand->getInventory()); + $player->setCurrentWindow(new BrewingStandInventoryWindow($player, $stand->getInventory(), $this)); } } @@ -113,19 +115,22 @@ public function onScheduledUpdate() : void{ if($brewing->onUpdate()){ $world->scheduleDelayedBlockUpdate($this->position, 1); } + } + } - $changed = false; - foreach(BrewingStandSlot::cases() as $slot){ - $occupied = !$brewing->getInventory()->isSlotEmpty($slot->getSlotNumber()); - if($occupied !== $this->hasSlot($slot)){ - $this->setSlot($slot, $occupied); - $changed = true; - } + public function onContainerUpdate(Inventory $inventory) : void{ + $world = $this->position->getWorld(); + $changed = false; + foreach(BrewingStandSlot::cases() as $slot){ + $occupied = !$inventory->isSlotEmpty($slot->getSlotNumber()); + if($occupied !== $this->hasSlot($slot)){ + $this->setSlot($slot, $occupied); + $changed = true; } + } - if($changed){ - $world->setBlock($this->position, $this); - } + if($changed){ + $world->setBlock($this->position, $this); } } } diff --git a/src/block/Campfire.php b/src/block/Campfire.php index 9f4c42a9ccc..b85f6d030b2 100644 --- a/src/block/Campfire.php +++ b/src/block/Campfire.php @@ -23,7 +23,6 @@ namespace pocketmine\block; -use pocketmine\block\inventory\CampfireInventory; use pocketmine\block\tile\Campfire as TileCampfire; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\block\utils\LightableTrait; @@ -38,6 +37,7 @@ use pocketmine\event\block\CampfireCookEvent; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageEvent; +use pocketmine\inventory\Inventory; use pocketmine\item\Durable; use pocketmine\item\enchantment\VanillaEnchantments; use pocketmine\item\Item; @@ -67,13 +67,11 @@ class Campfire extends Transparent{ LightableTrait::describeBlockOnlyState as encodeLitState; } - private const UPDATE_INTERVAL_TICKS = 10; - /** * @deprecated This was added by mistake. It can't be relied on as the inventory won't be initialized if this block * has never been set in the world. */ - protected CampfireInventory $inventory; + protected ?Inventory $inventory = null; /** * @var int[] slot => ticks @@ -93,7 +91,8 @@ public function readStateFromWorld() : Block{ $this->inventory = $tile->getInventory(); $this->cookingTimes = $tile->getCookingTimes(); }else{ - $this->inventory = new CampfireInventory($this->position); + $this->inventory = null; + $this->cookingTimes = []; } return $this; @@ -137,7 +136,7 @@ protected function recalculateCollisionBoxes() : array{ * @deprecated This was added by mistake. It can't be relied on as the inventory won't be initialized if this block * has never been set in the world. */ - public function getInventory() : CampfireInventory{ + public function getInventory() : ?Inventory{ return $this->inventory; } @@ -200,10 +199,11 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player return true; } - if($this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($this->getFurnaceType())->match($item) !== null){ + $inventory = $this->inventory; + if($inventory !== null && $this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($this->getFurnaceType())->match($item) !== null){ $ingredient = clone $item; $ingredient->setCount(1); - if(count($this->inventory->addItem($ingredient)) === 0){ + if(count($inventory->addItem($ingredient)) === 0){ $item->pop(); $this->position->getWorld()->addSound($this->position, new ItemFrameAddItemSound()); return true; @@ -238,12 +238,12 @@ public function onProjectileHit(Projectile $projectile, RayTraceResult $hitResul } public function onScheduledUpdate() : void{ - if($this->lit){ - $items = $this->inventory->getContents(); + if($this->lit && ($inventory = $this->inventory) !== null){ + $items = $inventory->getContents(); $furnaceType = $this->getFurnaceType(); $maxCookDuration = $furnaceType->getCookDurationTicks(); foreach($items as $slot => $item){ - $this->setCookingTime($slot, min($maxCookDuration, $this->getCookingTime($slot) + self::UPDATE_INTERVAL_TICKS)); + $this->setCookingTime($slot, min($maxCookDuration, $this->getCookingTime($slot) + 1)); if($this->getCookingTime($slot) >= $maxCookDuration){ $result = ($recipe = $this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($furnaceType)->match($item)) instanceof FurnaceRecipe ? @@ -257,21 +257,27 @@ public function onScheduledUpdate() : void{ continue; } - $this->inventory->setItem($slot, VanillaItems::AIR()); + $inventory->setItem($slot, VanillaItems::AIR()); $this->setCookingTime($slot, 0); $this->position->getWorld()->dropItem($this->position->add(0.5, 1, 0.5), $ev->getResult()); } } - if(count($items) > 0){ - $this->position->getWorld()->setBlock($this->position, $this); + $tile = $this->position->getWorld()->getTile($this->position); + if($tile instanceof TileCampfire){ + //TODO: we probably need to rethink how these are tracked + $tile->setCookingTimes($this->cookingTimes); } if(mt_rand(1, 6) === 1){ $this->position->getWorld()->addSound($this->position, $furnaceType->getCookSound()); } - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, self::UPDATE_INTERVAL_TICKS); + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); } } + public function onContainerUpdate(Inventory $inventory) : void{ + $this->position->getWorld()->setBlock($this->position, $this); //update visual state + } + private function extinguish() : void{ $this->position->getWorld()->addSound($this->position, new FireExtinguishSound()); $this->position->getWorld()->setBlock($this->position, $this->setLit(false)); @@ -280,6 +286,6 @@ private function extinguish() : void{ private function ignite() : void{ $this->position->getWorld()->addSound($this->position, new FlintSteelSound()); $this->position->getWorld()->setBlock($this->position, $this->setLit(true)); - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, self::UPDATE_INTERVAL_TICKS); + $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); } } diff --git a/src/block/CartographyTable.php b/src/block/CartographyTable.php index 67d950c5ac6..436850e83bd 100644 --- a/src/block/CartographyTable.php +++ b/src/block/CartographyTable.php @@ -23,7 +23,7 @@ namespace pocketmine\block; -use pocketmine\block\inventory\CartographyTableInventory; +use pocketmine\block\inventory\window\CartographyTableInventoryWindow; use pocketmine\item\Item; use pocketmine\math\Vector3; use pocketmine\player\Player; @@ -32,7 +32,7 @@ final class CartographyTable extends Opaque{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($player !== null){ - $player->setCurrentWindow(new CartographyTableInventory($this->position)); + $player->setCurrentWindow(new CartographyTableInventoryWindow($player, $this)); } return true; diff --git a/src/block/Chest.php b/src/block/Chest.php index dca21576aa9..d872693bd1f 100644 --- a/src/block/Chest.php +++ b/src/block/Chest.php @@ -23,7 +23,11 @@ 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\FacesOppositePlacingPlayerTrait; use pocketmine\block\utils\SupportType; use pocketmine\event\block\ChestPairEvent; @@ -31,9 +35,16 @@ use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; use pocketmine\math\Vector3; +use pocketmine\network\mcpe\protocol\BlockEventPacket; +use pocketmine\network\mcpe\protocol\types\BlockPosition; use pocketmine\player\Player; +use pocketmine\world\Position; +use pocketmine\world\sound\ChestCloseSound; +use pocketmine\world\sound\ChestOpenSound; +use pocketmine\world\sound\Sound; -class Chest extends Transparent{ +class Chest extends Transparent implements AnimatedContainer{ + use AnimatedContainerTrait; use FacesOppositePlacingPlayerTrait; /** @@ -48,6 +59,25 @@ public function getSupportType(int $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, $clockwise); + $c = $position->getSide($side); + $pair = $world->getTile($c); + if($pair instanceof TileChest && $pair->isPaired() && $pair->getPair() === $tile){ + return [$clockwise, $pair]; + } + } + } + return null; + } + public function onPostPlace() : void{ $world = $this->position->getWorld(); $tile = $world->getTile($this->position); @@ -74,18 +104,28 @@ public function onPostPlace() : void{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($player instanceof Player){ - - $chest = $this->position->getWorld()->getTile($this->position); + $world = $this->position->getWorld(); + $chest = $world->getTile($this->position); if($chest instanceof TileChest){ + [$pairOnLeft, $pair] = $this->locatePair($this->position) ?? [false, null]; if( !$this->getSide(Facing::UP)->isTransparent() || - (($pair = $chest->getPair()) !== null && !$pair->getBlock()->getSide(Facing::UP)->isTransparent()) || + ($pair !== null && !$pair->getBlock()->getSide(Facing::UP)->isTransparent()) || !$chest->canOpenWith($item->getCustomName()) ){ return true; } - $player->setCurrentWindow($chest->getInventory()); + if($pair !== null){ + [$left, $right] = $pairOnLeft ? [$pair->getBlock(), $this] : [$this, $pair->getBlock()]; + + //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 + $window = new DoubleChestInventoryWindow($player, $chest->getInventory(), $left, $right); + } + + $player->setCurrentWindow($window ?? new BlockInventoryWindow($player, $chest->getInventory(), $this)); } } @@ -95,4 +135,30 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player public function getFuelTime() : int{ return 300; } + + protected function getContainerOpenSound() : Sound{ + return new ChestOpenSound(); + } + + protected function getContainerCloseSound() : Sound{ + return new ChestCloseSound(); + } + + protected function doContainerAnimation(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); + + $pairInfo = $this->locatePair($this->position); + if($pairInfo !== null){ + [, $pair] = $pairInfo; + $this->doContainerAnimation($pair->getPosition(), $isOpen); + $this->playContainerSound($pair->getPosition(), $isOpen); + } + } } diff --git a/src/block/ChiseledBookshelf.php b/src/block/ChiseledBookshelf.php index 73c4861bf35..4c91f1d9404 100644 --- a/src/block/ChiseledBookshelf.php +++ b/src/block/ChiseledBookshelf.php @@ -28,6 +28,7 @@ use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; use pocketmine\block\utils\HorizontalFacingTrait; use pocketmine\data\runtime\RuntimeDataDescriber; +use pocketmine\inventory\Inventory; use pocketmine\item\Book; use pocketmine\item\EnchantedBook; use pocketmine\item\Item; @@ -164,6 +165,20 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player return true; } + public function onContainerUpdate(Inventory $inventory) : void{ + $changed = false; + foreach(ChiseledBookshelfSlot::cases() as $case){ + $hasItem = !$inventory->isSlotEmpty($case->value); + if($this->hasSlot($case) !== $hasItem){ + $this->setSlot($case, $hasItem); + $changed = true; + } + } + if($changed){ + $this->position->getWorld()->setBlock($this->position, $this); + } + } + public function getDropsForCompatibleTool(Item $item) : array{ return []; } diff --git a/src/block/CraftingTable.php b/src/block/CraftingTable.php index dcd9edce2cd..06824824b76 100644 --- a/src/block/CraftingTable.php +++ b/src/block/CraftingTable.php @@ -23,7 +23,7 @@ namespace pocketmine\block; -use pocketmine\block\inventory\CraftingTableInventory; +use pocketmine\block\inventory\window\CraftingTableInventoryWindow; use pocketmine\item\Item; use pocketmine\math\Vector3; use pocketmine\player\Player; @@ -32,7 +32,7 @@ class CraftingTable extends Opaque{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($player instanceof Player){ - $player->setCurrentWindow(new CraftingTableInventory($this->position)); + $player->setCurrentWindow(new CraftingTableInventoryWindow($player, $this)); } return true; diff --git a/src/block/EnchantingTable.php b/src/block/EnchantingTable.php index 6a6c936b220..db052ecf5fd 100644 --- a/src/block/EnchantingTable.php +++ b/src/block/EnchantingTable.php @@ -23,7 +23,7 @@ namespace pocketmine\block; -use pocketmine\block\inventory\EnchantInventory; +use pocketmine\block\inventory\window\EnchantingTableInventoryWindow; use pocketmine\block\utils\SupportType; use pocketmine\item\Item; use pocketmine\math\AxisAlignedBB; @@ -48,7 +48,7 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player if($player instanceof Player){ //TODO lock - $player->setCurrentWindow(new EnchantInventory($this->position)); + $player->setCurrentWindow(new EnchantingTableInventoryWindow($player, $this)); } return true; diff --git a/src/block/EnderChest.php b/src/block/EnderChest.php index 9004f7c79f9..f1fee127eb1 100644 --- a/src/block/EnderChest.php +++ b/src/block/EnderChest.php @@ -23,17 +23,29 @@ namespace pocketmine\block; -use pocketmine\block\inventory\EnderChestInventory; +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\FacesOppositePlacingPlayerTrait; use pocketmine\block\utils\SupportType; use pocketmine\item\Item; use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; use pocketmine\math\Vector3; +use pocketmine\network\mcpe\protocol\BlockEventPacket; +use pocketmine\network\mcpe\protocol\types\BlockPosition; use pocketmine\player\Player; +use pocketmine\world\Position; +use pocketmine\world\sound\EnderChestCloseSound; +use pocketmine\world\sound\EnderChestOpenSound; +use pocketmine\world\sound\Sound; -class EnderChest extends Transparent{ +class EnderChest extends Transparent implements AnimatedContainer{ + use AnimatedContainerTrait { + onContainerOpen as private traitOnContainerOpen; + onContainerClose as private traitOnContainerClose; + } use FacesOppositePlacingPlayerTrait; public function getLightLevel() : int{ @@ -56,8 +68,7 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player if($player instanceof Player){ $enderChest = $this->position->getWorld()->getTile($this->position); if($enderChest instanceof TileEnderChest && $this->getSide(Facing::UP)->isTransparent()){ - $enderChest->setViewerCount($enderChest->getViewerCount() + 1); - $player->setCurrentWindow(new EnderChestInventory($this->position, $player->getEnderInventory())); + $player->setCurrentWindow(new BlockInventoryWindow($player, $player->getEnderInventory(), $this)); } } @@ -73,4 +84,43 @@ public function getDropsForCompatibleTool(Item $item) : array{ public function isAffectedBySilkTouch() : bool{ return true; } + + protected function getContainerViewerCount() : int{ + $enderChest = $this->position->getWorld()->getTile($this->position); + if(!$enderChest instanceof TileEnderChest){ + return 0; + } + return $enderChest->getViewerCount(); + } + + private function updateContainerViewerCount(int $amount) : void{ + $enderChest = $this->position->getWorld()->getTile($this->position); + if($enderChest instanceof TileEnderChest){ + $enderChest->setViewerCount($enderChest->getViewerCount() + $amount); + } + } + + protected function getContainerOpenSound() : Sound{ + return new EnderChestOpenSound(); + } + + protected function getContainerCloseSound() : Sound{ + return new EnderChestCloseSound(); + } + + protected function doContainerAnimation(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 onContainerClose() : void{ + $this->traitOnContainerClose(); + $this->updateContainerViewerCount(-1); + } } diff --git a/src/block/Furnace.php b/src/block/Furnace.php index 7a64e3cd3e5..412ac74a818 100644 --- a/src/block/Furnace.php +++ b/src/block/Furnace.php @@ -23,6 +23,7 @@ namespace pocketmine\block; +use pocketmine\block\inventory\window\FurnaceInventoryWindow; use pocketmine\block\tile\Furnace as TileFurnace; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; use pocketmine\block\utils\LightableTrait; @@ -61,7 +62,7 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player if($player instanceof Player){ $furnace = $this->position->getWorld()->getTile($this->position); if($furnace instanceof TileFurnace && $furnace->canOpenWith($item->getCustomName())){ - $player->setCurrentWindow($furnace->getInventory()); + $player->setCurrentWindow(new FurnaceInventoryWindow($player, $furnace->getInventory(), $this, $this->furnaceType)); } } diff --git a/src/block/Hopper.php b/src/block/Hopper.php index 0d823674b23..67273b04dfd 100644 --- a/src/block/Hopper.php +++ b/src/block/Hopper.php @@ -23,6 +23,7 @@ namespace pocketmine\block; +use pocketmine\block\inventory\window\HopperInventoryWindow; use pocketmine\block\tile\Hopper as TileHopper; use pocketmine\block\utils\PoweredByRedstoneTrait; use pocketmine\block\utils\SupportType; @@ -84,7 +85,7 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player if($player !== null){ $tile = $this->position->getWorld()->getTile($this->position); if($tile instanceof TileHopper){ //TODO: find a way to have inventories open on click without this boilerplate in every block - $player->setCurrentWindow($tile->getInventory()); + $player->setCurrentWindow(new HopperInventoryWindow($player, $tile->getInventory(), $this)); } return true; } diff --git a/src/block/Loom.php b/src/block/Loom.php index d3dd4f3c766..a4d6f7da419 100644 --- a/src/block/Loom.php +++ b/src/block/Loom.php @@ -23,7 +23,7 @@ namespace pocketmine\block; -use pocketmine\block\inventory\LoomInventory; +use pocketmine\block\inventory\window\LoomInventoryWindow; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; use pocketmine\item\Item; use pocketmine\math\Vector3; @@ -34,7 +34,7 @@ final class Loom extends Opaque{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($player !== null){ - $player->setCurrentWindow(new LoomInventory($this->position)); + $player->setCurrentWindow(new LoomInventoryWindow($player, $this)); return true; } return false; diff --git a/src/block/ShulkerBox.php b/src/block/ShulkerBox.php index d557401eec3..c575ecae1b4 100644 --- a/src/block/ShulkerBox.php +++ b/src/block/ShulkerBox.php @@ -23,16 +23,26 @@ 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\AnyFacingTrait; use pocketmine\block\utils\SupportType; use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Item; use pocketmine\math\Vector3; +use pocketmine\network\mcpe\protocol\BlockEventPacket; +use pocketmine\network\mcpe\protocol\types\BlockPosition; use pocketmine\player\Player; use pocketmine\world\BlockTransaction; +use pocketmine\world\Position; +use pocketmine\world\sound\ShulkerBoxCloseSound; +use pocketmine\world\sound\ShulkerBoxOpenSound; +use pocketmine\world\sound\Sound; -class ShulkerBox extends Opaque{ +class ShulkerBox extends Opaque implements AnimatedContainer{ + use AnimatedContainerTrait; use AnyFacingTrait; protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{ @@ -105,7 +115,7 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player return true; } - $player->setCurrentWindow($shulker->getInventory()); + $player->setCurrentWindow(new BlockInventoryWindow($player, $shulker->getInventory(), $this)); } } @@ -115,4 +125,18 @@ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player public function getSupportType(int $facing) : SupportType{ return SupportType::NONE; } + + protected function getContainerOpenSound() : Sound{ + return new ShulkerBoxOpenSound(); + } + + protected function getContainerCloseSound() : Sound{ + return new ShulkerBoxCloseSound(); + } + + protected function doContainerAnimation(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)); + } } diff --git a/src/block/SmithingTable.php b/src/block/SmithingTable.php index 741e9c02fdc..77816c42dee 100644 --- a/src/block/SmithingTable.php +++ b/src/block/SmithingTable.php @@ -23,7 +23,7 @@ namespace pocketmine\block; -use pocketmine\block\inventory\SmithingTableInventory; +use pocketmine\block\inventory\window\SmithingTableInventoryWindow; use pocketmine\item\Item; use pocketmine\math\Vector3; use pocketmine\player\Player; @@ -32,7 +32,7 @@ final class SmithingTable extends Opaque{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($player !== null){ - $player->setCurrentWindow(new SmithingTableInventory($this->position)); + $player->setCurrentWindow(new SmithingTableInventoryWindow($player, $this)); } return true; diff --git a/src/block/Stonecutter.php b/src/block/Stonecutter.php index 30c19d25dcb..20fc73da52a 100644 --- a/src/block/Stonecutter.php +++ b/src/block/Stonecutter.php @@ -23,7 +23,7 @@ namespace pocketmine\block; -use pocketmine\block\inventory\StonecutterInventory; +use pocketmine\block\inventory\window\StonecutterInventoryWindow; use pocketmine\block\utils\FacesOppositePlacingPlayerTrait; use pocketmine\block\utils\SupportType; use pocketmine\item\Item; @@ -37,7 +37,7 @@ class Stonecutter extends Transparent{ public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ if($player !== null){ - $player->setCurrentWindow(new StonecutterInventory($this->position)); + $player->setCurrentWindow(new StonecutterInventoryWindow($player, $this)); } return true; } diff --git a/src/block/inventory/AnimatedBlockInventoryTrait.php b/src/block/inventory/AnimatedBlockInventoryTrait.php deleted file mode 100644 index 8720c985b1e..00000000000 --- a/src/block/inventory/AnimatedBlockInventoryTrait.php +++ /dev/null @@ -1,67 +0,0 @@ -getViewers()); - } - - /** - * @return Player[] - * @phpstan-return array - */ - abstract public function getViewers() : array; - - abstract protected function getOpenSound() : Sound; - - abstract protected function getCloseSound() : Sound; - - public function onOpen(Player $who) : void{ - parent::onOpen($who); - - if($this->holder->isValid() && $this->getViewerCount() === 1){ - //TODO: this crap really shouldn't be managed by the inventory - $this->animateBlock(true); - $this->holder->getWorld()->addSound($this->holder->add(0.5, 0.5, 0.5), $this->getOpenSound()); - } - } - - abstract protected function animateBlock(bool $isOpen) : void; - - public function onClose(Player $who) : void{ - if($this->holder->isValid() && $this->getViewerCount() === 1){ - //TODO: this crap really shouldn't be managed by the inventory - $this->animateBlock(false); - $this->holder->getWorld()->addSound($this->holder->add(0.5, 0.5, 0.5), $this->getCloseSound()); - } - parent::onClose($who); - } -} diff --git a/src/block/inventory/BarrelInventory.php b/src/block/inventory/BarrelInventory.php deleted file mode 100644 index 0d17d2a3e5b..00000000000 --- a/src/block/inventory/BarrelInventory.php +++ /dev/null @@ -1,57 +0,0 @@ -holder = $holder; - parent::__construct(27); - } - - protected function getOpenSound() : Sound{ - return new BarrelOpenSound(); - } - - protected function getCloseSound() : Sound{ - return new BarrelCloseSound(); - } - - protected function animateBlock(bool $isOpen) : void{ - $holder = $this->getHolder(); - $world = $holder->getWorld(); - $block = $world->getBlock($holder); - if($block instanceof Barrel){ - $world->setBlock($holder, $block->setOpen($isOpen)); - } - } -} diff --git a/src/block/inventory/BlockInventoryTrait.php b/src/block/inventory/BlockInventoryTrait.php deleted file mode 100644 index 980e947f02b..00000000000 --- a/src/block/inventory/BlockInventoryTrait.php +++ /dev/null @@ -1,34 +0,0 @@ -holder; - } -} diff --git a/src/block/inventory/CartographyTableInventory.php b/src/block/inventory/CartographyTableInventory.php deleted file mode 100644 index 7bd9146ac6f..00000000000 --- a/src/block/inventory/CartographyTableInventory.php +++ /dev/null @@ -1,37 +0,0 @@ -holder = $holder; - parent::__construct(2); - } -} diff --git a/src/block/inventory/ChestInventory.php b/src/block/inventory/ChestInventory.php deleted file mode 100644 index b61fab57c35..00000000000 --- a/src/block/inventory/ChestInventory.php +++ /dev/null @@ -1,56 +0,0 @@ -holder = $holder; - parent::__construct(27); - } - - protected function getOpenSound() : Sound{ - return new ChestOpenSound(); - } - - protected function getCloseSound() : Sound{ - return new ChestCloseSound(); - } - - public function animateBlock(bool $isOpen) : void{ - $holder = $this->getHolder(); - - //event ID is always 1 for a chest - $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0)); - } -} diff --git a/src/block/inventory/CraftingTableInventory.php b/src/block/inventory/CraftingTableInventory.php deleted file mode 100644 index 767e8a5f49c..00000000000 --- a/src/block/inventory/CraftingTableInventory.php +++ /dev/null @@ -1,37 +0,0 @@ -holder = $holder; - parent::__construct(CraftingGrid::SIZE_BIG); - } -} diff --git a/src/block/inventory/DoubleChestInventory.php b/src/block/inventory/DoubleChestInventory.php deleted file mode 100644 index a7eb4a4398c..00000000000 --- a/src/block/inventory/DoubleChestInventory.php +++ /dev/null @@ -1,118 +0,0 @@ -holder = $this->left->getHolder(); - parent::__construct(); - } - - public function getInventory() : self{ - return $this; - } - - 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); - } - - protected 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); - } - - protected function getOpenSound() : Sound{ return new ChestOpenSound(); } - - protected function getCloseSound() : Sound{ return new ChestCloseSound(); } - - protected function animateBlock(bool $isOpen) : void{ - $this->left->animateBlock($isOpen); - $this->right->animateBlock($isOpen); - } - - public function getLeftSide() : ChestInventory{ - return $this->left; - } - - public function getRightSide() : ChestInventory{ - return $this->right; - } -} diff --git a/src/block/inventory/EnchantInventory.php b/src/block/inventory/EnchantInventory.php deleted file mode 100644 index 6dffbad32b7..00000000000 --- a/src/block/inventory/EnchantInventory.php +++ /dev/null @@ -1,88 +0,0 @@ - - */ - private array $options = []; - - public function __construct(Position $holder){ - $this->holder = $holder; - parent::__construct(2); - } - - protected function onSlotChange(int $index, Item $before) : void{ - if($index === self::SLOT_INPUT){ - foreach($this->viewers as $viewer){ - $this->options = []; - $item = $this->getInput(); - $options = Helper::generateOptions($this->holder, $item, $viewer->getEnchantmentSeed()); - - $event = new PlayerEnchantingOptionsRequestEvent($viewer, $this, $options); - $event->call(); - if(!$event->isCancelled() && count($event->getOptions()) > 0){ - $this->options = array_values($event->getOptions()); - $viewer->getNetworkSession()->getInvManager()?->syncEnchantingTableOptions($this->options); - } - } - } - - parent::onSlotChange($index, $before); - } - - public function getInput() : Item{ - return $this->getItem(self::SLOT_INPUT); - } - - public function getLapis() : Item{ - return $this->getItem(self::SLOT_LAPIS); - } - - public function getOutput(int $optionId) : ?Item{ - $option = $this->getOption($optionId); - return $option === null ? null : Helper::enchantItem($this->getInput(), $option->getEnchantments()); - } - - public function getOption(int $optionId) : ?EnchantingOption{ - return $this->options[$optionId] ?? null; - } -} diff --git a/src/block/inventory/EnderChestInventory.php b/src/block/inventory/EnderChestInventory.php deleted file mode 100644 index c1d7c5401d3..00000000000 --- a/src/block/inventory/EnderChestInventory.php +++ /dev/null @@ -1,88 +0,0 @@ -holder = $holder; - } - - public function getEnderInventory() : PlayerEnderInventory{ - return $this->inventory; - } - - public function getViewerCount() : int{ - $enderChest = $this->getHolder()->getWorld()->getTile($this->getHolder()); - if(!$enderChest instanceof EnderChest){ - return 0; - } - return $enderChest->getViewerCount(); - } - - protected function getOpenSound() : Sound{ - return new EnderChestOpenSound(); - } - - protected function getCloseSound() : Sound{ - return new EnderChestCloseSound(); - } - - protected function animateBlock(bool $isOpen) : void{ - $holder = $this->getHolder(); - - //event ID is always 1 for a chest - $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0)); - } - - public function onClose(Player $who) : void{ - $this->animatedBlockInventoryTrait_onClose($who); - $enderChest = $this->getHolder()->getWorld()->getTile($this->getHolder()); - if($enderChest instanceof EnderChest){ - $enderChest->setViewerCount($enderChest->getViewerCount() - 1); - } - } -} diff --git a/src/block/inventory/ShulkerBoxInventory.php b/src/block/inventory/ShulkerBoxInventory.php deleted file mode 100644 index d915a995182..00000000000 --- a/src/block/inventory/ShulkerBoxInventory.php +++ /dev/null @@ -1,67 +0,0 @@ -holder = $holder; - parent::__construct(27); - } - - protected function getOpenSound() : Sound{ - return new ShulkerBoxOpenSound(); - } - - protected function getCloseSound() : Sound{ - return new ShulkerBoxCloseSound(); - } - - public function canAddItem(Item $item) : bool{ - $blockTypeId = ItemTypeIds::toBlockTypeId($item->getTypeId()); - if($blockTypeId === BlockTypeIds::SHULKER_BOX || $blockTypeId === BlockTypeIds::DYED_SHULKER_BOX){ - return false; - } - return parent::canAddItem($item); - } - - protected function animateBlock(bool $isOpen) : void{ - $holder = $this->getHolder(); - - //event ID is always 1 for a chest - $holder->getWorld()->broadcastPacketToViewers($holder, BlockEventPacket::create(BlockPosition::fromVector3($holder), 1, $isOpen ? 1 : 0)); - } -} diff --git a/src/block/inventory/SmithingTableInventory.php b/src/block/inventory/SmithingTableInventory.php deleted file mode 100644 index 2f67ac9d2dd..00000000000 --- a/src/block/inventory/SmithingTableInventory.php +++ /dev/null @@ -1,37 +0,0 @@ -holder = $holder; - parent::__construct(3); - } -} diff --git a/src/block/inventory/AnvilInventory.php b/src/block/inventory/window/AnvilInventoryWindow.php similarity index 68% rename from src/block/inventory/AnvilInventory.php rename to src/block/inventory/window/AnvilInventoryWindow.php index 7d906a6326e..8137201514f 100644 --- a/src/block/inventory/AnvilInventory.php +++ b/src/block/inventory/window/AnvilInventoryWindow.php @@ -21,20 +21,21 @@ declare(strict_types=1); -namespace pocketmine\block\inventory; +namespace pocketmine\block\inventory\window; +use pocketmine\block\Block; use pocketmine\inventory\SimpleInventory; -use pocketmine\inventory\TemporaryInventory; -use pocketmine\world\Position; - -class AnvilInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{ - use BlockInventoryTrait; +use pocketmine\player\Player; +use pocketmine\player\TemporaryInventoryWindow; +final class AnvilInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{ public const SLOT_INPUT = 0; public const SLOT_MATERIAL = 1; - public function __construct(Position $holder){ - $this->holder = $holder; - parent::__construct(2); + public function __construct( + Player $viewer, + Block $holder + ){ + parent::__construct($viewer, new SimpleInventory(2), $holder); } } diff --git a/src/block/inventory/window/BlockInventoryWindow.php b/src/block/inventory/window/BlockInventoryWindow.php new file mode 100644 index 00000000000..edb45012bc5 --- /dev/null +++ b/src/block/inventory/window/BlockInventoryWindow.php @@ -0,0 +1,57 @@ +holder; } + + public function onOpen() : void{ + parent::onOpen(); + if($this->holder instanceof AnimatedContainer){ + $this->holder->onContainerOpen(); + } + } + + public function onClose() : void{ + if($this->holder instanceof AnimatedContainer){ + $this->holder->onContainerClose(); + } + parent::onClose(); + } +} diff --git a/src/block/inventory/BrewingStandInventory.php b/src/block/inventory/window/BrewingStandInventoryWindow.php similarity index 72% rename from src/block/inventory/BrewingStandInventory.php rename to src/block/inventory/window/BrewingStandInventoryWindow.php index 8bab4ba97b0..ee9af83c653 100644 --- a/src/block/inventory/BrewingStandInventory.php +++ b/src/block/inventory/window/BrewingStandInventoryWindow.php @@ -21,22 +21,12 @@ declare(strict_types=1); -namespace pocketmine\block\inventory; - -use pocketmine\inventory\SimpleInventory; -use pocketmine\world\Position; - -class BrewingStandInventory extends SimpleInventory implements BlockInventory{ - use BlockInventoryTrait; +namespace pocketmine\block\inventory\window; +final class BrewingStandInventoryWindow extends BlockInventoryWindow{ public const SLOT_INGREDIENT = 0; public const SLOT_BOTTLE_LEFT = 1; public const SLOT_BOTTLE_MIDDLE = 2; public const SLOT_BOTTLE_RIGHT = 3; public const SLOT_FUEL = 4; - - public function __construct(Position $holder, int $size = 5){ - $this->holder = $holder; - parent::__construct($size); - } } diff --git a/src/inventory/PlayerCursorInventory.php b/src/block/inventory/window/CartographyTableInventoryWindow.php similarity index 68% rename from src/inventory/PlayerCursorInventory.php rename to src/block/inventory/window/CartographyTableInventoryWindow.php index 30f9e3aec25..c9998231c30 100644 --- a/src/inventory/PlayerCursorInventory.php +++ b/src/block/inventory/window/CartographyTableInventoryWindow.php @@ -21,18 +21,19 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\block\inventory\window; +use pocketmine\block\Block; +use pocketmine\inventory\SimpleInventory; use pocketmine\player\Player; +use pocketmine\player\TemporaryInventoryWindow; + +final class CartographyTableInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{ -class PlayerCursorInventory extends SimpleInventory implements TemporaryInventory{ public function __construct( - protected Player $holder + Player $viewer, + Block $holder ){ - parent::__construct(1); - } - - public function getHolder() : Player{ - return $this->holder; + parent::__construct($viewer, new SimpleInventory(2), $holder); } } diff --git a/src/inventory/PlayerCraftingInventory.php b/src/block/inventory/window/CraftingTableInventoryWindow.php similarity index 68% rename from src/inventory/PlayerCraftingInventory.php rename to src/block/inventory/window/CraftingTableInventoryWindow.php index 75752c9e716..914a307965c 100644 --- a/src/inventory/PlayerCraftingInventory.php +++ b/src/block/inventory/window/CraftingTableInventoryWindow.php @@ -21,16 +21,19 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\block\inventory\window; +use pocketmine\block\Block; use pocketmine\crafting\CraftingGrid; use pocketmine\player\Player; -final class PlayerCraftingInventory extends CraftingGrid implements TemporaryInventory{ +final class CraftingTableInventoryWindow extends BlockInventoryWindow{ - public function __construct(private Player $holder){ - parent::__construct(CraftingGrid::SIZE_SMALL); + public function __construct( + Player $viewer, + Block $holder + ){ + //TODO: generics would be good for this, since it has special methods + parent::__construct($viewer, new CraftingGrid(CraftingGrid::SIZE_BIG), $holder); } - - public function getHolder() : Player{ return $this->holder; } } diff --git a/src/block/inventory/window/DoubleChestInventoryWindow.php b/src/block/inventory/window/DoubleChestInventoryWindow.php new file mode 100644 index 00000000000..3fcf96ac5e9 --- /dev/null +++ b/src/block/inventory/window/DoubleChestInventoryWindow.php @@ -0,0 +1,44 @@ +left); + } + + public function getLeft() : Block{ return $this->left; } + + public function getRight() : Block{ return $this->right; } +} diff --git a/src/block/inventory/window/EnchantingTableInventoryWindow.php b/src/block/inventory/window/EnchantingTableInventoryWindow.php new file mode 100644 index 00000000000..d6ea754a90b --- /dev/null +++ b/src/block/inventory/window/EnchantingTableInventoryWindow.php @@ -0,0 +1,104 @@ + $weakThis */ + $weakThis = \WeakReference::create($this); + $this->listener = new CallbackInventoryListener( + onSlotChange: static function(Inventory $_, int $slot) use ($weakThis) : void{ //remaining params unneeded + if($slot === self::SLOT_INPUT && ($strongThis = $weakThis->get()) !== null){ + $strongThis->regenerateOptions(); + } + }, + onContentChange: static function() use ($weakThis) : void{ + if(($strongThis = $weakThis->get()) !== null){ + $strongThis->regenerateOptions(); + } + } + ); + $this->inventory->getListeners()->add($this->listener); + } + + public function __destruct(){ + $this->inventory->getListeners()->remove($this->listener); + } + + private function regenerateOptions() : void{ + $this->options = []; + $item = $this->getInput(); + $options = Helper::generateOptions($this->holder->getPosition(), $item, $this->viewer->getEnchantmentSeed()); + + $event = new PlayerEnchantingOptionsRequestEvent($this->viewer, $this, $options); + $event->call(); + if(!$event->isCancelled() && count($event->getOptions()) > 0){ + $this->options = array_values($event->getOptions()); + $this->viewer->getNetworkSession()->getInvManager()?->syncEnchantingTableOptions($this->options); + } + } + + public function getInput() : Item{ + return $this->inventory->getItem(self::SLOT_INPUT); + } + + public function getLapis() : Item{ + return $this->inventory->getItem(self::SLOT_LAPIS); + } + + public function getOutput(int $optionId) : ?Item{ + $option = $this->getOption($optionId); + return $option === null ? null : Helper::enchantItem($this->getInput(), $option->getEnchantments()); + } + + public function getOption(int $optionId) : ?EnchantingOption{ + return $this->options[$optionId] ?? null; + } +} diff --git a/src/block/inventory/FurnaceInventory.php b/src/block/inventory/window/FurnaceInventoryWindow.php similarity index 67% rename from src/block/inventory/FurnaceInventory.php rename to src/block/inventory/window/FurnaceInventoryWindow.php index ff44d6b7063..9667bf6315e 100644 --- a/src/block/inventory/FurnaceInventory.php +++ b/src/block/inventory/window/FurnaceInventoryWindow.php @@ -21,51 +21,51 @@ declare(strict_types=1); -namespace pocketmine\block\inventory; +namespace pocketmine\block\inventory\window; +use pocketmine\block\Block; use pocketmine\crafting\FurnaceType; -use pocketmine\inventory\SimpleInventory; +use pocketmine\inventory\Inventory; use pocketmine\item\Item; -use pocketmine\world\Position; - -class FurnaceInventory extends SimpleInventory implements BlockInventory{ - use BlockInventoryTrait; +use pocketmine\player\Player; +final class FurnaceInventoryWindow extends BlockInventoryWindow{ public const SLOT_INPUT = 0; public const SLOT_FUEL = 1; public const SLOT_RESULT = 2; public function __construct( - Position $holder, + Player $viewer, + Inventory $inventory, + Block $holder, private FurnaceType $furnaceType ){ - $this->holder = $holder; - parent::__construct(3); + parent::__construct($viewer, $inventory, $holder); } public function getFurnaceType() : FurnaceType{ return $this->furnaceType; } public function getResult() : Item{ - return $this->getItem(self::SLOT_RESULT); + return $this->inventory->getItem(self::SLOT_RESULT); } public function getFuel() : Item{ - return $this->getItem(self::SLOT_FUEL); + return $this->inventory->getItem(self::SLOT_FUEL); } public function getSmelting() : Item{ - return $this->getItem(self::SLOT_INPUT); + return $this->inventory->getItem(self::SLOT_INPUT); } public function setResult(Item $item) : void{ - $this->setItem(self::SLOT_RESULT, $item); + $this->inventory->setItem(self::SLOT_RESULT, $item); } public function setFuel(Item $item) : void{ - $this->setItem(self::SLOT_FUEL, $item); + $this->inventory->setItem(self::SLOT_FUEL, $item); } public function setSmelting(Item $item) : void{ - $this->setItem(self::SLOT_INPUT, $item); + $this->inventory->setItem(self::SLOT_INPUT, $item); } } diff --git a/src/block/inventory/BlockInventory.php b/src/block/inventory/window/HopperInventoryWindow.php similarity index 84% rename from src/block/inventory/BlockInventory.php rename to src/block/inventory/window/HopperInventoryWindow.php index 063a451a9ab..b89f688720f 100644 --- a/src/block/inventory/BlockInventory.php +++ b/src/block/inventory/window/HopperInventoryWindow.php @@ -21,10 +21,8 @@ declare(strict_types=1); -namespace pocketmine\block\inventory; +namespace pocketmine\block\inventory\window; -use pocketmine\world\Position; +final class HopperInventoryWindow extends BlockInventoryWindow{ -interface BlockInventory{ - public function getHolder() : Position; } diff --git a/src/block/inventory/LoomInventory.php b/src/block/inventory/window/LoomInventoryWindow.php similarity index 69% rename from src/block/inventory/LoomInventory.php rename to src/block/inventory/window/LoomInventoryWindow.php index fd34620a05f..d59050ba451 100644 --- a/src/block/inventory/LoomInventory.php +++ b/src/block/inventory/window/LoomInventoryWindow.php @@ -21,21 +21,23 @@ declare(strict_types=1); -namespace pocketmine\block\inventory; +namespace pocketmine\block\inventory\window; +use pocketmine\block\Block; use pocketmine\inventory\SimpleInventory; -use pocketmine\inventory\TemporaryInventory; -use pocketmine\world\Position; +use pocketmine\player\Player; +use pocketmine\player\TemporaryInventoryWindow; -final class LoomInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{ - use BlockInventoryTrait; +final class LoomInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{ public const SLOT_BANNER = 0; public const SLOT_DYE = 1; public const SLOT_PATTERN = 2; - public function __construct(Position $holder, int $size = 3){ - $this->holder = $holder; - parent::__construct($size); + public function __construct( + Player $viewer, + Block $holder + ){ + parent::__construct($viewer, new SimpleInventory(3), $holder); } } diff --git a/src/block/inventory/HopperInventory.php b/src/block/inventory/window/SmithingTableInventoryWindow.php similarity index 67% rename from src/block/inventory/HopperInventory.php rename to src/block/inventory/window/SmithingTableInventoryWindow.php index a20e9ae1a18..4524ecb2a45 100644 --- a/src/block/inventory/HopperInventory.php +++ b/src/block/inventory/window/SmithingTableInventoryWindow.php @@ -21,16 +21,15 @@ declare(strict_types=1); -namespace pocketmine\block\inventory; +namespace pocketmine\block\inventory\window; +use pocketmine\block\Block; use pocketmine\inventory\SimpleInventory; -use pocketmine\world\Position; +use pocketmine\player\Player; +use pocketmine\player\TemporaryInventoryWindow; -class HopperInventory extends SimpleInventory implements BlockInventory{ - use BlockInventoryTrait; - - public function __construct(Position $holder, int $size = 5){ - $this->holder = $holder; - parent::__construct($size); +final class SmithingTableInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{ + public function __construct(Player $viewer, Block $holder){ + parent::__construct($viewer, new SimpleInventory(3), $holder); } } diff --git a/src/block/inventory/StonecutterInventory.php b/src/block/inventory/window/StonecutterInventoryWindow.php similarity index 68% rename from src/block/inventory/StonecutterInventory.php rename to src/block/inventory/window/StonecutterInventoryWindow.php index 4ed644ff2c3..a17925110fa 100644 --- a/src/block/inventory/StonecutterInventory.php +++ b/src/block/inventory/window/StonecutterInventoryWindow.php @@ -21,19 +21,17 @@ declare(strict_types=1); -namespace pocketmine\block\inventory; +namespace pocketmine\block\inventory\window; +use pocketmine\block\Block; use pocketmine\inventory\SimpleInventory; -use pocketmine\inventory\TemporaryInventory; -use pocketmine\world\Position; - -class StonecutterInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{ - use BlockInventoryTrait; +use pocketmine\player\Player; +use pocketmine\player\TemporaryInventoryWindow; +final class StonecutterInventoryWindow extends BlockInventoryWindow implements TemporaryInventoryWindow{ public const SLOT_INPUT = 0; - public function __construct(Position $holder){ - $this->holder = $holder; - parent::__construct(1); + public function __construct(Player $viewer, Block $holder){ + parent::__construct($viewer, new SimpleInventory(1), $holder); } } diff --git a/src/block/tile/Barrel.php b/src/block/tile/Barrel.php index a7f3532142b..23f7383ca7c 100644 --- a/src/block/tile/Barrel.php +++ b/src/block/tile/Barrel.php @@ -23,20 +23,21 @@ namespace pocketmine\block\tile; -use pocketmine\block\inventory\BarrelInventory; +use pocketmine\inventory\Inventory; +use pocketmine\inventory\SimpleInventory; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; use pocketmine\world\World; -class Barrel extends Spawnable implements Container, Nameable{ +class Barrel extends Spawnable implements ContainerTile, Nameable{ use NameableTrait; - use ContainerTrait; + use ContainerTileTrait; - protected BarrelInventory $inventory; + protected Inventory $inventory; public function __construct(World $world, Vector3 $pos){ parent::__construct($world, $pos); - $this->inventory = new BarrelInventory($this->position); + $this->inventory = new SimpleInventory(27); } public function readSaveData(CompoundTag $nbt) : void{ @@ -49,18 +50,11 @@ protected function writeSaveData(CompoundTag $nbt) : void{ $this->saveItems($nbt); } - public function close() : void{ - if(!$this->closed){ - $this->inventory->removeAllViewers(); - parent::close(); - } - } - - public function getInventory() : BarrelInventory{ + public function getInventory() : Inventory{ return $this->inventory; } - public function getRealInventory() : BarrelInventory{ + public function getRealInventory() : Inventory{ return $this->inventory; } diff --git a/src/block/tile/BrewingStand.php b/src/block/tile/BrewingStand.php index c3a331e6c63..42828671c58 100644 --- a/src/block/tile/BrewingStand.php +++ b/src/block/tile/BrewingStand.php @@ -23,12 +23,12 @@ namespace pocketmine\block\tile; -use pocketmine\block\inventory\BrewingStandInventory; +use pocketmine\block\inventory\window\BrewingStandInventoryWindow; use pocketmine\crafting\BrewingRecipe; use pocketmine\event\block\BrewingFuelUseEvent; use pocketmine\event\block\BrewItemEvent; -use pocketmine\inventory\CallbackInventoryListener; use pocketmine\inventory\Inventory; +use pocketmine\inventory\SimpleInventory; use pocketmine\item\Item; use pocketmine\item\VanillaItems; use pocketmine\math\Vector3; @@ -40,11 +40,11 @@ use function array_map; use function count; -class BrewingStand extends Spawnable implements Container, Nameable{ +class BrewingStand extends Spawnable implements ContainerTile, Nameable{ use NameableTrait { addAdditionalSpawnData as addNameSpawnData; } - use ContainerTrait; + use ContainerTileTrait; public const BREW_TIME_TICKS = 400; // Brew time in ticks @@ -54,7 +54,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{ private const TAG_REMAINING_FUEL_TIME = "Fuel"; //TAG_Byte private const TAG_REMAINING_FUEL_TIME_PE = "FuelAmount"; //TAG_Short - private BrewingStandInventory $inventory; + private Inventory $inventory; private int $brewTime = 0; private int $maxFuelTime = 0; @@ -62,10 +62,7 @@ class BrewingStand extends Spawnable implements Container, Nameable{ public function __construct(World $world, Vector3 $pos){ parent::__construct($world, $pos); - $this->inventory = new BrewingStandInventory($this->position); - $this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange(static function(Inventory $unused) use ($world, $pos) : void{ - $world->scheduleDelayedBlockUpdate($pos, 1); - })); + $this->inventory = new SimpleInventory(5); } public function readSaveData(CompoundTag $nbt) : void{ @@ -104,19 +101,11 @@ public function getDefaultName() : string{ return "Brewing Stand"; } - public function close() : void{ - if(!$this->closed){ - $this->inventory->removeAllViewers(); - - parent::close(); - } - } - - public function getInventory() : BrewingStandInventory{ + public function getInventory() : Inventory{ return $this->inventory; } - public function getRealInventory() : BrewingStandInventory{ + public function getRealInventory() : Inventory{ return $this->inventory; } @@ -132,7 +121,7 @@ private function checkFuel(Item $item) : void{ } $item->pop(); - $this->inventory->setItem(BrewingStandInventory::SLOT_FUEL, $item); + $this->inventory->setItem(BrewingStandInventoryWindow::SLOT_FUEL, $item); $this->maxFuelTime = $this->remainingFuelTime = $ev->getFuelTime(); } @@ -142,14 +131,14 @@ private function checkFuel(Item $item) : void{ * @phpstan-return array */ private function getBrewableRecipes() : array{ - $ingredient = $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT); + $ingredient = $this->inventory->getItem(BrewingStandInventoryWindow::SLOT_INGREDIENT); if($ingredient->isNull()){ return []; } $recipes = []; $craftingManager = $this->position->getWorld()->getServer()->getCraftingManager(); - foreach([BrewingStandInventory::SLOT_BOTTLE_LEFT, BrewingStandInventory::SLOT_BOTTLE_MIDDLE, BrewingStandInventory::SLOT_BOTTLE_RIGHT] as $slot){ + foreach([BrewingStandInventoryWindow::SLOT_BOTTLE_LEFT, BrewingStandInventoryWindow::SLOT_BOTTLE_MIDDLE, BrewingStandInventoryWindow::SLOT_BOTTLE_RIGHT] as $slot){ $input = $this->inventory->getItem($slot); if($input->isNull()){ continue; @@ -176,8 +165,8 @@ public function onUpdate() : bool{ $ret = false; - $fuel = $this->inventory->getItem(BrewingStandInventory::SLOT_FUEL); - $ingredient = $this->inventory->getItem(BrewingStandInventory::SLOT_INGREDIENT); + $fuel = $this->inventory->getItem(BrewingStandInventoryWindow::SLOT_FUEL); + $ingredient = $this->inventory->getItem(BrewingStandInventoryWindow::SLOT_INGREDIENT); $recipes = $this->getBrewableRecipes(); $canBrew = count($recipes) !== 0; @@ -219,7 +208,7 @@ public function onUpdate() : bool{ } $ingredient->pop(); - $this->inventory->setItem(BrewingStandInventory::SLOT_INGREDIENT, $ingredient); + $this->inventory->setItem(BrewingStandInventoryWindow::SLOT_INGREDIENT, $ingredient); $this->brewTime = 0; }else{ diff --git a/src/block/tile/Campfire.php b/src/block/tile/Campfire.php index ad4a193d7b8..4efb70a0d51 100644 --- a/src/block/tile/Campfire.php +++ b/src/block/tile/Campfire.php @@ -23,10 +23,8 @@ namespace pocketmine\block\tile; -use pocketmine\block\Campfire as BlockCampfire; -use pocketmine\block\inventory\CampfireInventory; -use pocketmine\inventory\CallbackInventoryListener; use pocketmine\inventory\Inventory; +use pocketmine\inventory\SimpleInventory; use pocketmine\item\Item; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; @@ -34,8 +32,8 @@ use pocketmine\network\mcpe\convert\TypeConverter; use pocketmine\world\World; -class Campfire extends Spawnable implements Container{ - use ContainerTrait; +class Campfire extends Spawnable implements ContainerTile{ + use ContainerTileTrait; private const TAG_FIRST_INPUT_ITEM = "Item1"; //TAG_Compound private const TAG_SECOND_INPUT_ITEM = "Item2"; //TAG_Compound @@ -47,28 +45,21 @@ class Campfire extends Spawnable implements Container{ private const TAG_THIRD_COOKING_TIME = "ItemTime3"; //TAG_Int private const TAG_FOURTH_COOKING_TIME = "ItemTime4"; //TAG_Int - protected CampfireInventory $inventory; + protected Inventory $inventory; /** @var array */ private array $cookingTimes = []; public function __construct(World $world, Vector3 $pos){ parent::__construct($world, $pos); - $this->inventory = new CampfireInventory($this->position); - $this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange( - static function(Inventory $unused) use ($world, $pos) : void{ - $block = $world->getBlock($pos); - if($block instanceof BlockCampfire){ - $world->setBlock($pos, $block); - } - }) - ); + $this->inventory = new SimpleInventory(4); + $this->inventory->setMaxStackSize(1); } - public function getInventory() : CampfireInventory{ + public function getInventory() : Inventory{ return $this->inventory; } - public function getRealInventory() : CampfireInventory{ + public function getRealInventory() : Inventory{ return $this->inventory; } diff --git a/src/block/tile/Chest.php b/src/block/tile/Chest.php index 4f97eed234a..6cb523c7de2 100644 --- a/src/block/tile/Chest.php +++ b/src/block/tile/Chest.php @@ -23,8 +23,9 @@ namespace pocketmine\block\tile; -use pocketmine\block\inventory\ChestInventory; -use pocketmine\block\inventory\DoubleChestInventory; +use pocketmine\inventory\CombinedInventoryProxy; +use pocketmine\inventory\Inventory; +use pocketmine\inventory\SimpleInventory; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\IntTag; @@ -32,11 +33,11 @@ use pocketmine\world\World; use function abs; -class Chest extends Spawnable implements Container, Nameable{ +class Chest extends Spawnable implements ContainerTile, Nameable{ use NameableTrait { addAdditionalSpawnData as addNameSpawnData; } - use ContainerTrait { + use ContainerTileTrait { onBlockDestroyedHook as containerTraitBlockDestroyedHook; } @@ -44,15 +45,15 @@ class Chest extends Spawnable implements Container, Nameable{ public const TAG_PAIRZ = "pairz"; public const TAG_PAIR_LEAD = "pairlead"; - protected ChestInventory $inventory; - protected ?DoubleChestInventory $doubleInventory = null; + protected Inventory $inventory; + protected ?CombinedInventoryProxy $doubleInventory = null; private ?int $pairX = null; private ?int $pairZ = null; public function __construct(World $world, Vector3 $pos){ parent::__construct($world, $pos); - $this->inventory = new ChestInventory($this->position); + $this->inventory = new SimpleInventory(27); } public function readSaveData(CompoundTag $nbt) : void{ @@ -93,8 +94,6 @@ public function getCleanedNBT() : ?CompoundTag{ public function close() : void{ if(!$this->closed){ - $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(); @@ -114,14 +113,14 @@ protected function onBlockDestroyedHook() : void{ $this->containerTraitBlockDestroyedHook(); } - public function getInventory() : ChestInventory|DoubleChestInventory{ + public function getInventory() : Inventory|CombinedInventoryProxy{ 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() : ChestInventory{ + public function getRealInventory() : Inventory{ return $this->inventory; } @@ -140,9 +139,9 @@ protected function checkPairing() : void{ $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 CombinedInventoryProxy([$pair->inventory, $this->inventory]); }else{ - $this->doubleInventory = $pair->doubleInventory = new DoubleChestInventory($this->inventory, $pair->inventory); + $this->doubleInventory = $pair->doubleInventory = new CombinedInventoryProxy([$this->inventory, $pair->inventory]); } } } diff --git a/src/block/tile/ChiseledBookshelf.php b/src/block/tile/ChiseledBookshelf.php index 06175e27f4d..e8b5e73ec42 100644 --- a/src/block/tile/ChiseledBookshelf.php +++ b/src/block/tile/ChiseledBookshelf.php @@ -37,8 +37,8 @@ use pocketmine\world\World; use function count; -class ChiseledBookshelf extends Tile implements Container{ - use ContainerTrait; +class ChiseledBookshelf extends Tile implements ContainerTile{ + use ContainerTileTrait; private const TAG_LAST_INTERACTED_SLOT = "LastInteractedSlot"; //TAG_Int @@ -86,7 +86,7 @@ protected function writeSaveData(CompoundTag $nbt) : void{ } protected function loadItems(CompoundTag $tag) : void{ - if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){ + if(($inventoryTag = $tag->getTag(ContainerTile::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){ $inventory = $this->getRealInventory(); $listeners = $inventory->getListeners()->toArray(); $inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization @@ -111,7 +111,7 @@ protected function loadItems(CompoundTag $tag) : void{ $inventory->getListeners()->add(...$listeners); } - if(($lockTag = $tag->getTag(Container::TAG_LOCK)) instanceof StringTag){ + if(($lockTag = $tag->getTag(ContainerTile::TAG_LOCK)) instanceof StringTag){ $this->lock = $lockTag->getValue(); } } @@ -130,10 +130,10 @@ protected function saveItems(CompoundTag $tag) : void{ } } - $tag->setTag(Container::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound)); + $tag->setTag(ContainerTile::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound)); if($this->lock !== null){ - $tag->setString(Container::TAG_LOCK, $this->lock); + $tag->setString(ContainerTile::TAG_LOCK, $this->lock); } } } diff --git a/src/block/tile/Container.php b/src/block/tile/ContainerTile.php similarity index 95% rename from src/block/tile/Container.php rename to src/block/tile/ContainerTile.php index dd257dd9c5b..e5ce2dfe19b 100644 --- a/src/block/tile/Container.php +++ b/src/block/tile/ContainerTile.php @@ -26,7 +26,7 @@ use pocketmine\inventory\Inventory; use pocketmine\inventory\InventoryHolder; -interface Container extends InventoryHolder{ +interface ContainerTile extends InventoryHolder{ public const TAG_ITEMS = "Items"; public const TAG_LOCK = "Lock"; diff --git a/src/block/tile/ContainerTrait.php b/src/block/tile/ContainerTileTrait.php similarity index 83% rename from src/block/tile/ContainerTrait.php rename to src/block/tile/ContainerTileTrait.php index fdd050a4169..0f07f51d600 100644 --- a/src/block/tile/ContainerTrait.php +++ b/src/block/tile/ContainerTileTrait.php @@ -34,16 +34,16 @@ use pocketmine\world\Position; /** - * This trait implements most methods in the {@link Container} interface. It should only be used by Tiles. + * This trait implements most methods in the {@link ContainerTile} interface. It should only be used by Tiles. */ -trait ContainerTrait{ +trait ContainerTileTrait{ /** @var string|null */ private $lock = null; abstract public function getRealInventory() : Inventory; protected function loadItems(CompoundTag $tag) : void{ - if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){ + if(($inventoryTag = $tag->getTag(ContainerTile::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){ $inventory = $this->getRealInventory(); $listeners = $inventory->getListeners()->toArray(); $inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization @@ -64,7 +64,7 @@ protected function loadItems(CompoundTag $tag) : void{ $inventory->getListeners()->add(...$listeners); } - if(($lockTag = $tag->getTag(Container::TAG_LOCK)) instanceof StringTag){ + if(($lockTag = $tag->getTag(ContainerTile::TAG_LOCK)) instanceof StringTag){ $this->lock = $lockTag->getValue(); } } @@ -75,15 +75,15 @@ protected function saveItems(CompoundTag $tag) : void{ $items[] = $item->nbtSerialize($slot); } - $tag->setTag(Container::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound)); + $tag->setTag(ContainerTile::TAG_ITEMS, new ListTag($items, NBT::TAG_Compound)); if($this->lock !== null){ - $tag->setString(Container::TAG_LOCK, $this->lock); + $tag->setString(ContainerTile::TAG_LOCK, $this->lock); } } /** - * @see Container::canOpenWith() + * @see ContainerTile::canOpenWith() */ public function canOpenWith(string $key) : bool{ return $this->lock === null || $this->lock === $key; diff --git a/src/block/tile/Furnace.php b/src/block/tile/Furnace.php index a706a827e74..1657a4eed27 100644 --- a/src/block/tile/Furnace.php +++ b/src/block/tile/Furnace.php @@ -24,13 +24,13 @@ namespace pocketmine\block\tile; use pocketmine\block\Furnace as BlockFurnace; -use pocketmine\block\inventory\FurnaceInventory; +use pocketmine\block\inventory\window\FurnaceInventoryWindow; use pocketmine\crafting\FurnaceRecipe; use pocketmine\crafting\FurnaceType; use pocketmine\event\inventory\FurnaceBurnEvent; use pocketmine\event\inventory\FurnaceSmeltEvent; -use pocketmine\inventory\CallbackInventoryListener; use pocketmine\inventory\Inventory; +use pocketmine\inventory\SimpleInventory; use pocketmine\item\Item; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; @@ -40,27 +40,22 @@ use function array_map; use function max; -abstract class Furnace extends Spawnable implements Container, Nameable{ +abstract class Furnace extends Spawnable implements ContainerTile, Nameable{ use NameableTrait; - use ContainerTrait; + use ContainerTileTrait; public const TAG_BURN_TIME = "BurnTime"; public const TAG_COOK_TIME = "CookTime"; public const TAG_MAX_TIME = "MaxTime"; - protected FurnaceInventory $inventory; + protected Inventory $inventory; private int $remainingFuelTime = 0; private int $cookTime = 0; private int $maxFuelTime = 0; public function __construct(World $world, Vector3 $pos){ parent::__construct($world, $pos); - $this->inventory = new FurnaceInventory($this->position, $this->getFurnaceType()); - $this->inventory->getListeners()->add(CallbackInventoryListener::onAnyChange( - static function(Inventory $unused) use ($world, $pos) : void{ - $world->scheduleDelayedBlockUpdate($pos, 1); - }) - ); + $this->inventory = new SimpleInventory(3); } public function readSaveData(CompoundTag $nbt) : void{ @@ -78,10 +73,6 @@ public function readSaveData(CompoundTag $nbt) : void{ $this->loadName($nbt); $this->loadItems($nbt); - - if($this->remainingFuelTime > 0){ - $this->position->getWorld()->scheduleDelayedBlockUpdate($this->position, 1); - } } protected function writeSaveData(CompoundTag $nbt) : void{ @@ -96,19 +87,11 @@ public function getDefaultName() : string{ return "Furnace"; } - public function close() : void{ - if(!$this->closed){ - $this->inventory->removeAllViewers(); - - parent::close(); - } - } - - public function getInventory() : FurnaceInventory{ + public function getInventory() : Inventory{ return $this->inventory; } - public function getRealInventory() : FurnaceInventory{ + public function getRealInventory() : Inventory{ return $this->getInventory(); } @@ -123,7 +106,7 @@ protected function checkFuel(Item $fuel) : void{ $this->onStartSmelting(); if($this->remainingFuelTime > 0 && $ev->isBurning()){ - $this->inventory->setFuel($fuel->getFuelResidue()); + $this->inventory->setItem(FurnaceInventoryWindow::SLOT_FUEL, $fuel->getFuelResidue()); } } @@ -159,9 +142,9 @@ public function onUpdate() : bool{ $ret = false; - $fuel = $this->inventory->getFuel(); - $raw = $this->inventory->getSmelting(); - $product = $this->inventory->getResult(); + $fuel = $this->inventory->getItem(FurnaceInventoryWindow::SLOT_FUEL); + $raw = $this->inventory->getItem(FurnaceInventoryWindow::SLOT_INPUT); + $product = $this->inventory->getItem(FurnaceInventoryWindow::SLOT_RESULT); $furnaceType = $this->getFurnaceType(); $smelt = $this->position->getWorld()->getServer()->getCraftingManager()->getFurnaceRecipeManager($furnaceType)->match($raw); @@ -184,9 +167,9 @@ public function onUpdate() : bool{ $ev->call(); if(!$ev->isCancelled()){ - $this->inventory->setResult($ev->getResult()); + $this->inventory->setItem(FurnaceInventoryWindow::SLOT_RESULT, $ev->getResult()); $raw->pop(); - $this->inventory->setSmelting($raw); + $this->inventory->setItem(FurnaceInventoryWindow::SLOT_INPUT, $raw); } $this->cookTime -= $furnaceType->getCookDurationTicks(); diff --git a/src/block/tile/Hopper.php b/src/block/tile/Hopper.php index 5c39bc2bd5a..988d55c42ed 100644 --- a/src/block/tile/Hopper.php +++ b/src/block/tile/Hopper.php @@ -23,24 +23,25 @@ namespace pocketmine\block\tile; -use pocketmine\block\inventory\HopperInventory; +use pocketmine\inventory\Inventory; +use pocketmine\inventory\SimpleInventory; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; use pocketmine\world\World; -class Hopper extends Spawnable implements Container, Nameable{ +class Hopper extends Spawnable implements ContainerTile, Nameable{ - use ContainerTrait; + use ContainerTileTrait; use NameableTrait; private const TAG_TRANSFER_COOLDOWN = "TransferCooldown"; - private HopperInventory $inventory; + private Inventory $inventory; private int $transferCooldown = 0; public function __construct(World $world, Vector3 $pos){ parent::__construct($world, $pos); - $this->inventory = new HopperInventory($this->position); + $this->inventory = new SimpleInventory(5); } public function readSaveData(CompoundTag $nbt) : void{ @@ -57,23 +58,15 @@ protected function writeSaveData(CompoundTag $nbt) : void{ $nbt->setInt(self::TAG_TRANSFER_COOLDOWN, $this->transferCooldown); } - public function close() : void{ - if(!$this->closed){ - $this->inventory->removeAllViewers(); - - parent::close(); - } - } - public function getDefaultName() : string{ return "Hopper"; } - public function getInventory() : HopperInventory{ + public function getInventory() : Inventory{ return $this->inventory; } - public function getRealInventory() : HopperInventory{ + public function getRealInventory() : Inventory{ return $this->inventory; } } diff --git a/src/block/tile/ShulkerBox.php b/src/block/tile/ShulkerBox.php index a30b75c4ea8..a7d5b9617cc 100644 --- a/src/block/tile/ShulkerBox.php +++ b/src/block/tile/ShulkerBox.php @@ -23,28 +23,42 @@ namespace pocketmine\block\tile; -use pocketmine\block\inventory\ShulkerBoxInventory; +use pocketmine\block\BlockTypeIds; +use pocketmine\inventory\Inventory; +use pocketmine\inventory\SimpleInventory; +use pocketmine\inventory\transaction\action\validator\CallbackSlotValidator; +use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\item\Item; +use pocketmine\item\ItemTypeIds; use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\nbt\tag\CompoundTag; use pocketmine\world\World; -class ShulkerBox extends Spawnable implements Container, Nameable{ +class ShulkerBox extends Spawnable implements ContainerTile, Nameable{ use NameableTrait { addAdditionalSpawnData as addNameSpawnData; } - use ContainerTrait; + use ContainerTileTrait; public const TAG_FACING = "facing"; protected int $facing = Facing::NORTH; - protected ShulkerBoxInventory $inventory; + protected Inventory $inventory; public function __construct(World $world, Vector3 $pos){ parent::__construct($world, $pos); - $this->inventory = new ShulkerBoxInventory($this->position); + $this->inventory = new SimpleInventory(27); + + $this->inventory->getSlotValidators()->add(new CallbackSlotValidator(static function(Inventory $_, Item $item) : ?TransactionValidationException{ //remaining params not needed + $blockTypeId = ItemTypeIds::toBlockTypeId($item->getTypeId()); + if($blockTypeId === BlockTypeIds::SHULKER_BOX || $blockTypeId === BlockTypeIds::DYED_SHULKER_BOX){ + return new TransactionValidationException("Shulker box inventory cannot contain shulker boxes"); + } + + return null; + })); } public function readSaveData(CompoundTag $nbt) : void{ @@ -66,13 +80,6 @@ public function copyDataFromItem(Item $item) : void{ } } - public function close() : void{ - if(!$this->closed){ - $this->inventory->removeAllViewers(); - parent::close(); - } - } - protected function onBlockDestroyedHook() : void{ //NOOP override of ContainerTrait - shulker boxes retain their contents when destroyed } @@ -93,11 +100,11 @@ public function setFacing(int $facing) : void{ $this->facing = $facing; } - public function getInventory() : ShulkerBoxInventory{ + public function getInventory() : Inventory{ return $this->inventory; } - public function getRealInventory() : ShulkerBoxInventory{ + public function getRealInventory() : Inventory{ return $this->inventory; } diff --git a/src/block/inventory/CampfireInventory.php b/src/block/utils/AnimatedContainer.php similarity index 61% rename from src/block/inventory/CampfireInventory.php rename to src/block/utils/AnimatedContainer.php index ae762473e23..9af901f18c0 100644 --- a/src/block/inventory/CampfireInventory.php +++ b/src/block/utils/AnimatedContainer.php @@ -21,20 +21,18 @@ declare(strict_types=1); -namespace pocketmine\block\inventory; +namespace pocketmine\block\utils; -use pocketmine\inventory\SimpleInventory; -use pocketmine\world\Position; +interface AnimatedContainer{ + /** + * 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; -class CampfireInventory extends SimpleInventory implements BlockInventory{ - use BlockInventoryTrait; - - public function __construct(Position $holder){ - $this->holder = $holder; - parent::__construct(4); - } - - public function getMaxStackSize() : int{ - return 1; - } + /** + * 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; } diff --git a/src/block/utils/AnimatedContainerTrait.php b/src/block/utils/AnimatedContainerTrait.php new file mode 100644 index 00000000000..ab1337b1b85 --- /dev/null +++ b/src/block/utils/AnimatedContainerTrait.php @@ -0,0 +1,71 @@ +getPosition(); + $tile = $position->getWorld()->getTile($position); + if($tile instanceof InventoryHolder){ + return count($tile->getInventory()->getViewers()); + } + return 0; + } + + abstract protected function getContainerOpenSound() : Sound; + + abstract protected function getContainerCloseSound() : Sound; + + abstract protected function doContainerAnimation(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()); + } + + abstract protected function getPosition() : Position; + + protected function doContainerEffects(bool $isOpen) : void{ + $position = $this->getPosition(); + $this->doContainerAnimation($position, $isOpen); + $this->playContainerSound($position, $isOpen); + } + + public function onContainerOpen() : void{ + if($this->getContainerViewerCount() === 1){ + $this->doContainerEffects(true); + } + } + + public function onContainerClose() : void{ + if($this->getContainerViewerCount() === 1){ + $this->doContainerEffects(false); + } + } +} diff --git a/src/block/utils/BrewingStandSlot.php b/src/block/utils/BrewingStandSlot.php index c7d74d8da03..51f0df50190 100644 --- a/src/block/utils/BrewingStandSlot.php +++ b/src/block/utils/BrewingStandSlot.php @@ -23,7 +23,7 @@ namespace pocketmine\block\utils; -use pocketmine\block\inventory\BrewingStandInventory; +use pocketmine\block\inventory\window\BrewingStandInventoryWindow; enum BrewingStandSlot{ case EAST; @@ -35,9 +35,9 @@ enum BrewingStandSlot{ */ public function getSlotNumber() : int{ return match($this){ - self::EAST => BrewingStandInventory::SLOT_BOTTLE_LEFT, - self::NORTHWEST => BrewingStandInventory::SLOT_BOTTLE_MIDDLE, - self::SOUTHWEST => BrewingStandInventory::SLOT_BOTTLE_RIGHT + self::EAST => BrewingStandInventoryWindow::SLOT_BOTTLE_LEFT, + self::NORTHWEST => BrewingStandInventoryWindow::SLOT_BOTTLE_MIDDLE, + self::SOUTHWEST => BrewingStandInventoryWindow::SLOT_BOTTLE_RIGHT }; } } diff --git a/src/command/defaults/EnchantCommand.php b/src/command/defaults/EnchantCommand.php index 191a146b036..6b73aad6c41 100644 --- a/src/command/defaults/EnchantCommand.php +++ b/src/command/defaults/EnchantCommand.php @@ -56,7 +56,7 @@ public function execute(CommandSender $sender, string $commandLabel, array $args return true; } - $item = $player->getInventory()->getItemInHand(); + $item = $player->getHotbar()->getHeldItem(); if($item->isNull()){ $sender->sendMessage(KnownTranslationFactory::commands_enchant_noItem()); @@ -79,7 +79,7 @@ public function execute(CommandSender $sender, string $commandLabel, array $args //this is necessary to deal with enchanted books, which are a different item type than regular books $enchantedItem = EnchantingHelper::enchantItem($item, [new EnchantmentInstance($enchantment, $level)]); - $player->getInventory()->setItemInHand($enchantedItem); + $player->getHotbar()->setHeldItem($enchantedItem); self::broadcastCommandMessage($sender, KnownTranslationFactory::commands_enchant_success($player->getName())); return true; diff --git a/src/crafting/CraftingGrid.php b/src/crafting/CraftingGrid.php index a41b5e3a73d..47cca94baed 100644 --- a/src/crafting/CraftingGrid.php +++ b/src/crafting/CraftingGrid.php @@ -29,7 +29,7 @@ use function min; use const PHP_INT_MAX; -abstract class CraftingGrid extends SimpleInventory{ +class CraftingGrid extends SimpleInventory{ public const SIZE_SMALL = 2; public const SIZE_BIG = 3; diff --git a/src/entity/ExperienceManager.php b/src/entity/ExperienceManager.php index 9cff48f330c..5e04728f8c5 100644 --- a/src/entity/ExperienceManager.php +++ b/src/entity/ExperienceManager.php @@ -243,7 +243,7 @@ public function onPickupXp(int $xpValue) : void{ //TODO: replace this with a more generic equipment getting/setting interface $equipment = []; - if(($item = $this->entity->getInventory()->getItemInHand()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){ + if(($item = $this->entity->getHotbar()->getHeldItem()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){ $equipment[$mainHandIndex] = $item; } if(($item = $this->entity->getOffHandInventory()->getItem(0)) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){ @@ -263,7 +263,7 @@ public function onPickupXp(int $xpValue) : void{ $xpValue -= (int) ceil($repairAmount / 2); if($k === $mainHandIndex){ - $this->entity->getInventory()->setItemInHand($repairItem); + $this->entity->getHotbar()->setHeldItem($repairItem); }elseif($k === $offHandIndex){ $this->entity->getOffHandInventory()->setItem(0, $repairItem); }else{ diff --git a/src/entity/Human.php b/src/entity/Human.php index a38a81dcbe1..bf96bc43dad 100644 --- a/src/entity/Human.php +++ b/src/entity/Human.php @@ -32,11 +32,10 @@ use pocketmine\event\entity\EntityDamageEvent; use pocketmine\event\player\PlayerExhaustEvent; use pocketmine\inventory\CallbackInventoryListener; +use pocketmine\inventory\Hotbar; use pocketmine\inventory\Inventory; use pocketmine\inventory\InventoryHolder; -use pocketmine\inventory\PlayerEnderInventory; -use pocketmine\inventory\PlayerInventory; -use pocketmine\inventory\PlayerOffHandInventory; +use pocketmine\inventory\SimpleInventory; use pocketmine\item\enchantment\EnchantingHelper; use pocketmine\item\enchantment\VanillaEnchantments; use pocketmine\item\Item; @@ -101,9 +100,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ public function getNetworkTypeId() : string{ return EntityIds::PLAYER; } - protected PlayerInventory $inventory; - protected PlayerOffHandInventory $offHandInventory; - protected PlayerEnderInventory $enderInventory; + protected Hotbar $hotbar; + protected Inventory $inventory; + protected Inventory $offHandInventory; + protected Inventory $enderInventory; protected UuidInterface $uuid; @@ -237,13 +237,17 @@ public function getXpDropAmount() : int{ return min(100, 7 * $this->xpManager->getXpLevel()); } - public function getInventory() : PlayerInventory{ + public function getHotbar() : Hotbar{ + return $this->hotbar; + } + + public function getInventory() : Inventory{ return $this->inventory; } - public function getOffHandInventory() : PlayerOffHandInventory{ return $this->offHandInventory; } + public function getOffHandInventory() : Inventory{ return $this->offHandInventory; } - public function getEnderInventory() : PlayerEnderInventory{ + public function getEnderInventory() : Inventory{ return $this->enderInventory; } @@ -274,25 +278,27 @@ protected function initEntity(CompoundTag $nbt) : void{ $this->hungerManager = new HungerManager($this); $this->xpManager = new ExperienceManager($this); - $this->inventory = new PlayerInventory($this); + $this->inventory = new SimpleInventory(36); + $this->hotbar = new Hotbar($this->inventory); + $syncHeldItem = fn() => NetworkBroadcastUtils::broadcastEntityEvent( $this->getViewers(), fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this) ); $this->inventory->getListeners()->add(new CallbackInventoryListener( function(Inventory $unused, int $slot, Item $unused2) use ($syncHeldItem) : void{ - if($slot === $this->inventory->getHeldItemIndex()){ + if($slot === $this->hotbar->getSelectedIndex()){ $syncHeldItem(); } }, function(Inventory $unused, array $oldItems) use ($syncHeldItem) : void{ - if(array_key_exists($this->inventory->getHeldItemIndex(), $oldItems)){ + if(array_key_exists($this->hotbar->getSelectedIndex(), $oldItems)){ $syncHeldItem(); } } )); - $this->offHandInventory = new PlayerOffHandInventory($this); - $this->enderInventory = new PlayerEnderInventory($this); + $this->offHandInventory = new SimpleInventory(1); + $this->enderInventory = new SimpleInventory(27); $this->initHumanData($nbt); $inventoryTag = $nbt->getListTag(self::TAG_INVENTORY); @@ -335,8 +341,9 @@ function(Inventory $unused, array $oldItems) use ($syncHeldItem) : void{ self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems); } - $this->inventory->setHeldItemIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0)); - $this->inventory->getHeldItemIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent( + $this->hotbar->setSelectedIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0)); + //TODO: cyclic reference + $this->hotbar->getSelectedIndexChangeListeners()->add(fn() => NetworkBroadcastUtils::broadcastEntityEvent( $this->getViewers(), fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobMainHandItemChange($recipients, $this) )); @@ -376,7 +383,7 @@ public function applyDamageModifiers(EntityDamageEvent $source) : void{ $type = $source->getCause(); if($type !== EntityDamageEvent::CAUSE_SUICIDE && $type !== EntityDamageEvent::CAUSE_VOID - && ($this->inventory->getItemInHand() instanceof Totem || $this->offHandInventory->getItem(0) instanceof Totem)){ + && ($this->hotbar->getHeldItem() instanceof Totem || $this->offHandInventory->getItem(0) instanceof Totem)){ $compensation = $this->getHealth() - $source->getFinalDamage() - 1; if($compensation <= -1){ @@ -398,10 +405,10 @@ protected function applyPostDamageEffects(EntityDamageEvent $source) : void{ $this->broadcastAnimation(new TotemUseAnimation($this)); $this->broadcastSound(new TotemUseSound()); - $hand = $this->inventory->getItemInHand(); + $hand = $this->hotbar->getHeldItem(); if($hand instanceof Totem){ $hand->pop(); //Plugins could alter max stack size - $this->inventory->setItemInHand($hand); + $this->hotbar->setHeldItem($hand); }elseif(($offHand = $this->offHandInventory->getItem(0)) instanceof Totem){ $offHand->pop(); $this->offHandInventory->setItem(0, $offHand); @@ -434,8 +441,8 @@ public function saveNBT() : CompoundTag{ $nbt->setTag(self::TAG_INVENTORY, $inventoryTag); //Normal inventory - $slotCount = $this->inventory->getSize() + $this->inventory->getHotbarSize(); - for($slot = $this->inventory->getHotbarSize(); $slot < $slotCount; ++$slot){ + $slotCount = $this->inventory->getSize() + $this->hotbar->getSize(); + for($slot = $this->hotbar->getSize(); $slot < $slotCount; ++$slot){ $item = $this->inventory->getItem($slot - 9); if(!$item->isNull()){ $inventoryTag->push($item->nbtSerialize($slot)); @@ -450,7 +457,7 @@ public function saveNBT() : CompoundTag{ } } - $nbt->setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->inventory->getHeldItemIndex()); + $nbt->setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->hotbar->getSelectedIndex()); $offHandItem = $this->offHandInventory->getItem(0); if(!$offHandItem->isNull()){ @@ -504,7 +511,7 @@ protected function sendSpawnPacket(Player $player) : void{ $this->location->pitch, $this->location->yaw, $this->location->yaw, //TODO: head yaw - ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($this->getInventory()->getItemInHand())), + ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($this->hotbar->getHeldItem())), GameMode::SURVIVAL, $this->getAllNetworkData(), new PropertySyncData([], []), @@ -538,8 +545,8 @@ public function getOffsetPosition(Vector3 $vector3) : Vector3{ } protected function onDispose() : void{ + $this->hotbar->getSelectedIndexChangeListeners()->clear(); $this->inventory->removeAllViewers(); - $this->inventory->getHeldItemIndexChangeListeners()->clear(); $this->offHandInventory->removeAllViewers(); $this->enderInventory->removeAllViewers(); parent::onDispose(); @@ -547,9 +554,6 @@ protected function onDispose() : void{ protected function destroyCycles() : void{ unset( - $this->inventory, - $this->offHandInventory, - $this->enderInventory, $this->hungerManager, $this->xpManager ); diff --git a/src/entity/Living.php b/src/entity/Living.php index 852344784a6..9a1a28d835b 100644 --- a/src/entity/Living.php +++ b/src/entity/Living.php @@ -151,7 +151,7 @@ protected function initEntity(CompoundTag $nbt) : void{ $this->effectManager->getEffectAddHooks()->add(function() : void{ $this->networkPropertiesDirty = true; }); $this->effectManager->getEffectRemoveHooks()->add(function() : void{ $this->networkPropertiesDirty = true; }); - $this->armorInventory = new ArmorInventory($this); + $this->armorInventory = new ArmorInventory(); //TODO: load/save armor inventory contents $this->armorInventory->getListeners()->add(CallbackInventoryListener::onAnyChange(fn() => NetworkBroadcastUtils::broadcastEntityEvent( $this->getViewers(), @@ -983,7 +983,6 @@ protected function onDispose() : void{ protected function destroyCycles() : void{ unset( - $this->armorInventory, $this->effectManager ); parent::destroyCycles(); diff --git a/src/event/inventory/InventoryCloseEvent.php b/src/event/inventory/InventoryCloseEvent.php index 4401a71264b..be5415e27f9 100644 --- a/src/event/inventory/InventoryCloseEvent.php +++ b/src/event/inventory/InventoryCloseEvent.php @@ -23,12 +23,12 @@ namespace pocketmine\event\inventory; -use pocketmine\inventory\Inventory; +use pocketmine\player\InventoryWindow; use pocketmine\player\Player; class InventoryCloseEvent extends InventoryEvent{ public function __construct( - Inventory $inventory, + InventoryWindow $inventory, private Player $who ){ parent::__construct($inventory); diff --git a/src/event/inventory/InventoryEvent.php b/src/event/inventory/InventoryEvent.php index 4202946791b..30250505ba8 100644 --- a/src/event/inventory/InventoryEvent.php +++ b/src/event/inventory/InventoryEvent.php @@ -27,15 +27,15 @@ namespace pocketmine\event\inventory; use pocketmine\event\Event; -use pocketmine\inventory\Inventory; +use pocketmine\player\InventoryWindow; use pocketmine\player\Player; abstract class InventoryEvent extends Event{ public function __construct( - protected Inventory $inventory + protected InventoryWindow $inventory ){} - public function getInventory() : Inventory{ + public function getInventory() : InventoryWindow{ return $this->inventory; } @@ -43,6 +43,6 @@ public function getInventory() : Inventory{ * @return Player[] */ public function getViewers() : array{ - return $this->inventory->getViewers(); + return $this->inventory->getInventory()->getViewers(); } } diff --git a/src/event/inventory/InventoryOpenEvent.php b/src/event/inventory/InventoryOpenEvent.php index f45056bf458..44275b671b3 100644 --- a/src/event/inventory/InventoryOpenEvent.php +++ b/src/event/inventory/InventoryOpenEvent.php @@ -25,14 +25,14 @@ use pocketmine\event\Cancellable; use pocketmine\event\CancellableTrait; -use pocketmine\inventory\Inventory; +use pocketmine\player\InventoryWindow; use pocketmine\player\Player; class InventoryOpenEvent extends InventoryEvent implements Cancellable{ use CancellableTrait; public function __construct( - Inventory $inventory, + InventoryWindow $inventory, private Player $who ){ parent::__construct($inventory); diff --git a/src/event/player/PlayerEnchantingOptionsRequestEvent.php b/src/event/player/PlayerEnchantingOptionsRequestEvent.php index 833185f7605..14e0ed73c4a 100644 --- a/src/event/player/PlayerEnchantingOptionsRequestEvent.php +++ b/src/event/player/PlayerEnchantingOptionsRequestEvent.php @@ -23,10 +23,9 @@ namespace pocketmine\event\player; -use pocketmine\block\inventory\EnchantInventory; +use pocketmine\block\inventory\window\EnchantingTableInventoryWindow; use pocketmine\event\Cancellable; use pocketmine\event\CancellableTrait; -use pocketmine\event\Event; use pocketmine\item\enchantment\EnchantingOption; use pocketmine\player\Player; use pocketmine\utils\Utils; @@ -44,13 +43,13 @@ class PlayerEnchantingOptionsRequestEvent extends PlayerEvent implements Cancell */ public function __construct( Player $player, - private readonly EnchantInventory $inventory, + private readonly EnchantingTableInventoryWindow $inventory, private array $options ){ $this->player = $player; } - public function getInventory() : EnchantInventory{ + public function getInventory() : EnchantingTableInventoryWindow{ return $this->inventory; } diff --git a/src/inventory/ArmorInventory.php b/src/inventory/ArmorInventory.php index 8591cc65bf3..6985f460a61 100644 --- a/src/inventory/ArmorInventory.php +++ b/src/inventory/ArmorInventory.php @@ -24,7 +24,6 @@ namespace pocketmine\inventory; use pocketmine\block\BlockTypeIds; -use pocketmine\entity\Living; use pocketmine\inventory\transaction\action\validator\CallbackSlotValidator; use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\item\Armor; @@ -37,18 +36,12 @@ class ArmorInventory extends SimpleInventory{ public const SLOT_LEGS = 2; public const SLOT_FEET = 3; - public function __construct( - protected Living $holder - ){ + public function __construct(){ parent::__construct(4); $this->validators->add(new CallbackSlotValidator(self::validate(...))); } - public function getHolder() : Living{ - return $this->holder; - } - public function getHelmet() : Item{ return $this->getItem(self::SLOT_HEAD); } diff --git a/src/inventory/BaseInventory.php b/src/inventory/BaseInventory.php index 0d5d1ffe602..adf8d3ff747 100644 --- a/src/inventory/BaseInventory.php +++ b/src/inventory/BaseInventory.php @@ -102,28 +102,14 @@ public function setContents(array $items) : void{ $listeners = $this->listeners->toArray(); $this->listeners->clear(); - $viewers = $this->viewers; - $this->viewers = []; $this->internalSetContents($items); $this->listeners->add(...$listeners); //don't directly write, in case listeners were added while operation was in progress - foreach($viewers as $id => $viewer){ - $this->viewers[$id] = $viewer; - } $this->onContentChange($oldContents); } - /** - * Helper for utility functions which search the inventory. - * TODO: make this abstract instead of providing a slow default implementation (BC break) - */ - protected function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{ - $item = $this->getItem($slot); - return $item->equals($test, true, $checkTags) ? $item->getCount() : 0; - } - public function contains(Item $item) : bool{ $count = max(1, $item->getCount()); $checkTags = $item->hasNamedTag(); @@ -351,7 +337,7 @@ public function getViewers() : array{ */ public function removeAllViewers() : void{ foreach($this->viewers as $hash => $viewer){ - if($viewer->getCurrentWindow() === $this){ //this might not be the case for the player's own inventory + if($viewer->getCurrentWindow()?->getInventory() === $this){ //this might not be the case for the player's own inventory $viewer->removeCurrentWindow(); } unset($this->viewers[$hash]); @@ -370,13 +356,6 @@ protected function onSlotChange(int $index, Item $before) : void{ foreach($this->listeners as $listener){ $listener->onSlotChange($this, $index, $before); } - foreach($this->viewers as $viewer){ - $invManager = $viewer->getNetworkSession()->getInvManager(); - if($invManager === null){ - continue; - } - $invManager->onSlotChange($this, $index); - } } /** @@ -387,14 +366,6 @@ protected function onContentChange(array $itemsBefore) : void{ foreach($this->listeners as $listener){ $listener->onContentChange($this, $itemsBefore); } - - foreach($this->getViewers() as $viewer){ - $invManager = $viewer->getNetworkSession()->getInvManager(); - if($invManager === null){ - continue; - } - $invManager->syncContents($this); - } } public function slotExists(int $slot) : bool{ diff --git a/src/inventory/CombinedInventoryProxy.php b/src/inventory/CombinedInventoryProxy.php new file mode 100644 index 00000000000..5b6d1a668db --- /dev/null +++ b/src/inventory/CombinedInventoryProxy.php @@ -0,0 +1,194 @@ + + */ + private array $backingInventories = []; + /** + * @var Inventory[] + * @phpstan-var array + */ + private array $slotToInventoryMap = []; + /** + * @var int[] + * @phpstan-var array + */ + private array $inventoryToOffsetMap = []; + + private InventoryListener $backingInventoryListener; + private bool $modifyingBackingInventory = false; + + /** + * @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; + + $weakThis = \WeakReference::create($this); + $getThis = static fn() => $weakThis->get() ?? throw new AssumptionFailedError("Listener should've been unregistered in __destruct()"); + + $this->backingInventoryListener = new CallbackInventoryListener( + onSlotChange: static function(Inventory $inventory, int $slot, Item $oldItem) use ($getThis) : void{ + $strongThis = $getThis(); + if($strongThis->modifyingBackingInventory){ + return; + } + + $offset = $strongThis->inventoryToOffsetMap[spl_object_id($inventory)]; + $strongThis->onSlotChange($offset + $slot, $oldItem); + }, + onContentChange: static function(Inventory $inventory, array $oldContents) use ($getThis) : void{ + $strongThis = $getThis(); + if($strongThis->modifyingBackingInventory){ + return; + } + + if(count($strongThis->backingInventories) === 1){ + $strongThis->onContentChange($oldContents); + }else{ + $offset = $strongThis->inventoryToOffsetMap[spl_object_id($inventory)]; + for($slot = 0, $limit = $inventory->getSize(); $slot < $limit; $slot++){ + $strongThis->onSlotChange($offset + $slot, $oldContents[$slot] ?? VanillaItems::AIR()); + } + } + } + ); + foreach($this->backingInventories as $inventory){ + $inventory->getListeners()->add($this->backingInventoryListener); + } + } + + public function __destruct(){ + foreach($this->backingInventories as $inventory){ + $inventory->getListeners()->remove($this->backingInventoryListener); + } + } + + /** + * @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); + + //Make sure our backing listener doesn't dispatch double updates to our own listeners + $this->modifyingBackingInventory = true; + try{ + $inventory->setItem($actualSlot, $item); + }finally{ + $this->modifyingBackingInventory = false; + } + } + + 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]; + + //Make sure our backing listener doesn't dispatch double updates to our own listeners + $this->modifyingBackingInventory = true; + try{ + $backingInventory->setContents($backingInventoryContents); + }finally{ + $this->modifyingBackingInventory = false; + } + } + } + + 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/src/inventory/DelegateInventory.php b/src/inventory/DelegateInventory.php deleted file mode 100644 index a211732cf46..00000000000 --- a/src/inventory/DelegateInventory.php +++ /dev/null @@ -1,103 +0,0 @@ -backingInventory->getListeners()->add($this->inventoryListener = new CallbackInventoryListener( - static function(Inventory $unused, int $slot, Item $oldItem) use ($weakThis) : void{ - if(($strongThis = $weakThis->get()) !== null){ - $strongThis->backingInventoryChanging = true; - try{ - $strongThis->onSlotChange($slot, $oldItem); - }finally{ - $strongThis->backingInventoryChanging = false; - } - } - }, - static function(Inventory $unused, array $oldContents) use ($weakThis) : void{ - if(($strongThis = $weakThis->get()) !== null){ - $strongThis->backingInventoryChanging = true; - try{ - $strongThis->onContentChange($oldContents); - }finally{ - $strongThis->backingInventoryChanging = false; - } - } - } - )); - } - - public function __destruct(){ - $this->backingInventory->getListeners()->remove($this->inventoryListener); - } - - public function getSize() : int{ - return $this->backingInventory->getSize(); - } - - public function getItem(int $index) : Item{ - return $this->backingInventory->getItem($index); - } - - protected function internalSetItem(int $index, Item $item) : void{ - $this->backingInventory->setItem($index, $item); - } - - public function getContents(bool $includeEmpty = false) : array{ - return $this->backingInventory->getContents($includeEmpty); - } - - protected function internalSetContents(array $items) : void{ - $this->backingInventory->setContents($items); - } - - public function isSlotEmpty(int $index) : bool{ - return $this->backingInventory->isSlotEmpty($index); - } - - protected function onSlotChange(int $index, Item $before) : void{ - if($this->backingInventoryChanging){ - parent::onSlotChange($index, $before); - } - } - - protected function onContentChange(array $itemsBefore) : void{ - if($this->backingInventoryChanging){ - parent::onContentChange($itemsBefore); - } - } -} diff --git a/src/inventory/PlayerInventory.php b/src/inventory/Hotbar.php similarity index 63% rename from src/inventory/PlayerInventory.php rename to src/inventory/Hotbar.php index fdaa0adff88..a98a868d458 100644 --- a/src/inventory/PlayerInventory.php +++ b/src/inventory/Hotbar.php @@ -23,30 +23,30 @@ namespace pocketmine\inventory; -use pocketmine\entity\Human; use pocketmine\item\Item; -use pocketmine\player\Player; use pocketmine\utils\ObjectSet; -class PlayerInventory extends SimpleInventory{ - - protected Human $holder; - protected int $itemInHandIndex = 0; +final class Hotbar{ + protected int $selectedIndex = 0; /** * @var \Closure[]|ObjectSet * @phpstan-var ObjectSet<\Closure(int $oldIndex) : void> */ - protected ObjectSet $heldItemIndexChangeListeners; - - public function __construct(Human $player){ - $this->holder = $player; - $this->heldItemIndexChangeListeners = new ObjectSet(); - parent::__construct(36); + protected ObjectSet $selectedIndexChangeListeners; + + public function __construct( + private Inventory $inventory, + private int $size = 9 + ){ + if($this->inventory->getSize() < $this->size){ + throw new \InvalidArgumentException("Inventory size must be at least $this->size"); + } + $this->selectedIndexChangeListeners = new ObjectSet(); } public function isHotbarSlot(int $slot) : bool{ - return $slot >= 0 && $slot < $this->getHotbarSize(); + return $slot >= 0 && $slot < $this->getSize(); } /** @@ -54,7 +54,7 @@ public function isHotbarSlot(int $slot) : bool{ */ private function throwIfNotHotbarSlot(int $slot) : void{ if(!$this->isHotbarSlot($slot)){ - throw new \InvalidArgumentException("$slot is not a valid hotbar slot index (expected 0 - " . ($this->getHotbarSize() - 1) . ")"); + throw new \InvalidArgumentException("$slot is not a valid hotbar slot index (expected 0 - " . ($this->getSize() - 1) . ")"); } } @@ -65,14 +65,14 @@ private function throwIfNotHotbarSlot(int $slot) : void{ */ public function getHotbarSlotItem(int $hotbarSlot) : Item{ $this->throwIfNotHotbarSlot($hotbarSlot); - return $this->getItem($hotbarSlot); + return $this->inventory->getItem($hotbarSlot); } /** * Returns the hotbar slot number the holder is currently holding. */ - public function getHeldItemIndex() : int{ - return $this->itemInHandIndex; + public function getSelectedIndex() : int{ + return $this->selectedIndex; } /** @@ -82,13 +82,13 @@ public function getHeldItemIndex() : int{ * * @throws \InvalidArgumentException if the hotbar slot is out of range */ - public function setHeldItemIndex(int $hotbarSlot) : void{ + public function setSelectedIndex(int $hotbarSlot) : void{ $this->throwIfNotHotbarSlot($hotbarSlot); - $oldIndex = $this->itemInHandIndex; - $this->itemInHandIndex = $hotbarSlot; + $oldIndex = $this->selectedIndex; + $this->selectedIndex = $hotbarSlot; - foreach($this->heldItemIndexChangeListeners as $callback){ + foreach($this->selectedIndexChangeListeners as $callback){ $callback($oldIndex); } } @@ -97,30 +97,26 @@ public function setHeldItemIndex(int $hotbarSlot) : void{ * @return \Closure[]|ObjectSet * @phpstan-return ObjectSet<\Closure(int $oldIndex) : void> */ - public function getHeldItemIndexChangeListeners() : ObjectSet{ return $this->heldItemIndexChangeListeners; } + public function getSelectedIndexChangeListeners() : ObjectSet{ return $this->selectedIndexChangeListeners; } /** * Returns the currently-held item. */ - public function getItemInHand() : Item{ - return $this->getHotbarSlotItem($this->itemInHandIndex); + public function getHeldItem() : Item{ + return $this->getHotbarSlotItem($this->selectedIndex); } /** * Sets the item in the currently-held slot to the specified item. */ - public function setItemInHand(Item $item) : void{ - $this->setItem($this->getHeldItemIndex(), $item); + public function setHeldItem(Item $item) : void{ + $this->inventory->setItem($this->getSelectedIndex(), $item); } /** * Returns the number of slots in the hotbar. */ - public function getHotbarSize() : int{ - return 9; - } - - public function getHolder() : Human{ - return $this->holder; + public function getSize() : int{ + return $this->size; } } diff --git a/src/inventory/Inventory.php b/src/inventory/Inventory.php index 5c81d7d9c86..de460ca2927 100644 --- a/src/inventory/Inventory.php +++ b/src/inventory/Inventory.php @@ -98,6 +98,13 @@ public function canAddItem(Item $item) : bool; */ public function getAddableItemQuantity(Item $item) : int; + /** + * Returns the number of items in the inventory that match the given item. + * + * @param bool $checkTags If true, the NBT of the items will also be checked and must be the same to be counted. + */ + public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int; + /** * Returns whether the total amount of matching items is at least the stack size of the given item. Multiple stacks * of the same item are added together. @@ -179,6 +186,11 @@ public function swap(int $slot1, int $slot2) : void; */ public function getViewers() : array; + /** + * Tells all Players viewing this inventory to stop viewing it and discard associated windows. + */ + public function removeAllViewers() : void; + /** * Called when a player opens this inventory. */ diff --git a/src/inventory/PlayerOffHandInventory.php b/src/inventory/PlayerOffHandInventory.php deleted file mode 100644 index 127b09f98a6..00000000000 --- a/src/inventory/PlayerOffHandInventory.php +++ /dev/null @@ -1,37 +0,0 @@ -holder = $player; - parent::__construct(1); - } - - public function getHolder() : Human{ return $this->holder; } -} diff --git a/src/inventory/SimpleInventory.php b/src/inventory/SimpleInventory.php index 4b44326fa17..a6a3393227b 100644 --- a/src/inventory/SimpleInventory.php +++ b/src/inventory/SimpleInventory.php @@ -84,7 +84,7 @@ protected function internalSetContents(array $items) : void{ } } - protected function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{ + public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{ $slotItem = $this->slots[$slot]; return $slotItem !== null && $slotItem->equals($test, true, $checkTags) ? $slotItem->getCount() : 0; } diff --git a/src/inventory/transaction/InventoryTransaction.php b/src/inventory/transaction/InventoryTransaction.php index 47290e40151..bc9621314f6 100644 --- a/src/inventory/transaction/InventoryTransaction.php +++ b/src/inventory/transaction/InventoryTransaction.php @@ -28,6 +28,7 @@ use pocketmine\inventory\transaction\action\InventoryAction; use pocketmine\inventory\transaction\action\SlotChangeAction; use pocketmine\item\Item; +use pocketmine\player\InventoryWindow; use pocketmine\player\Player; use pocketmine\utils\Utils; use function array_keys; @@ -59,10 +60,10 @@ class InventoryTransaction{ protected bool $hasExecuted = false; /** - * @var Inventory[] - * @phpstan-var array + * @var InventoryWindow[] + * @phpstan-var array */ - protected array $inventories = []; + protected array $inventoryWindows = []; /** * @var InventoryAction[] @@ -87,11 +88,11 @@ public function getSource() : Player{ } /** - * @return Inventory[] - * @phpstan-return array + * @return InventoryWindow[] + * @phpstan-return array */ - public function getInventories() : array{ - return $this->inventories; + public function getInventoryWindows() : array{ + return $this->inventoryWindows; } /** @@ -111,8 +112,8 @@ public function addAction(InventoryAction $action) : void{ if(!isset($this->actions[$hash = spl_object_id($action)])){ $this->actions[$hash] = $action; $action->onAddToTransaction($this); - if($action instanceof SlotChangeAction && !isset($this->inventories[$inventoryId = spl_object_id($action->getInventory())])){ - $this->inventories[$inventoryId] = $action->getInventory(); + if($action instanceof SlotChangeAction && !isset($this->inventoryWindows[$inventoryId = spl_object_id($action->getInventoryWindow())])){ + $this->inventoryWindows[$inventoryId] = $action->getInventoryWindow(); } }else{ throw new \InvalidArgumentException("Tried to add the same action to a transaction twice"); @@ -196,8 +197,8 @@ protected function squashDuplicateSlotChanges() : void{ foreach($this->actions as $key => $action){ if($action instanceof SlotChangeAction){ - $slotChanges[$h = (spl_object_hash($action->getInventory()) . "@" . $action->getSlot())][] = $action; - $inventories[$h] = $action->getInventory(); + $slotChanges[$h = (spl_object_hash($action->getInventoryWindow()) . "@" . $action->getSlot())][] = $action; + $inventories[$h] = $action->getInventoryWindow(); $slots[$h] = $action->getSlot(); } } @@ -207,10 +208,11 @@ protected function squashDuplicateSlotChanges() : void{ continue; } - $inventory = $inventories[$hash]; + $window = $inventories[$hash]; + $inventory = $window->getInventory(); $slot = $slots[$hash]; if(!$inventory->slotExists($slot)){ //this can get hit for crafting tables because the validation happens after this compaction - throw new TransactionValidationException("Slot $slot does not exist in inventory " . get_class($inventory)); + throw new TransactionValidationException("Slot $slot does not exist in inventory window " . get_class($window)); } $sourceItem = $inventory->getItem($slot); @@ -225,7 +227,7 @@ protected function squashDuplicateSlotChanges() : void{ if(!$targetItem->equalsExact($sourceItem)){ //sometimes we get actions on the crafting grid whose source and target items are the same, so dump them - $this->addAction(new SlotChangeAction($inventory, $slot, $sourceItem, $targetItem)); + $this->addAction(new SlotChangeAction($window, $slot, $sourceItem, $targetItem)); } } } diff --git a/src/inventory/transaction/TransactionBuilderInventory.php b/src/inventory/transaction/SlotChangeActionBuilder.php similarity index 70% rename from src/inventory/transaction/TransactionBuilderInventory.php rename to src/inventory/transaction/SlotChangeActionBuilder.php index 95b6c4a147c..7fe2490d9a7 100644 --- a/src/inventory/transaction/TransactionBuilderInventory.php +++ b/src/inventory/transaction/SlotChangeActionBuilder.php @@ -28,6 +28,7 @@ use pocketmine\inventory\transaction\action\SlotChangeAction; use pocketmine\item\Item; use pocketmine\item\VanillaItems; +use pocketmine\player\InventoryWindow; /** * This class facilitates generating SlotChangeActions to build an inventory transaction. @@ -35,7 +36,7 @@ * This allows you to use the normal Inventory API methods like addItem() and so on to build a transaction, without * modifying the original inventory. */ -final class TransactionBuilderInventory extends BaseInventory{ +final class SlotChangeActionBuilder extends BaseInventory{ /** * @var \SplFixedArray|(Item|null)[] @@ -44,14 +45,14 @@ final class TransactionBuilderInventory extends BaseInventory{ private \SplFixedArray $changedSlots; public function __construct( - private Inventory $actualInventory + private InventoryWindow $inventoryWindow ){ parent::__construct(); - $this->changedSlots = new \SplFixedArray($this->actualInventory->getSize()); + $this->changedSlots = new \SplFixedArray($this->inventoryWindow->getInventory()->getSize()); } - public function getActualInventory() : Inventory{ - return $this->actualInventory; + public function getInventoryWindow() : InventoryWindow{ + return $this->inventoryWindow; } protected function internalSetContents(array $items) : void{ @@ -65,21 +66,21 @@ protected function internalSetContents(array $items) : void{ } protected function internalSetItem(int $index, Item $item) : void{ - if(!$item->equalsExact($this->actualInventory->getItem($index))){ + if(!$item->equalsExact($this->inventoryWindow->getInventory()->getItem($index))){ $this->changedSlots[$index] = $item->isNull() ? VanillaItems::AIR() : clone $item; } } public function getSize() : int{ - return $this->actualInventory->getSize(); + return $this->inventoryWindow->getInventory()->getSize(); } public function getItem(int $index) : Item{ - return $this->changedSlots[$index] !== null ? clone $this->changedSlots[$index] : $this->actualInventory->getItem($index); + return $this->changedSlots[$index] !== null ? clone $this->changedSlots[$index] : $this->inventoryWindow->getInventory()->getItem($index); } public function getContents(bool $includeEmpty = false) : array{ - $contents = $this->actualInventory->getContents($includeEmpty); + $contents = $this->inventoryWindow->getInventory()->getContents($includeEmpty); foreach($this->changedSlots as $index => $item){ if($item !== null){ if($includeEmpty || !$item->isNull()){ @@ -92,16 +93,25 @@ public function getContents(bool $includeEmpty = false) : array{ return $contents; } + public function getMatchingItemCount(int $slot, Item $test, bool $checkTags) : int{ + $slotItem = $this->changedSlots[$slot] ?? null; + if($slotItem !== null){ + return $slotItem->equals($test, true, $checkTags) ? $slotItem->getCount() : 0; + } + return $this->inventoryWindow->getInventory()->getMatchingItemCount($slot, $test, $checkTags); + } + /** * @return SlotChangeAction[] */ public function generateActions() : array{ $result = []; + $inventory = $this->inventoryWindow->getInventory(); foreach($this->changedSlots as $index => $newItem){ if($newItem !== null){ - $oldItem = $this->actualInventory->getItem($index); + $oldItem = $inventory->getItem($index); if(!$newItem->equalsExact($oldItem)){ - $result[] = new SlotChangeAction($this->actualInventory, $index, $oldItem, $newItem); + $result[] = new SlotChangeAction($this->inventoryWindow, $index, $oldItem, $newItem); } } } diff --git a/src/inventory/transaction/TransactionBuilder.php b/src/inventory/transaction/TransactionBuilder.php index f56b2aaa160..6232a44a4d5 100644 --- a/src/inventory/transaction/TransactionBuilder.php +++ b/src/inventory/transaction/TransactionBuilder.php @@ -23,13 +23,13 @@ namespace pocketmine\inventory\transaction; -use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\action\InventoryAction; +use pocketmine\player\InventoryWindow; use function spl_object_id; final class TransactionBuilder{ - /** @var TransactionBuilderInventory[] */ + /** @var SlotChangeActionBuilder[] */ private array $inventories = []; /** @var InventoryAction[] */ @@ -39,9 +39,9 @@ public function addAction(InventoryAction $action) : void{ $this->extraActions[spl_object_id($action)] = $action; } - public function getInventory(Inventory $inventory) : TransactionBuilderInventory{ + public function getActionBuilder(InventoryWindow $inventory) : SlotChangeActionBuilder{ $id = spl_object_id($inventory); - return $this->inventories[$id] ??= new TransactionBuilderInventory($inventory); + return $this->inventories[$id] ??= new SlotChangeActionBuilder($inventory); } /** diff --git a/src/inventory/transaction/action/SlotChangeAction.php b/src/inventory/transaction/action/SlotChangeAction.php index 3c9b8e5cf79..553a640087b 100644 --- a/src/inventory/transaction/action/SlotChangeAction.php +++ b/src/inventory/transaction/action/SlotChangeAction.php @@ -27,6 +27,7 @@ use pocketmine\inventory\SlotValidatedInventory; use pocketmine\inventory\transaction\TransactionValidationException; use pocketmine\item\Item; +use pocketmine\player\InventoryWindow; use pocketmine\player\Player; /** @@ -34,7 +35,7 @@ */ class SlotChangeAction extends InventoryAction{ public function __construct( - protected Inventory $inventory, + protected InventoryWindow $inventoryWindow, private int $inventorySlot, Item $sourceItem, Item $targetItem @@ -43,10 +44,10 @@ public function __construct( } /** - * Returns the inventory involved in this action. + * Returns the inventory window involved in this action. */ - public function getInventory() : Inventory{ - return $this->inventory; + public function getInventoryWindow() : InventoryWindow{ + return $this->inventoryWindow; } /** @@ -62,21 +63,22 @@ public function getSlot() : int{ * @throws TransactionValidationException */ public function validate(Player $source) : void{ - if(!$this->inventory->slotExists($this->inventorySlot)){ + $inventory = $this->inventoryWindow->getInventory(); + if(!$inventory->slotExists($this->inventorySlot)){ throw new TransactionValidationException("Slot does not exist"); } - if(!$this->inventory->getItem($this->inventorySlot)->equalsExact($this->sourceItem)){ + if(!$inventory->getItem($this->inventorySlot)->equalsExact($this->sourceItem)){ throw new TransactionValidationException("Slot does not contain expected original item"); } if($this->targetItem->getCount() > $this->targetItem->getMaxStackSize()){ throw new TransactionValidationException("Target item exceeds item type max stack size"); } - if($this->targetItem->getCount() > $this->inventory->getMaxStackSize()){ + if($this->targetItem->getCount() > $inventory->getMaxStackSize()){ throw new TransactionValidationException("Target item exceeds inventory max stack size"); } - if($this->inventory instanceof SlotValidatedInventory && !$this->targetItem->isNull()){ - foreach($this->inventory->getSlotValidators() as $validator){ - $ret = $validator->validate($this->inventory, $this->targetItem, $this->inventorySlot); + if($inventory instanceof SlotValidatedInventory && !$this->targetItem->isNull()){ + foreach($inventory->getSlotValidators() as $validator){ + $ret = $validator->validate($inventory, $this->targetItem, $this->inventorySlot); if($ret !== null){ throw new TransactionValidationException("Target item is not accepted by the inventory at slot #" . $this->inventorySlot . ": " . $ret->getMessage(), 0, $ret); } @@ -88,6 +90,6 @@ public function validate(Player $source) : void{ * Sets the item into the target inventory. */ public function execute(Player $source) : void{ - $this->inventory->setItem($this->inventorySlot, $this->targetItem); + $this->inventoryWindow->getInventory()->setItem($this->inventorySlot, $this->targetItem); } } diff --git a/src/item/Armor.php b/src/item/Armor.php index 63a8003adc9..b50d151d18a 100644 --- a/src/item/Armor.php +++ b/src/item/Armor.php @@ -145,7 +145,7 @@ public function onClickAir(Player $player, Vector3 $directionVector, array &$ret $thisCopy = clone $this; $new = $thisCopy->pop(); $player->getArmorInventory()->setItem($this->getArmorSlot(), $new); - $player->getInventory()->setItemInHand($existing); + $player->getHotbar()->setHeldItem($existing); $sound = $new->getMaterial()->getEquipSound(); if($sound !== null){ $player->broadcastSound($sound); diff --git a/src/network/mcpe/ComplexInventoryMapEntry.php b/src/network/mcpe/ComplexWindowMapEntry.php similarity index 88% rename from src/network/mcpe/ComplexInventoryMapEntry.php rename to src/network/mcpe/ComplexWindowMapEntry.php index dfd3e999a47..fa457e14f68 100644 --- a/src/network/mcpe/ComplexInventoryMapEntry.php +++ b/src/network/mcpe/ComplexWindowMapEntry.php @@ -23,9 +23,9 @@ namespace pocketmine\network\mcpe; -use pocketmine\inventory\Inventory; +use pocketmine\player\InventoryWindow; -final class ComplexInventoryMapEntry{ +final class ComplexWindowMapEntry{ /** * @var int[] @@ -38,7 +38,7 @@ final class ComplexInventoryMapEntry{ * @phpstan-param array $slotMap */ public function __construct( - private Inventory $inventory, + private InventoryWindow $inventory, private array $slotMap ){ foreach($slotMap as $slot => $index){ @@ -46,7 +46,7 @@ public function __construct( } } - public function getInventory() : Inventory{ return $this->inventory; } + public function getWindow() : InventoryWindow{ return $this->inventory; } /** * @return int[] diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php index 7df8c734be6..58fccbb5f59 100644 --- a/src/network/mcpe/InventoryManager.php +++ b/src/network/mcpe/InventoryManager.php @@ -23,24 +23,26 @@ namespace pocketmine\network\mcpe; -use pocketmine\block\inventory\AnvilInventory; -use pocketmine\block\inventory\BlockInventory; -use pocketmine\block\inventory\BrewingStandInventory; -use pocketmine\block\inventory\CartographyTableInventory; -use pocketmine\block\inventory\CraftingTableInventory; -use pocketmine\block\inventory\EnchantInventory; -use pocketmine\block\inventory\FurnaceInventory; -use pocketmine\block\inventory\HopperInventory; -use pocketmine\block\inventory\LoomInventory; -use pocketmine\block\inventory\SmithingTableInventory; -use pocketmine\block\inventory\StonecutterInventory; +use pocketmine\block\inventory\window\AnvilInventoryWindow; +use pocketmine\block\inventory\window\BlockInventoryWindow; +use pocketmine\block\inventory\window\BrewingStandInventoryWindow; +use pocketmine\block\inventory\window\CartographyTableInventoryWindow; +use pocketmine\block\inventory\window\CraftingTableInventoryWindow; +use pocketmine\block\inventory\window\EnchantingTableInventoryWindow; +use pocketmine\block\inventory\window\FurnaceInventoryWindow; +use pocketmine\block\inventory\window\HopperInventoryWindow; +use pocketmine\block\inventory\window\LoomInventoryWindow; +use pocketmine\block\inventory\window\SmithingTableInventoryWindow; +use pocketmine\block\inventory\window\StonecutterInventoryWindow; use pocketmine\crafting\FurnaceType; use pocketmine\data\bedrock\EnchantmentIdMap; use pocketmine\inventory\Inventory; +use pocketmine\inventory\InventoryListener; use pocketmine\inventory\transaction\action\SlotChangeAction; use pocketmine\inventory\transaction\InventoryTransaction; use pocketmine\item\enchantment\EnchantingOption; use pocketmine\item\enchantment\EnchantmentInstance; +use pocketmine\item\Item; use pocketmine\network\mcpe\cache\CreativeInventoryCache; use pocketmine\network\mcpe\protocol\ClientboundPacket; use pocketmine\network\mcpe\protocol\ContainerClosePacket; @@ -61,7 +63,9 @@ use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset; use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes; use pocketmine\network\PacketHandlingException; +use pocketmine\player\InventoryWindow; use pocketmine\player\Player; +use pocketmine\player\PlayerInventoryWindow; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\ObjectSet; use function array_fill_keys; @@ -76,27 +80,27 @@ use function spl_object_id; /** - * @phpstan-type ContainerOpenClosure \Closure(int $id, Inventory $inventory) : (list|null) + * @phpstan-type ContainerOpenClosure \Closure(int $id, InventoryWindow $window) : (list|null) */ -class InventoryManager{ +class InventoryManager implements InventoryListener{ /** * @var InventoryManagerEntry[] spl_object_id(Inventory) => InventoryManagerEntry * @phpstan-var array */ - private array $inventories = []; + private array $entries = []; /** - * @var Inventory[] network window ID => Inventory - * @phpstan-var array + * @var InventoryWindow[] network window ID => InventoryWindow + * @phpstan-var array */ - private array $networkIdToInventoryMap = []; + private array $networkIdToWindowMap = []; /** - * @var ComplexInventoryMapEntry[] net slot ID => ComplexWindowMapEntry - * @phpstan-var array + * @var ComplexWindowMapEntry[] net slot ID => ComplexWindowMapEntry + * @phpstan-var array */ - private array $complexSlotToInventoryMap = []; + private array $complexSlotToWindowMap = []; - private int $lastInventoryNetworkId = ContainerIds::FIRST; + private int $lastWindowNetworkId = ContainerIds::FIRST; private int $currentWindowType = WindowTypes::CONTAINER; private int $clientSelectedHotbarSlot = -1; @@ -126,33 +130,48 @@ public function __construct( $this->containerOpenCallbacks = new ObjectSet(); $this->containerOpenCallbacks->add(self::createContainerOpen(...)); - $this->add(ContainerIds::INVENTORY, $this->player->getInventory()); - $this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory()); - $this->add(ContainerIds::ARMOR, $this->player->getArmorInventory()); - $this->addComplex(UIInventorySlotOffset::CURSOR, $this->player->getCursorInventory()); - $this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $this->player->getCraftingGrid()); + foreach($this->player->getPermanentWindows() as $window){ + match($window->getType()){ + PlayerInventoryWindow::TYPE_INVENTORY => $this->add(ContainerIds::INVENTORY, $window), + PlayerInventoryWindow::TYPE_OFFHAND => $this->add(ContainerIds::OFFHAND, $window), + PlayerInventoryWindow::TYPE_ARMOR => $this->add(ContainerIds::ARMOR, $window), + PlayerInventoryWindow::TYPE_CURSOR => $this->addComplex(UIInventorySlotOffset::CURSOR, $window), + PlayerInventoryWindow::TYPE_CRAFTING => $this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $window), + default => throw new AssumptionFailedError("Unknown permanent window type " . $window->getType()) + }; + } - $this->player->getInventory()->getHeldItemIndexChangeListeners()->add($this->syncSelectedHotbarSlot(...)); + $this->player->getHotbar()->getSelectedIndexChangeListeners()->add($this->syncSelectedHotbarSlot(...)); } - private function associateIdWithInventory(int $id, Inventory $inventory) : void{ - $this->networkIdToInventoryMap[$id] = $inventory; + private function associateIdWithInventory(int $id, InventoryWindow $window) : void{ + $this->networkIdToWindowMap[$id] = $window; } private function getNewWindowId() : int{ - $this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % ContainerIds::LAST); - return $this->lastInventoryNetworkId; + $this->lastWindowNetworkId = max(ContainerIds::FIRST, ($this->lastWindowNetworkId + 1) % ContainerIds::LAST); + return $this->lastWindowNetworkId; + } + + private function getEntry(Inventory $inventory) : ?InventoryManagerEntry{ + return $this->entries[spl_object_id($inventory)] ?? null; + } + + public function getInventoryWindow(Inventory $inventory) : ?InventoryWindow{ + return $this->getEntry($inventory)?->window; } - private function add(int $id, Inventory $inventory) : void{ - if(isset($this->inventories[spl_object_id($inventory)])){ - throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked"); + private function add(int $id, InventoryWindow $window) : void{ + $k = spl_object_id($window->getInventory()); + if(isset($this->entries[$k])){ + throw new \InvalidArgumentException("Inventory " . get_class($window) . " is already tracked"); } - $this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry($inventory); - $this->associateIdWithInventory($id, $inventory); + $this->entries[$k] = new InventoryManagerEntry($window); + $window->getInventory()->getListeners()->add($this); + $this->associateIdWithInventory($id, $window); } - private function addDynamic(Inventory $inventory) : int{ + private function addDynamic(InventoryWindow $inventory) : int{ $id = $this->getNewWindowId(); $this->add($id, $inventory); return $id; @@ -162,17 +181,19 @@ private function addDynamic(Inventory $inventory) : int{ * @param int[]|int $slotMap * @phpstan-param array|int $slotMap */ - private function addComplex(array|int $slotMap, Inventory $inventory) : void{ - if(isset($this->inventories[spl_object_id($inventory)])){ - throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked"); + private function addComplex(array|int $slotMap, InventoryWindow $window) : void{ + $k = spl_object_id($window->getInventory()); + if(isset($this->entries[$k])){ + throw new \InvalidArgumentException("Inventory " . get_class($window) . " is already tracked"); } - $complexSlotMap = new ComplexInventoryMapEntry($inventory, is_int($slotMap) ? [$slotMap => 0] : $slotMap); - $this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry( - $inventory, + $complexSlotMap = new ComplexWindowMapEntry($window, is_int($slotMap) ? [$slotMap => 0] : $slotMap); + $this->entries[$k] = new InventoryManagerEntry( + $window, $complexSlotMap ); + $window->getInventory()->getListeners()->add($this); foreach($complexSlotMap->getSlotMap() as $netSlot => $coreSlot){ - $this->complexSlotToInventoryMap[$netSlot] = $complexSlotMap; + $this->complexSlotToWindowMap[$netSlot] = $complexSlotMap; } } @@ -180,7 +201,7 @@ private function addComplex(array|int $slotMap, Inventory $inventory) : void{ * @param int[]|int $slotMap * @phpstan-param array|int $slotMap */ - private function addComplexDynamic(array|int $slotMap, Inventory $inventory) : int{ + private function addComplexDynamic(array|int $slotMap, InventoryWindow $inventory) : int{ $this->addComplex($slotMap, $inventory); $id = $this->getNewWindowId(); $this->associateIdWithInventory($id, $inventory); @@ -188,48 +209,52 @@ private function addComplexDynamic(array|int $slotMap, Inventory $inventory) : i } private function remove(int $id) : void{ - $inventory = $this->networkIdToInventoryMap[$id]; - unset($this->networkIdToInventoryMap[$id]); - if($this->getWindowId($inventory) === null){ - unset($this->inventories[spl_object_id($inventory)]); - foreach($this->complexSlotToInventoryMap as $netSlot => $entry){ - if($entry->getInventory() === $inventory){ - unset($this->complexSlotToInventoryMap[$netSlot]); + $window = $this->networkIdToWindowMap[$id]; + $inventory = $window->getInventory(); + unset($this->networkIdToWindowMap[$id]); + if($this->getWindowId($window) === null){ + $inventory->getListeners()->remove($this); + unset($this->entries[spl_object_id($inventory)]); + foreach($this->complexSlotToWindowMap as $netSlot => $entry){ + if($entry->getWindow() === $window){ + unset($this->complexSlotToWindowMap[$netSlot]); } } } } - public function getWindowId(Inventory $inventory) : ?int{ - return ($id = array_search($inventory, $this->networkIdToInventoryMap, true)) !== false ? $id : null; + public function getWindowId(InventoryWindow $window) : ?int{ + return ($id = array_search($window, $this->networkIdToWindowMap, true)) !== false ? $id : null; } public function getCurrentWindowId() : int{ - return $this->lastInventoryNetworkId; + return $this->lastWindowNetworkId; } /** - * @phpstan-return array{Inventory, int}|null + * @phpstan-return array{InventoryWindow, int}|null */ public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{ if($windowId === ContainerIds::UI){ - $entry = $this->complexSlotToInventoryMap[$netSlotId] ?? null; + $entry = $this->complexSlotToWindowMap[$netSlotId] ?? null; if($entry === null){ return null; } - $inventory = $entry->getInventory(); + $window = $entry->getWindow(); $coreSlotId = $entry->mapNetToCore($netSlotId); - return $coreSlotId !== null && $inventory->slotExists($coreSlotId) ? [$inventory, $coreSlotId] : null; + return $coreSlotId !== null && $window->getInventory()->slotExists($coreSlotId) ? [$window, $coreSlotId] : null; } - $inventory = $this->networkIdToInventoryMap[$windowId] ?? null; - if($inventory !== null && $inventory->slotExists($netSlotId)){ - return [$inventory, $netSlotId]; + $window = $this->networkIdToWindowMap[$windowId] ?? null; + if($window !== null && $window->getInventory()->slotExists($netSlotId)){ + return [$window, $netSlotId]; } return null; } - private function addPredictedSlotChange(Inventory $inventory, int $slot, ItemStack $item) : void{ - $this->inventories[spl_object_id($inventory)]->predictions[$slot] = $item; + private function addPredictedSlotChange(InventoryWindow $window, int $slot, ItemStack $item) : void{ + //TODO: does this need a null check? + $entry = $this->getEntry($window->getInventory()) ?? throw new AssumptionFailedError("Assume this should never be null"); + $entry->predictions[$slot] = $item; } public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{ @@ -238,7 +263,7 @@ public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : v if($action instanceof SlotChangeAction){ //TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead $itemStack = $typeConverter->coreItemStackToNet($action->getTargetItem()); - $this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $itemStack); + $this->addPredictedSlotChange($action->getInventoryWindow(), $action->getSlot(), $itemStack); } } } @@ -266,8 +291,8 @@ public function addRawPredictedSlotChanges(array $networkInventoryActions) : voi continue; } - [$inventory, $slot] = $info; - $this->addPredictedSlotChange($inventory, $slot, $action->newItem->getItemStack()); + [$window, $slot] = $info; + $this->addPredictedSlotChange($window, $slot, $action->newItem->getItemStack()); } } @@ -302,32 +327,32 @@ private function openWindowDeferred(\Closure $func) : void{ * @return int[]|null * @phpstan-return array|null */ - private function createComplexSlotMapping(Inventory $inventory) : ?array{ + private function createComplexSlotMapping(InventoryWindow $inventory) : ?array{ //TODO: make this dynamic so plugins can add mappings for stuff not implemented by PM return match(true){ - $inventory instanceof AnvilInventory => UIInventorySlotOffset::ANVIL, - $inventory instanceof EnchantInventory => UIInventorySlotOffset::ENCHANTING_TABLE, - $inventory instanceof LoomInventory => UIInventorySlotOffset::LOOM, - $inventory instanceof StonecutterInventory => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventory::SLOT_INPUT], - $inventory instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT, - $inventory instanceof CartographyTableInventory => UIInventorySlotOffset::CARTOGRAPHY_TABLE, - $inventory instanceof SmithingTableInventory => UIInventorySlotOffset::SMITHING_TABLE, + $inventory instanceof AnvilInventoryWindow => UIInventorySlotOffset::ANVIL, + $inventory instanceof EnchantingTableInventoryWindow => UIInventorySlotOffset::ENCHANTING_TABLE, + $inventory instanceof LoomInventoryWindow => UIInventorySlotOffset::LOOM, + $inventory instanceof StonecutterInventoryWindow => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventoryWindow::SLOT_INPUT], + $inventory instanceof CraftingTableInventoryWindow => UIInventorySlotOffset::CRAFTING3X3_INPUT, + $inventory instanceof CartographyTableInventoryWindow => UIInventorySlotOffset::CARTOGRAPHY_TABLE, + $inventory instanceof SmithingTableInventoryWindow => UIInventorySlotOffset::SMITHING_TABLE, default => null, }; } - public function onCurrentWindowChange(Inventory $inventory) : void{ + public function onCurrentWindowChange(InventoryWindow $window) : void{ $this->onCurrentWindowRemove(); - $this->openWindowDeferred(function() use ($inventory) : void{ - if(($slotMap = $this->createComplexSlotMapping($inventory)) !== null){ - $windowId = $this->addComplexDynamic($slotMap, $inventory); + $this->openWindowDeferred(function() use ($window) : void{ + if(($slotMap = $this->createComplexSlotMapping($window)) !== null){ + $windowId = $this->addComplexDynamic($slotMap, $window); }else{ - $windowId = $this->addDynamic($inventory); + $windowId = $this->addDynamic($window); } foreach($this->containerOpenCallbacks as $callback){ - $pks = $callback($windowId, $inventory); + $pks = $callback($windowId, $window); if($pks !== null){ $windowType = null; foreach($pks as $pk){ @@ -338,7 +363,7 @@ public function onCurrentWindowChange(Inventory $inventory) : void{ $this->session->sendDataPacket($pk); } $this->currentWindowType = $windowType ?? WindowTypes::CONTAINER; - $this->syncContents($inventory); + $this->syncContents($window); return; } } @@ -353,27 +378,27 @@ public function getContainerOpenCallbacks() : ObjectSet{ return $this->container * @return ClientboundPacket[]|null * @phpstan-return list|null */ - protected static function createContainerOpen(int $id, Inventory $inv) : ?array{ + protected static function createContainerOpen(int $id, InventoryWindow $window) : ?array{ //TODO: we should be using some kind of tagging system to identify the types. Instanceof is flaky especially //if the class isn't final, not to mention being inflexible. - if($inv instanceof BlockInventory){ - $blockPosition = BlockPosition::fromVector3($inv->getHolder()); + if($window instanceof BlockInventoryWindow){ + $blockPosition = BlockPosition::fromVector3($window->getHolder()->getPosition()); $windowType = match(true){ - $inv instanceof LoomInventory => WindowTypes::LOOM, - $inv instanceof FurnaceInventory => match($inv->getFurnaceType()){ + $window instanceof LoomInventoryWindow => WindowTypes::LOOM, + $window instanceof FurnaceInventoryWindow => match($window->getFurnaceType()){ FurnaceType::FURNACE => WindowTypes::FURNACE, FurnaceType::BLAST_FURNACE => WindowTypes::BLAST_FURNACE, FurnaceType::SMOKER => WindowTypes::SMOKER, FurnaceType::CAMPFIRE, FurnaceType::SOUL_CAMPFIRE => throw new \LogicException("Campfire inventory cannot be displayed to a player") }, - $inv instanceof EnchantInventory => WindowTypes::ENCHANTMENT, - $inv instanceof BrewingStandInventory => WindowTypes::BREWING_STAND, - $inv instanceof AnvilInventory => WindowTypes::ANVIL, - $inv instanceof HopperInventory => WindowTypes::HOPPER, - $inv instanceof CraftingTableInventory => WindowTypes::WORKBENCH, - $inv instanceof StonecutterInventory => WindowTypes::STONECUTTER, - $inv instanceof CartographyTableInventory => WindowTypes::CARTOGRAPHY, - $inv instanceof SmithingTableInventory => WindowTypes::SMITHING_TABLE, + $window instanceof EnchantingTableInventoryWindow => WindowTypes::ENCHANTMENT, + $window instanceof BrewingStandInventoryWindow => WindowTypes::BREWING_STAND, + $window instanceof AnvilInventoryWindow => WindowTypes::ANVIL, + $window instanceof HopperInventoryWindow => WindowTypes::HOPPER, + $window instanceof CraftingTableInventoryWindow => WindowTypes::WORKBENCH, + $window instanceof StonecutterInventoryWindow => WindowTypes::STONECUTTER, + $window instanceof CartographyTableInventoryWindow => WindowTypes::CARTOGRAPHY, + $window instanceof SmithingTableInventoryWindow => WindowTypes::SMITHING_TABLE, default => WindowTypes::CONTAINER }; return [ContainerOpenPacket::blockInv($id, $windowType, $blockPosition)]; @@ -386,7 +411,8 @@ public function onClientOpenMainInventory() : void{ $this->openWindowDeferred(function() : void{ $windowId = $this->getNewWindowId(); - $this->associateIdWithInventory($windowId, $this->player->getInventory()); + $window = $this->getInventoryWindow($this->player->getInventory()) ?? throw new AssumptionFailedError("This should never be null"); + $this->associateIdWithInventory($windowId, $window); $this->currentWindowType = WindowTypes::INVENTORY; $this->session->sendDataPacket(ContainerOpenPacket::entityInv( @@ -398,25 +424,25 @@ public function onClientOpenMainInventory() : void{ } public function onCurrentWindowRemove() : void{ - if(isset($this->networkIdToInventoryMap[$this->lastInventoryNetworkId])){ - $this->remove($this->lastInventoryNetworkId); - $this->session->sendDataPacket(ContainerClosePacket::create($this->lastInventoryNetworkId, $this->currentWindowType, true)); + if(isset($this->networkIdToWindowMap[$this->lastWindowNetworkId])){ + $this->remove($this->lastWindowNetworkId); + $this->session->sendDataPacket(ContainerClosePacket::create($this->lastWindowNetworkId, $this->currentWindowType, true)); if($this->pendingCloseWindowId !== null){ throw new AssumptionFailedError("We should not have opened a new window while a window was waiting to be closed"); } - $this->pendingCloseWindowId = $this->lastInventoryNetworkId; + $this->pendingCloseWindowId = $this->lastWindowNetworkId; $this->enchantingTableOptions = []; } } public function onClientRemoveWindow(int $id) : void{ - if($id === $this->lastInventoryNetworkId){ - if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){ + if($id === $this->lastWindowNetworkId){ + if(isset($this->networkIdToWindowMap[$id]) && $id !== $this->pendingCloseWindowId){ $this->remove($id); $this->player->removeCurrentWindow(); } }else{ - $this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastInventoryNetworkId"); + $this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastWindowNetworkId"); } //Always send this, even if no window matches. If we told the client to close a window, it will behave as if it @@ -468,13 +494,25 @@ private function itemStacksEqual(ItemStack $left, ItemStack $right) : bool{ $this->itemStackExtraDataEqual($left, $right); } - public function onSlotChange(Inventory $inventory, int $slot) : void{ - $inventoryEntry = $this->inventories[spl_object_id($inventory)] ?? null; + public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem) : void{ + $window = $this->getInventoryWindow($inventory); + if($window === null){ + //this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory + //is cleared before removal. + return; + } + $this->requestSyncSlot($window, $slot); + } + + public function requestSyncSlot(InventoryWindow $window, int $slot) : void{ + $inventory = $window->getInventory(); + $inventoryEntry = $this->getEntry($inventory); if($inventoryEntry === null){ //this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory //is cleared before removal. return; } + $currentItem = $this->session->getTypeConverter()->coreItemStackToNet($inventory->getItem($slot)); $clientSideItem = $inventoryEntry->predictions[$slot] ?? null; if($clientSideItem === null || !$this->itemStacksEqual($currentItem, $clientSideItem)){ @@ -502,7 +540,7 @@ private function sendInventorySlotPackets(int $windowId, int $netSlot, ItemStack $this->session->sendDataPacket(InventorySlotPacket::create( $windowId, $netSlot, - new FullContainerName($this->lastInventoryNetworkId), + new FullContainerName($this->lastWindowNetworkId), new ItemStackWrapper(0, ItemStack::null()), new ItemStackWrapper(0, ItemStack::null()) )); @@ -511,7 +549,7 @@ private function sendInventorySlotPackets(int $windowId, int $netSlot, ItemStack $this->session->sendDataPacket(InventorySlotPacket::create( $windowId, $netSlot, - new FullContainerName($this->lastInventoryNetworkId), + new FullContainerName($this->lastWindowNetworkId), new ItemStackWrapper(0, ItemStack::null()), $itemStackWrapper )); @@ -532,18 +570,15 @@ private function sendInventoryContentPackets(int $windowId, array $itemStackWrap $this->session->sendDataPacket(InventoryContentPacket::create( $windowId, array_fill_keys(array_keys($itemStackWrappers), new ItemStackWrapper(0, ItemStack::null())), - new FullContainerName($this->lastInventoryNetworkId), + new FullContainerName($this->lastWindowNetworkId), new ItemStackWrapper(0, ItemStack::null()) )); //now send the real contents - $this->session->sendDataPacket(InventoryContentPacket::create($windowId, $itemStackWrappers, new FullContainerName($this->lastInventoryNetworkId), new ItemStackWrapper(0, ItemStack::null()))); + $this->session->sendDataPacket(InventoryContentPacket::create($windowId, $itemStackWrappers, new FullContainerName($this->lastWindowNetworkId), new ItemStackWrapper(0, ItemStack::null()))); } - public function syncSlot(Inventory $inventory, int $slot, ItemStack $itemStack) : void{ - $entry = $this->inventories[spl_object_id($inventory)] ?? null; - if($entry === null){ - throw new \LogicException("Cannot sync an untracked inventory"); - } + private function syncSlot(InventoryWindow $window, int $slot, ItemStack $itemStack) : void{ + $entry = $this->getEntry($window->getInventory()) ?? throw new \LogicException("Cannot sync an untracked inventory"); $itemStackInfo = $entry->itemStackInfos[$slot]; if($itemStackInfo === null){ throw new \LogicException("Cannot sync an untracked inventory slot"); @@ -552,7 +587,7 @@ public function syncSlot(Inventory $inventory, int $slot, ItemStack $itemStack) $windowId = ContainerIds::UI; $netSlot = $entry->complexSlotMap->mapCoreToNet($slot) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null"); }else{ - $windowId = $this->getWindowId($inventory) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null"); + $windowId = $this->getWindowId($window) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null"); $netSlot = $slot; } @@ -570,8 +605,18 @@ public function syncSlot(Inventory $inventory, int $slot, ItemStack $itemStack) unset($entry->predictions[$slot], $entry->pendingSyncs[$slot]); } - public function syncContents(Inventory $inventory) : void{ - $entry = $this->inventories[spl_object_id($inventory)] ?? null; + public function onContentChange(Inventory $inventory, array $oldContents) : void{ + //this can be null when an inventory changed during InventoryCloseEvent, or when a temporary inventory + //is cleared before removal. + $window = $this->getInventoryWindow($inventory); + if($window !== null){ + $this->syncContents($window); + } + } + + private function syncContents(InventoryWindow $window) : void{ + $inventory = $window->getInventory(); + $entry = $this->getEntry($inventory); if($entry === null){ //this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory //is cleared before removal. @@ -580,7 +625,7 @@ public function syncContents(Inventory $inventory) : void{ if($entry->complexSlotMap !== null){ $windowId = ContainerIds::UI; }else{ - $windowId = $this->getWindowId($inventory); + $windowId = $this->getWindowId($window); } if($windowId !== null){ $entry->predictions = []; @@ -607,8 +652,8 @@ public function syncContents(Inventory $inventory) : void{ } public function syncAll() : void{ - foreach($this->inventories as $entry){ - $this->syncContents($entry->inventory); + foreach($this->entries as $entry){ + $this->syncContents($entry->window); } } @@ -618,8 +663,8 @@ public function requestSyncAll() : void{ public function syncMismatchedPredictedSlotChanges() : void{ $typeConverter = $this->session->getTypeConverter(); - foreach($this->inventories as $entry){ - $inventory = $entry->inventory; + foreach($this->entries as $entry){ + $inventory = $entry->window->getInventory(); foreach($entry->predictions as $slot => $expectedItem){ if(!$inventory->slotExists($slot) || $entry->itemStackInfos[$slot] === null){ continue; //TODO: size desync ??? @@ -637,14 +682,14 @@ public function syncMismatchedPredictedSlotChanges() : void{ public function flushPendingUpdates() : void{ if($this->fullSyncRequested){ $this->fullSyncRequested = false; - $this->session->getLogger()->debug("Full inventory sync requested, sending contents of " . count($this->inventories) . " inventories"); + $this->session->getLogger()->debug("Full inventory sync requested, sending contents of " . count($this->entries) . " inventories"); $this->syncAll(); }else{ - foreach($this->inventories as $entry){ + foreach($this->entries as $entry){ if(count($entry->pendingSyncs) === 0){ continue; } - $inventory = $entry->inventory; + $inventory = $entry->window; $this->session->getLogger()->debug("Syncing slots " . implode(", ", array_keys($entry->pendingSyncs)) . " in inventory " . get_class($inventory) . "#" . spl_object_id($inventory)); foreach($entry->pendingSyncs as $slot => $itemStack){ $this->syncSlot($inventory, $slot, $itemStack); @@ -655,7 +700,13 @@ public function flushPendingUpdates() : void{ } public function syncData(Inventory $inventory, int $propertyId, int $value) : void{ - $windowId = $this->getWindowId($inventory); + //TODO: the handling of this data has always kinda sucked. Probably ought to route it through InventoryWindow + //somehow, but I'm not sure exactly how that should look. + $window = $this->getInventoryWindow($inventory); + if($window === null){ + return; + } + $windowId = $this->getWindowId($window); if($windowId !== null){ $this->session->sendDataPacket(ContainerSetDataPacket::create($windowId, $propertyId, $value)); } @@ -667,12 +718,9 @@ public function onClientSelectHotbarSlot(int $slot) : void{ public function syncSelectedHotbarSlot() : void{ $playerInventory = $this->player->getInventory(); - $selected = $playerInventory->getHeldItemIndex(); + $selected = $this->player->getHotbar()->getSelectedIndex(); if($selected !== $this->clientSelectedHotbarSlot){ - $inventoryEntry = $this->inventories[spl_object_id($playerInventory)] ?? null; - if($inventoryEntry === null){ - throw new AssumptionFailedError("Player inventory should always be tracked"); - } + $inventoryEntry = $this->getEntry($playerInventory) ?? throw new AssumptionFailedError("Player inventory should always be tracked"); $itemStackInfo = $inventoryEntry->itemStackInfos[$selected] ?? null; if($itemStackInfo === null){ throw new AssumptionFailedError("Untracked player inventory slot $selected"); @@ -680,7 +728,7 @@ public function syncSelectedHotbarSlot() : void{ $this->session->sendDataPacket(MobEquipmentPacket::create( $this->player->getId(), - new ItemStackWrapper($itemStackInfo->getStackId(), $this->session->getTypeConverter()->coreItemStackToNet($playerInventory->getItemInHand())), + new ItemStackWrapper($itemStackInfo->getStackId(), $this->session->getTypeConverter()->coreItemStackToNet($playerInventory->getItem($selected))), $selected, $selected, ContainerIds::INVENTORY @@ -732,8 +780,7 @@ private function newItemStackId() : int{ } public function getItemStackInfo(Inventory $inventory, int $slot) : ?ItemStackInfo{ - $entry = $this->inventories[spl_object_id($inventory)] ?? null; - return $entry?->itemStackInfos[$slot] ?? null; + return $this->getEntry($inventory)?->itemStackInfos[$slot] ?? null; } private function trackItemStack(InventoryManagerEntry $entry, int $slotId, ItemStack $itemStack, ?int $itemStackRequestId) : ItemStackInfo{ diff --git a/src/network/mcpe/InventoryManagerEntry.php b/src/network/mcpe/InventoryManagerEntry.php index deb2e8e4db4..8f7c07de232 100644 --- a/src/network/mcpe/InventoryManagerEntry.php +++ b/src/network/mcpe/InventoryManagerEntry.php @@ -23,8 +23,8 @@ namespace pocketmine\network\mcpe; -use pocketmine\inventory\Inventory; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; +use pocketmine\player\InventoryWindow; final class InventoryManagerEntry{ /** @@ -46,7 +46,7 @@ final class InventoryManagerEntry{ public array $pendingSyncs = []; public function __construct( - public Inventory $inventory, - public ?ComplexInventoryMapEntry $complexSlotMap = null + public InventoryWindow $window, + public ?ComplexWindowMapEntry $complexSlotMap = null ){} } diff --git a/src/network/mcpe/StandardEntityEventBroadcaster.php b/src/network/mcpe/StandardEntityEventBroadcaster.php index 3e2df399483..b19a631dc9d 100644 --- a/src/network/mcpe/StandardEntityEventBroadcaster.php +++ b/src/network/mcpe/StandardEntityEventBroadcaster.php @@ -103,12 +103,12 @@ public function onEntityRemoved(array $recipients, Entity $entity) : void{ public function onMobMainHandItemChange(array $recipients, Human $mob) : void{ //TODO: we could send zero for slot here because remote players don't need to know which slot was selected - $inv = $mob->getInventory(); + $inv = $mob->getHotbar(); $this->sendDataPacket($recipients, MobEquipmentPacket::create( $mob->getId(), - ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($inv->getItemInHand())), - $inv->getHeldItemIndex(), - $inv->getHeldItemIndex(), + ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($inv->getHeldItem())), + $inv->getSelectedIndex(), + $inv->getSelectedIndex(), ContainerIds::INVENTORY )); } diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index 88b4ba1a053..906b5529ed8 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -308,11 +308,11 @@ public function handleActorEvent(ActorEventPacket $packet) : bool{ switch($packet->eventId){ case ActorEvent::EATING_ITEM: //TODO: ignore this and handle it server-side - $item = $this->player->getInventory()->getItemInHand(); + $item = $this->player->getHotbar()->getHeldItem(); if($item->isNull()){ return false; } - $this->player->broadcastAnimation(new ConsumingItemAnimation($this->player, $this->player->getInventory()->getItemInHand())); + $this->player->broadcastAnimation(new ConsumingItemAnimation($this->player, $item)); break; default: return false; @@ -359,7 +359,7 @@ public function handleInventoryTransaction(InventoryTransactionPacket $packet) : [$windowId, $slot] = ItemStackContainerIdTranslator::translate($containerInfo->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $netSlot); $inventoryAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slot); if($inventoryAndSlot !== null){ //trigger the normal slot sync logic - $this->inventoryManager->onSlotChange($inventoryAndSlot[0], $inventoryAndSlot[1]); + $this->inventoryManager->requestSyncSlot($inventoryAndSlot[0], $inventoryAndSlot[1]); } } } @@ -465,7 +465,8 @@ private function handleNormalTransaction(NormalTransactionData $data, int $itemS $droppedItem = $sourceSlotItem->pop($droppedCount); $builder = new TransactionBuilder(); - $builder->getInventory($inventory)->setItem($sourceSlot, $sourceSlotItem); + $window = $this->inventoryManager->getInventoryWindow($inventory) ?? throw new AssumptionFailedError("This should never happen"); + $builder->getActionBuilder($window)->setItem($sourceSlot, $sourceSlotItem); $builder->addAction(new DropItemAction($droppedItem)); $transaction = new InventoryTransaction($this->player, $builder->generateActions()); diff --git a/src/network/mcpe/handler/ItemStackRequestExecutor.php b/src/network/mcpe/handler/ItemStackRequestExecutor.php index 6db8f1e12b8..2e2138c291c 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -23,16 +23,15 @@ namespace pocketmine\network\mcpe\handler; -use pocketmine\block\inventory\EnchantInventory; -use pocketmine\inventory\Inventory; +use pocketmine\block\inventory\window\EnchantingTableInventoryWindow; use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction; use pocketmine\inventory\transaction\action\DropItemAction; use pocketmine\inventory\transaction\CraftingTransaction; use pocketmine\inventory\transaction\EnchantingTransaction; use pocketmine\inventory\transaction\InventoryTransaction; +use pocketmine\inventory\transaction\SlotChangeActionBuilder; use pocketmine\inventory\transaction\TransactionBuilder; -use pocketmine\inventory\transaction\TransactionBuilderInventory; use pocketmine\item\Item; use pocketmine\network\mcpe\InventoryManager; use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds; @@ -52,6 +51,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\TakeStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse; use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset; +use pocketmine\player\InventoryWindow; use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; @@ -82,25 +82,22 @@ public function __construct( $this->builder = new TransactionBuilder(); } - protected function prettyInventoryAndSlot(Inventory $inventory, int $slot) : string{ - if($inventory instanceof TransactionBuilderInventory){ - $inventory = $inventory->getActualInventory(); - } + protected function prettyWindowAndSlot(InventoryWindow $inventory, int $slot) : string{ return (new \ReflectionClass($inventory))->getShortName() . "#" . spl_object_id($inventory) . ", slot: $slot"; } /** * @throws ItemStackRequestProcessException */ - private function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : void{ - $info = $this->inventoryManager->getItemStackInfo($inventory, $slotId); + private function matchItemStack(InventoryWindow $window, int $slotId, int $clientItemStackId) : void{ + $info = $this->inventoryManager->getItemStackInfo($window->getInventory(), $slotId); if($info === null){ throw new AssumptionFailedError("The inventory is tracked and the slot is valid, so this should not be null"); } if(!($clientItemStackId < 0 ? $info->getRequestId() === $clientItemStackId : $info->getStackId() === $clientItemStackId)){ throw new ItemStackRequestProcessException( - $this->prettyInventoryAndSlot($inventory, $slotId) . ": " . + $this->prettyWindowAndSlot($window, $slotId) . ": " . "Mismatched expected itemstack, " . "client expected: $clientItemStackId, server actual: " . $info->getStackId() . ", last modified by request: " . ($info->getRequestId() ?? "none") ); @@ -108,7 +105,7 @@ private function matchItemStack(Inventory $inventory, int $slotId, int $clientIt } /** - * @phpstan-return array{TransactionBuilderInventory, int} + * @phpstan-return array{SlotChangeActionBuilder, int} * * @throws ItemStackRequestProcessException */ @@ -118,16 +115,17 @@ protected function getBuilderInventoryAndSlot(ItemStackRequestSlotInfo $info) : if($windowAndSlot === null){ throw new ItemStackRequestProcessException("No open inventory matches container UI ID: " . $info->getContainerName()->getContainerId() . ", slot ID: " . $info->getSlotId()); } - [$inventory, $slot] = $windowAndSlot; + [$window, $slot] = $windowAndSlot; + $inventory = $window->getInventory(); if(!$inventory->slotExists($slot)){ - throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyInventoryAndSlot($inventory, $slot)); + throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyWindowAndSlot($window, $slot)); } if($info->getStackId() !== $this->request->getRequestId()){ //the itemstack may have been modified by the current request - $this->matchItemStack($inventory, $slot, $info->getStackId()); + $this->matchItemStack($window, $slot, $info->getStackId()); } - return [$this->builder->getInventory($inventory), $slot]; + return [$this->builder->getActionBuilder($window), $slot]; } /** @@ -152,12 +150,12 @@ protected function removeItemFromSlot(ItemStackRequestSlotInfo $slotInfo, int $c [$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo); if($count < 1){ //this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits - throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack"); + throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Cannot take less than 1 items from a stack"); } $existingItem = $inventory->getItem($slot); if($existingItem->getCount() < $count){ - throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount()); + throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount()); } $removed = $existingItem->pop($count); @@ -175,12 +173,12 @@ protected function addItemToSlot(ItemStackRequestSlotInfo $slotInfo, Item $item, [$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo); if($count < 1){ //this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits - throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack"); + throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Cannot take less than 1 items from a stack"); } $existingItem = $inventory->getItem($slot); if(!$existingItem->isNull() && !$existingItem->canStackWith($item)){ - throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Can only add items to an empty slot, or a slot containing the same item"); + throw new ItemStackRequestProcessException($this->prettyWindowAndSlot($inventory->getInventoryWindow(), $slot) . ": Can only add items to an empty slot, or a slot containing the same item"); } //we can't use the existing item here; it may be an empty stack @@ -337,7 +335,7 @@ protected function processItemStackRequestAction(ItemStackRequestAction $action) $this->setNextCreatedItem($item, true); }elseif($action instanceof CraftRecipeStackRequestAction){ $window = $this->player->getCurrentWindow(); - if($window instanceof EnchantInventory){ + if($window instanceof EnchantingTableInventoryWindow){ $optionId = $this->inventoryManager->getEnchantingTableOptionIndex($action->getRecipeId()); if($optionId !== null && ($option = $window->getOption($optionId)) !== null){ $this->specialTransaction = new EnchantingTransaction($this->player, $option, $optionId + 1); diff --git a/src/network/mcpe/handler/ItemStackResponseBuilder.php b/src/network/mcpe/handler/ItemStackResponseBuilder.php index faf479ee2db..7041d3f9c50 100644 --- a/src/network/mcpe/handler/ItemStackResponseBuilder.php +++ b/src/network/mcpe/handler/ItemStackResponseBuilder.php @@ -59,7 +59,8 @@ private function getInventoryAndSlot(int $containerInterfaceId, int $slotId) : ? if($windowAndSlot === null){ return null; } - [$inventory, $slot] = $windowAndSlot; + [$window, $slot] = $windowAndSlot; + $inventory = $window->getInventory(); if(!$inventory->slotExists($slot)){ return null; } diff --git a/src/inventory/PlayerEnderInventory.php b/src/player/InventoryWindow.php similarity index 62% rename from src/inventory/PlayerEnderInventory.php rename to src/player/InventoryWindow.php index c10c42b12ee..78dcef64af7 100644 --- a/src/inventory/PlayerEnderInventory.php +++ b/src/player/InventoryWindow.php @@ -21,17 +21,30 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\player; -use pocketmine\entity\Human; +use pocketmine\inventory\Inventory; + +abstract class InventoryWindow{ -final class PlayerEnderInventory extends SimpleInventory{ public function __construct( - private Human $holder, - int $size = 27 - ){ - parent::__construct($size); + protected Player $viewer, + protected Inventory $inventory + ){} + + public function getViewer() : Player{ + return $this->viewer; + } + + public function getInventory() : Inventory{ + return $this->inventory; } - public function getHolder() : Human{ return $this->holder; } + public function onOpen() : void{ + $this->inventory->onOpen($this->viewer); + } + + public function onClose() : void{ + $this->inventory->onClose($this->viewer); + } } diff --git a/src/player/Player.php b/src/player/Player.php index 5e57ab1cca7..744fb0fcba3 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -85,9 +85,7 @@ use pocketmine\inventory\CallbackInventoryListener; use pocketmine\inventory\CreativeInventory; use pocketmine\inventory\Inventory; -use pocketmine\inventory\PlayerCraftingInventory; -use pocketmine\inventory\PlayerCursorInventory; -use pocketmine\inventory\TemporaryInventory; +use pocketmine\inventory\SimpleInventory; use pocketmine\inventory\transaction\action\DropItemAction; use pocketmine\inventory\transaction\InventoryTransaction; use pocketmine\inventory\transaction\TransactionBuilder; @@ -223,11 +221,11 @@ public static function isValidUserName(?string $name) : bool{ protected bool $authenticated; protected PlayerInfo $playerInfo; - protected ?Inventory $currentWindow = null; - /** @var Inventory[] */ + protected ?InventoryWindow $currentWindow = null; + /** @var PlayerInventoryWindow[] */ protected array $permanentWindows = []; - protected PlayerCursorInventory $cursorInventory; - protected PlayerCraftingInventory $craftingGrid; + protected Inventory $cursorInventory; + protected CraftingGrid $craftingGrid; protected CreativeInventory $creativeInventory; protected int $messageCounter = 2; @@ -354,7 +352,7 @@ protected function initHumanData(CompoundTag $nbt) : void{ } private function callDummyItemHeldEvent() : void{ - $slot = $this->inventory->getHeldItemIndex(); + $slot = $this->hotbar->getSelectedIndex(); $event = new PlayerItemHeldEvent($this, $this->inventory->getItem($slot), $slot); $event->call(); @@ -369,7 +367,7 @@ protected function initEntity(CompoundTag $nbt) : void{ $this->inventory->getListeners()->add(new CallbackInventoryListener( function(Inventory $unused, int $slot) : void{ - if($slot === $this->inventory->getHeldItemIndex()){ + if($slot === $this->hotbar->getSelectedIndex()){ $this->setUsingItem(false); $this->callDummyItemHeldEvent(); @@ -1578,10 +1576,10 @@ public function chat(string $message) : bool{ } public function selectHotbarSlot(int $hotbarSlot) : bool{ - if(!$this->inventory->isHotbarSlot($hotbarSlot)){ //TODO: exception here? + if(!$this->hotbar->isHotbarSlot($hotbarSlot)){ //TODO: exception here? return false; } - if($hotbarSlot === $this->inventory->getHeldItemIndex()){ + if($hotbarSlot === $this->hotbar->getSelectedIndex()){ return true; } @@ -1591,7 +1589,7 @@ public function selectHotbarSlot(int $hotbarSlot) : bool{ return false; } - $this->inventory->setHeldItemIndex($hotbarSlot); + $this->hotbar->setSelectedIndex($hotbarSlot); $this->setUsingItem(false); return true; @@ -1603,7 +1601,7 @@ public function selectHotbarSlot(int $hotbarSlot) : bool{ private function returnItemsFromAction(Item $oldHeldItem, Item $newHeldItem, array $extraReturnedItems) : void{ $heldItemChanged = false; - if(!$newHeldItem->equalsExact($oldHeldItem) && $oldHeldItem->equalsExact($this->inventory->getItemInHand())){ + if(!$newHeldItem->equalsExact($oldHeldItem) && $oldHeldItem->equalsExact($this->hotbar->getHeldItem())){ //determine if the item was changed in some meaningful way, or just damaged/changed count //if it was really changed we always need to set it, whether we have finite resources or not $newReplica = clone $oldHeldItem; @@ -1617,7 +1615,7 @@ private function returnItemsFromAction(Item $oldHeldItem, Item $newHeldItem, arr if($newHeldItem instanceof Durable && $newHeldItem->isBroken()){ $this->broadcastSound(new ItemBreakSound()); } - $this->inventory->setItemInHand($newHeldItem); + $this->hotbar->setHeldItem($newHeldItem); $heldItemChanged = true; } } @@ -1627,7 +1625,7 @@ private function returnItemsFromAction(Item $oldHeldItem, Item $newHeldItem, arr } if($heldItemChanged && count($extraReturnedItems) > 0 && $newHeldItem->isNull()){ - $this->inventory->setItemInHand(array_shift($extraReturnedItems)); + $this->hotbar->setHeldItem(array_shift($extraReturnedItems)); } foreach($this->inventory->addItem(...$extraReturnedItems) as $drop){ //TODO: we can't generate a transaction for this since the items aren't coming from an inventory :( @@ -1649,7 +1647,7 @@ private function returnItemsFromAction(Item $oldHeldItem, Item $newHeldItem, arr */ public function useHeldItem() : bool{ $directionVector = $this->getDirectionVector(); - $item = $this->inventory->getItemInHand(); + $item = $this->hotbar->getHeldItem(); $oldItem = clone $item; $ev = new PlayerItemUseEvent($this, $item, $directionVector); @@ -1683,7 +1681,7 @@ public function useHeldItem() : bool{ * @return bool if the consumption succeeded. */ public function consumeHeldItem() : bool{ - $slot = $this->inventory->getItemInHand(); + $slot = $this->hotbar->getHeldItem(); if($slot instanceof ConsumableItem){ $oldItem = clone $slot; @@ -1716,7 +1714,7 @@ public function consumeHeldItem() : bool{ */ public function releaseHeldItem() : bool{ try{ - $item = $this->inventory->getItemInHand(); + $item = $this->hotbar->getHeldItem(); if(!$this->isUsingItem() || $this->hasItemCooldown($item)){ return false; } @@ -1786,21 +1784,21 @@ public function pickEntity(int $entityId) : bool{ private function equipOrAddPickedItem(int $existingSlot, Item $item) : void{ if($existingSlot !== -1){ - if($existingSlot < $this->inventory->getHotbarSize()){ - $this->inventory->setHeldItemIndex($existingSlot); + if($existingSlot < $this->hotbar->getSize()){ + $this->hotbar->setSelectedIndex($existingSlot); }else{ - $this->inventory->swap($this->inventory->getHeldItemIndex(), $existingSlot); + $this->inventory->swap($this->hotbar->getSelectedIndex(), $existingSlot); } }else{ $firstEmpty = $this->inventory->firstEmpty(); if($firstEmpty === -1){ //full inventory - $this->inventory->setItemInHand($item); - }elseif($firstEmpty < $this->inventory->getHotbarSize()){ + $this->hotbar->setHeldItem($item); + }elseif($firstEmpty < $this->hotbar->getSize()){ $this->inventory->setItem($firstEmpty, $item); - $this->inventory->setHeldItemIndex($firstEmpty); + $this->hotbar->setSelectedIndex($firstEmpty); }else{ - $this->inventory->swap($this->inventory->getHeldItemIndex(), $firstEmpty); - $this->inventory->setItemInHand($item); + $this->inventory->swap($this->hotbar->getSelectedIndex(), $firstEmpty); + $this->hotbar->setHeldItem($item); } } } @@ -1817,7 +1815,7 @@ public function attackBlock(Vector3 $pos, int $face) : bool{ $target = $this->getWorld()->getBlock($pos); - $ev = new PlayerInteractEvent($this, $this->inventory->getItemInHand(), $target, null, $face, PlayerInteractEvent::LEFT_CLICK_BLOCK); + $ev = new PlayerInteractEvent($this, $this->hotbar->getHeldItem(), $target, null, $face, PlayerInteractEvent::LEFT_CLICK_BLOCK); if($this->isSpectator()){ $ev->cancel(); } @@ -1826,7 +1824,7 @@ public function attackBlock(Vector3 $pos, int $face) : bool{ return false; } $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers()); - if($target->onAttack($this->inventory->getItemInHand(), $face, $this)){ + if($target->onAttack($this->hotbar->getHeldItem(), $face, $this)){ return true; } @@ -1867,7 +1865,7 @@ public function breakBlock(Vector3 $pos) : bool{ if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){ $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers()); $this->stopBreakBlock($pos); - $item = $this->inventory->getItemInHand(); + $item = $this->hotbar->getHeldItem(); $oldItem = clone $item; $returnedItems = []; if($this->getWorld()->useBreakOn($pos, $item, $this, true, $returnedItems)){ @@ -1892,7 +1890,7 @@ public function interactBlock(Vector3 $pos, int $face, Vector3 $clickOffset) : b if($this->canInteract($pos->add(0.5, 0.5, 0.5), $this->isCreative() ? self::MAX_REACH_DISTANCE_CREATIVE : self::MAX_REACH_DISTANCE_SURVIVAL)){ $this->broadcastAnimation(new ArmSwingAnimation($this), $this->getViewers()); - $item = $this->inventory->getItemInHand(); //this is a copy of the real item + $item = $this->hotbar->getHeldItem(); //this is a copy of the real item $oldItem = clone $item; $returnedItems = []; if($this->getWorld()->useItemOn($pos, $item, $face, $clickOffset, $this, true, $returnedItems)){ @@ -1921,7 +1919,7 @@ public function attackEntity(Entity $entity) : bool{ return false; } - $heldItem = $this->inventory->getItemInHand(); + $heldItem = $this->hotbar->getHeldItem(); $oldItem = clone $heldItem; $ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_ENTITY_ATTACK, $heldItem->getAttackPoints()); @@ -2007,15 +2005,15 @@ public function interactEntity(Entity $entity, Vector3 $clickPos) : bool{ $ev->call(); - $item = $this->inventory->getItemInHand(); + $item = $this->hotbar->getHeldItem(); $oldItem = clone $item; if(!$ev->isCancelled()){ if($item->onInteractEntity($this, $entity, $clickPos)){ - if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->inventory->getItemInHand())){ + if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->hotbar->getHeldItem())){ if($item instanceof Durable && $item->isBroken()){ $this->broadcastSound(new ItemBreakSound()); } - $this->inventory->setItemInHand($item); + $this->hotbar->setHeldItem($item); } } return $entity->onInteract($this, $clickPos); @@ -2356,7 +2354,7 @@ public function onPostDisconnect(Translatable|string $reason, Translatable|strin $this->loadQueue = []; $this->removeCurrentWindow(); - $this->removePermanentInventories(); + $this->removePermanentWindows(); $this->perm->getPermissionRecalculationCallbacks()->clear(); @@ -2372,8 +2370,6 @@ protected function onDispose() : void{ protected function destroyCycles() : void{ $this->networkSession = null; - unset($this->cursorInventory); - unset($this->craftingGrid); $this->spawnPosition = null; $this->deathPosition = null; $this->blockBreakHandler = null; @@ -2453,8 +2449,8 @@ protected function onDeath() : void{ $this->getWorld()->dropItem($this->location, $item); } + $this->hotbar->setSelectedIndex(0); $clearInventory = fn(Inventory $inventory) => $inventory->setContents(array_filter($inventory->getContents(), fn(Item $item) => $item->keepOnDeath())); - $this->inventory->setHeldItemIndex(0); $clearInventory($this->inventory); $clearInventory($this->armorInventory); $clearInventory($this->offHandInventory); @@ -2648,15 +2644,19 @@ public function teleport(Vector3 $pos, ?float $yaw = null, ?float $pitch = null) } protected function addDefaultWindows() : void{ - $this->cursorInventory = new PlayerCursorInventory($this); - $this->craftingGrid = new PlayerCraftingInventory($this); + $this->cursorInventory = new SimpleInventory(1); + $this->craftingGrid = new CraftingGrid(CraftingGrid::SIZE_SMALL); - $this->addPermanentInventories($this->inventory, $this->armorInventory, $this->cursorInventory, $this->offHandInventory, $this->craftingGrid); - - //TODO: more windows + $this->addPermanentWindows([ + new PlayerInventoryWindow($this, $this->inventory, PlayerInventoryWindow::TYPE_INVENTORY), + new PlayerInventoryWindow($this, $this->armorInventory, PlayerInventoryWindow::TYPE_ARMOR), + new PlayerInventoryWindow($this, $this->cursorInventory, PlayerInventoryWindow::TYPE_CURSOR), + new PlayerInventoryWindow($this, $this->offHandInventory, PlayerInventoryWindow::TYPE_OFFHAND), + new PlayerInventoryWindow($this, $this->craftingGrid, PlayerInventoryWindow::TYPE_CRAFTING), + ]); } - public function getCursorInventory() : PlayerCursorInventory{ + public function getCursorInventory() : Inventory{ return $this->cursorInventory; } @@ -2687,22 +2687,37 @@ public function setCreativeInventory(CreativeInventory $inventory) : void{ * inventory. */ private function doCloseInventory() : void{ - $inventories = [$this->craftingGrid, $this->cursorInventory]; - if($this->currentWindow instanceof TemporaryInventory){ - $inventories[] = $this->currentWindow; + $windowsToClear = []; + $mainInventoryWindow = null; + foreach($this->permanentWindows as $window){ + if($window->getType() === PlayerInventoryWindow::TYPE_CRAFTING || $window->getType() === PlayerInventoryWindow::TYPE_CURSOR){ + $windowsToClear[] = $window; + }elseif($window->getType() === PlayerInventoryWindow::TYPE_INVENTORY){ + $mainInventoryWindow = $window; + } + } + if($mainInventoryWindow === null){ + //TODO: in the future this might not be the case, if we implement support for the player closing their + //inventory window outside the protocol layer + //in that case we'd have to create a new ephemeral window here + throw new AssumptionFailedError("This should never be null"); + } + + if($this->currentWindow instanceof TemporaryInventoryWindow){ + $windowsToClear[] = $this->currentWindow; } $builder = new TransactionBuilder(); - foreach($inventories as $inventory){ - $contents = $inventory->getContents(); + foreach($windowsToClear as $window){ + $contents = $window->getInventory()->getContents(); if(count($contents) > 0){ - $drops = $builder->getInventory($this->inventory)->addItem(...$contents); + $drops = $builder->getActionBuilder($mainInventoryWindow)->addItem(...$contents); foreach($drops as $drop){ $builder->addAction(new DropItemAction($drop)); } - $builder->getInventory($inventory)->clearAll(); + $builder->getActionBuilder($window)->clearAll(); } } @@ -2714,8 +2729,8 @@ private function doCloseInventory() : void{ $this->logger->debug("Successfully evacuated items from temporary inventories"); }catch(TransactionCancelledException){ $this->logger->debug("Plugin cancelled transaction evacuating items from temporary inventories; items will be destroyed"); - foreach($inventories as $inventory){ - $inventory->clearAll(); + foreach($windowsToClear as $window){ + $window->getInventory()->clearAll(); } }catch(TransactionValidationException $e){ throw new AssumptionFailedError("This server-generated transaction should never be invalid", 0, $e); @@ -2726,18 +2741,21 @@ private function doCloseInventory() : void{ /** * Returns the inventory the player is currently viewing. This might be a chest, furnace, or any other container. */ - public function getCurrentWindow() : ?Inventory{ + public function getCurrentWindow() : ?InventoryWindow{ return $this->currentWindow; } /** * Opens an inventory window to the player. Returns if it was successful. */ - public function setCurrentWindow(Inventory $inventory) : bool{ - if($inventory === $this->currentWindow){ + public function setCurrentWindow(InventoryWindow $window) : bool{ + if($window === $this->currentWindow){ return true; } - $ev = new InventoryOpenEvent($inventory, $this); + if($window->getViewer() !== $this){ + throw new \InvalidArgumentException("Cannot reuse InventoryWindow instances, please create a new one for each player"); + } + $ev = new InventoryOpenEvent($window, $this); $ev->call(); if($ev->isCancelled()){ return false; @@ -2748,10 +2766,10 @@ public function setCurrentWindow(Inventory $inventory) : bool{ if(($inventoryManager = $this->getNetworkSession()->getInvManager()) === null){ throw new \InvalidArgumentException("Player cannot open inventories in this state"); } - $this->logger->debug("Opening inventory " . get_class($inventory) . "#" . spl_object_id($inventory)); - $inventoryManager->onCurrentWindowChange($inventory); - $inventory->onOpen($this); - $this->currentWindow = $inventory; + $this->logger->debug("Opening inventory " . get_class($window) . "#" . spl_object_id($window)); + $inventoryManager->onCurrentWindowChange($window); + $window->onOpen(); + $this->currentWindow = $window; return true; } @@ -2760,7 +2778,7 @@ public function removeCurrentWindow() : void{ if($this->currentWindow !== null){ $currentWindow = $this->currentWindow; $this->logger->debug("Closing inventory " . get_class($this->currentWindow) . "#" . spl_object_id($this->currentWindow)); - $this->currentWindow->onClose($this); + $this->currentWindow->onClose(); if(($inventoryManager = $this->getNetworkSession()->getInvManager()) !== null){ $inventoryManager->onCurrentWindowRemove(); } @@ -2769,20 +2787,31 @@ public function removeCurrentWindow() : void{ } } - protected function addPermanentInventories(Inventory ...$inventories) : void{ - foreach($inventories as $inventory){ - $inventory->onOpen($this); - $this->permanentWindows[spl_object_id($inventory)] = $inventory; + /** + * @param PlayerInventoryWindow[] $windows + */ + protected function addPermanentWindows(array $windows) : void{ + foreach($windows as $window){ + $window->onOpen(); + $this->permanentWindows[spl_object_id($window)] = $window; } } - protected function removePermanentInventories() : void{ - foreach($this->permanentWindows as $inventory){ - $inventory->onClose($this); + protected function removePermanentWindows() : void{ + foreach($this->permanentWindows as $window){ + $window->onClose(); } $this->permanentWindows = []; } + /** + * @return PlayerInventoryWindow[] + * @internal + */ + public function getPermanentWindows() : array{ + return $this->permanentWindows; + } + /** * Opens the player's sign editor GUI for the sign at the given position. * TODO: add support for editing the rear side of the sign (not currently supported due to technical limitations) diff --git a/src/player/PlayerInventoryWindow.php b/src/player/PlayerInventoryWindow.php new file mode 100644 index 00000000000..e9bdf8f7439 --- /dev/null +++ b/src/player/PlayerInventoryWindow.php @@ -0,0 +1,51 @@ +type; } +} diff --git a/src/player/SurvivalBlockBreakHandler.php b/src/player/SurvivalBlockBreakHandler.php index e31e77ef7c7..95db3916871 100644 --- a/src/player/SurvivalBlockBreakHandler.php +++ b/src/player/SurvivalBlockBreakHandler.php @@ -66,7 +66,7 @@ private function calculateBreakProgressPerTick() : float{ return 0.0; } //TODO: improve this to take stuff like swimming, ladders, enchanted tools into account, fix wrong tool break time calculations for bad tools (pmmp/PocketMine-MP#211) - $breakTimePerTick = $this->block->getBreakInfo()->getBreakTime($this->player->getInventory()->getItemInHand()) * 20; + $breakTimePerTick = $this->block->getBreakInfo()->getBreakTime($this->player->getHotbar()->getHeldItem()) * 20; if($breakTimePerTick > 0){ return 1 / $breakTimePerTick; diff --git a/src/inventory/TemporaryInventory.php b/src/player/TemporaryInventoryWindow.php similarity index 90% rename from src/inventory/TemporaryInventory.php rename to src/player/TemporaryInventoryWindow.php index 26a53b1718c..c4238656415 100644 --- a/src/inventory/TemporaryInventory.php +++ b/src/player/TemporaryInventoryWindow.php @@ -21,8 +21,8 @@ declare(strict_types=1); -namespace pocketmine\inventory; +namespace pocketmine\player; -interface TemporaryInventory extends Inventory{ +interface TemporaryInventoryWindow{ } diff --git a/src/world/World.php b/src/world/World.php index ff65377c094..863cbd90fb8 100644 --- a/src/world/World.php +++ b/src/world/World.php @@ -30,6 +30,7 @@ use pocketmine\block\Block; use pocketmine\block\BlockTypeIds; use pocketmine\block\RuntimeBlockStateRegistry; +use pocketmine\block\tile\ContainerTile; use pocketmine\block\tile\Spawnable; use pocketmine\block\tile\Tile; use pocketmine\block\tile\TileFactory; @@ -57,6 +58,8 @@ use pocketmine\event\world\WorldParticleEvent; use pocketmine\event\world\WorldSaveEvent; use pocketmine\event\world\WorldSoundEvent; +use pocketmine\inventory\Inventory; +use pocketmine\inventory\InventoryListener; use pocketmine\item\Item; use pocketmine\item\ItemUseResult; use pocketmine\item\LegacyStringToItemParser; @@ -144,7 +147,7 @@ * @phpstan-type BlockPosHash int * @phpstan-type ChunkBlockPosHash int */ -class World implements ChunkManager{ +class World implements ChunkManager, InventoryListener{ private static int $worldIdCounter = 1; @@ -282,6 +285,12 @@ class World implements ChunkManager{ */ private array $chunks = []; + /** + * @var Vector3[]|\WeakMap + * @phpstan-var \WeakMap + */ + private \WeakMap $containerToBlockPositionMap; + /** * @var Vector3[][] chunkHash => [relativeBlockHash => Vector3] * @phpstan-var array> @@ -506,6 +515,8 @@ public function __construct( } }); + $this->containerToBlockPositionMap = new \WeakMap(); + $this->scheduledBlockUpdateQueue = new ReversePriorityQueue(); $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); @@ -2787,6 +2798,10 @@ public function addTile(Tile $tile) : void{ //delegate tile ticking to the corresponding block $this->scheduleDelayedBlockUpdate($pos->asVector3(), 1); + if($tile instanceof ContainerTile){ + $tile->getInventory()->getListeners()->add($this); + $this->containerToBlockPositionMap[$tile->getInventory()] = $pos->asVector3(); + } } /** @@ -2805,11 +2820,41 @@ public function removeTile(Tile $tile) : void{ if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){ $this->chunks[$hash]->removeTile($tile); } + if($tile instanceof ContainerTile){ + $inventory = $tile->getInventory(); + $inventory->removeAllViewers(); + $inventory->getListeners()->remove($this); + unset($this->containerToBlockPositionMap[$inventory]); + } foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){ $listener->onBlockChanged($pos->asVector3()); } } + private function notifyInventoryUpdate(Inventory $inventory) : void{ + $blockPosition = $this->containerToBlockPositionMap[$inventory] ?? null; + if($blockPosition !== null){ + $this->getBlock($blockPosition)->onContainerUpdate($inventory); + $this->scheduleDelayedBlockUpdate($blockPosition, 1); + } + } + + /** + * @internal + * @see InventoryListener + */ + public function onSlotChange(Inventory $inventory, int $slot, Item $oldItem) : void{ + $this->notifyInventoryUpdate($inventory); + } + + /** + * @internal + * @see InventoryListener + */ + public function onContentChange(Inventory $inventory, array $oldContents) : void{ + $this->notifyInventoryUpdate($inventory); + } + public function isChunkInUse(int $x, int $z) : bool{ return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) && count($this->chunkLoaders[$index]) > 0; } diff --git a/tests/phpunit/inventory/CombinedInventoryProxyTest.php b/tests/phpunit/inventory/CombinedInventoryProxyTest.php new file mode 100644 index 00000000000..5ab7eb3a87c --- /dev/null +++ b/tests/phpunit/inventory/CombinedInventoryProxyTest.php @@ -0,0 +1,269 @@ + + */ + 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 CombinedInventoryProxy($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 CombinedInventoryProxy($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 CombinedInventoryProxy($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 CombinedInventoryProxy($backing); + $inventory->setContents($altItems); + + $this->verifyWriteItems($backing, $altItems); + } + + public function testGetSize() : void{ + self::assertSame(4, (new CombinedInventoryProxy($this->createInventories()))->getSize()); + } + + public function testGetMatchingItemCount() : void{ + $inventory = new CombinedInventoryProxy($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 CombinedInventoryProxy($this->createInventories()); + + self::assertTrue($inventory->isSlotEmpty(2)); + self::assertFalse($inventory->isSlotEmpty(0)); + self::assertFalse($inventory->isSlotEmpty(1)); + self::assertFalse($inventory->isSlotEmpty(3)); + } + + public function testListenersOnProxySlotUpdate() : void{ + $inventory = new CombinedInventoryProxy($this->createInventories()); + + $numChanges = 0; + $inventory->getListeners()->add(new CallbackInventoryListener( + onSlotChange: function(Inventory $inventory, int $slot, Item $before) use (&$numChanges) : void{ + $numChanges++; + }, + onContentChange: null + )); + $inventory->setItem(0, VanillaItems::DIAMOND_SWORD()); + self::assertSame(1, $numChanges, "Inventory listener detected wrong number of changes"); + } + + public function testListenersOnProxyContentUpdate() : void{ + $inventory = new CombinedInventoryProxy($this->createInventories()); + + $numChanges = 0; + $inventory->getListeners()->add(new CallbackInventoryListener( + onSlotChange: null, + onContentChange: function(Inventory $inventory, array $oldItems) use (&$numChanges) : void{ + $numChanges++; + } + )); + $inventory->setContents(self::getAltItems()); + self::assertSame(1, $numChanges, "Expected onContentChange to be called exactly 1 time"); + } + + public function testListenersOnBackingSlotUpdate() : void{ + $backing = $this->createInventories(); + $inventory = new CombinedInventoryProxy($backing); + + $slotChangeDetected = null; + $numChanges = 0; + $inventory->getListeners()->add(new CallbackInventoryListener( + onSlotChange: function(Inventory $inventory, int $slot, Item $before) use (&$slotChangeDetected, &$numChanges) : void{ + $slotChangeDetected = $slot; + $numChanges++; + }, + onContentChange: null + )); + $backing[2]->setItem(0, VanillaItems::DIAMOND_SWORD()); + self::assertNotNull($slotChangeDetected, "Inventory listener didn't hear about backing inventory update"); + self::assertSame(2, $slotChangeDetected, "Inventory listener detected unexpected slot change"); + self::assertSame(1, $numChanges, "Inventory listener detected wrong number of changes"); + } + + /** + * When a combined inventory has multiple backing inventories, content updates of the backing inventories must be + * turned into slot updates on the proxy, to avoid syncing the entire proxy inventory. + */ + public function testListenersOnBackingContentUpdate() : void{ + $backing = $this->createInventories(); + $inventory = new CombinedInventoryProxy($backing); + + $slotChanges = []; + $inventory->getListeners()->add(new CallbackInventoryListener( + onSlotChange: function(Inventory $inventory, int $slot, Item $before) use (&$slotChanges) : void{ + $slotChanges[] = $slot; + }, + onContentChange: null + )); + $backing[2]->setContents([VanillaItems::DIAMOND_SWORD(), VanillaItems::DIAMOND()]); + self::assertCount(2, $slotChanges, "Inventory listener detected wrong number of changes"); + self::assertSame([2, 3], $slotChanges, "Incorrect slots updated"); + } + + /** + * If a combined inventory has only 1 backing inventory, content updates on the backing inventory can be directly + * processed as content updates on the proxy inventory without modification. This allows optimizations when only 1 + * backing inventory is used. + * This test verifies that this special case works as expected. + */ + public function testListenersOnSingleBackingContentUpdate() : void{ + $backing = new SimpleInventory(2); + $inventory = new CombinedInventoryProxy([$backing]); + + $numChanges = 0; + $inventory->getListeners()->add(new CallbackInventoryListener( + onSlotChange: null, + onContentChange: function(Inventory $inventory, array $oldItems) use (&$numChanges) : void{ + $numChanges++; + } + )); + $inventory->setContents([VanillaItems::DIAMOND_SWORD(), VanillaItems::DIAMOND()]); + self::assertSame(1, $numChanges, "Expected onContentChange to be called exactly 1 time"); + } +}