mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-07-01 23:59:53 +00:00
Rewritten crafting, fixed #45
This commit is contained in:
parent
043ae487de
commit
240cc3043a
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
193
src/pocketmine/inventory/transaction/CraftingTransaction.php
Normal file
193
src/pocketmine/inventory/transaction/CraftingTransaction.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?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\inventory\transaction;
|
||||
|
||||
use pocketmine\event\inventory\CraftItemEvent;
|
||||
use pocketmine\inventory\BigCraftingGrid;
|
||||
use pocketmine\inventory\CraftingRecipe;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\ItemFactory;
|
||||
use pocketmine\Player;
|
||||
|
||||
class CraftingTransaction extends InventoryTransaction{
|
||||
|
||||
protected $gridSize;
|
||||
/** @var Item[][] */
|
||||
protected $inputs;
|
||||
/** @var Item[][] */
|
||||
protected $secondaryOutputs;
|
||||
/** @var Item|null */
|
||||
protected $primaryOutput;
|
||||
|
||||
private $isReindexed = false;
|
||||
|
||||
/** @var CraftingRecipe|null */
|
||||
protected $recipe = null;
|
||||
|
||||
public function __construct(Player $source, $actions = []){
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
<?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\inventory\transaction\action;
|
||||
|
||||
use pocketmine\inventory\transaction\CraftingTransaction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\Player;
|
||||
|
||||
/**
|
||||
* Action used to take the primary result item during crafting.
|
||||
*/
|
||||
class CraftingTakeResultAction extends InventoryAction{
|
||||
|
||||
public function onAddToTransaction(InventoryTransaction $transaction) : void{
|
||||
if($transaction instanceof CraftingTransaction){
|
||||
$transaction->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{
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?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\inventory\transaction\action;
|
||||
|
||||
use pocketmine\inventory\transaction\CraftingTransaction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\Player;
|
||||
|
||||
/**
|
||||
* Action used to take ingredients out of the crafting grid, or put secondary results into the crafting grid, when
|
||||
* crafting.
|
||||
*/
|
||||
class CraftingTransferMaterialAction extends InventoryAction{
|
||||
/** @var int */
|
||||
private $slot;
|
||||
|
||||
public function __construct(Item $sourceItem, Item $targetItem, int $slot){
|
||||
parent::__construct($sourceItem, $targetItem);
|
||||
$this->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{
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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{
|
||||
|
@ -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 = [];
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user