Merge branch 'complex-inventory-rewrite' into next-minor

This commit is contained in:
Dylan K. Taylor 2022-08-18 17:30:00 +01:00
commit d6af2b12f4
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
3 changed files with 181 additions and 64 deletions

View File

@ -0,0 +1,64 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe;
use pocketmine\inventory\Inventory;
final class ComplexWindowMapEntry{
/**
* @var int[]
* @phpstan-var array<int, int>
*/
private array $reverseSlotMap = [];
/**
* @param int[] $slotMap
* @phpstan-param array<int, int> $slotMap
*/
public function __construct(
private Inventory $inventory,
private array $slotMap
){
foreach($slotMap as $slot => $index){
$this->reverseSlotMap[$index] = $slot;
}
}
public function getInventory() : Inventory{ return $this->inventory; }
/**
* @return int[]
* @phpstan-return array<int, int>
*/
public function getSlotMap() : array{ return $this->slotMap; }
public function mapNetToCore(int $slot) : ?int{
return $this->slotMap[$slot] ?? null;
}
public function mapCoreToNet(int $slot) : ?int{
return $this->reverseSlotMap[$slot] ?? null;
}
}

View File

@ -53,6 +53,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry; use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction; use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes; use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;
use pocketmine\network\PacketHandlingException; use pocketmine\network\PacketHandlingException;
use pocketmine\player\Player; use pocketmine\player\Player;
@ -61,6 +62,7 @@ use pocketmine\utils\ObjectSet;
use function array_map; use function array_map;
use function array_search; use function array_search;
use function get_class; use function get_class;
use function is_int;
use function max; use function max;
use function spl_object_id; use function spl_object_id;
@ -70,6 +72,17 @@ use function spl_object_id;
class InventoryManager{ class InventoryManager{
/** @var Inventory[] */ /** @var Inventory[] */
private array $windowMap = []; private array $windowMap = [];
/**
* @var ComplexWindowMapEntry[]
* @phpstan-var array<int, ComplexWindowMapEntry>
*/
private array $complexWindows = [];
/**
* @var ComplexWindowMapEntry[]
* @phpstan-var array<int, ComplexWindowMapEntry>
*/
private array $complexSlotToWindowMap = [];
private int $lastInventoryNetworkId = ContainerIds::FIRST; private int $lastInventoryNetworkId = ContainerIds::FIRST;
/** /**
@ -96,7 +109,8 @@ class InventoryManager{
$this->add(ContainerIds::INVENTORY, $this->player->getInventory()); $this->add(ContainerIds::INVENTORY, $this->player->getInventory());
$this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory()); $this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory());
$this->add(ContainerIds::ARMOR, $this->player->getArmorInventory()); $this->add(ContainerIds::ARMOR, $this->player->getArmorInventory());
$this->add(ContainerIds::UI, $this->player->getCursorInventory()); $this->addComplex(UIInventorySlotOffset::CURSOR, $this->player->getCursorInventory());
$this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $this->player->getCraftingGrid());
$this->player->getInventory()->getHeldItemIndexChangeListeners()->add(function() : void{ $this->player->getInventory()->getHeldItemIndexChangeListeners()->add(function() : void{
$this->syncSelectedHotbarSlot(); $this->syncSelectedHotbarSlot();
@ -113,8 +127,27 @@ class InventoryManager{
return $this->lastInventoryNetworkId; return $this->lastInventoryNetworkId;
} }
/**
* @param int[]|int $slotMap
* @phpstan-param array<int, int>|int $slotMap
*/
private function addComplex(array|int $slotMap, Inventory $inventory) : void{
$entry = new ComplexWindowMapEntry($inventory, is_int($slotMap) ? [$slotMap => 0] : $slotMap);
$this->complexWindows[spl_object_id($inventory)] = $entry;
foreach($entry->getSlotMap() as $netSlot => $coreSlot){
$this->complexSlotToWindowMap[$netSlot] = $entry;
}
}
private function remove(int $id) : void{ private function remove(int $id) : void{
unset($this->windowMap[$id], $this->initiatedSlotChanges[$id]); $inventory = $this->windowMap[$id];
$splObjectId = spl_object_id($inventory);
unset($this->windowMap[$id], $this->initiatedSlotChanges[$id], $this->complexWindows[$splObjectId]);
foreach($this->complexSlotToWindowMap as $netSlot => $entry){
if($entry->getInventory() === $inventory){
unset($this->complexSlotToWindowMap[$netSlot]);
}
}
} }
public function getWindowId(Inventory $inventory) : ?int{ public function getWindowId(Inventory $inventory) : ?int{
@ -125,8 +158,22 @@ class InventoryManager{
return $this->lastInventoryNetworkId; return $this->lastInventoryNetworkId;
} }
public function getWindow(int $windowId) : ?Inventory{ /**
return $this->windowMap[$windowId] ?? null; * @phpstan-return array{Inventory, int}
*/
public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{
if($windowId === ContainerIds::UI){
$entry = $this->complexSlotToWindowMap[$netSlotId] ?? null;
if($entry === null){
return null;
}
$coreSlotId = $entry->mapNetToCore($netSlotId);
return $coreSlotId !== null ? [$entry->getInventory(), $coreSlotId] : null;
}
if(isset($this->windowMap[$windowId])){
return [$this->windowMap[$windowId], $netSlotId];
}
return null;
} }
public function onTransactionStart(InventoryTransaction $tx) : void{ public function onTransactionStart(InventoryTransaction $tx) : void{
@ -179,11 +226,30 @@ class InventoryManager{
} }
} }
/**
* @return int[]|null
* @phpstan-return array<int, int>|null
*/
private function createComplexSlotMapping(Inventory $inventory) : ?array{
//TODO: make this dynamic so plugins can add mappings for stuff not implemented by PM
return match(true){
$inventory instanceof AnvilInventory => UIInventorySlotOffset::ANVIL,
$inventory instanceof EnchantInventory => UIInventorySlotOffset::ENCHANTING_TABLE,
$inventory instanceof LoomInventory => UIInventorySlotOffset::LOOM,
$inventory instanceof StonecutterInventory => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventory::SLOT_INPUT],
$inventory instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT,
default => null,
};
}
public function onCurrentWindowChange(Inventory $inventory) : void{ public function onCurrentWindowChange(Inventory $inventory) : void{
$this->onCurrentWindowRemove(); $this->onCurrentWindowRemove();
$this->openWindowDeferred(function() use ($inventory) : void{ $this->openWindowDeferred(function() use ($inventory) : void{
$windowId = $this->addDynamic($inventory); $windowId = $this->addDynamic($inventory);
if(($slotMap = $this->createComplexSlotMapping($inventory)) !== null){
$this->addComplex($slotMap, $inventory);
}
foreach($this->containerOpenCallbacks as $callback){ foreach($this->containerOpenCallbacks as $callback){
$pks = $callback($windowId, $inventory); $pks = $callback($windowId, $inventory);
@ -280,10 +346,17 @@ class InventoryManager{
} }
public function syncSlot(Inventory $inventory, int $slot) : void{ public function syncSlot(Inventory $inventory, int $slot) : void{
$slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null;
if($slotMap !== null){
$windowId = ContainerIds::UI;
$netSlot = $slotMap->mapCoreToNet($slot) ?? null;
}else{
$windowId = $this->getWindowId($inventory); $windowId = $this->getWindowId($inventory);
if($windowId !== null){ $netSlot = $slot;
}
if($windowId !== null && $netSlot !== null){
$currentItem = $inventory->getItem($slot); $currentItem = $inventory->getItem($slot);
$clientSideItem = $this->initiatedSlotChanges[$windowId][$slot] ?? null; $clientSideItem = $this->initiatedSlotChanges[$windowId][$netSlot] ?? null;
if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){ if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){
$itemStackWrapper = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem)); $itemStackWrapper = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem));
if($windowId === ContainerIds::OFFHAND){ if($windowId === ContainerIds::OFFHAND){
@ -294,30 +367,37 @@ class InventoryManager{
//BDS (Bedrock Dedicated Server) also seems to work this way. //BDS (Bedrock Dedicated Server) also seems to work this way.
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper])); $this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper]));
}else{ }else{
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $slot, $itemStackWrapper)); $this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
} }
} }
unset($this->initiatedSlotChanges[$windowId][$slot]); unset($this->initiatedSlotChanges[$windowId][$netSlot]);
} }
} }
public function syncContents(Inventory $inventory) : void{ public function syncContents(Inventory $inventory) : void{
$slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null;
if($slotMap !== null){
$windowId = ContainerIds::UI;
}else{
$windowId = $this->getWindowId($inventory); $windowId = $this->getWindowId($inventory);
if($windowId !== null){ }
unset($this->initiatedSlotChanges[$windowId]);
$typeConverter = TypeConverter::getInstance(); $typeConverter = TypeConverter::getInstance();
if($windowId === ContainerIds::UI){ if($windowId !== null){
//TODO: HACK! if($slotMap !== null){
//Since 1.13, cursor is now part of a larger "UI inventory", and sending contents for this larger inventory does foreach($inventory->getContents(true) as $slotId => $item){
//not work the way it's intended to. Even if it did, it would be necessary to send all 51 slots just to update $packetSlot = $slotMap->mapCoreToNet($slotId) ?? null;
//this one, which is just not worth it. if($packetSlot === null){
//This workaround isn't great, but it's at least simple. continue;
}
unset($this->initiatedSlotChanges[$windowId][$packetSlot]);
$this->session->sendDataPacket(InventorySlotPacket::create( $this->session->sendDataPacket(InventorySlotPacket::create(
$windowId, $windowId,
0, $packetSlot,
ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem(0))) ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem($slotId)))
)); ));
}
}else{ }else{
unset($this->initiatedSlotChanges[$windowId]);
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, array_map(function(Item $itemStack) use ($typeConverter) : ItemStackWrapper{ $this->session->sendDataPacket(InventoryContentPacket::create($windowId, array_map(function(Item $itemStack) use ($typeConverter) : ItemStackWrapper{
return ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($itemStack)); return ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($itemStack));
}, $inventory->getContents(true)))); }, $inventory->getContents(true))));
@ -329,16 +409,20 @@ class InventoryManager{
foreach($this->windowMap as $inventory){ foreach($this->windowMap as $inventory){
$this->syncContents($inventory); $this->syncContents($inventory);
} }
foreach($this->complexWindows as $entry){
$this->syncContents($entry->getInventory());
}
} }
public function syncMismatchedPredictedSlotChanges() : void{ public function syncMismatchedPredictedSlotChanges() : void{
foreach($this->initiatedSlotChanges as $windowId => $slots){ foreach($this->initiatedSlotChanges as $windowId => $slots){
if(!isset($this->windowMap[$windowId])){ foreach($slots as $netSlot => $expectedItem){
$located = $this->locateWindowAndSlot($windowId, $netSlot);
if($located === null){
continue; continue;
} }
$inventory = $this->windowMap[$windowId]; [$inventory, $slot] = $located;
foreach($slots as $slot => $expectedItem){
if(!$inventory->slotExists($slot)){ if(!$inventory->slotExists($slot)){
continue; //TODO: size desync ??? continue; //TODO: size desync ???
} }

View File

@ -24,11 +24,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert; namespace pocketmine\network\mcpe\convert;
use pocketmine\block\BlockLegacyIds; use pocketmine\block\BlockLegacyIds;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\inventory\CraftingTableInventory;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\LoomInventory;
use pocketmine\block\inventory\StonecutterInventory;
use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\CreateItemAction;
use pocketmine\inventory\transaction\action\DestroyItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction;
use pocketmine\inventory\transaction\action\DropItemAction; use pocketmine\inventory\transaction\action\DropItemAction;
@ -273,38 +268,12 @@ class TypeConverter{
} }
switch($action->sourceType){ switch($action->sourceType){
case NetworkInventoryAction::SOURCE_CONTAINER: case NetworkInventoryAction::SOURCE_CONTAINER:
$window = null; if($action->windowId === ContainerIds::UI && $action->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
if($action->windowId === ContainerIds::UI && $action->inventorySlot > 0){
if($action->inventorySlot === UIInventorySlotOffset::CREATED_ITEM_OUTPUT){
return null; //useless noise return null; //useless noise
} }
$pSlot = $action->inventorySlot; $located = $inventoryManager->locateWindowAndSlot($action->windowId, $action->inventorySlot);
if($located !== null){
$slot = UIInventorySlotOffset::CRAFTING2X2_INPUT[$pSlot] ?? null; [$window, $slot] = $located;
if($slot !== null){
$window = $player->getCraftingGrid();
}elseif(($current = $player->getCurrentWindow()) !== null){
$slotMap = match(true){
$current instanceof AnvilInventory => UIInventorySlotOffset::ANVIL,
$current instanceof EnchantInventory => UIInventorySlotOffset::ENCHANTING_TABLE,
$current instanceof LoomInventory => UIInventorySlotOffset::LOOM,
$current instanceof StonecutterInventory => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventory::SLOT_INPUT],
$current instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT,
default => null
};
if($slotMap !== null){
$window = $current;
$slot = $slotMap[$pSlot] ?? null;
}
}
if($slot === null){
throw new TypeConversionException("Unmatched UI inventory slot offset $pSlot");
}
}else{
$window = $inventoryManager->getWindow($action->windowId);
$slot = $action->inventorySlot;
}
if($window !== null){
return new SlotChangeAction($window, $slot, $old, $new); return new SlotChangeAction($window, $slot, $old, $new);
} }