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