PocketMine-MP 5.32.2 git-237b304ef9858756b018e44e8f298093f66f823b
Loading...
Searching...
No Matches
ItemStackRequestExecutor.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\network\mcpe\handler;
25
62use function array_key_first;
63use function count;
64use function spl_object_id;
65
67 private TransactionBuilder $builder;
68
70 private array $requestSlotInfos = [];
71
72 private ?InventoryTransaction $specialTransaction = null;
73
75 private array $craftingResults = [];
76
77 private ?Item $nextCreatedItem = null;
78 private bool $createdItemFromCreativeInventory = false;
79 private int $createdItemsTakenCount = 0;
80
81 public function __construct(
82 private Player $player,
83 private InventoryManager $inventoryManager,
84 private ItemStackRequest $request
85 ){
86 $this->builder = new TransactionBuilder();
87 }
88
89 protected function prettyInventoryAndSlot(Inventory $inventory, int $slot) : string{
90 if($inventory instanceof TransactionBuilderInventory){
91 $inventory = $inventory->getActualInventory();
92 }
93 return (new \ReflectionClass($inventory))->getShortName() . "#" . spl_object_id($inventory) . ", slot: $slot";
94 }
95
99 private function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : void{
100 $info = $this->inventoryManager->getItemStackInfo($inventory, $slotId);
101 if($info === null){
102 throw new AssumptionFailedError("The inventory is tracked and the slot is valid, so this should not be null");
103 }
104
105 if(!($clientItemStackId < 0 ? $info->getRequestId() === $clientItemStackId : $info->getStackId() === $clientItemStackId)){
107 $this->prettyInventoryAndSlot($inventory, $slotId) . ": " .
108 "Mismatched expected itemstack, " .
109 "client expected: $clientItemStackId, server actual: " . $info->getStackId() . ", last modified by request: " . ($info->getRequestId() ?? "none")
110 );
111 }
112 }
113
119 protected function getBuilderInventoryAndSlot(ItemStackRequestSlotInfo $info) : array{
120 [$windowId, $slotId] = ItemStackContainerIdTranslator::translate($info->getContainerName()->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $info->getSlotId());
121 $windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
122 if($windowAndSlot === null){
123 throw new ItemStackRequestProcessException("No open inventory matches container UI ID: " . $info->getContainerName()->getContainerId() . ", slot ID: " . $info->getSlotId());
124 }
125 [$inventory, $slot] = $windowAndSlot;
126 if(!$inventory->slotExists($slot)){
127 throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyInventoryAndSlot($inventory, $slot));
128 }
129
130 if($info->getStackId() !== $this->request->getRequestId()){ //the itemstack may have been modified by the current request
131 $this->matchItemStack($inventory, $slot, $info->getStackId());
132 }
133
134 return [$this->builder->getInventory($inventory), $slot];
135 }
136
140 protected function transferItems(ItemStackRequestSlotInfo $source, ItemStackRequestSlotInfo $destination, int $count) : void{
141 $removed = $this->removeItemFromSlot($source, $count);
142 $this->addItemToSlot($destination, $removed, $count);
143 }
144
149 protected function removeItemFromSlot(ItemStackRequestSlotInfo $slotInfo, int $count) : Item{
150 if($slotInfo->getContainerName()->getContainerId() === ContainerUIIds::CREATED_OUTPUT && $slotInfo->getSlotId() === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
151 //special case for the "created item" output slot
152 //TODO: do we need to send a response for this slot info?
153 return $this->takeCreatedItem($count);
154 }
155 $this->requestSlotInfos[] = $slotInfo;
156 [$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
157 if($count < 1){
158 //this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
159 throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
160 }
161
162 $existingItem = $inventory->getItem($slot);
163 if($existingItem->getCount() < $count){
164 throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount());
165 }
166
167 $removed = $existingItem->pop($count);
168 $inventory->setItem($slot, $existingItem);
169
170 return $removed;
171 }
172
177 protected function addItemToSlot(ItemStackRequestSlotInfo $slotInfo, Item $item, int $count) : void{
178 $this->requestSlotInfos[] = $slotInfo;
179 [$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
180 if($count < 1){
181 //this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
182 throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
183 }
184
185 $existingItem = $inventory->getItem($slot);
186 if(!$existingItem->isNull() && !$existingItem->canStackWith($item)){
187 throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Can only add items to an empty slot, or a slot containing the same item");
188 }
189
190 //we can't use the existing item here; it may be an empty stack
191 $newItem = clone $item;
192 $newItem->setCount($existingItem->getCount() + $count);
193 $inventory->setItem($slot, $newItem);
194 }
195
196 protected function dropItem(Item $item, int $count) : void{
197 if($count < 1){
198 throw new ItemStackRequestProcessException("Cannot drop less than 1 of an item");
199 }
200 $this->builder->addAction(new DropItemAction((clone $item)->setCount($count)));
201 }
202
206 protected function setNextCreatedItem(?Item $item, bool $creative = false) : void{
207 if($item !== null && $item->isNull()){
208 $item = null;
209 }
210 if($this->nextCreatedItem !== null){
211 //while this is more complicated than simply adding the action when the item is taken, this ensures that
212 //plugins can tell the difference between 1 item that got split into 2 slots, vs 2 separate items.
213 if($this->createdItemFromCreativeInventory && $this->createdItemsTakenCount > 0){
214 $this->nextCreatedItem->setCount($this->createdItemsTakenCount);
215 $this->builder->addAction(new CreateItemAction($this->nextCreatedItem));
216 }elseif($this->createdItemsTakenCount < $this->nextCreatedItem->getCount()){
217 throw new ItemStackRequestProcessException("Not all of the previous created item was taken");
218 }
219 }
220 $this->nextCreatedItem = $item;
221 $this->createdItemFromCreativeInventory = $creative;
222 $this->createdItemsTakenCount = 0;
223 }
224
228 protected function beginCrafting(int $recipeId, int $repetitions) : void{
229 if($this->specialTransaction !== null){
230 throw new ItemStackRequestProcessException("Another special transaction is already in progress");
231 }
232 if($repetitions < 1){
233 throw new ItemStackRequestProcessException("Cannot craft a recipe less than 1 time");
234 }
235 if($repetitions > 256){
236 //TODO: we can probably lower this limit to 64, but I'm unsure if there are cases where the client may
237 //request more than 64 repetitions of a recipe.
238 //It's already hard-limited to 256 repetitions in the protocol, so this is just a sanity check.
239 throw new ItemStackRequestProcessException("Cannot craft a recipe more than 256 times");
240 }
241 $craftingManager = $this->player->getServer()->getCraftingManager();
242 $recipeIndex = $recipeId - CraftingDataCache::RECIPE_ID_OFFSET;
243 $recipe = $craftingManager->getCraftingRecipeFromIndex($recipeIndex);
244 if($recipe === null){
245 throw new ItemStackRequestProcessException("No such crafting recipe index: $recipeIndex");
246 }
247
248 $this->specialTransaction = new CraftingTransaction($this->player, $craftingManager, [], $recipe, $repetitions);
249
250 //TODO: Since the system assumes that crafting can only be done in the crafting grid, we have to give it a
251 //crafting grid to make the API happy. No implementation of getResultsFor() actually uses the crafting grid
252 //right now, so this will work, but this will become a problem in the future for things like shulker boxes and
253 //custom crafting recipes.
254 $craftingResults = $recipe->getResultsFor($this->player->getCraftingGrid());
255 foreach($craftingResults as $k => $craftingResult){
256 $craftingResult->setCount($craftingResult->getCount() * $repetitions);
257 $this->craftingResults[$k] = $craftingResult;
258 }
259 if(count($this->craftingResults) === 1){
260 //for multi-output recipes, later actions will tell us which result to create and when
261 $this->setNextCreatedItem($this->craftingResults[array_key_first($this->craftingResults)]);
262 }
263 }
264
268 protected function takeCreatedItem(int $count) : Item{
269 if($count < 1){
270 //this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
271 throw new ItemStackRequestProcessException("Cannot take less than 1 created item");
272 }
273 $createdItem = $this->nextCreatedItem;
274 if($createdItem === null){
275 throw new ItemStackRequestProcessException("No created item is waiting to be taken");
276 }
277
278 if(!$this->createdItemFromCreativeInventory){
279 $availableCount = $createdItem->getCount() - $this->createdItemsTakenCount;
280 if($count > $availableCount){
281 throw new ItemStackRequestProcessException("Not enough created items available to be taken (have $availableCount, tried to take $count)");
282 }
283 }
284
285 $this->createdItemsTakenCount += $count;
286 $takenItem = clone $createdItem;
287 $takenItem->setCount($count);
288 if(!$this->createdItemFromCreativeInventory && $this->createdItemsTakenCount >= $createdItem->getCount()){
289 $this->setNextCreatedItem(null);
290 }
291 return $takenItem;
292 }
293
297 private function assertDoingCrafting() : void{
298 if(!$this->specialTransaction instanceof CraftingTransaction && !$this->specialTransaction instanceof EnchantingTransaction){
299 if($this->specialTransaction === null){
300 throw new ItemStackRequestProcessException("Expected CraftRecipe or CraftRecipeAuto action to precede this action");
301 }else{
302 throw new ItemStackRequestProcessException("A different special transaction is already in progress");
303 }
304 }
305 }
306
310 protected function processItemStackRequestAction(ItemStackRequestAction $action) : void{
311 if(
312 $action instanceof TakeStackRequestAction ||
313 $action instanceof PlaceStackRequestAction
314 ){
315 $this->transferItems($action->getSource(), $action->getDestination(), $action->getCount());
316 }elseif($action instanceof SwapStackRequestAction){
317 $this->requestSlotInfos[] = $action->getSlot1();
318 $this->requestSlotInfos[] = $action->getSlot2();
319
320 [$inventory1, $slot1] = $this->getBuilderInventoryAndSlot($action->getSlot1());
321 [$inventory2, $slot2] = $this->getBuilderInventoryAndSlot($action->getSlot2());
322
323 $item1 = $inventory1->getItem($slot1);
324 $item2 = $inventory2->getItem($slot2);
325 $inventory1->setItem($slot1, $item2);
326 $inventory2->setItem($slot2, $item1);
327 }elseif($action instanceof DropStackRequestAction){
328 //TODO: this action has a "randomly" field, I have no idea what it's used for
329 $dropped = $this->removeItemFromSlot($action->getSource(), $action->getCount());
330 $this->builder->addAction(new DropItemAction($dropped));
331
332 }elseif($action instanceof DestroyStackRequestAction){
333 $destroyed = $this->removeItemFromSlot($action->getSource(), $action->getCount());
334 $this->builder->addAction(new DestroyItemAction($destroyed));
335
336 }elseif($action instanceof CreativeCreateStackRequestAction){
337 $item = $this->player->getCreativeInventory()->getItem($action->getCreativeItemId());
338 if($item === null){
339 throw new ItemStackRequestProcessException("No such creative item index: " . $action->getCreativeItemId());
340 }
341
342 $this->setNextCreatedItem($item, true);
343 }elseif($action instanceof CraftRecipeStackRequestAction){
344 $window = $this->player->getCurrentWindow();
345 if($window instanceof EnchantInventory){
346 $optionId = $this->inventoryManager->getEnchantingTableOptionIndex($action->getRecipeId());
347 if($optionId !== null && ($option = $window->getOption($optionId)) !== null){
348 $this->specialTransaction = new EnchantingTransaction($this->player, $option, $optionId + 1);
349 $this->setNextCreatedItem($window->getOutput($optionId));
350 }
351 }else{
352 $this->beginCrafting($action->getRecipeId(), $action->getRepetitions());
353 }
354 }elseif($action instanceof CraftRecipeAutoStackRequestAction){
355 $this->beginCrafting($action->getRecipeId(), $action->getRepetitions());
356 }elseif($action instanceof CraftingConsumeInputStackRequestAction){
357 $this->assertDoingCrafting();
358 $this->removeItemFromSlot($action->getSource(), $action->getCount()); //output discarded - we allow CraftingTransaction to verify the balance
359
360 }elseif($action instanceof CraftingCreateSpecificResultStackRequestAction){
361 $this->assertDoingCrafting();
362
363 $nextResultItem = $this->craftingResults[$action->getResultIndex()] ?? null;
364 if($nextResultItem === null){
365 throw new ItemStackRequestProcessException("No such crafting result index: " . $action->getResultIndex());
366 }
367 $this->setNextCreatedItem($nextResultItem);
368 }elseif($action instanceof DeprecatedCraftingResultsStackRequestAction){
369 //no obvious use
370 }elseif($action instanceof MineBlockStackRequestAction){
371 $slot = $action->getHotbarSlot();
372 $this->requestSlotInfos[] = new ItemStackRequestSlotInfo(new FullContainerName(ContainerUIIds::HOTBAR), $slot, $action->getStackId());
373 $inventory = $this->player->getInventory();
374 $usedItem = $inventory->slotExists($slot) ? $inventory->getItem($slot) : null;
375 $predictedDamage = $action->getPredictedDurability();
376 if($usedItem instanceof Durable && $predictedDamage >= 0 && $predictedDamage <= $usedItem->getMaxDurability()){
377 $usedItem->setDamage($predictedDamage);
378 $this->inventoryManager->addPredictedSlotChange($inventory, $slot, $usedItem);
379 }
380 }else{
381 throw new ItemStackRequestProcessException("Unhandled item stack request action");
382 }
383 }
384
389 foreach(Utils::promoteKeys($this->request->getActions()) as $k => $action){
390 try{
391 $this->processItemStackRequestAction($action);
393 throw new ItemStackRequestProcessException("Error processing action $k (" . (new \ReflectionClass($action))->getShortName() . "): " . $e->getMessage(), 0, $e);
394 }
395 }
396 $this->setNextCreatedItem(null);
397 $inventoryActions = $this->builder->generateActions();
398 if(count($inventoryActions) === 0){
399 return null;
400 }
401
402 $transaction = $this->specialTransaction ?? new InventoryTransaction($this->player);
403 foreach($inventoryActions as $action){
404 $transaction->addAction($action);
405 }
406
407 return $transaction;
408 }
409
410 public function getItemStackResponseBuilder() : ItemStackResponseBuilder{
411 $builder = new ItemStackResponseBuilder($this->request->getRequestId(), $this->inventoryManager);
412 foreach($this->requestSlotInfos as $requestInfo){
413 $builder->addSlot($requestInfo->getContainerName()->getContainerId(), $requestInfo->getSlotId());
414 }
415
416 return $builder;
417 }
418
419 public function buildItemStackResponse() : ItemStackResponse{
420 return $this->getItemStackResponseBuilder()->build();
421 }
422}
setCount(int $count)
Definition Item.php:421
addItemToSlot(ItemStackRequestSlotInfo $slotInfo, Item $item, int $count)
removeItemFromSlot(ItemStackRequestSlotInfo $slotInfo, int $count)
transferItems(ItemStackRequestSlotInfo $source, ItemStackRequestSlotInfo $destination, int $count)
setItem(int $index, Item $item)