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