From 383be5426e46ca73a4efe5b09dde1aba65040add Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Tue, 24 May 2022 21:44:57 +0100 Subject: [PATCH] Rewrite network item serialization to use ItemSerializer --- src/network/mcpe/convert/ItemTranslator.php | 180 ++++++-------------- src/network/mcpe/convert/TypeConverter.php | 50 ++---- 2 files changed, 64 insertions(+), 166 deletions(-) diff --git a/src/network/mcpe/convert/ItemTranslator.php b/src/network/mcpe/convert/ItemTranslator.php index 5dac8334a..6bd890333 100644 --- a/src/network/mcpe/convert/ItemTranslator.php +++ b/src/network/mcpe/convert/ItemTranslator.php @@ -23,145 +23,66 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\convert; -use pocketmine\data\bedrock\LegacyItemIdToStringIdMap; +use pocketmine\data\bedrock\item\ItemDeserializer; +use pocketmine\data\bedrock\item\ItemSerializer; +use pocketmine\data\bedrock\item\ItemTypeSerializeException; +use pocketmine\data\bedrock\item\SavedItemData; +use pocketmine\item\ItemFactory; use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\SingletonTrait; -use pocketmine\utils\Utils; -use Webmozart\PathUtil\Path; -use function array_key_exists; -use function file_get_contents; -use function is_array; -use function is_numeric; -use function is_string; -use function json_decode; /** * This class handles translation between network item ID+metadata to PocketMine-MP internal ID+metadata and vice versa. */ final class ItemTranslator{ + public const NO_BLOCK_RUNTIME_ID = 0; + use SingletonTrait; - /** - * @var int[] - * @phpstan-var array - */ - private array $simpleCoreToNetMapping = []; - /** - * @var int[] - * @phpstan-var array - */ - private array $simpleNetToCoreMapping = []; - - /** - * runtimeId = array[internalId][metadata] - * @var int[][] - * @phpstan-var array> - */ - private array $complexCoreToNetMapping = []; - /** - * [internalId, metadata] = array[runtimeId] - * @var int[][] - * @phpstan-var array - */ - private array $complexNetToCoreMapping = []; - private static function make() : self{ - $data = Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'r16_to_current_item_map.json')), "Missing required resource file"); - $json = json_decode($data, true); - if(!is_array($json) || !isset($json["simple"], $json["complex"]) || !is_array($json["simple"]) || !is_array($json["complex"])){ - throw new AssumptionFailedError("Invalid item table format"); - } - - $legacyStringToIntMap = LegacyItemIdToStringIdMap::getInstance(); - - /** @phpstan-var array $simpleMappings */ - $simpleMappings = []; - foreach($json["simple"] as $oldId => $newId){ - if(!is_string($oldId) || !is_string($newId)){ - throw new AssumptionFailedError("Invalid item table format"); - } - $intId = $legacyStringToIntMap->stringToLegacy($oldId); - if($intId === null){ - //new item without a fixed legacy ID - we can't handle this right now - continue; - } - $simpleMappings[$newId] = $intId; - } - foreach(Utils::stringifyKeys($legacyStringToIntMap->getStringToLegacyMap()) as $stringId => $intId){ - if(isset($simpleMappings[$stringId])){ - throw new \UnexpectedValueException("Old ID $stringId collides with new ID"); - } - $simpleMappings[$stringId] = $intId; - } - - /** @phpstan-var array $complexMappings */ - $complexMappings = []; - foreach($json["complex"] as $oldId => $map){ - if(!is_string($oldId) || !is_array($map)){ - throw new AssumptionFailedError("Invalid item table format"); - } - foreach($map as $meta => $newId){ - if(!is_numeric($meta) || !is_string($newId)){ - throw new AssumptionFailedError("Invalid item table format"); - } - $intId = $legacyStringToIntMap->stringToLegacy($oldId); - if($intId === null){ - //new item without a fixed legacy ID - we can't handle this right now - continue; - } - $complexMappings[$newId] = [$intId, (int) $meta]; - } - } - - return new self(GlobalItemTypeDictionary::getInstance()->getDictionary(), $simpleMappings, $complexMappings); + return new self(GlobalItemTypeDictionary::getInstance()->getDictionary(), new ItemSerializer(), new ItemDeserializer()); } - /** - * @param int[] $simpleMappings - * @param int[][] $complexMappings - * @phpstan-param array $simpleMappings - * @phpstan-param array> $complexMappings - */ - public function __construct(ItemTypeDictionary $dictionary, array $simpleMappings, array $complexMappings){ - foreach($dictionary->getEntries() as $entry){ - $stringId = $entry->getStringId(); - $netId = $entry->getNumericId(); - if(isset($complexMappings[$stringId])){ - [$id, $meta] = $complexMappings[$stringId]; - $this->complexCoreToNetMapping[$id][$meta] = $netId; - $this->complexNetToCoreMapping[$netId] = [$id, $meta]; - }elseif(isset($simpleMappings[$stringId])){ - $this->simpleCoreToNetMapping[$simpleMappings[$stringId]] = $netId; - $this->simpleNetToCoreMapping[$netId] = $simpleMappings[$stringId]; - }else{ - //not all items have a legacy mapping - for now, we only support the ones that do - continue; - } - } - } + public function __construct( + private ItemTypeDictionary $dictionary, + private ItemSerializer $itemSerializer, + private ItemDeserializer $itemDeserializer + ){} /** * @return int[]|null - * @phpstan-return array{int, int}|null + * @phpstan-return array{int, int, int}|null */ public function toNetworkIdQuiet(int $internalId, int $internalMeta) : ?array{ - if($internalMeta === -1){ - $internalMeta = 0x7fff; - } - if(isset($this->complexCoreToNetMapping[$internalId][$internalMeta])){ - return [$this->complexCoreToNetMapping[$internalId][$internalMeta], 0]; - } - if(array_key_exists($internalId, $this->simpleCoreToNetMapping)){ - return [$this->simpleCoreToNetMapping[$internalId], $internalMeta]; + //TODO: we should probably come up with a cache for this + + try{ + $itemData = $this->itemSerializer->serialize(ItemFactory::getInstance()->get($internalId, $internalMeta)); + }catch(ItemTypeSerializeException){ + //TODO: this will swallow any serializer error; this is not ideal, but it should be OK since unit tests + //should cover this + return null; } - return null; + $numericId = $this->dictionary->fromStringId($itemData->getName()); + $blockStateData = $itemData->getBlock(); + + if($blockStateData !== null){ + $blockRuntimeId = RuntimeBlockMapping::getInstance()->getBlockStateDictionary()->lookupStateIdFromData($blockStateData); + if($blockRuntimeId === null){ + throw new AssumptionFailedError("Unmapped blockstate returned by blockstate serializer: " . $blockStateData->toNbt()); + } + }else{ + $blockRuntimeId = self::NO_BLOCK_RUNTIME_ID; //this is technically a valid block runtime ID, but is used to represent "no block" (derp mojang) + } + + return [$numericId, $itemData->getMeta(), $blockRuntimeId]; } /** * @return int[] - * @phpstan-return array{int, int} + * @phpstan-return array{int, int, int} */ public function toNetworkId(int $internalId, int $internalMeta) : array{ return $this->toNetworkIdQuiet($internalId, $internalMeta) ?? @@ -173,19 +94,15 @@ final class ItemTranslator{ * @phpstan-return array{int, int} * @throws TypeConversionException */ - public function fromNetworkId(int $networkId, int $networkMeta, ?bool &$isComplexMapping = null) : array{ - if(isset($this->complexNetToCoreMapping[$networkId])){ - if($networkMeta !== 0){ - throw new TypeConversionException("Unexpected non-zero network meta on complex item mapping"); - } - $isComplexMapping = true; - return $this->complexNetToCoreMapping[$networkId]; - } - $isComplexMapping = false; - if(isset($this->simpleNetToCoreMapping[$networkId])){ - return [$this->simpleNetToCoreMapping[$networkId], $networkMeta]; - } - throw new TypeConversionException("Unmapped network ID/metadata combination $networkId:$networkMeta"); + public function fromNetworkId(int $networkId, int $networkMeta, int $networkBlockRuntimeId) : array{ + $stringId = $this->dictionary->fromIntId($networkId); + + $blockStateData = $networkBlockRuntimeId !== self::NO_BLOCK_RUNTIME_ID ? + RuntimeBlockMapping::getInstance()->getBlockStateDictionary()->getDataFromStateId($networkBlockRuntimeId) : + null; + + $item = $this->itemDeserializer->deserialize(new SavedItemData($stringId, $networkMeta, $blockStateData)); + return [$item->getId(), $item->getMeta()]; } /** @@ -194,11 +111,10 @@ final class ItemTranslator{ * @throws TypeConversionException */ public function fromNetworkIdWithWildcardHandling(int $networkId, int $networkMeta) : array{ - $isComplexMapping = false; if($networkMeta !== 0x7fff){ - return $this->fromNetworkId($networkId, $networkMeta); + return $this->fromNetworkId($networkId, $networkMeta, 0); } - [$id, $meta] = $this->fromNetworkId($networkId, 0, $isComplexMapping); - return [$id, $isComplexMapping ? $meta : -1]; + [$id, ] = $this->fromNetworkId($networkId, 0, 0); + return [$id, -1]; } } diff --git a/src/network/mcpe/convert/TypeConverter.php b/src/network/mcpe/convert/TypeConverter.php index e010a27e9..e590ae5d8 100644 --- a/src/network/mcpe/convert/TypeConverter.php +++ b/src/network/mcpe/convert/TypeConverter.php @@ -22,7 +22,6 @@ declare(strict_types=1); namespace pocketmine\network\mcpe\convert; -use pocketmine\block\BlockLegacyIds; use pocketmine\block\inventory\AnvilInventory; use pocketmine\block\inventory\CraftingTableInventory; use pocketmine\block\inventory\EnchantInventory; @@ -37,6 +36,7 @@ use pocketmine\item\Durable; use pocketmine\item\Item; use pocketmine\item\ItemFactory; use pocketmine\item\ItemIds; +use pocketmine\item\VanillaItems; use pocketmine\nbt\NbtException; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\IntTag; @@ -141,20 +141,18 @@ class TypeConverter{ $nbt = clone $itemStack->getNamedTag(); } - $isBlockItem = $itemStack->getId() < 256; - $idMeta = ItemTranslator::getInstance()->toNetworkIdQuiet($itemStack->getId(), $itemStack->getMeta()); if($idMeta === null){ //Display unmapped items as INFO_UPDATE, but stick something in their NBT to make sure they don't stack with //other unmapped items. - [$id, $meta] = ItemTranslator::getInstance()->toNetworkId(ItemIds::INFO_UPDATE, 0); + [$id, $meta, $blockRuntimeId] = ItemTranslator::getInstance()->toNetworkId(ItemIds::INFO_UPDATE, 0); if($nbt === null){ $nbt = new CompoundTag(); } $nbt->setInt(self::PM_ID_TAG, $itemStack->getId()); $nbt->setInt(self::PM_META_TAG, $itemStack->getMeta()); }else{ - [$id, $meta] = $idMeta; + [$id, $meta, $blockRuntimeId] = $idMeta; if($itemStack instanceof Durable && $itemStack->getDamage() > 0){ if($nbt !== null){ @@ -166,22 +164,6 @@ class TypeConverter{ $nbt = new CompoundTag(); } $nbt->setInt(self::DAMAGE_TAG, $itemStack->getDamage()); - }elseif($isBlockItem && $itemStack->getMeta() !== 0){ - //TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the - //client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata - //client-side. Aside from being very annoying, this also breaks various server-side behaviours. - if($nbt === null){ - $nbt = new CompoundTag(); - } - $nbt->setInt(self::PM_META_TAG, $itemStack->getMeta()); - } - } - - $blockRuntimeId = 0; - if($isBlockItem){ - $block = $itemStack->getBlock(); - if($block->getId() !== BlockLegacyIds::AIR){ - $blockRuntimeId = RuntimeBlockMapping::getInstance()->toRuntimeId($block->getFullId()); } } @@ -202,17 +184,24 @@ class TypeConverter{ */ public function netItemStackToCore(ItemStack $itemStack) : Item{ if($itemStack->getId() === 0){ - return ItemFactory::getInstance()->get(ItemIds::AIR, 0, 0); + return VanillaItems::AIR(); } $compound = $itemStack->getNbt(); - [$id, $meta] = ItemTranslator::getInstance()->fromNetworkId($itemStack->getId(), $itemStack->getMeta()); + [$id, $meta] = ItemTranslator::getInstance()->fromNetworkId($itemStack->getId(), $itemStack->getMeta(), $itemStack->getBlockRuntimeId()); if($compound !== null){ $compound = clone $compound; - if(($idTag = $compound->getTag(self::PM_ID_TAG)) instanceof IntTag){ - $id = $idTag->getValue(); - $compound->removeTag(self::PM_ID_TAG); + + if($id === ItemIds::INFO_UPDATE && $meta === 0){ + if(($idTag = $compound->getTag(self::PM_ID_TAG)) instanceof IntTag){ + $id = $idTag->getValue(); + $compound->removeTag(self::PM_ID_TAG); + } + if(($metaTag = $compound->getTag(self::PM_META_TAG)) instanceof IntTag){ + $meta = $metaTag->getValue(); + $compound->removeTag(self::PM_META_TAG); + } } if(($damageTag = $compound->getTag(self::DAMAGE_TAG)) instanceof IntTag){ $meta = $damageTag->getValue(); @@ -221,14 +210,7 @@ class TypeConverter{ $compound->removeTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION); $compound->setTag(self::DAMAGE_TAG, $conflicted); } - }elseif(($metaTag = $compound->getTag(self::PM_META_TAG)) instanceof IntTag){ - //TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the - //client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata - //client-side. Aside from being very annoying, this also breaks various server-side behaviours. - $meta = $metaTag->getValue(); - $compound->removeTag(self::PM_META_TAG); - } - if($compound->count() === 0){ + }elseif($compound->count() === 0){ $compound = null; } }