PocketMine-MP 5.30.2 git-98f04176111e5ecab5e8814ffc69d992bfb64939
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
61use function array_key_first;
62use function count;
63use function spl_object_id;
64
66 private TransactionBuilder $builder;
67
69 private array $requestSlotInfos = [];
70
71 private ?InventoryTransaction $specialTransaction = null;
72
74 private array $craftingResults = [];
75
76 private ?Item $nextCreatedItem = null;
77 private bool $createdItemFromCreativeInventory = false;
78 private int $createdItemsTakenCount = 0;
79
80 public function __construct(
81 private Player $player,
82 private InventoryManager $inventoryManager,
83 private ItemStackRequest $request
84 ){
85 $this->builder = new TransactionBuilder();
86 }
87
88 protected function prettyInventoryAndSlot(Inventory $inventory, int $slot) : string{
89 if($inventory instanceof TransactionBuilderInventory){
90 $inventory = $inventory->getActualInventory();
91 }
92 return (new \ReflectionClass($inventory))->getShortName() . "#" . spl_object_id($inventory) . ", slot: $slot";
93 }
94
98 private function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : void{
99 $info = $this->inventoryManager->getItemStackInfo($inventory, $slotId);
100 if($info === null){
101 throw new AssumptionFailedError("The inventory is tracked and the slot is valid, so this should not be null");
102 }
103
104 if(!($clientItemStackId < 0 ? $info->getRequestId() === $clientItemStackId : $info->getStackId() === $clientItemStackId)){
106 $this->prettyInventoryAndSlot($inventory, $slotId) . ": " .
107 "Mismatched expected itemstack, " .
108 "client expected: $clientItemStackId, server actual: " . $info->getStackId() . ", last modified by request: " . ($info->getRequestId() ?? "none")
109 );
110 }
111 }
112
118 protected function getBuilderInventoryAndSlot(ItemStackRequestSlotInfo $info) : array{
119 [$windowId, $slotId] = ItemStackContainerIdTranslator::translate($info->getContainerName()->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $info->getSlotId());
120 $windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
121 if($windowAndSlot === null){
122 throw new ItemStackRequestProcessException("No open inventory matches container UI ID: " . $info->getContainerName()->getContainerId() . ", slot ID: " . $info->getSlotId());
123 }
124 [$inventory, $slot] = $windowAndSlot;
125 if(!$inventory->slotExists($slot)){
126 throw new ItemStackRequestProcessException("No such inventory slot :" . $this->prettyInventoryAndSlot($inventory, $slot));
127 }
128
129 if($info->getStackId() !== $this->request->getRequestId()){ //the itemstack may have been modified by the current request
130 $this->matchItemStack($inventory, $slot, $info->getStackId());
131 }
132
133 return [$this->builder->getInventory($inventory), $slot];
134 }
135
139 protected function transferItems(ItemStackRequestSlotInfo $source, ItemStackRequestSlotInfo $destination, int $count) : void{
140 $removed = $this->removeItemFromSlot($source, $count);
141 $this->addItemToSlot($destination, $removed, $count);
142 }
143
148 protected function removeItemFromSlot(ItemStackRequestSlotInfo $slotInfo, int $count) : Item{
149 if($slotInfo->getContainerName()->getContainerId() === ContainerUIIds::CREATED_OUTPUT && $slotInfo->getSlotId() === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
150 //special case for the "created item" output slot
151 //TODO: do we need to send a response for this slot info?
152 return $this->takeCreatedItem($count);
153 }
154 $this->requestSlotInfos[] = $slotInfo;
155 [$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
156 if($count < 1){
157 //this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
158 throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
159 }
160
161 $existingItem = $inventory->getItem($slot);
162 if($existingItem->getCount() < $count){
163 throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take $count items from a stack of " . $existingItem->getCount());
164 }
165
166 $removed = $existingItem->pop($count);
167 $inventory->setItem($slot, $existingItem);
168
169 return $removed;
170 }
171
176 protected function addItemToSlot(ItemStackRequestSlotInfo $slotInfo, Item $item, int $count) : void{
177 $this->requestSlotInfos[] = $slotInfo;
178 [$inventory, $slot] = $this->getBuilderInventoryAndSlot($slotInfo);
179 if($count < 1){
180 //this should be impossible at the protocol level, but in case of buggy core code this will prevent exploits
181 throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Cannot take less than 1 items from a stack");
182 }
183
184 $existingItem = $inventory->getItem($slot);
185 if(!$existingItem->isNull() && !$existingItem->canStackWith($item)){
186 throw new ItemStackRequestProcessException($this->prettyInventoryAndSlot($inventory, $slot) . ": Can only add items to an empty slot, or a slot containing the same item");
187 }
188
189 //we can't use the existing item here; it may be an empty stack
190 $newItem = clone $item;
191 $newItem->setCount($existingItem->getCount() + $count);
192 $inventory->setItem($slot, $newItem);
193 }
194
195 protected function dropItem(Item $item, int $count) : void{
196 if($count < 1){
197 throw new ItemStackRequestProcessException("Cannot drop less than 1 of an item");
198 }
199 $this->builder->addAction(new DropItemAction((clone $item)->setCount($count)));
200 }
201
205 protected function setNextCreatedItem(?Item $item, bool $creative = false) : void{
206 if($item !== null && $item->isNull()){
207 $item = null;
208 }
209 if($this->nextCreatedItem !== null){
210 //while this is more complicated than simply adding the action when the item is taken, this ensures that
211 //plugins can tell the difference between 1 item that got split into 2 slots, vs 2 separate items.
212 if($this->createdItemFromCreativeInventory && $this->createdItemsTakenCount > 0){
213 $this->nextCreatedItem->setCount($this->createdItemsTakenCount);
214 $this->builder->addAction(new CreateItemAction($this->nextCreatedItem));
215 }elseif($this->createdItemsTakenCount < $this->nextCreatedItem->getCount()){
216 throw new ItemStackRequestProcessException("Not all of the previous created item was taken");
217 }
218 }
219 $this->nextCreatedItem = $item;
220 $this->createdItemFromCreativeInventory = $creative;
221 $this->createdItemsTakenCount = 0;
222 }
223
227 protected function beginCrafting(int $recipeId, int $repetitions) : void{
228 if($this->specialTransaction !== null){
229 throw new ItemStackRequestProcessException("Another special transaction is already in progress");
230 }
231 if($repetitions < 1){
232 throw new ItemStackRequestProcessException("Cannot craft a recipe less than 1 time");
233 }
234 if($repetitions > 256){
235 //TODO: we can probably lower this limit to 64, but I'm unsure if there are cases where the client may
236 //request more than 64 repetitions of a recipe.
237 //It's already hard-limited to 256 repetitions in the protocol, so this is just a sanity check.
238 throw new ItemStackRequestProcessException("Cannot craft a recipe more than 256 times");
239 }
240 $craftingManager = $this->player->getServer()->getCraftingManager();
241 $recipe = $craftingManager->getCraftingRecipeFromIndex($recipeId);
242 if($recipe === null){
243 throw new ItemStackRequestProcessException("No such crafting recipe index: $recipeId");
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 EnchantInventory){
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 $this->inventoryManager->addPredictedSlotChange($inventory, $slot, $usedItem);
377 }
378 }else{
379 throw new ItemStackRequestProcessException("Unhandled item stack request action");
380 }
381 }
382
387 foreach(Utils::promoteKeys($this->request->getActions()) as $k => $action){
388 try{
389 $this->processItemStackRequestAction($action);
391 throw new ItemStackRequestProcessException("Error processing action $k (" . (new \ReflectionClass($action))->getShortName() . "): " . $e->getMessage(), 0, $e);
392 }
393 }
394 $this->setNextCreatedItem(null);
395 $inventoryActions = $this->builder->generateActions();
396 if(count($inventoryActions) === 0){
397 return null;
398 }
399
400 $transaction = $this->specialTransaction ?? new InventoryTransaction($this->player);
401 foreach($inventoryActions as $action){
402 $transaction->addAction($action);
403 }
404
405 return $transaction;
406 }
407
408 public function getItemStackResponseBuilder() : ItemStackResponseBuilder{
409 $builder = new ItemStackResponseBuilder($this->request->getRequestId(), $this->inventoryManager);
410 foreach($this->requestSlotInfos as $requestInfo){
411 $builder->addSlot($requestInfo->getContainerName()->getContainerId(), $requestInfo->getSlotId());
412 }
413
414 return $builder;
415 }
416
417 public function buildItemStackResponse() : ItemStackResponse{
418 return $this->getItemStackResponseBuilder()->build();
419 }
420}
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)