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