PocketMine-MP 5.21.2 git-a6534ecbbbcf369264567d27e5ed70f7f5be9816
Loading...
Searching...
No Matches
CraftingTransaction.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\inventory\transaction;
25
33use function array_fill_keys;
34use function array_keys;
35use function array_pop;
36use function count;
37use function intdiv;
38use function min;
39use function uasort;
40
57 protected ?CraftingRecipe $recipe = null;
58 protected ?int $repetitions = null;
59
61 protected array $inputs = [];
63 protected array $outputs = [];
64
65 private CraftingManager $craftingManager;
66
67 public function __construct(Player $source, CraftingManager $craftingManager, array $actions = [], ?CraftingRecipe $recipe = null, ?int $repetitions = null){
68 parent::__construct($source, $actions);
69 $this->craftingManager = $craftingManager;
70 $this->recipe = $recipe;
71 $this->repetitions = $repetitions;
72 }
73
78 private static function packItems(array $providedItems) : array{
79 $packedProvidedItems = [];
80 while(count($providedItems) > 0){
81 $item = array_pop($providedItems);
82 foreach($providedItems as $k => $otherItem){
83 if($item->canStackWith($otherItem)){
84 $item->setCount($item->getCount() + $otherItem->getCount());
85 unset($providedItems[$k]);
86 }
87 }
88 $packedProvidedItems[] = $item;
89 }
90
91 return $packedProvidedItems;
92 }
93
98 public static function matchIngredients(array $providedItems, array $recipeIngredients, int $expectedIterations) : void{
99 if(count($recipeIngredients) === 0){
100 throw new TransactionValidationException("No recipe ingredients given");
101 }
102 if(count($providedItems) === 0){
103 throw new TransactionValidationException("No transaction items given");
104 }
105
106 $packedProvidedItems = self::packItems(Utils::cloneObjectArray($providedItems));
107 $packedProvidedItemMatches = array_fill_keys(array_keys($packedProvidedItems), 0);
108
109 $recipeIngredientMatches = [];
110
111 foreach($recipeIngredients as $ingredientIndex => $recipeIngredient){
112 $acceptedItems = [];
113 foreach($packedProvidedItems as $itemIndex => $packedItem){
114 if($recipeIngredient->accepts($packedItem)){
115 $packedProvidedItemMatches[$itemIndex]++;
116 $acceptedItems[$itemIndex] = $itemIndex;
117 }
118 }
119
120 if(count($acceptedItems) === 0){
121 throw new TransactionValidationException("No provided items satisfy ingredient requirement $recipeIngredient");
122 }
123
124 $recipeIngredientMatches[$ingredientIndex] = $acceptedItems;
125 }
126
127 foreach($packedProvidedItemMatches as $itemIndex => $itemMatchCount){
128 if($itemMatchCount === 0){
129 $item = $packedProvidedItems[$itemIndex];
130 throw new TransactionValidationException("Provided item $item is not accepted by any recipe ingredient");
131 }
132 }
133
134 //Most picky ingredients first - avoid picky ingredient getting their items stolen by wildcard ingredients
135 //TODO: this is still insufficient when multiple wildcard ingredients have overlaps, but we don't (yet) have to
136 //worry about those.
137 uasort($recipeIngredientMatches, fn(array $a, array $b) => count($a) <=> count($b));
138
139 foreach($recipeIngredientMatches as $ingredientIndex => $acceptedItems){
140 $needed = $expectedIterations;
141
142 foreach($packedProvidedItems as $itemIndex => $item){
143 if(!isset($acceptedItems[$itemIndex])){
144 continue;
145 }
146
147 $taken = min($needed, $item->getCount());
148 $needed -= $taken;
149 $item->setCount($item->getCount() - $taken);
150
151 if($item->getCount() === 0){
152 unset($packedProvidedItems[$itemIndex]);
153 }
154
155 if($needed === 0){
156 //validation passed!
157 continue 2;
158 }
159 }
160
161 $recipeIngredient = $recipeIngredients[$ingredientIndex];
162 $actualIterations = $expectedIterations - $needed;
163 throw new TransactionValidationException("Not enough items to satisfy recipe ingredient $recipeIngredient for $expectedIterations (only have enough items for $actualIterations iterations)");
164 }
165
166 if(count($packedProvidedItems) > 0){
167 throw new TransactionValidationException("Not all provided items were used");
168 }
169 }
170
177 protected function matchOutputs(array $txItems, array $recipeItems) : int{
178 if(count($recipeItems) === 0){
179 throw new TransactionValidationException("No recipe items given");
180 }
181 if(count($txItems) === 0){
182 throw new TransactionValidationException("No transaction items given");
183 }
184
185 $iterations = 0;
186 while(count($recipeItems) > 0){
188 $recipeItem = array_pop($recipeItems);
189 $needCount = $recipeItem->getCount();
190 foreach($recipeItems as $i => $otherRecipeItem){
191 if($otherRecipeItem->canStackWith($recipeItem)){ //make sure they have the same wildcards set
192 $needCount += $otherRecipeItem->getCount();
193 unset($recipeItems[$i]);
194 }
195 }
196
197 $haveCount = 0;
198 foreach($txItems as $j => $txItem){
199 if($txItem->canStackWith($recipeItem)){
200 $haveCount += $txItem->getCount();
201 unset($txItems[$j]);
202 }
203 }
204
205 if($haveCount % $needCount !== 0){
206 //wrong count for this output, should divide exactly
207 throw new TransactionValidationException("Expected an exact multiple of required $recipeItem (given: $haveCount, needed: $needCount)");
208 }
209
210 $multiplier = intdiv($haveCount, $needCount);
211 if($multiplier < 1){
212 throw new TransactionValidationException("Expected more than zero items matching $recipeItem (given: $haveCount, needed: $needCount)");
213 }
214 if($iterations === 0){
215 $iterations = $multiplier;
216 }elseif($multiplier !== $iterations){
217 //wrong count for this output, should match previous outputs
218 throw new TransactionValidationException("Expected $recipeItem x$iterations, but found x$multiplier");
219 }
220 }
221
222 if(count($txItems) > 0){
223 //all items should be destroyed in this process
224 throw new TransactionValidationException("Expected 0 items left over, have " . count($txItems));
225 }
226
227 return $iterations;
228 }
229
230 private function validateRecipe(CraftingRecipe $recipe, ?int $expectedRepetitions) : int{
231 //compute number of times recipe was crafted
232 $repetitions = $this->matchOutputs($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()));
233 if($expectedRepetitions !== null && $repetitions !== $expectedRepetitions){
234 throw new TransactionValidationException("Expected $expectedRepetitions repetitions, got $repetitions");
235 }
236 //assert that $repetitions x recipe ingredients should be consumed
237 self::matchIngredients($this->inputs, $recipe->getIngredientList(), $repetitions);
238
239 return $repetitions;
240 }
241
242 public function validate() : void{
243 $this->squashDuplicateSlotChanges();
244 if(count($this->actions) < 1){
245 throw new TransactionValidationException("Transaction must have at least one action to be executable");
246 }
247
248 $this->matchItems($this->outputs, $this->inputs);
249
250 if($this->recipe === null){
251 $failed = 0;
252 foreach($this->craftingManager->matchRecipeByOutputs($this->outputs) as $recipe){
253 try{
254 //compute number of times recipe was crafted
255 $this->repetitions = $this->matchOutputs($this->outputs, $recipe->getResultsFor($this->source->getCraftingGrid()));
256 //assert that $repetitions x recipe ingredients should be consumed
257 self::matchIngredients($this->inputs, $recipe->getIngredientList(), $this->repetitions);
258
259 //Success!
260 $this->recipe = $recipe;
261 break;
263 //failed
264 ++$failed;
265 }
266 }
267
268 if($this->recipe === null){
269 throw new TransactionValidationException("Unable to match a recipe to transaction (tried to match against $failed recipes)");
270 }
271 }else{
272 $this->repetitions = $this->validateRecipe($this->recipe, $this->repetitions);
273 }
274 }
275
276 protected function callExecuteEvent() : bool{
277 $ev = new CraftItemEvent($this, $this->recipe, $this->repetitions, $this->inputs, $this->outputs);
278 $ev->call();
279 return !$ev->isCancelled();
280 }
281}
static matchIngredients(array $providedItems, array $recipeIngredients, int $expectedIterations)