Burn meta wildcards from Item, allow more dynamic recipe inputs

this was an obstacle for getting rid of legacy item IDs.
This commit is contained in:
Dylan K. Taylor 2022-06-27 13:33:26 +01:00
parent bc5a600d59
commit 55cb68e5b5
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
20 changed files with 473 additions and 127 deletions

View File

@ -184,7 +184,7 @@ class CraftingManager{
public function registerPotionTypeRecipe(PotionTypeRecipe $recipe) : void{ public function registerPotionTypeRecipe(PotionTypeRecipe $recipe) : void{
$input = $recipe->getInput(); $input = $recipe->getInput();
$ingredient = $recipe->getIngredient(); $ingredient = $recipe->getIngredient();
$this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":" . ($ingredient->hasAnyDamageValue() ? "?" : $ingredient->getMeta())] = $recipe; $this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":" . $ingredient->getMeta()] = $recipe;
foreach($this->recipeRegisteredCallbacks as $callback){ foreach($this->recipeRegisteredCallbacks as $callback){
$callback(); $callback();
@ -193,7 +193,7 @@ class CraftingManager{
public function registerPotionContainerChangeRecipe(PotionContainerChangeRecipe $recipe) : void{ public function registerPotionContainerChangeRecipe(PotionContainerChangeRecipe $recipe) : void{
$ingredient = $recipe->getIngredient(); $ingredient = $recipe->getIngredient();
$this->potionContainerChangeRecipes[$recipe->getInputItemId()][$ingredient->getId() . ":" . ($ingredient->hasAnyDamageValue() ? "?" : $ingredient->getMeta())] = $recipe; $this->potionContainerChangeRecipes[$recipe->getInputItemId()][$ingredient->getId() . ":" . $ingredient->getMeta()] = $recipe;
foreach($this->recipeRegisteredCallbacks as $callback){ foreach($this->recipeRegisteredCallbacks as $callback){
$callback(); $callback();
@ -253,8 +253,6 @@ class CraftingManager{
public function matchBrewingRecipe(Item $input, Item $ingredient) : ?BrewingRecipe{ public function matchBrewingRecipe(Item $input, Item $ingredient) : ?BrewingRecipe{
return $this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":" . $ingredient->getMeta()] ?? return $this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":" . $ingredient->getMeta()] ??
$this->potionTypeRecipes[$input->getId() . ":" . $input->getMeta()][$ingredient->getId() . ":?"] ?? $this->potionContainerChangeRecipes[$input->getId()][$ingredient->getId() . ":" . $ingredient->getMeta()] ?? null;
$this->potionContainerChangeRecipes[$input->getId()][$ingredient->getId() . ":" . $ingredient->getMeta()] ??
$this->potionContainerChangeRecipes[$input->getId()][$ingredient->getId() . ":?"] ?? null;
} }
} }

View File

@ -23,14 +23,17 @@ declare(strict_types=1);
namespace pocketmine\crafting; namespace pocketmine\crafting;
use pocketmine\data\bedrock\item\ItemTypeDeserializeException;
use pocketmine\item\Durable; use pocketmine\item\Durable;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\item\ItemFactory; use pocketmine\item\ItemFactory;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use pocketmine\world\format\io\GlobalItemDataHandlers;
use function array_map; use function array_map;
use function file_get_contents; use function file_get_contents;
use function is_array; use function is_array;
use function is_int;
use function json_decode; use function json_decode;
final class CraftingManagerFromDataHelper{ final class CraftingManagerFromDataHelper{
@ -41,7 +44,7 @@ final class CraftingManagerFromDataHelper{
private static function containsUnknownItems(array $items) : bool{ private static function containsUnknownItems(array $items) : bool{
$factory = ItemFactory::getInstance(); $factory = ItemFactory::getInstance();
foreach($items as $item){ foreach($items as $item){
if($item instanceof Durable || $item->hasAnyDamageValue()){ if($item instanceof Durable){
//TODO: this check is imperfect and might cause problems if meta 0 isn't used for some reason //TODO: this check is imperfect and might cause problems if meta 0 isn't used for some reason
if(!$factory->isRegistered($item->getId())){ if(!$factory->isRegistered($item->getId())){
return true; return true;
@ -54,6 +57,30 @@ final class CraftingManagerFromDataHelper{
return false; return false;
} }
/**
* @param mixed[] $data
*/
private static function deserializeIngredient(array $data) : ?RecipeIngredient{
if(!isset($data["id"]) || !is_int($data["id"])){
throw new \InvalidArgumentException("Invalid input data, expected int ID");
}
if(isset($data["damage"]) && $data["damage"] === -1){
try{
$typeData = GlobalItemDataHandlers::getUpgrader()->upgradeItemTypeDataInt($data["id"], 0, 1, null);
}catch(ItemTypeDeserializeException){
//probably unknown item
return null;
}
return new MetaWildcardRecipeIngredient($typeData->getTypeData()->getName());
}
//TODO: we need to stop using jsonDeserialize for this
$item = Item::jsonDeserialize($data);
return self::containsUnknownItems([$item]) ? null : new ExactRecipeIngredient($item);
}
public static function make(string $filePath) : CraftingManager{ public static function make(string $filePath) : CraftingManager{
$recipes = json_decode(Utils::assumeNotFalse(file_get_contents($filePath), "Missing required resource file"), true); $recipes = json_decode(Utils::assumeNotFalse(file_get_contents($filePath), "Missing required resource file"), true);
if(!is_array($recipes)){ if(!is_array($recipes)){
@ -61,6 +88,7 @@ final class CraftingManagerFromDataHelper{
} }
$result = new CraftingManager(); $result = new CraftingManager();
$ingredientDeserializerFunc = \Closure::fromCallable([self::class, "deserializeIngredient"]);
$itemDeserializerFunc = \Closure::fromCallable([Item::class, 'jsonDeserialize']); $itemDeserializerFunc = \Closure::fromCallable([Item::class, 'jsonDeserialize']);
foreach($recipes["shapeless"] as $recipe){ foreach($recipes["shapeless"] as $recipe){
@ -73,9 +101,16 @@ final class CraftingManagerFromDataHelper{
if($recipeType === null){ if($recipeType === null){
continue; continue;
} }
$inputs = array_map($itemDeserializerFunc, $recipe["input"]); $inputs = [];
foreach($recipe["input"] as $inputData){
$input = $ingredientDeserializerFunc($inputData);
if($input === null){ //unknown input item
continue;
}
$inputs[] = $input;
}
$outputs = array_map($itemDeserializerFunc, $recipe["output"]); $outputs = array_map($itemDeserializerFunc, $recipe["output"]);
if(self::containsUnknownItems($inputs) || self::containsUnknownItems($outputs)){ if(self::containsUnknownItems($outputs)){
continue; continue;
} }
$result->registerShapelessRecipe(new ShapelessRecipe( $result->registerShapelessRecipe(new ShapelessRecipe(
@ -88,9 +123,16 @@ final class CraftingManagerFromDataHelper{
if($recipe["block"] !== "crafting_table"){ //TODO: filter others out for now to avoid breaking economics if($recipe["block"] !== "crafting_table"){ //TODO: filter others out for now to avoid breaking economics
continue; continue;
} }
$inputs = array_map($itemDeserializerFunc, $recipe["input"]); $inputs = [];
foreach($recipe["input"] as $symbol => $inputData){
$input = $ingredientDeserializerFunc($inputData);
if($input === null){ //unknown input item
continue;
}
$inputs[$symbol] = $input;
}
$outputs = array_map($itemDeserializerFunc, $recipe["output"]); $outputs = array_map($itemDeserializerFunc, $recipe["output"]);
if(self::containsUnknownItems($inputs) || self::containsUnknownItems($outputs)){ if(self::containsUnknownItems($outputs)){
continue; continue;
} }
$result->registerShapedRecipe(new ShapedRecipe( $result->registerShapedRecipe(new ShapedRecipe(
@ -111,8 +153,8 @@ final class CraftingManagerFromDataHelper{
continue; continue;
} }
$output = Item::jsonDeserialize($recipe["output"]); $output = Item::jsonDeserialize($recipe["output"]);
$input = Item::jsonDeserialize($recipe["input"]); $input = self::deserializeIngredient($recipe["input"]);
if(self::containsUnknownItems([$output, $input])){ if($input === null || self::containsUnknownItems([$output])){
continue; continue;
} }
$result->getFurnaceRecipeManager($furnaceType)->register(new FurnaceRecipe( $result->getFurnaceRecipeManager($furnaceType)->register(new FurnaceRecipe(
@ -135,9 +177,9 @@ final class CraftingManagerFromDataHelper{
)); ));
} }
foreach($recipes["potion_container_change"] as $recipe){ foreach($recipes["potion_container_change"] as $recipe){
$input = ItemFactory::getInstance()->get($recipe["input_item_id"], -1); $input = ItemFactory::getInstance()->get($recipe["input_item_id"]);
$ingredient = Item::jsonDeserialize($recipe["ingredient"]); $ingredient = Item::jsonDeserialize($recipe["ingredient"]);
$output = ItemFactory::getInstance()->get($recipe["output_item_id"], -1); $output = ItemFactory::getInstance()->get($recipe["output_item_id"]);
if(self::containsUnknownItems([$input, $ingredient, $output])){ if(self::containsUnknownItems([$input, $ingredient, $output])){
continue; continue;

View File

@ -29,7 +29,7 @@ interface CraftingRecipe{
/** /**
* Returns a list of items needed to craft this recipe. This MUST NOT include Air items or items with a zero count. * Returns a list of items needed to craft this recipe. This MUST NOT include Air items or items with a zero count.
* *
* @return Item[] * @return RecipeIngredient[]
*/ */
public function getIngredientList() : array; public function getIngredientList() : array;

View File

@ -0,0 +1,56 @@
<?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\crafting;
use pocketmine\item\Item;
/**
* Recipe ingredient that matches exactly one item, without wildcards.
* Note that recipe inputs cannot require NBT.
*/
final class ExactRecipeIngredient implements RecipeIngredient{
public function __construct(private Item $item){
if($item->isNull()){
throw new \InvalidArgumentException("Recipe ingredients must not be air items");
}
if($item->getCount() !== 1){
throw new \InvalidArgumentException("Recipe ingredients cannot require count");
}
$this->item = clone $item;
}
public function getItem() : Item{ return clone $this->item; }
public function accepts(Item $item) : bool{
//client-side, recipe inputs can't actually require NBT
//but on the PM side, we currently check for it if the input requires it, so we have to continue to do so for
//the sake of consistency
return $item->getCount() >= 1 && $this->item->equals($item, true, $this->item->hasNamedTag());
}
public function __toString() : string{
return "ExactRecipeIngredient(" . $this->item . ")";
}
}

View File

@ -29,14 +29,13 @@ class FurnaceRecipe{
public function __construct( public function __construct(
private Item $result, private Item $result,
private Item $ingredient private RecipeIngredient $ingredient
){ ){
$this->result = clone $result; $this->result = clone $result;
$this->ingredient = clone $ingredient;
} }
public function getInput() : Item{ public function getInput() : RecipeIngredient{
return clone $this->ingredient; return $this->ingredient;
} }
public function getResult() : Item{ public function getResult() : Item{

View File

@ -25,11 +25,18 @@ namespace pocketmine\crafting;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\utils\ObjectSet; use pocketmine\utils\ObjectSet;
use function morton2d_encode;
final class FurnaceRecipeManager{ final class FurnaceRecipeManager{
/** @var FurnaceRecipe[] */ /** @var FurnaceRecipe[] */
protected array $furnaceRecipes = []; protected array $furnaceRecipes = [];
/**
* @var FurnaceRecipe[]
* @phpstan-var array<int, FurnaceRecipe>
*/
private array $lookupCache = [];
/** @phpstan-var ObjectSet<\Closure(FurnaceRecipe) : void> */ /** @phpstan-var ObjectSet<\Closure(FurnaceRecipe) : void> */
private ObjectSet $recipeRegisteredCallbacks; private ObjectSet $recipeRegisteredCallbacks;
@ -52,14 +59,27 @@ final class FurnaceRecipeManager{
} }
public function register(FurnaceRecipe $recipe) : void{ public function register(FurnaceRecipe $recipe) : void{
$input = $recipe->getInput(); $this->furnaceRecipes[] = $recipe;
$this->furnaceRecipes[$input->getId() . ":" . ($input->hasAnyDamageValue() ? "?" : $input->getMeta())] = $recipe;
foreach($this->recipeRegisteredCallbacks as $callback){ foreach($this->recipeRegisteredCallbacks as $callback){
$callback($recipe); $callback($recipe);
} }
} }
public function match(Item $input) : ?FurnaceRecipe{ public function match(Item $input) : ?FurnaceRecipe{
return $this->furnaceRecipes[$input->getId() . ":" . $input->getMeta()] ?? $this->furnaceRecipes[$input->getId() . ":?"] ?? null; $index = morton2d_encode($input->getId(), $input->getMeta());
$simpleRecipe = $this->lookupCache[$index] ?? null;
if($simpleRecipe !== null){
return $simpleRecipe;
}
foreach($this->furnaceRecipes as $recipe){
if($recipe->getInput()->accepts($input)){
//remember that this item is accepted by this recipe, so we don't need to bruteforce it again
$this->lookupCache[$index] = $recipe;
return $recipe;
}
}
return null;
} }
} }

View File

@ -0,0 +1,57 @@
<?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\crafting;
use pocketmine\item\Item;
use pocketmine\world\format\io\GlobalItemDataHandlers;
/**
* Recipe ingredient that matches items by their Minecraft ID only. This is used for things like the crafting table
* recipe from planks (multiple types of planks are accepted).
*
* WARNING: Plugins shouldn't usually use this. This is a hack that relies on internal Minecraft behaviour, which might
* change or break at any time.
*
* @internal
*/
final class MetaWildcardRecipeIngredient implements RecipeIngredient{
public function __construct(
private string $itemId,
){}
public function getItemId() : string{ return $this->itemId; }
public function accepts(Item $item) : bool{
if($item->getCount() < 1){
return false;
}
return GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName() === $this->itemId;
}
public function __toString() : string{
return "MetaWildcardRecipeIngredient($this->itemId)";
}
}

View File

@ -0,0 +1,31 @@
<?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\crafting;
use pocketmine\item\Item;
interface RecipeIngredient extends \Stringable{
public function accepts(Item $item) : bool;
}

View File

@ -24,7 +24,6 @@ declare(strict_types=1);
namespace pocketmine\crafting; namespace pocketmine\crafting;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\utils\Utils; use pocketmine\utils\Utils;
use function array_values; use function array_values;
use function count; use function count;
@ -35,7 +34,7 @@ use function strpos;
class ShapedRecipe implements CraftingRecipe{ class ShapedRecipe implements CraftingRecipe{
/** @var string[] */ /** @var string[] */
private array $shape = []; private array $shape = [];
/** @var Item[] char => Item map */ /** @var RecipeIngredient[] char => RecipeIngredient map */
private array $ingredientList = []; private array $ingredientList = [];
/** @var Item[] */ /** @var Item[] */
private array $results = []; private array $results = [];
@ -46,15 +45,15 @@ class ShapedRecipe implements CraftingRecipe{
/** /**
* Constructs a ShapedRecipe instance. * Constructs a ShapedRecipe instance.
* *
* @param string[] $shape <br> * @param string[] $shape <br>
* Array of 1, 2, or 3 strings representing the rows of the recipe. * Array of 1, 2, or 3 strings representing the rows of the recipe.
* This accepts an array of 1, 2 or 3 strings. Each string should be of the same length and must be at most 3 * This accepts an array of 1, 2 or 3 strings. Each string should be of the same length and must be at most 3
* characters long. Each character represents a unique type of ingredient. Spaces are interpreted as air. * characters long. Each character represents a unique type of ingredient. Spaces are interpreted as air.
* @param Item[] $ingredients <br> * @param RecipeIngredient[] $ingredients <br>
* Char => Item map of items to be set into the shape. * Char => Item map of items to be set into the shape.
* This accepts an array of Items, indexed by character. Every unique character (except space) in the shape * This accepts an array of Items, indexed by character. Every unique character (except space) in the shape
* array MUST have a corresponding item in this list. Space character is automatically treated as air. * array MUST have a corresponding item in this list. Space character is automatically treated as air.
* @param Item[] $results List of items that this recipe produces when crafted. * @param Item[] $results List of items that this recipe produces when crafted.
* *
* Note: Recipes **do not** need to be square. Do NOT add padding for empty rows/columns. * Note: Recipes **do not** need to be square. Do NOT add padding for empty rows/columns.
*/ */
@ -119,7 +118,7 @@ class ShapedRecipe implements CraftingRecipe{
} }
/** /**
* @return Item[][] * @return (RecipeIngredient|null)[][]
*/ */
public function getIngredientMap() : array{ public function getIngredientMap() : array{
$ingredients = []; $ingredients = [];
@ -134,7 +133,7 @@ class ShapedRecipe implements CraftingRecipe{
} }
/** /**
* @return Item[] * @return RecipeIngredient[]
*/ */
public function getIngredientList() : array{ public function getIngredientList() : array{
$ingredients = []; $ingredients = [];
@ -142,7 +141,7 @@ class ShapedRecipe implements CraftingRecipe{
for($y = 0; $y < $this->height; ++$y){ for($y = 0; $y < $this->height; ++$y){
for($x = 0; $x < $this->width; ++$x){ for($x = 0; $x < $this->width; ++$x){
$ingredient = $this->getIngredient($x, $y); $ingredient = $this->getIngredient($x, $y);
if(!$ingredient->isNull()){ if($ingredient !== null){
$ingredients[] = $ingredient; $ingredients[] = $ingredient;
} }
} }
@ -151,9 +150,8 @@ class ShapedRecipe implements CraftingRecipe{
return $ingredients; return $ingredients;
} }
public function getIngredient(int $x, int $y) : Item{ public function getIngredient(int $x, int $y) : ?RecipeIngredient{
$exists = $this->ingredientList[$this->shape[$y][$x]] ?? null; return $this->ingredientList[$this->shape[$y][$x]] ?? null;
return $exists !== null ? clone $exists : VanillaItems::AIR();
} }
/** /**
@ -170,7 +168,12 @@ class ShapedRecipe implements CraftingRecipe{
$given = $grid->getIngredient($reverse ? $this->width - $x - 1 : $x, $y); $given = $grid->getIngredient($reverse ? $this->width - $x - 1 : $x, $y);
$required = $this->getIngredient($x, $y); $required = $this->getIngredient($x, $y);
if(!$required->equals($given, !$required->hasAnyDamageValue(), $required->hasNamedTag()) || $required->getCount() > $given->getCount()){
if($required === null){
if(!$given->isNull()){
return false; //hole, such as that in the center of a chest recipe, should not be filled
}
}elseif(!$required->accepts($given)){
return false; return false;
} }
} }

View File

@ -28,30 +28,24 @@ use pocketmine\utils\Utils;
use function count; use function count;
class ShapelessRecipe implements CraftingRecipe{ class ShapelessRecipe implements CraftingRecipe{
/** @var Item[] */ /** @var RecipeIngredient[] */
private array $ingredients = []; private array $ingredients = [];
/** @var Item[] */ /** @var Item[] */
private array $results; private array $results;
private ShapelessRecipeType $type; private ShapelessRecipeType $type;
/** /**
* @param Item[] $ingredients No more than 9 total. This applies to sum of item stack counts, not count of array. * @param RecipeIngredient[] $ingredients No more than 9 total. This applies to sum of item stack counts, not count of array.
* @param Item[] $results List of result items created by this recipe. * @param Item[] $results List of result items created by this recipe.
* TODO: we'll want to make the type parameter mandatory in PM5 * TODO: we'll want to make the type parameter mandatory in PM5
*/ */
public function __construct(array $ingredients, array $results, ?ShapelessRecipeType $type = null){ public function __construct(array $ingredients, array $results, ?ShapelessRecipeType $type = null){
$this->type = $type ?? ShapelessRecipeType::CRAFTING(); $this->type = $type ?? ShapelessRecipeType::CRAFTING();
foreach($ingredients as $item){
//Ensure they get split up properly
if(count($this->ingredients) + $item->getCount() > 9){
throw new \InvalidArgumentException("Shapeless recipes cannot have more than 9 ingredients");
}
while($item->getCount() > 0){ if(count($ingredients) > 9){
$this->ingredients[] = $item->pop(); throw new \InvalidArgumentException("Shapeless recipes cannot have more than 9 ingredients");
}
} }
$this->ingredients = $ingredients;
$this->results = Utils::cloneObjectArray($results); $this->results = Utils::cloneObjectArray($results);
} }
@ -71,28 +65,23 @@ class ShapelessRecipe implements CraftingRecipe{
} }
/** /**
* @return Item[] * @return RecipeIngredient[]
*/ */
public function getIngredientList() : array{ public function getIngredientList() : array{
return Utils::cloneObjectArray($this->ingredients); return $this->ingredients;
} }
public function getIngredientCount() : int{ public function getIngredientCount() : int{
$count = 0; return count($this->ingredients);
foreach($this->ingredients as $ingredient){
$count += $ingredient->getCount();
}
return $count;
} }
public function matchesCraftingGrid(CraftingGrid $grid) : bool{ public function matchesCraftingGrid(CraftingGrid $grid) : bool{
//don't pack the ingredients - shapeless recipes require that each ingredient be in a separate slot //don't pack the ingredients - shapeless recipes require that each ingredient be in a separate slot
$input = $grid->getContents(); $input = $grid->getContents();
foreach($this->ingredients as $needItem){ foreach($this->ingredients as $ingredient){
foreach($input as $j => $haveItem){ foreach($input as $j => $haveItem){
if($haveItem->equals($needItem, !$needItem->hasAnyDamageValue(), $needItem->hasNamedTag()) && $haveItem->getCount() >= $needItem->getCount()){ if($ingredient->accepts($haveItem)){
unset($input[$j]); unset($input[$j]);
continue 2; continue 2;
} }

View File

@ -72,9 +72,6 @@ final class ItemSerializer{
* @phpstan-param \Closure(TItemType) : Data $serializer * @phpstan-param \Closure(TItemType) : Data $serializer
*/ */
public function map(Item $item, \Closure $serializer) : void{ public function map(Item $item, \Closure $serializer) : void{
if($item->hasAnyDamageValue()){
throw new \InvalidArgumentException("Cannot serialize a recipe wildcard");
}
$index = $item->getTypeId(); $index = $item->getTypeId();
if(isset($this->itemSerializers[$index])){ if(isset($this->itemSerializers[$index])){
//TODO: REMOVE ME //TODO: REMOVE ME
@ -106,9 +103,6 @@ final class ItemSerializer{
if($item->isNull()){ if($item->isNull()){
throw new \InvalidArgumentException("Cannot serialize a null itemstack"); throw new \InvalidArgumentException("Cannot serialize a null itemstack");
} }
if($item->hasAnyDamageValue()){
throw new \InvalidArgumentException("Cannot serialize a recipe input as a saved itemstack");
}
if($item instanceof ItemBlock){ if($item instanceof ItemBlock){
$data = $this->serializeBlockItem($item->getBlock()); $data = $this->serializeBlockItem($item->getBlock());
}else{ }else{

View File

@ -70,6 +70,47 @@ final class ItemDataUpgrader{
ksort($this->idMetaUpgradeSchemas, SORT_NUMERIC); ksort($this->idMetaUpgradeSchemas, SORT_NUMERIC);
} }
/**
* This function replaces the legacy ItemFactory::get().
*
* Unlike ItemFactory::get(), it returns a SavedItemStackData which you can do with as you please.
* If you want to deserialize it into a PocketMine-MP itemstack, pass it to the ItemDeserializer.
*
* @see ItemDataUpgrader::upgradeItemTypeDataInt()
*/
public function upgradeItemTypeDataString(string $rawNameId, int $meta, int $count, ?CompoundTag $nbt) : SavedItemStackData{
if(($r12BlockId = $this->r12ItemIdToBlockIdMap->itemIdToBlockId($rawNameId)) !== null){
$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 SavedItemStackData(
new SavedItemData($newNameId, $newMeta, $blockStateData, $nbt),
$count,
null,
null,
[],
[]
);
}
/**
* This function replaces the legacy ItemFactory::get().
*/
public function upgradeItemTypeDataInt(int $legacyNumericId, int $meta, int $count, ?CompoundTag $nbt) : SavedItemStackData{
$rawNameId = $this->legacyIntToStringIdMap->legacyToString($legacyNumericId);
if($rawNameId === null){
throw new SavedDataLoadingException("Unmapped legacy item ID $legacyNumericId");
}
return $this->upgradeItemTypeDataString($rawNameId, $meta, $count, $nbt);
}
/** /**
* @throws SavedDataLoadingException * @throws SavedDataLoadingException
*/ */

View File

@ -105,10 +105,9 @@ abstract class BaseInventory implements Inventory{
public function contains(Item $item) : bool{ public function contains(Item $item) : bool{
$count = max(1, $item->getCount()); $count = max(1, $item->getCount());
$checkDamage = !$item->hasAnyDamageValue();
$checkTags = $item->hasNamedTag(); $checkTags = $item->hasNamedTag();
foreach($this->getContents() as $i){ foreach($this->getContents() as $i){
if($item->equals($i, $checkDamage, $checkTags)){ if($item->equals($i, true, $checkTags)){
$count -= $i->getCount(); $count -= $i->getCount();
if($count <= 0){ if($count <= 0){
return true; return true;
@ -121,23 +120,22 @@ abstract class BaseInventory implements Inventory{
public function all(Item $item) : array{ public function all(Item $item) : array{
$slots = []; $slots = [];
$checkDamage = !$item->hasAnyDamageValue();
$checkTags = $item->hasNamedTag(); $checkTags = $item->hasNamedTag();
foreach($this->getContents() as $index => $i){ foreach($this->getContents() as $index => $i){
if($item->equals($i, $checkDamage, $checkTags)){ if($item->equals($i, true, $checkTags)){
$slots[$index] = $i; $slots[$index] = $i;
} }
} }
return $slots; return $slots;
} }
public function first(Item $item, bool $exact = false) : int{ public function first(Item $item, bool $exact = false) : int{
$count = $exact ? $item->getCount() : max(1, $item->getCount()); $count = $exact ? $item->getCount() : max(1, $item->getCount());
$checkDamage = $exact || !$item->hasAnyDamageValue();
$checkTags = $exact || $item->hasNamedTag(); $checkTags = $exact || $item->hasNamedTag();
foreach($this->getContents() as $index => $i){ foreach($this->getContents() as $index => $i){
if($item->equals($i, $checkDamage, $checkTags) && ($i->getCount() === $count || (!$exact && $i->getCount() > $count))){ if($item->equals($i, true, $checkTags) && ($i->getCount() === $count || (!$exact && $i->getCount() > $count))){
return $index; return $index;
} }
} }
@ -245,11 +243,10 @@ abstract class BaseInventory implements Inventory{
} }
public function remove(Item $item) : void{ public function remove(Item $item) : void{
$checkDamage = !$item->hasAnyDamageValue();
$checkTags = $item->hasNamedTag(); $checkTags = $item->hasNamedTag();
foreach($this->getContents() as $index => $i){ foreach($this->getContents() as $index => $i){
if($item->equals($i, $checkDamage, $checkTags)){ if($item->equals($i, true, $checkTags)){
$this->clear($index); $this->clear($index);
} }
} }
@ -272,7 +269,7 @@ abstract class BaseInventory implements Inventory{
} }
foreach($itemSlots as $index => $slot){ foreach($itemSlots as $index => $slot){
if($slot->equals($item, !$slot->hasAnyDamageValue(), $slot->hasNamedTag())){ if($slot->equals($item, true, $slot->hasNamedTag())){
$amount = min($item->getCount(), $slot->getCount()); $amount = min($item->getCount(), $slot->getCount());
$slot->setCount($slot->getCount() - $amount); $slot->setCount($slot->getCount() - $amount);
$item->setCount($item->getCount() - $amount); $item->setCount($item->getCount() - $amount);

View File

@ -25,12 +25,18 @@ namespace pocketmine\inventory\transaction;
use pocketmine\crafting\CraftingManager; use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\CraftingRecipe; use pocketmine\crafting\CraftingRecipe;
use pocketmine\crafting\RecipeIngredient;
use pocketmine\event\inventory\CraftItemEvent; use pocketmine\event\inventory\CraftItemEvent;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\utils\Utils;
use function array_fill_keys;
use function array_keys;
use function array_pop; use function array_pop;
use function count; use function count;
use function intdiv; use function intdiv;
use function min;
use function uasort;
/** /**
* This transaction type is specialized for crafting validation. It shares most of the same semantics of the base * This transaction type is specialized for crafting validation. It shares most of the same semantics of the base
@ -63,13 +69,110 @@ class CraftingTransaction extends InventoryTransaction{
$this->craftingManager = $craftingManager; $this->craftingManager = $craftingManager;
} }
/**
* @param Item[] $providedItems
* @return Item[]
*/
private static function packItems(array $providedItems) : array{
$packedProvidedItems = [];
while(count($providedItems) > 0){
$item = array_pop($providedItems);
foreach($providedItems as $k => $otherItem){
if($item->canStackWith($otherItem)){
$item->setCount($item->getCount() + $otherItem->getCount());
unset($providedItems[$k]);
}
}
$packedProvidedItems[] = $item;
}
return $packedProvidedItems;
}
/**
* @param Item[] $providedItems
* @param RecipeIngredient[] $recipeIngredients
*/
public static function matchIngredients(array $providedItems, array $recipeIngredients, int $expectedIterations) : void{
if(count($recipeIngredients) === 0){
throw new TransactionValidationException("No recipe ingredients given");
}
if(count($providedItems) === 0){
throw new TransactionValidationException("No transaction items given");
}
$packedProvidedItems = self::packItems(Utils::cloneObjectArray($providedItems));
$packedProvidedItemMatches = array_fill_keys(array_keys($packedProvidedItems), 0);
$recipeIngredientMatches = [];
foreach($recipeIngredients as $ingredientIndex => $recipeIngredient){
$acceptedItems = [];
foreach($packedProvidedItems as $itemIndex => $packedItem){
if($recipeIngredient->accepts($packedItem)){
$packedProvidedItemMatches[$itemIndex]++;
$acceptedItems[$itemIndex] = $itemIndex;
}
}
if(count($acceptedItems) === 0){
throw new TransactionValidationException("No provided items satisfy ingredient requirement $recipeIngredient");
}
$recipeIngredientMatches[$ingredientIndex] = $acceptedItems;
}
foreach($packedProvidedItemMatches as $itemIndex => $itemMatchCount){
if($itemMatchCount === 0){
$item = $packedProvidedItems[$itemIndex];
throw new TransactionValidationException("Provided item $item is not accepted by any recipe ingredient");
}
}
//Most picky ingredients first - avoid picky ingredient getting their items stolen by wildcard ingredients
//TODO: this is still insufficient when multiple wildcard ingredients have overlaps, but we don't (yet) have to
//worry about those.
uasort($recipeIngredientMatches, fn(array $a, array $b) => count($a) <=> count($b));
foreach($recipeIngredientMatches as $ingredientIndex => $acceptedItems){
$needed = $expectedIterations;
foreach($packedProvidedItems as $itemIndex => $item){
if(!isset($acceptedItems[$itemIndex])){
continue;
}
$taken = min($needed, $item->getCount());
$needed -= $taken;
$item->setCount($item->getCount() - $taken);
if($item->getCount() === 0){
unset($packedProvidedItems[$itemIndex]);
}
if($needed === 0){
//validation passed!
continue 2;
}
}
$recipeIngredient = $recipeIngredients[$ingredientIndex];
$actualIterations = $expectedIterations - $needed;
throw new TransactionValidationException("Not enough items to satisfy recipe ingredient $recipeIngredient for $expectedIterations (only have enough items for $actualIterations iterations)");
}
if(count($packedProvidedItems) > 0){
throw new TransactionValidationException("Not all provided items were used");
}
}
/** /**
* @param Item[] $txItems * @param Item[] $txItems
* @param Item[] $recipeItems * @param Item[] $recipeItems
* *
* @throws TransactionValidationException * @throws TransactionValidationException
*/ */
protected function matchRecipeItems(array $txItems, array $recipeItems, bool $wildcards, int $iterations = 0) : int{ protected function matchOutputs(array $txItems, array $recipeItems) : int{
if(count($recipeItems) === 0){ if(count($recipeItems) === 0){
throw new TransactionValidationException("No recipe items given"); throw new TransactionValidationException("No recipe items given");
} }
@ -77,6 +180,7 @@ class CraftingTransaction extends InventoryTransaction{
throw new TransactionValidationException("No transaction items given"); throw new TransactionValidationException("No transaction items given");
} }
$iterations = 0;
while(count($recipeItems) > 0){ while(count($recipeItems) > 0){
/** @var Item $recipeItem */ /** @var Item $recipeItem */
$recipeItem = array_pop($recipeItems); $recipeItem = array_pop($recipeItems);
@ -90,7 +194,7 @@ class CraftingTransaction extends InventoryTransaction{
$haveCount = 0; $haveCount = 0;
foreach($txItems as $j => $txItem){ foreach($txItems as $j => $txItem){
if($txItem->equals($recipeItem, !$wildcards || !$recipeItem->hasAnyDamageValue(), !$wildcards || $recipeItem->hasNamedTag())){ if($txItem->canStackWith($recipeItem)){
$haveCount += $txItem->getCount(); $haveCount += $txItem->getCount();
unset($txItems[$j]); unset($txItems[$j]);
} }
@ -115,7 +219,7 @@ class CraftingTransaction extends InventoryTransaction{
if(count($txItems) > 0){ if(count($txItems) > 0){
//all items should be destroyed in this process //all items should be destroyed in this process
throw new TransactionValidationException("Expected 0 ingredients left over, have " . count($txItems)); throw new TransactionValidationException("Expected 0 items left over, have " . count($txItems));
} }
return $iterations; return $iterations;
@ -133,9 +237,9 @@ class CraftingTransaction extends InventoryTransaction{
foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){ foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
try{ try{
//compute number of times recipe was crafted //compute number of times recipe was crafted
$this->repetitions = $this->matchRecipeItems($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()), false); $this->repetitions = $this->matchOutputs($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()));
//assert that $repetitions x recipe ingredients should be consumed //assert that $repetitions x recipe ingredients should be consumed
$this->matchRecipeItems($this->inputs, $recipe->getIngredientList(), true, $this->repetitions); self::matchIngredients($this->inputs, $recipe->getIngredientList(), $this->repetitions);
//Success! //Success!
$this->recipe = $recipe; $this->recipe = $recipe;

View File

@ -581,7 +581,7 @@ class Item implements \JsonSerializable{
} }
final public function __toString() : string{ final public function __toString() : string{
return "Item " . $this->name . " (" . $this->getId() . ":" . ($this->hasAnyDamageValue() ? "?" : $this->getMeta()) . ")x" . $this->count . ($this->hasNamedTag() ? " tags:0x" . base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($this->getNamedTag()))) : ""); return "Item " . $this->name . " (" . $this->getId() . ":" . $this->getMeta() . ")x" . $this->count . ($this->hasNamedTag() ? " tags:0x" . base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($this->getNamedTag()))) : "");
} }
/** /**
@ -647,7 +647,7 @@ class Item implements \JsonSerializable{
* @param int $slot optional, the inventory slot of the item * @param int $slot optional, the inventory slot of the item
*/ */
public function nbtSerialize(int $slot = -1) : CompoundTag{ public function nbtSerialize(int $slot = -1) : CompoundTag{
return GlobalItemDataHandlers::getSerializer()->serializeStack($this, $slot !== -1 ? $slot : null); return GlobalItemDataHandlers::getSerializer()->serializeStack($this, $slot !== -1 ? $slot : null)->toNbt();
} }
/** /**

View File

@ -457,27 +457,30 @@ class ItemFactory{
public function get(int $id, int $meta = 0, int $count = 1, ?CompoundTag $tags = null) : Item{ public function get(int $id, int $meta = 0, int $count = 1, ?CompoundTag $tags = null) : Item{
/** @var Item|null $item */ /** @var Item|null $item */
$item = null; $item = null;
if($meta !== -1){
if(isset($this->list[$offset = self::getListOffset($id, $meta)])){ if($meta < 0 || $meta > 0x7ffe){ //0x7fff would cause problems with recipe wildcards
$item = clone $this->list[$offset]; throw new \InvalidArgumentException("Meta cannot be negative or larger than " . 0x7ffe);
}elseif(isset($this->list[$zero = self::getListOffset($id, 0)]) && $this->list[$zero] instanceof Durable){ }
if($meta <= $this->list[$zero]->getMaxDurability()){
$item = clone $this->list[$zero]; if(isset($this->list[$offset = self::getListOffset($id, $meta)])){
$item->setDamage($meta); $item = clone $this->list[$offset];
}else{ }elseif(isset($this->list[$zero = self::getListOffset($id, 0)]) && $this->list[$zero] instanceof Durable){
$item = new Item(new IID($id, $meta)); if($meta <= $this->list[$zero]->getMaxDurability()){
} $item = clone $this->list[$zero];
}elseif($id < 256){ //intentionally includes negatives, for extended block IDs $item->setDamage($meta);
//TODO: do not assume that item IDs and block IDs are the same or related }else{
$blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta(self::itemToBlockId($id), $meta & 0xf); $item = new Item(new IID($id, $meta));
if($blockStateData !== null){ }
try{ }elseif($id < 256){ //intentionally includes negatives, for extended block IDs
$blockStateId = GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData); //TODO: do not assume that item IDs and block IDs are the same or related
$item = new ItemBlock(new IID($id, $meta), BlockFactory::getInstance()->fromFullBlock($blockStateId)); $blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta(self::itemToBlockId($id), $meta & 0xf);
}catch(BlockStateDeserializeException $e){ if($blockStateData !== null){
\GlobalLogger::get()->logException($e); try{
//fallthru $blockStateId = GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData);
} $item = new ItemBlock(new IID($id, $meta), BlockFactory::getInstance()->fromFullBlock($blockStateId));
}catch(BlockStateDeserializeException $e){
\GlobalLogger::get()->logException($e);
//fallthru
} }
} }
} }

View File

@ -31,8 +31,11 @@ class ItemIdentifier{
if($id < -0x8000 || $id > 0x7fff){ //signed short range if($id < -0x8000 || $id > 0x7fff){ //signed short range
throw new \InvalidArgumentException("ID must be in range " . -0x8000 . " - " . 0x7fff); throw new \InvalidArgumentException("ID must be in range " . -0x8000 . " - " . 0x7fff);
} }
if($meta < 0 || $meta > 0x7ffe){
throw new \InvalidArgumentException("Meta must be in range 0 - " . 0x7ffe);
}
$this->id = $id; $this->id = $id;
$this->meta = $meta !== -1 ? $meta & 0x7FFF : -1; $this->meta = $meta;
} }
public function getId() : int{ public function getId() : int{

View File

@ -104,6 +104,9 @@ final class LegacyStringToItemParser{
$meta = 0; $meta = 0;
}elseif(is_numeric($b[1])){ }elseif(is_numeric($b[1])){
$meta = (int) $b[1]; $meta = (int) $b[1];
if($meta < 0 || $meta > 0x7ffe){
throw new LegacyStringToItemParserException("Meta value $meta is outside the range 0 - " . 0x7ffe);
}
}else{ }else{
throw new LegacyStringToItemParserException("Unable to parse \"" . $b[1] . "\" from \"" . $input . "\" as a valid meta value"); throw new LegacyStringToItemParserException("Unable to parse \"" . $b[1] . "\" from \"" . $input . "\" as a valid meta value");
} }

View File

@ -25,6 +25,7 @@ namespace pocketmine\network\mcpe\cache;
use pocketmine\crafting\CraftingManager; use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\FurnaceType; use pocketmine\crafting\FurnaceType;
use pocketmine\crafting\RecipeIngredient;
use pocketmine\crafting\ShapelessRecipeType; use pocketmine\crafting\ShapelessRecipeType;
use pocketmine\item\Item; use pocketmine\item\Item;
use pocketmine\item\ItemFactory; use pocketmine\item\ItemFactory;
@ -37,7 +38,7 @@ use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipe as ProtocolFurna
use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipeBlockName; use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipeBlockName;
use pocketmine\network\mcpe\protocol\types\recipe\PotionContainerChangeRecipe as ProtocolPotionContainerChangeRecipe; use pocketmine\network\mcpe\protocol\types\recipe\PotionContainerChangeRecipe as ProtocolPotionContainerChangeRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\PotionTypeRecipe as ProtocolPotionTypeRecipe; use pocketmine\network\mcpe\protocol\types\recipe\PotionTypeRecipe as ProtocolPotionTypeRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient; use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient as ProtocolRecipeIngredient;
use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShapedRecipe; use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShapedRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe; use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe;
use pocketmine\timings\Timings; use pocketmine\timings\Timings;
@ -91,8 +92,8 @@ final class CraftingDataCache{
$recipesWithTypeIds[] = new ProtocolShapelessRecipe( $recipesWithTypeIds[] = new ProtocolShapelessRecipe(
CraftingDataPacket::ENTRY_SHAPELESS, CraftingDataPacket::ENTRY_SHAPELESS,
Binary::writeInt(++$counter), Binary::writeInt(++$counter),
array_map(function(Item $item) use ($converter) : RecipeIngredient{ array_map(function(RecipeIngredient $item) use ($converter) : ProtocolRecipeIngredient{
return $converter->coreItemStackToRecipeIngredient($item); return $converter->coreRecipeIngredientToNet($item);
}, $recipe->getIngredientList()), }, $recipe->getIngredientList()),
array_map(function(Item $item) use ($converter) : ItemStack{ array_map(function(Item $item) use ($converter) : ItemStack{
return $converter->coreItemStackToNet($item); return $converter->coreItemStackToNet($item);
@ -110,7 +111,7 @@ final class CraftingDataCache{
for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){ for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){
for($column = 0, $width = $recipe->getWidth(); $column < $width; ++$column){ for($column = 0, $width = $recipe->getWidth(); $column < $width; ++$column){
$inputs[$row][$column] = $converter->coreItemStackToRecipeIngredient($recipe->getIngredient($column, $row)); $inputs[$row][$column] = $converter->coreRecipeIngredientToNet($recipe->getIngredient($column, $row));
} }
} }
$recipesWithTypeIds[] = $r = new ProtocolShapedRecipe( $recipesWithTypeIds[] = $r = new ProtocolShapedRecipe(
@ -136,7 +137,7 @@ final class CraftingDataCache{
default => throw new AssumptionFailedError("Unreachable"), default => throw new AssumptionFailedError("Unreachable"),
}; };
foreach($manager->getFurnaceRecipeManager($furnaceType)->getAll() as $recipe){ foreach($manager->getFurnaceRecipeManager($furnaceType)->getAll() as $recipe){
$input = $converter->coreItemStackToRecipeIngredient($recipe->getInput()); $input = $converter->coreRecipeIngredientToNet($recipe->getInput());
$recipesWithTypeIds[] = new ProtocolFurnaceRecipe( $recipesWithTypeIds[] = new ProtocolFurnaceRecipe(
CraftingDataPacket::ENTRY_FURNACE_DATA, CraftingDataPacket::ENTRY_FURNACE_DATA,
$input->getId(), $input->getId(),

View File

@ -29,6 +29,9 @@ use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\LoomInventory; use pocketmine\block\inventory\LoomInventory;
use pocketmine\block\inventory\StonecutterInventory; use pocketmine\block\inventory\StonecutterInventory;
use pocketmine\block\VanillaBlocks; use pocketmine\block\VanillaBlocks;
use pocketmine\crafting\ExactRecipeIngredient;
use pocketmine\crafting\MetaWildcardRecipeIngredient;
use pocketmine\crafting\RecipeIngredient;
use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\CreateItemAction;
use pocketmine\inventory\transaction\action\DestroyItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction;
use pocketmine\inventory\transaction\action\DropItemAction; use pocketmine\inventory\transaction\action\DropItemAction;
@ -48,11 +51,12 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction; use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset; use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient; use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient as ProtocolRecipeIngredient;
use pocketmine\player\GameMode; use pocketmine\player\GameMode;
use pocketmine\player\Player; use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait; use pocketmine\utils\SingletonTrait;
use function get_class;
class TypeConverter{ class TypeConverter{
use SingletonTrait; use SingletonTrait;
@ -115,39 +119,40 @@ class TypeConverter{
} }
} }
public function coreItemStackToRecipeIngredient(Item $itemStack) : RecipeIngredient{ public function coreRecipeIngredientToNet(?RecipeIngredient $ingredient) : ProtocolRecipeIngredient{
if($itemStack->isNull()){ if($ingredient === null){
return new RecipeIngredient(0, 0, 0); return new ProtocolRecipeIngredient(0, 0, 0);
} }
if($itemStack->hasAnyDamageValue()){ if($ingredient instanceof MetaWildcardRecipeIngredient){
[$id, ] = ItemTranslator::getInstance()->toNetworkId(ItemFactory::getInstance()->get($itemStack->getId())); $id = GlobalItemTypeDictionary::getInstance()->getDictionary()->fromStringId($ingredient->getItemId());
$meta = self::RECIPE_INPUT_WILDCARD_META; $meta = self::RECIPE_INPUT_WILDCARD_META;
}else{ }elseif($ingredient instanceof ExactRecipeIngredient){
[$id, $meta] = ItemTranslator::getInstance()->toNetworkId($itemStack); $item = $ingredient->getItem();
[$id, $meta] = ItemTranslator::getInstance()->toNetworkId($item);
if($id < 256){ if($id < 256){
//TODO: this is needed for block crafting recipes to work - we need to replace this with some kind of //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 //blockstate <-> meta mapping table so that we can remove the legacy code from the core
$meta = $itemStack->getMeta(); $meta = $item->getMeta();
} }
}else{
throw new \LogicException("Unsupported recipe ingredient type " . get_class($ingredient) . ", only " . ExactRecipeIngredient::class . " and " . MetaWildcardRecipeIngredient::class . " are supported");
} }
return new RecipeIngredient($id, $meta, $itemStack->getCount()); return new ProtocolRecipeIngredient($id, $meta, 1);
} }
public function recipeIngredientToCoreItemStack(RecipeIngredient $ingredient) : Item{ public function netRecipeIngredientToCore(ProtocolRecipeIngredient $ingredient) : ?RecipeIngredient{
if($ingredient->getId() === 0){ if($ingredient->getId() === 0){
return VanillaItems::AIR(); return null;
}
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 //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);
if($ingredient->getMeta() === self::RECIPE_INPUT_WILDCARD_META){ return new ExactRecipeIngredient($result);
$idItem = ItemTranslator::getInstance()->fromNetworkId($ingredient->getId(), 0, 0);
$result = ItemFactory::getInstance()->get($idItem->getId(), -1);
}else{
$result = ItemTranslator::getInstance()->fromNetworkId($ingredient->getId(), $ingredient->getMeta(), 0);
}
$result->setCount($ingredient->getCount());
return $result;
} }
public function coreItemStackToNet(Item $itemStack) : ItemStack{ public function coreItemStackToNet(Item $itemStack) : ItemStack{
@ -237,8 +242,8 @@ class TypeConverter{
if($id !== null && ($id < -0x8000 || $id >= 0x7fff)){ if($id !== null && ($id < -0x8000 || $id >= 0x7fff)){
throw new TypeConversionException("Item ID must be in range " . -0x8000 . " ... " . 0x7fff . " (received $id)"); throw new TypeConversionException("Item ID must be in range " . -0x8000 . " ... " . 0x7fff . " (received $id)");
} }
if($meta < 0 || $meta >= 0x7fff){ //this meta value may have been restored from the NBT if($meta < 0 || $meta >= 0x7ffe){ //this meta value may have been restored from the NBT
throw new TypeConversionException("Item meta must be in range 0 ... " . 0x7fff . " (received $meta)"); throw new TypeConversionException("Item meta must be in range 0 ... " . 0x7ffe . " (received $meta)");
} }
$itemResult = ItemFactory::getInstance()->get($id ?? $itemResult->getId(), $meta); $itemResult = ItemFactory::getInstance()->get($id ?? $itemResult->getId(), $meta);
} }