PocketMine-MP 5.35.1 git-d241807752ab518f2ffc7e3b71f6a72f9cdf0c30
Loading...
Searching...
No Matches
InventoryManager.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;
25
37use pocketmine\crafting\FurnaceType;
69use function array_fill_keys;
70use function array_keys;
71use function array_map;
72use function array_search;
73use function count;
74use function get_class;
75use function implode;
76use function is_int;
77use function max;
78use function spl_object_id;
79
88 private array $inventories = [];
89
94 private array $networkIdToInventoryMap = [];
99 private array $complexSlotToInventoryMap = [];
100
101 private int $lastInventoryNetworkId = ContainerIds::FIRST;
102 private int $currentWindowType = WindowTypes::CONTAINER;
103
104 private int $clientSelectedHotbarSlot = -1;
105
107 private ObjectSet $containerOpenCallbacks;
108
109 private ?int $pendingCloseWindowId = null;
111 private ?\Closure $pendingOpenWindowCallback = null;
112
113 private int $nextItemStackId = 1;
114 private ?int $currentItemStackRequestId = null;
115
116 private bool $fullSyncRequested = false;
117
119 private array $enchantingTableOptions = [];
120 //TODO: this should be based on the total number of crafting recipes - if there are ever 100k recipes, this will
121 //conflict with regular recipes
122 private int $nextEnchantingTableOptionId = 100000;
123
124 public function __construct(
125 private Player $player,
126 private NetworkSession $session
127 ){
128 $this->containerOpenCallbacks = new ObjectSet();
129 $this->containerOpenCallbacks->add(self::createContainerOpen(...));
130
131 $this->add(ContainerIds::INVENTORY, $this->player->getInventory());
132 $this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory());
133 $this->add(ContainerIds::ARMOR, $this->player->getArmorInventory());
134 $this->addComplex(UIInventorySlotOffset::CURSOR, $this->player->getCursorInventory());
135 $this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $this->player->getCraftingGrid());
136
137 $this->player->getInventory()->getHeldItemIndexChangeListeners()->add($this->syncSelectedHotbarSlot(...));
138 }
139
140 private function associateIdWithInventory(int $id, Inventory $inventory) : void{
141 $this->networkIdToInventoryMap[$id] = $inventory;
142 }
143
144 private function getNewWindowId() : int{
145 $this->lastInventoryNetworkId = max(ContainerIds::FIRST, ($this->lastInventoryNetworkId + 1) % ContainerIds::LAST);
146 return $this->lastInventoryNetworkId;
147 }
148
149 private function add(int $id, Inventory $inventory) : void{
150 if(isset($this->inventories[spl_object_id($inventory)])){
151 throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
152 }
153 $this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry($inventory);
154 $this->associateIdWithInventory($id, $inventory);
155 }
156
157 private function addDynamic(Inventory $inventory) : int{
158 $id = $this->getNewWindowId();
159 $this->add($id, $inventory);
160 return $id;
161 }
162
167 private function addComplex(array|int $slotMap, Inventory $inventory) : void{
168 if(isset($this->inventories[spl_object_id($inventory)])){
169 throw new \InvalidArgumentException("Inventory " . get_class($inventory) . " is already tracked");
170 }
171 $complexSlotMap = new ComplexInventoryMapEntry($inventory, is_int($slotMap) ? [$slotMap => 0] : $slotMap);
172 $this->inventories[spl_object_id($inventory)] = new InventoryManagerEntry(
173 $inventory,
174 $complexSlotMap
175 );
176 foreach($complexSlotMap->getSlotMap() as $netSlot => $coreSlot){
177 $this->complexSlotToInventoryMap[$netSlot] = $complexSlotMap;
178 }
179 }
180
185 private function addComplexDynamic(array|int $slotMap, Inventory $inventory) : int{
186 $this->addComplex($slotMap, $inventory);
187 $id = $this->getNewWindowId();
188 $this->associateIdWithInventory($id, $inventory);
189 return $id;
190 }
191
192 private function remove(int $id) : void{
193 $inventory = $this->networkIdToInventoryMap[$id];
194 unset($this->networkIdToInventoryMap[$id]);
195 if($this->getWindowId($inventory) === null){
196 unset($this->inventories[spl_object_id($inventory)]);
197 foreach($this->complexSlotToInventoryMap as $netSlot => $entry){
198 if($entry->getInventory() === $inventory){
199 unset($this->complexSlotToInventoryMap[$netSlot]);
200 }
201 }
202 }
203 }
204
205 public function getWindowId(Inventory $inventory) : ?int{
206 return ($id = array_search($inventory, $this->networkIdToInventoryMap, true)) !== false ? $id : null;
207 }
208
209 public function getCurrentWindowId() : int{
210 return $this->lastInventoryNetworkId;
211 }
212
216 public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{
217 if($windowId === ContainerIds::UI){
218 $entry = $this->complexSlotToInventoryMap[$netSlotId] ?? null;
219 if($entry === null){
220 return null;
221 }
222 $inventory = $entry->getInventory();
223 $coreSlotId = $entry->mapNetToCore($netSlotId);
224 return $coreSlotId !== null && $inventory->slotExists($coreSlotId) ? [$inventory, $coreSlotId] : null;
225 }
226 $inventory = $this->networkIdToInventoryMap[$windowId] ?? null;
227 if($inventory !== null && $inventory->slotExists($netSlotId)){
228 return [$inventory, $netSlotId];
229 }
230 return null;
231 }
232
233 private function addPredictedSlotChangeInternal(Inventory $inventory, int $slot, ItemStack $item) : void{
234 $this->inventories[spl_object_id($inventory)]->predictions[$slot] = $item;
235 }
236
237 public function addPredictedSlotChange(Inventory $inventory, int $slot, Item $item) : void{
238 $typeConverter = $this->session->getTypeConverter();
239 $itemStack = $typeConverter->coreItemStackToNet($item);
240 $this->addPredictedSlotChangeInternal($inventory, $slot, $itemStack);
241 }
242
243 public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
244 foreach($tx->getActions() as $action){
245 if($action instanceof SlotChangeAction){
246 //TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
247 $this->addPredictedSlotChange(
248 $action->getInventory(),
249 $action->getSlot(),
250 $action->getTargetItem()
251 );
252 }
253 }
254 }
255
260 public function addRawPredictedSlotChanges(array $networkInventoryActions) : void{
261 foreach($networkInventoryActions as $action){
262 if($action->sourceType !== NetworkInventoryAction::SOURCE_CONTAINER){
263 continue;
264 }
265
266 //legacy transactions should not modify or predict anything other than these inventories, since these are
267 //the only ones accessible when not in-game (ItemStackRequest is used for everything else)
268 if(match($action->windowId){
269 ContainerIds::INVENTORY, ContainerIds::OFFHAND, ContainerIds::ARMOR => false,
270 default => true
271 }){
272 throw new PacketHandlingException("Legacy transactions cannot predict changes to inventory with ID " . $action->windowId);
273 }
274 $info = $this->locateWindowAndSlot($action->windowId, $action->inventorySlot);
275 if($info === null){
276 continue;
277 }
278
279 [$inventory, $slot] = $info;
280 $this->addPredictedSlotChangeInternal($inventory, $slot, $action->newItem->getItemStack());
281 }
282 }
283
284 public function setCurrentItemStackRequestId(?int $id) : void{
285 $this->currentItemStackRequestId = $id;
286 }
287
302 private function openWindowDeferred(\Closure $func) : void{
303 if($this->pendingCloseWindowId !== null){
304 $this->session->getLogger()->debug("Deferring opening of new window, waiting for close ack of window $this->pendingCloseWindowId");
305 $this->pendingOpenWindowCallback = $func;
306 }else{
307 $func();
308 }
309 }
310
315 private function createComplexSlotMapping(Inventory $inventory) : ?array{
316 //TODO: make this dynamic so plugins can add mappings for stuff not implemented by PM
317 return match(true){
318 $inventory instanceof AnvilInventory => UIInventorySlotOffset::ANVIL,
319 $inventory instanceof EnchantInventory => UIInventorySlotOffset::ENCHANTING_TABLE,
320 $inventory instanceof LoomInventory => UIInventorySlotOffset::LOOM,
321 $inventory instanceof StonecutterInventory => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventory::SLOT_INPUT],
322 $inventory instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT,
323 $inventory instanceof CartographyTableInventory => UIInventorySlotOffset::CARTOGRAPHY_TABLE,
324 $inventory instanceof SmithingTableInventory => UIInventorySlotOffset::SMITHING_TABLE,
325 default => null,
326 };
327 }
328
329 public function onCurrentWindowChange(Inventory $inventory) : void{
330 $this->onCurrentWindowRemove();
331
332 $this->openWindowDeferred(function() use ($inventory) : void{
333 if(($slotMap = $this->createComplexSlotMapping($inventory)) !== null){
334 $windowId = $this->addComplexDynamic($slotMap, $inventory);
335 }else{
336 $windowId = $this->addDynamic($inventory);
337 }
338
339 foreach($this->containerOpenCallbacks as $callback){
340 $pks = $callback($windowId, $inventory);
341 if($pks !== null){
342 $windowType = null;
343 foreach($pks as $pk){
344 if($pk instanceof ContainerOpenPacket){
345 //workaround useless bullshit in 1.21 - ContainerClose requires a type now for some reason
346 $windowType = $pk->windowType;
347 }
348 $this->session->sendDataPacket($pk);
349 }
350 $this->currentWindowType = $windowType ?? WindowTypes::CONTAINER;
351 $this->syncContents($inventory);
352 return;
353 }
354 }
355 throw new \LogicException("Unsupported inventory type");
356 });
357 }
358
360 public function getContainerOpenCallbacks() : ObjectSet{ return $this->containerOpenCallbacks; }
361
366 protected static function createContainerOpen(int $id, Inventory $inv) : ?array{
367 //TODO: we should be using some kind of tagging system to identify the types. Instanceof is flaky especially
368 //if the class isn't final, not to mention being inflexible.
369 if($inv instanceof BlockInventory){
370 $blockPosition = BlockPosition::fromVector3($inv->getHolder());
371 $windowType = match(true){
372 $inv instanceof LoomInventory => WindowTypes::LOOM,
373 $inv instanceof FurnaceInventory => match($inv->getFurnaceType()){
374 FurnaceType::FURNACE => WindowTypes::FURNACE,
375 FurnaceType::BLAST_FURNACE => WindowTypes::BLAST_FURNACE,
376 FurnaceType::SMOKER => WindowTypes::SMOKER,
377 FurnaceType::CAMPFIRE, FurnaceType::SOUL_CAMPFIRE => throw new \LogicException("Campfire inventory cannot be displayed to a player")
378 },
379 $inv instanceof EnchantInventory => WindowTypes::ENCHANTMENT,
380 $inv instanceof BrewingStandInventory => WindowTypes::BREWING_STAND,
381 $inv instanceof AnvilInventory => WindowTypes::ANVIL,
382 $inv instanceof HopperInventory => WindowTypes::HOPPER,
383 $inv instanceof CraftingTableInventory => WindowTypes::WORKBENCH,
384 $inv instanceof StonecutterInventory => WindowTypes::STONECUTTER,
385 $inv instanceof CartographyTableInventory => WindowTypes::CARTOGRAPHY,
386 $inv instanceof SmithingTableInventory => WindowTypes::SMITHING_TABLE,
387 default => WindowTypes::CONTAINER
388 };
389 return [ContainerOpenPacket::blockInv($id, $windowType, $blockPosition)];
390 }
391 return null;
392 }
393
394 public function onClientOpenMainInventory() : void{
395 $this->onCurrentWindowRemove();
396
397 $this->openWindowDeferred(function() : void{
398 $windowId = $this->getNewWindowId();
399 $this->associateIdWithInventory($windowId, $this->player->getInventory());
400 $this->currentWindowType = WindowTypes::INVENTORY;
401
402 $this->session->sendDataPacket(ContainerOpenPacket::entityInv(
403 $windowId,
404 $this->currentWindowType,
405 $this->player->getId()
406 ));
407 });
408 }
409
410 public function onCurrentWindowRemove() : void{
411 if(isset($this->networkIdToInventoryMap[$this->lastInventoryNetworkId])){
412 $this->remove($this->lastInventoryNetworkId);
413 $this->session->sendDataPacket(ContainerClosePacket::create($this->lastInventoryNetworkId, $this->currentWindowType, true));
414 if($this->pendingCloseWindowId !== null){
415 throw new AssumptionFailedError("We should not have opened a new window while a window was waiting to be closed");
416 }
417 $this->pendingCloseWindowId = $this->lastInventoryNetworkId;
418 $this->enchantingTableOptions = [];
419 }
420 }
421
422 public function onClientRemoveWindow(int $id) : void{
423 if(Binary::signByte($id) === ContainerIds::NONE){ //TODO: REMOVE signByte() once BedrockProtocol + ext-encoding are implemented
424 //TODO: HACK! Since 1.21.100 (and probably earlier), the client will send -1 to close windows that it can't
425 //view for some reason, e.g. if the chat window was already open. This is pretty awkward, since it means
426 //that we can only assume it refers to the most recently sent window, and if we don't handle it,
427 //InventoryManager will never get the green light to send subsequent windows, which breaks inventory UIs.
428 //Fortunately, we already wait for close acks anyway, so the window ID is technically useless...?
429 $this->session->getLogger()->debug("Client rejected opening of a window, assuming it was $this->lastInventoryNetworkId");
430 $id = $this->lastInventoryNetworkId;
431 }
432 if($id === $this->lastInventoryNetworkId){
433 if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){
434 $this->remove($id);
435 $this->player->removeCurrentWindow();
436 }
437 }else{
438 $this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastInventoryNetworkId");
439 }
440
441 //Always send this, even if no window matches. If we told the client to close a window, it will behave as if it
442 //initiated the close and expect an ack.
443 $this->session->sendDataPacket(ContainerClosePacket::create($id, $this->currentWindowType, false));
444
445 if($this->pendingCloseWindowId === $id){
446 $this->pendingCloseWindowId = null;
447 if($this->pendingOpenWindowCallback !== null){
448 $this->session->getLogger()->debug("Opening deferred window after close ack of window $id");
449 ($this->pendingOpenWindowCallback)();
450 $this->pendingOpenWindowCallback = null;
451 }
452 }
453 }
454
462 private function itemStackExtraDataEqual(ItemStack $left, ItemStack $right) : bool{
463 if($left->getRawExtraData() === $right->getRawExtraData()){
464 return true;
465 }
466
467 $typeConverter = $this->session->getTypeConverter();
468 $leftExtraData = $typeConverter->deserializeItemStackExtraData($left->getRawExtraData(), $left->getId());
469 $rightExtraData = $typeConverter->deserializeItemStackExtraData($right->getRawExtraData(), $right->getId());
470
471 $leftNbt = $leftExtraData->getNbt();
472 $rightNbt = $rightExtraData->getNbt();
473 return
474 $leftExtraData->getCanPlaceOn() === $rightExtraData->getCanPlaceOn() &&
475 $leftExtraData->getCanDestroy() === $rightExtraData->getCanDestroy() && (
476 $leftNbt === $rightNbt || //this covers null === null and fast object identity
477 ($leftNbt !== null && $rightNbt !== null && $leftNbt->equals($rightNbt))
478 );
479 }
480
481 private function itemStacksEqual(ItemStack $left, ItemStack $right) : bool{
482 return
483 $left->getId() === $right->getId() &&
484 $left->getMeta() === $right->getMeta() &&
485 $left->getBlockRuntimeId() === $right->getBlockRuntimeId() &&
486 $left->getCount() === $right->getCount() &&
487 $this->itemStackExtraDataEqual($left, $right);
488 }
489
490 public function onSlotChange(Inventory $inventory, int $slot) : void{
491 $inventoryEntry = $this->inventories[spl_object_id($inventory)] ?? null;
492 if($inventoryEntry === null){
493 //this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
494 //is cleared before removal.
495 return;
496 }
497 $currentItem = $this->session->getTypeConverter()->coreItemStackToNet($inventory->getItem($slot));
498 $clientSideItem = $inventoryEntry->predictions[$slot] ?? null;
499 if($clientSideItem === null || !$this->itemStacksEqual($currentItem, $clientSideItem)){
500 //no prediction or incorrect - do not associate this with the currently active itemstack request
501 $this->trackItemStack($inventoryEntry, $slot, $currentItem, null);
502 $inventoryEntry->pendingSyncs[$slot] = $currentItem;
503 }else{
504 //correctly predicted - associate the change with the currently active itemstack request
505 $this->trackItemStack($inventoryEntry, $slot, $currentItem, $this->currentItemStackRequestId);
506 }
507
508 unset($inventoryEntry->predictions[$slot]);
509 }
510
511 private function sendInventorySlotPackets(int $windowId, int $netSlot, ItemStackWrapper $itemStackWrapper) : void{
512 /*
513 * TODO: HACK!
514 * As of 1.20.12, the client ignores change of itemstackID in some cases when the old item == the new item.
515 * Notably, this happens with armor, offhand and enchanting tables, but not with main inventory.
516 * While we could track the items previously sent to the client, that's a waste of memory and would
517 * cost performance. Instead, clear the slot(s) first, then send the new item(s).
518 * The network cost of doing this is fortunately minimal, as an air itemstack is only 1 byte.
519 */
520 if($itemStackWrapper->getStackId() !== 0){
521 $this->session->sendDataPacket(InventorySlotPacket::create(
522 $windowId,
523 $netSlot,
524 new FullContainerName($this->lastInventoryNetworkId),
525 new ItemStackWrapper(0, ItemStack::null()),
526 new ItemStackWrapper(0, ItemStack::null())
527 ));
528 }
529 //now send the real contents
530 $this->session->sendDataPacket(InventorySlotPacket::create(
531 $windowId,
532 $netSlot,
533 new FullContainerName($this->lastInventoryNetworkId),
534 new ItemStackWrapper(0, ItemStack::null()),
535 $itemStackWrapper
536 ));
537 }
538
542 private function sendInventoryContentPackets(int $windowId, array $itemStackWrappers) : void{
543 /*
544 * TODO: HACK!
545 * As of 1.20.12, the client ignores change of itemstackID in some cases when the old item == the new item.
546 * Notably, this happens with armor, offhand and enchanting tables, but not with main inventory.
547 * While we could track the items previously sent to the client, that's a waste of memory and would
548 * cost performance. Instead, clear the slot(s) first, then send the new item(s).
549 * The network cost of doing this is fortunately minimal, as an air itemstack is only 1 byte.
550 */
551 $this->session->sendDataPacket(InventoryContentPacket::create(
552 $windowId,
553 array_fill_keys(array_keys($itemStackWrappers), new ItemStackWrapper(0, ItemStack::null())),
554 new FullContainerName($this->lastInventoryNetworkId),
555 new ItemStackWrapper(0, ItemStack::null())
556 ));
557 //now send the real contents
558 $this->session->sendDataPacket(InventoryContentPacket::create($windowId, $itemStackWrappers, new FullContainerName($this->lastInventoryNetworkId), new ItemStackWrapper(0, ItemStack::null())));
559 }
560
561 public function syncSlot(Inventory $inventory, int $slot, ItemStack $itemStack) : void{
562 $entry = $this->inventories[spl_object_id($inventory)] ?? null;
563 if($entry === null){
564 throw new \LogicException("Cannot sync an untracked inventory");
565 }
566 $itemStackInfo = $entry->itemStackInfos[$slot];
567 if($itemStackInfo === null){
568 throw new \LogicException("Cannot sync an untracked inventory slot");
569 }
570 if($entry->complexSlotMap !== null){
571 $windowId = ContainerIds::UI;
572 $netSlot = $entry->complexSlotMap->mapCoreToNet($slot) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
573 }else{
574 $windowId = $this->getWindowId($inventory) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
575 $netSlot = $slot;
576 }
577
578 $itemStackWrapper = new ItemStackWrapper($itemStackInfo->getStackId(), $itemStack);
579 if($windowId === ContainerIds::OFFHAND){
580 //TODO: HACK!
581 //The client may sometimes ignore the InventorySlotPacket for the offhand slot.
582 //This can cause a lot of problems (totems, arrows, and more...).
583 //The workaround is to send an InventoryContentPacket instead
584 //BDS (Bedrock Dedicated Server) also seems to work this way.
585 $this->sendInventoryContentPackets($windowId, [$itemStackWrapper]);
586 }else{
587 $this->sendInventorySlotPackets($windowId, $netSlot, $itemStackWrapper);
588 }
589 unset($entry->predictions[$slot], $entry->pendingSyncs[$slot]);
590 }
591
592 public function syncContents(Inventory $inventory) : void{
593 $entry = $this->inventories[spl_object_id($inventory)] ?? null;
594 if($entry === null){
595 //this can happen when an inventory changed during InventoryCloseEvent, or when a temporary inventory
596 //is cleared before removal.
597 return;
598 }
599 if($entry->complexSlotMap !== null){
600 $windowId = ContainerIds::UI;
601 }else{
602 $windowId = $this->getWindowId($inventory);
603 }
604 if($windowId !== null){
605 $entry->predictions = [];
606 $entry->pendingSyncs = [];
607 $contents = [];
608 $typeConverter = $this->session->getTypeConverter();
609 foreach($inventory->getContents(true) as $slot => $item){
610 $itemStack = $typeConverter->coreItemStackToNet($item);
611 $info = $this->trackItemStack($entry, $slot, $itemStack, null);
612 $contents[] = new ItemStackWrapper($info->getStackId(), $itemStack);
613 }
614 if($entry->complexSlotMap !== null){
615 foreach($contents as $slotId => $info){
616 $packetSlot = $entry->complexSlotMap->mapCoreToNet($slotId) ?? null;
617 if($packetSlot === null){
618 continue;
619 }
620 $this->sendInventorySlotPackets($windowId, $packetSlot, $info);
621 }
622 }else{
623 $this->sendInventoryContentPackets($windowId, $contents);
624 }
625 }
626 }
627
628 public function syncAll() : void{
629 foreach($this->inventories as $entry){
630 $this->syncContents($entry->inventory);
631 }
632 }
633
634 public function requestSyncAll() : void{
635 $this->fullSyncRequested = true;
636 }
637
638 public function syncMismatchedPredictedSlotChanges() : void{
639 $typeConverter = $this->session->getTypeConverter();
640 foreach($this->inventories as $entry){
641 $inventory = $entry->inventory;
642 foreach($entry->predictions as $slot => $expectedItem){
643 if(!$inventory->slotExists($slot) || $entry->itemStackInfos[$slot] === null){
644 continue; //TODO: size desync ???
645 }
646
647 //any prediction that still exists at this point is a slot that was predicted to change but didn't
648 $this->session->getLogger()->debug("Detected prediction mismatch in inventory " . get_class($inventory) . "#" . spl_object_id($inventory) . " slot $slot");
649 $entry->pendingSyncs[$slot] = $typeConverter->coreItemStackToNet($inventory->getItem($slot));
650 }
651
652 $entry->predictions = [];
653 }
654 }
655
656 public function flushPendingUpdates() : void{
657 if($this->fullSyncRequested){
658 $this->fullSyncRequested = false;
659 $this->session->getLogger()->debug("Full inventory sync requested, sending contents of " . count($this->inventories) . " inventories");
660 $this->syncAll();
661 }else{
662 foreach($this->inventories as $entry){
663 if(count($entry->pendingSyncs) === 0){
664 continue;
665 }
666 $inventory = $entry->inventory;
667 $this->session->getLogger()->debug("Syncing slots " . implode(", ", array_keys($entry->pendingSyncs)) . " in inventory " . get_class($inventory) . "#" . spl_object_id($inventory));
668 foreach($entry->pendingSyncs as $slot => $itemStack){
669 $this->syncSlot($inventory, $slot, $itemStack);
670 }
671 $entry->pendingSyncs = [];
672 }
673 }
674 }
675
676 public function syncData(Inventory $inventory, int $propertyId, int $value) : void{
677 $windowId = $this->getWindowId($inventory);
678 if($windowId !== null){
679 $this->session->sendDataPacket(ContainerSetDataPacket::create($windowId, $propertyId, $value));
680 }
681 }
682
683 public function onClientSelectHotbarSlot(int $slot) : void{
684 $this->clientSelectedHotbarSlot = $slot;
685 }
686
687 public function syncSelectedHotbarSlot() : void{
688 $playerInventory = $this->player->getInventory();
689 $selected = $playerInventory->getHeldItemIndex();
690 if($selected !== $this->clientSelectedHotbarSlot){
691 $inventoryEntry = $this->inventories[spl_object_id($playerInventory)] ?? null;
692 if($inventoryEntry === null){
693 throw new AssumptionFailedError("Player inventory should always be tracked");
694 }
695 $itemStackInfo = $inventoryEntry->itemStackInfos[$selected] ?? null;
696 if($itemStackInfo === null){
697 throw new AssumptionFailedError("Untracked player inventory slot $selected");
698 }
699
700 $this->session->sendDataPacket(MobEquipmentPacket::create(
701 $this->player->getId(),
702 new ItemStackWrapper($itemStackInfo->getStackId(), $this->session->getTypeConverter()->coreItemStackToNet($playerInventory->getItemInHand())),
703 $selected,
704 $selected,
705 ContainerIds::INVENTORY
706 ));
707 $this->clientSelectedHotbarSlot = $selected;
708 }
709 }
710
711 public function syncCreative() : void{
712 $this->session->sendDataPacket(CreativeInventoryCache::getInstance()->buildPacket($this->player->getCreativeInventory(), $this->session));
713 }
714
719 public function syncEnchantingTableOptions(array $options) : void{
720 $protocolOptions = [];
721
722 foreach($options as $index => $option){
723 $optionId = $this->nextEnchantingTableOptionId++;
724 $this->enchantingTableOptions[$optionId] = $index;
725
726 $protocolEnchantments = array_map(
727 fn(EnchantmentInstance $e) => new Enchant(EnchantmentIdMap::getInstance()->toId($e->getType()), $e->getLevel()),
728 $option->getEnchantments()
729 );
730 // We don't pay attention to the $slotFlags, $heldActivatedEnchantments and $selfActivatedEnchantments
731 // as everything works fine without them (perhaps these values are used somehow in the BDS).
732 $protocolOptions[] = new ProtocolEnchantOption(
733 $option->getRequiredXpLevel(),
734 0, $protocolEnchantments,
735 [],
736 [],
737 $option->getDisplayName(),
738 $optionId
739 );
740 }
741
742 $this->session->sendDataPacket(PlayerEnchantOptionsPacket::create($protocolOptions));
743 }
744
745 public function getEnchantingTableOptionIndex(int $recipeId) : ?int{
746 return $this->enchantingTableOptions[$recipeId] ?? null;
747 }
748
749 private function newItemStackId() : int{
750 return $this->nextItemStackId++;
751 }
752
753 public function getItemStackInfo(Inventory $inventory, int $slot) : ?ItemStackInfo{
754 $entry = $this->inventories[spl_object_id($inventory)] ?? null;
755 return $entry?->itemStackInfos[$slot] ?? null;
756 }
757
758 private function trackItemStack(InventoryManagerEntry $entry, int $slotId, ItemStack $itemStack, ?int $itemStackRequestId) : ItemStackInfo{
759 //TODO: ItemStack->isNull() would be nice to have here
760 $info = new ItemStackInfo($itemStackRequestId, $itemStack->getId() === 0 ? 0 : $this->newItemStackId());
761 return $entry->itemStackInfos[$slotId] = $info;
762 }
763}
locateWindowAndSlot(int $windowId, int $netSlotId)
static createContainerOpen(int $id, Inventory $inv)
addRawPredictedSlotChanges(array $networkInventoryActions)