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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 H%-;SV68$au From 31005cb463e37a05bb36eeddf1b5b6569ca2fec9 Mon Sep 17 00:00:00 2001 From: FoxWorn3365 Date: Sun, 23 Jul 2023 03:20:50 +0200 Subject: [PATCH 03/11] Work together for v1.0 What's new? - 2 Item trade - New menu interface (more user friendly) - Changed the base saving format for shop - Now if you leave blank the name in /sk create it will generate a random name correctly without crash. TODO - Disable and enable the store from the shop info - The history is correctly stored but somehow not shared when you click the book in the config **OPEN AN ISSUE FOR MORE SUGGESTIONS** --- README.md | 6 + plugin.yml | 6 + src/FoxWorn3365/Shopkeepers/Core.php | 177 ++++++++------ .../Shopkeepers/Menu/EditItemMenu.php | 79 ++++-- src/FoxWorn3365/Shopkeepers/Menu/EditMenu.php | 13 +- .../Shopkeepers/Menu/ShopInfoMenu.php | 65 ++++- .../Shopkeepers/shop/ElementContainer.php | 11 +- src/FoxWorn3365/Shopkeepers/shop/Manager.php | 2 +- src/FoxWorn3365/Shopkeepers/utils/Factory.php | 10 +- src/FoxWorn3365/Shopkeepers/utils/Utils.php | 29 ++- src/muqsit/invmenu/InvMenu.php | 186 ++++++++++++++ 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 | 64 +++++ src/muqsit/invmenu/session/PlayerSession.php | 102 ++++++++ .../network/NetworkStackLatencyEntry.php | 21 ++ .../invmenu/session/network/PlayerNetwork.php | 230 ++++++++++++++++++ .../handler/ClosurePlayerNetworkHandler.php | 22 ++ .../network/handler/PlayerNetworkHandler.php | 13 + .../handler/PlayerNetworkHandlerRegistry.php | 41 ++++ .../DeterministicInvMenuTransaction.php | 60 +++++ .../transaction/InvMenuTransaction.php | 43 ++++ .../transaction/InvMenuTransactionResult.php | 48 ++++ .../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 + 60 files changed, 2570 insertions(+), 103 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..cdd7e1b 100644 --- a/README.md +++ b/README.md @@ -200,13 +200,16 @@ Now, let's see the object: ```json { "author":"", + "enabled":true, "admin":false, "title":"", "namevisible":false, + "history":base64EncodedHistoryOfTransactions, "inventory":[], "items":[ { "buy":nbtSerializedItem, + "buy2":?nbtSerializedItem, "sell":nbtSerializedItem } ] @@ -215,9 +218,11 @@ Now, let's see the object: | Name | Type | Description | | --- | --- | --- | | author | string | The username of the shop author | +| enabled | bool | Is the shop enabled? This option can be changed by the player in the menu config | | admin | bool | If the shop is an Admin shop, so you don't have to put items in the inventory and you won't earn anything | | title | string | The real name of the shop | | namevisible | bool | Should the shop name be visible when summoned? | +| history | string | A base64 encoded string with all the transactions | | inventory | array | The inventory of the shop | | items | array | A list of all recepies (max 9) | @@ -234,6 +239,7 @@ Also [this plugin](https://github.com/FrozenArea/TradeAPI) helped me! - [x] Player shop - [x] Shopkeeper's inventory updating when players buy - [x] Shopkeeper deny a trade if the inventory is without the item +- [x] Double trade - [ ] Online shopkeeper editor - [ ] Online shopkeeper shop (yes, you will be able to make shopping from your phone on the subway!) diff --git a/plugin.yml b/plugin.yml index ec4282f..b186c88 100644 --- a/plugin.yml +++ b/plugin.yml @@ -43,6 +43,12 @@ permissions: shopkeepers.shop.namevisible: description: "Allow users to decide if the shopkepeer's name should be visible or no" default: true + shopkeepers.shop.history: + description: "Allow users to view the trade history of his Shopkeepers" + default: true + shopkeepers.shop.enableDisable: + description: "Allow users to enable and disable their own Shopkeepers from the menu" + default: true shopkeepers.shop.admin: description: "Allows users to decide if the shopkeepers should be admin or none" default: op diff --git a/src/FoxWorn3365/Shopkeepers/Core.php b/src/FoxWorn3365/Shopkeepers/Core.php index 02f27ae..80d9c1c 100644 --- a/src/FoxWorn3365/Shopkeepers/Core.php +++ b/src/FoxWorn3365/Shopkeepers/Core.php @@ -49,6 +49,7 @@ use pocketmine\network\mcpe\protocol\ActorEventPacket as EntityEventPacket; use pocketmine\network\mcpe\protocol\ItemStackRequestPacket; use pocketmine\network\mcpe\protocol\ContainerClosePacket; +use pocketmine\network\mcpe\protocol\BookEditPacket; // Custom use FoxWorn3365\Shopkeepers\Menu\InfoMenu; @@ -160,6 +161,7 @@ public function onPlayerEntityInteract(Interaction $event) : void { $manager = new Manager($cm); $this->trades->{$event->getPlayer()->getName()} = new \stdClass; $this->trades->{$event->getPlayer()->getName()}->config = $data; + $this->trades->{$event->getPlayer()->getName()}->items = []; $manager->send($event->getPlayer(), $entity); } } @@ -229,8 +231,8 @@ public function onCommand(CommandSender $sender, Command $command, $label, array $sender->sendMessage(self::NOT_PERM_MSG); } - if (empty($name = $args[1])) { - $name = $this->generateRandomString(7); + if (empty($name = @$args[1])) { + $name = $this->generateRandomString(6); } foreach ($this->config->get('banned-shop-names', []) as $banned) { @@ -348,10 +350,16 @@ public function onPacket(DataPacketReceiveEvent $event) : void { $ic->slot = false; $ic->cconsume = false; $ic->specialstack = false; + $ic->finalconsume = false; + $ic->count = 1; + $ic->added = false; + $count = 1; $cm = null; $quota = 0; $itemglobal = null; $inventoryInsideConfig = null; + $log = ""; + if ($event->getPacket() instanceof ItemStackRequestPacket) { $inventory = $event->getOrigin()->getPlayer()->getInventory(); @@ -365,7 +373,10 @@ public function onPacket(DataPacketReceiveEvent $event) : void { } $this->tradeQueue->{$event->getOrigin()->getPlayer()->getName()} = true; if (@$this->trades->{$event->getOrigin()->getPlayer()->getName()} !== null) { - $this->trades->{$event->getOrigin()->getPlayer()->getName()}->items = []; + $this->trades->{$event->getOrigin()->getPlayer()->getName()}->itemsAdd = []; + $this->trades->{$event->getOrigin()->getPlayer()->getName()}->quota = []; + // Update the log with the player + $log = date("d/m/Y - H:i:s") . " >> Player §l{$event->getOrigin()->getPlayer()->getName()} §rpurchased "; // Save the config manager $cm = new ConfigManager($this->trades->{$event->getOrigin()->getPlayer()->getName()}->config->author, $this->getDataFolder()); $cm->setSingleKey($this->trades->{$event->getOrigin()->getPlayer()->getName()}->config->shop); @@ -390,6 +401,7 @@ public function onPacket(DataPacketReceiveEvent $event) : void { } */ $item->setCount($result->getCount()); + $log .= "§l{$item->getCount()}§r {$item->getName()} for "; // Before set this we need to check and update the villager's inventory $total = $result->getCount(); if (!$cm->get()->{$cm->getSingleKey()}->admin) { @@ -431,7 +443,7 @@ public function onPacket(DataPacketReceiveEvent $event) : void { $object->inventory = $inventoryInsideConfig; $cm->set($cm->getSingleKey(), $object); } - $this->trades->{$event->getOrigin()->getPlayer()->getName()}->items[] = $item; + $this->trades->{$event->getOrigin()->getPlayer()->getName()}->itemsAdd[] = $item; } } } @@ -440,90 +452,117 @@ public function onPacket(DataPacketReceiveEvent $event) : void { } elseif ($action instanceof CraftingConsumeInputStackRequestAction) { if (!$ic->cconsume && $ic->crafting && !$ic->slot) { $ic->cconsume = true; - $this->trades->{$event->getOrigin()->getPlayer()->getName()}->quota = $action->getCount(); + $this->trades->{$event->getOrigin()->getPlayer()->getName()}->quota[] = $action->getCount(); + } elseif (!$ic->slot && $ic->cconsume && $ic->crafting && !$ic->finalconsume) { + $ic->finalconsume = true; + $ic->count = 2; + $this->trades->{$event->getOrigin()->getPlayer()->getName()}->quota[] = $action->getCount(); } } elseif ($action instanceof PlaceStackRequestAction) { if (!$ic->slot && $ic->crafting && $ic->cconsume && $cm instanceof ConfigManager) { $ic->slot = true; - $dest = $action->getDestination()->getSlotId(); - // Put cctr to slot - if (@$this->trades->{$event->getOrigin()->getPlayer()->getName()} !== null) { - $data = $this->trades->{$event->getOrigin()->getPlayer()->getName()}; - $quota = $this->trades->{$event->getOrigin()->getPlayer()->getName()}->quota; - $referredItem = clone $data->item; - if ($quota <= 0) { - // Error on qta - return; - } - if ($data->count > 1) { - $this->trades->{$event->getOrigin()->getPlayer()->getName()}->count = $this->trades->{$event->getOrigin()->getPlayer()->getName()}->count - $quota; - $data->item->setCount($data->count - $quota); - foreach ($this->trades->{$event->getOrigin()->getPlayer()->getName()}->items as $item) { - $event->getOrigin()->getPlayer()->getInventory()->addItem($item); + for ($a = 0; $a < $ic->count; $a++) { + $localCount = $a; + + $dest = $action->getDestination()->getSlotId(); + // Put cctr to slot + if (@$this->trades->{$event->getOrigin()->getPlayer()->getName()} !== null) { + $data = $this->trades->{$event->getOrigin()->getPlayer()->getName()}; + $quota = $data->quota[$localCount]; + $referredItem = clone $data->items[$localCount]->item; + if ($quota <= 0) { + // Error on qta + return; } - $first = $event->getOrigin()->getPlayer()->getInventory()->first($data->item); - if ($first > -1) { - $item = $event->getOrigin()->getPlayer()->getInventory()->getItem($first); - $item->setCount($item->getCount() - $quota); - $event->getOrigin()->getPlayer()->getInventory()->setItem($first, $item); - } elseif ($first == -1 && $data->item->getCount() !== 0) { - $item = $data->item; - $item->setCount($item->getCount() - $quota); - $event->getOrigin()->getPlayer()->getInventory()->addItem($item); + + if ($a === 0) { + $log .= "§l" . $data->items[$localCount]->item->getCount() . "§r " . $data->items[$localCount]->item->getName(); + } else { + $log .= " and §l" . $data->items[$localCount]->item->getCount() . "§r " . $data->items[$localCount]->item->getName(); + $config = $cm->get()->{$cm->getSingleKey()}; + $inv = (array)json_decode(base64_decode($config->history)); + $inv[] = $log; + $config->history = base64_encode(json_encode($inv)); + $cm->set($cm->getSingleKey(), $config); } - // Now add the earned to the Shopkeeper's inventory! - $count = $quota; - $posed = false; - $inventoryInsideConfig = $cm->get()->{$cm->getSingleKey()}->inventory; - if (!$cm->get()->{$cm->getSingleKey()}->admin) { - foreach ($inventoryInsideConfig as $slot => $indexedItem) { - $indexedItem = NbtManager::decode($indexedItem); - if ($indexedItem->equals($referredItem)) { - // Add if is not 64 - if ($indexedItem->getCount() + $count <= 64) { - // Perfect, add and exit! - $indexedItem->setCount($indexedItem->getCount() + $count); - $inventoryInsideConfig[$slot] = NbtManager::encode($indexedItem); - $posed = true; - $count = 0; - break; - } elseif ($indexedItem->getCount() + $count > 64) { - // Add what we can - $count = $count - (64 - $indexedItem->getCount()); - $indexedItem->setCount(64); - $inventoryInsideConfig[$slot] = NbtManager::encode($indexedItem); - } + + if ($data->items[$localCount]->count > 1) { + $this->trades->{$event->getOrigin()->getPlayer()->getName()}->count = $this->trades->{$event->getOrigin()->getPlayer()->getName()}->items[$localCount]->count - $quota; + $data->items[$localCount]->item->setCount($data->count - $quota); + foreach ($this->trades->{$event->getOrigin()->getPlayer()->getName()}->itemsAdd as $item) { + if (!$ic->added) { + $ic->added = true; + $event->getOrigin()->getPlayer()->getInventory()->addItem($item); } } - - if (!$posed && $count <= 64 && $count > 0) { - for ($a = 0; $a < 51; $a++) { - if (empty($inventoryInsideConfig[$a])) { - $referredItem->setCount($count); - $inventoryInsideConfig[$a] = NbtManager::encode($referredItem); - break; + $first = $event->getOrigin()->getPlayer()->getInventory()->first($data->items[$localCount]->item); + if ($first > -1) { + $item = $event->getOrigin()->getPlayer()->getInventory()->getItem($first); + $item->setCount($item->getCount() - $quota); + $event->getOrigin()->getPlayer()->getInventory()->setItem($first, $item); + } elseif ($first == -1 && $data->items[$localCount]->item->getCount() !== 0) { + $item = $data->items[$localCount]->item; + $item->setCount($item->getCount() - $quota); + $event->getOrigin()->getPlayer()->getInventory()->addItem($item); + } + // Now add the earned to the Shopkeeper's inventory! + $count = $quota; + $posed = false; + $inventoryInsideConfig = $cm->get()->{$cm->getSingleKey()}->inventory; + if (!$cm->get()->{$cm->getSingleKey()}->admin) { + foreach ($inventoryInsideConfig as $slot => $indexedItem) { + $indexedItem = NbtManager::decode($indexedItem); + if ($indexedItem->equals($referredItem)) { + // Add if is not 64 + if ($indexedItem->getCount() + $count <= 64) { + // Perfect, add and exit! + $indexedItem->setCount($indexedItem->getCount() + $count); + $inventoryInsideConfig[$slot] = NbtManager::encode($indexedItem); + $posed = true; + $count = 0; + break; + } elseif ($indexedItem->getCount() + $count > 64) { + // Add what we can + $count = $count - (64 - $indexedItem->getCount()); + $indexedItem->setCount(64); + $inventoryInsideConfig[$slot] = NbtManager::encode($indexedItem); + } } } + + if (!$posed && $count <= 64 && $count > 0) { + for ($a = 0; $a < 51; $a++) { + if (empty($inventoryInsideConfig[$a])) { + $referredItem->setCount($count); + $inventoryInsideConfig[$a] = NbtManager::encode($referredItem); + break; + } + } + } + + $config = $cm->get()->{$cm->getSingleKey()}; + $config->inventory = $inventoryInsideConfig; + $cm->set($cm->getSingleKey(), $config); } - - $config = $cm->get()->{$cm->getSingleKey()}; - $config->inventory = $inventoryInsideConfig; - $cm->set($cm->getSingleKey(), $config); - } - } - } else { - $event->getOrigin()->getPlayer()->sendMessage("§cError!\nIt seems that you are not trading rn!"); + $count++; + } + } else { + $event->getOrigin()->getPlayer()->sendMessage("§cError!\nIt seems that you are not trading rn!"); + } } + $this->trades->{$event->getOrigin()->getPlayer()->getName()} = null; } else { if (!$ic->specialstack) { - $ic->specialstack = true; if (@$this->trades->{$event->getOrigin()->getPlayer()->getName()} !== null && $action->getSource()->getContainerId() != 47) { // So we need to get the item from the slot if ($this->trades->{$event->getOrigin()->getPlayer()->getName()} instanceof \stdClass) { // If it's stdClass it's beautiful! if ($event->getOrigin()->getPlayer()->getInventory()->getSize() > $action->getSource()->getSlotId()) { - $this->trades->{$event->getOrigin()->getPlayer()->getName()}->item = @$event->getOrigin()->getPlayer()->getInventory()->getItem($action->getSource()->getSlotId()) ?? VanillaItems::AIR(); - $this->trades->{$event->getOrigin()->getPlayer()->getName()}->count = $action->getCount(); + echo "\n\nTR\n\n"; + $item = new \stdClass; + $item->item = @$event->getOrigin()->getPlayer()->getInventory()->getItem($action->getSource()->getSlotId()) ?? VanillaItems::AIR(); + $item->count = $action->getCount(); + $this->trades->{$event->getOrigin()->getPlayer()->getName()}->items[] = $item; } } } diff --git a/src/FoxWorn3365/Shopkeepers/Menu/EditItemMenu.php b/src/FoxWorn3365/Shopkeepers/Menu/EditItemMenu.php index 892d3d2..d5d2d7a 100644 --- a/src/FoxWorn3365/Shopkeepers/Menu/EditItemMenu.php +++ b/src/FoxWorn3365/Shopkeepers/Menu/EditItemMenu.php @@ -84,8 +84,9 @@ function edit() : InvMenu { $slot->sellsign = 12; $slot->buysign = 9; - $sell = $object->sell; - $buy = $object->buy; + $sell = @$object->sell; + $buy = @$object->buy; + $buy2 = @$object->buy2; $signsell = Factory::sign(4, 'Put what do you want to sell to my right!'); $signbuy = Factory::sign(4, 'Put what do you want to buy to my right!'); @@ -103,6 +104,12 @@ function edit() : InvMenu { $buyitem = SerializedItem::decode($buy); } + if ($buy2 === null) { + $buyitem2 = VanillaItems::AIR(); + } else { + $buyitem2 = SerializedItem::decode($buy2); + } + // Now load simple screen $this->menu->setName("Editing shop {$this->config->title}"); $this->menu->getInventory()->setItem(17, Factory::item(160, 14, "Delete the item")); @@ -111,13 +118,18 @@ function edit() : InvMenu { $this->menu->getInventory()->setItem(1, Factory::item(35, 13, "+1")); $this->menu->getInventory()->setItem(19, Factory::item(35, 14, "-1")); + // 2nd buy qta increasator and decreasator + $this->menu->getInventory()->setItem(2, Factory::item(35, 13, "+1")); + $this->menu->getInventory()->setItem(20, Factory::item(35, 14, "-1")); + // Sell qta increasator and decreasator - $this->menu->getInventory()->setItem(4, Factory::item(35, 13, "+1")); - $this->menu->getInventory()->setItem(22, Factory::item(35, 14, "-1")); + $this->menu->getInventory()->setItem(5, Factory::item(35, 13, "+1")); + $this->menu->getInventory()->setItem(23, Factory::item(35, 14, "-1")); // Put data $this->menu->getInventory()->setItem(10, $buyitem); - $this->menu->getInventory()->setItem(13, $sellitem); + $this->menu->getInventory()->setItem(11, $buyitem2); + $this->menu->getInventory()->setItem(14, $sellitem); $cm = $this->cm; $config = $this->config; @@ -163,23 +175,43 @@ function edit() : InvMenu { $object->buy = SerializedItem::encode($item); } break; - case 4: - $item = $inventory->getItem(13); + case 2: + $item = $inventory->getItem(11); + if ($item->getCount() + 1 > 64) { + $transaction->getPlayer()->sendMessage("§cYou can't sell an item for more than 64 items!"); + } else { + $item->setCount($item->getCount()+1); + $inventory->setItem(11, $item); + $object->buy2 = SerializedItem::encode($item); + } + break; + case 20: + $item = $inventory->getItem(11); + if ($item->getCount()-1 < 1) { + $transaction->getPlayer()->sendMessage("§cYou can't sell an item for less than 1 item!"); + } else { + $item->setCount($item->getCount()-1); + $inventory->setItem(11, $item); + $object->buy2 = SerializedItem::encode($item); + } + break; + case 5: + $item = $inventory->getItem(14); if ($item->getCount()+1 > 64) { $transaction->getPlayer()->sendMessage("§cYou can't sell more than 64 items!"); } else { $item->setCount($item->getCount()+1); - $inventory->setItem(13, $item); + $inventory->setItem(14, $item); $object->sell = SerializedItem::encode($item); } break; - case 22: - $item = $inventory->getItem(13); + case 23: + $item = $inventory->getItem(14); if ($item->getCount()-1 < 1) { $transaction->getPlayer()->sendMessage("§cYou can't sell less than 1 item!"); } else { $item->setCount($item->getCount()-1); - $inventory->setItem(13, $item); + $inventory->setItem(14, $item); $object->sell = SerializedItem::encode($item); } break; @@ -193,14 +225,33 @@ function edit() : InvMenu { $inventory->setItem(10, $transaction->getItemClickedWith()); } break; - case 13: + case 14: if ($transaction->getItemClickedWith() !== null && $transaction->getItemClickedWith() != VanillaItems::AIR()) { // Let's change the object also in the inventory - $inventory->clear(13); + $inventory->clear(14); // Now let's decode the item $object->sell = SerializedItem::encode($transaction->getItemClickedWith()); usleep(5000); - $inventory->setItem(13, $transaction->getItemClickedWith()); + $inventory->setItem(14, $transaction->getItemClickedWith()); + } + break; + case 11: + $presence = $inventory->getItem(10); + if ($presence === VanillaItems::AIR() || $presence === null) { + $transaction->getPlayer()->sendMessage("§4Sorry but you cannot cannot set the first buy item!"); + break; + } + + if ($transaction->getItemClickedWith() !== null && $transaction->getItemClickedWith() != VanillaItems::AIR()) { + // Let's change the object also in the inventory + $inventory->clear(11); + // Now let's decode the item + $object->buy2 = SerializedItem::encode($transaction->getItemClickedWith()); + usleep(5000); + $inventory->setItem(11, $transaction->getItemClickedWith()); + } elseif ($transaction->getItemClickedWith() === null || $transaction->getItemClickedWith() === VanillaItems::AIR()) { + $object->buy2 = null; + $inventory->clear(11); } break; } diff --git a/src/FoxWorn3365/Shopkeepers/Menu/EditMenu.php b/src/FoxWorn3365/Shopkeepers/Menu/EditMenu.php index 6929d50..efcc26b 100644 --- a/src/FoxWorn3365/Shopkeepers/Menu/EditMenu.php +++ b/src/FoxWorn3365/Shopkeepers/Menu/EditMenu.php @@ -46,7 +46,7 @@ function __construct(ConfigManager $cm, string $name) { } public function create() : InvMenu { - $this->menu->setName("Edit shop {$this->config->title}"); + $this->menu->setName("§l§bTrades §r§l- §r{$this->config->title}"); // LAST SLOT: 26 $slotcount = 9; $defaultconfig = (object)[ @@ -82,17 +82,24 @@ public function create() : InvMenu { if ($itemconstructor->getVanillaName() == 'Stained Glass Pane') { $displayname = "No item set!"; $setname = "Nothing"; + $displaycount = ""; } else { $displayname = $itemconstructor->getVanillaName(); + $displaycount = $itemconstructor->getCount() . ' '; // Load buy block and add to the description if (@$bk->buy !== null) { $buy = SerializedItem::decode($bk->buy); - $setname = "{$buy->getCount()} {$buy->getName()}"; + $setname = "§l{$buy->getCount()} §r{$buy->getName()}"; } else { $setname = "Nothing!"; } + + if (@$bk->buy2 !== null) { + $buy2 = SerializedItem::decode($bk->buy2); + $setname = "{$setname} and for §l{$buy2->getCount()} §r{$buy2->getName()}"; + } } - $itemconstructor->setCustomName("§r{$displayname}\nSold for: {$setname}"); + $itemconstructor->setCustomName("§r§l{$displaycount}{$displayname}\n\n§r§oSold for:§r {$setname}"); $itemmenu = Utils::getIntItem(35, 1); $itemmenu->setCustomName("§rEdit this item"); $this->menu->getInventory()->setItem($slotcount, $itemconstructor); diff --git a/src/FoxWorn3365/Shopkeepers/Menu/ShopInfoMenu.php b/src/FoxWorn3365/Shopkeepers/Menu/ShopInfoMenu.php index 8f9041a..09ff71a 100644 --- a/src/FoxWorn3365/Shopkeepers/Menu/ShopInfoMenu.php +++ b/src/FoxWorn3365/Shopkeepers/Menu/ShopInfoMenu.php @@ -49,7 +49,7 @@ function __construct(ConfigManager $cm, bool $local = false) { } public function create() : InvMenu { - $this->menu->setName("View shop {$this->cm->getSingleKey()}"); + $this->menu->setName("§b§lInfo §r§l- §r{$this->cm->getSingleKey()}"); $inventory = $this->menu->getInventory(); // Draw the useful line @@ -60,32 +60,33 @@ public function create() : InvMenu { $inventory->setItem(4, Factory::egg($this->cm->getSingleKey())); // Now set the informations - $inventory->setItem(10, Factory::item(377, 0, '§lConfig')); + $inventory->setItem(10, Factory::item(377, 0, "§l§6Config\n\n§r§oSee settings for this Shopkeeper")); // Villager inventory if (!$this->config->admin) { - $inventory->setItem(12, Factory::item(54, 0, "§lInventory")); + $inventory->setItem(12, Factory::stringItem("minecraft:chest", "§l§9Inventory\n\n§r§oSee the inventory of the Shopkeeper")); } else { - $inventory->setItem(12, Factory::barrier("§l§cShop inventory\n§rDisabled!\n§oThis is an admin shop!")); // ID: -161 Meta: 0 BRID: 10390 + $inventory->setItem(12, Factory::barrier("§l§cShop inventory\n\n§rDisabled!\n§oThis is an admin shop!")); // ID: -161 Meta: 0 BRID: 10390 } // Shop discounts announcer for v1.0 - $inventory->setItem(20, Factory::item(388, 0, "§o§lSales\n\n§r§oThis function will be implemented with the §bSales & Shops §r§oupdate AKA §lv1.0")); + if (@!$this->config->enabled) { + $inventory->setItem(20, Factory::stringItem("minecraft:torch", "§2§lEnable\n\n§r§oEnable this Shopkeeper. Yeeeee")); + } else { + $inventory->setItem(20, Factory::stringItem("minecraft:torch", "§4§lDisable\n\n§r§oDisable this Shopkeeper until a new order (yes, from you)")); + } // Summon option - $head = Utils::getItem("minecraft:skull"); - $head->setCustomName("§r§lSummon"); - $inventory->setItem(22, $head); + $inventory->setItem(22, Factory::stringItem("minecraft:skull", "§l§8Summon\n\n§r§oSummon an entity for this shop.\n§e§oNOTE: §r§oIt will look in your current direction!")); // Misteryous option - $inventory->setItem(24, Factory::barrier("§oUnknown\n\nThis function will be implemented with the §bSales & Shops §r§oupdate AKA §lv1.0")); + $inventory->setItem(24, Factory::stringItem("minecraft:writable_book", "§b§lTrades History\n\n§r§oSee the trades history of this Shopkeeper")); // Edit Shopkeepers trades - $st = Utils::getItem("minecraft:smithing_table"); - $st->setCustomName("§r§lTrades"); - $inventory->setItem(14, $st); + $inventory->setItem(14, Factory::stringItem("minecraft:smithing_table", "§d§lTrades\n\n§r§oView and edit the Shopkeeper's trades")); - $inventory->setItem(16, Factory::item(35, 14, "§c§lDelete")); + // Delete options + $inventory->setItem(16, Factory::item(35, 14, "§c§lDelete\n\n§r§oThis Shopkeeper will be deleted §cFOREVER§r§o!")); $cm = $this->cm; $config = $this->config; @@ -144,6 +145,44 @@ public function create() : InvMenu { $villager->spawnToAll(); $transaction->getPlayer()->removeCurrentWindow(); break; + case 20: + if (!$transaction->getPlayer()->hasPermission("shopkeepers.shop.enableDisable")) { + $transaction->getPlayer()->removeCurrentWindow(); + $transaction->getPlayer()->sendMessage(self::NOT_PERM_MSG); + break; + } + + if (@!$config->enabled) { + $config->enabled = true; + } else { + $config->enabled = false; + } + + $cm->set($cm->getSingleKey(), $config); + break; + case 24: + if (!$transaction->getPlayer()->hasPermission("shopkeepers.shop.history")) { + $transaction->getPlayer()->removeCurrentWindow(); + $transaction->getPlayer()->sendMessage(self::NOT_PERM_MSG); + break; + } + + $transaction->getPlayer()->removeCurrentWindow(); + $transaction->getPlayer()->sendMessage("For the complete history please use /sk history [PAGE]"); + $message = "§lLast 20 trades for this Shopkeeper:\n"; + $array = (array)json_decode(base64_decode($config->history)); + if (count($array) > 20) { + $count = count($array) - 20; + } else { + $count = count($array); + } + + for ($a = $count; $a < count($array); $a++) { + $item = $array[$a]; + $message = "\n{$item}"; + } + $transaction->getPlayer()->sendMessage($message); + break; } return $transaction->discard(); }); diff --git a/src/FoxWorn3365/Shopkeepers/shop/ElementContainer.php b/src/FoxWorn3365/Shopkeepers/shop/ElementContainer.php index 3730776..c47d9af 100644 --- a/src/FoxWorn3365/Shopkeepers/shop/ElementContainer.php +++ b/src/FoxWorn3365/Shopkeepers/shop/ElementContainer.php @@ -31,7 +31,7 @@ class ElementContainer { protected array $elements = []; public ListTag $nbt; - public function add(Item|string $sell, Item|string $buy, array $inventory, bool $admin = false) : void { + public function add(Item|string $sell, Item|string $buy, array $inventory, bool $admin = false, Item|string $buy2 = null) : void { $tag = CompoundTag::create(); // Simple tag management to optimize the speed. Anyways the system will give always a string so it's kida useless @@ -40,6 +40,14 @@ public function add(Item|string $sell, Item|string $buy, array $inventory, bool } else { $tag->setTag("buyA", NbtManager::partialDecode($buy)); } + + if ($buy2 !== null) { + if ($buy2 instanceof Item) { + $tag->setTag("buyB", $buy2->nbtSerialize(-1)); + } else { + $tag->setTag("buyB", NbtManager::partialDecode($buy2)); + } + } if ($sell instanceof Item) { $tag->setTag("sell", $sell->nbtSerialize(-1)); @@ -80,6 +88,7 @@ public function add(Item|string $sell, Item|string $buy, array $inventory, bool $tag->setInt("demand", 1); $tag->setInt("traderExp", 0); $tag->setInt("priceMultiplierA", 0.0); + $tag->setInt("priceMultiplierB", 0.0); $this->elements[] = $tag; } diff --git a/src/FoxWorn3365/Shopkeepers/shop/Manager.php b/src/FoxWorn3365/Shopkeepers/shop/Manager.php index 3e40470..d104c8a 100644 --- a/src/FoxWorn3365/Shopkeepers/shop/Manager.php +++ b/src/FoxWorn3365/Shopkeepers/shop/Manager.php @@ -53,7 +53,7 @@ public function send(Player $player, Shopkeeper $entity) : void { if (!(!empty($itemconfig->sell) && !empty($itemconfig->buy)) && gettype($this->config->inventory) !== 'array') { continue; } - $this->container->add($itemconfig->sell, $itemconfig->buy, $this->config->inventory, $this->config->admin); + $this->container->add($itemconfig->sell, @$itemconfig->buy, $this->config->inventory, $this->config->admin, @$itemconfig->buy2); } $shop = new Shop($this->container->toNBT(), $player, $entity, $this->config->title); diff --git a/src/FoxWorn3365/Shopkeepers/utils/Factory.php b/src/FoxWorn3365/Shopkeepers/utils/Factory.php index 8694d10..ee8352d 100644 --- a/src/FoxWorn3365/Shopkeepers/utils/Factory.php +++ b/src/FoxWorn3365/Shopkeepers/utils/Factory.php @@ -61,7 +61,8 @@ public static function egg(string $name, int $count = 1) : ?Item { } public static function barrier(string $name, int $count = 1) : ?Item { - $barrier = ItemUtils::decode(-161, 0, 10390); + //$barrier = ItemUtils::decode(-161, 0, 10390); + $barrier = Utils::getItem("minecraft:barrier"); $barrier->setCustomName("§r{$name}"); $barrier->setCount($count); return $barrier; @@ -77,4 +78,11 @@ public static function nbt(string $nbt, string $name, int $count = 1) { public static function itemStack(int $id, int $meta, int $netId, int $count = 1) : ItemStack { return new ItemStack($id, $meta, $count, $netId, new CompoundTag(), [], []); } + + public static function stringItem(string $item, string $name, int $count = 1) : ?Item { + $item = Utils::getItem($item); + $item->setCustomName("§r{$name}"); + $item->setCount($count); + return $item; + } } \ No newline at end of file diff --git a/src/FoxWorn3365/Shopkeepers/utils/Utils.php b/src/FoxWorn3365/Shopkeepers/utils/Utils.php index 1a1710c..620fc01 100644 --- a/src/FoxWorn3365/Shopkeepers/utils/Utils.php +++ b/src/FoxWorn3365/Shopkeepers/utils/Utils.php @@ -112,12 +112,31 @@ public static function shopTypeChecker(string $data_dir, object $object, string // Oh shit is not array! if (gettype($shop->inventory) === 'object') { self::errorLogger($data_dir, "NOTICE", "Value of 'inventory' inside shop '{$name}', file '{$file}' is an object! Corrected"); - $shop->inventory = (array)$shop->inventory; + $it = []; + foreach ($shop->inventory as $item) { + $it[] = $item; + } + $shop->inventory = $it; } else { self::errorLogger($data_dir, "WARNING", "Value of 'inventory' inside shop '{$name}', file '{$file}' is not a correct value! Neutralized"); $shop->inventory = []; } } + + // Validate and add if not present the history camp + if (@$shop->history === null) { + $shop->history = ""; + } elseif (gettype($shop->history) !== 'string') { + $shop->history = ""; + } + + // Validate and add if not present the enabled camp + if (@$shop->enabled === null) { + $shop->enabled = true; + } elseif (gettype($shop->enabled) !== 'bool') { + $shop->enabled = true; + } + // Update the shop $end->{$name} = $shop; } @@ -132,6 +151,14 @@ public static function comparator(Item $buy, int $sellcount, array $items) : str } } + public static function entityFixer(string $data_dir, object|array $data) : void { + if (gettype($data) !== 'array') { + if (gettype($data) === 'object') { + file_put_contents($data_dir, (array)$data); + } + } + } + public static function randomizer(int $lenght) : int { $buffer = ""; for ($a = 0; $a < $lenght; $a++) { diff --git a/src/muqsit/invmenu/InvMenu.php b/src/muqsit/invmenu/InvMenu.php new file mode 100644 index 0000000..6a804a5 --- /dev/null +++ b/src/muqsit/invmenu/InvMenu.php @@ -0,0 +1,186 @@ +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; + }; + } + + readonly public 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 __destruct(){ + $this->setInventory(null); + } + + /** + * @deprecated Access {@see InvMenu::$type} directly + * @return InvMenuType + */ + 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->network; + + // 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..ea48f69 --- /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)?->network->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->network->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->post_transaction_callback; + if($network_stack_callback !== null){ + $network_stack_callbacks[] = $network_stack_callback; + } + if($result->cancelled){ + $event->cancel(); + break; + } + } + + if(count($network_stack_callbacks) > 0){ + $player_instance->network->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..7609b8e --- /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..0bf39d9 --- /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..d41d992 --- /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..b9c1915 --- /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; + } + + /** + * @deprecated Access {@see PlayerManager::$network_handler_registry} directly + * @return PlayerNetworkHandlerRegistry + */ + 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..d546e8d --- /dev/null +++ b/src/muqsit/invmenu/session/PlayerSession.php @@ -0,0 +1,102 @@ +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; + }); + } + } + + /** + * @deprecated Access {@see PlayerSession::$network} directly + * @return PlayerNetwork + */ + 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..21eef61 --- /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..0e905c3 --- /dev/null +++ b/src/muqsit/invmenu/session/network/PlayerNetwork.php @@ -0,0 +1,230 @@ +|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( + readonly private NetworkSession $network_session, + readonly 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; + } +} 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..6b2314d --- /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{ + $timestamp = mt_rand(); + return new NetworkStackLatencyEntry($timestamp * 1000000, $then, $timestamp); + })); + $this->register(DeviceOS::PLAYSTATION, new ClosurePlayerNetworkHandler(static function(Closure $then) : NetworkStackLatencyEntry{ + $timestamp = mt_rand(); + return new NetworkStackLatencyEntry($timestamp * 1000000, $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; + } +} diff --git a/src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php b/src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php new file mode 100644 index 0000000..8a0371e --- /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(Player) : void)|null $callback + * @return self + */ + public function then(?Closure $callback) : self{ + $this->post_transaction_callback = $callback; + return $this; + } + + /** + * @deprecated Access {@see InvMenuTransactionResult::$post_transaction_callback} directly + * @return (Closure(Player) : void)|null + */ + 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..59dcb48 --- /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..d4b5f74 --- /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( + readonly private string $actor_identifier, + readonly private int $actor_runtime_identifier, + readonly private array $actor_metadata, + readonly private int $size, + readonly 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..8303f94 --- /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..e29d291 --- /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..fd8ccb6 --- /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..89c8e81 --- /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( + readonly private string $actor_identifier, + readonly private int $actor_runtime_identifier, + readonly private array $actor_metadata, + readonly private ?InvMenuGraphicNetworkTranslator $network_translator = null, + readonly 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..34ef106 --- /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; + } + + readonly private BlockInvMenuGraphic $block_graphic; + readonly private Vector3 $position; + readonly private CompoundTag $tile; + readonly private ?InvMenuGraphicNetworkTranslator $network_translator; + readonly 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..3a8003b --- /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), TypeConverter::getInstance()->getBlockTranslator()->internalIdToNetworkId($this->block->getStateId()), 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 = TypeConverter::getInstance(); + $block = $world->getBlockAt($this->position->x, $this->position->y, $this->position->z); + $network->sendDataPacket(UpdateBlockPacket::create(BlockPosition::fromVector3($this->position), $runtime_block_mapping->getBlockTranslator()->internalIdToNetworkId($block->getStateId()), 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; + } +} 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..af389da --- /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: Mon, 24 Jul 2023 00:47:55 +0200 Subject: [PATCH 04/11] Final relase of v1.0 --- .poggit.yml | 5 +- README.md | 40 ++++- plugin-banner-white.png | Bin 82609 -> 0 bytes plugin-banner.png | Bin 61192 -> 0 bytes plugin.yml | 2 +- src/FoxWorn3365/Shopkeepers/Core.php | 162 +++++++++++++----- src/FoxWorn3365/Shopkeepers/EntityManager.php | 25 ++- .../Shopkeepers/Menu/EditItemMenu.php | 30 ++-- src/FoxWorn3365/Shopkeepers/Menu/InfoMenu.php | 2 +- .../Shopkeepers/Menu/ShopConfigMenu.php | 2 +- .../Shopkeepers/Menu/ShopInfoMenu.php | 24 ++- .../Shopkeepers/Menu/ShopInventoryMenu.php | 2 +- .../Shopkeepers/entity/HumanShopkeeper.php | 75 ++++++++ .../Shopkeepers/entity/Shopkeeper.php | 1 + src/FoxWorn3365/Shopkeepers/shop/Manager.php | 12 +- src/FoxWorn3365/Shopkeepers/shop/Shop.php | 5 +- .../Shopkeepers/utils/SkinUtils.php | 50 ++++++ src/Himbeer/LibSkin/LibSkin.php | 34 ++++ src/Himbeer/LibSkin/SkinConverter.php | 106 ++++++++++++ src/Himbeer/LibSkin/SkinGatherer.php | 128 ++++++++++++++ 20 files changed, 617 insertions(+), 88 deletions(-) delete mode 100644 plugin-banner-white.png delete mode 100644 plugin-banner.png create mode 100644 src/FoxWorn3365/Shopkeepers/entity/HumanShopkeeper.php create mode 100644 src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php create mode 100644 src/Himbeer/LibSkin/LibSkin.php create mode 100644 src/Himbeer/LibSkin/SkinConverter.php create mode 100644 src/Himbeer/LibSkin/SkinGatherer.php diff --git a/.poggit.yml b/.poggit.yml index 12b3e00..cf90bc8 100644 --- a/.poggit.yml +++ b/.poggit.yml @@ -8,4 +8,7 @@ projects: libs: - src: muqsit/InvMenu/InvMenu version: ^4.6.1 - branch: pm5 \ No newline at end of file + branch: pm5 + - src: Himbeer/LibSkin/LibSkin + version: ^2.0.0 + branch: master \ No newline at end of file diff --git a/README.md b/README.md index cdd7e1b..7eb5d4f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- + Add Shopkeepers to your PocketMine-MP world! Allow the creation of simple barter stores between players or create adminshops!

@@ -16,7 +16,7 @@

--- -

Shopkeepers v0.9.1 for PocketMine-MP 5

+

Shopkeepers v1.0 for PocketMine-MP 5


**⚠️ We are not in any way related to the [Shopkeepers plugin](https://dev.bukkit.org/projects/shopkeepers) for Bukkit!** @@ -59,6 +59,26 @@ Here a list of all commands that you can use: | summon | SHOP NAME | Summon a Shopkeeper entity (as a Villager) for your Shop | | rename | SHOP NAME and NEW NAME | [NOT AVAILABLE] Rename a current shop | | list | none | Show all of your shops | +| history | SHOP NAME and PAGE | Show the trade history for the shopkeeper | + +## Shopkeepers Skin System (SSS) +Yes, the v1.0 brought an epic function: now you can set a skin of a Shopkeeper.
+Unfortunately players can't add a skin of a Shopkeeper for multiple reasons: +- Memory +- Memory +- Memory +- Hmm, Memory? + +Anyways, to avoid abuse of this system we have made this feature usable only by the server administrator. + +### How to add a skin of a Shopkeeper +You should have seen something new in the `Shopkeepers` folder, the `skins` folder, and that is where all the skins should be put. +> **Warning**
+> Skins MUST BE in a `.png` file! + +The file name should be composed as follows: `_.png`, for example `FoxWorn3365_Fox.png` is valid and will be used by the plugin. +### I don't want to select skins +Well, if no skin is provided the classic villager is spawned. yeee ## F.A.Q. ### How to create an Admin shop @@ -66,8 +86,8 @@ There is not really an Admin shop but you can activate this function by using th ### How to see a Shopkeeper's inventory There are two ways: -- Use the command `/sk info ` and then click on the chest at the center of the GUI -- Click on the Shopkeeper (Villager) entity +- Use the command `/sk info ` and then click on the chest! +- Click on the Shopkeeper (Villager) entity and then click on the chest! ### I want to access to the Shopkeeper's trade page but if i click the entity i access the inventory! Easy: shift and click on the Shopkeeper @@ -75,6 +95,15 @@ Easy: shift and click on the Shopkeeper ### How to despawn a Shopkeeper More easy: just hit it, it will die in only one hit! +### I have [ClearLag](https://poggit.pmmp.io/p/ClearLag/2.1.0) and it removes the Shopkeepers entites! +Edit the config of ClearLag changing to `false` [this option](https://github.com/tobiaskirchmaier/ClearLag/blob/03e2a03a5f8868216dfc89eb78f51523ff228d6b/resources/config.yml#L84C5-L84C24). + +### How can I change the skin of a Shopkeeper +We actually support a Skin System, please read up. + +### OMG I CAN'T ACCESS TO THE INVENTORYM uyigqwieduwefibef +If the Shopkeeper is an Admin Shop it does not have an inventory! + ## Bug reporting Reporting bugs ~~to developers~~ to the developer😢 is very important to ensure the stability of the plugin, so in order to better track and manage all reports it is **incredibly necessary** that they are reported via [GitHub Issues](https://github.com/FoxWorn3365/Shopkeepers/issues).
Here is what to include in the reporting to make it perfect: @@ -87,7 +116,7 @@ Here is what to include in the reporting to make it perfect: 4. (OPTIONAL) The plugin download source > Knowing where you downloaded the plugin from might help, always better to know some information 5. (OPTIONAL) The `Shopkeepers.phar` -> "Last wade," in case we can analyze the source +> "Last wade", in case we can analyze the source ## Contribution guide Any contribution is greatly appreciated because you help me to lighten my workload, so here are some small guidelines to follow when you want to contribute: @@ -240,6 +269,7 @@ Also [this plugin](https://github.com/FrozenArea/TradeAPI) helped me! - [x] Shopkeeper's inventory updating when players buy - [x] Shopkeeper deny a trade if the inventory is without the item - [x] Double trade +- [x] Skin system - [ ] Online shopkeeper editor - [ ] Online shopkeeper shop (yes, you will be able to make shopping from your phone on the subway!) 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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 H%-;SV68$au diff --git a/plugin-banner.png b/plugin-banner.png deleted file mode 100644 index 83250a3c6d51af06371a4e15ded923082d1d534f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61192 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�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" + usage: "/shopkeepers [list|create|summon|rename|edit|info|history] " aliases: - sk - shopk diff --git a/src/FoxWorn3365/Shopkeepers/Core.php b/src/FoxWorn3365/Shopkeepers/Core.php index 80d9c1c..d2eb443 100644 --- a/src/FoxWorn3365/Shopkeepers/Core.php +++ b/src/FoxWorn3365/Shopkeepers/Core.php @@ -36,6 +36,7 @@ use pocketmine\utils\TextFormat; use pocketmine\Server; use pocketmine\level\Position; +use pocketmine\entity\Entity; // Events use pocketmine\event\entity\EntityDamageEvent as Damage; @@ -50,6 +51,7 @@ use pocketmine\network\mcpe\protocol\ItemStackRequestPacket; use pocketmine\network\mcpe\protocol\ContainerClosePacket; use pocketmine\network\mcpe\protocol\BookEditPacket; +use pocketmine\network\mcpe\protocol\InventoryTransactionPacket; // Custom use FoxWorn3365\Shopkeepers\Menu\InfoMenu; @@ -58,9 +60,11 @@ use FoxWorn3365\Shopkeepers\Menu\ListMenu; use FoxWorn3365\Shopkeepers\Menu\ShopInfoMenu; use FoxWorn3365\Shopkeepers\entity\Shopkeeper; +use FoxWorn3365\Shopkeepers\entity\HumanShopkeeper; use FoxWorn3365\Shopkeepers\shop\Manager; use FoxWorn3365\Shopkeepers\utils\NbtManager; use FoxWorn3365\Shopkeepers\utils\Utils; +use FoxWorn3365\Shopkeepers\utils\SkinUtils; // Newtork Parts use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest; @@ -69,6 +73,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DeprecatedCraftingResultsStackRequestAction; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\convert\TypeConverter; +use pocketmine\network\mcpe\protocol\types\inventory\UseItemOnEntityTransactionData; // Exceptions use pocketmine\network\mcpe\convert\TypeConversionException; @@ -78,6 +83,7 @@ class Core extends PluginBase implements Listener { protected EntityManager $entities; protected object $trades; protected object $tradeQueue; + protected object $handle; protected string $defaultConfig = "IwojIFNob3BrZWVwZXJzIHYwLjkuMSBieSBGb3hXb3JtMzM2NQojIChDKSAyMDIzLW5vdyBGb3hXb3JuMzM2NQojIAojIFJlbGFzZWQgdW5kZXIgdGhlIEdQTC0zLjAgbGljZW5zZSAKIyBodHRwczovL2dpdGh1Yi5jb20vRm94V29ybjMzNjUvU2hvcGtlZXBlcnMvYmxvYi9tYWluL0xJQ0VOU0UKIwoKZW5hYmxlZDogdHJ1ZQoKIyBNYXggc2hvcGtlZXBlcidzIGVudGl0aWVzIGZvciBvbmUgcGxheWVyIChQRVIgU0hPUCkKbWF4LWVudGl0aWVzLWZvci1wbGF5ZXI6IDUKIyBQbGF5ZXIgdGhhdCBjYW4gYnlwYXNzIHRoaXMgbGltaXRhdGlvbgptYXgtZW50aXRpZXMtYnlwYXNzOgogIC0gWW91ck1pbmVjcmFmdFVzZXJuYW1lCgojIE1vZGVyYXRpb24gc2V0dGluZ3MgICAtIFRISVMgSVMgQSBDT05UQUlOIENPTkRJVElPTiBzbyBpZiB5b3Ugc2V0ICdwcm8nIGFsc28gbmFtZXMgbGlrZSAnYXByb24nLCAncHJvdG90eXB1cycsICdwcm90bycsICdwcm8nIGFuZCBpdCdzIGNhc2UgSU5TRU5TSVRJVkUKYmFubmVkLXNob3AtbmFtZXM6CiAgLSBoaXRsZXIKICAtIG5hemkKCiMgQmFubmVkIHNob3AgaXRlbSBuYW1lcyBzbyB0aGV5IGNhbid0IGJlIHNvbGQgb3IgYm91Z2h0CmJhbm5lZC1pdGVtLW5hbWVzOgogIC0gZGlhbW9uZF9heGUKCiMgQmFubmVkIGl0ZW0gSURzIApiYW5uZWQtaXRlbS1pZHM6CiAgLSAyNTU="; @@ -85,12 +91,13 @@ class Core extends PluginBase implements Listener { protected const NOT_PERM_MSG = "§cSorry but you don't have permissions to use this command!\nPlease contact your server administrator"; public const AUTHOR = "FoxWorn3365"; - public const VERSION = "0.9.1-pre"; + public const VERSION = "1.0.0"; public function onLoad() : void { $this->menu = new \stdClass; $this->trades = new \stdClass; $this->tradeQueue = new \stdClass; + $this->handle = new \stdClass; } public function onEnable() : void { @@ -102,6 +109,7 @@ public function onEnable() : void { // Create the config folder if it does not exists @mkdir($this->getDataFolder()); + @mkdir($this->getDataFolder() . 'skins'); // Check for file integrity Utils::integrityChecker($this->getDataFolder()); @@ -132,43 +140,18 @@ public function onPlayerJoin(PlayerJoinEvent $event) { // Create the player's object queue for managing event procession $this->tradeQueue->{$event->getPlayer()->getName()} = false; + $this->handle->{$event->getPlayer()->getName()} = false; } public function onPlayerEntityInteract(Interaction $event) : void { - if (!$event->getPlayer()->hasPermission("shopkeepers.shop.use")) { - $event->getPlayer()->sendMessage(self::NOT_PERM_MSG); - } - $entity = $event->getEntity(); - if ($entity instanceof Shopkeeper) { - $data = $entity->getConfig(); - $cm = new ConfigManager($data->author, $this->getDataFolder()); - $cm->setSingleKey($data->shop); - if (@$cm->get()->{$data->shop} === null) { - // Oh no, no config! - $event->getPlayer()->sendMessage("§cSorry but this shop does not exists anymore!"); - // Remove the shop - $this->entities->remove($this->entities->generateEntityHash($event->getEntity())); - $event->getEntity()->kill(); - return; - } elseif ($data->author === $event->getPlayer()->getName() && !$event->getPlayer()->isSneaking()) { - // Open the shopkeeper's ~~inventory~~ info page RN! - $menu = new ShopInfoMenu($cm, true); - $menu->create()->send($event->getPlayer()); - } else { - // It's a shopkeeper! - // BEAUTIFUL! - // Now let's open the shopkeeper interface - $manager = new Manager($cm); - $this->trades->{$event->getPlayer()->getName()} = new \stdClass; - $this->trades->{$event->getPlayer()->getName()}->config = $data; - $this->trades->{$event->getPlayer()->getName()}->items = []; - $manager->send($event->getPlayer(), $entity); - } + if (!$this->handle->{$event->getPlayer()->getName()}) { + $this->handle->{$event->getPlayer()->getName()} = true; + $this->entityInteractionLoad($event->getEntity(), $event->getPlayer()); } } public function onEntitySpawn(EntitySpawnEvent $event) : void { - if ($event->getEntity() instanceof Shopkeeper) { + if ($event->getEntity() instanceof Shopkeeper || $event->getEntity() instanceof HumanShopkeeper) { // Add the shopkeeper to entity interface if (!$event->getEntity()->hasCustomShopkeeperEntityId()) { // FIRST, check if the limit is not trepassed @@ -198,6 +181,7 @@ public function onCommand(CommandSender $sender, Command $command, $label, array $sender->sendMessage("This command can be only executed by in-game players!"); return false; } + $shop = new ConfigManager($sender, $this->getDataFolder()); // Empty treath @@ -304,7 +288,14 @@ public function onCommand(CommandSender $sender, Command $command, $label, array $shopdata = new \stdClass; $shopdata->author = $sender->getName(); $shopdata->shop = $name; - $villager = new Shopkeeper($pos, $shopdata); + if (SkinUtils::find($name, $sender->getName(), $this->getDataFolder())) { + // Has a skin, let's summon an human entity after getting the skin + $skin = SkinUtils::get($name, $sender->getName(), $this->getDataFolder()); + $villager = new HumanShopkeeper($pos, $skin, $shopdata); + } else { + // A simple Shopkeeper, so summon a villager-like entity + $villager = new Shopkeeper($pos, $shopdata); + } $villager->setNameTag($name); $villager->setNameTagAlwaysVisible($shop->get()->{$name}->namevisible); $villager->spawnToAll(); @@ -313,6 +304,40 @@ public function onCommand(CommandSender $sender, Command $command, $label, array } elseif ($args[0] === "remove" || $args[0] === "despawn") { $sender->sendMessage("To remove a shopkeeper just hit it!"); return true; + } elseif ($args[0] === "history" && !empty($args[1])) { + if (!$sender->hasPermission("shopkeepers.shop.history")) { + $sender->sendMessage(self::NOT_PERM_MSG); + } + + $name = $args[1]; + if (@$shop->get()?->{$name} === null) { + $sender->sendMessage("You don't have a shop called {$name}!"); + return false; + } + + if (!empty($args[2])) { + $page = $args[2]; + } else { + $page = 1; + } + + $history = (array)json_decode(base64_decode(@$shop->get()->{$name}->history)); + + // Let's divide in pages + $pages = ceil(count($history)/20); + + if ($page > $pages) { + $sender->sendMessage("§cSorry but there are only {$pages} pages!"); + } else { + $message = "§lTrade history for Shopkeeper {$name}.§r\nPage §l{$page}§r/{$pages}\n"; + for ($a = ($page-1)*20; $a < $page*20; $a++) { + if (!empty($history[$a])) { + $message .= "\n" . $history[$a]; + } + } + $sender->sendMessage($message); + } + return true; } elseif ($args[0] === "rename" && !empty($args[1]) && !empty($args[2])) { if (!$sender->hasPermission("shopkeepers.shop.rename")) { $sender->sendMessage(self::NOT_PERM_MSG); @@ -359,10 +384,12 @@ public function onPacket(DataPacketReceiveEvent $event) : void { $itemglobal = null; $inventoryInsideConfig = null; $log = ""; - + $stackcount = 1; + $maxcount = 1; + if ($event->getPacket() instanceof ItemStackRequestPacket) { $inventory = $event->getOrigin()->getPlayer()->getInventory(); - + $maxcount = count($event->getPacket()->getRequests()); foreach ($event->getPacket()->getRequests() as $request) { if ($request instanceof ItemStackRequest) { foreach ($request->getActions() as $action) { @@ -435,7 +462,7 @@ public function onPacket(DataPacketReceiveEvent $event) : void { } if (gettype($inventoryInsideConfig) !== 'array') { - Utils::errorLogger($this->getDataFolder(), "ERROR", "InventoryInsideConfig at Core.php#364 was an object and not an array!\nPlase report this with an issue!"); + Utils::errorLogger($this->getDataFolder(), "ERROR", "InventoryInsideConfig at Core.php#476 was an object and not an array!\nPlase report this with an issue!"); $inventoryInsideConfig = []; } @@ -476,9 +503,9 @@ public function onPacket(DataPacketReceiveEvent $event) : void { } if ($a === 0) { - $log .= "§l" . $data->items[$localCount]->item->getCount() . "§r " . $data->items[$localCount]->item->getName(); + $log .= "§l" . $quota . "§r " . $data->items[$localCount]->item->getName(); } else { - $log .= " and §l" . $data->items[$localCount]->item->getCount() . "§r " . $data->items[$localCount]->item->getName(); + $log .= " and §l" . $quota . "§r " . $data->items[$localCount]->item->getName(); $config = $cm->get()->{$cm->getSingleKey()}; $inv = (array)json_decode(base64_decode($config->history)); $inv[] = $log; @@ -486,7 +513,7 @@ public function onPacket(DataPacketReceiveEvent $event) : void { $cm->set($cm->getSingleKey(), $config); } - if ($data->items[$localCount]->count > 1) { + if ($data->items[$localCount]->count > 0) { $this->trades->{$event->getOrigin()->getPlayer()->getName()}->count = $this->trades->{$event->getOrigin()->getPlayer()->getName()}->items[$localCount]->count - $quota; $data->items[$localCount]->item->setCount($data->count - $quota); foreach ($this->trades->{$event->getOrigin()->getPlayer()->getName()}->itemsAdd as $item) { @@ -553,16 +580,20 @@ public function onPacket(DataPacketReceiveEvent $event) : void { $this->trades->{$event->getOrigin()->getPlayer()->getName()} = null; } else { if (!$ic->specialstack) { + if ($count > $maxcount) { + $ic->specialstack = true; + } + if (@$this->trades->{$event->getOrigin()->getPlayer()->getName()} !== null && $action->getSource()->getContainerId() != 47) { // So we need to get the item from the slot if ($this->trades->{$event->getOrigin()->getPlayer()->getName()} instanceof \stdClass) { // If it's stdClass it's beautiful! if ($event->getOrigin()->getPlayer()->getInventory()->getSize() > $action->getSource()->getSlotId()) { - echo "\n\nTR\n\n"; $item = new \stdClass; $item->item = @$event->getOrigin()->getPlayer()->getInventory()->getItem($action->getSource()->getSlotId()) ?? VanillaItems::AIR(); $item->count = $action->getCount(); $this->trades->{$event->getOrigin()->getPlayer()->getName()}->items[] = $item; + $stackcount++; } } } @@ -578,14 +609,26 @@ public function onPacket(DataPacketReceiveEvent $event) : void { } } } elseif ($event->getPacket() instanceof ContainerClosePacket) { + $this->handle->{$event->getOrigin()->getPlayer()->getName()} = false; if (@$this->trades->{$event->getOrigin()->getPlayer()->getName()} !== null) { $this->trades->{$event->getOrigin()->getPlayer()->getName()} = null; } + } elseif ($event->getPacket() instanceof InventoryTransactionPacket && $event->getPacket()->trData instanceof UseItemOnEntityTransactionData && $event->getPacket()->trData->getActionType() === UseItemOnEntityTransactionData::ACTION_INTERACT) { + //$entity = $this->getServer()->getWorldManager()->findEntity($event->getPacket()->trData->getEntityRuntimeId()); + $entity = $this->getServer()->getWorldManager()->findEntity($event->getPacket()->trData->getActorRuntimeId()); + $player = $entity->getWorld()->getNearestEntity($event->getPacket()->trData->getPlayerPosition(), 2); + + if (($entity instanceof Shopkeeper || $entity instanceof HumanShopkeeper) && $player instanceof Player) { + if (!$this->handle->{$player->getName()}) { + $this->handle->{$player->getName()} = true; + $this->entityInteractionLoad($entity, $player); + } + } } } - public function onEntityDamage(Damage $event) { - if ($event->getEntity() instanceof Shopkeeper) { + public function onEntityDamage(Damage $event) : void { + if ($event->getEntity() instanceof Shopkeeper || $event->getEntity() instanceof HumanShopkeeper) { if ($event instanceof EntityDamageByEntityEvent) { if (@$event->getDamager() === null) { $event->cancel(); @@ -613,6 +656,43 @@ public function onEntityDamage(Damage $event) { } } + public function entityInteractionLoad(Entity $entity, Player $player) : void { + if (!$player->hasPermission("shopkeepers.shop.use")) { + $player->sendMessage(self::NOT_PERM_MSG); + } + if ($entity instanceof Shopkeeper || $entity instanceof HumanShopkeeper) { + $data = $entity->getConfig(); + $cm = new ConfigManager($data->author, $this->getDataFolder()); + $cm->setSingleKey($data->shop); + if (@$cm->get()->{$data->shop} === null) { + // Oh no, no config! + $player->sendMessage("§cSorry but this shop does not exists anymore!"); + // Remove the shop + $this->entities->remove($this->entities->generateEntityHash($entity)); + $entity->kill(); + return; + } elseif ($data->author === $player->getName() && !$player->isSneaking()) { + // Open the shopkeeper's ~~inventory~~ info page RN! + $menu = new ShopInfoMenu($cm, true); + $menu->create()->send($player); + } else { + // It's a shopkeeper! + // BEAUTIFUL! + // Now let's open the shopkeeper interface + // First, check if the shop is enabled + if (@$cm->get()->{$cm->getSingleKey()}->enabled) { + $manager = new Manager($cm); + $this->trades->{$player->getName()} = new \stdClass; + $this->trades->{$player->getName()}->config = $data; + $this->trades->{$player->getName()}->items = []; + $manager->send($player, $entity); + } else { + $event->getPlayer()->sendMessage("Sorry but this shop is §cdisabled§r!"); + } + } + } + } + // https://stackoverflow.com/questions/4356289/php-random-string-generator // I'm only lazy protected function generateRandomString($length = 10) : string { diff --git a/src/FoxWorn3365/Shopkeepers/EntityManager.php b/src/FoxWorn3365/Shopkeepers/EntityManager.php index b1571b1..696758b 100644 --- a/src/FoxWorn3365/Shopkeepers/EntityManager.php +++ b/src/FoxWorn3365/Shopkeepers/EntityManager.php @@ -23,7 +23,9 @@ use pocketmine\entity\Location; use pocketmine\Server; +use FoxWorn3365\Shopkeepers\utils\SkinUtils; use FoxWorn3365\Shopkeepers\entity\Shopkeeper; +use FoxWorn3365\Shopkeepers\entity\HumanShopkeeper; class EntityManager { protected string $base; @@ -49,7 +51,7 @@ protected function retrive() : void { } } - public function add(Shopkeeper $shop) : void { + public function add(Shopkeeper|HumanShopkeeper $shop) : void { // x, y, yw, z, pitch, world, (base64)data //$this->elements[] = "{$shop->getLocation()->getX()},{$shop->getLocation()->getY()},{$shop->getLocation()->getYaw()},{$shop->getLocation()->getZ()},{$shop->getLocation()->getPitch()},{$shop->getWorld()->getId()}," . base64_encode(json_encode($shop->getConfig())); $this->elements[] = $this->generateEntityHash($shop); @@ -61,17 +63,7 @@ public function get(int $slot) : ?string { return $this->elements[$slot]; } - public function loadAll(Server $server) : void { - foreach ($this->elements as $data) { - $data = explode(",", $data); - $location = new Location($data[0], $data[1], $data[3], $server->getWorldManager()->getWorld($data[5]), $data[2], $data[4]); - $shopkeeper = new Shopkeeper($location); - $shopkeeper->setConfig(json_decode(base64_decode($data[6]))); - $shopkeeper->spawnToAll(); - } - } - - public function generateEntityHash(Shopkeeper $shop) : string { + public function generateEntityHash(Shopkeeper|HumanShopkeeper $shop) : string { // GitHub user: "Use object plz" // Me: okok i'll do it AND NOW ITZ TIMW! return base64_encode(json_encode([ @@ -128,10 +120,15 @@ public function loadPlayer(Player $player) : void { } } - protected static function createEntity(string $rawdata, Server $server) : Shopkeeper { + protected static function createEntity(string $rawdata, Server $server) : Shopkeeper|HumanShopkeeper { $data = (object)json_decode(base64_decode($rawdata)); $location = new Location($data->x, $data->y, $data->z, $server->getWorldManager()->getWorld($data->world), $data->yaw, $data->pitch); - $entity = new Shopkeeper($location, json_decode(base64_decode($data->config)), $data->id); + if (SkinUtils::find(json_decode(base64_decode($data->config))->shop, json_decode(base64_decode($data->config))->author, $server->getPluginManager()->getPlugin("Shopkeepers")->getDataFolder())) { + $skin = SkinUtils::load(json_decode(base64_decode($data->config))->shop, json_decode(base64_decode($data->config))->author, $server->getPluginManager()->getPlugin("Shopkeepers")->getDataFolder()); + $entity = new HumanShopkeeper($location, $skin, json_decode(base64_decode($data->config)), $data->id); + } else { + $entity = new Shopkeeper($location, json_decode(base64_decode($data->config)), $data->id); + } $tags = json_decode(base64_decode($data->nametag)); $entity->setNameTag($tags->tag); $entity->setNameTagAlwaysVisible($tags->visible); diff --git a/src/FoxWorn3365/Shopkeepers/Menu/EditItemMenu.php b/src/FoxWorn3365/Shopkeepers/Menu/EditItemMenu.php index d5d2d7a..0e70860 100644 --- a/src/FoxWorn3365/Shopkeepers/Menu/EditItemMenu.php +++ b/src/FoxWorn3365/Shopkeepers/Menu/EditItemMenu.php @@ -21,6 +21,7 @@ namespace FoxWorn3365\Shopkeepers\Menu; use pocketmine\item\VanillaItems; +use pocketmine\block\VanillaBlocks; use pocketmine\item\Item; // Pocketmine Network part @@ -104,12 +105,6 @@ function edit() : InvMenu { $buyitem = SerializedItem::decode($buy); } - if ($buy2 === null) { - $buyitem2 = VanillaItems::AIR(); - } else { - $buyitem2 = SerializedItem::decode($buy2); - } - // Now load simple screen $this->menu->setName("Editing shop {$this->config->title}"); $this->menu->getInventory()->setItem(17, Factory::item(160, 14, "Delete the item")); @@ -126,9 +121,12 @@ function edit() : InvMenu { $this->menu->getInventory()->setItem(5, Factory::item(35, 13, "+1")); $this->menu->getInventory()->setItem(23, Factory::item(35, 14, "-1")); + //$this->menu->getInventory()->setItem(11, Factory::stringItem("minecraft:chest", "porcodio")); // Put data $this->menu->getInventory()->setItem(10, $buyitem); - $this->menu->getInventory()->setItem(11, $buyitem2); + if ($buy2 !== null && SerializedItem::decode($buy2)->getName() !== "Air") { + $this->menu->getInventory()->setItem(11, SerializedItem::decode($buy2)); + } $this->menu->getInventory()->setItem(14, $sellitem); $cm = $this->cm; @@ -177,6 +175,9 @@ function edit() : InvMenu { break; case 2: $item = $inventory->getItem(11); + if ($item->getName() === "Air") { + break; + } if ($item->getCount() + 1 > 64) { $transaction->getPlayer()->sendMessage("§cYou can't sell an item for more than 64 items!"); } else { @@ -187,6 +188,9 @@ function edit() : InvMenu { break; case 20: $item = $inventory->getItem(11); + if ($item->getName() === "Air") { + break; + } if ($item->getCount()-1 < 1) { $transaction->getPlayer()->sendMessage("§cYou can't sell an item for less than 1 item!"); } else { @@ -237,21 +241,27 @@ function edit() : InvMenu { break; case 11: $presence = $inventory->getItem(10); - if ($presence === VanillaItems::AIR() || $presence === null) { + if (@$presence->getName() == "Air" || $presence === null) { $transaction->getPlayer()->sendMessage("§4Sorry but you cannot cannot set the first buy item!"); + usleep(2500); + $inventory->clear(11); + $object->buy2 = null; break; } - if ($transaction->getItemClickedWith() !== null && $transaction->getItemClickedWith() != VanillaItems::AIR()) { + if ($transaction->getItemClickedWith() !== null && @$transaction->getItemClickedWith()->getVanillaName() != "Air") { // Let's change the object also in the inventory $inventory->clear(11); // Now let's decode the item $object->buy2 = SerializedItem::encode($transaction->getItemClickedWith()); usleep(5000); $inventory->setItem(11, $transaction->getItemClickedWith()); - } elseif ($transaction->getItemClickedWith() === null || $transaction->getItemClickedWith() === VanillaItems::AIR()) { + } elseif ($transaction->getItemClickedWith() === null || @$transaction->getItemClickedWith()->getVanillaName() == "Air") { $object->buy2 = null; + usleep(2500); $inventory->clear(11); + } else { + var_dump($transaction->getItemClickedWith()->getBlock()->getName()); } break; } diff --git a/src/FoxWorn3365/Shopkeepers/Menu/InfoMenu.php b/src/FoxWorn3365/Shopkeepers/Menu/InfoMenu.php index 6fd8f26..7b1050c 100644 --- a/src/FoxWorn3365/Shopkeepers/Menu/InfoMenu.php +++ b/src/FoxWorn3365/Shopkeepers/Menu/InfoMenu.php @@ -39,7 +39,7 @@ function __construct() { } public function create(Player $player, string $basedir) : InvMenu { - $this->menu->setName("Welcome to Shopkeepers"); + $this->menu->setName("§b§lShopkeepers"); $inventory = $this->menu->getInventory(); // Draw the upper and downer line diff --git a/src/FoxWorn3365/Shopkeepers/Menu/ShopConfigMenu.php b/src/FoxWorn3365/Shopkeepers/Menu/ShopConfigMenu.php index 0302f85..2eab368 100644 --- a/src/FoxWorn3365/Shopkeepers/Menu/ShopConfigMenu.php +++ b/src/FoxWorn3365/Shopkeepers/Menu/ShopConfigMenu.php @@ -47,7 +47,7 @@ function __construct(ConfigManager $cm) { } public function create() : InvMenu { - $this->menu->setName("Edit shop {$this->cm->getSingleKey()}"); + $this->menu->setName("§6§lConfig §r§l- §r{$this->cm->getSingleKey()}"); $inventory = $this->menu->getInventory(); // Draw the upper and downer line diff --git a/src/FoxWorn3365/Shopkeepers/Menu/ShopInfoMenu.php b/src/FoxWorn3365/Shopkeepers/Menu/ShopInfoMenu.php index 09ff71a..7520d58 100644 --- a/src/FoxWorn3365/Shopkeepers/Menu/ShopInfoMenu.php +++ b/src/FoxWorn3365/Shopkeepers/Menu/ShopInfoMenu.php @@ -30,8 +30,10 @@ use FoxWorn3365\Shopkeepers\utils\Factory; use FoxWorn3365\Shopkeepers\utils\NbtManager; use FoxWorn3365\Shopkeepers\ConfigManager; +use FoxWorn3365\Shopkeepers\utils\SkinUtils; use FoxWorn3365\Shopkeepers\entity\Shopkeeper; +use FoxWorn3365\Shopkeepers\entity\HumanShopkeeper; class ShopInfoMenu { protected InvMenu $menu; @@ -66,7 +68,7 @@ public function create() : InvMenu { if (!$this->config->admin) { $inventory->setItem(12, Factory::stringItem("minecraft:chest", "§l§9Inventory\n\n§r§oSee the inventory of the Shopkeeper")); } else { - $inventory->setItem(12, Factory::barrier("§l§cShop inventory\n\n§rDisabled!\n§oThis is an admin shop!")); // ID: -161 Meta: 0 BRID: 10390 + $inventory->setItem(12, Factory::barrier("§l§cShop inventory\n\n§rDisabled!\n§oThis is an admin shop!")); } // Shop discounts announcer for v1.0 @@ -92,7 +94,7 @@ public function create() : InvMenu { $config = $this->config; $local = $this->local; - $this->menu->setListener(function($transaction) use ($cm, $config, $local) { + $this->menu->setListener(function($transaction) use ($cm, &$config, $local) { $slot = $transaction->getAction()->getSlot(); switch ($slot) { case 10: @@ -138,10 +140,16 @@ public function create() : InvMenu { $shopdata = new \stdClass; $shopdata->author = $transaction->getPlayer()->getName(); $shopdata->shop = $cm->getSingleKey(); - $villager = new Shopkeeper($transaction->getPlayer()->getLocation()); + if (SkinUtils::find($cm->getSingleKey(), $transaction->getPlayer()->getName(), $transaction->getPlayer()->getServer()->getPluginManager()->getPlugin("Shopkeepers")->getDataFolder())) { + // Has a skin, let's summon an human entity after getting the skin + $skin = SkinUtils::get($cm->getSingleKey(), $transaction->getPlayer()->getName(), $transaction->getPlayer()->getServer()->getPluginManager()->getPlugin("Shopkeepers")->getDataFolder()); + $villager = new HumanShopkeeper($transaction->getPlayer()->getLocation(), $skin, $shopdata); + } else { + // A simple Shopkeeper, so summon a villager-like entity + $villager = new Shopkeeper($transaction->getPlayer()->getLocation(), $shopdata); + } $villager->setNameTag($cm->getSingleKey()); $villager->setNameTagAlwaysVisible($config->namevisible); - $villager->setConfig($shopdata); $villager->spawnToAll(); $transaction->getPlayer()->removeCurrentWindow(); break; @@ -159,6 +167,7 @@ public function create() : InvMenu { } $cm->set($cm->getSingleKey(), $config); + $transaction->getPlayer()->removeCurrentWindow(); break; case 24: if (!$transaction->getPlayer()->hasPermission("shopkeepers.shop.history")) { @@ -169,17 +178,18 @@ public function create() : InvMenu { $transaction->getPlayer()->removeCurrentWindow(); $transaction->getPlayer()->sendMessage("For the complete history please use /sk history [PAGE]"); - $message = "§lLast 20 trades for this Shopkeeper:\n"; $array = (array)json_decode(base64_decode($config->history)); if (count($array) > 20) { + $message = "§lLast 20 trades for this Shopkeeper:§r\n"; $count = count($array) - 20; } else { - $count = count($array); + $message = "§lLast " . count($array) . " trades for this Shopkeeper:§r\n"; + $count = 0; } for ($a = $count; $a < count($array); $a++) { $item = $array[$a]; - $message = "\n{$item}"; + $message .= "\n{$item}"; } $transaction->getPlayer()->sendMessage($message); break; diff --git a/src/FoxWorn3365/Shopkeepers/Menu/ShopInventoryMenu.php b/src/FoxWorn3365/Shopkeepers/Menu/ShopInventoryMenu.php index e93b620..5905b25 100644 --- a/src/FoxWorn3365/Shopkeepers/Menu/ShopInventoryMenu.php +++ b/src/FoxWorn3365/Shopkeepers/Menu/ShopInventoryMenu.php @@ -45,7 +45,7 @@ function __construct(ConfigManager $cm) { } public function create() : InvMenu { - $this->menu->setName("Editing {$this->cm->getSingleKey()}'s inventory..."); + $this->menu->setName("§c§lInventory §r§l- §r{$this->cm->getSingleKey()}"); $inventory = $this->menu->getInventory(); // First, let's import the inventory foreach ($this->config->inventory as $slot => $item) { diff --git a/src/FoxWorn3365/Shopkeepers/entity/HumanShopkeeper.php b/src/FoxWorn3365/Shopkeepers/entity/HumanShopkeeper.php new file mode 100644 index 0000000..5371371 --- /dev/null +++ b/src/FoxWorn3365/Shopkeepers/entity/HumanShopkeeper.php @@ -0,0 +1,75 @@ +setCanSaveWithChunk(false); + + $this->shopconfig = $generalizedConfig; + $this->customShopkeeperEntityId = $customId; + } + + public function getName(): string { + return "Shop Villager"; + } + + public static function getNetworkTypeId(): string { + return "minecraft:villager"; + } + + protected function getInitialSizeInfo() : EntitySizeInfo { + return new EntitySizeInfo(1.8, 0.6, 1.62); + } + + public function setConfig(object $config) : void { + $this->shopconfig = $config; + } + + public function getConfig() : object { + return $this->shopconfig; + } + + public function setCustomShopkeeperEntityId(int $id) : void { + $this->customShopkeeperEntityId = $id; + } + + public function getCustomShopkeeperEntityId() : ?int { + return $this->customShopkeeperEntityId; + } + + public function hasCustomShopkeeperEntityId() : bool { + if ($this->customShopkeeperEntityId === null) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/FoxWorn3365/Shopkeepers/entity/Shopkeeper.php b/src/FoxWorn3365/Shopkeepers/entity/Shopkeeper.php index 8c6c909..14b5255 100644 --- a/src/FoxWorn3365/Shopkeepers/entity/Shopkeeper.php +++ b/src/FoxWorn3365/Shopkeepers/entity/Shopkeeper.php @@ -21,6 +21,7 @@ namespace FoxWorn3365\Shopkeepers\entity; use pocketmine\entity\Villager; +use pocketmine\entity\Human; use pocketmine\entity\Location; use pocketmine\entity\EntitySizeInfo; diff --git a/src/FoxWorn3365/Shopkeepers/shop/Manager.php b/src/FoxWorn3365/Shopkeepers/shop/Manager.php index d104c8a..8f04ba7 100644 --- a/src/FoxWorn3365/Shopkeepers/shop/Manager.php +++ b/src/FoxWorn3365/Shopkeepers/shop/Manager.php @@ -30,13 +30,14 @@ use FoxWorn3365\Shopkeepers\utils\Utils; use FoxWorn3365\Shopkeepers\utils\ItemUtils; use FoxWorn3365\Shopkeepers\entity\Shopkeeper; +use FoxWorn3365\Shopkeepers\entity\HumanShopkeeper; class Manager { protected Shop $shop; protected ConfigManager $cm; protected object $config; protected Player $player; - protected Shopkeeper $entity; + protected Shopkeeper|HumanShopkeeper $entity; protected ElementContainer $container; function __construct(ConfigManager $cm) { @@ -45,15 +46,18 @@ function __construct(ConfigManager $cm) { $this->container = new ElementContainer(); } - public function send(Player $player, Shopkeeper $entity) : void { + public function send(Player $player, Shopkeeper|HumanShopkeeper $entity) : void { $this->player = $player; $this->entity = $entity; foreach ($this->config->items as $itemconfig) { if ($itemconfig === null) { continue; } - if (!(!empty($itemconfig->sell) && !empty($itemconfig->buy)) && gettype($this->config->inventory) !== 'array') { + if (gettype(@$this->config->inventory) !== 'array') { continue; + } else { + if (@$itemconfig->sell !== null && @$itemconfig->buy !== null) { + $this->container->add($itemconfig->sell, @$itemconfig->buy, $this->config->inventory, $this->config->admin, @$itemconfig->buy2); + } } - $this->container->add($itemconfig->sell, @$itemconfig->buy, $this->config->inventory, $this->config->admin, @$itemconfig->buy2); } $shop = new Shop($this->container->toNBT(), $player, $entity, $this->config->title); diff --git a/src/FoxWorn3365/Shopkeepers/shop/Shop.php b/src/FoxWorn3365/Shopkeepers/shop/Shop.php index f70bee8..b8983f0 100644 --- a/src/FoxWorn3365/Shopkeepers/shop/Shop.php +++ b/src/FoxWorn3365/Shopkeepers/shop/Shop.php @@ -34,14 +34,15 @@ use pocketmine\player\Player; use FoxWorn3365\Shopkeepers\entity\Shopkeeper; +use FoxWorn3365\Shopkeepers\entity\HumanShopkeeper; class Shop { protected ListTag $elements; protected Player $player; - protected Shopkeeper $shop; + protected Shopkeeper|HumanShopkeeper $shop; protected string $title; - function __construct(ListTag $elements, Player $player, Shopkeeper $shop, string $title = "Trade") { + function __construct(ListTag $elements, Player $player, Shopkeeper|HumanShopkeeper $shop, string $title = "Trade") { $this->elements = $elements; $this->player = $player; $this->shop = $shop; diff --git a/src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php b/src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php new file mode 100644 index 0000000..77f59e3 --- /dev/null +++ b/src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php @@ -0,0 +1,50 @@ + 64, + 64 * 64 * 4 => 64, + 128 * 128 * 4 => 128 + ]; + + public const SKIN_HEIGHT_MAP = [ + 64 * 32 * 4 => 32, + 64 * 64 * 4 => 64, + 128 * 128 * 4 => 128 + ]; + + public static function validateSize(int $size) { + if (!in_array($size, self::ACCEPTED_SKIN_SIZES)) { + throw new Exception("Invalid skin size"); + } + } +} \ No newline at end of file diff --git a/src/Himbeer/LibSkin/SkinConverter.php b/src/Himbeer/LibSkin/SkinConverter.php new file mode 100644 index 0000000..48ce54d --- /dev/null +++ b/src/Himbeer/LibSkin/SkinConverter.php @@ -0,0 +1,106 @@ +> 24) & 0xff; + $r = ($rgba >> 16) & 0xff; + $g = ($rgba >> 8) & 0xff; + $b = $rgba & 0xff; + $skinData .= chr($r) . chr($g) . chr($b) . chr(~(($a << 1) | ($a >> 6)) & 0xff); + } + } + if ($destroyImage) imagedestroy($image); + return $skinData; + } +} diff --git a/src/Himbeer/LibSkin/SkinGatherer.php b/src/Himbeer/LibSkin/SkinGatherer.php new file mode 100644 index 0000000..2bb8ee3 --- /dev/null +++ b/src/Himbeer/LibSkin/SkinGatherer.php @@ -0,0 +1,128 @@ +getOfflinePlayerData($playerName); + if ($namedTag === null) { + return null; + } + $skinTag = $namedTag->getCompoundTag("Skin"); + if ($skinTag === null) { + return null; + } + $skinData = $skinTag->getByteArray("Data"); + return $skinData; + } + + /** + * @param string $userName + * @param callable $callback A function which gets called when the request is finished, with the first argument being the skin data (or null) and the second the success/error state + * + * @throws Exception + */ + public static function getJavaEditionSkinData(string $userName, callable $callback) { + self::getJavaEditionSkinUrl($userName, function($skinUrl, $state) use ($callback) { + $callback($skinUrl === null ? null : SkinConverter::imageToSkinDataFromPngPath($skinUrl), $state); + }); + } + + /** + * @param string $userName Java Edition player name + * @param callable $callback A function which gets called when the request is finished, with the first argument being the URL (or null) and the second the success/error state + */ + public static function getJavaEditionSkinUrl(string $userName, callable $callback) { + self::asyncHttpGetRequest("https://api.mojang.com/users/profiles/minecraft/{$userName}", function(InternetRequestResult|null $response) use ($callback) { + if ($response === null) { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + return; + } + $body = $response->getBody(); + if ($body === "") { + if ($response->getCode() === 204) { // Status Code 204: No Content + $callback(null, self::MCJE_STATE_ERR_PLAYER_NOT_FOUND); + } else { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + } + return; + } + $data = json_decode($body, true); + if ($data === null || !isset($data["id"])) { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + return; + } + self::asyncHttpGetRequest("https://sessionserver.mojang.com/session/minecraft/profile/{$data["id"]}", function(InternetRequestResult|null $response) use ($callback) { + if ($response === null) { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + return; + } + $body = $response->getBody(); + if ($body === "") { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + return; + } + $data = json_decode($body, true); + if ($data === null || !isset($data["properties"][0]["name"]) || $data["properties"][0]["name"] !== "textures") { + if (isset($data["error"]) && $data["error"] === "TooManyRequestsException") { + $callback(null, self::MCJE_STATE_ERR_TOO_MANY_REQUESTS); + } else { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + } + return; + } + if (isset($data["properties"][0]["value"]) && ($b64dec = base64_decode($data["properties"][0]["value"]))) { + $textureInfo = json_decode($b64dec, true); + if ($textureInfo !== null && isset($textureInfo["textures"]["SKIN"]["url"])) { + $skinUrl = $textureInfo["textures"]["SKIN"]["url"]; + $callback($skinUrl, self::MCJE_STATE_SUCCESS); + return; + } + } + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + }); + }); + } + + /** + * @param string $url + * @param callable $callback + */ + private static function asyncHttpGetRequest(string $url, callable $callback) { + /** + * @param InternetRequestResult[] $results + * + * @return void + */ + $bulkCurlTaskCallback = function(array $results) use ($callback) { + if (isset($results[0]) && !$results[0] instanceof InternetException) { + $callback($results[0]); + } else { + $callback(null); + } + }; + $task = new BulkCurlTask([ + new BulkCurlTaskOperation($url) + ], $bulkCurlTaskCallback); + Server::getInstance()->getAsyncPool()->submitTask($task); + } +} \ No newline at end of file From f43c050da496cdbbd790687d1fd83e4a2c85ec68 Mon Sep 17 00:00:00 2001 From: FoxWorn3365 Date: Mon, 24 Jul 2023 00:56:43 +0200 Subject: [PATCH 05/11] Fixed the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7eb5d4f..2d331c9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ **⚠️ We are not in any way related to the [Shopkeepers plugin](https://dev.bukkit.org/projects/shopkeepers) for Bukkit!** ## Introduction video -Watch the video on YouTube +Watch the video on YouTube ## Features - Players can create theyr own Shopkeepers and manage it From 641af5d6284aed18919a3fdfe63433baf4097396 Mon Sep 17 00:00:00 2001 From: FoxWorn3365 Date: Mon, 24 Jul 2023 00:59:53 +0200 Subject: [PATCH 06/11] Usual version without virion --- src/muqsit/invmenu/InvMenu.php | 186 -------------- 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 | 64 ----- src/muqsit/invmenu/session/PlayerSession.php | 102 -------- .../network/NetworkStackLatencyEntry.php | 21 -- .../invmenu/session/network/PlayerNetwork.php | 230 ------------------ .../handler/ClosurePlayerNetworkHandler.php | 22 -- .../network/handler/PlayerNetworkHandler.php | 13 - .../handler/PlayerNetworkHandlerRegistry.php | 41 ---- .../DeterministicInvMenuTransaction.php | 60 ----- .../transaction/InvMenuTransaction.php | 43 ---- .../transaction/InvMenuTransactionResult.php | 48 ---- .../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 - 50 files changed, 2275 deletions(-) delete mode 100644 src/muqsit/invmenu/InvMenu.php delete mode 100644 src/muqsit/invmenu/InvMenuEventHandler.php delete mode 100644 src/muqsit/invmenu/InvMenuHandler.php delete mode 100644 src/muqsit/invmenu/inventory/InvMenuInventory.php delete mode 100644 src/muqsit/invmenu/inventory/SharedInvMenuSynchronizer.php delete mode 100644 src/muqsit/invmenu/inventory/SharedInventoryNotifier.php delete mode 100644 src/muqsit/invmenu/inventory/SharedInventorySynchronizer.php delete mode 100644 src/muqsit/invmenu/session/InvMenuInfo.php delete mode 100644 src/muqsit/invmenu/session/PlayerManager.php delete mode 100644 src/muqsit/invmenu/session/PlayerSession.php delete mode 100644 src/muqsit/invmenu/session/network/NetworkStackLatencyEntry.php delete mode 100644 src/muqsit/invmenu/session/network/PlayerNetwork.php delete mode 100644 src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php delete mode 100644 src/muqsit/invmenu/session/network/handler/PlayerNetworkHandler.php delete mode 100644 src/muqsit/invmenu/session/network/handler/PlayerNetworkHandlerRegistry.php delete mode 100644 src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php delete mode 100644 src/muqsit/invmenu/transaction/InvMenuTransaction.php delete mode 100644 src/muqsit/invmenu/transaction/InvMenuTransactionResult.php delete mode 100644 src/muqsit/invmenu/transaction/SimpleInvMenuTransaction.php delete mode 100644 src/muqsit/invmenu/type/ActorFixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/BlockActorFixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/BlockFixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/DoublePairableBlockActorFixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/FixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/InvMenuType.php delete mode 100644 src/muqsit/invmenu/type/InvMenuTypeIds.php delete mode 100644 src/muqsit/invmenu/type/InvMenuTypeRegistry.php delete mode 100644 src/muqsit/invmenu/type/graphic/ActorInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/BlockActorInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/BlockInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/InvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/MultiBlockInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/PositionedInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/ActorInvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/InvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/MultiInvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/util/InvMenuTypeBuilders.php delete mode 100644 src/muqsit/invmenu/type/util/InvMenuTypeHelper.php delete mode 100644 src/muqsit/invmenu/type/util/builder/ActorFixedInvMenuTypeBuilder.php delete mode 100644 src/muqsit/invmenu/type/util/builder/ActorInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php delete mode 100644 src/muqsit/invmenu/type/util/builder/BlockFixedInvMenuTypeBuilder.php delete mode 100644 src/muqsit/invmenu/type/util/builder/BlockInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php delete mode 100644 src/muqsit/invmenu/type/util/builder/FixedInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/InvMenuTypeBuilder.php diff --git a/src/muqsit/invmenu/InvMenu.php b/src/muqsit/invmenu/InvMenu.php deleted file mode 100644 index 6a804a5..0000000 --- a/src/muqsit/invmenu/InvMenu.php +++ /dev/null @@ -1,186 +0,0 @@ -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; - }; - } - - readonly public 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 __destruct(){ - $this->setInventory(null); - } - - /** - * @deprecated Access {@see InvMenu::$type} directly - * @return InvMenuType - */ - 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->network; - - // 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 deleted file mode 100644 index ea48f69..0000000 --- a/src/muqsit/invmenu/InvMenuEventHandler.php +++ /dev/null @@ -1,97 +0,0 @@ -getPacket(); - if($packet instanceof NetworkStackLatencyPacket){ - $player = $event->getOrigin()->getPlayer(); - if($player !== null){ - $this->player_manager->getNullable($player)?->network->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->network->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->post_transaction_callback; - if($network_stack_callback !== null){ - $network_stack_callbacks[] = $network_stack_callback; - } - if($result->cancelled){ - $event->cancel(); - break; - } - } - - if(count($network_stack_callbacks) > 0){ - $player_instance->network->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 deleted file mode 100644 index 9c5cc9a..0000000 --- a/src/muqsit/invmenu/InvMenuHandler.php +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index d13b1fa..0000000 --- a/src/muqsit/invmenu/inventory/InvMenuInventory.php +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 7609b8e..0000000 --- a/src/muqsit/invmenu/inventory/SharedInvMenuSynchronizer.php +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 0bf39d9..0000000 --- a/src/muqsit/invmenu/inventory/SharedInventoryNotifier.php +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index d41d992..0000000 --- a/src/muqsit/invmenu/inventory/SharedInventorySynchronizer.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index b9c1915..0000000 --- a/src/muqsit/invmenu/session/InvMenuInfo.php +++ /dev/null @@ -1,17 +0,0 @@ -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; - } - - /** - * @deprecated Access {@see PlayerManager::$network_handler_registry} directly - * @return PlayerNetworkHandlerRegistry - */ - 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 deleted file mode 100644 index d546e8d..0000000 --- a/src/muqsit/invmenu/session/PlayerSession.php +++ /dev/null @@ -1,102 +0,0 @@ -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; - }); - } - } - - /** - * @deprecated Access {@see PlayerSession::$network} directly - * @return PlayerNetwork - */ - 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 deleted file mode 100644 index 21eef61..0000000 --- a/src/muqsit/invmenu/session/network/NetworkStackLatencyEntry.php +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 0e905c3..0000000 --- a/src/muqsit/invmenu/session/network/PlayerNetwork.php +++ /dev/null @@ -1,230 +0,0 @@ -|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( - readonly private NetworkSession $network_session, - readonly 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; - } -} diff --git a/src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php b/src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php deleted file mode 100644 index 6b2314d..0000000 --- a/src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index fc38d87..0000000 --- a/src/muqsit/invmenu/session/network/handler/PlayerNetworkHandler.php +++ /dev/null @@ -1,13 +0,0 @@ -registerDefault(new ClosurePlayerNetworkHandler(static function(Closure $then) : NetworkStackLatencyEntry{ - $timestamp = mt_rand(); - return new NetworkStackLatencyEntry($timestamp * 1000000, $then, $timestamp); - })); - $this->register(DeviceOS::PLAYSTATION, new ClosurePlayerNetworkHandler(static function(Closure $then) : NetworkStackLatencyEntry{ - $timestamp = mt_rand(); - return new NetworkStackLatencyEntry($timestamp * 1000000, $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; - } -} diff --git a/src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php b/src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php deleted file mode 100644 index 8a0371e..0000000 --- a/src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 7db7694..0000000 --- a/src/muqsit/invmenu/transaction/InvMenuTransaction.php +++ /dev/null @@ -1,43 +0,0 @@ -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(Player) : void)|null $callback - * @return self - */ - public function then(?Closure $callback) : self{ - $this->post_transaction_callback = $callback; - return $this; - } - - /** - * @deprecated Access {@see InvMenuTransactionResult::$post_transaction_callback} directly - * @return (Closure(Player) : void)|null - */ - 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 deleted file mode 100644 index 59dcb48..0000000 --- a/src/muqsit/invmenu/transaction/SimpleInvMenuTransaction.php +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index d4b5f74..0000000 --- a/src/muqsit/invmenu/type/ActorFixedInvMenuType.php +++ /dev/null @@ -1,44 +0,0 @@ - $actor_metadata - * @param int $size - * @param InvMenuGraphicNetworkTranslator|null $network_translator - */ - public function __construct( - readonly private string $actor_identifier, - readonly private int $actor_runtime_identifier, - readonly private array $actor_metadata, - readonly private int $size, - readonly 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 deleted file mode 100644 index 8303f94..0000000 --- a/src/muqsit/invmenu/type/BlockActorFixedInvMenuType.php +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index e29d291..0000000 --- a/src/muqsit/invmenu/type/BlockFixedInvMenuType.php +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index fd8ccb6..0000000 --- a/src/muqsit/invmenu/type/DoublePairableBlockActorFixedInvMenuType.php +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 7f6a5cc..0000000 --- a/src/muqsit/invmenu/type/FixedInvMenuType.php +++ /dev/null @@ -1,18 +0,0 @@ - */ - 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 deleted file mode 100644 index 89c8e81..0000000 --- a/src/muqsit/invmenu/type/graphic/ActorInvMenuGraphic.php +++ /dev/null @@ -1,71 +0,0 @@ - $actor_metadata - * @param InvMenuGraphicNetworkTranslator|null $network_translator - * @param int $animation_duration - */ - public function __construct( - readonly private string $actor_identifier, - readonly private int $actor_runtime_identifier, - readonly private array $actor_metadata, - readonly private ?InvMenuGraphicNetworkTranslator $network_translator = null, - readonly 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 deleted file mode 100644 index 34ef106..0000000 --- a/src/muqsit/invmenu/type/graphic/BlockActorInvMenuGraphic.php +++ /dev/null @@ -1,70 +0,0 @@ -setString(Tile::TAG_ID, $tile_id); - if($name !== null){ - $tag->setString(Nameable::TAG_CUSTOM_NAME, $name); - } - return $tag; - } - - readonly private BlockInvMenuGraphic $block_graphic; - readonly private Vector3 $position; - readonly private CompoundTag $tile; - readonly private ?InvMenuGraphicNetworkTranslator $network_translator; - readonly 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 deleted file mode 100644 index 3a8003b..0000000 --- a/src/muqsit/invmenu/type/graphic/BlockInvMenuGraphic.php +++ /dev/null @@ -1,59 +0,0 @@ -position; - } - - public function send(Player $player, ?string $name) : void{ - $player->getNetworkSession()->sendDataPacket(UpdateBlockPacket::create(BlockPosition::fromVector3($this->position), TypeConverter::getInstance()->getBlockTranslator()->internalIdToNetworkId($this->block->getStateId()), 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 = TypeConverter::getInstance(); - $block = $world->getBlockAt($this->position->x, $this->position->y, $this->position->z); - $network->sendDataPacket(UpdateBlockPacket::create(BlockPosition::fromVector3($this->position), $runtime_block_mapping->getBlockTranslator()->internalIdToNetworkId($block->getStateId()), 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; - } -} diff --git a/src/muqsit/invmenu/type/graphic/InvMenuGraphic.php b/src/muqsit/invmenu/type/graphic/InvMenuGraphic.php deleted file mode 100644 index 8c263d7..0000000 --- a/src/muqsit/invmenu/type/graphic/InvMenuGraphic.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 8b3c83d..0000000 --- a/src/muqsit/invmenu/type/graphic/PositionedInvMenuGraphic.php +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 3406800..0000000 --- a/src/muqsit/invmenu/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 5ead44f..0000000 --- a/src/muqsit/invmenu/type/graphic/network/InvMenuGraphicNetworkTranslator.php +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index af389da..0000000 --- a/src/muqsit/invmenu/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index a749a14..0000000 --- a/src/muqsit/invmenu/type/util/InvMenuTypeBuilders.php +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index a43d4f5..0000000 --- a/src/muqsit/invmenu/type/util/builder/ActorFixedInvMenuTypeBuilder.php +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 1face00..0000000 --- a/src/muqsit/invmenu/type/util/builder/ActorInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index e062297..0000000 --- a/src/muqsit/invmenu/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index e3cb3fb..0000000 --- a/src/muqsit/invmenu/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index afef7a2..0000000 --- a/src/muqsit/invmenu/type/util/builder/BlockFixedInvMenuTypeBuilder.php +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 26ad398..0000000 --- a/src/muqsit/invmenu/type/util/builder/BlockInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index a66dd3a..0000000 --- a/src/muqsit/invmenu/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index a46a26a..0000000 --- a/src/muqsit/invmenu/type/util/builder/FixedInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 31b0d64..0000000 --- a/src/muqsit/invmenu/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index ae07040..0000000 --- a/src/muqsit/invmenu/type/util/builder/InvMenuTypeBuilder.php +++ /dev/null @@ -1,12 +0,0 @@ - Date: Mon, 24 Jul 2023 01:18:18 +0200 Subject: [PATCH 07/11] Fixed some errors --- plugin.yml | 2 +- src/FoxWorn3365/Shopkeepers/Core.php | 2 - .../Shopkeepers/entity/HumanShopkeeper.php | 5 +- .../Shopkeepers/utils/SkinUtils.php | 2 +- src/Himbeer/LibSkin/LibSkin.php | 34 ----- src/Himbeer/LibSkin/SkinConverter.php | 106 --------------- src/Himbeer/LibSkin/SkinGatherer.php | 128 ------------------ 7 files changed, 5 insertions(+), 274 deletions(-) delete mode 100644 src/Himbeer/LibSkin/LibSkin.php delete mode 100644 src/Himbeer/LibSkin/SkinConverter.php delete mode 100644 src/Himbeer/LibSkin/SkinGatherer.php diff --git a/plugin.yml b/plugin.yml index 6a64eb4..0c11d25 100644 --- a/plugin.yml +++ b/plugin.yml @@ -1,5 +1,5 @@ name: Shopkeepers -version: 0.9.1 +version: 1.0.0 api: 5.0.0 main: FoxWorn3365\Shopkeepers\Core diff --git a/src/FoxWorn3365/Shopkeepers/Core.php b/src/FoxWorn3365/Shopkeepers/Core.php index d2eb443..8606c75 100644 --- a/src/FoxWorn3365/Shopkeepers/Core.php +++ b/src/FoxWorn3365/Shopkeepers/Core.php @@ -87,8 +87,6 @@ class Core extends PluginBase implements Listener { protected string $defaultConfig = "IwojIFNob3BrZWVwZXJzIHYwLjkuMSBieSBGb3hXb3JtMzM2NQojIChDKSAyMDIzLW5vdyBGb3hXb3JuMzM2NQojIAojIFJlbGFzZWQgdW5kZXIgdGhlIEdQTC0zLjAgbGljZW5zZSAKIyBodHRwczovL2dpdGh1Yi5jb20vRm94V29ybjMzNjUvU2hvcGtlZXBlcnMvYmxvYi9tYWluL0xJQ0VOU0UKIwoKZW5hYmxlZDogdHJ1ZQoKIyBNYXggc2hvcGtlZXBlcidzIGVudGl0aWVzIGZvciBvbmUgcGxheWVyIChQRVIgU0hPUCkKbWF4LWVudGl0aWVzLWZvci1wbGF5ZXI6IDUKIyBQbGF5ZXIgdGhhdCBjYW4gYnlwYXNzIHRoaXMgbGltaXRhdGlvbgptYXgtZW50aXRpZXMtYnlwYXNzOgogIC0gWW91ck1pbmVjcmFmdFVzZXJuYW1lCgojIE1vZGVyYXRpb24gc2V0dGluZ3MgICAtIFRISVMgSVMgQSBDT05UQUlOIENPTkRJVElPTiBzbyBpZiB5b3Ugc2V0ICdwcm8nIGFsc28gbmFtZXMgbGlrZSAnYXByb24nLCAncHJvdG90eXB1cycsICdwcm90bycsICdwcm8nIGFuZCBpdCdzIGNhc2UgSU5TRU5TSVRJVkUKYmFubmVkLXNob3AtbmFtZXM6CiAgLSBoaXRsZXIKICAtIG5hemkKCiMgQmFubmVkIHNob3AgaXRlbSBuYW1lcyBzbyB0aGV5IGNhbid0IGJlIHNvbGQgb3IgYm91Z2h0CmJhbm5lZC1pdGVtLW5hbWVzOgogIC0gZGlhbW9uZF9heGUKCiMgQmFubmVkIGl0ZW0gSURzIApiYW5uZWQtaXRlbS1pZHM6CiAgLSAyNTU="; - protected float $server = 5.0; - protected const NOT_PERM_MSG = "§cSorry but you don't have permissions to use this command!\nPlease contact your server administrator"; public const AUTHOR = "FoxWorn3365"; public const VERSION = "1.0.0"; diff --git a/src/FoxWorn3365/Shopkeepers/entity/HumanShopkeeper.php b/src/FoxWorn3365/Shopkeepers/entity/HumanShopkeeper.php index 5371371..d12fa5a 100644 --- a/src/FoxWorn3365/Shopkeepers/entity/HumanShopkeeper.php +++ b/src/FoxWorn3365/Shopkeepers/entity/HumanShopkeeper.php @@ -14,8 +14,9 @@ * - Contribution guidelines: https://github.com/FoxWorn3365/Shopkeepers#contributing * - Author GitHub: https://github.com/FoxWorn3365 * - * Current file: /entity/Shopkeeper.php - * Description: The plugin's custom entity, useful because of the local data saved in Shopkeeper::$shopconfig + * Current file: /entity/HumanShopkeeper.php + * Description: The plugin's 2nd custom entity, useful because of the local data saved in HumanShopkeeper::$shopconfig. + * This entity, because it's an Human does support skin! */ namespace FoxWorn3365\Shopkeepers\entity; diff --git a/src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php b/src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php index 77f59e3..66ad454 100644 --- a/src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php +++ b/src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php @@ -16,7 +16,7 @@ * * Current file: /utils/Draw.php * STATIC CLASS - * Description: Implements simple drawing tools like Paint (NOT 3D!!!) + * Description: Implements an easy way to manage skins using Himbeer\LibSkin\SkinConverter */ namespace FoxWorn3365\Shopkeepers\utils; diff --git a/src/Himbeer/LibSkin/LibSkin.php b/src/Himbeer/LibSkin/LibSkin.php deleted file mode 100644 index 2ec4c36..0000000 --- a/src/Himbeer/LibSkin/LibSkin.php +++ /dev/null @@ -1,34 +0,0 @@ - 64, - 64 * 64 * 4 => 64, - 128 * 128 * 4 => 128 - ]; - - public const SKIN_HEIGHT_MAP = [ - 64 * 32 * 4 => 32, - 64 * 64 * 4 => 64, - 128 * 128 * 4 => 128 - ]; - - public static function validateSize(int $size) { - if (!in_array($size, self::ACCEPTED_SKIN_SIZES)) { - throw new Exception("Invalid skin size"); - } - } -} \ No newline at end of file diff --git a/src/Himbeer/LibSkin/SkinConverter.php b/src/Himbeer/LibSkin/SkinConverter.php deleted file mode 100644 index 48ce54d..0000000 --- a/src/Himbeer/LibSkin/SkinConverter.php +++ /dev/null @@ -1,106 +0,0 @@ -> 24) & 0xff; - $r = ($rgba >> 16) & 0xff; - $g = ($rgba >> 8) & 0xff; - $b = $rgba & 0xff; - $skinData .= chr($r) . chr($g) . chr($b) . chr(~(($a << 1) | ($a >> 6)) & 0xff); - } - } - if ($destroyImage) imagedestroy($image); - return $skinData; - } -} diff --git a/src/Himbeer/LibSkin/SkinGatherer.php b/src/Himbeer/LibSkin/SkinGatherer.php deleted file mode 100644 index 2bb8ee3..0000000 --- a/src/Himbeer/LibSkin/SkinGatherer.php +++ /dev/null @@ -1,128 +0,0 @@ -getOfflinePlayerData($playerName); - if ($namedTag === null) { - return null; - } - $skinTag = $namedTag->getCompoundTag("Skin"); - if ($skinTag === null) { - return null; - } - $skinData = $skinTag->getByteArray("Data"); - return $skinData; - } - - /** - * @param string $userName - * @param callable $callback A function which gets called when the request is finished, with the first argument being the skin data (or null) and the second the success/error state - * - * @throws Exception - */ - public static function getJavaEditionSkinData(string $userName, callable $callback) { - self::getJavaEditionSkinUrl($userName, function($skinUrl, $state) use ($callback) { - $callback($skinUrl === null ? null : SkinConverter::imageToSkinDataFromPngPath($skinUrl), $state); - }); - } - - /** - * @param string $userName Java Edition player name - * @param callable $callback A function which gets called when the request is finished, with the first argument being the URL (or null) and the second the success/error state - */ - public static function getJavaEditionSkinUrl(string $userName, callable $callback) { - self::asyncHttpGetRequest("https://api.mojang.com/users/profiles/minecraft/{$userName}", function(InternetRequestResult|null $response) use ($callback) { - if ($response === null) { - $callback(null, self::MCJE_STATE_ERR_UNKNOWN); - return; - } - $body = $response->getBody(); - if ($body === "") { - if ($response->getCode() === 204) { // Status Code 204: No Content - $callback(null, self::MCJE_STATE_ERR_PLAYER_NOT_FOUND); - } else { - $callback(null, self::MCJE_STATE_ERR_UNKNOWN); - } - return; - } - $data = json_decode($body, true); - if ($data === null || !isset($data["id"])) { - $callback(null, self::MCJE_STATE_ERR_UNKNOWN); - return; - } - self::asyncHttpGetRequest("https://sessionserver.mojang.com/session/minecraft/profile/{$data["id"]}", function(InternetRequestResult|null $response) use ($callback) { - if ($response === null) { - $callback(null, self::MCJE_STATE_ERR_UNKNOWN); - return; - } - $body = $response->getBody(); - if ($body === "") { - $callback(null, self::MCJE_STATE_ERR_UNKNOWN); - return; - } - $data = json_decode($body, true); - if ($data === null || !isset($data["properties"][0]["name"]) || $data["properties"][0]["name"] !== "textures") { - if (isset($data["error"]) && $data["error"] === "TooManyRequestsException") { - $callback(null, self::MCJE_STATE_ERR_TOO_MANY_REQUESTS); - } else { - $callback(null, self::MCJE_STATE_ERR_UNKNOWN); - } - return; - } - if (isset($data["properties"][0]["value"]) && ($b64dec = base64_decode($data["properties"][0]["value"]))) { - $textureInfo = json_decode($b64dec, true); - if ($textureInfo !== null && isset($textureInfo["textures"]["SKIN"]["url"])) { - $skinUrl = $textureInfo["textures"]["SKIN"]["url"]; - $callback($skinUrl, self::MCJE_STATE_SUCCESS); - return; - } - } - $callback(null, self::MCJE_STATE_ERR_UNKNOWN); - }); - }); - } - - /** - * @param string $url - * @param callable $callback - */ - private static function asyncHttpGetRequest(string $url, callable $callback) { - /** - * @param InternetRequestResult[] $results - * - * @return void - */ - $bulkCurlTaskCallback = function(array $results) use ($callback) { - if (isset($results[0]) && !$results[0] instanceof InternetException) { - $callback($results[0]); - } else { - $callback(null); - } - }; - $task = new BulkCurlTask([ - new BulkCurlTaskOperation($url) - ], $bulkCurlTaskCallback); - Server::getInstance()->getAsyncPool()->submitTask($task); - } -} \ No newline at end of file From bb814d500a0661e6527ea36f1d38cdc4db8b01d3 Mon Sep 17 00:00:00 2001 From: FoxWorn3365 Date: Mon, 24 Jul 2023 01:21:03 +0200 Subject: [PATCH 08/11] Fixed some mistakes --- README.md | 8 +++++--- plugin.yml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2aa4ea1..40ec92e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

--- -

Shopkeepers v1.0 for PocketMine-MP 4

+

Shopkeepers v1.0 for PocketMine-MP 5


**⚠️ We are not in any way related to the [Shopkeepers plugin](https://dev.bukkit.org/projects/shopkeepers) for Bukkit!** @@ -31,12 +31,14 @@ - Shopkeeper inventory for non-admin Shopkeepers - Hit prevention for shopkeepers - Easy configuration with in-game GUI +- Double trade supported +- Custom skin support ## 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**
-> 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]() +> 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) ## 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 6a64eb4..0c11d25 100644 --- a/plugin.yml +++ b/plugin.yml @@ -1,5 +1,5 @@ name: Shopkeepers -version: 0.9.1 +version: 1.0.0 api: 5.0.0 main: FoxWorn3365\Shopkeepers\Core From 5257aedb9d54c2311b2ab835a33c43e6052c8945 Mon Sep 17 00:00:00 2001 From: FoxWorn3365 Date: Mon, 24 Jul 2023 01:21:37 +0200 Subject: [PATCH 09/11] Theyr was funny --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 40ec92e..21857c0 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Watch the video on YouTube ## Features -- Players can create theyr own Shopkeepers and manage it +- Players can create their own Shopkeepers and manage it - Admin Shopkeepers - Vanilla trade page - Shopkeeper inventory for non-admin Shopkeepers From 0abc506ec40d39b622d7579eea2813cb157dd27b Mon Sep 17 00:00:00 2001 From: FoxWorn3365 Date: Mon, 24 Jul 2023 01:22:58 +0200 Subject: [PATCH 10/11] the
was important --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 21857c0..81f3bb6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +
Add Shopkeepers to your PocketMine-MP world! Allow the creation of simple barter stores between players or create adminshops!

From e78292da593ed55cad420e396943fd79ff78094a Mon Sep 17 00:00:00 2001 From: FoxWorn3365 Date: Mon, 24 Jul 2023 01:30:13 +0200 Subject: [PATCH 11/11] Just forgot to remove InvMenu LibSkin seems to not be an available virion so i need to include it in the code rn --- .poggit.yml | 5 +- src/Himbeer/LibSkin/LibSkin.php | 34 +++ src/Himbeer/LibSkin/SkinConverter.php | 106 ++++++++ src/Himbeer/LibSkin/SkinGatherer.php | 128 ++++++++++ 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 - 54 files changed, 269 insertions(+), 2251 deletions(-) create mode 100644 src/Himbeer/LibSkin/LibSkin.php create mode 100644 src/Himbeer/LibSkin/SkinConverter.php create mode 100644 src/Himbeer/LibSkin/SkinGatherer.php delete mode 100644 src/muqsit/invmenu/InvMenu.php delete mode 100644 src/muqsit/invmenu/InvMenuEventHandler.php delete mode 100644 src/muqsit/invmenu/InvMenuHandler.php delete mode 100644 src/muqsit/invmenu/inventory/InvMenuInventory.php delete mode 100644 src/muqsit/invmenu/inventory/SharedInvMenuSynchronizer.php delete mode 100644 src/muqsit/invmenu/inventory/SharedInventoryNotifier.php delete mode 100644 src/muqsit/invmenu/inventory/SharedInventorySynchronizer.php delete mode 100644 src/muqsit/invmenu/session/InvMenuInfo.php delete mode 100644 src/muqsit/invmenu/session/PlayerManager.php delete mode 100644 src/muqsit/invmenu/session/PlayerSession.php delete mode 100644 src/muqsit/invmenu/session/network/NetworkStackLatencyEntry.php delete mode 100644 src/muqsit/invmenu/session/network/PlayerNetwork.php delete mode 100644 src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php delete mode 100644 src/muqsit/invmenu/session/network/handler/PlayerNetworkHandler.php delete mode 100644 src/muqsit/invmenu/session/network/handler/PlayerNetworkHandlerRegistry.php delete mode 100644 src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php delete mode 100644 src/muqsit/invmenu/transaction/InvMenuTransaction.php delete mode 100644 src/muqsit/invmenu/transaction/InvMenuTransactionResult.php delete mode 100644 src/muqsit/invmenu/transaction/SimpleInvMenuTransaction.php delete mode 100644 src/muqsit/invmenu/type/ActorFixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/BlockActorFixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/BlockFixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/DoublePairableBlockActorFixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/FixedInvMenuType.php delete mode 100644 src/muqsit/invmenu/type/InvMenuType.php delete mode 100644 src/muqsit/invmenu/type/InvMenuTypeIds.php delete mode 100644 src/muqsit/invmenu/type/InvMenuTypeRegistry.php delete mode 100644 src/muqsit/invmenu/type/graphic/ActorInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/BlockActorInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/BlockInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/InvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/MultiBlockInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/PositionedInvMenuGraphic.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/ActorInvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/InvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/MultiInvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php delete mode 100644 src/muqsit/invmenu/type/util/InvMenuTypeBuilders.php delete mode 100644 src/muqsit/invmenu/type/util/InvMenuTypeHelper.php delete mode 100644 src/muqsit/invmenu/type/util/builder/ActorFixedInvMenuTypeBuilder.php delete mode 100644 src/muqsit/invmenu/type/util/builder/ActorInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php delete mode 100644 src/muqsit/invmenu/type/util/builder/BlockFixedInvMenuTypeBuilder.php delete mode 100644 src/muqsit/invmenu/type/util/builder/BlockInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php delete mode 100644 src/muqsit/invmenu/type/util/builder/FixedInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php delete mode 100644 src/muqsit/invmenu/type/util/builder/InvMenuTypeBuilder.php diff --git a/.poggit.yml b/.poggit.yml index cf90bc8..12b3e00 100644 --- a/.poggit.yml +++ b/.poggit.yml @@ -8,7 +8,4 @@ projects: libs: - src: muqsit/InvMenu/InvMenu version: ^4.6.1 - branch: pm5 - - src: Himbeer/LibSkin/LibSkin - version: ^2.0.0 - branch: master \ No newline at end of file + branch: pm5 \ No newline at end of file diff --git a/src/Himbeer/LibSkin/LibSkin.php b/src/Himbeer/LibSkin/LibSkin.php new file mode 100644 index 0000000..2ec4c36 --- /dev/null +++ b/src/Himbeer/LibSkin/LibSkin.php @@ -0,0 +1,34 @@ + 64, + 64 * 64 * 4 => 64, + 128 * 128 * 4 => 128 + ]; + + public const SKIN_HEIGHT_MAP = [ + 64 * 32 * 4 => 32, + 64 * 64 * 4 => 64, + 128 * 128 * 4 => 128 + ]; + + public static function validateSize(int $size) { + if (!in_array($size, self::ACCEPTED_SKIN_SIZES)) { + throw new Exception("Invalid skin size"); + } + } +} \ No newline at end of file diff --git a/src/Himbeer/LibSkin/SkinConverter.php b/src/Himbeer/LibSkin/SkinConverter.php new file mode 100644 index 0000000..48ce54d --- /dev/null +++ b/src/Himbeer/LibSkin/SkinConverter.php @@ -0,0 +1,106 @@ +> 24) & 0xff; + $r = ($rgba >> 16) & 0xff; + $g = ($rgba >> 8) & 0xff; + $b = $rgba & 0xff; + $skinData .= chr($r) . chr($g) . chr($b) . chr(~(($a << 1) | ($a >> 6)) & 0xff); + } + } + if ($destroyImage) imagedestroy($image); + return $skinData; + } +} diff --git a/src/Himbeer/LibSkin/SkinGatherer.php b/src/Himbeer/LibSkin/SkinGatherer.php new file mode 100644 index 0000000..2bb8ee3 --- /dev/null +++ b/src/Himbeer/LibSkin/SkinGatherer.php @@ -0,0 +1,128 @@ +getOfflinePlayerData($playerName); + if ($namedTag === null) { + return null; + } + $skinTag = $namedTag->getCompoundTag("Skin"); + if ($skinTag === null) { + return null; + } + $skinData = $skinTag->getByteArray("Data"); + return $skinData; + } + + /** + * @param string $userName + * @param callable $callback A function which gets called when the request is finished, with the first argument being the skin data (or null) and the second the success/error state + * + * @throws Exception + */ + public static function getJavaEditionSkinData(string $userName, callable $callback) { + self::getJavaEditionSkinUrl($userName, function($skinUrl, $state) use ($callback) { + $callback($skinUrl === null ? null : SkinConverter::imageToSkinDataFromPngPath($skinUrl), $state); + }); + } + + /** + * @param string $userName Java Edition player name + * @param callable $callback A function which gets called when the request is finished, with the first argument being the URL (or null) and the second the success/error state + */ + public static function getJavaEditionSkinUrl(string $userName, callable $callback) { + self::asyncHttpGetRequest("https://api.mojang.com/users/profiles/minecraft/{$userName}", function(InternetRequestResult|null $response) use ($callback) { + if ($response === null) { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + return; + } + $body = $response->getBody(); + if ($body === "") { + if ($response->getCode() === 204) { // Status Code 204: No Content + $callback(null, self::MCJE_STATE_ERR_PLAYER_NOT_FOUND); + } else { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + } + return; + } + $data = json_decode($body, true); + if ($data === null || !isset($data["id"])) { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + return; + } + self::asyncHttpGetRequest("https://sessionserver.mojang.com/session/minecraft/profile/{$data["id"]}", function(InternetRequestResult|null $response) use ($callback) { + if ($response === null) { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + return; + } + $body = $response->getBody(); + if ($body === "") { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + return; + } + $data = json_decode($body, true); + if ($data === null || !isset($data["properties"][0]["name"]) || $data["properties"][0]["name"] !== "textures") { + if (isset($data["error"]) && $data["error"] === "TooManyRequestsException") { + $callback(null, self::MCJE_STATE_ERR_TOO_MANY_REQUESTS); + } else { + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + } + return; + } + if (isset($data["properties"][0]["value"]) && ($b64dec = base64_decode($data["properties"][0]["value"]))) { + $textureInfo = json_decode($b64dec, true); + if ($textureInfo !== null && isset($textureInfo["textures"]["SKIN"]["url"])) { + $skinUrl = $textureInfo["textures"]["SKIN"]["url"]; + $callback($skinUrl, self::MCJE_STATE_SUCCESS); + return; + } + } + $callback(null, self::MCJE_STATE_ERR_UNKNOWN); + }); + }); + } + + /** + * @param string $url + * @param callable $callback + */ + private static function asyncHttpGetRequest(string $url, callable $callback) { + /** + * @param InternetRequestResult[] $results + * + * @return void + */ + $bulkCurlTaskCallback = function(array $results) use ($callback) { + if (isset($results[0]) && !$results[0] instanceof InternetException) { + $callback($results[0]); + } else { + $callback(null); + } + }; + $task = new BulkCurlTask([ + new BulkCurlTaskOperation($url) + ], $bulkCurlTaskCallback); + Server::getInstance()->getAsyncPool()->submitTask($task); + } +} \ No newline at end of file diff --git a/src/muqsit/invmenu/InvMenu.php b/src/muqsit/invmenu/InvMenu.php deleted file mode 100644 index 27b77a7..0000000 --- a/src/muqsit/invmenu/InvMenu.php +++ /dev/null @@ -1,178 +0,0 @@ -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 deleted file mode 100644 index 5de6be4..0000000 --- a/src/muqsit/invmenu/InvMenuEventHandler.php +++ /dev/null @@ -1,97 +0,0 @@ -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 deleted file mode 100644 index 9c5cc9a..0000000 --- a/src/muqsit/invmenu/InvMenuHandler.php +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index d13b1fa..0000000 --- a/src/muqsit/invmenu/inventory/InvMenuInventory.php +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 3f7aaf7..0000000 --- a/src/muqsit/invmenu/inventory/SharedInvMenuSynchronizer.php +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index dab01fe..0000000 --- a/src/muqsit/invmenu/inventory/SharedInventoryNotifier.php +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 1b6ba6d..0000000 --- a/src/muqsit/invmenu/inventory/SharedInventorySynchronizer.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index e5ff2c0..0000000 --- a/src/muqsit/invmenu/session/InvMenuInfo.php +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index 73440d7..0000000 --- a/src/muqsit/invmenu/session/PlayerSession.php +++ /dev/null @@ -1,98 +0,0 @@ -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 deleted file mode 100644 index 4405f58..0000000 --- a/src/muqsit/invmenu/session/network/NetworkStackLatencyEntry.php +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 40726ae..0000000 --- a/src/muqsit/invmenu/session/network/PlayerNetwork.php +++ /dev/null @@ -1,229 +0,0 @@ -|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 deleted file mode 100644 index 0e5e0c1..0000000 --- a/src/muqsit/invmenu/session/network/handler/ClosurePlayerNetworkHandler.php +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index fc38d87..0000000 --- a/src/muqsit/invmenu/session/network/handler/PlayerNetworkHandler.php +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 91c3a5e..0000000 --- a/src/muqsit/invmenu/transaction/DeterministicInvMenuTransaction.php +++ /dev/null @@ -1,60 +0,0 @@ -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 deleted file mode 100644 index 7db7694..0000000 --- a/src/muqsit/invmenu/transaction/InvMenuTransaction.php +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 97b8d4c..0000000 --- a/src/muqsit/invmenu/transaction/SimpleInvMenuTransaction.php +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index ec3212b..0000000 --- a/src/muqsit/invmenu/type/ActorFixedInvMenuType.php +++ /dev/null @@ -1,44 +0,0 @@ - $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 deleted file mode 100644 index 388e054..0000000 --- a/src/muqsit/invmenu/type/BlockActorFixedInvMenuType.php +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 1c19159..0000000 --- a/src/muqsit/invmenu/type/BlockFixedInvMenuType.php +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 8b36f70..0000000 --- a/src/muqsit/invmenu/type/DoublePairableBlockActorFixedInvMenuType.php +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 7f6a5cc..0000000 --- a/src/muqsit/invmenu/type/FixedInvMenuType.php +++ /dev/null @@ -1,18 +0,0 @@ - */ - 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 deleted file mode 100644 index 4af2950..0000000 --- a/src/muqsit/invmenu/type/graphic/ActorInvMenuGraphic.php +++ /dev/null @@ -1,71 +0,0 @@ - $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 deleted file mode 100644 index 0619065..0000000 --- a/src/muqsit/invmenu/type/graphic/BlockActorInvMenuGraphic.php +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 2e35029..0000000 --- a/src/muqsit/invmenu/type/graphic/BlockInvMenuGraphic.php +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 8c263d7..0000000 --- a/src/muqsit/invmenu/type/graphic/InvMenuGraphic.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 8b3c83d..0000000 --- a/src/muqsit/invmenu/type/graphic/PositionedInvMenuGraphic.php +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 3406800..0000000 --- a/src/muqsit/invmenu/type/graphic/network/BlockInvMenuGraphicNetworkTranslator.php +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index 5ead44f..0000000 --- a/src/muqsit/invmenu/type/graphic/network/InvMenuGraphicNetworkTranslator.php +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 81e7750..0000000 --- a/src/muqsit/invmenu/type/graphic/network/WindowTypeInvMenuGraphicNetworkTranslator.php +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index a749a14..0000000 --- a/src/muqsit/invmenu/type/util/InvMenuTypeBuilders.php +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index a43d4f5..0000000 --- a/src/muqsit/invmenu/type/util/builder/ActorFixedInvMenuTypeBuilder.php +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 1face00..0000000 --- a/src/muqsit/invmenu/type/util/builder/ActorInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index e062297..0000000 --- a/src/muqsit/invmenu/type/util/builder/AnimationDurationInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index e3cb3fb..0000000 --- a/src/muqsit/invmenu/type/util/builder/BlockActorFixedInvMenuTypeBuilder.php +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index afef7a2..0000000 --- a/src/muqsit/invmenu/type/util/builder/BlockFixedInvMenuTypeBuilder.php +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 26ad398..0000000 --- a/src/muqsit/invmenu/type/util/builder/BlockInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index a66dd3a..0000000 --- a/src/muqsit/invmenu/type/util/builder/DoublePairableBlockActorFixedInvMenuTypeBuilder.php +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index a46a26a..0000000 --- a/src/muqsit/invmenu/type/util/builder/FixedInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 31b0d64..0000000 --- a/src/muqsit/invmenu/type/util/builder/GraphicNetworkTranslatableInvMenuTypeBuilderTrait.php +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index ae07040..0000000 --- a/src/muqsit/invmenu/type/util/builder/InvMenuTypeBuilder.php +++ /dev/null @@ -1,12 +0,0 @@ -