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