110    public static function matchIngredients(array $providedItems, array $recipeIngredients, 
int $expectedIterations) : void{
 
  111        if(count($recipeIngredients) === 0){
 
  114        if(count($providedItems) === 0){
 
  118        $packedProvidedItems = self::packItems(Utils::cloneObjectArray($providedItems));
 
  119        $packedProvidedItemMatches = array_fill_keys(array_keys($packedProvidedItems), 0);
 
  121        $recipeIngredientMatches = [];
 
  123        foreach($recipeIngredients as $ingredientIndex => $recipeIngredient){
 
  125            foreach($packedProvidedItems as $itemIndex => $packedItem){
 
  126                if($recipeIngredient->accepts($packedItem)){
 
  127                    $packedProvidedItemMatches[$itemIndex]++;
 
  128                    $acceptedItems[$itemIndex] = $itemIndex;
 
  132            if(count($acceptedItems) === 0){
 
  133                throw new TransactionValidationException(
"No provided items satisfy ingredient requirement $recipeIngredient");
 
  136            $recipeIngredientMatches[$ingredientIndex] = $acceptedItems;
 
  139        foreach($packedProvidedItemMatches as $itemIndex => $itemMatchCount){
 
  140            if($itemMatchCount === 0){
 
  141                $item = $packedProvidedItems[$itemIndex];
 
  142                throw new TransactionValidationException(
"Provided item $item is not accepted by any recipe ingredient");
 
  149        uasort($recipeIngredientMatches, fn(array $a, array $b) => count($a) <=> count($b));
 
  151        foreach($recipeIngredientMatches as $ingredientIndex => $acceptedItems){
 
  152            $needed = $expectedIterations;
 
  154            foreach($packedProvidedItems as $itemIndex => $item){
 
  155                if(!isset($acceptedItems[$itemIndex])){
 
  159                $taken = min($needed, $item->getCount());
 
  161                $item->setCount($item->getCount() - $taken);
 
  163                if($item->getCount() === 0){
 
  164                    unset($packedProvidedItems[$itemIndex]);
 
  173            $recipeIngredient = $recipeIngredients[$ingredientIndex];
 
  174            $actualIterations = $expectedIterations - $needed;
 
  175            throw new TransactionValidationException(
"Not enough items to satisfy recipe ingredient $recipeIngredient for $expectedIterations (only have enough items for $actualIterations iterations)");
 
  178        if(count($packedProvidedItems) > 0){
 
  179            throw new TransactionValidationException(
"Not all provided items were used");
 
 
  192    protected function matchOutputs(array $txItems, array $recipeItems) : int{
 
  193        if(count($recipeItems) === 0){
 
  196        if(count($txItems) === 0){
 
  201        while(count($recipeItems) > 0){
 
  203            $recipeItem = array_pop($recipeItems);
 
  204            $needCount = $recipeItem->getCount();
 
  205            foreach($recipeItems as $i => $otherRecipeItem){
 
  206                if($otherRecipeItem->canStackWith($recipeItem)){ 
 
  207                    $needCount += $otherRecipeItem->getCount();
 
  208                    unset($recipeItems[$i]);
 
  213            foreach($txItems as $j => $txItem){
 
  214                if($txItem->canStackWith($recipeItem)){
 
  215                    $haveCount += $txItem->getCount();
 
  220            if($haveCount % $needCount !== 0){
 
  222                throw new TransactionValidationException(
"Expected an exact multiple of required $recipeItem (given: $haveCount, needed: $needCount)");
 
  225            $multiplier = intdiv($haveCount, $needCount);
 
  227                throw new TransactionValidationException(
"Expected more than zero items matching $recipeItem (given: $haveCount, needed: $needCount)");
 
  229            if($iterations === 0){
 
  230                $iterations = $multiplier;
 
  231            }elseif($multiplier !== $iterations){
 
  233                throw new TransactionValidationException(
"Expected $recipeItem x$iterations, but found x$multiplier");
 
  237        if(count($txItems) > 0){
 
  239            throw new TransactionValidationException(
"Expected 0 items left over, have " . count($txItems));
 
 
  245    private function validateRecipe(CraftingRecipe $recipe, ?
int $expectedRepetitions) : int{
 
  258        $this->squashDuplicateSlotChanges();
 
  259        if(count($this->actions) < 1){
 
  263        $this->matchItems($this->outputs, $this->inputs);
 
  265        if($this->recipe === 
null){
 
  267            foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
 
  270                    $this->repetitions = $this->matchOutputs($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()));
 
  272                    self::matchIngredients($this->inputs, $recipe->getIngredientList(), $this->repetitions);
 
  275                    $this->recipe = $recipe;
 
  283            if($this->recipe === 
null){
 
  284                throw new TransactionValidationException(
"Unable to match a recipe to transaction (tried to match against $failed recipes)");
 
  287            $this->repetitions = $this->validateRecipe($this->recipe, $this->repetitions);
 
 
  292        $ev = new CraftItemEvent($this, $this->recipe, $this->repetitions, $this->inputs, $this->outputs);