diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php
index 6365dd959..59ae68979 100644
--- a/src/pocketmine/Player.php
+++ b/src/pocketmine/Player.php
@@ -2302,11 +2302,17 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
}
}
- if($this->craftingTransaction->getPrimaryOutput() !== null){
- //we get the actions for this in several packets, so we can't execute it until we get the result
+ if($packet->isFinalCraftingPart){
+ //we get the actions for this in several packets, so we need to wait until we have all the pieces before
+ //trying to execute it
+
+ $result = $this->craftingTransaction->execute();
+ if(!$result){
+ $this->server->getLogger()->debug("Failed to execute crafting transaction from " . $this->getName());
+ }
- $this->craftingTransaction->execute();
$this->craftingTransaction = null;
+ return $result;
}
return true;
diff --git a/src/pocketmine/event/inventory/CraftItemEvent.php b/src/pocketmine/event/inventory/CraftItemEvent.php
index f5bedd51f..68a9b56c9 100644
--- a/src/pocketmine/event/inventory/CraftItemEvent.php
+++ b/src/pocketmine/event/inventory/CraftItemEvent.php
@@ -25,35 +25,83 @@ namespace pocketmine\event\inventory;
use pocketmine\event\Cancellable;
use pocketmine\event\Event;
+use pocketmine\inventory\CraftingRecipe;
use pocketmine\inventory\Recipe;
use pocketmine\inventory\transaction\CraftingTransaction;
+use pocketmine\item\Item;
use pocketmine\Player;
class CraftItemEvent extends Event implements Cancellable{
/** @var CraftingTransaction */
private $transaction;
+ /** @var CraftingRecipe */
+ private $recipe;
+ /** @var int */
+ private $repetitions;
+ /** @var Item[] */
+ private $inputs;
+ /** @var Item[] */
+ private $outputs;
/**
* @param CraftingTransaction $transaction
+ * @param CraftingRecipe $recipe
+ * @param int $repetitions
+ * @param Item[] $inputs
+ * @param Item[] $outputs
*/
- public function __construct(CraftingTransaction $transaction){
+ public function __construct(CraftingTransaction $transaction, CraftingRecipe $recipe, int $repetitions, array $inputs, array $outputs){
$this->transaction = $transaction;
+ $this->recipe = $recipe;
+ $this->repetitions = $repetitions;
+ $this->inputs = $inputs;
+ $this->outputs = $outputs;
}
+ /**
+ * Returns the inventory transaction involved in this crafting event.
+ *
+ * @return CraftingTransaction
+ */
public function getTransaction() : CraftingTransaction{
return $this->transaction;
}
/**
- * @return Recipe
+ * Returns the recipe crafted.
+ *
+ * @return CraftingRecipe
*/
- public function getRecipe() : Recipe{
- $recipe = $this->transaction->getRecipe();
- if($recipe === null){
- throw new \RuntimeException("This shouldn't be called if the transaction can't be executed");
- }
+ public function getRecipe() : CraftingRecipe{
+ return $this->recipe;
+ }
- return $recipe;
+ /**
+ * Returns the number of times the recipe was crafted. This is usually 1, but might be more in the case of recipe
+ * book shift-clicks (which craft lots of items in a batch).
+ *
+ * @return int
+ */
+ public function getRepetitions() : int{
+ return $this->repetitions;
+ }
+
+ /**
+ * Returns a list of items destroyed as ingredients of the recipe.
+ *
+ * @return Item[]
+ */
+ public function getInputs() : array{
+ return $this->inputs;
+ }
+
+ /**
+ * Returns a list of items created by crafting the recipe.
+ *
+ * @return Item[]
+ */
+ public function getOutputs() : array{
+ return $this->outputs;
}
/**
diff --git a/src/pocketmine/inventory/CraftingGrid.php b/src/pocketmine/inventory/CraftingGrid.php
index 898a490af..5b4143557 100644
--- a/src/pocketmine/inventory/CraftingGrid.php
+++ b/src/pocketmine/inventory/CraftingGrid.php
@@ -23,6 +23,8 @@ declare(strict_types=1);
namespace pocketmine\inventory;
+use pocketmine\item\Item;
+use pocketmine\item\ItemFactory;
use pocketmine\Player;
class CraftingGrid extends BaseInventory{
@@ -34,6 +36,15 @@ class CraftingGrid extends BaseInventory{
/** @var int */
private $gridWidth;
+ /** @var int|null */
+ private $startX;
+ /** @var int|null */
+ private $xLen;
+ /** @var int|null */
+ private $startY;
+ /** @var int|null */
+ private $yLen;
+
public function __construct(Player $holder, int $gridWidth){
$this->holder = $holder;
$this->gridWidth = $gridWidth;
@@ -56,6 +67,16 @@ class CraftingGrid extends BaseInventory{
return "Crafting";
}
+ public function setItem(int $index, Item $item, bool $send = true) : bool{
+ if(parent::setItem($index, $item, $send)){
+ $this->seekRecipeBounds();
+
+ return true;
+ }
+
+ return false;
+ }
+
public function sendSlot(int $index, $target) : void{
//we can't send a slot of a client-sided inventory window
}
@@ -70,4 +91,70 @@ class CraftingGrid extends BaseInventory{
public function getHolder(){
return $this->holder;
}
+
+ private function seekRecipeBounds() : void{
+ $minX = PHP_INT_MAX;
+ $maxX = 0;
+
+ $minY = PHP_INT_MAX;
+ $maxY = 0;
+
+ $empty = true;
+
+ for($y = 0; $y < $this->gridWidth; ++$y){
+ for($x = 0; $x < $this->gridWidth; ++$x){
+ if(!$this->isSlotEmpty($y * $this->gridWidth + $x)){
+ $minX = min($minX, $x);
+ $maxX = max($maxX, $x);
+
+ $minY = min($minY, $y);
+ $maxY = max($maxY, $y);
+
+ $empty = false;
+ }
+ }
+ }
+
+ if(!$empty){
+ $this->startX = $minX;
+ $this->xLen = $maxX - $minX + 1;
+ $this->startY = $minY;
+ $this->yLen = $maxY - $minY + 1;
+ }else{
+ $this->startX = $this->xLen = $this->startY = $this->yLen = null;
+ }
+ }
+
+ /**
+ * Returns the item at offset x,y, offset by where the starts of the recipe rectangle are.
+ *
+ * @param int $x
+ * @param int $y
+ *
+ * @return Item
+ */
+ public function getIngredient(int $x, int $y) : Item{
+ if($this->startX !== null and $this->startY !== null){
+ return $this->getItem(($y + $this->startY) * $this->gridWidth + ($x + $this->startX));
+ }
+
+ throw new \InvalidStateException("No ingredients found in grid");
+ }
+
+ /**
+ * Returns the width of the recipe we're trying to craft, based on items currently in the grid.
+ *
+ * @return int
+ */
+ public function getRecipeWidth() : int{
+ return $this->xLen ?? 0;
+ }
+
+ /**
+ * Returns the height of the recipe we're trying to craft, based on items currently in the grid.
+ * @return int
+ */
+ public function getRecipeHeight() : int{
+ return $this->yLen ?? 0;
+ }
}
diff --git a/src/pocketmine/inventory/CraftingManager.php b/src/pocketmine/inventory/CraftingManager.php
index 3181a6fcf..68a4cc8a5 100644
--- a/src/pocketmine/inventory/CraftingManager.php
+++ b/src/pocketmine/inventory/CraftingManager.php
@@ -59,20 +59,13 @@ class CraftingManager{
foreach($recipes->getAll() as $recipe){
switch($recipe["type"]){
case 0:
- // TODO: handle multiple result items
- $first = $recipe["output"][0];
- $result = new ShapelessRecipe(Item::jsonDeserialize($first));
-
- foreach($recipe["input"] as $ingredient){
- $result->addIngredient(Item::jsonDeserialize($ingredient));
- }
- $this->registerRecipe($result);
+ $this->registerRecipe(new ShapelessRecipe(
+ array_map(function(array $data) : Item{ return Item::jsonDeserialize($data); }, $recipe["input"]),
+ array_map(function(array $data) : Item{ return Item::jsonDeserialize($data); }, $recipe["output"])
+ ));
break;
case 1:
- $first = array_shift($recipe["output"]);
-
$this->registerRecipe(new ShapedRecipe(
- Item::jsonDeserialize($first),
$recipe["shape"],
array_map(function(array $data) : Item{ return Item::jsonDeserialize($data); }, $recipe["input"]),
array_map(function(array $data) : Item{ return Item::jsonDeserialize($data); }, $recipe["output"])
@@ -151,6 +144,41 @@ class CraftingManager{
return $retval;
}
+ /**
+ * @param Item[] $items
+ *
+ * @return Item[]
+ */
+ private static function pack(array $items) : array{
+ /** @var Item[] $result */
+ $result = [];
+
+ foreach($items as $i => $item){
+ foreach($result as $otherItem){
+ if($item->equals($otherItem)){
+ $otherItem->setCount($otherItem->getCount() + $item->getCount());
+ continue 2;
+ }
+ }
+
+ //No matching item found
+ $result[] = clone $item;
+ }
+
+ return $result;
+ }
+
+ private static function hashOutputs(array $outputs) : string{
+ $outputs = self::pack($outputs);
+ usort($outputs, [self::class, "sort"]);
+ foreach($outputs as $o){
+ //this reduces accuracy of hash, but it's necessary to deal with recipe book shift-clicking stupidity
+ $o->setCount(1);
+ }
+
+ return json_encode($outputs);
+ }
+
/**
* @param UUID $id
* @return CraftingRecipe|null
@@ -192,7 +220,7 @@ class CraftingManager{
* @param ShapedRecipe $recipe
*/
public function registerShapedRecipe(ShapedRecipe $recipe) : void{
- $this->shapedRecipes[json_encode($recipe->getResult())][json_encode($recipe->getIngredientMap())] = $recipe;
+ $this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
$this->craftingDataCache = null;
}
@@ -200,9 +228,7 @@ class CraftingManager{
* @param ShapelessRecipe $recipe
*/
public function registerShapelessRecipe(ShapelessRecipe $recipe) : void{
- $ingredients = $recipe->getIngredientList();
- usort($ingredients, [self::class, "sort"]);
- $this->shapelessRecipes[json_encode($recipe->getResult())][json_encode($ingredients)] = $recipe;
+ $this->shapelessRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
$this->craftingDataCache = null;
}
@@ -216,61 +242,27 @@ class CraftingManager{
}
/**
- * Clones a map of Item objects to avoid accidental modification.
- *
- * @param Item[][] $map
- * @return Item[][]
- */
- private function cloneItemMap(array $map) : array{
- /** @var Item[] $row */
- foreach($map as $y => $row){
- foreach($row as $x => $item){
- $map[$y][$x] = clone $item;
- }
- }
-
- return $map;
- }
-
- /**
- * @param Item[][] $inputMap
- * @param Item $primaryOutput
- * @param Item[][] $extraOutputMap
+ * @param CraftingGrid $grid
+ * @param Item[] $outputs
*
* @return CraftingRecipe|null
*/
- public function matchRecipe(array $inputMap, Item $primaryOutput, array $extraOutputMap) : ?CraftingRecipe{
+ public function matchRecipe(CraftingGrid $grid, array $outputs) : ?CraftingRecipe{
//TODO: try to match special recipes before anything else (first they need to be implemented!)
- $outputHash = json_encode($primaryOutput);
+ $outputHash = self::hashOutputs($outputs);
+
if(isset($this->shapedRecipes[$outputHash])){
- $inputHash = json_encode($inputMap);
- $recipe = $this->shapedRecipes[$outputHash][$inputHash] ?? null;
-
- if($recipe !== null and $recipe->matchItems($this->cloneItemMap($inputMap), $this->cloneItemMap($extraOutputMap))){ //matched a recipe by hash
- return $recipe;
- }
-
foreach($this->shapedRecipes[$outputHash] as $recipe){
- if($recipe->matchItems($this->cloneItemMap($inputMap), $this->cloneItemMap($extraOutputMap))){
+ if($recipe->matchesCraftingGrid($grid)){
return $recipe;
}
}
}
if(isset($this->shapelessRecipes[$outputHash])){
- $list = array_merge(...$inputMap);
- usort($list, [self::class, "sort"]);
-
- $inputHash = json_encode($list);
- $recipe = $this->shapelessRecipes[$outputHash][$inputHash] ?? null;
-
- if($recipe !== null and $recipe->matchItems($this->cloneItemMap($inputMap), $this->cloneItemMap($extraOutputMap))){
- return $recipe;
- }
-
foreach($this->shapelessRecipes[$outputHash] as $recipe){
- if($recipe->matchItems($this->cloneItemMap($inputMap), $this->cloneItemMap($extraOutputMap))){
+ if($recipe->matchesCraftingGrid($grid)){
return $recipe;
}
}
@@ -293,8 +285,7 @@ class CraftingManager{
*/
public function registerRecipe(Recipe $recipe) : void{
if($recipe instanceof CraftingRecipe){
- $result = $recipe->getResult();
- $recipe->setId($uuid = UUID::fromData((string) ++self::$RECIPE_COUNT, (string) $result->getId(), (string) $result->getDamage(), (string) $result->getCount(), $result->getCompoundTag()));
+ $recipe->setId($uuid = UUID::fromData((string) ++self::$RECIPE_COUNT, json_encode(self::pack($recipe->getResults()))));
$this->recipes[$uuid->toBinary()] = $recipe;
}
diff --git a/src/pocketmine/inventory/CraftingRecipe.php b/src/pocketmine/inventory/CraftingRecipe.php
index d82a634f4..d0e31ff9e 100644
--- a/src/pocketmine/inventory/CraftingRecipe.php
+++ b/src/pocketmine/inventory/CraftingRecipe.php
@@ -39,23 +39,25 @@ interface CraftingRecipe extends Recipe{
public function setId(UUID $id);
/**
- * @return Item[]
- */
- public function getExtraResults() : array;
-
- /**
- * @return Item[]
- */
- public function getAllResults() : array;
-
- /**
- * Returns whether the specified list of crafting grid inputs and outputs matches this recipe. Outputs DO NOT
- * include the primary result item.
+ * Returns a list of items needed to craft this recipe. This MUST NOT include Air items or items with a zero count.
*
- * @param Item[][] $input 2D array of items taken from the crafting grid
- * @param Item[][] $output 2D array of items put back into the crafting grid (secondary results)
+ * @return Item[]
+ */
+ public function getIngredientList() : array;
+
+ /**
+ * Returns a list of items created by crafting this recipe.
+ *
+ * @return Item[]
+ */
+ public function getResults() : array;
+
+ /**
+ * Returns whether the given crafting grid meets the requirements to craft this recipe.
+ *
+ * @param CraftingGrid $grid
*
* @return bool
*/
- public function matchItems(array $input, array $output) : bool;
+ public function matchesCraftingGrid(CraftingGrid $grid) : bool;
}
diff --git a/src/pocketmine/inventory/Recipe.php b/src/pocketmine/inventory/Recipe.php
index a16e22e4f..883eea6d9 100644
--- a/src/pocketmine/inventory/Recipe.php
+++ b/src/pocketmine/inventory/Recipe.php
@@ -23,14 +23,7 @@ declare(strict_types=1);
namespace pocketmine\inventory;
-use pocketmine\item\Item;
-
interface Recipe{
- /**
- * @return Item
- */
- public function getResult() : Item;
-
public function registerToCraftingManager(CraftingManager $manager) : void;
}
diff --git a/src/pocketmine/inventory/ShapedRecipe.php b/src/pocketmine/inventory/ShapedRecipe.php
index 2356b93b7..2b7386cd7 100644
--- a/src/pocketmine/inventory/ShapedRecipe.php
+++ b/src/pocketmine/inventory/ShapedRecipe.php
@@ -28,11 +28,6 @@ use pocketmine\item\ItemFactory;
use pocketmine\utils\UUID;
class ShapedRecipe implements CraftingRecipe{
- /** @var Item */
- private $primaryResult;
- /** @var Item[] */
- private $extraResults = [];
-
/** @var UUID|null */
private $id = null;
@@ -40,90 +35,76 @@ class ShapedRecipe implements CraftingRecipe{
private $shape = [];
/** @var Item[] char => Item map */
private $ingredientList = [];
+ /** @var Item[] */
+ private $results = [];
+
+ /** @var int */
+ private $height;
+ /** @var int */
+ private $width;
/**
* Constructs a ShapedRecipe instance.
*
- * @param Item $primaryResult
- * @param string[] $shape
+ * @param string[] $shape
* 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
+ * @param Item[] $ingredients
* 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[] $extraResults
- * List of additional result items to leave in the crafting grid afterwards. Used for things like cake recipe
- * empty buckets.
+ * @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.
*/
- public function __construct(Item $primaryResult, array $shape, array $ingredients, array $extraResults = []){
- $rowCount = count($shape);
- if($rowCount > 3 or $rowCount <= 0){
- throw new \InvalidArgumentException("Shaped recipes may only have 1, 2 or 3 rows, not $rowCount");
+ public function __construct(array $shape, array $ingredients, array $results){
+ $this->height = count($shape);
+ if($this->height > 3 or $this->height <= 0){
+ throw new \InvalidArgumentException("Shaped recipes may only have 1, 2 or 3 rows, not $this->height");
}
$shape = array_values($shape);
- $columnCount = strlen($shape[0]);
- if($columnCount > 3 or $columnCount <= 0){
- throw new \InvalidArgumentException("Shaped recipes may only have 1, 2 or 3 columns, not $columnCount");
+ $this->width = strlen($shape[0]);
+ if($this->width > 3 or $this->width <= 0){
+ throw new \InvalidArgumentException("Shaped recipes may only have 1, 2 or 3 columns, not $this->width");
}
foreach($shape as $y => $row){
- if(strlen($row) !== $columnCount){
- throw new \InvalidArgumentException("Shaped recipe rows must all have the same length (expected $columnCount, got " . strlen($row) . ")");
+ if(strlen($row) !== $this->width){
+ throw new \InvalidArgumentException("Shaped recipe rows must all have the same length (expected $this->width, got " . strlen($row) . ")");
}
- for($x = 0; $x < $columnCount; ++$x){
+ for($x = 0; $x < $this->width; ++$x){
if($row{$x} !== ' ' and !isset($ingredients[$row{$x}])){
throw new \InvalidArgumentException("No item specified for symbol '" . $row{$x} . "'");
}
}
}
- $this->primaryResult = clone $primaryResult;
- foreach($extraResults as $item){
- $this->extraResults[] = clone $item;
- }
$this->shape = $shape;
foreach($ingredients as $char => $i){
$this->setIngredient($char, $i);
}
+
+ $this->results = array_map(function(Item $item) : Item{ return clone $item; }, $results);
}
public function getWidth() : int{
- return strlen($this->shape[0]);
+ return $this->width;
}
public function getHeight() : int{
- return count($this->shape);
- }
-
- /**
- * @return Item
- */
- public function getResult() : Item{
- return $this->primaryResult;
+ return $this->height;
}
/**
* @return Item[]
*/
- public function getExtraResults() : array{
- return $this->extraResults;
- }
-
- /**
- * @return Item[]
- */
- public function getAllResults() : array{
- $results = $this->extraResults;
- array_unshift($results, $this->primaryResult);
- return $results;
+ public function getResults() : array{
+ return array_map(function(Item $item) : Item{ return clone $item; }, $this->results);
}
/**
@@ -164,8 +145,8 @@ class ShapedRecipe implements CraftingRecipe{
public function getIngredientMap() : array{
$ingredients = [];
- for($y = 0, $y2 = $this->getHeight(); $y < $y2; ++$y){
- for($x = 0, $x2 = $this->getWidth(); $x < $x2; ++$x){
+ for($y = 0; $y < $this->height; ++$y){
+ for($x = 0; $x < $this->width; ++$x){
$ingredients[$y][$x] = $this->getIngredient($x, $y);
}
}
@@ -173,6 +154,24 @@ class ShapedRecipe implements CraftingRecipe{
return $ingredients;
}
+ /**
+ * @return Item[]
+ */
+ public function getIngredientList() : array{
+ $ingredients = [];
+
+ for($y = 0; $y < $this->height; ++$y){
+ for($x = 0; $x < $this->width; ++$x){
+ $ingredient = $this->getIngredient($x, $y);
+ if(!$ingredient->isNull()){
+ $ingredients[] = $ingredient;
+ }
+ }
+ }
+
+ return $ingredients;
+ }
+
/**
* @param int $x
* @param int $y
@@ -197,34 +196,20 @@ class ShapedRecipe implements CraftingRecipe{
}
/**
- * @param Item[][] $input
+ * @param CraftingGrid $grid
+ * @param bool $reverse
*
* @return bool
*/
- private function matchInputMap(array $input) : bool{
- $map = $this->getIngredientMap();
+ private function matchInputMap(CraftingGrid $grid, bool $reverse) : bool{
+ for($y = 0; $y < $this->height; ++$y){
+ for($x = 0; $x < $this->width; ++$x){
- //match the given items to the requested items
- for($y = 0, $y2 = $this->getHeight(); $y < $y2; ++$y){
- for($x = 0, $x2 = $this->getWidth(); $x < $x2; ++$x){
- $given = $input[$y][$x] ?? null;
- $required = $map[$y][$x];
-
- if($given === null or !$required->equals($given, !$required->hasAnyDamageValue(), $required->hasCompoundTag()) or $required->getCount() !== $given->getCount()){
+ $given = $grid->getIngredient($reverse ? $this->width - $x - 1 : $x, $y);
+ $required = $this->getIngredient($x, $y);
+ if(!$required->equals($given, !$required->hasAnyDamageValue(), $required->hasCompoundTag()) or $required->getCount() > $given->getCount()){
return false;
}
-
- unset($input[$y][$x]);
- }
- }
-
- //check if there are any items left in the grid outside of the recipe
- /** @var Item[] $row */
- foreach($input as $y => $row){
- foreach($row as $x => $needItem){
- if(!$needItem->isNull()){
- return false; //too many input ingredients
- }
}
}
@@ -232,38 +217,15 @@ class ShapedRecipe implements CraftingRecipe{
}
/**
- * @param Item[][] $input
- * @param Item[][] $output
+ * @param CraftingGrid $grid
*
* @return bool
*/
- public function matchItems(array $input, array $output) : bool{
- if(
- !$this->matchInputMap($input) and //as-is
- !$this->matchInputMap(array_map(function(array $row) : array{ return array_reverse($row, false); }, $input)) //mirrored
- ){
+ public function matchesCraftingGrid(CraftingGrid $grid) : bool{
+ if($this->width !== $grid->getRecipeWidth() or $this->height !== $grid->getRecipeHeight()){
return false;
}
- //and then, finally, check that the output items are good:
-
- /** @var Item[] $haveItems */
- $haveItems = array_merge(...$output);
- $needItems = $this->getExtraResults();
- foreach($haveItems as $j => $haveItem){
- if($haveItem->isNull()){
- unset($haveItems[$j]);
- continue;
- }
-
- foreach($needItems as $i => $needItem){
- if($needItem->equals($haveItem, !$needItem->hasAnyDamageValue(), $needItem->hasCompoundTag()) and $needItem->getCount() === $haveItem->getCount()){
- unset($haveItems[$j], $needItems[$i]);
- break;
- }
- }
- }
-
- return count($haveItems) === 0 and count($needItems) === 0;
+ return $this->matchInputMap($grid, false) or $this->matchInputMap($grid, true);
}
}
diff --git a/src/pocketmine/inventory/ShapelessRecipe.php b/src/pocketmine/inventory/ShapelessRecipe.php
index c591a0b20..405a33c71 100644
--- a/src/pocketmine/inventory/ShapelessRecipe.php
+++ b/src/pocketmine/inventory/ShapelessRecipe.php
@@ -27,17 +27,25 @@ use pocketmine\item\Item;
use pocketmine\utils\UUID;
class ShapelessRecipe implements CraftingRecipe{
- /** @var Item */
- private $output;
-
/** @var UUID|null */
private $id = null;
/** @var Item[] */
private $ingredients = [];
+ /** @var Item[] */
+ private $results;
- public function __construct(Item $result){
- $this->output = clone $result;
+ /**
+ * @param Item[] $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.
+ */
+ public function __construct(array $ingredients, array $results){
+ foreach($ingredients as $item){
+ //Ensure they get split up properly
+ $this->addIngredient($item);
+ }
+
+ $this->results = array_map(function(Item $item) : Item{ return clone $item; }, $results);
}
/**
@@ -58,16 +66,8 @@ class ShapelessRecipe implements CraftingRecipe{
$this->id = $id;
}
- public function getResult() : Item{
- return clone $this->output;
- }
-
- public function getExtraResults() : array{
- return []; //TODO
- }
-
- public function getAllResults() : array{
- return [$this->getResult()]; //TODO
+ public function getResults() : array{
+ return array_map(function(Item $item) : Item{ return clone $item; }, $this->results);
}
/**
@@ -78,7 +78,7 @@ class ShapelessRecipe implements CraftingRecipe{
* @throws \InvalidArgumentException
*/
public function addIngredient(Item $item) : ShapelessRecipe{
- if(count($this->ingredients) >= 9){
+ if(count($this->ingredients) + $item->getCount() > 9){
throw new \InvalidArgumentException("Shapeless recipes cannot have more than 9 ingredients");
}
@@ -112,12 +112,7 @@ class ShapelessRecipe implements CraftingRecipe{
* @return Item[]
*/
public function getIngredientList() : array{
- $ingredients = [];
- foreach($this->ingredients as $ingredient){
- $ingredients[] = clone $ingredient;
- }
-
- return $ingredients;
+ return array_map(function(Item $item) : Item{ return clone $item; }, $this->ingredients);
}
/**
@@ -137,53 +132,25 @@ class ShapelessRecipe implements CraftingRecipe{
}
/**
- * @param Item[][] $input
- * @param Item[][] $output
+ * @param CraftingGrid $grid
*
* @return bool
*/
- public function matchItems(array $input, array $output) : bool{
- /** @var Item[] $haveInputs */
- $haveInputs = array_merge(...$input); //we don't care how the items were arranged
- $needInputs = $this->getIngredientList();
+ 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();
- if(!$this->matchItemList($haveInputs, $needInputs)){
- return false;
- }
-
- /** @var Item[] $haveOutputs */
- $haveOutputs = array_merge(...$output);
- $needOutputs = $this->getExtraResults();
-
- if(!$this->matchItemList($haveOutputs, $needOutputs)){
- return false;
- }
-
- return true;
- }
-
- /**
- * @param Item[] $haveItems
- * @param Item[] $needItems
- *
- * @return bool
- */
- private function matchItemList(array $haveItems, array $needItems) : bool{
- foreach($haveItems as $j => $haveItem){
- if($haveItem->isNull()){
- unset($haveItems[$j]);
- continue;
- }
-
-
- foreach($needItems as $i => $needItem){
- if($needItem->equals($haveItem, !$needItem->hasAnyDamageValue(), $needItem->hasCompoundTag()) and $needItem->getCount() === $haveItem->getCount()){
- unset($haveItems[$j], $needItems[$i]);
- break;
+ foreach($this->ingredients as $needItem){
+ foreach($input as $j => $haveItem){
+ if($haveItem->equals($needItem, !$needItem->hasAnyDamageValue(), $needItem->hasCompoundTag()) and $haveItem->getCount() >= $needItem->getCount()){
+ unset($input[$j]);
+ continue 2;
}
}
+
+ return false; //failed to match the needed item to a given item
}
- return count($haveItems) === 0 and count($needItems) === 0;
+ return empty($input); //crafting grid should be empty apart from the given ingredient stacks
}
}
diff --git a/src/pocketmine/inventory/transaction/CraftingTransaction.php b/src/pocketmine/inventory/transaction/CraftingTransaction.php
index cd446367c..48f549b75 100644
--- a/src/pocketmine/inventory/transaction/CraftingTransaction.php
+++ b/src/pocketmine/inventory/transaction/CraftingTransaction.php
@@ -26,132 +26,117 @@ namespace pocketmine\inventory\transaction;
use pocketmine\event\inventory\CraftItemEvent;
use pocketmine\inventory\CraftingRecipe;
use pocketmine\item\Item;
-use pocketmine\item\ItemFactory;
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
use pocketmine\network\mcpe\protocol\types\ContainerIds;
-use pocketmine\Player;
+use pocketmine\utils\MainLogger;
class CraftingTransaction extends InventoryTransaction{
-
- protected $gridSize;
- /** @var Item[][] */
- protected $inputs;
- /** @var Item[][] */
- protected $secondaryOutputs;
- /** @var Item|null */
- protected $primaryOutput;
-
/** @var CraftingRecipe|null */
- protected $recipe = null;
+ protected $recipe;
+ /** @var int|null */
+ protected $repetitions;
- public function __construct(Player $source, $actions = []){
- $this->gridSize = $source->getCraftingGrid()->getGridWidth();
+ /** @var Item[] */
+ protected $inputs = [];
+ /** @var Item[] */
+ protected $outputs = [];
- $air = ItemFactory::get(Item::AIR, 0, 0);
- $this->inputs = array_fill(0, $this->gridSize, array_fill(0, $this->gridSize, $air));
- $this->secondaryOutputs = array_fill(0, $this->gridSize, array_fill(0, $this->gridSize, $air));
-
- parent::__construct($source, $actions);
- }
-
- public function setInput(int $index, Item $item) : void{
- $y = (int) ($index / $this->gridSize);
- $x = $index % $this->gridSize;
-
- if(!isset($this->inputs[$y][$x])){
- return;
+ /**
+ * @param Item[] $txItems
+ * @param Item[] $recipeItems
+ * @param bool $wildcards
+ *
+ * @return int
+ * @throws \InvalidStateException
+ * @throws \InvalidArgumentException
+ */
+ protected function matchRecipeItems(array $txItems, array $recipeItems, bool $wildcards) : int{
+ if(empty($recipeItems)){
+ throw new \InvalidArgumentException("No recipe items given");
+ }
+ if(empty($txItems)){
+ throw new \InvalidArgumentException("No transaction items given");
}
- if($this->inputs[$y][$x]->isNull()){
- $this->inputs[$y][$x] = clone $item;
- }elseif(!$this->inputs[$y][$x]->equals($item)){
- throw new \RuntimeException("Input $index has already been set and does not match the current item (expected " . $this->inputs[$y][$x] . ", got " . $item . ")");
- }
- }
-
- public function getInputMap() : array{
- return $this->inputs;
- }
-
- public function setExtraOutput(int $index, Item $item) : void{
- $y = (int) ($index / $this->gridSize);
- $x = $index % $this->gridSize;
-
- if(!isset($this->secondaryOutputs[$y][$x])){
- return;
- }
-
- if($this->secondaryOutputs[$y][$x]->isNull()){
- $this->secondaryOutputs[$y][$x] = clone $item;
- }elseif(!$this->secondaryOutputs[$y][$x]->equals($item)){
- throw new \RuntimeException("Output $index has already been set and does not match the current item (expected " . $this->secondaryOutputs[$y][$x] . ", got " . $item . ")");
- }
- }
-
- public function getPrimaryOutput() : ?Item{
- return $this->primaryOutput;
- }
-
- public function setPrimaryOutput(Item $item) : void{
- if($this->primaryOutput === null){
- $this->primaryOutput = clone $item;
- }elseif(!$this->primaryOutput->equals($item)){
- throw new \RuntimeException("Primary result item has already been set and does not match the current item (expected " . $this->primaryOutput . ", got " . $item . ")");
- }
- }
-
- public function getRecipe() : ?CraftingRecipe{
- return $this->recipe;
- }
-
- private function reindexInputs() : array{
- $minX = PHP_INT_MAX;
- $maxX = 0;
-
- $minY = PHP_INT_MAX;
- $maxY = 0;
-
- $empty = true;
-
- foreach($this->inputs as $y => $row){
- foreach($row as $x => $item){
- if(!$item->isNull()){
- $minX = min($minX, $x);
- $maxX = max($maxX, $x);
-
- $minY = min($minY, $y);
- $maxY = max($maxY, $y);
-
- $empty = false;
+ $iterations = 0;
+ while(!empty($recipeItems)){
+ /** @var Item $recipeItem */
+ $recipeItem = array_pop($recipeItems);
+ $needCount = $recipeItem->getCount();
+ foreach($recipeItems as $i => $otherRecipeItem){
+ if($otherRecipeItem->equals($recipeItem)){ //make sure they have the same wildcards set
+ $needCount += $otherRecipeItem->getCount();
+ unset($recipeItems[$i]);
}
}
- }
- if($empty){
- return [];
- }
+ $haveCount = 0;
+ foreach($txItems as $j => $txItem){
+ if($txItem->equals($recipeItem, !$wildcards or !$recipeItem->hasAnyDamageValue(), !$wildcards or $recipeItem->hasCompoundTag())){
+ $haveCount += $txItem->getCount();
+ unset($txItems[$j]);
+ }
+ }
- $air = ItemFactory::get(Item::AIR, 0, 0);
- $reindexed = array_fill(0, $maxY - $minY + 1, array_fill(0, $maxX - $minX + 1, $air));
- foreach($reindexed as $y => $row){
- foreach($row as $x => $item){
- $reindexed[$y][$x] = $this->inputs[$y + $minY][$x + $minX];
+ if($haveCount % $needCount !== 0){
+ //wrong count for this output, should divide exactly
+ throw new \InvalidStateException("Expected an exact multiple of required $recipeItem (given: $haveCount, needed: $needCount)");
+ }
+
+ $multiplier = intdiv($haveCount, $needCount);
+ if($multiplier < 1){
+ throw new \InvalidStateException("Expected more than zero items matching $recipeItem (given: $haveCount, needed: $needCount)");
+ }
+ if($iterations === 0){
+ $iterations = $multiplier;
+ }elseif($multiplier !== $iterations){
+ //wrong count for this output, should match previous outputs
+ throw new \InvalidStateException("Expected $recipeItem x$iterations, but found x$multiplier");
}
}
- return $reindexed;
+ if($iterations < 1){
+ throw new \InvalidStateException("Tried to craft zero times");
+ }
+ if(!empty($txItems)){
+ //all items should be destroyed in this process
+ throw new \InvalidStateException("Expected 0 ingredients left over, have " . count($txItems));
+ }
+
+ return $iterations;
}
+
public function canExecute() : bool{
- $inputs = $this->reindexInputs();
+ $this->squashDuplicateSlotChanges();
+ if(count($this->actions) < 1){
+ return false;
+ }
- $this->recipe = $this->source->getServer()->getCraftingManager()->matchRecipe($inputs, $this->primaryOutput, $this->secondaryOutputs);
+ $this->matchItems($this->outputs, $this->inputs);
- return $this->recipe !== null and parent::canExecute();
+ $this->recipe = $this->source->getServer()->getCraftingManager()->matchRecipe($this->source->getCraftingGrid(), $this->outputs);
+ if($this->recipe === null){
+ return false;
+ }
+
+ try{
+ $this->repetitions = $this->matchRecipeItems($this->outputs, $this->recipe->getResults(), false);
+
+ if(($inputIterations = $this->matchRecipeItems($this->inputs, $this->recipe->getIngredientList(), true)) !== $this->repetitions){
+ throw new \InvalidStateException("Tried to craft recipe $this->repetitions times in batch, but have enough inputs for $inputIterations");
+ }
+
+ return true;
+ }catch(\InvalidStateException $e){
+ MainLogger::getLogger()->debug("Failed to validate crafting transaction for " . $this->source->getName() . ": " . $e->getMessage());
+ }
+
+ return false;
}
protected function callExecuteEvent() : bool{
- $this->source->getServer()->getPluginManager()->callEvent($ev = new CraftItemEvent($this));
+ $this->source->getServer()->getPluginManager()->callEvent($ev = new CraftItemEvent($this, $this->recipe, $this->repetitions, $this->inputs, $this->outputs));
return !$ev->isCancelled();
}
@@ -171,37 +156,39 @@ class CraftingTransaction extends InventoryTransaction{
public function execute() : bool{
if(parent::execute()){
- switch($this->primaryOutput->getId()){
- case Item::CRAFTING_TABLE:
- $this->source->awardAchievement("buildWorkBench");
- break;
- case Item::WOODEN_PICKAXE:
- $this->source->awardAchievement("buildPickaxe");
- break;
- case Item::FURNACE:
- $this->source->awardAchievement("buildFurnace");
- break;
- case Item::WOODEN_HOE:
- $this->source->awardAchievement("buildHoe");
- break;
- case Item::BREAD:
- $this->source->awardAchievement("makeBread");
- break;
- case Item::CAKE:
- $this->source->awardAchievement("bakeCake");
- break;
- case Item::STONE_PICKAXE:
- case Item::GOLDEN_PICKAXE:
- case Item::IRON_PICKAXE:
- case Item::DIAMOND_PICKAXE:
- $this->source->awardAchievement("buildBetterPickaxe");
- break;
- case Item::WOODEN_SWORD:
- $this->source->awardAchievement("buildSword");
- break;
- case Item::DIAMOND:
- $this->source->awardAchievement("diamond");
- break;
+ foreach($this->outputs as $item){
+ switch($item->getId()){
+ case Item::CRAFTING_TABLE:
+ $this->source->awardAchievement("buildWorkBench");
+ break;
+ case Item::WOODEN_PICKAXE:
+ $this->source->awardAchievement("buildPickaxe");
+ break;
+ case Item::FURNACE:
+ $this->source->awardAchievement("buildFurnace");
+ break;
+ case Item::WOODEN_HOE:
+ $this->source->awardAchievement("buildHoe");
+ break;
+ case Item::BREAD:
+ $this->source->awardAchievement("makeBread");
+ break;
+ case Item::CAKE:
+ $this->source->awardAchievement("bakeCake");
+ break;
+ case Item::STONE_PICKAXE:
+ case Item::GOLDEN_PICKAXE:
+ case Item::IRON_PICKAXE:
+ case Item::DIAMOND_PICKAXE:
+ $this->source->awardAchievement("buildBetterPickaxe");
+ break;
+ case Item::WOODEN_SWORD:
+ $this->source->awardAchievement("buildSword");
+ break;
+ case Item::DIAMOND:
+ $this->source->awardAchievement("diamond");
+ break;
+ }
}
return true;
diff --git a/src/pocketmine/inventory/transaction/action/CraftingTakeResultAction.php b/src/pocketmine/inventory/transaction/action/CraftingTakeResultAction.php
deleted file mode 100644
index 3ac7fd302..000000000
--- a/src/pocketmine/inventory/transaction/action/CraftingTakeResultAction.php
+++ /dev/null
@@ -1,59 +0,0 @@
-setPrimaryOutput($this->getSourceItem());
- }else{
- throw new \InvalidStateException(get_class($this) . " can only be added to CraftingTransactions");
- }
- }
-
- public function isValid(Player $source) : bool{
- return true;
- }
-
- public function execute(Player $source) : bool{
- return true;
- }
-
- public function onExecuteSuccess(Player $source) : void{
-
- }
-
- public function onExecuteFail(Player $source) : void{
-
- }
-
-}
diff --git a/src/pocketmine/inventory/transaction/action/CraftingTransferMaterialAction.php b/src/pocketmine/inventory/transaction/action/CraftingTransferMaterialAction.php
deleted file mode 100644
index 3d3267007..000000000
--- a/src/pocketmine/inventory/transaction/action/CraftingTransferMaterialAction.php
+++ /dev/null
@@ -1,73 +0,0 @@
-slot = $slot;
- }
-
- public function onAddToTransaction(InventoryTransaction $transaction) : void{
- if($transaction instanceof CraftingTransaction){
- if($this->sourceItem->isNull()){
- $transaction->setInput($this->slot, $this->targetItem);
- }elseif($this->targetItem->isNull()){
- $transaction->setExtraOutput($this->slot, $this->sourceItem);
- }else{
- throw new \InvalidStateException("Invalid " . get_class($this) . ", either source or target item must be air, got source: " . $this->sourceItem . ", target: " . $this->targetItem);
- }
- }else{
- throw new \InvalidStateException(get_class($this) . " can only be added to CraftingTransactions");
- }
- }
-
- public function isValid(Player $source) : bool{
- return true;
- }
-
- public function execute(Player $source) : bool{
- return true;
- }
-
- public function onExecuteSuccess(Player $source) : void{
-
- }
-
- public function onExecuteFail(Player $source) : void{
-
- }
-}
diff --git a/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php b/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php
index 78fa3f2f2..15ec5013a 100644
--- a/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php
+++ b/src/pocketmine/network/mcpe/protocol/CraftingDataPacket.php
@@ -133,7 +133,7 @@ class CraftingDataPacket extends DataPacket{
$stream->putSlot($item);
}
- $results = $recipe->getAllResults();
+ $results = $recipe->getResults();
$stream->putUnsignedVarInt(count($results));
foreach($results as $item){
$stream->putSlot($item);
@@ -154,7 +154,7 @@ class CraftingDataPacket extends DataPacket{
}
}
- $results = $recipe->getAllResults();
+ $results = $recipe->getResults();
$stream->putUnsignedVarInt(count($results));
foreach($results as $item){
$stream->putSlot($item);
diff --git a/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php b/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php
index 6b107b200..7a3556b70 100644
--- a/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php
+++ b/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php
@@ -56,6 +56,12 @@ class InventoryTransactionPacket extends DataPacket{
* determine whether we're doing a crafting transaction.
*/
public $isCraftingPart = false;
+ /**
+ * @var bool
+ * NOTE: THIS FIELD DOES NOT EXIST IN THE PROTOCOL, it's merely used for convenience for PocketMine-MP to easily
+ * determine whether we're doing a crafting transaction.
+ */
+ public $isFinalCraftingPart = false;
/** @var NetworkInventoryAction[] */
public $actions = [];
diff --git a/src/pocketmine/network/mcpe/protocol/types/NetworkInventoryAction.php b/src/pocketmine/network/mcpe/protocol/types/NetworkInventoryAction.php
index fcb820717..0373ee7c5 100644
--- a/src/pocketmine/network/mcpe/protocol/types/NetworkInventoryAction.php
+++ b/src/pocketmine/network/mcpe/protocol/types/NetworkInventoryAction.php
@@ -23,8 +23,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol\types;
-use pocketmine\inventory\transaction\action\CraftingTakeResultAction;
-use pocketmine\inventory\transaction\action\CraftingTransferMaterialAction;
use pocketmine\inventory\transaction\action\CreativeInventoryAction;
use pocketmine\inventory\transaction\action\DropItemAction;
use pocketmine\inventory\transaction\action\InventoryAction;
@@ -111,8 +109,10 @@ class NetworkInventoryAction{
case self::SOURCE_TODO:
$this->windowId = $packet->getVarInt();
switch($this->windowId){
- case self::SOURCE_TYPE_CRAFTING_USE_INGREDIENT:
+ /** @noinspection PhpMissingBreakStatementInspection */
case self::SOURCE_TYPE_CRAFTING_RESULT:
+ $packet->isFinalCraftingPart = true;
+ case self::SOURCE_TYPE_CRAFTING_USE_INGREDIENT:
$packet->isCraftingPart = true;
break;
}
@@ -193,9 +193,8 @@ class NetworkInventoryAction{
$window = $player->getCraftingGrid();
return new SlotChangeAction($window, $this->inventorySlot, $this->oldItem, $this->newItem);
case self::SOURCE_TYPE_CRAFTING_RESULT:
- return new CraftingTakeResultAction($this->oldItem, $this->newItem);
case self::SOURCE_TYPE_CRAFTING_USE_INGREDIENT:
- return new CraftingTransferMaterialAction($this->oldItem, $this->newItem, $this->inventorySlot);
+ return null;
case self::SOURCE_TYPE_CONTAINER_DROP_CONTENTS:
//TODO: this type applies to all fake windows, not just crafting