PocketMine-MP 5.33.2 git-919492bdcad8510eb6606439eb77e1c604f1d1ea
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
34use function array_values;
35use function assert;
36use function count;
37use function get_class;
38use function min;
39use function spl_object_hash;
40use function spl_object_id;
41
58 protected bool $hasExecuted = false;
59
64 protected array $inventoryWindows = [];
65
70 protected array $actions = [];
71
75 public function __construct(
76 protected Player $source,
77 array $actions = []
78 ){
79 foreach($actions as $action){
80 $this->addAction($action);
81 }
82 }
83
84 public function getSource() : Player{
85 return $this->source;
86 }
87
92 public function getInventoryWindows() : array{
93 return $this->inventoryWindows;
94 }
95
108 public function getActions() : array{
109 return $this->actions;
110 }
111
112 public function addAction(InventoryAction $action) : void{
113 if(!isset($this->actions[$hash = spl_object_id($action)])){
114 $this->actions[$hash] = $action;
115 if($action instanceof SlotChangeAction && !isset($this->inventoryWindows[$inventoryId = spl_object_id($action->getInventoryWindow())])){
116 $this->inventoryWindows[$inventoryId] = $action->getInventoryWindow();
117 }
118 }else{
119 throw new \InvalidArgumentException("Tried to add the same action to a transaction twice");
120 }
121 }
122
131 protected function matchItems(array &$needItems, array &$haveItems) : void{
132 $needItems = [];
133 $haveItems = [];
134 foreach($this->actions as $key => $action){
135 $targetItem = $action->getTargetItem();
136 if(!$targetItem->isNull()){
137 $needItems[] = $targetItem;
138 }
139
140 try{
141 $action->validate($this->source);
143 throw new TransactionValidationException(get_class($action) . "#" . spl_object_id($action) . ": " . $e->getMessage(), 0, $e);
144 }
145
146 $sourceItem = $action->getSourceItem();
147 if(!$sourceItem->isNull()){
148 $haveItems[] = $sourceItem;
149 }
150 }
151
152 foreach($needItems as $i => $needItem){
153 foreach($haveItems as $j => $haveItem){
154 if($needItem->canStackWith($haveItem)){
155 $amount = min($needItem->getCount(), $haveItem->getCount());
156 $needItem->setCount($needItem->getCount() - $amount);
157 $haveItem->setCount($haveItem->getCount() - $amount);
158 if($haveItem->getCount() === 0){
159 unset($haveItems[$j]);
160 }
161 if($needItem->getCount() === 0){
162 unset($needItems[$i]);
163 break;
164 }
165 }
166 }
167 }
168 $needItems = array_values($needItems);
169 $haveItems = array_values($haveItems);
170 }
171
182 protected function squashDuplicateSlotChanges() : void{
183 $slotChanges = [];
184 $inventories = [];
185 $slots = [];
186
187 foreach($this->actions as $key => $action){
188 if($action instanceof SlotChangeAction){
189 $slotChanges[$h = (spl_object_hash($action->getInventoryWindow()) . "@" . $action->getSlot())][] = $action;
190 $inventories[$h] = $action->getInventoryWindow();
191 $slots[$h] = $action->getSlot();
192 }
193 }
194
195 foreach(Utils::stringifyKeys($slotChanges) as $hash => $list){
196 if(count($list) === 1){ //No need to compact slot changes if there is only one on this slot
197 continue;
198 }
199
200 $window = $inventories[$hash];
201 $inventory = $window->getInventory();
202 $slot = $slots[$hash];
203 if(!$inventory->slotExists($slot)){ //this can get hit for crafting tables because the validation happens after this compaction
204 throw new TransactionValidationException("Slot $slot does not exist in inventory window " . get_class($window));
205 }
206 $sourceItem = $inventory->getItem($slot);
207
208 $targetItem = $this->findResultItem($sourceItem, $list);
209 if($targetItem === null){
210 throw new TransactionValidationException("Failed to compact " . count($list) . " duplicate actions");
211 }
212
213 foreach($list as $action){
214 unset($this->actions[spl_object_id($action)]);
215 }
216
217 if(!$targetItem->equalsExact($sourceItem)){
218 //sometimes we get actions on the crafting grid whose source and target items are the same, so dump them
219 $this->addAction(new SlotChangeAction($window, $slot, $sourceItem, $targetItem));
220 }
221 }
222 }
223
228 protected function findResultItem(Item $needOrigin, array $possibleActions) : ?Item{
229 assert(count($possibleActions) > 0);
230
231 $candidate = null;
232 $newList = $possibleActions;
233 foreach($possibleActions as $i => $action){
234 if($action->getSourceItem()->equalsExact($needOrigin)){
235 if($candidate !== null){
236 /*
237 * we found multiple possible actions that match the origin action
238 * this means that there are multiple ways that this chain could play out
239 * if we cared so much about this, we could build all the possible chains in parallel and see which
240 * variation managed to complete the chain, but this has an extremely high complexity which is not
241 * worth the trouble for this scenario (we don't usually expect to see chains longer than a couple
242 * of actions in here anyway), and might still result in multiple possible results.
243 */
244 return null;
245 }
246 $candidate = $action;
247 unset($newList[$i]);
248 }
249 }
250 if($candidate === null){
251 //chaining is not possible with this origin, none of the actions are valid
252 return null;
253 }
254
255 if(count($newList) === 0){
256 return $candidate->getTargetItem();
257 }
258 return $this->findResultItem($candidate->getTargetItem(), $newList);
259 }
260
266 public function validate() : void{
267 $this->squashDuplicateSlotChanges();
268
269 $haveItems = [];
270 $needItems = [];
271 $this->matchItems($needItems, $haveItems);
272 if(count($this->actions) === 0){
273 throw new TransactionValidationException("Inventory transaction must have at least one action to be executable");
274 }
275
276 if(count($haveItems) > 0){
277 throw new TransactionValidationException("Transaction does not balance (tried to destroy some items)");
278 }
279 if(count($needItems) > 0){
280 throw new TransactionValidationException("Transaction does not balance (tried to create some items)");
281 }
282 }
283
284 protected function callExecuteEvent() : bool{
285 $ev = new InventoryTransactionEvent($this);
286 $ev->call();
287 return !$ev->isCancelled();
288 }
289
295 public function execute() : void{
296 if($this->hasExecuted()){
297 throw new TransactionValidationException("Transaction has already been executed");
298 }
299
300 $this->validate();
301
302 if(!$this->callExecuteEvent()){
303 throw new TransactionCancelledException("Transaction event cancelled");
304 }
305
306 foreach($this->actions as $action){
307 if(!$action->onPreExecute($this->source)){
308 throw new TransactionCancelledException("One of the actions in this transaction was cancelled");
309 }
310 }
311
312 foreach($this->actions as $action){
313 $action->execute($this->source);
314 }
315
316 $this->hasExecuted = true;
317 }
318
319 public function hasExecuted() : bool{
320 return $this->hasExecuted;
321 }
322}
findResultItem(Item $needOrigin, array $possibleActions)
__construct(protected Player $source, array $actions=[])