Crafting recipe network serialization no longer depends on PM's internal legacy metadata

WOOOOOOOOOOOOOOOOOOOOOOHOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO!!!!!!!!!!!!!!!!!!!!!
This commit is contained in:
Dylan K. Taylor 2022-07-02 16:37:39 +01:00
parent 8858b16a25
commit 7994da07be
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
6 changed files with 156 additions and 28 deletions

8
composer.lock generated
View File

@ -280,12 +280,12 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/pmmp/BedrockData.git", "url": "https://github.com/pmmp/BedrockData.git",
"reference": "6a28ede3e9cdf1c548e85ce24382fee5f1bd9d75" "reference": "a546e15f6a8d7498fb25d5a02ce16184a429bb78"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/6a28ede3e9cdf1c548e85ce24382fee5f1bd9d75", "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/a546e15f6a8d7498fb25d5a02ce16184a429bb78",
"reference": "6a28ede3e9cdf1c548e85ce24382fee5f1bd9d75", "reference": "a546e15f6a8d7498fb25d5a02ce16184a429bb78",
"shasum": "" "shasum": ""
}, },
"type": "library", "type": "library",
@ -298,7 +298,7 @@
"issues": "https://github.com/pmmp/BedrockData/issues", "issues": "https://github.com/pmmp/BedrockData/issues",
"source": "https://github.com/pmmp/BedrockData/tree/modern-world-support" "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", "name": "pocketmine/bedrock-item-upgrade-schema",

View File

@ -27,27 +27,53 @@ use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\nbt\TreeRoot; use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer; use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use function array_map; 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 * Handles translation of network block runtime IDs into blockstate data, and vice versa
*/ */
final class BlockStateDictionary{ final class BlockStateDictionary{
private BlockStateLookupCache $lookupCache; private BlockStateLookupCache $stateDataToStateIdLookupCache;
/**
* @var int[][]|null
* @phpstan-var array<int, array<string, int>>|null
*/
private ?array $idMetaToStateIdLookupCache = null;
/** /**
* @param BlockStateData[] $states * @param BlockStateDictionaryEntry[] $states
* *
* @phpstan-param list<BlockStateData> $states * @phpstan-param list<BlockStateDictionaryEntry> $states
*/ */
public function __construct( public function __construct(
private array $states 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<int, array<string, int>>
*/
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{ 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. * Returns null if there were no matches.
*/ */
public function lookupStateIdFromData(BlockStateData $data) : ?int{ 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. * Returns an array mapping runtime ID => blockstate data.
* @return BlockStateData[] * @return BlockStateDictionaryEntry[]
* @phpstan-return array<int, BlockStateData> * @phpstan-return array<int, BlockStateDictionaryEntry>
*/ */
public function getStates() : array{ return $this->states; } public function getStates() : array{ return $this->states; }
public static function loadFromString(string $contents) : self{ public static function loadFromString(string $blockPaletteContents, string $metaMapContents) : self{
return new self(array_map( $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()), 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);
} }
} }

View File

@ -0,0 +1,38 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
use pocketmine\data\bedrock\block\BlockStateData;
final class BlockStateDictionaryEntry{
public function __construct(
private BlockStateData $stateData,
private int $meta
){}
public function getStateData() : BlockStateData{ return $this->stateData; }
public function getMeta() : int{ return $this->meta; }
}

View File

@ -53,9 +53,12 @@ final class RuntimeBlockMapping{
private static function make() : self{ private static function make() : self{
$canonicalBlockStatesFile = Path::join(\pocketmine\BEDROCK_DATA_PATH, "canonical_block_states.nbt"); $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( return new self(
BlockStateDictionary::loadFromString($contents), BlockStateDictionary::loadFromString($canonicalBlockStatesRaw, $metaMappingRaw),
GlobalBlockStateHandlers::getSerializer() GlobalBlockStateHandlers::getSerializer()
); );
} }

View File

@ -32,6 +32,7 @@ use pocketmine\block\VanillaBlocks;
use pocketmine\crafting\ExactRecipeIngredient; use pocketmine\crafting\ExactRecipeIngredient;
use pocketmine\crafting\MetaWildcardRecipeIngredient; use pocketmine\crafting\MetaWildcardRecipeIngredient;
use pocketmine\crafting\RecipeIngredient; use pocketmine\crafting\RecipeIngredient;
use pocketmine\data\bedrock\item\BlockItemIdMap;
use pocketmine\data\SavedDataLoadingException; use pocketmine\data\SavedDataLoadingException;
use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\CreateItemAction;
use pocketmine\inventory\transaction\action\DestroyItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction;
@ -126,11 +127,12 @@ class TypeConverter{
$meta = self::RECIPE_INPUT_WILDCARD_META; $meta = self::RECIPE_INPUT_WILDCARD_META;
}elseif($ingredient instanceof ExactRecipeIngredient){ }elseif($ingredient instanceof ExactRecipeIngredient){
$item = $ingredient->getItem(); $item = $ingredient->getItem();
[$id, $meta] = ItemTranslator::getInstance()->toNetworkId($item); [$id, $meta, $blockRuntimeId] = ItemTranslator::getInstance()->toNetworkId($item);
if($id < 256){ if($blockRuntimeId !== ItemTranslator::NO_BLOCK_RUNTIME_ID){
//TODO: this is needed for block crafting recipes to work - we need to replace this with some kind of $meta = RuntimeBlockMapping::getInstance()->getBlockStateDictionary()->getMetaFromStateId($blockRuntimeId);
//blockstate <-> meta mapping table so that we can remove the legacy code from the core if($meta === null){
$meta = $item->getMeta(); throw new AssumptionFailedError("Every block state should have an associated meta value");
}
} }
}else{ }else{
throw new \LogicException("Unsupported recipe ingredient type " . get_class($ingredient) . ", only " . ExactRecipeIngredient::class . " and " . MetaWildcardRecipeIngredient::class . " are supported"); 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; return null;
} }
$itemId = GlobalItemTypeDictionary::getInstance()->getDictionary()->fromIntId($ingredient->getId());
if($ingredient->getMeta() === self::RECIPE_INPUT_WILDCARD_META){ if($ingredient->getMeta() === self::RECIPE_INPUT_WILDCARD_META){
$itemId = GlobalItemTypeDictionary::getInstance()->getDictionary()->fromIntId($ingredient->getId());
return new MetaWildcardRecipeIngredient($itemId); return new MetaWildcardRecipeIngredient($itemId);
} }
//TODO: this won't be handled properly for blockitems because a block runtimeID is expected rather than a meta value $meta = $ingredient->getMeta();
$result = ItemTranslator::getInstance()->fromNetworkId($ingredient->getId(), $ingredient->getMeta(), 0); $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); return new ExactRecipeIngredient($result);
} }

