From 3ebe3f76e445279d9642ead2af0c49af2504b630 Mon Sep 17 00:00:00 2001
From: FoxWorn3365
Date: Mon, 26 Jun 2023 21:03:03 +0200
Subject: [PATCH 01/11] Updated the dev version for pmmp4, this is a FINAL
relase
---
README.md | 6 +-
plugin.yml | 2 +-
src/muqsit/invmenu/InvMenu.php | 178 ++++++++++++++
src/muqsit/invmenu/InvMenuEventHandler.php | 97 ++++++++
src/muqsit/invmenu/InvMenuHandler.php | 46 ++++
.../invmenu/inventory/InvMenuInventory.php | 23 ++
.../inventory/SharedInvMenuSynchronizer.php | 32 +++
.../inventory/SharedInventoryNotifier.php | 31 +++
.../inventory/SharedInventorySynchronizer.php | 28 +++
src/muqsit/invmenu/session/InvMenuInfo.php | 17 ++
src/muqsit/invmenu/session/PlayerManager.php | 60 +++++
src/muqsit/invmenu/session/PlayerSession.php | 98 ++++++++
.../network/NetworkStackLatencyEntry.php | 21 ++
.../invmenu/session/network/PlayerNetwork.php | 229 ++++++++++++++++++
.../handler/ClosurePlayerNetworkHandler.php | 22 ++
.../network/handler/PlayerNetworkHandler.php | 13 +
.../handler/PlayerNetworkHandlerRegistry.php | 39 +++
.../DeterministicInvMenuTransaction.php | 60 +++++
.../transaction/InvMenuTransaction.php | 43 ++++
.../transaction/InvMenuTransactionResult.php | 39 +++
.../transaction/SimpleInvMenuTransaction.php | 57 +++++
.../invmenu/type/ActorFixedInvMenuType.php | 44 ++++
.../type/BlockActorFixedInvMenuType.php | 54 +++++
.../invmenu/type/BlockFixedInvMenuType.php | 41 ++++
...ublePairableBlockActorFixedInvMenuType.php | 70 ++++++
src/muqsit/invmenu/type/FixedInvMenuType.php | 18 ++
src/muqsit/invmenu/type/InvMenuType.php | 17 ++
src/muqsit/invmenu/type/InvMenuTypeIds.php | 12 +
.../invmenu/type/InvMenuTypeRegistry.php | 72 ++++++
.../type/graphic/ActorInvMenuGraphic.php | 71 ++++++
.../type/graphic/BlockActorInvMenuGraphic.php | 70 ++++++
.../type/graphic/BlockInvMenuGraphic.php | 59 +++++
.../invmenu/type/graphic/InvMenuGraphic.php | 28 +++
.../type/graphic/MultiBlockInvMenuGraphic.php | 65 +++++
.../type/graphic/PositionedInvMenuGraphic.php | 12 +
.../ActorInvMenuGraphicNetworkTranslator.php | 22 ++
.../BlockInvMenuGraphicNetworkTranslator.php | 33 +++
.../InvMenuGraphicNetworkTranslator.php | 14 ++
.../MultiInvMenuGraphicNetworkTranslator.php | 25 ++
...dowTypeInvMenuGraphicNetworkTranslator.php | 20 ++
.../invmenu/type/util/InvMenuTypeBuilders.php | 29 +++
.../invmenu/type/util/InvMenuTypeHelper.php | 52 ++++
.../builder/ActorFixedInvMenuTypeBuilder.php | 38 +++
.../builder/ActorInvMenuTypeBuilderTrait.php | 45 ++++
...imationDurationInvMenuTypeBuilderTrait.php | 19 ++
.../BlockActorFixedInvMenuTypeBuilder.php | 35 +++
.../builder/BlockFixedInvMenuTypeBuilder.php | 22 ++
.../builder/BlockInvMenuTypeBuilderTrait.php | 22 ++
...rableBlockActorFixedInvMenuTypeBuilder.php | 35 +++
.../builder/FixedInvMenuTypeBuilderTrait.php | 21 ++
...orkTranslatableInvMenuTypeBuilderTrait.php | 37 +++
.../type/util/builder/InvMenuTypeBuilder.php | 12 +
52 files changed, 2251 insertions(+), 4 deletions(-)
create mode 100644 src/muqsit/invmenu/InvMenu.php
create mode 100644 src/muqsit/invmenu/InvMenuEventHandler.php
create mode 100644 src/muqsit/invmenu/InvMenuHandler.php
create mode 100644 src/muqsit/invmenu/inventory/InvMenuInventory.php
create mode 100644 src/muqsit/invmenu/inventory/SharedInvMenuSynchronizer.php
create mode 100644 src/muqsit/invmenu/inventory/SharedInventoryNotifier.php
create mode 100644 src/muqsit/invmenu/inventory/SharedInventorySynchronizer.php
create mode 100644 src/muqsit/invmenu/session/InvMenuInfo.php
create mode 100644 src/muqsit/invmenu/session/PlayerManager.php
create mode 100644 src/muqsit/invmenu/session/PlayerSession.php
create mode 100644 src/muqsit/invmenu/session/network/NetworkStackLatencyEntry.php
create mode 100644 src/muqsit/invmenu/session/network/PlayerNetwork.php
create mode 100644 src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php
create mode 100644 src/muqsit/invmenu/session/network/handler/PlayerNetworkHandler.php
create mode 100644 src/muqsit/invmenu/session/network/handler/PlayerNetworkHandlerRegistry.php
create mode 100644 src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php
create mode 100644 src/muqsit/invmenu/transaction/InvMenuTransaction.php
create mode 100644 src/muqsit/invmenu/transaction/InvMenuTransactionResult.php
create mode 100644 src/muqsit/invmenu/transaction/SimpleInvMenuTransaction.php
create mode 100644 src/muqsit/invmenu/type/ActorFixedInvMenuType.php
create mode 100644 src/muqsit/invmenu/type/BlockActorFixedInvMenuType.php
create mode 100644 src/muqsit/invmenu/type/BlockFixedInvMenuType.php
create mode 100644 src/muqsit/invmenu/type/DoublePairableBlockActorFixedInvMenuType.php
create mode 100644 src/muqsit/invmenu/type/FixedInvMenuType.php
create mode 100644 src/muqsit/invmenu/type/InvMenuType.php
create mode 100644 src/muqsit/invmenu/type/InvMenuTypeIds.php
create mode 100644 src/muqsit/invmenu/type/InvMenuTypeRegistry.php
create mode 100644 src/muqsit/invmenu/type/graphic/ActorInvMenuGraphic.php
create mode 100644 src/muqsit/invmenu/type/graphic/BlockActorInvMenuGraphic.php
create mode 100644 src/muqsit/invmenu/type/graphic/BlockInvMenuGraphic.php
create mode 100644 src/muqsit/invmenu/type/graphic/InvMenuGraphic.php
create mode 100644 src/muqsit/invmenu/type/graphic/MultiBlockInvMenuGraphic.php
create mode 100644 src/muqsit/invmenu/type/graphic/PositionedInvMenuGraphic.php
create mode 100644 src/muqsit/invmenu/type/graphic/network/ActorInvMenuGraphicNetworkTranslator.php
create mode 100644 src/muqsit/invmenu/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php
create mode 100644 src/muqsit/invmenu/type/graphic/network/InvMenuGraphicNetworkTranslator.php
create mode 100644 src/muqsit/invmenu/type/graphic/network/MultiInvMenuGraphicNetworkTranslator.php
create mode 100644 src/muqsit/invmenu/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php
create mode 100644 src/muqsit/invmenu/type/util/InvMenuTypeBuilders.php
create mode 100644 src/muqsit/invmenu/type/util/InvMenuTypeHelper.php
create mode 100644 src/muqsit/invmenu/type/util/builder/ActorFixedInvMenuTypeBuilder.php
create mode 100644 src/muqsit/invmenu/type/util/builder/ActorInvMenuTypeBuilderTrait.php
create mode 100644 src/muqsit/invmenu/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php
create mode 100644 src/muqsit/invmenu/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php
create mode 100644 src/muqsit/invmenu/type/util/builder/BlockFixedInvMenuTypeBuilder.php
create mode 100644 src/muqsit/invmenu/type/util/builder/BlockInvMenuTypeBuilderTrait.php
create mode 100644 src/muqsit/invmenu/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php
create mode 100644 src/muqsit/invmenu/type/util/builder/FixedInvMenuTypeBuilderTrait.php
create mode 100644 src/muqsit/invmenu/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php
create mode 100644 src/muqsit/invmenu/type/util/builder/InvMenuTypeBuilder.php
diff --git a/README.md b/README.md
index 60984d2..1936c73 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
---
-Shopkeepers v0.9.1 for PocketMine-MP 5
+Shopkeepers v0.9.1 for PocketMine-MP 4
**⚠️ We are not in any way related to the [Shopkeepers plugin](https://dev.bukkit.org/projects/shopkeepers) for Bukkit!**
@@ -35,8 +35,8 @@
## Compatibility
**Shopkeepers** is made to be multi-version, in fact I announce with great joy that the plugin is available for both PocketMine-MP 5 and PocketMine-MP 4!
> **Warning**
-> The Shopkeepers version for PocketMine-MP 4 is available exclusively here on GitHub since InvMenu has versions that are not compatible with each other!
-> The branch can be found [here](https://github.com/FoxWorn3365/Shopkeepers/tree/pmmp4)
+> This is the branch for **PocketMine-MP 4** only!
+> The branch for PocketMine-MP **5** can be found [here](https://github.com/FoxWorn3365/Shopkeepers) or on [poggit]()
## Configuration
The configuration of **Shopkeepers** allows you to customize some values to make it suitable for all servers.
diff --git a/plugin.yml b/plugin.yml
index ec4282f..863fe7c 100644
--- a/plugin.yml
+++ b/plugin.yml
@@ -1,6 +1,6 @@
name: Shopkeepers
version: 0.9.1
-api: 5.0.0
+api: 4.0.0
main: FoxWorn3365\Shopkeepers\Core
author: FoxWorn3365
diff --git a/src/muqsit/invmenu/InvMenu.php b/src/muqsit/invmenu/InvMenu.php
new file mode 100644
index 0000000..27b77a7
--- /dev/null
+++ b/src/muqsit/invmenu/InvMenu.php
@@ -0,0 +1,178 @@
+get($identifier), ...$args);
+ }
+
+ /**
+ * @param (Closure(DeterministicInvMenuTransaction) : void)|null $listener
+ * @return Closure(InvMenuTransaction) : InvMenuTransactionResult
+ */
+ public static function readonly(?Closure $listener = null) : Closure{
+ return static function(InvMenuTransaction $transaction) use($listener) : InvMenuTransactionResult{
+ $result = $transaction->discard();
+ if($listener !== null){
+ $listener(new DeterministicInvMenuTransaction($transaction, $result));
+ }
+ return $result;
+ };
+ }
+
+ protected InvMenuType $type;
+ protected ?string $name = null;
+ protected ?Closure $listener = null;
+ protected ?Closure $inventory_close_listener = null;
+ protected Inventory $inventory;
+ protected ?SharedInvMenuSynchronizer $synchronizer = null;
+
+ public function __construct(InvMenuType $type, ?Inventory $custom_inventory = null){
+ if(!InvMenuHandler::isRegistered()){
+ throw new LogicException("Tried creating menu before calling " . InvMenuHandler::class . "::register()");
+ }
+ $this->type = $type;
+ $this->inventory = $this->type->createInventory();
+ $this->setInventory($custom_inventory);
+ }
+
+ public function getType() : InvMenuType{
+ return $this->type;
+ }
+
+ public function getName() : ?string{
+ return $this->name;
+ }
+
+ public function setName(?string $name) : self{
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * @param (Closure(InvMenuTransaction) : InvMenuTransactionResult)|null $listener
+ * @return self
+ */
+ public function setListener(?Closure $listener) : self{
+ $this->listener = $listener;
+ return $this;
+ }
+
+ /**
+ * @param (Closure(Player, Inventory) : void)|null $listener
+ * @return self
+ */
+ public function setInventoryCloseListener(?Closure $listener) : self{
+ $this->inventory_close_listener = $listener;
+ return $this;
+ }
+
+ /**
+ * @param Player $player
+ * @param string|null $name
+ * @param (Closure(bool) : void)|null $callback
+ */
+ final public function send(Player $player, ?string $name = null, ?Closure $callback = null) : void{
+ $player->removeCurrentWindow();
+
+ $session = InvMenuHandler::getPlayerManager()->get($player);
+ $network = $session->getNetwork();
+
+ // Avoid players from spamming InvMenu::send() and other similar
+ // requests and filling up queued tasks in memory.
+ // It would be better if this check were implemented by plugins,
+ // however I suppose it is more convenient if done within InvMenu...
+ if($network->getPending() >= 8){
+ $network->dropPending();
+ }else{
+ $network->dropPendingOfType(PlayerNetwork::DELAY_TYPE_OPERATION);
+ }
+
+ $network->waitUntil(PlayerNetwork::DELAY_TYPE_OPERATION, 0, function(bool $success) use($player, $session, $name, $callback) : bool{
+ if(!$success){
+ if($callback !== null){
+ $callback(false);
+ }
+ return false;
+ }
+
+ $graphic = $this->type->createGraphic($this, $player);
+ if($graphic !== null){
+ $session->setCurrentMenu(new InvMenuInfo($this, $graphic, $name), static function(bool $success) use($callback) : void{
+ if($callback !== null){
+ $callback($success);
+ }
+ });
+ }else{
+ if($callback !== null){
+ $callback(false);
+ }
+ }
+ return false;
+ });
+ }
+
+ public function getInventory() : Inventory{
+ return $this->inventory;
+ }
+
+ public function setInventory(?Inventory $custom_inventory) : void{
+ if($this->synchronizer !== null){
+ $this->synchronizer->destroy();
+ $this->synchronizer = null;
+ }
+
+ if($custom_inventory !== null){
+ $this->synchronizer = new SharedInvMenuSynchronizer($this, $custom_inventory);
+ }
+ }
+
+ /**
+ * @internal use InvMenu::send() instead.
+ *
+ * @param Player $player
+ * @return bool
+ */
+ public function sendInventory(Player $player) : bool{
+ return $player->setCurrentWindow($this->getInventory());
+ }
+
+ public function handleInventoryTransaction(Player $player, Item $out, Item $in, SlotChangeAction $action, InventoryTransaction $transaction) : InvMenuTransactionResult{
+ $inv_menu_txn = new SimpleInvMenuTransaction($player, $out, $in, $action, $transaction);
+ return $this->listener !== null ? ($this->listener)($inv_menu_txn) : $inv_menu_txn->continue();
+ }
+
+ public function onClose(Player $player) : void{
+ if($this->inventory_close_listener !== null){
+ ($this->inventory_close_listener)($player, $this->getInventory());
+ }
+
+ InvMenuHandler::getPlayerManager()->get($player)->removeCurrentMenu();
+ }
+}
diff --git a/src/muqsit/invmenu/InvMenuEventHandler.php b/src/muqsit/invmenu/InvMenuEventHandler.php
new file mode 100644
index 0000000..5de6be4
--- /dev/null
+++ b/src/muqsit/invmenu/InvMenuEventHandler.php
@@ -0,0 +1,97 @@
+getPacket();
+ if($packet instanceof NetworkStackLatencyPacket){
+ $player = $event->getOrigin()->getPlayer();
+ if($player !== null){
+ $this->player_manager->getNullable($player)?->getNetwork()->notify($packet->timestamp);
+ }
+ }
+ }
+
+ /**
+ * @param InventoryCloseEvent $event
+ * @priority MONITOR
+ */
+ public function onInventoryClose(InventoryCloseEvent $event) : void{
+ $player = $event->getPlayer();
+ $session = $this->player_manager->getNullable($player);
+ if($session === null){
+ return;
+ }
+
+ $current = $session->getCurrent();
+ if($current !== null && $event->getInventory() === $current->menu->getInventory()){
+ $current->menu->onClose($player);
+ }
+ $session->getNetwork()->waitUntil(PlayerNetwork::DELAY_TYPE_ANIMATION_WAIT, 325, static fn(bool $success) : bool => false);
+ }
+
+ /**
+ * @param InventoryTransactionEvent $event
+ * @priority NORMAL
+ */
+ public function onInventoryTransaction(InventoryTransactionEvent $event) : void{
+ $transaction = $event->getTransaction();
+ $player = $transaction->getSource();
+
+ $player_instance = $this->player_manager->get($player);
+ $current = $player_instance->getCurrent();
+ if($current === null){
+ return;
+ }
+
+ $inventory = $current->menu->getInventory();
+ $network_stack_callbacks = [];
+ foreach($transaction->getActions() as $action){
+ if(!($action instanceof SlotChangeAction) || $action->getInventory() !== $inventory){
+ continue;
+ }
+
+ $result = $current->menu->handleInventoryTransaction($player, $action->getSourceItem(), $action->getTargetItem(), $action, $transaction);
+ $network_stack_callback = $result->getPostTransactionCallback();
+ if($network_stack_callback !== null){
+ $network_stack_callbacks[] = $network_stack_callback;
+ }
+ if($result->isCancelled()){
+ $event->cancel();
+ break;
+ }
+ }
+
+ if(count($network_stack_callbacks) > 0){
+ $player_instance->getNetwork()->wait(PlayerNetwork::DELAY_TYPE_ANIMATION_WAIT, static function(bool $success) use($player, $network_stack_callbacks) : bool{
+ if($success){
+ foreach($network_stack_callbacks as $callback){
+ $callback($player);
+ }
+ }
+ return false;
+ });
+ }
+ }
+}
diff --git a/src/muqsit/invmenu/InvMenuHandler.php b/src/muqsit/invmenu/InvMenuHandler.php
new file mode 100644
index 0000000..9c5cc9a
--- /dev/null
+++ b/src/muqsit/invmenu/InvMenuHandler.php
@@ -0,0 +1,46 @@
+getName()} attempted to register " . self::class . " twice.");
+ }
+
+ self::$registrant = $plugin;
+ self::$type_registry = new InvMenuTypeRegistry();
+ self::$player_manager = new PlayerManager(self::getRegistrant());
+ Server::getInstance()->getPluginManager()->registerEvents(new InvMenuEventHandler(self::getPlayerManager()), $plugin);
+ }
+
+ public static function isRegistered() : bool{
+ return self::$registrant instanceof Plugin;
+ }
+
+ public static function getRegistrant() : Plugin{
+ return self::$registrant ?? throw new LogicException("Cannot obtain registrant before registration");
+ }
+
+ public static function getTypeRegistry() : InvMenuTypeRegistry{
+ return self::$type_registry;
+ }
+
+ public static function getPlayerManager() : PlayerManager{
+ return self::$player_manager;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/inventory/InvMenuInventory.php b/src/muqsit/invmenu/inventory/InvMenuInventory.php
new file mode 100644
index 0000000..d13b1fa
--- /dev/null
+++ b/src/muqsit/invmenu/inventory/InvMenuInventory.php
@@ -0,0 +1,23 @@
+holder = new Position(0, 0, 0, null);
+ }
+
+ public function getHolder() : Position{
+ return $this->holder;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/inventory/SharedInvMenuSynchronizer.php b/src/muqsit/invmenu/inventory/SharedInvMenuSynchronizer.php
new file mode 100644
index 0000000..3f7aaf7
--- /dev/null
+++ b/src/muqsit/invmenu/inventory/SharedInvMenuSynchronizer.php
@@ -0,0 +1,32 @@
+inventory = $inventory;
+
+ $menu_inventory = $menu->getInventory();
+ $this->synchronizer = new SharedInventorySynchronizer($menu_inventory);
+ $inventory->getListeners()->add($this->synchronizer);
+
+ $this->notifier = new SharedInventoryNotifier($this->inventory, $this->synchronizer);
+ $menu_inventory->setContents($inventory->getContents());
+ $menu_inventory->getListeners()->add($this->notifier);
+ }
+
+ public function destroy() : void{
+ $this->synchronizer->getSynchronizingInventory()->getListeners()->remove($this->notifier);
+ $this->inventory->getListeners()->remove($this->synchronizer);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/inventory/SharedInventoryNotifier.php b/src/muqsit/invmenu/inventory/SharedInventoryNotifier.php
new file mode 100644
index 0000000..dab01fe
--- /dev/null
+++ b/src/muqsit/invmenu/inventory/SharedInventoryNotifier.php
@@ -0,0 +1,31 @@
+inventory->getListeners()->remove($this->synchronizer);
+ $this->inventory->setContents($inventory->getContents());
+ $this->inventory->getListeners()->add($this->synchronizer);
+ }
+
+ public function onSlotChange(Inventory $inventory, int $slot, Item $old_item) : void{
+ if($slot < $inventory->getSize()){
+ $this->inventory->getListeners()->remove($this->synchronizer);
+ $this->inventory->setItem($slot, $inventory->getItem($slot));
+ $this->inventory->getListeners()->add($this->synchronizer);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/inventory/SharedInventorySynchronizer.php b/src/muqsit/invmenu/inventory/SharedInventorySynchronizer.php
new file mode 100644
index 0000000..1b6ba6d
--- /dev/null
+++ b/src/muqsit/invmenu/inventory/SharedInventorySynchronizer.php
@@ -0,0 +1,28 @@
+inventory;
+ }
+
+ public function onContentChange(Inventory $inventory, array $old_contents) : void{
+ $this->inventory->setContents($inventory->getContents());
+ }
+
+ public function onSlotChange(Inventory $inventory, int $slot, Item $old_item) : void{
+ $this->inventory->setItem($slot, $inventory->getItem($slot));
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/session/InvMenuInfo.php b/src/muqsit/invmenu/session/InvMenuInfo.php
new file mode 100644
index 0000000..e5ff2c0
--- /dev/null
+++ b/src/muqsit/invmenu/session/InvMenuInfo.php
@@ -0,0 +1,17 @@
+network_handler_registry = new PlayerNetworkHandlerRegistry();
+
+ $plugin_manager = Server::getInstance()->getPluginManager();
+ $plugin_manager->registerEvent(PlayerLoginEvent::class, function(PlayerLoginEvent $event) : void{
+ $this->create($event->getPlayer());
+ }, EventPriority::MONITOR, $registrant);
+ $plugin_manager->registerEvent(PlayerQuitEvent::class, function(PlayerQuitEvent $event) : void{
+ $this->destroy($event->getPlayer());
+ }, EventPriority::MONITOR, $registrant);
+ }
+
+ private function create(Player $player) : void{
+ $this->sessions[$player->getId()] = new PlayerSession($player, new PlayerNetwork(
+ $player->getNetworkSession(),
+ $this->network_handler_registry->get($player->getPlayerInfo()->getExtraData()["DeviceOS"] ?? -1)
+ ));
+ }
+
+ private function destroy(Player $player) : void{
+ if(isset($this->sessions[$player_id = $player->getId()])){
+ $this->sessions[$player_id]->finalize();
+ unset($this->sessions[$player_id]);
+ }
+ }
+
+ public function get(Player $player) : PlayerSession{
+ return $this->sessions[$player->getId()];
+ }
+
+ public function getNullable(Player $player) : ?PlayerSession{
+ return $this->sessions[$player->getId()] ?? null;
+ }
+
+ public function getNetworkHandlerRegistry() : PlayerNetworkHandlerRegistry{
+ return $this->network_handler_registry;
+ }
+}
diff --git a/src/muqsit/invmenu/session/PlayerSession.php b/src/muqsit/invmenu/session/PlayerSession.php
new file mode 100644
index 0000000..73440d7
--- /dev/null
+++ b/src/muqsit/invmenu/session/PlayerSession.php
@@ -0,0 +1,98 @@
+current !== null){
+ $this->current->graphic->remove($this->player);
+ $this->player->removeCurrentWindow();
+ }
+ $this->network->finalize();
+ }
+
+ public function getCurrent() : ?InvMenuInfo{
+ return $this->current;
+ }
+
+ /**
+ * @internal use InvMenu::send() instead.
+ *
+ * @param InvMenuInfo|null $current
+ * @param (Closure(bool) : void)|null $callback
+ */
+ public function setCurrentMenu(?InvMenuInfo $current, ?Closure $callback = null) : void{
+ if($this->current !== null){
+ $this->current->graphic->remove($this->player);
+ }
+
+ $this->current = $current;
+
+ if($this->current !== null){
+ $current_id = spl_object_id($this->current);
+ $this->current->graphic->send($this->player, $this->current->graphic_name);
+ $this->network->waitUntil(PlayerNetwork::DELAY_TYPE_OPERATION, $this->current->graphic->getAnimationDuration(), function(bool $success) use($callback, $current_id) : bool{
+ $current = $this->current;
+ if($current !== null && spl_object_id($current) === $current_id){
+ if($success){
+ $this->network->onBeforeSendMenu($this, $current);
+ $result = $current->graphic->sendInventory($this->player, $current->menu->getInventory());
+ if($result){
+ if($callback !== null){
+ $callback(true);
+ }
+ return false;
+ }
+ }
+
+ $this->removeCurrentMenu();
+ }
+ if($callback !== null){
+ $callback(false);
+ }
+ return false;
+ });
+ }else{
+ $this->network->wait(PlayerNetwork::DELAY_TYPE_ANIMATION_WAIT, static function(bool $success) use($callback) : bool{
+ if($callback !== null){
+ $callback($success);
+ }
+ return false;
+ });
+ }
+ }
+
+ public function getNetwork() : PlayerNetwork{
+ return $this->network;
+ }
+
+ /**
+ * @internal use Player::removeCurrentWindow() instead
+ * @return bool
+ */
+ public function removeCurrentMenu() : bool{
+ if($this->current !== null){
+ $this->setCurrentMenu(null);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/muqsit/invmenu/session/network/NetworkStackLatencyEntry.php b/src/muqsit/invmenu/session/network/NetworkStackLatencyEntry.php
new file mode 100644
index 0000000..4405f58
--- /dev/null
+++ b/src/muqsit/invmenu/session/network/NetworkStackLatencyEntry.php
@@ -0,0 +1,21 @@
+timestamp = $timestamp;
+ $this->then = $then;
+ $this->network_timestamp = $network_timestamp ?? $timestamp;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/session/network/PlayerNetwork.php b/src/muqsit/invmenu/session/network/PlayerNetwork.php
new file mode 100644
index 0000000..40726ae
--- /dev/null
+++ b/src/muqsit/invmenu/session/network/PlayerNetwork.php
@@ -0,0 +1,229 @@
+|null) */
+ private Closure $container_open_callback;
+
+ private ?NetworkStackLatencyEntry $current = null;
+ private int $graphic_wait_duration = 200;
+
+ /** @var SplQueue */
+ private SplQueue $queue;
+
+ /** @var array */
+ private array $entry_types = [];
+
+ public function __construct(
+ private NetworkSession $network_session,
+ private PlayerNetworkHandler $handler
+ ){
+ $this->queue = new SplQueue();
+ $this->nullifyContainerOpenCallback();
+ }
+
+ public function finalize() : void{
+ $this->dropPending();
+ $this->network_session->getInvManager()?->getContainerOpenCallbacks()->remove($this->container_open_callback);
+ $this->nullifyContainerOpenCallback();
+ }
+
+ public function getGraphicWaitDuration() : int{
+ return $this->graphic_wait_duration;
+ }
+
+ /**
+ * Duration (in milliseconds) to wait between sending the graphic (block)
+ * and sending the inventory.
+ *
+ * @param int $graphic_wait_duration
+ */
+ public function setGraphicWaitDuration(int $graphic_wait_duration) : void{
+ if($graphic_wait_duration < 0){
+ throw new InvalidArgumentException("graphic_wait_duration must be >= 0, got {$graphic_wait_duration}");
+ }
+
+ $this->graphic_wait_duration = $graphic_wait_duration;
+ }
+
+ public function getPending() : int{
+ return $this->queue->count();
+ }
+
+ public function dropPending() : void{
+ foreach($this->queue as $entry){
+ ($entry->then)(false);
+ }
+ $this->queue = new SplQueue();
+ $this->entry_types = [];
+ $this->setCurrent(null);
+ }
+
+ /**
+ * @param self::DELAY_TYPE_* $type
+ */
+ public function dropPendingOfType(int $type) : void{
+ $previous = $this->queue;
+ $this->queue = new SplQueue();
+ foreach($previous as $entry){
+ if($this->entry_types[$id = spl_object_id($entry)] === $type){
+ ($entry->then)(false);
+ unset($this->entry_types[$id]);
+ }else{
+ $this->queue->enqueue($entry);
+ }
+ }
+ }
+
+ /**
+ * @param self::DELAY_TYPE_* $type
+ * @param Closure(bool) : bool $then
+ */
+ public function wait(int $type, Closure $then) : void{
+ $entry = $this->handler->createNetworkStackLatencyEntry($then);
+ if($this->current !== null){
+ $this->queue->enqueue($entry);
+ $this->entry_types[spl_object_id($entry)] = $type;
+ }else{
+ $this->setCurrent($entry);
+ }
+ }
+
+ /**
+ * Waits at least $wait_ms before calling $then(true).
+ *
+ * @param self::DELAY_TYPE_* $type
+ * @param int $wait_ms
+ * @param Closure(bool) : bool $then
+ */
+ public function waitUntil(int $type, int $wait_ms, Closure $then) : void{
+ if($wait_ms <= 0 && $this->queue->isEmpty()){
+ $then(true);
+ return;
+ }
+
+ $elapsed_ms = 0.0;
+ $this->wait($type, function(bool $success) use($wait_ms, $then, &$elapsed_ms) : bool{
+ if($this->current === null){
+ $then(false);
+ return false;
+ }
+
+ $elapsed_ms += (microtime(true) * 1000) - $this->current->sent_at;
+ if(!$success || $elapsed_ms >= $wait_ms){
+ $then($success);
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ private function setCurrent(?NetworkStackLatencyEntry $entry) : void{
+ if($this->current !== null){
+ $this->processCurrent(false);
+ }
+
+ $this->current = $entry;
+ if($entry !== null){
+ unset($this->entry_types[spl_object_id($entry)]);
+ if($this->network_session->sendDataPacket(NetworkStackLatencyPacket::create($entry->network_timestamp, true))){
+ $entry->sent_at = microtime(true) * 1000;
+ }else{
+ $this->processCurrent(false);
+ }
+ }
+ }
+
+ private function processCurrent(bool $success) : void{
+ if($this->current !== null){
+ $current = $this->current;
+ $repeat = ($current->then)($success);
+ $this->current = null;
+ if($repeat && $success){
+ $this->setCurrent($current);
+ }elseif(!$this->queue->isEmpty()){
+ $this->setCurrent($this->queue->dequeue());
+ }
+ }
+ }
+
+ public function notify(int $timestamp) : void{
+ if($this->current !== null && $timestamp === $this->current->timestamp){
+ $this->processCurrent(true);
+ }
+ }
+
+ public function onBeforeSendMenu(PlayerSession $session, InvMenuInfo $info) : void{
+ $translator = $info->graphic->getNetworkTranslator();
+ if($translator === null){
+ return;
+ }
+
+ $callbacks = $this->network_session->getInvManager()?->getContainerOpenCallbacks();
+ if($callbacks === null){
+ return;
+ }
+
+ $callbacks->remove($this->container_open_callback);
+
+ // Take priority over other container open callbacks.
+ // PocketMine's default container open callback disallows any BlockInventory
+ // from having a custom callback
+ $previous = $callbacks->toArray();
+ $callbacks->clear();
+ $callbacks->add($this->container_open_callback = function(int $window_id, Inventory $inventory) use($info, $session, $translator, $previous, $callbacks) : ?array{
+ $callbacks->remove($this->container_open_callback);
+ $this->nullifyContainerOpenCallback();
+ if($inventory === $info->menu->getInventory()){
+ $packets = null;
+ foreach($previous as $callback){
+ $packets = $callback($window_id, $inventory);
+ if($packets !== null){
+ break;
+ }
+ }
+
+ $packets ??= [ContainerOpenPacket::blockInv(
+ $window_id,
+ WindowTypes::CONTAINER,
+ $inventory instanceof BlockInventory ? BlockPosition::fromVector3($inventory->getHolder()) : new BlockPosition(0, 0, 0)
+ )];
+
+ foreach($packets as $packet){
+ if($packet instanceof ContainerOpenPacket){
+ $translator->translate($session, $info, $packet);
+ }
+ }
+ return $packets;
+ }
+ return null;
+ }, ...$previous);
+ }
+
+ private function nullifyContainerOpenCallback() : void{
+ $this->container_open_callback = static fn(int $window_id, Inventory $inventory) : ?array => null;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php b/src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php
new file mode 100644
index 0000000..0e5e0c1
--- /dev/null
+++ b/src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php
@@ -0,0 +1,22 @@
+creator)($then);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/session/network/handler/PlayerNetworkHandler.php b/src/muqsit/invmenu/session/network/handler/PlayerNetworkHandler.php
new file mode 100644
index 0000000..fc38d87
--- /dev/null
+++ b/src/muqsit/invmenu/session/network/handler/PlayerNetworkHandler.php
@@ -0,0 +1,13 @@
+registerDefault(new ClosurePlayerNetworkHandler(static function(Closure $then) : NetworkStackLatencyEntry{
+ return new NetworkStackLatencyEntry(mt_rand() * 1000 /* TODO: remove this hack */, $then);
+ }));
+ $this->register(DeviceOS::PLAYSTATION, new ClosurePlayerNetworkHandler(static function(Closure $then) : NetworkStackLatencyEntry{
+ $timestamp = mt_rand();
+ return new NetworkStackLatencyEntry($timestamp, $then, $timestamp * 1000);
+ }));
+ }
+
+ public function registerDefault(PlayerNetworkHandler $handler) : void{
+ $this->default = $handler;
+ }
+
+ public function register(int $os_id, PlayerNetworkHandler $handler) : void{
+ $this->game_os_handlers[$os_id] = $handler;
+ }
+
+ public function get(int $os_id) : PlayerNetworkHandler{
+ return $this->game_os_handlers[$os_id] ?? $this->default;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php b/src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php
new file mode 100644
index 0000000..91c3a5e
--- /dev/null
+++ b/src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php
@@ -0,0 +1,60 @@
+result->then($callback);
+ }
+
+ public function getPlayer() : Player{
+ return $this->inner->getPlayer();
+ }
+
+ public function getOut() : Item{
+ return $this->inner->getOut();
+ }
+
+ public function getIn() : Item{
+ return $this->inner->getIn();
+ }
+
+ public function getItemClicked() : Item{
+ return $this->inner->getItemClicked();
+ }
+
+ public function getItemClickedWith() : Item{
+ return $this->inner->getItemClickedWith();
+ }
+
+ public function getAction() : SlotChangeAction{
+ return $this->inner->getAction();
+ }
+
+ public function getTransaction() : InventoryTransaction{
+ return $this->inner->getTransaction();
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/transaction/InvMenuTransaction.php b/src/muqsit/invmenu/transaction/InvMenuTransaction.php
new file mode 100644
index 0000000..7db7694
--- /dev/null
+++ b/src/muqsit/invmenu/transaction/InvMenuTransaction.php
@@ -0,0 +1,43 @@
+cancelled;
+ }
+
+ /**
+ * Notify when we have escaped from the event stack trace and the
+ * client's network stack trace.
+ * Useful for sending forms and other stuff that cant be sent right
+ * after closing inventory.
+ *
+ * @param (Closure(\pocketmine\player\Player) : void)|null $callback
+ * @return self
+ */
+ public function then(?Closure $callback) : self{
+ $this->post_transaction_callback = $callback;
+ return $this;
+ }
+
+ public function getPostTransactionCallback() : ?Closure{
+ return $this->post_transaction_callback;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/transaction/SimpleInvMenuTransaction.php b/src/muqsit/invmenu/transaction/SimpleInvMenuTransaction.php
new file mode 100644
index 0000000..97b8d4c
--- /dev/null
+++ b/src/muqsit/invmenu/transaction/SimpleInvMenuTransaction.php
@@ -0,0 +1,57 @@
+player;
+ }
+
+ public function getOut() : Item{
+ return $this->out;
+ }
+
+ public function getIn() : Item{
+ return $this->in;
+ }
+
+ public function getItemClicked() : Item{
+ return $this->getOut();
+ }
+
+ public function getItemClickedWith() : Item{
+ return $this->getIn();
+ }
+
+ public function getAction() : SlotChangeAction{
+ return $this->action;
+ }
+
+ public function getTransaction() : InventoryTransaction{
+ return $this->transaction;
+ }
+
+ public function continue() : InvMenuTransactionResult{
+ return new InvMenuTransactionResult(false);
+ }
+
+ public function discard() : InvMenuTransactionResult{
+ return new InvMenuTransactionResult(true);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/ActorFixedInvMenuType.php b/src/muqsit/invmenu/type/ActorFixedInvMenuType.php
new file mode 100644
index 0000000..ec3212b
--- /dev/null
+++ b/src/muqsit/invmenu/type/ActorFixedInvMenuType.php
@@ -0,0 +1,44 @@
+ $actor_metadata
+ * @param int $size
+ * @param InvMenuGraphicNetworkTranslator|null $network_translator
+ */
+ public function __construct(
+ private string $actor_identifier,
+ private int $actor_runtime_identifier,
+ private array $actor_metadata,
+ private int $size,
+ private ?InvMenuGraphicNetworkTranslator $network_translator = null
+ ){}
+
+ public function getSize() : int{
+ return $this->size;
+ }
+
+ public function createGraphic(InvMenu $menu, Player $player) : ?InvMenuGraphic{
+ return new ActorInvMenuGraphic($this->actor_identifier, $this->actor_runtime_identifier, $this->actor_metadata, $this->network_translator);
+ }
+
+ public function createInventory() : Inventory{
+ return new InvMenuInventory($this->size);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/BlockActorFixedInvMenuType.php b/src/muqsit/invmenu/type/BlockActorFixedInvMenuType.php
new file mode 100644
index 0000000..388e054
--- /dev/null
+++ b/src/muqsit/invmenu/type/BlockActorFixedInvMenuType.php
@@ -0,0 +1,54 @@
+size;
+ }
+
+ public function createGraphic(InvMenu $menu, Player $player) : ?InvMenuGraphic{
+ $position = $player->getPosition();
+ $origin = $position->addVector(InvMenuTypeHelper::getBehindPositionOffset($player))->floor();
+ if(!InvMenuTypeHelper::isValidYCoordinate($origin->y)){
+ return null;
+ }
+
+ $graphics = [new BlockActorInvMenuGraphic($this->block, $origin, BlockActorInvMenuGraphic::createTile($this->tile_id, $menu->getName()), $this->network_translator, $this->animation_duration)];
+ foreach(InvMenuTypeHelper::findConnectedBlocks("Chest", $position->getWorld(), $origin, Facing::HORIZONTAL) as $side){
+ $graphics[] = new BlockInvMenuGraphic(VanillaBlocks::BARRIER(), $side);
+ }
+
+ return count($graphics) > 1 ? new MultiBlockInvMenuGraphic($graphics) : $graphics[0];
+ }
+
+ public function createInventory() : Inventory{
+ return new InvMenuInventory($this->size);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/BlockFixedInvMenuType.php b/src/muqsit/invmenu/type/BlockFixedInvMenuType.php
new file mode 100644
index 0000000..1c19159
--- /dev/null
+++ b/src/muqsit/invmenu/type/BlockFixedInvMenuType.php
@@ -0,0 +1,41 @@
+size;
+ }
+
+ public function createGraphic(InvMenu $menu, Player $player) : ?InvMenuGraphic{
+ $origin = $player->getPosition()->addVector(InvMenuTypeHelper::getBehindPositionOffset($player))->floor();
+ if(!InvMenuTypeHelper::isValidYCoordinate($origin->y)){
+ return null;
+ }
+
+ return new BlockInvMenuGraphic($this->block, $origin, $this->network_translator);
+ }
+
+ public function createInventory() : Inventory{
+ return new InvMenuInventory($this->size);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/DoublePairableBlockActorFixedInvMenuType.php b/src/muqsit/invmenu/type/DoublePairableBlockActorFixedInvMenuType.php
new file mode 100644
index 0000000..8b36f70
--- /dev/null
+++ b/src/muqsit/invmenu/type/DoublePairableBlockActorFixedInvMenuType.php
@@ -0,0 +1,70 @@
+size;
+ }
+
+ public function createGraphic(InvMenu $menu, Player $player) : ?InvMenuGraphic{
+ $position = $player->getPosition();
+ $origin = $position->addVector(InvMenuTypeHelper::getBehindPositionOffset($player))->floor();
+ if(!InvMenuTypeHelper::isValidYCoordinate($origin->y)){
+ return null;
+ }
+
+ $graphics = [];
+ $menu_name = $menu->getName();
+ $world = $position->getWorld();
+ foreach([
+ [$origin, $origin->east(), [Facing::NORTH, Facing::SOUTH, Facing::WEST]],
+ [$origin->east(), $origin, [Facing::NORTH, Facing::SOUTH, Facing::EAST]]
+ ] as [$origin_pos, $pair_pos, $connected_sides]){
+ $graphics[] = new BlockActorInvMenuGraphic(
+ $this->block,
+ $origin_pos,
+ BlockActorInvMenuGraphic::createTile($this->tile_id, $menu_name)
+ ->setInt(Chest::TAG_PAIRX, $pair_pos->x)
+ ->setInt(Chest::TAG_PAIRZ, $pair_pos->z),
+ $this->network_translator,
+ $this->animation_duration
+ );
+ foreach(InvMenuTypeHelper::findConnectedBlocks("Chest", $world, $origin_pos, $connected_sides) as $side){
+ $graphics[] = new BlockInvMenuGraphic(VanillaBlocks::BARRIER(), $side);
+ }
+ }
+
+ return count($graphics) > 1 ? new MultiBlockInvMenuGraphic($graphics) : $graphics[0];
+ }
+
+ public function createInventory() : Inventory{
+ return new InvMenuInventory($this->size);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/FixedInvMenuType.php b/src/muqsit/invmenu/type/FixedInvMenuType.php
new file mode 100644
index 0000000..7f6a5cc
--- /dev/null
+++ b/src/muqsit/invmenu/type/FixedInvMenuType.php
@@ -0,0 +1,18 @@
+ */
+ private array $types = [];
+
+ /** @var array */
+ private array $identifiers = [];
+
+ public function __construct(){
+ $this->register(InvMenuTypeIds::TYPE_CHEST, InvMenuTypeBuilders::BLOCK_ACTOR_FIXED()
+ ->setBlock(VanillaBlocks::CHEST())
+ ->setSize(27)
+ ->setBlockActorId("Chest")
+ ->build());
+
+ $this->register(InvMenuTypeIds::TYPE_DOUBLE_CHEST, InvMenuTypeBuilders::DOUBLE_PAIRABLE_BLOCK_ACTOR_FIXED()
+ ->setBlock(VanillaBlocks::CHEST())
+ ->setSize(54)
+ ->setBlockActorId("Chest")
+ ->setAnimationDuration(75)
+ ->build());
+
+ $this->register(InvMenuTypeIds::TYPE_HOPPER, InvMenuTypeBuilders::BLOCK_ACTOR_FIXED()
+ ->setBlock(VanillaBlocks::HOPPER())
+ ->setSize(5)
+ ->setBlockActorId("Hopper")
+ ->setNetworkWindowType(WindowTypes::HOPPER)
+ ->build());
+ }
+
+ public function register(string $identifier, InvMenuType $type) : void{
+ if(isset($this->types[$identifier])){
+ unset($this->identifiers[spl_object_id($this->types[$identifier])], $this->types[$identifier]);
+ }
+
+ $this->types[$identifier] = $type;
+ $this->identifiers[spl_object_id($type)] = $identifier;
+ }
+
+ public function exists(string $identifier) : bool{
+ return isset($this->types[$identifier]);
+ }
+
+ public function get(string $identifier) : InvMenuType{
+ return $this->types[$identifier];
+ }
+
+ public function getIdentifier(InvMenuType $type) : string{
+ return $this->identifiers[spl_object_id($type)];
+ }
+
+ public function getOrNull(string $identifier) : ?InvMenuType{
+ return $this->types[$identifier] ?? null;
+ }
+
+ /**
+ * @return array
+ */
+ public function getAll() : array{
+ return $this->types;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/graphic/ActorInvMenuGraphic.php b/src/muqsit/invmenu/type/graphic/ActorInvMenuGraphic.php
new file mode 100644
index 0000000..4af2950
--- /dev/null
+++ b/src/muqsit/invmenu/type/graphic/ActorInvMenuGraphic.php
@@ -0,0 +1,71 @@
+ $actor_metadata
+ * @param InvMenuGraphicNetworkTranslator|null $network_translator
+ * @param int $animation_duration
+ */
+ public function __construct(
+ private string $actor_identifier,
+ private int $actor_runtime_identifier,
+ private array $actor_metadata,
+ private ?InvMenuGraphicNetworkTranslator $network_translator = null,
+ private int $animation_duration = 0
+ ){}
+
+ public function send(Player $player, ?string $name) : void{
+ $metadata = $this->actor_metadata;
+ if($name !== null){
+ $metadata[EntityMetadataProperties::NAMETAG] = new StringMetadataProperty($name);
+ }
+ $player->getNetworkSession()->sendDataPacket(AddActorPacket::create(
+ $this->actor_runtime_identifier,
+ $this->actor_runtime_identifier,
+ $this->actor_identifier,
+ $player->getPosition()->asVector3(),
+ null,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ [],
+ $metadata,
+ new PropertySyncData([], []),
+ []
+ ));
+ }
+
+ public function sendInventory(Player $player, Inventory $inventory) : bool{
+ return $player->setCurrentWindow($inventory);
+ }
+
+ public function remove(Player $player) : void{
+ $player->getNetworkSession()->sendDataPacket(RemoveActorPacket::create($this->actor_runtime_identifier));
+ }
+
+ public function getNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{
+ return $this->network_translator;
+ }
+
+ public function getAnimationDuration() : int{
+ return $this->animation_duration;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/graphic/BlockActorInvMenuGraphic.php b/src/muqsit/invmenu/type/graphic/BlockActorInvMenuGraphic.php
new file mode 100644
index 0000000..0619065
--- /dev/null
+++ b/src/muqsit/invmenu/type/graphic/BlockActorInvMenuGraphic.php
@@ -0,0 +1,70 @@
+setString(Tile::TAG_ID, $tile_id);
+ if($name !== null){
+ $tag->setString(Nameable::TAG_CUSTOM_NAME, $name);
+ }
+ return $tag;
+ }
+
+ private BlockInvMenuGraphic $block_graphic;
+ private Vector3 $position;
+ private CompoundTag $tile;
+ private ?InvMenuGraphicNetworkTranslator $network_translator;
+ private int $animation_duration;
+
+ public function __construct(Block $block, Vector3 $position, CompoundTag $tile, ?InvMenuGraphicNetworkTranslator $network_translator = null, int $animation_duration = 0){
+ $this->block_graphic = new BlockInvMenuGraphic($block, $position);
+ $this->position = $position;
+ $this->tile = $tile;
+ $this->network_translator = $network_translator;
+ $this->animation_duration = $animation_duration;
+ }
+
+ public function getPosition() : Vector3{
+ return $this->position;
+ }
+
+ public function send(Player $player, ?string $name) : void{
+ $this->block_graphic->send($player, $name);
+ if($name !== null){
+ $this->tile->setString(Nameable::TAG_CUSTOM_NAME, $name);
+ }
+ $player->getNetworkSession()->sendDataPacket(BlockActorDataPacket::create(BlockPosition::fromVector3($this->position), new CacheableNbt($this->tile)));
+ }
+
+ public function sendInventory(Player $player, Inventory $inventory) : bool{
+ return $player->setCurrentWindow($inventory);
+ }
+
+ public function remove(Player $player) : void{
+ $this->block_graphic->remove($player);
+ }
+
+ public function getNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{
+ return $this->network_translator;
+ }
+
+ public function getAnimationDuration() : int{
+ return $this->animation_duration;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/graphic/BlockInvMenuGraphic.php b/src/muqsit/invmenu/type/graphic/BlockInvMenuGraphic.php
new file mode 100644
index 0000000..2e35029
--- /dev/null
+++ b/src/muqsit/invmenu/type/graphic/BlockInvMenuGraphic.php
@@ -0,0 +1,59 @@
+position;
+ }
+
+ public function send(Player $player, ?string $name) : void{
+ $player->getNetworkSession()->sendDataPacket(UpdateBlockPacket::create(BlockPosition::fromVector3($this->position), RuntimeBlockMapping::getInstance()->toRuntimeId($this->block->getFullId()), UpdateBlockPacket::FLAG_NETWORK, UpdateBlockPacket::DATA_LAYER_NORMAL));
+ }
+
+ public function sendInventory(Player $player, Inventory $inventory) : bool{
+ return $player->setCurrentWindow($inventory);
+ }
+
+ public function remove(Player $player) : void{
+ $network = $player->getNetworkSession();
+ $world = $player->getWorld();
+ $runtime_block_mapping = RuntimeBlockMapping::getInstance();
+ $block = $world->getBlockAt($this->position->x, $this->position->y, $this->position->z);
+ $network->sendDataPacket(UpdateBlockPacket::create(BlockPosition::fromVector3($this->position), $runtime_block_mapping->toRuntimeId($block->getFullId()), UpdateBlockPacket::FLAG_NETWORK, UpdateBlockPacket::DATA_LAYER_NORMAL), true);
+
+ $tile = $world->getTileAt($this->position->x, $this->position->y, $this->position->z);
+ if($tile instanceof Spawnable){
+ $network->sendDataPacket(BlockActorDataPacket::create(BlockPosition::fromVector3($this->position), $tile->getSerializedSpawnCompound()), true);
+ }
+ }
+
+ public function getNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{
+ return $this->network_translator;
+ }
+
+ public function getAnimationDuration() : int{
+ return $this->animation_duration;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/graphic/InvMenuGraphic.php b/src/muqsit/invmenu/type/graphic/InvMenuGraphic.php
new file mode 100644
index 0000000..8c263d7
--- /dev/null
+++ b/src/muqsit/invmenu/type/graphic/InvMenuGraphic.php
@@ -0,0 +1,28 @@
+graphics);
+ if($first === false){
+ throw new LogicException("Tried sending inventory from a multi graphic consisting of zero entries");
+ }
+
+ return $first;
+ }
+
+ public function send(Player $player, ?string $name) : void{
+ foreach($this->graphics as $graphic){
+ $graphic->send($player, $name);
+ }
+ }
+
+ public function sendInventory(Player $player, Inventory $inventory) : bool{
+ return $this->first()->sendInventory($player, $inventory);
+ }
+
+ public function remove(Player $player) : void{
+ foreach($this->graphics as $graphic){
+ $graphic->remove($player);
+ }
+ }
+
+ public function getNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{
+ return $this->first()->getNetworkTranslator();
+ }
+
+ public function getPosition() : Vector3{
+ return $this->first()->getPosition();
+ }
+
+ public function getAnimationDuration() : int{
+ $max = 0;
+ foreach($this->graphics as $graphic){
+ $duration = $graphic->getAnimationDuration();
+ if($duration > $max){
+ $max = $duration;
+ }
+ }
+ return $max;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/graphic/PositionedInvMenuGraphic.php b/src/muqsit/invmenu/type/graphic/PositionedInvMenuGraphic.php
new file mode 100644
index 0000000..8b3c83d
--- /dev/null
+++ b/src/muqsit/invmenu/type/graphic/PositionedInvMenuGraphic.php
@@ -0,0 +1,12 @@
+actorUniqueId = $this->actor_runtime_id;
+ $packet->blockPosition = new BlockPosition(0, 0, 0);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php b/src/muqsit/invmenu/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php
new file mode 100644
index 0000000..3406800
--- /dev/null
+++ b/src/muqsit/invmenu/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php
@@ -0,0 +1,33 @@
+graphic;
+ if(!($graphic instanceof PositionedInvMenuGraphic)){
+ throw new InvalidArgumentException("Expected " . PositionedInvMenuGraphic::class . ", got " . get_class($graphic));
+ }
+
+ $pos = $graphic->getPosition();
+ $packet->blockPosition = new BlockPosition((int) $pos->x, (int) $pos->y, (int) $pos->z);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/graphic/network/InvMenuGraphicNetworkTranslator.php b/src/muqsit/invmenu/type/graphic/network/InvMenuGraphicNetworkTranslator.php
new file mode 100644
index 0000000..5ead44f
--- /dev/null
+++ b/src/muqsit/invmenu/type/graphic/network/InvMenuGraphicNetworkTranslator.php
@@ -0,0 +1,14 @@
+translators as $translator){
+ $translator->translate($session, $current, $packet);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php b/src/muqsit/invmenu/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php
new file mode 100644
index 0000000..81e7750
--- /dev/null
+++ b/src/muqsit/invmenu/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php
@@ -0,0 +1,20 @@
+windowType = $this->window_type;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/InvMenuTypeBuilders.php b/src/muqsit/invmenu/type/util/InvMenuTypeBuilders.php
new file mode 100644
index 0000000..a749a14
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/InvMenuTypeBuilders.php
@@ -0,0 +1,29 @@
+getDirectionVector();
+ $size = $player->size;
+ $offset->x *= -(1 + $size->getWidth());
+ $offset->y *= -(1 + $size->getHeight());
+ $offset->z *= -(1 + $size->getWidth());
+ return $offset;
+ }
+
+ public static function isValidYCoordinate(float $y) : bool{
+ return $y >= self::NETWORK_WORLD_Y_MIN && $y <= self::NETWORK_WORLD_Y_MAX;
+ }
+
+ /**
+ * @param string $tile_id
+ * @param World $world
+ * @param Vector3 $position
+ * @param list $sides
+ * @return Generator
+ */
+ public static function findConnectedBlocks(string $tile_id, World $world, Vector3 $position, array $sides) : Generator{
+ if($tile_id === "Chest"){
+ // setting a single chest at the spot of a pairable chest sends the client a double chest
+ // https://github.com/Muqsit/InvMenu/issues/207
+ foreach($sides as $side){
+ $pos = $position->getSide($side);
+ $tile = $world->getTileAt($pos->x, $pos->y, $pos->z);
+ if($tile instanceof Chest && $tile->getPair() !== null){
+ yield $pos;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/ActorFixedInvMenuTypeBuilder.php b/src/muqsit/invmenu/type/util/builder/ActorFixedInvMenuTypeBuilder.php
new file mode 100644
index 0000000..a43d4f5
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/ActorFixedInvMenuTypeBuilder.php
@@ -0,0 +1,38 @@
+getActorMetadata();
+ $metadata->setFloat(EntityMetadataProperties::BOUNDING_BOX_HEIGHT, 0.01);
+ $metadata->setFloat(EntityMetadataProperties::BOUNDING_BOX_WIDTH, 0.01);
+ $metadata->setGenericFlag(EntityMetadataFlags::INVISIBLE, true);
+ }
+
+ public function setNetworkWindowType(int $window_type) : self{
+ $this->parentSetNetworkWindowType($window_type);
+ $this->getActorMetadata()->setByte(EntityMetadataProperties::CONTAINER_TYPE, $window_type);
+ return $this;
+ }
+
+ public function setSize(int $size) : self{
+ $this->parentSetSize($size);
+ $this->getActorMetadata()->setInt(EntityMetadataProperties::CONTAINER_BASE_SIZE, $size);
+ return $this;
+ }
+
+ public function build() : ActorFixedInvMenuType{
+ return new ActorFixedInvMenuType($this->getActorIdentifier(), $this->getActorRuntimeIdentifier(), $this->getActorMetadata()->getAll(), $this->getSize(), $this->getGraphicNetworkTranslator());
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/ActorInvMenuTypeBuilderTrait.php b/src/muqsit/invmenu/type/util/builder/ActorInvMenuTypeBuilderTrait.php
new file mode 100644
index 0000000..1face00
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/ActorInvMenuTypeBuilderTrait.php
@@ -0,0 +1,45 @@
+actor_runtime_identifier ?? $this->setActorRuntimeIdentifier(Entity::nextRuntimeId())->getActorRuntimeIdentifier();
+ }
+
+ public function setActorRuntimeIdentifier(int $actor_runtime_identifier) : self{
+ $this->actor_runtime_identifier = $actor_runtime_identifier;
+ $this->addGraphicNetworkTranslator(new ActorInvMenuGraphicNetworkTranslator($this->actor_runtime_identifier));
+ return $this;
+ }
+
+ public function getActorMetadata() : EntityMetadataCollection{
+ return $this->actor_metadata ?? $this->setActorMetadata(new EntityMetadataCollection())->getActorMetadata();
+ }
+
+ public function setActorMetadata(EntityMetadataCollection $actor_metadata) : self{
+ $this->actor_metadata = $actor_metadata;
+ return $this;
+ }
+
+ public function getActorIdentifier() : string{
+ return $this->actor_identifier ?? $this->setActorIdentifier(EntityIds::CHEST_MINECART)->getActorIdentifier();
+ }
+
+ public function setActorIdentifier(string $actor_identifier) : self{
+ $this->actor_identifier = $actor_identifier;
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php b/src/muqsit/invmenu/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php
new file mode 100644
index 0000000..e062297
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php
@@ -0,0 +1,19 @@
+animation_duration = $animation_duration;
+ return $this;
+ }
+
+ protected function getAnimationDuration() : int{
+ return $this->animation_duration;
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php b/src/muqsit/invmenu/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php
new file mode 100644
index 0000000..e3cb3fb
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php
@@ -0,0 +1,35 @@
+addGraphicNetworkTranslator(BlockInvMenuGraphicNetworkTranslator::instance());
+ }
+
+ public function setBlockActorId(string $block_actor_id) : self{
+ $this->block_actor_id = $block_actor_id;
+ return $this;
+ }
+
+ private function getBlockActorId() : string{
+ return $this->block_actor_id ?? throw new LogicException("No block actor ID was specified");
+ }
+
+ public function build() : BlockActorFixedInvMenuType{
+ return new BlockActorFixedInvMenuType($this->getBlock(), $this->getSize(), $this->getBlockActorId(), $this->getGraphicNetworkTranslator(), $this->getAnimationDuration());
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/BlockFixedInvMenuTypeBuilder.php b/src/muqsit/invmenu/type/util/builder/BlockFixedInvMenuTypeBuilder.php
new file mode 100644
index 0000000..afef7a2
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/BlockFixedInvMenuTypeBuilder.php
@@ -0,0 +1,22 @@
+addGraphicNetworkTranslator(BlockInvMenuGraphicNetworkTranslator::instance());
+ }
+
+ public function build() : BlockFixedInvMenuType{
+ return new BlockFixedInvMenuType($this->getBlock(), $this->getSize(), $this->getGraphicNetworkTranslator());
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/BlockInvMenuTypeBuilderTrait.php b/src/muqsit/invmenu/type/util/builder/BlockInvMenuTypeBuilderTrait.php
new file mode 100644
index 0000000..26ad398
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/BlockInvMenuTypeBuilderTrait.php
@@ -0,0 +1,22 @@
+block = $block;
+ return $this;
+ }
+
+ protected function getBlock() : Block{
+ return $this->block ?? throw new LogicException("No block was provided");
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php b/src/muqsit/invmenu/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php
new file mode 100644
index 0000000..a66dd3a
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php
@@ -0,0 +1,35 @@
+addGraphicNetworkTranslator(BlockInvMenuGraphicNetworkTranslator::instance());
+ }
+
+ public function setBlockActorId(string $block_actor_id) : self{
+ $this->block_actor_id = $block_actor_id;
+ return $this;
+ }
+
+ private function getBlockActorId() : string{
+ return $this->block_actor_id ?? throw new LogicException("No block actor ID was specified");
+ }
+
+ public function build() : DoublePairableBlockActorFixedInvMenuType{
+ return new DoublePairableBlockActorFixedInvMenuType($this->getBlock(), $this->getSize(), $this->getBlockActorId(), $this->getGraphicNetworkTranslator(), $this->getAnimationDuration());
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/FixedInvMenuTypeBuilderTrait.php b/src/muqsit/invmenu/type/util/builder/FixedInvMenuTypeBuilderTrait.php
new file mode 100644
index 0000000..a46a26a
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/FixedInvMenuTypeBuilderTrait.php
@@ -0,0 +1,21 @@
+size = $size;
+ return $this;
+ }
+
+ protected function getSize() : int{
+ return $this->size ?? throw new LogicException("No size was provided");
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php b/src/muqsit/invmenu/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php
new file mode 100644
index 0000000..31b0d64
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php
@@ -0,0 +1,37 @@
+graphic_network_translators[] = $translator;
+ return $this;
+ }
+
+ public function setNetworkWindowType(int $window_type) : self{
+ $this->addGraphicNetworkTranslator(new WindowTypeInvMenuGraphicNetworkTranslator($window_type));
+ return $this;
+ }
+
+ protected function getGraphicNetworkTranslator() : ?InvMenuGraphicNetworkTranslator{
+ if(count($this->graphic_network_translators) === 0){
+ return null;
+ }
+
+ if(count($this->graphic_network_translators) === 1){
+ return $this->graphic_network_translators[array_key_first($this->graphic_network_translators)];
+ }
+
+ return new MultiInvMenuGraphicNetworkTranslator($this->graphic_network_translators);
+ }
+}
\ No newline at end of file
diff --git a/src/muqsit/invmenu/type/util/builder/InvMenuTypeBuilder.php b/src/muqsit/invmenu/type/util/builder/InvMenuTypeBuilder.php
new file mode 100644
index 0000000..ae07040
--- /dev/null
+++ b/src/muqsit/invmenu/type/util/builder/InvMenuTypeBuilder.php
@@ -0,0 +1,12 @@
+
Date: Tue, 27 Jun 2023 18:24:54 +0200
Subject: [PATCH 02/11] Validated the version, removed useless files
---
ShopInventory.md | 94 ----------------------------------------
plugin-banner-white.png | Bin 82609 -> 0 bytes
2 files changed, 94 deletions(-)
delete mode 100644 ShopInventory.md
delete mode 100644 plugin-banner-white.png
diff --git a/ShopInventory.md b/ShopInventory.md
deleted file mode 100644
index 3090f3b..0000000
--- a/ShopInventory.md
+++ /dev/null
@@ -1,94 +0,0 @@
-I loved this file because it was beautiful
-
-```php
-getSize(); $a++) {
- if (!$inventory->isSlotEmpty($a)) {
- $items[$a] = json_decode(self::itemSerializator($inventory->getItem($a)));
- }
- }
- return $items;
- }
-
- public static function unserialize(string $json) : array {
- $items = [];
- foreach (json_decode($json) as $slot => $itemInfo) {
- $items[$slot] = SerializedItem::decode($itemInfo);
- }
- return $items;
- }
-
- public static function itemSerializator(Item $item) : string {
- $object = self::compoundJsonizer($item->getNamedTag());
- $iteminfo = new \stdClass;
- $iteminfo->networkitem = ItemUtils::encode($item, true);
- $iteminfo->networkitem->type = 0;
- $iteminfo->count = $item->getCount();
- $iteminfo->customname = $item->getCustomName() ?? null;
- $iteminfo->serializednbt = $object;
- return json_encode($iteminfo);
- }
-
- public static function compoundJsonizer(CompoundTag $tag, bool $toObject = false) : object|string {
- $object = new \stdClass;
- $object->__mapping = new \stdClass;
- foreach ($tag->getValue() as $name => $value) {
- //print_r($value);
- if ($value instanceof CompoundTag) {
- $t = "CompoundTag";
- $value = self::compoundJsonizer($value, true);
- } elseif ($value instanceof Tag) {
- $t = $value::class;
- $value = $value->getValue();
- }
-
- if ($value instanceof Tag) {
- // OK WTF?
- $value = $value->getValue();
- }
-
- $object->__mapping->{$name} = str_replace('pocketmine\nbt\tag\\', '', $t);
-
- $object->{$name} = $value;
- }
-
- if ($toObject) {
- return $object;
- }
-
- return json_encode($object);
- }
-
- public static function jsonCompoundier(string $json) : CompoundTag {
- $tag = new CompoundTag();
- $mapping = null;
- foreach (json_decode($json) as $key => $value) {
- if ($key == "__mapping") {
- $mapping = $value;
- continue;
- }
-
- $type = str_replace('Tag', '', 'set' . $mapping->{$key});
-
- if ($type == 'setCompound') {
- $tag->setTag($key, self::jsonCompoundier(json_encode($value)));
- continue;
- }
-
- $tag->{$type}($key, $value);
- }
- return $tag;
- }
-}
-```
\ No newline at end of file
diff --git a/plugin-banner-white.png b/plugin-banner-white.png
deleted file mode 100644
index 53e49312ac2198aecb0c2394c663732205e723fc..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 82609
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