InventoryManager: disentangle slot tracking from slot syncing

This commit is contained in:
Dylan K. Taylor 2023-01-06 20:26:19 +00:00
parent d3cea2ca7c
commit 8633804f15
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
2 changed files with 62 additions and 57 deletions

View File

@ -348,7 +348,7 @@ abstract class BaseInventory implements Inventory{
if($invManager === null){ if($invManager === null){
continue; continue;
} }
$invManager->syncSlot($this, $index); $invManager->onSlotChange($this, $index);
} }
} }

View File

@ -382,51 +382,63 @@ class InventoryManager{
} }
} }
public function onSlotChange(Inventory $inventory, int $slot) : void{
$currentItem = $inventory->getItem($slot);
$predictions = $this->initiatedSlotChanges[spl_object_id($inventory)] ?? null;
$clientSideItem = $predictions?->getSlot($slot);
if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){
//no prediction or incorrect - do not associate this with the currently active itemstack request
$this->trackItemStack($inventory, $slot, $currentItem, null);
$this->syncSlot($inventory, $slot);
}else{
//correctly predicted - associate the change with the currently active itemstack request
$this->trackItemStack($inventory, $slot, $currentItem, $this->currentItemStackRequestId);
}
$predictions?->remove($slot);
}
public function syncSlot(Inventory $inventory, int $slot) : void{ public function syncSlot(Inventory $inventory, int $slot) : void{
$itemStackInfo = $this->getItemStackInfo($inventory, $slot);
if($itemStackInfo === null){
throw new \LogicException("Cannot sync an untracked inventory slot");
}
$slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null; $slotMap = $this->complexWindows[spl_object_id($inventory)] ?? null;
if($slotMap !== null){ if($slotMap !== null){
$windowId = ContainerIds::UI; $windowId = ContainerIds::UI;
$netSlot = $slotMap->mapCoreToNet($slot) ?? null; $netSlot = $slotMap->mapCoreToNet($slot) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
}else{ }else{
$windowId = $this->getWindowId($inventory); $windowId = $this->getWindowId($inventory) ?? throw new AssumptionFailedError("We already have an ItemStackInfo, so this should not be null");
$netSlot = $slot; $netSlot = $slot;
} }
if($windowId !== null && $netSlot !== null){
$currentItem = $inventory->getItem($slot); $itemStackWrapper = new ItemStackWrapper($itemStackInfo->getStackId(), $itemStackInfo->getItemStack());
$predictions = $this->initiatedSlotChanges[spl_object_id($inventory)] ?? null; if($windowId === ContainerIds::OFFHAND){
$clientSideItem = $predictions?->getSlot($slot); //TODO: HACK!
if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){ //The client may sometimes ignore the InventorySlotPacket for the offhand slot.
$itemStackWrapper = $this->wrapItemStack($inventory, $slot, $currentItem); //This can cause a lot of problems (totems, arrows, and more...).
if($windowId === ContainerIds::OFFHAND){ //The workaround is to send an InventoryContentPacket instead
//TODO: HACK! //BDS (Bedrock Dedicated Server) also seems to work this way.
//The client may sometimes ignore the InventorySlotPacket for the offhand slot. $this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper]));
//This can cause a lot of problems (totems, arrows, and more...). }else{
//The workaround is to send an InventoryContentPacket instead if($this->currentItemStackRequestId !== null){
//BDS (Bedrock Dedicated Server) also seems to work this way. //TODO: HACK!
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, [$itemStackWrapper])); //When right-clicking to equip armour, the client predicts the content of the armour slot, but
}else{ //doesn't report it in the transaction packet. The server then sends an InventorySlotPacket to
if($this->currentItemStackRequestId !== null){ //the client, assuming the slot changed for some other reason, since there is no prediction for
//TODO: HACK! //the slot.
//When right-clicking to equip armour, the client predicts the content of the armour slot, but //However, later requests involving that itemstack will refer to the request ID in which the
//doesn't report it in the transaction packet. The server then sends an InventorySlotPacket to //armour was equipped, instead of the stack ID provided by the server in the outgoing
//the client, assuming the slot changed for some other reason, since there is no prediction for //InventorySlotPacket. (Perhaps because the item is already the same as the client actually
//the slot. //predicted, but didn't tell us?)
//However, later requests involving that itemstack will refer to the request ID in which the //We work around this bug by setting the slot to air and then back to the correct item. In
//armour was equipped, instead of the stack ID provided by the server in the outgoing //theory, setting a different count and then back again (or changing any other property) would
//InventorySlotPacket. (Perhaps because the item is already the same as the client actually //also work, but this is simpler.
//predicted, but didn't tell us?) $this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, new ItemStackWrapper(0, ItemStack::null())));
//We work around this bug by setting the slot to air and then back to the correct item. In
//theory, setting a different count and then back again (or changing any other property) would
//also work, but this is simpler.
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, new ItemStackWrapper(0, ItemStack::null())));
}
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
}
}elseif($this->currentItemStackRequestId !== null){
$this->trackItemStack($inventory, $slot, $currentItem, $this->currentItemStackRequestId);
} }
$predictions?->remove($slot); $this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
} }
$predictions = $this->initiatedSlotChanges[spl_object_id($inventory)] ?? null;
$predictions?->remove($slot);
} }
public function syncContents(Inventory $inventory) : void{ public function syncContents(Inventory $inventory) : void{
@ -437,26 +449,25 @@ class InventoryManager{
$windowId = $this->getWindowId($inventory); $windowId = $this->getWindowId($inventory);
} }
if($windowId !== null){ if($windowId !== null){
unset($this->initiatedSlotChanges[spl_object_id($inventory)]);
$contents = [];
foreach($inventory->getContents(true) as $slot => $item){
$info = $this->trackItemStack($inventory, $slot, $item, null);
$contents[] = new ItemStackWrapper($info->getStackId(), $info->getItemStack());
}
if($slotMap !== null){ if($slotMap !== null){
$predictions = $this->initiatedSlotChanges[spl_object_id($inventory)] ?? null; foreach($contents as $slotId => $info){
foreach($inventory->getContents(true) as $slotId => $item){
$packetSlot = $slotMap->mapCoreToNet($slotId) ?? null; $packetSlot = $slotMap->mapCoreToNet($slotId) ?? null;
if($packetSlot === null){ if($packetSlot === null){
continue; continue;
} }
$predictions?->remove($slotId);
$this->session->sendDataPacket(InventorySlotPacket::create( $this->session->sendDataPacket(InventorySlotPacket::create(
$windowId, $windowId,
$packetSlot, $packetSlot,
$this->wrapItemStack($inventory, $slotId, $inventory->getItem($slotId)) $info
)); ));
} }
}else{ }else{
unset($this->initiatedSlotChanges[spl_object_id($inventory)]);
$contents = [];
foreach($inventory->getContents(true) as $slotId => $item){
$contents[] = $this->wrapItemStack($inventory, $slotId, $item);
}
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, $contents)); $this->session->sendDataPacket(InventoryContentPacket::create($windowId, $contents));
} }
} }
@ -475,14 +486,13 @@ class InventoryManager{
foreach($this->initiatedSlotChanges as $predictions){ foreach($this->initiatedSlotChanges as $predictions){
$inventory = $predictions->getInventory(); $inventory = $predictions->getInventory();
foreach($predictions->getSlots() as $slot => $expectedItem){ foreach($predictions->getSlots() as $slot => $expectedItem){
if(!$inventory->slotExists($slot)){ if(!$inventory->slotExists($slot) || $this->getItemStackInfo($inventory, $slot) === null){
continue; //TODO: size desync ??? continue; //TODO: size desync ???
} }
$actualItem = $inventory->getItem($slot);
if(!$actualItem->equalsExact($expectedItem)){ //any prediction that still exists at this point is a slot that was predicted to change but didn't
$this->session->getLogger()->debug("Detected prediction mismatch in inventory " . get_class($inventory) . "#" . spl_object_id($inventory) . " slot $slot"); $this->session->getLogger()->debug("Detected prediction mismatch in inventory " . get_class($inventory) . "#" . spl_object_id($inventory) . " slot $slot");
$this->syncSlot($inventory, $slot); $this->syncSlot($inventory, $slot);
}
} }
} }
@ -550,11 +560,6 @@ class InventoryManager{
return $this->itemStackInfos[spl_object_id($inventory)][$slotId] = $info; return $this->itemStackInfos[spl_object_id($inventory)][$slotId] = $info;
} }
private function wrapItemStack(Inventory $inventory, int $slotId, Item $item) : ItemStackWrapper{
$info = $this->trackItemStack($inventory, $slotId, $item, null);
return new ItemStackWrapper($info->getStackId(), $info->getItemStack());
}
public function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : bool{ public function matchItemStack(Inventory $inventory, int $slotId, int $clientItemStackId) : bool{
$inventoryObjectId = spl_object_id($inventory); $inventoryObjectId = spl_object_id($inventory);
if(!isset($this->itemStackInfos[$inventoryObjectId])){ if(!isset($this->itemStackInfos[$inventoryObjectId])){