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