PocketMine-MP 5.14.2 git-50e2c469a547a16a23b2dc691e70a51d34e29395
InventoryTransaction.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\inventory\transaction;
25
32use function array_keys;
33use function array_values;
34use function assert;
35use function count;
36use function get_class;
37use function min;
38use function shuffle;
39use function spl_object_hash;
40use function spl_object_id;
41
58 protected bool $hasExecuted = false;
59
61 protected array $inventories = [];
62
64 protected array $actions = [];
65
69 public function __construct(
70 protected Player $source,
71 array $actions = []
72 ){
73 foreach($actions as $action){
74 $this->addAction($action);
75 }
76 }
77
78 public function getSource() : Player{
79 return $this->source;
80 }
81
85 public function getInventories() : array{
86 return $this->inventories;
87 }
88
97 public function getActions() : array{
98 return $this->actions;
99 }
100
101 public function addAction(InventoryAction $action) : void{
102 if(!isset($this->actions[$hash = spl_object_id($action)])){
103 $this->actions[$hash] = $action;
104 $action->onAddToTransaction($this);
105 }else{
106 throw new \InvalidArgumentException("Tried to add the same action to a transaction twice");
107 }
108 }
109
113 private function shuffleActions() : void{
114 $keys = array_keys($this->actions);
115 shuffle($keys);
116 $actions = [];
117 foreach($keys as $key){
118 $actions[$key] = $this->actions[$key];
119 }
120 $this->actions = $actions;
121 }
122
127 public function addInventory(Inventory $inventory) : void{
128 if(!isset($this->inventories[$hash = spl_object_id($inventory)])){
129 $this->inventories[$hash] = $inventory;
130 }
131 }
132
141 protected function matchItems(array &$needItems, array &$haveItems) : void{
142 $needItems = [];
143 $haveItems = [];
144 foreach($this->actions as $key => $action){
145 if(!$action->getTargetItem()->isNull()){
146 $needItems[] = $action->getTargetItem();
147 }
148
149 try{
150 $action->validate($this->source);
152 throw new TransactionValidationException(get_class($action) . "#" . spl_object_id($action) . ": " . $e->getMessage(), 0, $e);
153 }
154
155 if(!$action->getSourceItem()->isNull()){
156 $haveItems[] = $action->getSourceItem();
157 }
158 }
159
160 foreach($needItems as $i => $needItem){
161 foreach($haveItems as $j => $haveItem){
162 if($needItem->canStackWith($haveItem)){
163 $amount = min($needItem->getCount(), $haveItem->getCount());
164 $needItem->setCount($needItem->getCount() - $amount);
165 $haveItem->setCount($haveItem->getCount() - $amount);
166 if($haveItem->getCount() === 0){
167 unset($haveItems[$j]);
168 }
169 if($needItem->getCount() === 0){
170 unset($needItems[$i]);
171 break;
172 }
173 }
174 }
175 }
176 $needItems = array_values($needItems);
177 $haveItems = array_values($haveItems);
178 }
179
190 protected function squashDuplicateSlotChanges() : void{
192 $slotChanges = [];
194 $inventories = [];
196 $slots = [];
197
198 foreach($this->actions as $key => $action){
199 if($action instanceof SlotChangeAction){
200 $slotChanges[$h = (spl_object_hash($action->getInventory()) . "@" . $action->getSlot())][] = $action;
201 $inventories[$h] = $action->getInventory();
202 $slots[$h] = $action->getSlot();
203 }
204 }
205
206 foreach($slotChanges as $hash => $list){
207 if(count($list) === 1){ //No need to compact slot changes if there is only one on this slot
208 continue;
209 }
210
211 $inventory = $inventories[$hash];
212 $slot = $slots[$hash];
213 if(!$inventory->slotExists($slot)){ //this can get hit for crafting tables because the validation happens after this compaction
214 throw new TransactionValidationException("Slot $slot does not exist in inventory " . get_class($inventory));
215 }
216 $sourceItem = $inventory->getItem($slot);
217
218 $targetItem = $this->findResultItem($sourceItem, $list);
219 if($targetItem === null){
220 throw new TransactionValidationException("Failed to compact " . count($list) . " duplicate actions");
221 }
222
223 foreach($list as $action){
224 unset($this->actions[spl_object_id($action)]);
225 }
226
227 if(!$targetItem->equalsExact($sourceItem)){
228 //sometimes we get actions on the crafting grid whose source and target items are the same, so dump them
229 $this->addAction(new SlotChangeAction($inventory, $slot, $sourceItem, $targetItem));
230 }
231 }
232 }
233
237 protected function findResultItem(Item $needOrigin, array $possibleActions) : ?Item{
238 assert(count($possibleActions) > 0);
239
240 $candidate = null;
241 $newList = $possibleActions;
242 foreach($possibleActions as $i => $action){
243 if($action->getSourceItem()->equalsExact($needOrigin)){
244 if($candidate !== null){
245 /*
246 * we found multiple possible actions that match the origin action
247 * this means that there are multiple ways that this chain could play out
248 * if we cared so much about this, we could build all the possible chains in parallel and see which
249 * variation managed to complete the chain, but this has an extremely high complexity which is not
250 * worth the trouble for this scenario (we don't usually expect to see chains longer than a couple
251 * of actions in here anyway), and might still result in multiple possible results.
252 */
253 return null;
254 }
255 $candidate = $action;
256 unset($newList[$i]);
257 }
258 }
259 if($candidate === null){
260 //chaining is not possible with this origin, none of the actions are valid
261 return null;
262 }
263
264 if(count($newList) === 0){
265 return $candidate->getTargetItem();
266 }
267 return $this->findResultItem($candidate->getTargetItem(), $newList);
268 }
269
275 public function validate() : void{
276 $this->squashDuplicateSlotChanges();
277
278 $haveItems = [];
279 $needItems = [];
280 $this->matchItems($needItems, $haveItems);
281 if(count($this->actions) === 0){
282 throw new TransactionValidationException("Inventory transaction must have at least one action to be executable");
283 }
284
285 if(count($haveItems) > 0){
286 throw new TransactionValidationException("Transaction does not balance (tried to destroy some items)");
287 }
288 if(count($needItems) > 0){
289 throw new TransactionValidationException("Transaction does not balance (tried to create some items)");
290 }
291 }
292
293 protected function callExecuteEvent() : bool{
294 $ev = new InventoryTransactionEvent($this);
295 $ev->call();
296 return !$ev->isCancelled();
297 }
298
304 public function execute() : void{
305 if($this->hasExecuted()){
306 throw new TransactionValidationException("Transaction has already been executed");
307 }
308
309 $this->shuffleActions();
310
311 $this->validate();
312
313 if(!$this->callExecuteEvent()){
314 throw new TransactionCancelledException("Transaction event cancelled");
315 }
316
317 foreach($this->actions as $action){
318 if(!$action->onPreExecute($this->source)){
319 throw new TransactionCancelledException("One of the actions in this transaction was cancelled");
320 }
321 }
322
323 foreach($this->actions as $action){
324 $action->execute($this->source);
325 }
326
327 $this->hasExecuted = true;
328 }
329
330 public function hasExecuted() : bool{
331 return $this->hasExecuted;
332 }
333}
findResultItem(Item $needOrigin, array $possibleActions)
__construct(protected Player $source, array $actions=[])
onAddToTransaction(InventoryTransaction $transaction)