View File

@ -23,14 +23,19 @@ declare(strict_types=1);
namespace pocketmine\tools\generate_block_palette_spec; namespace pocketmine\tools\generate_block_palette_spec;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\errorhandler\ErrorToExceptionHandler; use pocketmine\errorhandler\ErrorToExceptionHandler;
use pocketmine\nbt\NbtException; use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\ByteTag; use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\IntTag; use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\StringTag; use pocketmine\nbt\tag\StringTag;
use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\convert\BlockStateDictionary; 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\AssumptionFailedError;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use function array_map;
use function array_values; use function array_values;
use function count; use function count;
use function dirname; use function dirname;
@ -51,15 +56,26 @@ if(count($argv) !== 3){
[, $inputFile, $outputFile] = $argv; [, $inputFile, $outputFile] = $argv;
try{ 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){ }catch(NbtException){
fwrite(STDERR, "Invalid block palette file $argv[1]\n"); fwrite(STDERR, "Invalid block palette file $argv[1]\n");
exit(1); exit(1);
} }
$entries = [];
$fakeMeta = [];
foreach($states as $state){
$fakeMeta[$state->getName()] ??= 0;
$entries[] = new BlockStateDictionaryEntry($state, $fakeMeta[$state->getName()]++);
}
$palette = new BlockStateDictionary($entries);
$reportMap = []; $reportMap = [];
foreach($palette->getStates() as $state){ foreach($palette->getStates() as $entry){
$state = $entry->getStateData();
$name = $state->getName(); $name = $state->getName();
foreach($state->getStates() as $propertyName => $value){ foreach($state->getStates() as $propertyName => $value){
if($value instanceof IntTag || $value instanceof StringTag){ if($value instanceof IntTag || $value instanceof StringTag){