diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php index 4f55285ca..95a2c6e6c 100644 --- a/src/pocketmine/Player.php +++ b/src/pocketmine/Player.php @@ -37,7 +37,6 @@ use pocketmine\entity\Living; use pocketmine\event\entity\EntityDamageByBlockEvent; use pocketmine\event\entity\EntityDamageByEntityEvent; use pocketmine\event\entity\EntityDamageEvent; -use pocketmine\event\inventory\CraftItemEvent; use pocketmine\event\inventory\InventoryCloseEvent; use pocketmine\event\inventory\InventoryPickupArrowEvent; use pocketmine\event\inventory\InventoryPickupItemEvent; @@ -76,9 +75,8 @@ use pocketmine\inventory\FurnaceInventory; use pocketmine\inventory\Inventory; use pocketmine\inventory\PlayerCursorInventory; use pocketmine\inventory\PlayerInventory; -use pocketmine\inventory\ShapedRecipe; -use pocketmine\inventory\ShapelessRecipe; use pocketmine\inventory\transaction\action\InventoryAction; +use pocketmine\inventory\transaction\CraftingTransaction; use pocketmine\inventory\transaction\InventoryTransaction; use pocketmine\item\Item; use pocketmine\item\ItemFactory; @@ -110,7 +108,6 @@ use pocketmine\network\mcpe\protocol\ChunkRadiusUpdatedPacket; use pocketmine\network\mcpe\protocol\ClientToServerHandshakePacket; use pocketmine\network\mcpe\protocol\CommandBlockUpdatePacket; use pocketmine\network\mcpe\protocol\ContainerClosePacket; -use pocketmine\network\mcpe\protocol\CraftingEventPacket; use pocketmine\network\mcpe\protocol\DataPacket; use pocketmine\network\mcpe\protocol\DisconnectPacket; use pocketmine\network\mcpe\protocol\EntityEventPacket; @@ -237,6 +234,8 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ /** @var CraftingGrid */ protected $craftingGrid = null; + /** @var CraftingTransaction|null */ + protected $craftingTransaction = null; public $creationTime = 0; @@ -2189,6 +2188,25 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ $actions[] = $action; } + if($packet->isCraftingPart){ + if($this->craftingTransaction === null){ + $this->craftingTransaction = new CraftingTransaction($this, $actions); + }else{ + foreach($actions as $action){ + $this->craftingTransaction->addAction($action); + } + } + + 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 + + $this->craftingTransaction->execute(); //if it can't execute, no inventories will be modified + $this->craftingTransaction = null; + } + + return true; + } + switch($packet->transactionType){ case InventoryTransactionPacket::TYPE_NORMAL: $transaction = new InventoryTransaction($this, $actions); @@ -2778,172 +2796,6 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ return true; } - public function handleCraftingEvent(CraftingEventPacket $packet) : bool{ - if($this->spawned === false or !$this->isAlive()){ - return true; - } - - $recipe = $this->server->getCraftingManager()->getRecipe($packet->id); - - if($recipe === null or ($recipe->requiresCraftingTable() and $this->craftingType === 0) or count($packet->input) === 0){ - $this->inventory->sendContents($this); - return false; - } - - $canCraft = true; - - if($recipe instanceof ShapedRecipe){ - for($x = 0; $x < 3 and $canCraft; ++$x){ - for($y = 0; $y < 3; ++$y){ - /** @var Item $item */ - $item = $packet->input[$y * 3 + $x]; - $ingredient = $recipe->getIngredient($x, $y); - if($item->getCount() > 0){ - if($ingredient === null or !$ingredient->equals($item, !$ingredient->hasAnyDamageValue(), $ingredient->hasCompoundTag())){ - $canCraft = false; - break; - } - } - } - } - }elseif($recipe instanceof ShapelessRecipe){ - $needed = $recipe->getIngredientList(); - - for($x = 0; $x < 3 and $canCraft; ++$x){ - for($y = 0; $y < 3; ++$y){ - /** @var Item $item */ - $item = clone $packet->input[$y * 3 + $x]; - - foreach($needed as $k => $n){ - if($n->equals($item, !$n->hasAnyDamageValue(), $n->hasCompoundTag())){ - $remove = min($n->getCount(), $item->getCount()); - $n->setCount($n->getCount() - $remove); - $item->setCount($item->getCount() - $remove); - - if($n->getCount() === 0){ - unset($needed[$k]); - } - } - } - - if($item->getCount() > 0){ - $canCraft = false; - break; - } - } - } - - if(count($needed) > 0){ - $canCraft = false; - } - }else{ - $canCraft = false; - } - - /** @var Item[] $ingredients */ - $ingredients = $packet->input; - $result = $packet->output[0]; - - if(!$canCraft or !$recipe->getResult()->equals($result)){ - $this->server->getLogger()->debug("Unmatched recipe " . $recipe->getId() . " from player " . $this->getName() . ": expected " . $recipe->getResult() . ", got " . $result . ", using: " . implode(", ", $ingredients)); - $this->inventory->sendContents($this); - return false; - } - - $used = array_fill(0, $this->inventory->getSize(), 0); - - foreach($ingredients as $ingredient){ - $slot = -1; - foreach($this->inventory->getContents() as $index => $item){ - if($ingredient->getId() !== 0 and $ingredient->equals($item, !$ingredient->hasAnyDamageValue(), $ingredient->hasCompoundTag()) and ($item->getCount() - $used[$index]) >= 1){ - $slot = $index; - $used[$index]++; - break; - } - } - - if($ingredient->getId() !== 0 and $slot === -1){ - $canCraft = false; - break; - } - } - - if(!$canCraft){ - $this->server->getLogger()->debug("Unmatched recipe " . $recipe->getId() . " from player " . $this->getName() . ": client does not have enough items, using: " . implode(", ", $ingredients)); - $this->inventory->sendContents($this); - return false; - } - - $this->server->getPluginManager()->callEvent($ev = new CraftItemEvent($this, $ingredients, $recipe)); - - if($ev->isCancelled()){ - $this->inventory->sendContents($this); - return true; - } - - foreach($used as $slot => $count){ - if($count === 0){ - continue; - } - - $item = $this->inventory->getItem($slot); - - if($item->getCount() > $count){ - $newItem = clone $item; - $newItem->setCount($item->getCount() - $count); - }else{ - $newItem = ItemFactory::get(Item::AIR, 0, 0); - } - - $this->inventory->setItem($slot, $newItem); - } - - $extraItem = $this->inventory->addItem($recipe->getResult()); - if(count($extraItem) > 0){ - foreach($extraItem as $item){ - $this->level->dropItem($this, $item); - } - } - - switch($recipe->getResult()->getId()){ - case Item::CRAFTING_TABLE: - $this->awardAchievement("buildWorkBench"); - break; - case Item::WOODEN_PICKAXE: - $this->awardAchievement("buildPickaxe"); - break; - case Item::FURNACE: - $this->awardAchievement("buildFurnace"); - break; - case Item::WOODEN_HOE: - $this->awardAchievement("buildHoe"); - break; - case Item::BREAD: - $this->awardAchievement("makeBread"); - break; - case Item::CAKE: - //TODO: detect complex recipes like cake that leave remains - $this->awardAchievement("bakeCake"); - $this->inventory->addItem(ItemFactory::get(Item::BUCKET, 0, 3)); - break; - case Item::STONE_PICKAXE: - case Item::GOLDEN_PICKAXE: - case Item::IRON_PICKAXE: - case Item::DIAMOND_PICKAXE: - $this->awardAchievement("buildBetterPickaxe"); - break; - case Item::WOODEN_SWORD: - $this->awardAchievement("buildSword"); - break; - case Item::DIAMOND: - $this->awardAchievement("diamond"); - break; - } - - - return true; - } - public function handleAdventureSettings(AdventureSettingsPacket $packet) : bool{ if($packet->entityUniqueId !== $this->getId()){ return false; //TODO diff --git a/src/pocketmine/event/inventory/CraftItemEvent.php b/src/pocketmine/event/inventory/CraftItemEvent.php index e0c9c5bc7..170287cde 100644 --- a/src/pocketmine/event/inventory/CraftItemEvent.php +++ b/src/pocketmine/event/inventory/CraftItemEvent.php @@ -26,51 +26,55 @@ namespace pocketmine\event\inventory; use pocketmine\event\Cancellable; use pocketmine\event\Event; use pocketmine\inventory\Recipe; +use pocketmine\inventory\transaction\CraftingTransaction; use pocketmine\item\Item; use pocketmine\Player; class CraftItemEvent extends Event implements Cancellable{ public static $handlerList = null; - /** @var Item[] */ - private $input; - /** @var Recipe */ - private $recipe; - /** @var Player */ - private $player; - + /** @var CraftingTransaction */ + private $transaction; /** - * @param Player $player - * @param Item[] $input - * @param Recipe $recipe + * @param CraftingTransaction $transaction */ - public function __construct(Player $player, array $input, Recipe $recipe){ - $this->player = $player; - $this->input = $input; - $this->recipe = $recipe; + public function __construct(CraftingTransaction $transaction){ + $this->transaction = $transaction; + } + + public function getTransaction() : CraftingTransaction{ + return $this->transaction; } /** + * @deprecated This returns a one-dimensional array of ingredients and does not account for the positioning of + * items in the crafting grid. Prefer getting the input map from the transaction instead. + * * @return Item[] */ public function getInput() : array{ return array_map(function(Item $item) : Item{ return clone $item; - }, $this->input); + }, array_merge(...$this->transaction->getInputMap())); } /** * @return Recipe */ public function getRecipe() : Recipe{ - return $this->recipe; + $recipe = $this->transaction->getRecipe(); + if($recipe === null){ + throw new \RuntimeException("This shouldn't be called if the transaction can't be executed"); + } + + return $recipe; } /** * @return Player */ public function getPlayer() : Player{ - return $this->player; + return $this->transaction->getSource(); } } diff --git a/src/pocketmine/inventory/CraftingManager.php b/src/pocketmine/inventory/CraftingManager.php index 8b1e4614b..3513971db 100644 --- a/src/pocketmine/inventory/CraftingManager.php +++ b/src/pocketmine/inventory/CraftingManager.php @@ -37,8 +37,10 @@ class CraftingManager{ /** @var CraftingRecipe[] */ public $recipes = []; - /** @var CraftingRecipe[][] */ - protected $recipeLookup = []; + /** @var ShapedRecipe[][] */ + protected $shapedRecipes = []; + /** @var ShapelessRecipe[][] */ + protected $shapelessRecipes = []; /** @var FurnaceRecipe[] */ public $furnaceRecipes = []; @@ -128,6 +130,14 @@ class CraftingManager{ return $this->craftingDataCache; } + /** + * Function used to arrange Shapeless Recipe ingredient lists into a consistent order. + * + * @param Item $i1 + * @param Item $i2 + * + * @return int + */ public function sort(Item $i1, Item $i2){ if($i1->getId() > $i2->getId()){ return 1; @@ -162,6 +172,20 @@ class CraftingManager{ return $this->recipes; } + /** + * @return ShapelessRecipe[][] + */ + public function getShapelessRecipes() : array{ + return $this->shapelessRecipes; + } + + /** + * @return ShapedRecipe[][] + */ + public function getShapedRecipes() : array{ + return $this->shapedRecipes; + } + /** * @return FurnaceRecipe[] */ @@ -169,36 +193,11 @@ class CraftingManager{ return $this->furnaceRecipes; } - /** - * @param Item $input - * - * @return FurnaceRecipe|null - */ - public function matchFurnaceRecipe(Item $input) : ?FurnaceRecipe{ - return $this->furnaceRecipes[$input->getId() . ":" . $input->getDamage()] ?? $this->furnaceRecipes[$input->getId() . ":?"] ?? null; - } - /** * @param ShapedRecipe $recipe */ public function registerShapedRecipe(ShapedRecipe $recipe) : void{ - $result = $recipe->getResult(); - - /** @var Item[][] $ingredients */ - $ingredients = $recipe->getIngredientMap(); - $hash = ""; - foreach($ingredients as $v){ - foreach($v as $item){ - if($item !== null){ - /** @var Item $item */ - $hash .= $item->getId() . ":" . ($item->hasAnyDamageValue() ? "?" : $item->getDamage()) . "x" . $item->getCount() . ","; - } - } - - $hash .= ";"; - } - - $this->recipeLookup[$result->getId() . ":" . $result->getDamage()][$hash] = $recipe; + $this->shapedRecipes[json_encode($recipe->getResult())][json_encode($recipe->getIngredientMap())] = $recipe; $this->craftingDataCache = null; } @@ -206,14 +205,9 @@ class CraftingManager{ * @param ShapelessRecipe $recipe */ public function registerShapelessRecipe(ShapelessRecipe $recipe) : void{ - $result = $recipe->getResult(); - $hash = ""; $ingredients = $recipe->getIngredientList(); usort($ingredients, [$this, "sort"]); - foreach($ingredients as $item){ - $hash .= $item->getId() . ":" . ($item->hasAnyDamageValue() ? "?" : $item->getDamage()) . "x" . $item->getCount() . ","; - } - $this->recipeLookup[$result->getId() . ":" . $result->getDamage()][$hash] = $recipe; + $this->shapelessRecipes[json_encode($recipe->getResult())][json_encode($ingredients)] = $recipe; $this->craftingDataCache = null; } @@ -227,61 +221,76 @@ class CraftingManager{ } /** - * @param ShapelessRecipe $recipe - * @return bool + * Clones a map of Item objects to avoid accidental modification. + * + * @param Item[][] $map + * @return Item[][] */ - public function matchRecipe(ShapelessRecipe $recipe) : bool{ - if(!isset($this->recipeLookup[$idx = $recipe->getResult()->getId() . ":" . $recipe->getResult()->getDamage()])){ - return false; - } - - $hash = ""; - $ingredients = $recipe->getIngredientList(); - usort($ingredients, [$this, "sort"]); - foreach($ingredients as $item){ - $hash .= $item->getId() . ":" . ($item->hasAnyDamageValue() ? "?" : $item->getDamage()) . "x" . $item->getCount() . ","; - } - - if(isset($this->recipeLookup[$idx][$hash])){ - return true; - } - - $hasRecipe = null; - foreach($this->recipeLookup[$idx] as $possibleRecipe){ - if($possibleRecipe instanceof ShapelessRecipe){ - if($possibleRecipe->getIngredientCount() !== count($ingredients)){ - continue; - } - $checkInput = $possibleRecipe->getIngredientList(); - foreach($ingredients as $item){ - $amount = $item->getCount(); - foreach($checkInput as $k => $checkItem){ - if($checkItem->equals($item, !$checkItem->hasAnyDamageValue(), $checkItem->hasCompoundTag())){ - $remove = min($checkItem->getCount(), $amount); - $checkItem->setCount($checkItem->getCount() - $remove); - if($checkItem->getCount() === 0){ - unset($checkInput[$k]); - } - $amount -= $remove; - if($amount === 0){ - break; - } - } - } - } - - if(count($checkInput) === 0){ - $hasRecipe = $possibleRecipe; - break; - } - } - if($hasRecipe instanceof Recipe){ - break; + private function cloneItemMap(array $map) : array{ + /** @var Item[] $row */ + foreach($map as $y => $row){ + foreach($row as $x => $item){ + $item = clone $item; } } - return $hasRecipe !== null; + return $map; + } + /** + * @param Item[][] $inputMap + * @param Item $primaryOutput + * @param Item[][] $extraOutputMap + * + * @return CraftingRecipe|null + */ + public function matchRecipe(array $inputMap, Item $primaryOutput, array $extraOutputMap) : ?CraftingRecipe{ + //TODO: try to match special recipes before anything else (first they need to be implemented!) + + $outputHash = json_encode($primaryOutput); + 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))){ + return $recipe; + } + } + } + + if(isset($this->shapelessRecipes[$outputHash])){ + $list = array_merge(...$inputMap); + usort($list, [$this, "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))){ + return $recipe; + } + } + } + + return null; + } + + /** + * @param Item $input + * + * @return FurnaceRecipe|null + */ + public function matchFurnaceRecipe(Item $input) : ?FurnaceRecipe{ + return $this->furnaceRecipes[$input->getId() . ":" . $input->getDamage()] ?? $this->furnaceRecipes[$input->getId() . ":?"] ?? null; } /** diff --git a/src/pocketmine/inventory/CraftingRecipe.php b/src/pocketmine/inventory/CraftingRecipe.php index ee49f60b0..1eb19ea90 100644 --- a/src/pocketmine/inventory/CraftingRecipe.php +++ b/src/pocketmine/inventory/CraftingRecipe.php @@ -49,4 +49,15 @@ interface CraftingRecipe extends Recipe{ * @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. + * + * @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 bool + */ + public function matchItems(array $input, array $output) : bool; } \ No newline at end of file diff --git a/src/pocketmine/inventory/ShapedRecipe.php b/src/pocketmine/inventory/ShapedRecipe.php index 9f2a49ff6..8e4b746cb 100644 --- a/src/pocketmine/inventory/ShapedRecipe.php +++ b/src/pocketmine/inventory/ShapedRecipe.php @@ -199,4 +199,63 @@ class ShapedRecipe implements CraftingRecipe{ public function requiresCraftingTable() : bool{ return $this->getHeight() > 2 or $this->getWidth() > 2; } + + /** + * @param Item[][] $input + * @param Item[][] $output + * + * @return bool + */ + public function matchItems(array $input, array $output) : bool{ + $map = $this->getIngredientMap(); + + //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()){ + return false; + } + + unset($input[$y][$x]); + } + } + + //we shouldn't need to check if there's anything left in the map, the last block should take care of that + //however, we DO need to check if there are too many items 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 + } + } + } + + //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; + } } diff --git a/src/pocketmine/inventory/ShapelessRecipe.php b/src/pocketmine/inventory/ShapelessRecipe.php index 7f17363d2..b33445d52 100644 --- a/src/pocketmine/inventory/ShapelessRecipe.php +++ b/src/pocketmine/inventory/ShapelessRecipe.php @@ -143,4 +143,55 @@ class ShapelessRecipe implements CraftingRecipe{ public function requiresCraftingTable() : bool{ return count($this->ingredients) > 4; } + + /** + * @param Item[][] $input + * @param Item[][] $output + * + * @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(); + + 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; + } + } + } + + return count($haveItems) === 0 and count($needItems) === 0; + } } \ No newline at end of file diff --git a/src/pocketmine/inventory/transaction/CraftingTransaction.php b/src/pocketmine/inventory/transaction/CraftingTransaction.php new file mode 100644 index 000000000..14f4e7e45 --- /dev/null +++ b/src/pocketmine/inventory/transaction/CraftingTransaction.php @@ -0,0 +1,193 @@ +gridSize = ($source->getCraftingGrid() instanceof BigCraftingGrid) ? 3 : 2; + + $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($this->inputs[$y][$x]->isNull()){ + $this->inputs[$y][$x] = clone $item; + }else{ + throw new \RuntimeException("Input $index has already been set"); + } + } + + 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($this->secondaryOutputs[$y][$x]->isNull()){ + $this->secondaryOutputs[$y][$x] = clone $item; + }else{ + throw new \RuntimeException("Output $index has already been set"); + } + } + + public function getPrimaryOutput() : ?Item{ + return $this->primaryOutput; + } + + public function setPrimaryOutput(Item $item) : void{ + if($this->primaryOutput === null){ + $this->primaryOutput = clone $item; + }else{ + throw new \RuntimeException("Primary result item has already been set"); + } + } + + public function getRecipe() : ?CraftingRecipe{ + return $this->recipe; + } + + private function reindexInputs() : void{ + if($this->isReindexed){ + return; + } + + $xOffset = $this->gridSize; + $yOffset = $this->gridSize; + + $height = 0; + $width = 0; + + foreach($this->inputs as $y => $row){ + foreach($row as $x => $item){ + if(!$item->isNull()){ + $xOffset = min($x, $xOffset); + $yOffset = min($y, $yOffset); + + $height = max($y + 1 - $yOffset, $height); + $width = max($x + 1 - $xOffset, $width); + } + } + } + + if($height === 0 or $width === 0){ + return; + } + + $air = ItemFactory::get(Item::AIR, 0, 0); + $reindexed = array_fill(0, $height, array_fill(0, $width, $air)); + foreach($reindexed as $y => $row){ + foreach($row as $x => $item){ + $reindexed[$y][$x] = $this->inputs[$y + $yOffset][$x + $xOffset]; + } + } + + $this->inputs = $reindexed; + + $this->isReindexed = true; + } + + public function canExecute() : bool{ + $this->reindexInputs(); + + $this->recipe = $this->source->getServer()->getCraftingManager()->matchRecipe($this->inputs, $this->primaryOutput, $this->secondaryOutputs); + + return $this->recipe !== null and parent::canExecute(); + } + + protected function callExecuteEvent() : bool{ + $this->source->getServer()->getPluginManager()->callEvent($ev = new CraftItemEvent($this)); + return !$ev->isCancelled(); + } + + 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; + } + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/pocketmine/inventory/transaction/InventoryTransaction.php b/src/pocketmine/inventory/transaction/InventoryTransaction.php index 1cad513c1..1ca71119a 100644 --- a/src/pocketmine/inventory/transaction/InventoryTransaction.php +++ b/src/pocketmine/inventory/transaction/InventoryTransaction.php @@ -40,7 +40,7 @@ class InventoryTransaction{ private $creationTime; protected $hasExecuted = false; /** @var Player */ - protected $source = null; + protected $source; /** @var Inventory[] */ protected $inventories = []; @@ -52,7 +52,7 @@ class InventoryTransaction{ * @param Player $source * @param InventoryAction[] $actions */ - public function __construct(Player $source = null, array $actions = []){ + public function __construct(Player $source, array $actions = []){ $this->creationTime = microtime(true); $this->source = $source; foreach($actions as $action){ @@ -219,8 +219,6 @@ class InventoryTransaction{ } $this->addAction(new SlotChangeAction($originalAction->getInventory(), $originalAction->getSlot(), $originalAction->getSourceItem(), $lastTargetItem)); - - MainLogger::getLogger()->debug("Successfully compacted " . count($originalList) . " actions for " . $this->source->getName()); } return true; @@ -243,6 +241,11 @@ class InventoryTransaction{ } } + protected function callExecuteEvent() : bool{ + Server::getInstance()->getPluginManager()->callEvent($ev = new InventoryTransactionEvent($this)); + return !$ev->isCancelled(); + } + /** * @return bool */ @@ -251,8 +254,7 @@ class InventoryTransaction{ return false; } - Server::getInstance()->getPluginManager()->callEvent($ev = new InventoryTransactionEvent($this)); - if($ev->isCancelled()){ + if(!$this->callExecuteEvent()){ $this->handleFailed(); return true; } diff --git a/src/pocketmine/inventory/transaction/action/CraftingTakeResultAction.php b/src/pocketmine/inventory/transaction/action/CraftingTakeResultAction.php new file mode 100644 index 000000000..b3885fb5d --- /dev/null +++ b/src/pocketmine/inventory/transaction/action/CraftingTakeResultAction.php @@ -0,0 +1,59 @@ +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{ + + } + +} \ No newline at end of file diff --git a/src/pocketmine/inventory/transaction/action/CraftingTransferMaterialAction.php b/src/pocketmine/inventory/transaction/action/CraftingTransferMaterialAction.php new file mode 100644 index 000000000..9c0748812 --- /dev/null +++ b/src/pocketmine/inventory/transaction/action/CraftingTransferMaterialAction.php @@ -0,0 +1,73 @@ +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{ + + } +} \ No newline at end of file diff --git a/src/pocketmine/inventory/transaction/action/SlotChangeAction.php b/src/pocketmine/inventory/transaction/action/SlotChangeAction.php index c87548d21..dfb7a46dc 100644 --- a/src/pocketmine/inventory/transaction/action/SlotChangeAction.php +++ b/src/pocketmine/inventory/transaction/action/SlotChangeAction.php @@ -83,6 +83,7 @@ class SlotChangeAction extends InventoryAction{ * Adds this action's target inventory to the transaction's inventory list. * * @param InventoryTransaction $transaction + * */ public function onAddToTransaction(InventoryTransaction $transaction) : void{ $transaction->addInventory($this->inventory); diff --git a/src/pocketmine/network/mcpe/PlayerNetworkSessionAdapter.php b/src/pocketmine/network/mcpe/PlayerNetworkSessionAdapter.php index 224d30b87..ae753a5d5 100644 --- a/src/pocketmine/network/mcpe/PlayerNetworkSessionAdapter.php +++ b/src/pocketmine/network/mcpe/PlayerNetworkSessionAdapter.php @@ -172,7 +172,7 @@ class PlayerNetworkSessionAdapter extends NetworkSession{ } public function handleCraftingEvent(CraftingEventPacket $packet) : bool{ - return $this->player->handleCraftingEvent($packet); + return true; //this is a broken useless packet, so we don't use it } public function handleAdventureSettings(AdventureSettingsPacket $packet) : bool{ diff --git a/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php b/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php index 725e8db85..2fa8832d3 100644 --- a/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php +++ b/src/pocketmine/network/mcpe/protocol/InventoryTransactionPacket.php @@ -47,10 +47,16 @@ class InventoryTransactionPacket extends DataPacket{ const USE_ITEM_ON_ENTITY_ACTION_INTERACT = 0; const USE_ITEM_ON_ENTITY_ACTION_ATTACK = 1; - /** @var int */ public $transactionType; + /** + * @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 $isCraftingPart = 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 b7f5e67f9..242cf3d9d 100644 --- a/src/pocketmine/network/mcpe/protocol/types/NetworkInventoryAction.php +++ b/src/pocketmine/network/mcpe/protocol/types/NetworkInventoryAction.php @@ -23,6 +23,8 @@ 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; @@ -108,6 +110,12 @@ class NetworkInventoryAction{ break; case self::SOURCE_TODO: $this->windowId = $packet->getVarInt(); + switch($this->windowId){ + case self::SOURCE_TYPE_CRAFTING_USE_INGREDIENT: + case self::SOURCE_TYPE_CRAFTING_RESULT: + $packet->isCraftingPart = true; + break; + } break; } @@ -190,6 +198,10 @@ class NetworkInventoryAction{ case self::SOURCE_TYPE_CRAFTING_REMOVE_INGREDIENT: $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); case self::SOURCE_TYPE_CONTAINER_DROP_CONTENTS: //TODO: this type applies to all fake windows, not just crafting