diff --git a/composer.json b/composer.json index bbcbe7314..f8d060bcd 100644 --- a/composer.json +++ b/composer.json @@ -33,15 +33,15 @@ "composer-runtime-api": "^2.0", "adhocore/json-comment": "~1.2.0", "netresearch/jsonmapper": "~v5.0.0", - "pocketmine/bedrock-block-upgrade-schema": "~5.0.0+bedrock-1.21.40", - "pocketmine/bedrock-data": "~2.15.0+bedrock-1.21.50", + "pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60", + "pocketmine/bedrock-data": "~4.0.0+bedrock-1.21.60", "pocketmine/bedrock-item-upgrade-schema": "~1.14.0+bedrock-1.21.50", - "pocketmine/bedrock-protocol": "~35.0.0+bedrock-1.21.50", + "pocketmine/bedrock-protocol": "~36.0.0+bedrock-1.21.60", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", "pocketmine/color": "^0.3.0", "pocketmine/errorhandler": "^0.7.0", - "pocketmine/locale-data": "~2.22.0", + "pocketmine/locale-data": "~2.24.0", "pocketmine/log": "^0.4.0", "pocketmine/math": "~1.0.0", "pocketmine/nbt": "~1.0.0", diff --git a/composer.lock b/composer.lock index 52490968f..cc7f1f716 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": "a6aa0a34a230d325528d2e11f1439057", + "content-hash": "af7547291a131bfac6d7087957601325", "packages": [ { "name": "adhocore/json-comment", @@ -178,16 +178,16 @@ }, { "name": "pocketmine/bedrock-block-upgrade-schema", - "version": "5.0.0", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockBlockUpgradeSchema.git", - "reference": "20dd5c11e9915bacea4fe2cf649e1d23697a6e52" + "reference": "2218512e4b91f5bfd09ef55f7a4c4b04e169e41a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/20dd5c11e9915bacea4fe2cf649e1d23697a6e52", - "reference": "20dd5c11e9915bacea4fe2cf649e1d23697a6e52", + "url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/2218512e4b91f5bfd09ef55f7a4c4b04e169e41a", + "reference": "2218512e4b91f5bfd09ef55f7a4c4b04e169e41a", "shasum": "" }, "type": "library", @@ -198,22 +198,22 @@ "description": "Schemas describing how to upgrade saved block data in older Minecraft: Bedrock Edition world saves", "support": { "issues": "https://github.com/pmmp/BedrockBlockUpgradeSchema/issues", - "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/5.0.0" + "source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/5.1.0" }, - "time": "2024-11-03T14:13:50+00:00" + "time": "2025-02-11T17:41:44+00:00" }, { "name": "pocketmine/bedrock-data", - "version": "2.15.0+bedrock-1.21.50", + "version": "4.0.0+bedrock-1.21.60", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockData.git", - "reference": "6e819f36d781866ce63d2406be2ce7f2d1afd9ad" + "reference": "2e5f16ec2facac653f3f894f22eb831d880ba98e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/6e819f36d781866ce63d2406be2ce7f2d1afd9ad", - "reference": "6e819f36d781866ce63d2406be2ce7f2d1afd9ad", + "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/2e5f16ec2facac653f3f894f22eb831d880ba98e", + "reference": "2e5f16ec2facac653f3f894f22eb831d880ba98e", "shasum": "" }, "type": "library", @@ -224,9 +224,9 @@ "description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/BedrockData/issues", - "source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.50" + "source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.60" }, - "time": "2024-12-04T12:59:12+00:00" + "time": "2025-02-16T15:56:56+00:00" }, { "name": "pocketmine/bedrock-item-upgrade-schema", @@ -256,16 +256,16 @@ }, { "name": "pocketmine/bedrock-protocol", - "version": "35.0.3+bedrock-1.21.50", + "version": "36.0.0+bedrock-1.21.60", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "c4d62581cb62d29ec426914c6b4d7e0ff835da9c" + "reference": "2057de319c5c551001c2a544e08d1bc7727d9963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/c4d62581cb62d29ec426914c6b4d7e0ff835da9c", - "reference": "c4d62581cb62d29ec426914c6b4d7e0ff835da9c", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/2057de319c5c551001c2a544e08d1bc7727d9963", + "reference": "2057de319c5c551001c2a544e08d1bc7727d9963", "shasum": "" }, "require": { @@ -296,9 +296,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/35.0.3+bedrock-1.21.50" + "source": "https://github.com/pmmp/BedrockProtocol/tree/36.0.0+bedrock-1.21.60" }, - "time": "2025-01-07T23:06:29+00:00" + "time": "2025-02-16T15:59:08+00:00" }, { "name": "pocketmine/binaryutils", @@ -471,16 +471,16 @@ }, { "name": "pocketmine/locale-data", - "version": "2.22.1", + "version": "2.24.0", "source": { "type": "git", "url": "https://github.com/pmmp/Language.git", - "reference": "fa4e377c437391cfcfdedd08eea3a848eabd1b49" + "reference": "6ec5e92c77a2102b2692763733e4763012facae9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/Language/zipball/fa4e377c437391cfcfdedd08eea3a848eabd1b49", - "reference": "fa4e377c437391cfcfdedd08eea3a848eabd1b49", + "url": "https://api.github.com/repos/pmmp/Language/zipball/6ec5e92c77a2102b2692763733e4763012facae9", + "reference": "6ec5e92c77a2102b2692763733e4763012facae9", "shasum": "" }, "type": "library", @@ -488,9 +488,9 @@ "description": "Language resources used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/Language/issues", - "source": "https://github.com/pmmp/Language/tree/2.22.1" + "source": "https://github.com/pmmp/Language/tree/2.24.0" }, - "time": "2024-12-06T14:44:17+00:00" + "time": "2025-02-16T20:46:34+00:00" }, { "name": "pocketmine/log", @@ -2967,5 +2967,5 @@ "platform-overrides": { "php": "8.1.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/data/bedrock/BedrockDataFiles.php b/src/data/bedrock/BedrockDataFiles.php index 3341c0503..6176eee34 100644 --- a/src/data/bedrock/BedrockDataFiles.php +++ b/src/data/bedrock/BedrockDataFiles.php @@ -39,12 +39,11 @@ final class BedrockDataFiles{ public const BLOCK_STATE_META_MAP_JSON = BEDROCK_DATA_PATH . '/block_state_meta_map.json'; public const CANONICAL_BLOCK_STATES_NBT = BEDROCK_DATA_PATH . '/canonical_block_states.nbt'; public const COMMAND_ARG_TYPES_JSON = BEDROCK_DATA_PATH . '/command_arg_types.json'; - public const CREATIVEITEMS_JSON = BEDROCK_DATA_PATH . '/creativeitems.json'; public const ENTITY_ID_MAP_JSON = BEDROCK_DATA_PATH . '/entity_id_map.json'; public const ENTITY_IDENTIFIERS_NBT = BEDROCK_DATA_PATH . '/entity_identifiers.nbt'; + public const ENUMS_PY = BEDROCK_DATA_PATH . '/enums.py'; public const ITEM_TAGS_JSON = BEDROCK_DATA_PATH . '/item_tags.json'; public const LEVEL_SOUND_ID_MAP_JSON = BEDROCK_DATA_PATH . '/level_sound_id_map.json'; - public const PARTICLE_ID_MAP_JSON = BEDROCK_DATA_PATH . '/particle_id_map.json'; public const PROTOCOL_INFO_JSON = BEDROCK_DATA_PATH . '/protocol_info.json'; public const R12_TO_CURRENT_BLOCK_MAP_BIN = BEDROCK_DATA_PATH . '/r12_to_current_block_map.bin'; public const R16_TO_CURRENT_ITEM_MAP_JSON = BEDROCK_DATA_PATH . '/r16_to_current_item_map.json'; diff --git a/src/data/bedrock/block/BlockStateData.php b/src/data/bedrock/block/BlockStateData.php index 6624c4ae0..e90410ac7 100644 --- a/src/data/bedrock/block/BlockStateData.php +++ b/src/data/bedrock/block/BlockStateData.php @@ -45,8 +45,8 @@ final class BlockStateData{ public const CURRENT_VERSION = (1 << 24) | //major (21 << 16) | //minor - (40 << 8) | //patch - (1); //revision + (60 << 8) | //patch + (33); //revision public const TAG_NAME = "name"; public const TAG_STATES = "states"; diff --git a/src/data/bedrock/block/BlockStateNames.php b/src/data/bedrock/block/BlockStateNames.php index 68a3e1b1d..9fed77e4a 100644 --- a/src/data/bedrock/block/BlockStateNames.php +++ b/src/data/bedrock/block/BlockStateNames.php @@ -59,6 +59,7 @@ final class BlockStateNames{ public const COVERED_BIT = "covered_bit"; public const CRACKED_STATE = "cracked_state"; public const CRAFTING = "crafting"; + public const CREAKING_HEART_STATE = "creaking_heart_state"; public const DEAD_BIT = "dead_bit"; public const DEPRECATED = "deprecated"; public const DIRECTION = "direction"; diff --git a/src/data/bedrock/block/BlockStateStringValues.php b/src/data/bedrock/block/BlockStateStringValues.php index e8171e4da..e6ae30e64 100644 --- a/src/data/bedrock/block/BlockStateStringValues.php +++ b/src/data/bedrock/block/BlockStateStringValues.php @@ -56,6 +56,10 @@ final class BlockStateStringValues{ public const CRACKED_STATE_MAX_CRACKED = "max_cracked"; public const CRACKED_STATE_NO_CRACKS = "no_cracks"; + public const CREAKING_HEART_STATE_AWAKE = "awake"; + public const CREAKING_HEART_STATE_DORMANT = "dormant"; + public const CREAKING_HEART_STATE_UPROOTED = "uprooted"; + public const DRIPSTONE_THICKNESS_BASE = "base"; public const DRIPSTONE_THICKNESS_FRUSTUM = "frustum"; public const DRIPSTONE_THICKNESS_MERGE = "merge"; diff --git a/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php b/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php index fda0455aa..342f31490 100644 --- a/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php +++ b/src/data/bedrock/block/convert/BlockStateDeserializerHelper.php @@ -131,7 +131,7 @@ final class BlockStateDeserializerHelper{ //TODO: check if these need any special treatment to get the appropriate data to both halves of the door return $block ->setTop($in->readBool(BlockStateNames::UPPER_BLOCK_BIT)) - ->setFacing(Facing::rotateY($in->readLegacyHorizontalFacing(), false)) + ->setFacing($in->readCardinalHorizontalFacing()) ->setHingeRight($in->readBool(BlockStateNames::DOOR_HINGE_BIT)) ->setOpen($in->readBool(BlockStateNames::OPEN_BIT)); } @@ -145,7 +145,7 @@ final class BlockStateDeserializerHelper{ /** @throws BlockStateDeserializeException */ public static function decodeFenceGate(FenceGate $block, BlockStateReader $in) : FenceGate{ return $block - ->setFacing($in->readLegacyHorizontalFacing()) + ->setFacing($in->readCardinalHorizontalFacing()) ->setInWall($in->readBool(BlockStateNames::IN_WALL_BIT)) ->setOpen($in->readBool(BlockStateNames::OPEN_BIT)); } diff --git a/src/data/bedrock/block/convert/BlockStateSerializerHelper.php b/src/data/bedrock/block/convert/BlockStateSerializerHelper.php index 3e2215746..fa0591669 100644 --- a/src/data/bedrock/block/convert/BlockStateSerializerHelper.php +++ b/src/data/bedrock/block/convert/BlockStateSerializerHelper.php @@ -100,7 +100,7 @@ final class BlockStateSerializerHelper{ public static function encodeDoor(Door $block, Writer $out) : Writer{ return $out ->writeBool(BlockStateNames::UPPER_BLOCK_BIT, $block->isTop()) - ->writeLegacyHorizontalFacing(Facing::rotateY($block->getFacing(), true)) + ->writeCardinalHorizontalFacing($block->getFacing()) ->writeBool(BlockStateNames::DOOR_HINGE_BIT, $block->isHingeRight()) ->writeBool(BlockStateNames::OPEN_BIT, $block->isOpen()); } @@ -112,7 +112,7 @@ final class BlockStateSerializerHelper{ public static function encodeFenceGate(FenceGate $block, Writer $out) : Writer{ return $out - ->writeLegacyHorizontalFacing($block->getFacing()) + ->writeCardinalHorizontalFacing($block->getFacing()) ->writeBool(BlockStateNames::IN_WALL_BIT, $block->isInWall()) ->writeBool(BlockStateNames::OPEN_BIT, $block->isOpen()); } diff --git a/src/entity/Human.php b/src/entity/Human.php index 833196651..c94b76097 100644 --- a/src/entity/Human.php +++ b/src/entity/Human.php @@ -513,6 +513,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{ AbilitiesLayer::LAYER_BASE, array_fill(0, AbilitiesLayer::NUMBER_OF_ABILITIES, false), 0.0, + 0.0, 0.0 ) ])), diff --git a/src/inventory/CreativeCategory.php b/src/inventory/CreativeCategory.php new file mode 100644 index 000000000..bede48b28 --- /dev/null +++ b/src/inventory/CreativeCategory.php @@ -0,0 +1,34 @@ +getText()) : strlen($name); + if($nameLength === 0){ + throw new \InvalidArgumentException("Creative group name cannot be empty"); + } + } + + public function getName() : Translatable|string{ return $this->name; } + + public function getIcon() : Item{ return clone $this->icon; } +} diff --git a/src/inventory/CreativeInventory.php b/src/inventory/CreativeInventory.php index 57e5cbb4e..77eda8138 100644 --- a/src/inventory/CreativeInventory.php +++ b/src/inventory/CreativeInventory.php @@ -24,21 +24,23 @@ declare(strict_types=1); namespace pocketmine\inventory; use pocketmine\crafting\CraftingManagerFromDataHelper; -use pocketmine\crafting\json\ItemStackData; -use pocketmine\data\bedrock\BedrockDataFiles; +use pocketmine\inventory\json\CreativeGroupData; use pocketmine\item\Item; +use pocketmine\lang\Translatable; use pocketmine\utils\DestructorCallbackTrait; use pocketmine\utils\ObjectSet; use pocketmine\utils\SingletonTrait; -use pocketmine\utils\Utils; +use Symfony\Component\Filesystem\Path; +use function array_filter; +use function array_map; final class CreativeInventory{ use SingletonTrait; use DestructorCallbackTrait; /** - * @var Item[] - * @phpstan-var array + * @var CreativeInventoryEntry[] + * @phpstan-var array */ private array $creative = []; @@ -47,17 +49,32 @@ final class CreativeInventory{ private function __construct(){ $this->contentChangedCallbacks = new ObjectSet(); - $creativeItems = CraftingManagerFromDataHelper::loadJsonArrayOfObjectsFile( - BedrockDataFiles::CREATIVEITEMS_JSON, - ItemStackData::class - ); - foreach($creativeItems as $data){ - $item = CraftingManagerFromDataHelper::deserializeItemStack($data); - if($item === null){ - //unknown item - continue; + + foreach([ + "construction" => CreativeCategory::CONSTRUCTION, + "nature" => CreativeCategory::NATURE, + "equipment" => CreativeCategory::EQUIPMENT, + "items" => CreativeCategory::ITEMS, + ] as $categoryId => $categoryEnum){ + $groups = CraftingManagerFromDataHelper::loadJsonArrayOfObjectsFile( + Path::join(\pocketmine\BEDROCK_DATA_PATH, "creative", $categoryId . ".json"), + CreativeGroupData::class + ); + + foreach($groups as $groupData){ + $icon = $groupData->group_icon === null ? null : CraftingManagerFromDataHelper::deserializeItemStack($groupData->group_icon); + + $group = $icon === null ? null : new CreativeGroup( + new Translatable($groupData->group_name), + $icon + ); + + $items = array_filter(array_map(static fn($itemStack) => CraftingManagerFromDataHelper::deserializeItemStack($itemStack), $groupData->items)); + + foreach($items as $item){ + $this->add($item, $categoryEnum, $group); + } } - $this->add($item); } } @@ -75,16 +92,28 @@ final class CreativeInventory{ * @phpstan-return array */ public function getAll() : array{ - return Utils::cloneObjectArray($this->creative); + return array_map(fn(CreativeInventoryEntry $entry) => $entry->getItem(), $this->creative); + } + + /** + * @return CreativeInventoryEntry[] + * @phpstan-return array + */ + public function getAllEntries() : array{ + return $this->creative; } public function getItem(int $index) : ?Item{ - return isset($this->creative[$index]) ? clone $this->creative[$index] : null; + return $this->getEntry($index)?->getItem(); + } + + public function getEntry(int $index) : ?CreativeInventoryEntry{ + return $this->creative[$index] ?? null; } public function getItemIndex(Item $item) : int{ foreach($this->creative as $i => $d){ - if($item->equals($d, true, false)){ + if($d->matchesItem($item)){ return $i; } } @@ -96,8 +125,8 @@ final class CreativeInventory{ * Adds an item to the creative menu. * Note: Players who are already online when this is called will not see this change. */ - public function add(Item $item) : void{ - $this->creative[] = clone $item; + public function add(Item $item, CreativeCategory $category = CreativeCategory::ITEMS, ?CreativeGroup $group = null) : void{ + $this->creative[] = new CreativeInventoryEntry($item, $category, $group); $this->onContentChange(); } diff --git a/src/inventory/CreativeInventoryEntry.php b/src/inventory/CreativeInventoryEntry.php new file mode 100644 index 000000000..a5d568ee8 --- /dev/null +++ b/src/inventory/CreativeInventoryEntry.php @@ -0,0 +1,48 @@ +item = clone $item; + } + + public function getItem() : Item{ return clone $this->item; } + + public function getCategory() : CreativeCategory{ return $this->category; } + + public function getGroup() : ?CreativeGroup{ return $this->group; } + + public function matchesItem(Item $item) : bool{ + return $item->equals($this->item, checkDamage: true, checkCompound: false); + } +} diff --git a/src/inventory/json/CreativeGroupData.php b/src/inventory/json/CreativeGroupData.php new file mode 100644 index 000000000..70b73d3de --- /dev/null +++ b/src/inventory/json/CreativeGroupData.php @@ -0,0 +1,38 @@ +session->sendDataPacket(CreativeInventoryCache::getInstance()->getCache($this->player->getCreativeInventory())); + $this->session->sendDataPacket(CreativeInventoryCache::getInstance()->buildPacket($this->player->getCreativeInventory(), $this->session)); } /** diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index fdb63c467..8c457ed40 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -1058,7 +1058,7 @@ class NetworkSession{ ]; $layers = [ - new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, $for->getFlightSpeedMultiplier(), 0.1), + new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, $for->getFlightSpeedMultiplier(), 1, 0.1), ]; if(!$for->hasBlockCollision()){ //TODO: HACK! In 1.19.80, the client starts falling in our faux spectator mode when it clips into a @@ -1068,7 +1068,7 @@ class NetworkSession{ $layers[] = new AbilitiesLayer(AbilitiesLayer::LAYER_SPECTATOR, [ AbilitiesLayer::ABILITY_FLYING => true, - ], null, null); + ], null, null, null); } $this->sendDataPacket(UpdateAbilitiesPacket::create(new AbilitiesData( diff --git a/src/network/mcpe/cache/CreativeInventoryCache.php b/src/network/mcpe/cache/CreativeInventoryCache.php index 04fc52604..e543f343e 100644 --- a/src/network/mcpe/cache/CreativeInventoryCache.php +++ b/src/network/mcpe/cache/CreativeInventoryCache.php @@ -23,23 +23,30 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\cache; +use pocketmine\inventory\CreativeCategory; use pocketmine\inventory\CreativeInventory; +use pocketmine\lang\Translatable; use pocketmine\network\mcpe\convert\TypeConverter; +use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\protocol\CreativeContentPacket; -use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry; +use pocketmine\network\mcpe\protocol\types\inventory\CreativeGroupEntry; +use pocketmine\network\mcpe\protocol\types\inventory\CreativeItemEntry; +use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\utils\SingletonTrait; +use function is_string; use function spl_object_id; +use const PHP_INT_MIN; final class CreativeInventoryCache{ use SingletonTrait; /** - * @var CreativeContentPacket[] - * @phpstan-var array + * @var CreativeInventoryCacheEntry[] + * @phpstan-var array */ private array $caches = []; - public function getCache(CreativeInventory $inventory) : CreativeContentPacket{ + private function getCacheEntry(CreativeInventory $inventory) : CreativeInventoryCacheEntry{ $id = spl_object_id($inventory); if(!isset($this->caches[$id])){ $inventory->getDestructorCallbacks()->add(function() use ($id) : void{ @@ -48,7 +55,7 @@ final class CreativeInventoryCache{ $inventory->getContentChangedCallbacks()->add(function() use ($id) : void{ unset($this->caches[$id]); }); - $this->caches[$id] = $this->buildCreativeInventoryCache($inventory); + $this->caches[$id] = $this->buildCacheEntry($inventory); } return $this->caches[$id]; } @@ -56,14 +63,91 @@ final class CreativeInventoryCache{ /** * Rebuild the cache for the given inventory. */ - private function buildCreativeInventoryCache(CreativeInventory $inventory) : CreativeContentPacket{ - $entries = []; + private function buildCacheEntry(CreativeInventory $inventory) : CreativeInventoryCacheEntry{ + $categories = []; + $groups = []; + $typeConverter = TypeConverter::getInstance(); - //creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent - foreach($inventory->getAll() as $k => $item){ - $entries[] = new CreativeContentEntry($k, $typeConverter->coreItemStackToNet($item)); + + $nextIndex = 0; + $groupIndexes = []; + $itemGroupIndexes = []; + + foreach($inventory->getAllEntries() as $k => $entry){ + $group = $entry->getGroup(); + $category = $entry->getCategory(); + if($group === null){ + $groupId = PHP_INT_MIN; + }else{ + $groupId = spl_object_id($group); + unset($groupIndexes[$category->name][PHP_INT_MIN]); //start a new anonymous group for this category + } + + //group object may be reused by multiple categories + if(!isset($groupIndexes[$category->name][$groupId])){ + $groupIndexes[$category->name][$groupId] = $nextIndex++; + $categories[] = $category; + $groups[] = $group; + } + $itemGroupIndexes[$k] = $groupIndexes[$category->name][$groupId]; } - return CreativeContentPacket::create($entries); + //creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent + $items = []; + foreach($inventory->getAllEntries() as $k => $entry){ + $items[] = new CreativeItemEntry( + $k, + $typeConverter->coreItemStackToNet($entry->getItem()), + $itemGroupIndexes[$k] + ); + } + + return new CreativeInventoryCacheEntry($categories, $groups, $items); + } + + public function buildPacket(CreativeInventory $inventory, NetworkSession $session) : CreativeContentPacket{ + $player = $session->getPlayer() ?? throw new \LogicException("Cannot prepare creative data for a session without a player"); + $language = $player->getLanguage(); + $forceLanguage = $player->getServer()->isLanguageForced(); + $typeConverter = $session->getTypeConverter(); + $cachedEntry = $this->getCacheEntry($inventory); + $translate = function(Translatable|string $translatable) use ($session, $language, $forceLanguage) : string{ + if(is_string($translatable)){ + $message = $translatable; + }elseif(!$forceLanguage){ + [$message,] = $session->prepareClientTranslatableMessage($translatable); + }else{ + $message = $language->translate($translatable); + } + return $message; + }; + + $groupEntries = []; + foreach($cachedEntry->categories as $index => $category){ + $group = $cachedEntry->groups[$index]; + $categoryId = match ($category) { + CreativeCategory::CONSTRUCTION => CreativeContentPacket::CATEGORY_CONSTRUCTION, + CreativeCategory::NATURE => CreativeContentPacket::CATEGORY_NATURE, + CreativeCategory::EQUIPMENT => CreativeContentPacket::CATEGORY_EQUIPMENT, + CreativeCategory::ITEMS => CreativeContentPacket::CATEGORY_ITEMS + }; + if($group === null){ + $groupEntries[] = new CreativeGroupEntry($categoryId, "", ItemStack::null()); + }else{ + $groupIcon = $group->getIcon(); + //TODO: HACK! In 1.21.60, Workaround glitchy behaviour when an item is used as an icon for a group it + //doesn't belong to. Without this hack, both instances of the item will show a +, but neither of them + //will actually expand the group work correctly. + $groupIcon->getNamedTag()->setInt("___GroupBugWorkaround___", $index); + $groupName = $group->getName(); + $groupEntries[] = new CreativeGroupEntry( + $categoryId, + $translate($groupName), + $typeConverter->coreItemStackToNet($groupIcon) + ); + } + } + + return CreativeContentPacket::create($groupEntries, $cachedEntry->items); } } diff --git a/src/network/mcpe/cache/CreativeInventoryCacheEntry.php b/src/network/mcpe/cache/CreativeInventoryCacheEntry.php new file mode 100644 index 000000000..1fc0767df --- /dev/null +++ b/src/network/mcpe/cache/CreativeInventoryCacheEntry.php @@ -0,0 +1,48 @@ + $categories + * @phpstan-param list $groups + * @phpstan-param list $items + */ + public function __construct( + public readonly array $categories, + public readonly array $groups, + public readonly array $items, + ){ + //NOOP + } +} diff --git a/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php b/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php index d962063d3..ed8ae2cc7 100644 --- a/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php +++ b/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php @@ -23,10 +23,15 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\convert; +use pocketmine\errorhandler\ErrorToExceptionHandler; +use pocketmine\nbt\LittleEndianNbtSerializer; +use pocketmine\nbt\tag\CompoundTag; use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; +use pocketmine\network\mcpe\protocol\types\CacheableNbt; use pocketmine\network\mcpe\protocol\types\ItemTypeEntry; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; +use function base64_decode; use function is_array; use function is_bool; use function is_int; @@ -41,12 +46,15 @@ final class ItemTypeDictionaryFromDataHelper{ throw new AssumptionFailedError("Invalid item list format"); } + $emptyNBT = new CacheableNbt(new CompoundTag()); + $nbtSerializer = new LittleEndianNbtSerializer(); + $params = []; foreach(Utils::promoteKeys($table) as $name => $entry){ - if(!is_array($entry) || !is_string($name) || !isset($entry["component_based"], $entry["runtime_id"]) || !is_bool($entry["component_based"]) || !is_int($entry["runtime_id"])){ + if(!is_array($entry) || !is_string($name) || !isset($entry["component_based"], $entry["runtime_id"], $entry["version"]) || !is_bool($entry["component_based"]) || !is_int($entry["runtime_id"]) || !is_int($entry["version"]) || !(is_string($componentNbt = $entry["component_nbt"] ?? null) || $componentNbt === null)){ throw new AssumptionFailedError("Invalid item list format"); } - $params[] = new ItemTypeEntry($name, $entry["runtime_id"], $entry["component_based"]); + $params[] = new ItemTypeEntry($name, $entry["runtime_id"], $entry["component_based"], $entry["version"], $componentNbt === null ? $emptyNBT : new CacheableNbt($nbtSerializer->read(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => base64_decode($componentNbt, true)))->mustGetCompoundTag())); } return new ItemTypeDictionary($params); } diff --git a/src/network/mcpe/handler/PreSpawnPacketHandler.php b/src/network/mcpe/handler/PreSpawnPacketHandler.php index b80874938..9aa302c0c 100644 --- a/src/network/mcpe/handler/PreSpawnPacketHandler.php +++ b/src/network/mcpe/handler/PreSpawnPacketHandler.php @@ -28,6 +28,7 @@ use pocketmine\network\mcpe\cache\CraftingDataCache; use pocketmine\network\mcpe\cache\StaticPacketCache; use pocketmine\network\mcpe\InventoryManager; use pocketmine\network\mcpe\NetworkSession; +use pocketmine\network\mcpe\protocol\ItemRegistryPacket; use pocketmine\network\mcpe\protocol\PlayerAuthInputPacket; use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket; use pocketmine\network\mcpe\protocol\StartGamePacket; @@ -110,9 +111,11 @@ class PreSpawnPacketHandler extends PacketHandler{ new NetworkPermissions(disableClientSounds: true), [], 0, - $typeConverter->getItemTypeDictionary()->getEntries(), )); + $this->session->getLogger()->debug("Sending items"); + $this->session->sendDataPacket(ItemRegistryPacket::create($typeConverter->getItemTypeDictionary()->getEntries())); + $this->session->getLogger()->debug("Sending actor identifiers"); $this->session->sendDataPacket(StaticPacketCache::getInstance()->getAvailableActorIdentifiers()); diff --git a/src/world/format/io/data/BedrockWorldData.php b/src/world/format/io/data/BedrockWorldData.php index b2e079124..a9ca43bc3 100644 --- a/src/world/format/io/data/BedrockWorldData.php +++ b/src/world/format/io/data/BedrockWorldData.php @@ -51,12 +51,12 @@ use function time; class BedrockWorldData extends BaseNbtWorldData{ public const CURRENT_STORAGE_VERSION = 10; - public const CURRENT_STORAGE_NETWORK_VERSION = 748; + public const CURRENT_STORAGE_NETWORK_VERSION = 776; public const CURRENT_CLIENT_VERSION_TARGET = [ 1, //major 21, //minor - 40, //patch - 1, //revision + 60, //patch + 33, //revision 0 //is beta ]; diff --git a/tools/generate-bedrock-data-from-packets.php b/tools/generate-bedrock-data-from-packets.php index 9aac5185e..c48a4f017 100644 --- a/tools/generate-bedrock-data-from-packets.php +++ b/tools/generate-bedrock-data-from-packets.php @@ -35,6 +35,7 @@ use pocketmine\crafting\json\SmithingTrimRecipeData; use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\data\bedrock\item\BlockItemIdMap; use pocketmine\data\bedrock\item\ItemTypeNames; +use pocketmine\inventory\json\CreativeGroupData; use pocketmine\nbt\LittleEndianNbtSerializer; use pocketmine\nbt\NBT; use pocketmine\nbt\tag\CompoundTag; @@ -48,15 +49,17 @@ use pocketmine\network\mcpe\protocol\AvailableActorIdentifiersPacket; use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket; use pocketmine\network\mcpe\protocol\CraftingDataPacket; use pocketmine\network\mcpe\protocol\CreativeContentPacket; +use pocketmine\network\mcpe\protocol\ItemRegistryPacket; use pocketmine\network\mcpe\protocol\PacketPool; use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; use pocketmine\network\mcpe\protocol\serializer\PacketSerializer; use pocketmine\network\mcpe\protocol\StartGamePacket; use pocketmine\network\mcpe\protocol\types\CacheableNbt; -use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry; +use pocketmine\network\mcpe\protocol\types\inventory\CreativeGroupEntry; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraData; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraDataShield; +use pocketmine\network\mcpe\protocol\types\ItemTypeEntry; use pocketmine\network\mcpe\protocol\types\recipe\ComplexAliasItemDescriptor; use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipe; use pocketmine\network\mcpe\protocol\types\recipe\IntIdMetaItemDescriptor; @@ -134,6 +137,19 @@ class ParserPacketHandler extends PacketHandler{ return base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($statePropertiesTag))); } + /** + * @param ItemStackData[] $items + */ + private function creativeGroupEntryToJson(CreativeGroupEntry $entry, array $items) : CreativeGroupData{ + $data = new CreativeGroupData(); + + $data->group_name = $entry->getCategoryName(); + $data->group_icon = $entry->getIcon()->getId() === 0 ? null : $this->itemStackToJson($entry->getIcon()); + $data->items = $items; + + return $data; + } + private function itemStackToJson(ItemStack $itemStack) : ItemStackData{ if($itemStack->getId() === 0){ throw new InvalidArgumentException("Cannot serialize a null itemstack"); @@ -234,31 +250,68 @@ class ParserPacketHandler extends PacketHandler{ } public function handleStartGame(StartGamePacket $packet) : bool{ - $this->itemTypeDictionary = new ItemTypeDictionary($packet->itemTable); - - echo "updating legacy item ID mapping table\n"; - $table = []; - foreach($packet->itemTable as $entry){ - $table[$entry->getStringId()] = [ - "runtime_id" => $entry->getNumericId(), - "component_based" => $entry->isComponentBased() - ]; - } - ksort($table, SORT_STRING); - file_put_contents($this->bedrockDataPath . '/required_item_list.json', json_encode($table, JSON_PRETTY_PRINT) . "\n"); - foreach(Utils::promoteKeys($packet->levelSettings->experiments->getExperiments()) as $name => $experiment){ echo "Experiment \"$name\" is " . ($experiment ? "" : "not ") . "active\n"; } return true; } + public function handleItemRegistry(ItemRegistryPacket $packet) : bool{ + $this->itemTypeDictionary = new ItemTypeDictionary($packet->getEntries()); + + echo "updating legacy item ID mapping table\n"; + $emptyNBT = new CompoundTag(); + $table = []; + foreach($packet->getEntries() as $entry){ + $table[$entry->getStringId()] = [ + "runtime_id" => $entry->getNumericId(), + "component_based" => $entry->isComponentBased(), + "version" => $entry->getVersion(), + ]; + + $componentNBT = $entry->getComponentNbt()->getRoot(); + if(!$componentNBT->equals($emptyNBT)){ + $table[$entry->getStringId()]["component_nbt"] = base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($componentNBT))); + } + } + ksort($table, SORT_STRING); + file_put_contents($this->bedrockDataPath . '/required_item_list.json', json_encode($table, JSON_PRETTY_PRINT) . "\n"); + + echo "updating item registry\n"; + $items = array_map(function(ItemTypeEntry $entry) : array{ + return self::objectToOrderedArray($entry); + }, $packet->getEntries()); + file_put_contents($this->bedrockDataPath . '/item_registry.json', json_encode($items, JSON_PRETTY_PRINT) . "\n"); + return true; + } + public function handleCreativeContent(CreativeContentPacket $packet) : bool{ echo "updating creative inventory data\n"; - $items = array_map(function(CreativeContentEntry $entry) : array{ - return self::objectToOrderedArray($this->itemStackToJson($entry->getItem())); - }, $packet->getEntries()); - file_put_contents($this->bedrockDataPath . '/creativeitems.json', json_encode($items, JSON_PRETTY_PRINT) . "\n"); + + $groupItems = []; + foreach($packet->getItems() as $itemEntry){ + $groupItems[$itemEntry->getGroupId()][] = $this->itemStackToJson($itemEntry->getItem()); + } + + static $typeMap = [ + CreativeContentPacket::CATEGORY_CONSTRUCTION => "construction", + CreativeContentPacket::CATEGORY_NATURE => "nature", + CreativeContentPacket::CATEGORY_EQUIPMENT => "equipment", + CreativeContentPacket::CATEGORY_ITEMS => "items", + ]; + + $groupCategories = []; + foreach(Utils::promoteKeys($packet->getGroups()) as $groupId => $group){ + $category = $typeMap[$group->getCategoryId()] ?? throw new PacketHandlingException("Unknown creative category ID " . $group->getCategoryId()); + //FIXME: objectToOrderedArray might mess with the order of groupItems + //this isn't a problem right now because it's a list, but could cause problems in the future + $groupCategories[$category][] = self::objectToOrderedArray($this->creativeGroupEntryToJson($group, $groupItems[$groupId])); + } + + foreach(Utils::promoteKeys($groupCategories) as $category => $categoryGroups){ + file_put_contents($this->bedrockDataPath . '/creative/' . $category . '.json', json_encode($categoryGroups, JSON_PRETTY_PRINT) . "\n"); + } + return true; }