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": {
"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",

View File

@ -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<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(
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{
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<int, BlockStateData>
* @return BlockStateDictionaryEntry[]
* @phpstan-return array<int, BlockStateDictionaryEntry>
*/
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);
}
}

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{
$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()
);
}

View File

@ -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);
}

View File

@ -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){