diff --git a/README.md b/README.md index 60984d2..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!

@@ -16,21 +16,23 @@

--- -

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!** ## Introduction video -Watch the video on YouTube +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 - 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! @@ -59,6 +61,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 +88,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 +97,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 +118,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: @@ -200,13 +231,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 +249,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 +270,8 @@ 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 +- [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/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 53e4931..0000000 Binary files a/plugin-banner-white.png and /dev/null differ diff --git a/plugin-banner.png b/plugin-banner.png deleted file mode 100644 index 83250a3..0000000 Binary files a/plugin-banner.png and /dev/null differ diff --git a/plugin.yml b/plugin.yml index ec4282f..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 @@ -10,7 +10,7 @@ description: Add shopkeepers to your PocketMine server! commands: shopkeepers: description: The main shopkeepers command - usage: "/shopkeepers [list|create|summon|rename|edit|info] " + usage: "/shopkeepers [list|create|summon|rename|edit|info|history] " aliases: - sk - shopk @@ -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..8606c75 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; @@ -49,6 +50,8 @@ 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; +use pocketmine\network\mcpe\protocol\InventoryTransactionPacket; // Custom use FoxWorn3365\Shopkeepers\Menu\InfoMenu; @@ -57,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; @@ -68,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; @@ -77,19 +83,19 @@ class Core extends PluginBase implements Listener { protected EntityManager $entities; protected object $trades; protected object $tradeQueue; + protected object $handle; 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 = "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 { @@ -101,6 +107,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()); @@ -131,42 +138,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; - $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 @@ -196,6 +179,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 @@ -229,8 +213,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) { @@ -302,7 +286,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(); @@ -311,6 +302,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); @@ -348,13 +373,21 @@ 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 = ""; + $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) { @@ -365,7 +398,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 +426,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) { @@ -423,7 +460,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 = []; } @@ -431,7 +468,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 +477,121 @@ 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" . $quota . "§r " . $data->items[$localCount]->item->getName(); + } else { + $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; + $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 > 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) { + 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 ($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()) { - $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(); + $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++; } } } @@ -539,14 +607,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(); @@ -574,6 +654,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 892d3d2..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 @@ -84,8 +85,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!'); @@ -111,13 +113,21 @@ 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")); + //$this->menu->getInventory()->setItem(11, Factory::stringItem("minecraft:chest", "porcodio")); // Put data $this->menu->getInventory()->setItem(10, $buyitem); - $this->menu->getInventory()->setItem(13, $sellitem); + 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; $config = $this->config; @@ -163,23 +173,49 @@ function edit() : InvMenu { $object->buy = SerializedItem::encode($item); } break; - case 4: - $item = $inventory->getItem(13); + 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 { + $item->setCount($item->getCount()+1); + $inventory->setItem(11, $item); + $object->buy2 = SerializedItem::encode($item); + } + 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 { + $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 +229,39 @@ 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->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()->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()->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/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/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 8f9041a..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; @@ -49,7 +51,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,38 +62,39 @@ 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!")); } // 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; $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: @@ -137,13 +140,59 @@ 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; + 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); + $transaction->getPlayer()->removeCurrentWindow(); + 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]"); + $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 { + $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}"; + } + $transaction->getPlayer()->sendMessage($message); + break; } return $transaction->discard(); }); 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..d12fa5a --- /dev/null +++ b/src/FoxWorn3365/Shopkeepers/entity/HumanShopkeeper.php @@ -0,0 +1,76 @@ +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/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..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); } $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/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/SkinUtils.php b/src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php new file mode 100644 index 0000000..66ad454 --- /dev/null +++ b/src/FoxWorn3365/Shopkeepers/utils/SkinUtils.php @@ -0,0 +1,50 @@ +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/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