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