Rewritten crafting, fixed #45

This commit is contained in:
Dylan K. Taylor
2017-09-24 14:14:24 +01:00
parent 043ae487de
commit 240cc3043a
14 changed files with 611 additions and 279 deletions

View File

@ -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;
}
/**

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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;
}

View File

@ -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{
}
}

View File

@ -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{
}
}

View File

@ -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);