From 7994da07becbe476910418437c539a73cecae011 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Sat, 2 Jul 2022 16:37:39 +0100 Subject: [PATCH] Crafting recipe network serialization no longer depends on PM's internal legacy metadata WOOOOOOOOOOOOOOOOOOOOOOHOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO!!!!!!!!!!!!!!!!!!!!! --- composer.lock | 8 +- .../mcpe/convert/BlockStateDictionary.php | 85 ++++++++++++++++--- .../convert/BlockStateDictionaryEntry.php | 38 +++++++++ .../mcpe/convert/RuntimeBlockMapping.php | 7 +- src/network/mcpe/convert/TypeConverter.php | 26 ++++-- tools/generate-block-palette-spec.php | 20 ++++- 6 files changed, 156 insertions(+), 28 deletions(-) create mode 100644 src/network/mcpe/convert/BlockStateDictionaryEntry.php diff --git a/composer.lock b/composer.lock index 50a4050fd..8b5a59aab 100644 --- a/composer.lock +++ b/composer.lock @@ -280,12 +280,12 @@ "source": { "type": "git", "url": "https://github.com/pmmp/BedrockData.git", - "reference": "6a28ede3e9cdf1c548e85ce24382fee5f1bd9d75" + "reference": "a546e15f6a8d7498fb25d5a02ce16184a429bb78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/6a28ede3e9cdf1c548e85ce24382fee5f1bd9d75", - "reference": "6a28ede3e9cdf1c548e85ce24382fee5f1bd9d75", + "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/a546e15f6a8d7498fb25d5a02ce16184a429bb78", + "reference": "a546e15f6a8d7498fb25d5a02ce16184a429bb78", "shasum": "" }, "type": "library", @@ -298,7 +298,7 @@ "issues": "https://github.com/pmmp/BedrockData/issues", "source": "https://github.com/pmmp/BedrockData/tree/modern-world-support" }, - "time": "2022-06-08T14:00:34+00:00" + "time": "2022-07-02T15:28:28+00:00" }, { "name": "pocketmine/bedrock-item-upgrade-schema", diff --git a/src/network/mcpe/convert/BlockStateDictionary.php b/src/network/mcpe/convert/BlockStateDictionary.php index 48b31e67f..584b7db62 100644 --- a/src/network/mcpe/convert/BlockStateDictionary.php +++ b/src/network/mcpe/convert/BlockStateDictionary.php @@ -27,27 +27,53 @@ use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\nbt\TreeRoot; use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer; use function array_map; +use function get_debug_type; +use function is_array; +use function is_int; +use function json_decode; /** * Handles translation of network block runtime IDs into blockstate data, and vice versa */ final class BlockStateDictionary{ - private BlockStateLookupCache $lookupCache; + private BlockStateLookupCache $stateDataToStateIdLookupCache; + /** + * @var int[][]|null + * @phpstan-var array>|null + */ + private ?array $idMetaToStateIdLookupCache = null; /** - * @param BlockStateData[] $states + * @param BlockStateDictionaryEntry[] $states * - * @phpstan-param list $states + * @phpstan-param list $states */ public function __construct( private array $states ){ - $this->lookupCache = new BlockStateLookupCache($this->states); + $this->stateDataToStateIdLookupCache = new BlockStateLookupCache(array_map(fn(BlockStateDictionaryEntry $entry) => $entry->getStateData(), $this->states)); + } + + /** + * @return int[][] + * @phpstan-return array> + */ + private function getIdMetaToStateIdLookup() : array{ + if($this->idMetaToStateIdLookupCache === null){ + //TODO: if we ever allow mutating the dictionary, this would need to be rebuilt on modification + $this->idMetaToStateIdLookupCache = []; + + foreach($this->states as $i => $state){ + $this->idMetaToStateIdLookupCache[$state->getMeta()][$state->getStateData()->getName()] = $i; + } + } + + return $this->idMetaToStateIdLookupCache; } public function getDataFromStateId(int $networkRuntimeId) : ?BlockStateData{ - return $this->states[$networkRuntimeId] ?? null; + return ($this->states[$networkRuntimeId] ?? null)?->getStateData(); } /** @@ -55,20 +81,55 @@ final class BlockStateDictionary{ * Returns null if there were no matches. */ public function lookupStateIdFromData(BlockStateData $data) : ?int{ - return $this->lookupCache->lookupStateId($data); + return $this->stateDataToStateIdLookupCache->lookupStateId($data); + } + + /** + * Returns the blockstate meta value associated with the given blockstate runtime ID. + * This is used for serializing crafting recipe inputs. + */ + public function getMetaFromStateId(int $networkRuntimeId) : ?int{ + return ($this->states[$networkRuntimeId] ?? null)?->getMeta(); + } + + /** + * Returns the blockstate data associated with the given block ID and meta value. + * This is used for deserializing crafting recipe inputs. + */ + public function lookupStateIdFromIdMeta(string $id, int $meta) : ?int{ + return $this->getIdMetaToStateIdLookup()[$meta][$id] ?? null; } /** * Returns an array mapping runtime ID => blockstate data. - * @return BlockStateData[] - * @phpstan-return array + * @return BlockStateDictionaryEntry[] + * @phpstan-return array */ public function getStates() : array{ return $this->states; } - public static function loadFromString(string $contents) : self{ - return new self(array_map( + public static function loadFromString(string $blockPaletteContents, string $metaMapContents) : self{ + $metaMap = json_decode($metaMapContents, flags: JSON_THROW_ON_ERROR); + if(!is_array($metaMap)){ + throw new \InvalidArgumentException("Invalid metaMap, expected array for root type, got " . get_debug_type($metaMap)); + } + + $entries = []; + $states = array_map( fn(TreeRoot $root) => BlockStateData::fromNbt($root->mustGetCompoundTag()), - (new NetworkNbtSerializer())->readMultiple($contents) - )); + (new NetworkNbtSerializer())->readMultiple($blockPaletteContents) + ); + + foreach($states as $i => $state){ + $meta = $metaMap[$i] ?? null; + if($meta === null){ + throw new \InvalidArgumentException("Missing associated meta value for state $i (" . $state->toNbt() . ")"); + } + if(!is_int($meta)){ + throw new \InvalidArgumentException("Invalid metaMap offset $i, expected int, got " . get_debug_type($meta)); + } + $entries[$i] = new BlockStateDictionaryEntry($state, $meta); + } + + return new self($entries); } } diff --git a/src/network/mcpe/convert/BlockStateDictionaryEntry.php b/src/network/mcpe/convert/BlockStateDictionaryEntry.php new file mode 100644 index 000000000..53229963b --- /dev/null +++ b/src/network/mcpe/convert/BlockStateDictionaryEntry.php @@ -0,0 +1,38 @@ +stateData; } + + public function getMeta() : int{ return $this->meta; } +} diff --git a/src/network/mcpe/convert/RuntimeBlockMapping.php b/src/network/mcpe/convert/RuntimeBlockMapping.php index c0373de11..9c9402d31 100644 --- a/src/network/mcpe/convert/RuntimeBlockMapping.php +++ b/src/network/mcpe/convert/RuntimeBlockMapping.php @@ -53,9 +53,12 @@ final class RuntimeBlockMapping{ private static function make() : self{ $canonicalBlockStatesFile = Path::join(\pocketmine\BEDROCK_DATA_PATH, "canonical_block_states.nbt"); - $contents = Utils::assumeNotFalse(file_get_contents($canonicalBlockStatesFile), "Missing required resource file"); + $canonicalBlockStatesRaw = Utils::assumeNotFalse(file_get_contents($canonicalBlockStatesFile), "Missing required resource file"); + + $metaMappingFile = Path::join(\pocketmine\BEDROCK_DATA_PATH, 'block_state_meta_map.json'); + $metaMappingRaw = Utils::assumeNotFalse(file_get_contents($metaMappingFile), "Missing required resource file"); return new self( - BlockStateDictionary::loadFromString($contents), + BlockStateDictionary::loadFromString($canonicalBlockStatesRaw, $metaMappingRaw), GlobalBlockStateHandlers::getSerializer() ); } diff --git a/src/network/mcpe/convert/TypeConverter.php b/src/network/mcpe/convert/TypeConverter.php index 653ffed75..4f58a2835 100644 --- a/src/network/mcpe/convert/TypeConverter.php +++ b/src/network/mcpe/convert/TypeConverter.php @@ -32,6 +32,7 @@ use pocketmine\block\VanillaBlocks; use pocketmine\crafting\ExactRecipeIngredient; use pocketmine\crafting\MetaWildcardRecipeIngredient; use pocketmine\crafting\RecipeIngredient; +use pocketmine\data\bedrock\item\BlockItemIdMap; use pocketmine\data\SavedDataLoadingException; use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction; @@ -126,11 +127,12 @@ class TypeConverter{ $meta = self::RECIPE_INPUT_WILDCARD_META; }elseif($ingredient instanceof ExactRecipeIngredient){ $item = $ingredient->getItem(); - [$id, $meta] = ItemTranslator::getInstance()->toNetworkId($item); - if($id < 256){ - //TODO: this is needed for block crafting recipes to work - we need to replace this with some kind of - //blockstate <-> meta mapping table so that we can remove the legacy code from the core - $meta = $item->getMeta(); + [$id, $meta, $blockRuntimeId] = ItemTranslator::getInstance()->toNetworkId($item); + if($blockRuntimeId !== ItemTranslator::NO_BLOCK_RUNTIME_ID){ + $meta = RuntimeBlockMapping::getInstance()->getBlockStateDictionary()->getMetaFromStateId($blockRuntimeId); + if($meta === null){ + throw new AssumptionFailedError("Every block state should have an associated meta value"); + } } }else{ throw new \LogicException("Unsupported recipe ingredient type " . get_class($ingredient) . ", only " . ExactRecipeIngredient::class . " and " . MetaWildcardRecipeIngredient::class . " are supported"); @@ -143,13 +145,21 @@ class TypeConverter{ return null; } + $itemId = GlobalItemTypeDictionary::getInstance()->getDictionary()->fromIntId($ingredient->getId()); + if($ingredient->getMeta() === self::RECIPE_INPUT_WILDCARD_META){ - $itemId = GlobalItemTypeDictionary::getInstance()->getDictionary()->fromIntId($ingredient->getId()); return new MetaWildcardRecipeIngredient($itemId); } - //TODO: this won't be handled properly for blockitems because a block runtimeID is expected rather than a meta value - $result = ItemTranslator::getInstance()->fromNetworkId($ingredient->getId(), $ingredient->getMeta(), 0); + $meta = $ingredient->getMeta(); + $blockRuntimeId = null; + if(($blockId = BlockItemIdMap::getInstance()->lookupBlockId($itemId)) !== null){ + $blockRuntimeId = RuntimeBlockMapping::getInstance()->getBlockStateDictionary()->lookupStateIdFromIdMeta($blockId, $meta); + if($blockRuntimeId !== null){ + $meta = 0; + } + } + $result = ItemTranslator::getInstance()->fromNetworkId($ingredient->getId(), $meta, $blockRuntimeId ?? ItemTranslator::NO_BLOCK_RUNTIME_ID); return new ExactRecipeIngredient($result); } diff --git a/tools/generate-block-palette-spec.php b/tools/generate-block-palette-spec.php index 330e53123..a9992ec1a 100644 --- a/tools/generate-block-palette-spec.php +++ b/tools/generate-block-palette-spec.php @@ -23,14 +23,19 @@ declare(strict_types=1); namespace pocketmine\tools\generate_block_palette_spec; +use pocketmine\data\bedrock\block\BlockStateData; use pocketmine\errorhandler\ErrorToExceptionHandler; use pocketmine\nbt\NbtException; use pocketmine\nbt\tag\ByteTag; use pocketmine\nbt\tag\IntTag; use pocketmine\nbt\tag\StringTag; +use pocketmine\nbt\TreeRoot; use pocketmine\network\mcpe\convert\BlockStateDictionary; +use pocketmine\network\mcpe\convert\BlockStateDictionaryEntry; +use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; +use function array_map; use function array_values; use function count; use function dirname; @@ -51,15 +56,26 @@ if(count($argv) !== 3){ [, $inputFile, $outputFile] = $argv; try{ - $palette = BlockStateDictionary::loadFromString(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents($inputFile))); + $states = array_map( + fn(TreeRoot $root) => BlockStateData::fromNbt($root->mustGetCompoundTag()), + (new NetworkNbtSerializer())->readMultiple(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents($inputFile))) + ); }catch(NbtException){ fwrite(STDERR, "Invalid block palette file $argv[1]\n"); exit(1); } +$entries = []; +$fakeMeta = []; +foreach($states as $state){ + $fakeMeta[$state->getName()] ??= 0; + $entries[] = new BlockStateDictionaryEntry($state, $fakeMeta[$state->getName()]++); +} +$palette = new BlockStateDictionary($entries); $reportMap = []; -foreach($palette->getStates() as $state){ +foreach($palette->getStates() as $entry){ + $state = $entry->getStateData(); $name = $state->getName(); foreach($state->getStates() as $propertyName => $value){ if($value instanceof IntTag || $value instanceof StringTag){