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