diff --git a/composer.json b/composer.json index 9f634d6d9..444568035 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "pocketmine/bedrock-block-upgrade-schema": "dev-master@dev", "pocketmine/bedrock-data": "dev-modern-world-support@dev", "pocketmine/bedrock-item-upgrade-schema": "dev-master", - "pocketmine/bedrock-protocol": "~17.0.0+bedrock-1.19.50", + "pocketmine/bedrock-protocol": "~17.1.0+bedrock-1.19.50", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", "pocketmine/classloader": "^0.2.0", diff --git a/composer.lock b/composer.lock index 70ee2bb68..4f66fc6ee 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6c40129ef11a3cfc79081b8f53edda8e", + "content-hash": "614023b483badb5ec8720775dd2d3d12", "packages": [ { "name": "adhocore/json-comment", @@ -329,16 +329,16 @@ }, { "name": "pocketmine/bedrock-protocol", - "version": "17.0.0+bedrock-1.19.50", + "version": "17.1.0+bedrock-1.19.50", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "272e10197bb1603c0a81075bf5b9dae0d081a6e2" + "reference": "c572706cf5e3202718dd35a35dd30fe08cd671de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/272e10197bb1603c0a81075bf5b9dae0d081a6e2", - "reference": "272e10197bb1603c0a81075bf5b9dae0d081a6e2", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/c572706cf5e3202718dd35a35dd30fe08cd671de", + "reference": "c572706cf5e3202718dd35a35dd30fe08cd671de", "shasum": "" }, "require": { @@ -352,7 +352,7 @@ "ramsey/uuid": "^4.1" }, "require-dev": { - "phpstan/phpstan": "1.9.0", + "phpstan/phpstan": "1.9.3", "phpstan/phpstan-phpunit": "^1.0.0", "phpstan/phpstan-strict-rules": "^1.0.0", "phpunit/phpunit": "^9.5" @@ -370,9 +370,9 @@ "description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP", "support": { "issues": "https://github.com/pmmp/BedrockProtocol/issues", - "source": "https://github.com/pmmp/BedrockProtocol/tree/17.0.0+bedrock-1.19.50" + "source": "https://github.com/pmmp/BedrockProtocol/tree/17.1.0+bedrock-1.19.50" }, - "time": "2022-11-30T16:16:27+00:00" + "time": "2022-12-15T20:34:49+00:00" }, { "name": "pocketmine/binaryutils", diff --git a/src/entity/Human.php b/src/entity/Human.php index 793596765..bf68ebce2 100644 --- a/src/entity/Human.php +++ b/src/entity/Human.php @@ -378,7 +378,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ array_values($this->inventory->getContents()), array_values($this->armorInventory->getContents()), array_values($this->offHandInventory->getContents()), - ), function(Item $item) : bool{ return !$item->hasEnchantment(VanillaEnchantments::VANISHING()); }); + ), function(Item $item) : bool{ return !$item->hasEnchantment(VanillaEnchantments::VANISHING()) && !$item->keepOnDeath(); }); } public function saveNBT() : CompoundTag{ diff --git a/src/item/Item.php b/src/item/Item.php index 3ad52d8ad..9fe3ab25b 100644 --- a/src/item/Item.php +++ b/src/item/Item.php @@ -66,6 +66,8 @@ class Item implements \JsonSerializable{ public const TAG_DISPLAY_NAME = "Name"; public const TAG_DISPLAY_LORE = "Lore"; + public const TAG_KEEP_ON_DEATH = "minecraft:keep_on_death"; + private CompoundTag $nbt; protected int $count = 1; @@ -89,6 +91,8 @@ class Item implements \JsonSerializable{ */ protected array $canDestroy = []; + protected bool $keepOnDeath = false; + /** * Constructs a new Item type. This constructor should ONLY be used when constructing a new item TYPE to register * into the index. @@ -213,6 +217,17 @@ class Item implements \JsonSerializable{ } } + /** + * Returns whether players will retain this item on death. If a non-player dies it will be excluded from the drops. + */ + public function keepOnDeath() : bool{ + return $this->keepOnDeath; + } + + public function setKeepOnDeath(bool $keepOnDeath) : void{ + $this->keepOnDeath = $keepOnDeath; + } + /** * Returns whether this Item has a non-empty NBT. */ @@ -311,6 +326,8 @@ class Item implements \JsonSerializable{ $this->canDestroy[$entry->getValue()] = $entry->getValue(); } } + + $this->keepOnDeath = $tag->getByte(self::TAG_KEEP_ON_DEATH, 0) !== 0; } protected function serializeCompoundTag(CompoundTag $tag) : void{ @@ -368,6 +385,12 @@ class Item implements \JsonSerializable{ }else{ $tag->removeTag("CanDestroy"); } + + if($this->keepOnDeath){ + $tag->setByte(self::TAG_KEEP_ON_DEATH, 1); + }else{ + $tag->removeTag(self::TAG_KEEP_ON_DEATH); + } } public function getCount() : int{ diff --git a/src/player/Player.php b/src/player/Player.php index 391684c76..a09d50f71 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -131,6 +131,7 @@ use pocketmine\world\sound\Sound; use pocketmine\world\World; use Ramsey\Uuid\UuidInterface; use function abs; +use function array_filter; use function array_map; use function array_shift; use function assert; @@ -2280,10 +2281,11 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{ $this->getWorld()->dropItem($this->location, $item); } + $clearInventory = fn(Inventory $inventory) => $inventory->setContents(array_filter($inventory->getContents(), fn(Item $item) => $item->keepOnDeath())); $this->inventory->setHeldItemIndex(0); - $this->inventory->clearAll(); - $this->armorInventory->clearAll(); - $this->offHandInventory->clearAll(); + $clearInventory($this->inventory); + $clearInventory($this->armorInventory); + $clearInventory($this->offHandInventory); } if(!$ev->getKeepXp()){ diff --git a/src/resourcepacks/ResourcePackManager.php b/src/resourcepacks/ResourcePackManager.php index c8b2cb389..655f3a3a9 100644 --- a/src/resourcepacks/ResourcePackManager.php +++ b/src/resourcepacks/ResourcePackManager.php @@ -38,6 +38,8 @@ use function is_float; use function is_int; use function is_string; use function mkdir; +use function rtrim; +use function strlen; use function strtolower; use const DIRECTORY_SEPARATOR; @@ -93,41 +95,26 @@ class ResourcePackManager{ } $pack = (string) $pack; try{ - $packPath = Path::join($this->path, $pack); - if(!file_exists($packPath)){ - throw new ResourcePackException("File or directory not found"); - } - if(is_dir($packPath)){ - throw new ResourcePackException("Directory resource packs are unsupported"); - } + $newPack = $this->loadPackFromPath(Path::join($this->path, $pack)); - $newPack = null; - //Detect the type of resource pack. - $info = new \SplFileInfo($packPath); - switch($info->getExtension()){ - case "zip": - case "mcpack": - $newPack = new ZippedResourcePack($packPath); - break; - } + $this->resourcePacks[] = $newPack; + $index = strtolower($newPack->getPackId()); + $this->uuidList[$index] = $newPack; - if($newPack instanceof ResourcePack){ - $this->resourcePacks[] = $newPack; - $index = strtolower($newPack->getPackId()); - $this->uuidList[$index] = $newPack; - - $keyPath = Path::join($this->path, $pack . ".key"); - if(file_exists($keyPath)){ - try{ - $this->encryptionKeys[$index] = ErrorToExceptionHandler::trapAndRemoveFalse( - fn() => file_get_contents($keyPath) - ); - }catch(\ErrorException $e){ - throw new ResourcePackException("Could not read encryption key file: " . $e->getMessage(), 0, $e); - } + $keyPath = Path::join($this->path, $pack . ".key"); + if(file_exists($keyPath)){ + try{ + $key = ErrorToExceptionHandler::trapAndRemoveFalse( + fn() => file_get_contents($keyPath) + ); + }catch(\ErrorException $e){ + throw new ResourcePackException("Could not read encryption key file: " . $e->getMessage(), 0, $e); } - }else{ - throw new ResourcePackException("Format not recognized"); + $key = rtrim($key, "\r\n"); + if(strlen($key) !== 32){ + throw new ResourcePackException("Invalid encryption key length, must be exactly 32 bytes"); + } + $this->encryptionKeys[$index] = $key; } }catch(ResourcePackException $e){ $logger->critical("Could not load resource pack \"$pack\": " . $e->getMessage()); @@ -137,6 +124,25 @@ class ResourcePackManager{ $logger->debug("Successfully loaded " . count($this->resourcePacks) . " resource packs"); } + private function loadPackFromPath(string $packPath) : ResourcePack{ + if(!file_exists($packPath)){ + throw new ResourcePackException("File or directory not found"); + } + if(is_dir($packPath)){ + throw new ResourcePackException("Directory resource packs are unsupported"); + } + + //Detect the type of resource pack. + $info = new \SplFileInfo($packPath); + switch($info->getExtension()){ + case "zip": + case "mcpack": + return new ZippedResourcePack($packPath); + } + + throw new ResourcePackException("Format not recognized"); + } + /** * Returns the directory which resource packs are loaded from. */ @@ -159,6 +165,29 @@ class ResourcePackManager{ return $this->resourcePacks; } + /** + * Sets the resource packs to use. Packs earliest in the list will appear at the top of the stack (maximum + * priority), and later ones will appear below (lower priority), in the same manner as the Bedrock resource packs + * screen in-game. + * + * @param ResourcePack[] $resourceStack + * @phpstan-param list $resourceStack + */ + public function setResourceStack(array $resourceStack) : void{ + $uuidList = []; + $resourcePacks = []; + foreach($resourceStack as $pack){ + $uuid = strtolower($pack->getPackId()); + if(isset($uuidList[$uuid])){ + throw new \InvalidArgumentException("Cannot load two resource pack with the same UUID ($uuid)"); + } + $uuidList[$uuid] = $pack; + $resourcePacks[] = $pack; + } + $this->resourcePacks = $resourcePacks; + $this->uuidList = $uuidList; + } + /** * Returns the resource pack matching the specified UUID string, or null if the ID was not recognized. */ @@ -180,4 +209,23 @@ class ResourcePackManager{ public function getPackEncryptionKey(string $id) : ?string{ return $this->encryptionKeys[strtolower($id)] ?? null; } + + /** + * Sets the encryption key to use for decrypting the specified resource pack. The pack will **NOT** be decrypted by + * PocketMine-MP; the key is simply passed to the client to allow it to decrypt the pack after downloading it. + */ + public function setPackEncryptionKey(string $id, ?string $key) : void{ + $id = strtolower($id); + if($key === null){ + //allow deprovisioning keys for resource packs that have been removed + unset($this->encryptionKeys[$id]); + }elseif(isset($this->uuidList[$id])){ + if(strlen($key) !== 32){ + throw new \InvalidArgumentException("Encryption key must be exactly 32 bytes long"); + } + $this->encryptionKeys[$id] = $key; + }else{ + throw new \InvalidArgumentException("Unknown pack ID $id"); + } + } }