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