From 5ed75731f2c28c5c119ffd7b31b85d46f9656908 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Thu, 23 Jun 2022 19:02:16 +0100 Subject: [PATCH] First (untested) look at hooking all the itemstack serializer/deserializer stuff together this should address #5063 and related issues, if it works correctly. --- src/data/bedrock/item/SavedItemData.php | 47 +---- src/data/bedrock/item/SavedItemStackData.php | 87 +++++++++ .../bedrock/item/upgrade/ItemDataUpgrader.php | 174 ++++++++++++++++++ .../item/upgrade/ItemIdMetaUpgradeSchema.php | 49 +++++ .../upgrade/ItemIdMetaUpgradeSchemaUtils.php | 98 ++++++++++ .../upgrade}/LegacyItemIdToStringIdMap.php | 3 +- .../item/upgrade/R12ItemIdToBlockIdMap.php | 87 +++++++++ .../model/ItemIdMetaUpgradeSchemaModel.php | 39 ++++ src/item/Item.php | 56 ++---- .../format/io/GlobalItemDataHandlers.php | 59 ++++++ 10 files changed, 613 insertions(+), 86 deletions(-) create mode 100644 src/data/bedrock/item/SavedItemStackData.php create mode 100644 src/data/bedrock/item/upgrade/ItemDataUpgrader.php create mode 100644 src/data/bedrock/item/upgrade/ItemIdMetaUpgradeSchema.php create mode 100644 src/data/bedrock/item/upgrade/ItemIdMetaUpgradeSchemaUtils.php rename src/data/bedrock/{ => item/upgrade}/LegacyItemIdToStringIdMap.php (90%) create mode 100644 src/data/bedrock/item/upgrade/R12ItemIdToBlockIdMap.php create mode 100644 src/data/bedrock/item/upgrade/model/ItemIdMetaUpgradeSchemaModel.php create mode 100644 src/world/format/io/GlobalItemDataHandlers.php diff --git a/src/data/bedrock/item/SavedItemData.php b/src/data/bedrock/item/SavedItemData.php index 031c5e242..fa631e4e7 100644 --- a/src/data/bedrock/item/SavedItemData.php +++ b/src/data/bedrock/item/SavedItemData.php @@ -24,20 +24,14 @@ declare(strict_types=1); namespace pocketmine\data\bedrock\item; use pocketmine\data\bedrock\block\BlockStateData; -use pocketmine\data\bedrock\block\BlockStateDeserializeException; -use pocketmine\data\SavedDataLoadingException; -use pocketmine\nbt\NbtException; use pocketmine\nbt\tag\CompoundTag; -use pocketmine\nbt\tag\StringTag; -use function str_starts_with; final class SavedItemData{ public const TAG_NAME = "Name"; - private const TAG_DAMAGE = "Damage"; + public const TAG_DAMAGE = "Damage"; public const TAG_BLOCK = "Block"; - private const TAG_TAG = "tag"; - private const TAG_ITEM_IDENTIFIER = "ItemIdentifier"; + public const TAG_TAG = "tag"; public function __construct( private string $name, @@ -54,43 +48,6 @@ final class SavedItemData{ public function getTag() : ?CompoundTag{ return $this->tag; } - public static function fromNbt(CompoundTag $tag) : self{ - try{ - //required - $name = $tag->getString(self::TAG_NAME); - $damage = $tag->getShort(self::TAG_DAMAGE); - - //optional - $blockStateNbt = $tag->getCompoundTag(self::TAG_BLOCK); - $extraData = $tag->getCompoundTag(self::TAG_TAG); - }catch(NbtException $e){ - throw new SavedDataLoadingException($e->getMessage(), 0, $e); - } - - //TODO: this hack probably doesn't belong here; it's necessary to deal with spawn eggs from before 1.16.100 - if( - $name === ItemTypeIds::SPAWN_EGG && - ($itemIdentifierTag = $tag->getTag(self::TAG_ITEM_IDENTIFIER)) instanceof StringTag && - str_starts_with($itemIdentifierTag->getValue(), "minecraft:") - ){ - \GlobalLogger::get()->debug("Handling legacy spawn egg for " . $itemIdentifierTag->getValue()); - $name = $itemIdentifierTag->getValue() . "_spawn_egg"; - } - - try{ - $blockStateData = $blockStateNbt !== null ? BlockStateData::fromNbt($blockStateNbt) : null; - }catch(BlockStateDeserializeException $e){ - throw new SavedDataLoadingException("Failed to load item saved data: " . $e->getMessage(), 0, $e); - } - - return new self( - $name, - $damage, - $blockStateData, - $extraData - ); - } - public function toNbt() : CompoundTag{ $result = CompoundTag::create(); $result->setString(self::TAG_NAME, $this->name); diff --git a/src/data/bedrock/item/SavedItemStackData.php b/src/data/bedrock/item/SavedItemStackData.php new file mode 100644 index 000000000..b8ad648f0 --- /dev/null +++ b/src/data/bedrock/item/SavedItemStackData.php @@ -0,0 +1,87 @@ +typeData; } + + public function getCount() : int{ return $this->count; } + + public function getSlot() : ?int{ return $this->slot; } + + public function getWasPickedUp() : ?bool{ return $this->wasPickedUp; } + + /** @return string[] */ + public function getCanPlaceOn() : array{ return $this->canPlaceOn; } + + /** @return string[] */ + public function getCanDestroy() : array{ return $this->canDestroy; } + + public function toNbt() : CompoundTag{ + $result = CompoundTag::create(); + $result->setByte(self::TAG_COUNT, Binary::signByte($this->count)); + + if($this->slot !== null){ + $result->setByte(self::TAG_SLOT, Binary::signByte($this->slot)); + } + if($this->wasPickedUp !== null){ + $result->setByte(self::TAG_WAS_PICKED_UP, $this->wasPickedUp ? 1 : 0); + } + if(count($this->canPlaceOn) !== 0){ + $result->setTag(self::TAG_CAN_PLACE_ON, new ListTag(array_map(fn(string $s) => new StringTag($s), $this->canPlaceOn))); + } + if(count($this->canDestroy) !== 0){ + $result->setTag(self::TAG_CAN_DESTROY, new ListTag(array_map(fn(string $s) => new StringTag($s), $this->canDestroy))); + } + + return $result->merge($this->typeData->toNbt()); + } +} diff --git a/src/data/bedrock/item/upgrade/ItemDataUpgrader.php b/src/data/bedrock/item/upgrade/ItemDataUpgrader.php new file mode 100644 index 000000000..e55513c9a --- /dev/null +++ b/src/data/bedrock/item/upgrade/ItemDataUpgrader.php @@ -0,0 +1,174 @@ + + */ + private array $idMetaUpgradeSchemas = []; + + /** + * @param ItemIdMetaUpgradeSchema[] $idMetaUpgradeSchemas + * @phpstan-param array $idMetaUpgradeSchemas + */ + public function __construct( + private LegacyItemIdToStringIdMap $legacyIntToStringIdMap, + private R12ItemIdToBlockIdMap $r12ItemIdToBlockIdMap, + private BlockDataUpgrader $blockDataUpgrader, + array $idMetaUpgradeSchemas + ){ + foreach($idMetaUpgradeSchemas as $schema){ + $this->addIdMetaUpgradeSchema($schema); + } + } + + public function addIdMetaUpgradeSchema(ItemIdMetaUpgradeSchema $schema) : void{ + if(isset($this->idMetaUpgradeSchemas[$schema->getPriority()])){ + throw new \InvalidArgumentException("Already have a schema with priority " . $schema->getPriority()); + } + $this->idMetaUpgradeSchemas[$schema->getPriority()] = $schema; + ksort($this->idMetaUpgradeSchemas, SORT_NUMERIC); + } + + private function upgradeItemTypeNbt(CompoundTag $tag) : SavedItemData{ + if(($nameIdTag = $tag->getTag(SavedItemData::TAG_NAME)) instanceof StringTag){ + //Bedrock 1.6+ + + $rawNameId = $nameIdTag->getValue(); + }elseif(($idTag = $tag->getTag(self::TAG_LEGACY_ID)) instanceof ShortTag){ + //Bedrock <= 1.5, PM <= 1.12 + + $rawNameId = $this->legacyIntToStringIdMap->legacyToString($idTag->getValue()); + if($rawNameId === null){ + throw new SavedDataLoadingException("Legacy item ID " . $idTag->getValue() . " doesn't map to any modern string ID"); + } + }elseif($idTag instanceof StringTag){ + //PC item save format - best we can do here is hope the string IDs match + + $rawNameId = $idTag->getValue(); + }else{ + throw new SavedDataLoadingException("Item stack data should have either a name ID or a legacy ID"); + } + + $meta = $tag->getShort(SavedItemData::TAG_DAMAGE, 0); + + $blockStateNbt = $tag->getCompoundTag(SavedItemData::TAG_BLOCK); + if($blockStateNbt !== null){ + $blockStateData = $this->blockDataUpgrader->upgradeBlockStateNbt($blockStateNbt); + }elseif(($r12BlockId = $this->r12ItemIdToBlockIdMap->itemIdToBlockId($rawNameId)) !== null){ + //this is a legacy blockitem represented by ID + meta + $blockStateData = $this->blockDataUpgrader->upgradeStringIdMeta($r12BlockId, $meta); + }else{ + //probably a standard item + $blockStateData = null; + } + + [$newNameId, $newMeta] = $this->upgradeItemStringIdMeta($rawNameId, $meta); + + //TODO: this won't account for spawn eggs from before 1.16.100 - perhaps we're lucky and they just left the meta in there anyway? + + return new SavedItemData($newNameId, $newMeta, $blockStateData, $tag->getCompoundTag(SavedItemData::TAG_TAG)); + } + + /** + * @return string[] + */ + private static function deserializeListOfStrings(?ListTag $list, string $tagName) : array{ + if($list === null){ + return []; + } + if($list->getTagType() !== NBT::TAG_String){ + throw new SavedDataLoadingException("Unexpected type of list for tag '$tagName', expected TAG_String"); + } + $result = []; + foreach($list as $item){ + assert($item instanceof StringTag); + $result[] = $item->getValue(); + } + + return $result; + } + + public function upgradeItemStackNbt(CompoundTag $tag) : SavedItemStackData{ + $savedItemData = $this->upgradeItemTypeNbt($tag); + try{ + //required + $count = Binary::unsignByte($tag->getByte(SavedItemStackData::TAG_COUNT)); + + //optional + $slot = ($slotTag = $tag->getTag(SavedItemStackData::TAG_SLOT)) instanceof ByteTag ? Binary::unsignByte($slotTag->getValue()) : null; + $wasPickedUp = ($wasPickedUpTag = $tag->getTag(SavedItemStackData::TAG_WAS_PICKED_UP)) instanceof ByteTag ? $wasPickedUpTag->getValue() : null; + $canPlaceOnList = $tag->getListTag(SavedItemStackData::TAG_CAN_PLACE_ON); + $canDestroyList = $tag->getListTag(SavedItemStackData::TAG_CAN_DESTROY); + }catch(NbtException $e){ + throw new SavedDataLoadingException($e->getMessage(), 0, $e); + } + + return new SavedItemStackData( + $savedItemData, + $count, + $slot, + $wasPickedUp !== 0, + self::deserializeListOfStrings($canPlaceOnList, SavedItemStackData::TAG_CAN_PLACE_ON), + self::deserializeListOfStrings($canDestroyList, SavedItemStackData::TAG_CAN_DESTROY) + ); + } + + /** + * @phpstan-return array{string, int} + */ + public function upgradeItemStringIdMeta(string $id, int $meta) : array{ + $newId = $id; + $newMeta = $meta; + foreach($this->idMetaUpgradeSchemas as $schema){ + if(($remappedMetaId = $schema->remapMeta($newId, $newMeta)) !== null){ + $newId = $remappedMetaId; + $newMeta = 0; + }elseif(($renamedId = $schema->renameId($newId)) !== null){ + $newId = $renamedId; + } + } + + return [$id, $meta]; + } +} diff --git a/src/data/bedrock/item/upgrade/ItemIdMetaUpgradeSchema.php b/src/data/bedrock/item/upgrade/ItemIdMetaUpgradeSchema.php new file mode 100644 index 000000000..f0b398acb --- /dev/null +++ b/src/data/bedrock/item/upgrade/ItemIdMetaUpgradeSchema.php @@ -0,0 +1,49 @@ + $renamedIds + * @phpstan-param array> $remappedMetas + */ + public function __construct( + private array $renamedIds, + private array $remappedMetas, + private int $priority + ){} + + public function getPriority() : int{ return $this->priority; } + + public function renameId(string $id) : ?string{ + return $this->renamedIds[$id] ?? null; + } + + public function remapMeta(string $id, int $meta) : ?string{ + return $this->remappedMetas[$id][$meta] ?? null; + } +} diff --git a/src/data/bedrock/item/upgrade/ItemIdMetaUpgradeSchemaUtils.php b/src/data/bedrock/item/upgrade/ItemIdMetaUpgradeSchemaUtils.php new file mode 100644 index 000000000..9e95bf4a2 --- /dev/null +++ b/src/data/bedrock/item/upgrade/ItemIdMetaUpgradeSchemaUtils.php @@ -0,0 +1,98 @@ + + */ + public static function loadSchemas(string $path) : array{ + $iterator = new \RegexIterator( + new \FilesystemIterator( + $path, + \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS + ), + '/\/(\d{4}).*\.json$/', + \RegexIterator::GET_MATCH + ); + + $result = []; + + /** @var string[] $matches */ + foreach($iterator as $matches){ + $filename = $matches[0]; + $priority = (int) $matches[1]; + + $fullPath = Path::join($path, $filename); + + try{ + $raw = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents($fullPath)); + }catch(\ErrorException $e){ + throw new \RuntimeException("Loading schema file $fullPath: " . $e->getMessage(), 0, $e); + } + + try{ + $schema = self::loadSchemaFromString($raw, $priority); + }catch(\RuntimeException $e){ + throw new \RuntimeException("Loading schema file $fullPath: " . $e->getMessage(), 0, $e); + } + + $result[$priority] = $schema; + } + + ksort($result, SORT_NUMERIC); + return $result; + } + + public static function loadSchemaFromString(string $raw, int $priority) : ItemIdMetaUpgradeSchema{ + try{ + $json = json_decode($raw, false, flags: JSON_THROW_ON_ERROR); + }catch(\JsonException $e){ + throw new \RuntimeException($e->getMessage(), 0, $e); + } + if(!is_object($json)){ + throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object"); + } + + $jsonMapper = new \JsonMapper(); + try{ + $model = $jsonMapper->map($json, new ItemIdMetaUpgradeSchemaModel()); + }catch(\JsonMapper_Exception $e){ + throw new \RuntimeException($e->getMessage(), 0, $e); + } + + return new ItemIdMetaUpgradeSchema($model->renamedIds, $model->remappedMetas, $priority); + } +} diff --git a/src/data/bedrock/LegacyItemIdToStringIdMap.php b/src/data/bedrock/item/upgrade/LegacyItemIdToStringIdMap.php similarity index 90% rename from src/data/bedrock/LegacyItemIdToStringIdMap.php rename to src/data/bedrock/item/upgrade/LegacyItemIdToStringIdMap.php index 587a09fe0..49b1a2271 100644 --- a/src/data/bedrock/LegacyItemIdToStringIdMap.php +++ b/src/data/bedrock/item/upgrade/LegacyItemIdToStringIdMap.php @@ -21,8 +21,9 @@ declare(strict_types=1); -namespace pocketmine\data\bedrock; +namespace pocketmine\data\bedrock\item\upgrade; +use pocketmine\data\bedrock\LegacyToStringBidirectionalIdMap; use pocketmine\utils\SingletonTrait; use Webmozart\PathUtil\Path; diff --git a/src/data/bedrock/item/upgrade/R12ItemIdToBlockIdMap.php b/src/data/bedrock/item/upgrade/R12ItemIdToBlockIdMap.php new file mode 100644 index 000000000..723e8d993 --- /dev/null +++ b/src/data/bedrock/item/upgrade/R12ItemIdToBlockIdMap.php @@ -0,0 +1,87 @@ + $blockId){ + if(!is_string($itemId)){ + throw new AssumptionFailedError("Invalid blockitem ID mapping table, expected string as key"); + } + if(!is_string($blockId)){ + throw new AssumptionFailedError("Invalid blockitem ID mapping table, expected string as value"); + } + $builtMap[$itemId] = $blockId; + } + + return new self($builtMap); + } + + /** + * @param string[] $itemToBlock + * @phpstan-param array $itemToBlock + */ + public function __construct(private array $itemToBlock){} + + public function itemIdToBlockId(string $itemId) : ?string{ + return $this->itemToBlock[$itemId] ?? null; + } + + public function blockIdToItemId(string $blockId) : ?string{ + //we don't need this for any functionality, so we're not concerned about performance here + //however, it might be nice to have for debugging + $itemId = array_search($blockId, $this->itemToBlock, true); + return $itemId !== false ? $itemId : null; + } +} diff --git a/src/data/bedrock/item/upgrade/model/ItemIdMetaUpgradeSchemaModel.php b/src/data/bedrock/item/upgrade/model/ItemIdMetaUpgradeSchemaModel.php new file mode 100644 index 000000000..5765c5acf --- /dev/null +++ b/src/data/bedrock/item/upgrade/model/ItemIdMetaUpgradeSchemaModel.php @@ -0,0 +1,39 @@ + + */ + public array $renamedIds = []; + + /** + * @var string[][] + * @phpstan-var array> + */ + public array $remappedMetas = []; +} diff --git a/src/item/Item.php b/src/item/Item.php index 775ae3eb0..dd5e088fb 100644 --- a/src/item/Item.php +++ b/src/item/Item.php @@ -31,6 +31,7 @@ use pocketmine\block\BlockBreakInfo; use pocketmine\block\BlockToolType; use pocketmine\block\VanillaBlocks; use pocketmine\data\bedrock\EnchantmentIdMap; +use pocketmine\data\bedrock\item\SavedItemStackData; use pocketmine\data\SavedDataLoadingException; use pocketmine\entity\Entity; use pocketmine\item\enchantment\EnchantmentInstance; @@ -41,16 +42,14 @@ use pocketmine\nbt\NbtDataException; use pocketmine\nbt\NbtException; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\ListTag; -use pocketmine\nbt\tag\ShortTag; use pocketmine\nbt\tag\StringTag; use pocketmine\nbt\TreeRoot; use pocketmine\player\Player; -use pocketmine\utils\Binary; use pocketmine\utils\Utils; +use pocketmine\world\format\io\GlobalItemDataHandlers; use function base64_decode; use function base64_encode; use function count; -use function get_class; use function gettype; use function hex2bin; use function is_string; @@ -648,21 +647,16 @@ class Item implements \JsonSerializable{ * @param int $slot optional, the inventory slot of the item */ public function nbtSerialize(int $slot = -1) : CompoundTag{ - $result = CompoundTag::create() - ->setShort("id", $this->getId()) - ->setByte("Count", Binary::signByte($this->count)) - ->setShort("Damage", $this->getMeta()); + $typeData = GlobalItemDataHandlers::getSerializer()->serialize($this); - $tag = $this->getNamedTag(); - if($tag->count() > 0){ - $result->setTag("tag", $tag); - } - - if($slot !== -1){ - $result->setByte("Slot", $slot); - } - - return $result; + return (new SavedItemStackData( + $typeData, + $this->count, + $slot !== -1 ? $slot : null, + null, + [], //we currently represent canDestroy and canPlaceOn via NBT, like PC + [] + ))->toNbt(); } /** @@ -671,31 +665,13 @@ class Item implements \JsonSerializable{ * @throws SavedDataLoadingException */ public static function nbtDeserialize(CompoundTag $tag) : Item{ - if($tag->getTag("id") === null || $tag->getTag("Count") === null){ - return VanillaItems::AIR(); - } + $itemData = GlobalItemDataHandlers::getUpgrader()->upgradeItemStackNbt($tag); - $count = Binary::unsignByte($tag->getByte("Count")); - $meta = $tag->getShort("Damage", 0); + $item = GlobalItemDataHandlers::getDeserializer()->deserialize($itemData->getTypeData()); - $idTag = $tag->getTag("id"); - if($idTag instanceof ShortTag){ - $item = ItemFactory::getInstance()->get($idTag->getValue(), $meta, $count); - }elseif($idTag instanceof StringTag){ //PC item save format - try{ - $item = LegacyStringToItemParser::getInstance()->parse($idTag->getValue() . ":$meta"); - }catch(LegacyStringToItemParserException $e){ - //TODO: improve error handling - return VanillaItems::AIR(); - } - $item->setCount($count); - }else{ - throw new SavedDataLoadingException("Item CompoundTag ID must be an instance of StringTag or ShortTag, " . get_class($idTag) . " given"); - } - - $itemNBT = $tag->getCompoundTag("tag"); - if($itemNBT !== null){ - $item->setNamedTag(clone $itemNBT); + $item->setCount($itemData->getCount()); + if(($tagTag = $itemData->getTypeData()->getTag()) !== null){ + $item->setNamedTag(clone $tagTag); } return $item; diff --git a/src/world/format/io/GlobalItemDataHandlers.php b/src/world/format/io/GlobalItemDataHandlers.php new file mode 100644 index 000000000..850a679a1 --- /dev/null +++ b/src/world/format/io/GlobalItemDataHandlers.php @@ -0,0 +1,59 @@ +