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{
$input = $recipe->getInput();
$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){
$callback();
@ -193,7 +193,7 @@ class CraftingManager{
public function registerPotionContainerChangeRecipe(PotionContainerChangeRecipe $recipe) : void{
$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){
$callback();
@ -253,8 +253,6 @@ class CraftingManager{
public function matchBrewingRecipe(Item $input, Item $ingredient) : ?BrewingRecipe{
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()] ??
$this->potionContainerChangeRecipes[$input->getId()][$ingredient->getId() . ":?"] ?? null;
$this->potionContainerChangeRecipes[$input->getId()][$ingredient->getId() . ":" . $ingredient->getMeta()] ?? null;
}
}

View File

@ -23,14 +23,17 @@ declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\data\bedrock\item\ItemTypeDeserializeException;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use pocketmine\item\ItemFactory;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
use pocketmine\world\format\io\GlobalItemDataHandlers;
use function array_map;
use function file_get_contents;
use function is_array;
use function is_int;
use function json_decode;
final class CraftingManagerFromDataHelper{
@ -41,7 +44,7 @@ final class CraftingManagerFromDataHelper{
private static function containsUnknownItems(array $items) : bool{
$factory = ItemFactory::getInstance();
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
if(!$factory->isRegistered($item->getId())){
return true;
@ -54,6 +57,30 @@ final class CraftingManagerFromDataHelper{
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{
$recipes = json_decode(Utils::assumeNotFalse(file_get_contents($filePath), "Missing required resource file"), true);
if(!is_array($recipes)){
@ -61,6 +88,7 @@ final class CraftingManagerFromDataHelper{
}
$result = new CraftingManager();
$ingredientDeserializerFunc = \Closure::fromCallable([self::class, "deserializeIngredient"]);
$itemDeserializerFunc = \Closure::fromCallable([Item::class, 'jsonDeserialize']);
foreach($recipes["shapeless"] as $recipe){
@ -73,9 +101,16 @@ final class CraftingManagerFromDataHelper{
if($recipeType === null){
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"]);
if(self::containsUnknownItems($inputs) || self::containsUnknownItems($outputs)){
if(self::containsUnknownItems($outputs)){
continue;
}
$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
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"]);
if(self::containsUnknownItems($inputs) || self::containsUnknownItems($outputs)){
if(self::containsUnknownItems($outputs)){
continue;
}
$result->registerShapedRecipe(new ShapedRecipe(
@ -111,8 +153,8 @@ final class CraftingManagerFromDataHelper{
continue;
}
$output = Item::jsonDeserialize($recipe["output"]);
$input = Item::jsonDeserialize($recipe["input"]);
if(self::containsUnknownItems([$output, $input])){
$input = self::deserializeIngredient($recipe["input"]);
if($input === null || self::containsUnknownItems([$output])){
continue;
}
$result->getFurnaceRecipeManager($furnaceType)->register(new FurnaceRecipe(
@ -135,9 +177,9 @@ final class CraftingManagerFromDataHelper{
));
}
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"]);
$output = ItemFactory::getInstance()->get($recipe["output_item_id"], -1);
$output = ItemFactory::getInstance()->get($recipe["output_item_id"]);
if(self::containsUnknownItems([$input, $ingredient, $output])){
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.
*
* @return Item[]
* @return RecipeIngredient[]
*/
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(
private Item $result,
private Item $ingredient
private RecipeIngredient $ingredient
){
$this->result = clone $result;
$this->ingredient = clone $ingredient;
}
public function getInput() : Item{
return clone $this->ingredient;
public function getInput() : RecipeIngredient{
return $this->ingredient;
}
public function getResult() : Item{

View File

@ -25,11 +25,18 @@ namespace pocketmine\crafting;
use pocketmine\item\Item;
use pocketmine\utils\ObjectSet;
use function morton2d_encode;
final class FurnaceRecipeManager{
/** @var FurnaceRecipe[] */
protected array $furnaceRecipes = [];
/**
* @var FurnaceRecipe[]
* @phpstan-var array<int, FurnaceRecipe>
*/
private array $lookupCache = [];
/** @phpstan-var ObjectSet<\Closure(FurnaceRecipe) : void> */
private ObjectSet $recipeRegisteredCallbacks;
@ -52,14 +59,27 @@ final class FurnaceRecipeManager{
}
public function register(FurnaceRecipe $recipe) : void{
$input = $recipe->getInput();
$this->furnaceRecipes[$input->getId() . ":" . ($input->hasAnyDamageValue() ? "?" : $input->getMeta())] = $recipe;
$this->furnaceRecipes[] = $recipe;
foreach($this->recipeRegisteredCallbacks as $callback){
$callback($recipe);
}
}
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;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\utils\Utils;
use function array_values;
use function count;
@ -35,7 +34,7 @@ use function strpos;
class ShapedRecipe implements CraftingRecipe{
/** @var string[] */
private array $shape = [];
/** @var Item[] char => Item map */
/** @var RecipeIngredient[] char => RecipeIngredient map */
private array $ingredientList = [];
/** @var Item[] */
private array $results = [];
@ -46,15 +45,15 @@ class ShapedRecipe implements CraftingRecipe{
/**
* 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.
* 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.
* @param Item[] $ingredients <br>
* @param RecipeIngredient[] $ingredients <br>
* 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
* 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.
*/
@ -119,7 +118,7 @@ class ShapedRecipe implements CraftingRecipe{
}
/**
* @return Item[][]
* @return (RecipeIngredient|null)[][]
*/
public function getIngredientMap() : array{
$ingredients = [];
@ -134,7 +133,7 @@ class ShapedRecipe implements CraftingRecipe{
}
/**
* @return Item[]
* @return RecipeIngredient[]
*/
public function getIngredientList() : array{
$ingredients = [];
@ -142,7 +141,7 @@ class ShapedRecipe implements CraftingRecipe{
for($y = 0; $y < $this->height; ++$y){
for($x = 0; $x < $this->width; ++$x){
$ingredient = $this->getIngredient($x, $y);
if(!$ingredient->isNull()){
if($ingredient !== null){
$ingredients[] = $ingredient;
}
}
@ -151,9 +150,8 @@ class ShapedRecipe implements CraftingRecipe{
return $ingredients;
}
public function getIngredient(int $x, int $y) : Item{
$exists = $this->ingredientList[$this->shape[$y][$x]] ?? null;
return $exists !== null ? clone $exists : VanillaItems::AIR();
public function getIngredient(int $x, int $y) : ?RecipeIngredient{
return $this->ingredientList[$this->shape[$y][$x]] ?? null;
}
/**
@ -170,7 +168,12 @@ class ShapedRecipe implements CraftingRecipe{
$given = $grid->getIngredient($reverse ? $this->width - $x - 1 : $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;
}
}

View File

@ -28,30 +28,24 @@ use pocketmine\utils\Utils;
use function count;
class ShapelessRecipe implements CraftingRecipe{
/** @var Item[] */
/** @var RecipeIngredient[] */
private array $ingredients = [];
/** @var Item[] */
private array $results;
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.
* TODO: we'll want to make the type parameter mandatory in PM5
*/
public function __construct(array $ingredients, array $results, ?ShapelessRecipeType $type = null){
$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){
$this->ingredients[] = $item->pop();
}
if(count($ingredients) > 9){
throw new \InvalidArgumentException("Shapeless recipes cannot have more than 9 ingredients");
}
$this->ingredients = $ingredients;
$this->results = Utils::cloneObjectArray($results);
}
@ -71,28 +65,23 @@ class ShapelessRecipe implements CraftingRecipe{
}
/**
* @return Item[]
* @return RecipeIngredient[]
*/
public function getIngredientList() : array{
return Utils::cloneObjectArray($this->ingredients);
return $this->ingredients;
}
public function getIngredientCount() : int{
$count = 0;
foreach($this->ingredients as $ingredient){
$count += $ingredient->getCount();
}
return $count;
return count($this->ingredients);
}
public function matchesCraftingGrid(CraftingGrid $grid) : bool{
//don't pack the ingredients - shapeless recipes require that each ingredient be in a separate slot
$input = $grid->getContents();
foreach($this->ingredients as $needItem){
foreach($this->ingredients as $ingredient){
foreach($input as $j => $haveItem){
if($haveItem->equals($needItem, !$needItem->hasAnyDamageValue(), $needItem->hasNamedTag()) && $haveItem->getCount() >= $needItem->getCount()){
if($ingredient->accepts($haveItem)){
unset($input[$j]);
continue 2;
}

View File

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

View File

@ -70,6 +70,47 @@ final class ItemDataUpgrader{
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
*/

View File

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

View File

@ -25,12 +25,18 @@ namespace pocketmine\inventory\transaction;
use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\CraftingRecipe;
use pocketmine\crafting\RecipeIngredient;
use pocketmine\event\inventory\CraftItemEvent;
use pocketmine\item\Item;
use pocketmine\player\Player;
use pocketmine\utils\Utils;
use function array_fill_keys;
use function array_keys;
use function array_pop;
use function count;
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
@ -63,13 +69,110 @@ class CraftingTransaction extends InventoryTransaction{
$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[] $recipeItems
*
* @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){
throw new TransactionValidationException("No recipe items given");
}
@ -77,6 +180,7 @@ class CraftingTransaction extends InventoryTransaction{
throw new TransactionValidationException("No transaction items given");
}
$iterations = 0;
while(count($recipeItems) > 0){
/** @var Item $recipeItem */
$recipeItem = array_pop($recipeItems);
@ -90,7 +194,7 @@ class CraftingTransaction extends InventoryTransaction{
$haveCount = 0;
foreach($txItems as $j => $txItem){
if($txItem->equals($recipeItem, !$wildcards || !$recipeItem->hasAnyDamageValue(), !$wildcards || $recipeItem->hasNamedTag())){
if($txItem->canStackWith($recipeItem)){
$haveCount += $txItem->getCount();
unset($txItems[$j]);
}
@ -115,7 +219,7 @@ class CraftingTransaction extends InventoryTransaction{
if(count($txItems) > 0){
//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;
@ -133,9 +237,9 @@ class CraftingTransaction extends InventoryTransaction{
foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
try{
//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
$this->matchRecipeItems($this->inputs, $recipe->getIngredientList(), true, $this->repetitions);
self::matchIngredients($this->inputs, $recipe->getIngredientList(), $this->repetitions);
//Success!
$this->recipe = $recipe;

View File

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

View File

@ -104,6 +104,9 @@ final class LegacyStringToItemParser{
$meta = 0;
}elseif(is_numeric($b[1])){
$meta = (int) $b[1];
if($meta < 0 || $meta > 0x7ffe){
throw new LegacyStringToItemParserException("Meta value $meta is outside the range 0 - " . 0x7ffe);
}
}else{
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\FurnaceType;
use pocketmine\crafting\RecipeIngredient;
use pocketmine\crafting\ShapelessRecipeType;
use pocketmine\item\Item;
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\PotionContainerChangeRecipe as ProtocolPotionContainerChangeRecipe;
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\ShapelessRecipe as ProtocolShapelessRecipe;
use pocketmine\timings\Timings;
@ -91,8 +92,8 @@ final class CraftingDataCache{
$recipesWithTypeIds[] = new ProtocolShapelessRecipe(
CraftingDataPacket::ENTRY_SHAPELESS,
Binary::writeInt(++$counter),
array_map(function(Item $item) use ($converter) : RecipeIngredient{
return $converter->coreItemStackToRecipeIngredient($item);
array_map(function(RecipeIngredient $item) use ($converter) : ProtocolRecipeIngredient{
return $converter->coreRecipeIngredientToNet($item);
}, $recipe->getIngredientList()),
array_map(function(Item $item) use ($converter) : ItemStack{
return $converter->coreItemStackToNet($item);
@ -110,7 +111,7 @@ final class CraftingDataCache{
for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){
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(
@ -136,7 +137,7 @@ final class CraftingDataCache{
default => throw new AssumptionFailedError("Unreachable"),
};
foreach($manager->getFurnaceRecipeManager($furnaceType)->getAll() as $recipe){
$input = $converter->coreItemStackToRecipeIngredient($recipe->getInput());
$input = $converter->coreRecipeIngredientToNet($recipe->getInput());
$recipesWithTypeIds[] = new ProtocolFurnaceRecipe(
CraftingDataPacket::ENTRY_FURNACE_DATA,
$input->getId(),

View File

@ -29,6 +29,9 @@ use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\LoomInventory;
use pocketmine\block\inventory\StonecutterInventory;
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\DestroyItemAction;
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\NetworkInventoryAction;
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\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
use function get_class;
class TypeConverter{
use SingletonTrait;
@ -115,39 +119,40 @@ class TypeConverter{
}
}
public function coreItemStackToRecipeIngredient(Item $itemStack) : RecipeIngredient{
if($itemStack->isNull()){
return new RecipeIngredient(0, 0, 0);
public function coreRecipeIngredientToNet(?RecipeIngredient $ingredient) : ProtocolRecipeIngredient{
if($ingredient === null){
return new ProtocolRecipeIngredient(0, 0, 0);
}
if($itemStack->hasAnyDamageValue()){
[$id, ] = ItemTranslator::getInstance()->toNetworkId(ItemFactory::getInstance()->get($itemStack->getId()));
if($ingredient instanceof MetaWildcardRecipeIngredient){
$id = GlobalItemTypeDictionary::getInstance()->getDictionary()->fromStringId($ingredient->getItemId());
$meta = self::RECIPE_INPUT_WILDCARD_META;
}else{
[$id, $meta] = ItemTranslator::getInstance()->toNetworkId($itemStack);
}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 = $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){
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
if($ingredient->getMeta() === self::RECIPE_INPUT_WILDCARD_META){
$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;
$result = ItemTranslator::getInstance()->fromNetworkId($ingredient->getId(), $ingredient->getMeta(), 0);
return new ExactRecipeIngredient($result);
}
public function coreItemStackToNet(Item $itemStack) : ItemStack{
@ -237,8 +242,8 @@ class TypeConverter{
if($id !== null && ($id < -0x8000 || $id >= 0x7fff)){
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
throw new TypeConversionException("Item meta must be in range 0 ... " . 0x7fff . " (received $meta)");
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 ... " . 0x7ffe . " (received $meta)");
}
$itemResult = ItemFactory::getInstance()->get($id ?? $itemResult->getId(), $meta);
}