mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-04-23 00:55:57 +00:00
First look at ItemStackRequest usage (very unstable)
This commit is contained in:
parent
d6af2b12f4
commit
2b7510945a
@ -35,7 +35,7 @@
|
||||
"fgrosse/phpasn1": "^2.3",
|
||||
"netresearch/jsonmapper": "^4.0",
|
||||
"pocketmine/bedrock-data": "~1.10.0+bedrock-1.19.20",
|
||||
"pocketmine/bedrock-protocol": "~12.0.0+bedrock-1.19.20",
|
||||
"pocketmine/bedrock-protocol": "~12.1.0+bedrock-1.19.20",
|
||||
"pocketmine/binaryutils": "^0.2.1",
|
||||
"pocketmine/callback-validator": "^1.0.2",
|
||||
"pocketmine/classloader": "^0.2.0",
|
||||
|
38
composer.lock
generated
38
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "80afa24adf37096a23643e051d6128ce",
|
||||
"content-hash": "df65cc2917f6ab13745a48a6eb2fe48a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/json-comment",
|
||||
@ -63,16 +63,16 @@
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/brick/math.git",
|
||||
"reference": "de846578401f4e58f911b3afeb62ced56365ed87"
|
||||
"reference": "459f2781e1a08d52ee56b0b1444086e038561e3f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/brick/math/zipball/de846578401f4e58f911b3afeb62ced56365ed87",
|
||||
"reference": "de846578401f4e58f911b3afeb62ced56365ed87",
|
||||
"url": "https://api.github.com/repos/brick/math/zipball/459f2781e1a08d52ee56b0b1444086e038561e3f",
|
||||
"reference": "459f2781e1a08d52ee56b0b1444086e038561e3f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -107,7 +107,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/brick/math/issues",
|
||||
"source": "https://github.com/brick/math/tree/0.10.1"
|
||||
"source": "https://github.com/brick/math/tree/0.10.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -115,7 +115,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2022-08-01T22:54:31+00:00"
|
||||
"time": "2022-08-10T22:54:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fgrosse/phpasn1",
|
||||
@ -271,16 +271,16 @@
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/bedrock-protocol",
|
||||
"version": "12.0.0+bedrock-1.19.20",
|
||||
"version": "12.1.0+bedrock-1.19.20",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pmmp/BedrockProtocol.git",
|
||||
"reference": "c2778039544fa0c7c5bd3af7963149e7552f4215"
|
||||
"reference": "f754df1c7becfad89599052e3ac07852a02dc4dd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/c2778039544fa0c7c5bd3af7963149e7552f4215",
|
||||
"reference": "c2778039544fa0c7c5bd3af7963149e7552f4215",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/f754df1c7becfad89599052e3ac07852a02dc4dd",
|
||||
"reference": "f754df1c7becfad89599052e3ac07852a02dc4dd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -312,9 +312,9 @@
|
||||
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/pmmp/BedrockProtocol/issues",
|
||||
"source": "https://github.com/pmmp/BedrockProtocol/tree/bedrock-1.19.20"
|
||||
"source": "https://github.com/pmmp/BedrockProtocol/tree/12.1.0+bedrock-1.19.20"
|
||||
},
|
||||
"time": "2022-08-09T17:57:29+00:00"
|
||||
"time": "2022-08-16T19:55:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/binaryutils",
|
||||
@ -532,16 +532,16 @@
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/locale-data",
|
||||
"version": "2.8.3",
|
||||
"version": "2.8.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pmmp/Language.git",
|
||||
"reference": "113c115a3b8976917eb22b74dccab464831b6483"
|
||||
"reference": "6709467487d270c962deee16972c7786949d1511"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pmmp/Language/zipball/113c115a3b8976917eb22b74dccab464831b6483",
|
||||
"reference": "113c115a3b8976917eb22b74dccab464831b6483",
|
||||
"url": "https://api.github.com/repos/pmmp/Language/zipball/6709467487d270c962deee16972c7786949d1511",
|
||||
"reference": "6709467487d270c962deee16972c7786949d1511",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
@ -549,9 +549,9 @@
|
||||
"description": "Language resources used by PocketMine-MP",
|
||||
"support": {
|
||||
"issues": "https://github.com/pmmp/Language/issues",
|
||||
"source": "https://github.com/pmmp/Language/tree/2.8.3"
|
||||
"source": "https://github.com/pmmp/Language/tree/2.8.4"
|
||||
},
|
||||
"time": "2022-05-11T13:51:37+00:00"
|
||||
"time": "2022-08-16T17:47:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/log",
|
||||
|
@ -87,7 +87,7 @@ class InventoryManager{
|
||||
|
||||
/**
|
||||
* @var Item[][]
|
||||
* @phpstan-var array<int, array<int, Item>>
|
||||
* @phpstan-var array<int, InventoryManagerPredictedChanges>
|
||||
*/
|
||||
private array $initiatedSlotChanges = [];
|
||||
private int $clientSelectedHotbarSlot = -1;
|
||||
@ -99,6 +99,13 @@ class InventoryManager{
|
||||
/** @phpstan-var \Closure() : void */
|
||||
private ?\Closure $pendingOpenWindowCallback = null;
|
||||
|
||||
private int $nextItemStackId = 1;
|
||||
/**
|
||||
* @var int[][]
|
||||
* @phpstan-var array<int, array<int, ItemStackInfo>>
|
||||
*/
|
||||
private array $itemStackInfos = [];
|
||||
|
||||
public function __construct(
|
||||
private Player $player,
|
||||
private NetworkSession $session
|
||||
@ -141,11 +148,14 @@ class InventoryManager{
|
||||
|
||||
private function remove(int $id) : void{
|
||||
$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]);
|
||||
unset($this->windowMap[$id]);
|
||||
if($this->getWindowId($inventory) === null){
|
||||
$splObjectId = spl_object_id($inventory);
|
||||
unset($this->initiatedSlotChanges[$splObjectId], $this->itemStackInfos[$splObjectId], $this->complexWindows[$splObjectId]);
|
||||
foreach($this->complexSlotToWindowMap as $netSlot => $entry){
|
||||
if($entry->getInventory() === $inventory){
|
||||
unset($this->complexSlotToWindowMap[$netSlot]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -159,7 +169,7 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{Inventory, int}
|
||||
* @phpstan-return array{Inventory, int}|null
|
||||
*/
|
||||
public function locateWindowAndSlot(int $windowId, int $netSlotId) : ?array{
|
||||
if($windowId === ContainerIds::UI){
|
||||
@ -176,11 +186,15 @@ class InventoryManager{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onTransactionStart(InventoryTransaction $tx) : void{
|
||||
public function addPredictedSlotChange(Inventory $inventory, int $slot, Item $item) : void{
|
||||
$predictions = ($this->initiatedSlotChanges[spl_object_id($inventory)] ??= new InventoryManagerPredictedChanges($inventory));
|
||||
$predictions->add($slot, $item);
|
||||
}
|
||||
|
||||
public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
|
||||
foreach($tx->getActions() as $action){
|
||||
if($action instanceof SlotChangeAction && ($windowId = $this->getWindowId($action->getInventory())) !== null){
|
||||
//in some cases the inventory might not have a window ID, but still be referenced by a transaction (e.g. crafting grid changes), so we can't unconditionally record the change here or we might leak things
|
||||
$this->initiatedSlotChanges[$windowId][$action->getSlot()] = $action->getTargetItem();
|
||||
if($action instanceof SlotChangeAction){
|
||||
$this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $action->getTargetItem());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -189,17 +203,23 @@ class InventoryManager{
|
||||
* @param NetworkInventoryAction[] $networkInventoryActions
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
public function addPredictedSlotChanges(array $networkInventoryActions) : void{
|
||||
public function addRawPredictedSlotChanges(array $networkInventoryActions) : void{
|
||||
foreach($networkInventoryActions as $action){
|
||||
if($action->sourceType === NetworkInventoryAction::SOURCE_CONTAINER && isset($this->windowMap[$action->windowId])){
|
||||
//this won't cover stuff like crafting grid due to too much magic
|
||||
try{
|
||||
$item = TypeConverter::getInstance()->netItemStackToCore($action->newItem->getItemStack());
|
||||
}catch(TypeConversionException $e){
|
||||
throw new PacketHandlingException($e->getMessage(), 0, $e);
|
||||
}
|
||||
$this->initiatedSlotChanges[$action->windowId][$action->inventorySlot] = $item;
|
||||
if($action->sourceType !== NetworkInventoryAction::SOURCE_CONTAINER){
|
||||
continue;
|
||||
}
|
||||
$info = $this->locateWindowAndSlot($action->windowId, $action->inventorySlot);
|
||||
if($info === null){
|
||||
continue;
|
||||
}
|
||||
|
||||
[$inventory, $slot] = $info;
|
||||
try{
|
||||
$item = TypeConverter::getInstance()->netItemStackToCore($action->newItem->getItemStack());
|
||||
}catch(TypeConversionException $e){
|
||||
throw new PacketHandlingException($e->getMessage(), 0, $e);
|
||||
}
|
||||
$this->addPredictedSlotChange($inventory, $slot, $item);
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,9 +376,10 @@ class InventoryManager{
|
||||
}
|
||||
if($windowId !== null && $netSlot !== null){
|
||||
$currentItem = $inventory->getItem($slot);
|
||||
$clientSideItem = $this->initiatedSlotChanges[$windowId][$netSlot] ?? null;
|
||||
$predictions = $this->initiatedSlotChanges[spl_object_id($inventory)] ?? null;
|
||||
$clientSideItem = $predictions?->getSlot($slot);
|
||||
if($clientSideItem === null || !$clientSideItem->equalsExact($currentItem)){
|
||||
$itemStackWrapper = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($currentItem));
|
||||
$itemStackWrapper = $this->wrapItemStack($inventory, $slot, $currentItem);
|
||||
if($windowId === ContainerIds::OFFHAND){
|
||||
//TODO: HACK!
|
||||
//The client may sometimes ignore the InventorySlotPacket for the offhand slot.
|
||||
@ -370,7 +391,7 @@ class InventoryManager{
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create($windowId, $netSlot, $itemStackWrapper));
|
||||
}
|
||||
}
|
||||
unset($this->initiatedSlotChanges[$windowId][$netSlot]);
|
||||
$predictions?->remove($slot);
|
||||
}
|
||||
}
|
||||
|
||||
@ -381,26 +402,28 @@ class InventoryManager{
|
||||
}else{
|
||||
$windowId = $this->getWindowId($inventory);
|
||||
}
|
||||
$typeConverter = TypeConverter::getInstance();
|
||||
if($windowId !== null){
|
||||
if($slotMap !== null){
|
||||
$predictions = $this->initiatedSlotChanges[spl_object_id($inventory)] ?? null;
|
||||
foreach($inventory->getContents(true) as $slotId => $item){
|
||||
$packetSlot = $slotMap->mapCoreToNet($slotId) ?? null;
|
||||
if($packetSlot === null){
|
||||
continue;
|
||||
}
|
||||
unset($this->initiatedSlotChanges[$windowId][$packetSlot]);
|
||||
$predictions?->remove($slotId);
|
||||
$this->session->sendDataPacket(InventorySlotPacket::create(
|
||||
$windowId,
|
||||
$packetSlot,
|
||||
ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($inventory->getItem($slotId)))
|
||||
$this->wrapItemStack($inventory, $slotId, $inventory->getItem($slotId))
|
||||
));
|
||||
}
|
||||
}else{
|
||||
unset($this->initiatedSlotChanges[$windowId]);
|
||||
$this->session->sendDataPacket(InventoryContentPacket::create($windowId, array_map(function(Item $itemStack) use ($typeConverter) : ItemStackWrapper{
|
||||
return ItemStackWrapper::legacy($typeConverter->coreItemStackToNet($itemStack));
|
||||
}, $inventory->getContents(true))));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -415,14 +438,9 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
public function syncMismatchedPredictedSlotChanges() : void{
|
||||
foreach($this->initiatedSlotChanges as $windowId => $slots){
|
||||
foreach($slots as $netSlot => $expectedItem){
|
||||
$located = $this->locateWindowAndSlot($windowId, $netSlot);
|
||||
if($located === null){
|
||||
continue;
|
||||
}
|
||||
[$inventory, $slot] = $located;
|
||||
|
||||
foreach($this->initiatedSlotChanges as $predictions){
|
||||
$inventory = $predictions->getInventory();
|
||||
foreach($predictions->getSlots() as $slot => $expectedItem){
|
||||
if(!$inventory->slotExists($slot)){
|
||||
continue; //TODO: size desync ???
|
||||
}
|
||||
@ -449,11 +467,14 @@ class InventoryManager{
|
||||
}
|
||||
|
||||
public function syncSelectedHotbarSlot() : void{
|
||||
$selected = $this->player->getInventory()->getHeldItemIndex();
|
||||
$playerInventory = $this->player->getInventory();
|
||||
$selected = $playerInventory->getHeldItemIndex();
|
||||
if($selected !== $this->clientSelectedHotbarSlot){
|
||||
$itemStackInfo = $this->itemStackInfos[spl_object_id($playerInventory)][$selected];
|
||||
|
||||
$this->session->sendDataPacket(MobEquipmentPacket::create(
|
||||
$this->player->getId(),
|
||||
ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($this->player->getInventory()->getItemInHand())),
|
||||
new ItemStackWrapper($itemStackInfo->getStackId(), $itemStackInfo->getItemStack()),
|
||||
$selected,
|
||||
$selected,
|
||||
ContainerIds::INVENTORY
|
||||
@ -470,4 +491,48 @@ class InventoryManager{
|
||||
return new CreativeContentEntry($nextEntryId++, $typeConverter->coreItemStackToNet($item));
|
||||
}, $this->player->isSpectator() ? [] : CreativeInventory::getInstance()->getAll())));
|
||||
}
|
||||
|
||||
private function newItemStackId() : int{
|
||||
return $this->nextItemStackId++;
|
||||
}
|
||||
|
||||
public function trackItemStack(Inventory $inventory, int $slotId, Item $item, ?int $itemStackRequestId) : ItemStackInfo{
|
||||
$existing = $this->itemStackInfos[spl_object_id($inventory)][$slotId] ?? null;
|
||||
$typeConverter = TypeConverter::getInstance();
|
||||
$itemStack = $typeConverter->coreItemStackToNet($item);
|
||||
if($existing !== null && $existing->getItemStack()->equals($itemStack)){
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$info = new ItemStackInfo($itemStackRequestId, $item->isNull() ? 0 : $this->newItemStackId(), $itemStack);
|
||||
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 $itemStackId) : bool{
|
||||
$inventoryObjectId = spl_object_id($inventory);
|
||||
if(!isset($this->itemStackInfos[$inventoryObjectId])){
|
||||
$this->session->getLogger()->debug("Attempted to match item preimage unsynced inventory " . get_class($inventory) . "#" . $inventoryObjectId);
|
||||
return false;
|
||||
}
|
||||
$info = $this->itemStackInfos[$inventoryObjectId][$slotId] ?? null;
|
||||
if($info === null){
|
||||
$this->session->getLogger()->debug("Attempted to match item preimage for unsynced slot $slotId in " . get_class($inventory) . "#$inventoryObjectId that isn't synced");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!($itemStackId < 0 ? $info->getRequestId() === $itemStackId : $info->getStackId() === $itemStackId)){
|
||||
$this->session->getLogger()->debug(
|
||||
"Mismatched expected itemstack: " . get_class($inventory) . "#" . $inventoryObjectId . ", " .
|
||||
"slot: $slotId, expected: $itemStackId, actual: " . $info->getStackId() . ", last modified by request: " . ($info->getRequestId() ?? "none")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
61
src/network/mcpe/InventoryManagerPredictedChanges.php
Normal file
61
src/network/mcpe/InventoryManagerPredictedChanges.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?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;
|
||||
use pocketmine\item\Item;
|
||||
|
||||
final class InventoryManagerPredictedChanges{
|
||||
/**
|
||||
* @var Item[]
|
||||
* @phpstan-var array<int, Item>
|
||||
*/
|
||||
private array $slots = [];
|
||||
|
||||
public function __construct(
|
||||
private Inventory $inventory
|
||||
){}
|
||||
|
||||
public function getInventory() : Inventory{ return $this->inventory; }
|
||||
|
||||
/**
|
||||
* @return Item[]
|
||||
* @phpstan-return array<int, Item>
|
||||
*/
|
||||
public function getSlots() : array{
|
||||
return $this->slots;
|
||||
}
|
||||
|
||||
public function getSlot(int $slot) : ?Item{
|
||||
return $this->slots[$slot] ?? null;
|
||||
}
|
||||
|
||||
public function add(int $slot, Item $item) : void{
|
||||
$this->slots[$slot] = $item;
|
||||
}
|
||||
|
||||
public function remove(int $slot) : void{
|
||||
unset($this->slots[$slot]);
|
||||
}
|
||||
}
|
41
src/network/mcpe/ItemStackInfo.php
Normal file
41
src/network/mcpe/ItemStackInfo.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?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\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
|
||||
final class ItemStackInfo{
|
||||
|
||||
public function __construct(
|
||||
private ?int $requestId,
|
||||
private int $stackId,
|
||||
private ItemStack $itemStack
|
||||
){}
|
||||
|
||||
public function getRequestId() : ?int{ return $this->requestId; }
|
||||
|
||||
public function getStackId() : int{ return $this->stackId; }
|
||||
|
||||
public function getItemStack() : ItemStack{ return $this->itemStack; }
|
||||
}
|
@ -65,6 +65,8 @@ use pocketmine\network\mcpe\protocol\EmotePacket;
|
||||
use pocketmine\network\mcpe\protocol\InteractPacket;
|
||||
use pocketmine\network\mcpe\protocol\InventoryTransactionPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemFrameDropItemPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemStackRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\ItemStackResponsePacket;
|
||||
use pocketmine\network\mcpe\protocol\LabTablePacket;
|
||||
use pocketmine\network\mcpe\protocol\LecternUpdatePacket;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
@ -119,7 +121,6 @@ use function is_bool;
|
||||
use function is_infinite;
|
||||
use function is_nan;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use function max;
|
||||
use function mb_strlen;
|
||||
use function microtime;
|
||||
@ -263,6 +264,14 @@ class InGamePacketHandler extends PacketHandler{
|
||||
}
|
||||
}
|
||||
|
||||
$itemStackRequest = $packet->getItemStackRequest();
|
||||
if($itemStackRequest !== null){
|
||||
$executor = new ItemStackRequestExecutor($this->player, $this->inventoryManager, $itemStackRequest);
|
||||
$transaction = $executor->generateInventoryTransaction();
|
||||
$result = $this->executeInventoryTransaction($transaction);
|
||||
$this->session->sendDataPacket(ItemStackResponsePacket::create([$executor->buildItemStackResponse($result)]));
|
||||
}
|
||||
|
||||
$blockActions = $packet->getBlockActions();
|
||||
if($blockActions !== null){
|
||||
foreach($blockActions as $k => $blockAction){
|
||||
@ -333,6 +342,21 @@ class InGamePacketHandler extends PacketHandler{
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function executeInventoryTransaction(InventoryTransaction $transaction) : bool{
|
||||
$this->player->setUsingItem(false);
|
||||
$this->inventoryManager->addTransactionPredictedSlotChanges($transaction);
|
||||
try{
|
||||
$transaction->execute();
|
||||
}catch(TransactionException $e){
|
||||
$logger = $this->session->getLogger();
|
||||
$logger->debug("Failed to execute inventory transaction: " . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function handleNormalTransaction(NormalTransactionData $data) : bool{
|
||||
/** @var InventoryAction[] $actions */
|
||||
$actions = [];
|
||||
@ -380,18 +404,8 @@ class InGamePacketHandler extends PacketHandler{
|
||||
//all of the parts before we can execute it
|
||||
return true;
|
||||
}
|
||||
$this->player->setUsingItem(false);
|
||||
try{
|
||||
$this->inventoryManager->onTransactionStart($this->craftingTransaction);
|
||||
$this->craftingTransaction->execute();
|
||||
}catch(TransactionException $e){
|
||||
$this->session->getLogger()->debug("Failed to execute crafting transaction: " . $e->getMessage());
|
||||
|
||||
//TODO: only sync slots that the client tried to change
|
||||
foreach($this->craftingTransaction->getInventories() as $inventory){
|
||||
$this->inventoryManager->syncContents($inventory);
|
||||
}
|
||||
return false;
|
||||
return $this->executeInventoryTransaction($this->craftingTransaction);
|
||||
}finally{
|
||||
$this->craftingTransaction = null;
|
||||
}
|
||||
@ -408,30 +422,13 @@ class InGamePacketHandler extends PacketHandler{
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->player->setUsingItem(false);
|
||||
$transaction = new InventoryTransaction($this->player, $actions);
|
||||
$this->inventoryManager->onTransactionStart($transaction);
|
||||
try{
|
||||
$transaction->execute();
|
||||
}catch(TransactionException $e){
|
||||
$logger = $this->session->getLogger();
|
||||
$logger->debug("Failed to execute inventory transaction: " . $e->getMessage());
|
||||
$logger->debug("Actions: " . json_encode($data->getActions()));
|
||||
|
||||
foreach($transaction->getInventories() as $inventory){
|
||||
$this->inventoryManager->syncContents($inventory);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return $this->executeInventoryTransaction(new InventoryTransaction($this->player, $actions));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function handleUseItemTransaction(UseItemTransactionData $data) : bool{
|
||||
$this->player->selectHotbarSlot($data->getHotbarSlot());
|
||||
$this->inventoryManager->addPredictedSlotChanges($data->getActions());
|
||||
$this->inventoryManager->addRawPredictedSlotChanges($data->getActions());
|
||||
|
||||
switch($data->getActionType()){
|
||||
case UseItemTransactionData::ACTION_CLICK_BLOCK:
|
||||
@ -517,7 +514,7 @@ class InGamePacketHandler extends PacketHandler{
|
||||
}
|
||||
|
||||
$this->player->selectHotbarSlot($data->getHotbarSlot());
|
||||
$this->inventoryManager->addPredictedSlotChanges($data->getActions());
|
||||
$this->inventoryManager->addRawPredictedSlotChanges($data->getActions());
|
||||
|
||||
//TODO: use transactiondata for rollbacks here
|
||||
switch($data->getActionType()){
|
||||
@ -534,7 +531,7 @@ class InGamePacketHandler extends PacketHandler{
|
||||
|
||||
private function handleReleaseItemTransaction(ReleaseItemTransactionData $data) : bool{
|
||||
$this->player->selectHotbarSlot($data->getHotbarSlot());
|
||||
$this->inventoryManager->addPredictedSlotChanges($data->getActions());
|
||||
$this->inventoryManager->addRawPredictedSlotChanges($data->getActions());
|
||||
|
||||
//TODO: use transactiondata for rollbacks here (resending entire inventory is very wasteful)
|
||||
switch($data->getActionType()){
|
||||
@ -548,6 +545,20 @@ class InGamePacketHandler extends PacketHandler{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleItemStackRequest(ItemStackRequestPacket $packet) : bool{
|
||||
$responses = [];
|
||||
foreach($packet->getRequests() as $request){
|
||||
$executor = new ItemStackRequestExecutor($this->player, $this->inventoryManager, $request);
|
||||
$transaction = $executor->generateInventoryTransaction();
|
||||
$result = $this->executeInventoryTransaction($transaction);
|
||||
$responses[] = $executor->buildItemStackResponse($result);
|
||||
}
|
||||
|
||||
$this->session->sendDataPacket(ItemStackResponsePacket::create($responses));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleMobEquipment(MobEquipmentPacket $packet) : bool{
|
||||
if($packet->windowId === ContainerIds::OFFHAND){
|
||||
return true; //this happens when we put an item into the offhand
|
||||
|
288
src/network/mcpe/handler/ItemStackRequestExecutor.php
Normal file
288
src/network/mcpe/handler/ItemStackRequestExecutor.php
Normal file
@ -0,0 +1,288 @@
|
||||
<?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\handler;
|
||||
|
||||
use pocketmine\block\inventory\CraftingTableInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\transaction\action\DestroyItemAction;
|
||||
use pocketmine\inventory\transaction\action\DropItemAction;
|
||||
use pocketmine\inventory\transaction\CraftingTransaction;
|
||||
use pocketmine\inventory\transaction\InventoryTransaction;
|
||||
use pocketmine\inventory\transaction\TransactionBuilder;
|
||||
use pocketmine\inventory\transaction\TransactionBuilderInventory;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\network\mcpe\InventoryManager;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingConsumeInputStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingMarkSecondaryResultStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeAutoStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DeprecatedCraftingResultsStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DestroyStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DropStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequestSlotInfo;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\PlaceStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\SwapStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\TakeStackRequestAction;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponseContainerInfo;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponseSlotInfo;
|
||||
use pocketmine\network\PacketHandlingException;
|
||||
use pocketmine\player\Player;
|
||||
use function get_class;
|
||||
use function var_dump;
|
||||
|
||||
final class ItemStackRequestExecutor{
|
||||
private TransactionBuilder $builder;
|
||||
|
||||
/** @var ItemStackRequestSlotInfo[] */
|
||||
private array $requestSlotInfos = [];
|
||||
|
||||
private bool $crafting = false;
|
||||
|
||||
public function __construct(
|
||||
private Player $player,
|
||||
private InventoryManager $inventoryManager,
|
||||
private ItemStackRequest $request
|
||||
){
|
||||
$this->builder = new TransactionBuilder();
|
||||
}
|
||||
|
||||
private function translateContainerId(int $containerInterfaceId) : int{
|
||||
return match($containerInterfaceId){
|
||||
ContainerUIIds::ARMOR => ContainerIds::ARMOR,
|
||||
|
||||
ContainerUIIds::HOTBAR,
|
||||
ContainerUIIds::INVENTORY,
|
||||
ContainerUIIds::COMBINED_HOTBAR_AND_INVENTORY => ContainerIds::INVENTORY,
|
||||
|
||||
ContainerUIIds::OFFHAND => ContainerIds::OFFHAND,
|
||||
|
||||
ContainerUIIds::ANVIL_INPUT,
|
||||
ContainerUIIds::ANVIL_MATERIAL,
|
||||
ContainerUIIds::BEACON_PAYMENT,
|
||||
ContainerUIIds::CARTOGRAPHY_ADDITIONAL,
|
||||
ContainerUIIds::CARTOGRAPHY_INPUT,
|
||||
ContainerUIIds::COMPOUND_CREATOR_INPUT,
|
||||
ContainerUIIds::CRAFTING_INPUT,
|
||||
ContainerUIIds::CREATED_OUTPUT,
|
||||
ContainerUIIds::CURSOR,
|
||||
ContainerUIIds::ENCHANTING_INPUT,
|
||||
ContainerUIIds::ENCHANTING_MATERIAL,
|
||||
ContainerUIIds::GRINDSTONE_ADDITIONAL,
|
||||
ContainerUIIds::GRINDSTONE_INPUT,
|
||||
ContainerUIIds::LAB_TABLE_INPUT,
|
||||
ContainerUIIds::LOOM_DYE,
|
||||
ContainerUIIds::LOOM_INPUT,
|
||||
ContainerUIIds::LOOM_MATERIAL,
|
||||
ContainerUIIds::MATERIAL_REDUCER_INPUT,
|
||||
ContainerUIIds::MATERIAL_REDUCER_OUTPUT,
|
||||
ContainerUIIds::SMITHING_TABLE_INPUT,
|
||||
ContainerUIIds::SMITHING_TABLE_MATERIAL,
|
||||
ContainerUIIds::STONECUTTER_INPUT,
|
||||
ContainerUIIds::TRADE2_INGREDIENT1,
|
||||
ContainerUIIds::TRADE2_INGREDIENT2,
|
||||
ContainerUIIds::TRADE_INGREDIENT1,
|
||||
ContainerUIIds::TRADE_INGREDIENT2 => ContainerIds::UI,
|
||||
|
||||
ContainerUIIds::BARREL,
|
||||
ContainerUIIds::BLAST_FURNACE_INGREDIENT,
|
||||
ContainerUIIds::BREWING_STAND_FUEL,
|
||||
ContainerUIIds::BREWING_STAND_INPUT,
|
||||
ContainerUIIds::BREWING_STAND_RESULT,
|
||||
ContainerUIIds::FURNACE_FUEL,
|
||||
ContainerUIIds::FURNACE_INGREDIENT,
|
||||
ContainerUIIds::FURNACE_RESULT,
|
||||
ContainerUIIds::LEVEL_ENTITY, //chest
|
||||
ContainerUIIds::SHULKER_BOX,
|
||||
ContainerUIIds::SMOKER_INGREDIENT => $this->inventoryManager->getCurrentWindowId(),
|
||||
|
||||
//all preview slots are ignored, since the client shouldn't be modifying those directly
|
||||
|
||||
default => throw new PacketHandlingException("Unexpected container UI ID $containerInterfaceId")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{Inventory, int}
|
||||
*/
|
||||
private function getInventoryAndSlot(ItemStackRequestSlotInfo $info) : array{
|
||||
$windowId = $this->translateContainerId($info->getContainerId());
|
||||
$info = $this->inventoryManager->locateWindowAndSlot($windowId, $info->getSlotId());
|
||||
if($info === null){
|
||||
throw new PacketHandlingException("Stack request action cannot target an inventory that is not open");
|
||||
}
|
||||
[$inventory, $slot] = $info;
|
||||
if(!$inventory->slotExists($slot)){
|
||||
throw new PacketHandlingException("Stack request action cannot target an inventory slot that does not exist");
|
||||
}
|
||||
|
||||
return [$inventory, $slot];
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{TransactionBuilderInventory, int}
|
||||
*/
|
||||
private function getBuilderInventoryAndSlot(ItemStackRequestSlotInfo $info) : array{
|
||||
[$inventory, $slot] = $this->getInventoryAndSlot($info);
|
||||
|
||||
if(
|
||||
$info->getStackId() !== $this->request->getRequestId() && //using TransactionBuilderInventory enables this to work
|
||||
!$this->inventoryManager->matchItemStack($inventory, $slot, $info->getStackId())
|
||||
){
|
||||
throw new PacketHandlingException("Inventory " . $info->getContainerId() . ", slot " . $slot . ": server-side item does not match expected");
|
||||
}
|
||||
|
||||
return [$this->builder->getInventory($inventory), $slot];
|
||||
}
|
||||
|
||||
private function transferItems(ItemStackRequestSlotInfo $source, ItemStackRequestSlotInfo $destination, int $count) : void{
|
||||
[$sourceInventory, $sourceSlot] = $this->getBuilderInventoryAndSlot($source);
|
||||
[$targetInventory, $targetSlot] = $this->getBuilderInventoryAndSlot($destination);
|
||||
|
||||
$oldSourceItem = $sourceInventory->getItem($sourceSlot);
|
||||
$oldTargetItem = $targetInventory->getItem($targetSlot);
|
||||
|
||||
if(!$targetInventory->isSlotEmpty($targetSlot) && !$oldTargetItem->canStackWith($oldSourceItem)){
|
||||
throw new PacketHandlingException("Can only transfer items into an empty slot, or a slot containing the same item");
|
||||
}
|
||||
[$newSourceItem, $newTargetItem] = $this->splitStack($oldSourceItem, $count, $oldTargetItem->getCount());
|
||||
|
||||
$sourceInventory->setItem($sourceSlot, $newSourceItem);
|
||||
$targetInventory->setItem($targetSlot, $newTargetItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return array{Item, Item}
|
||||
*/
|
||||
private function splitStack(Item $item, int $transferredCount, int $targetCount) : array{
|
||||
if($item->getCount() < $transferredCount){
|
||||
throw new PacketHandlingException("Cannot take $transferredCount items from a stack of " . $item->getCount());
|
||||
}
|
||||
|
||||
$leftover = clone $item;
|
||||
$removed = $leftover->pop($transferredCount);
|
||||
$removed->setCount($removed->getCount() + $targetCount);
|
||||
if($leftover->isNull()){
|
||||
$leftover = VanillaItems::AIR();
|
||||
}
|
||||
|
||||
return [$leftover, $removed];
|
||||
}
|
||||
|
||||
private function processItemStackRequestAction(ItemStackRequestAction $action) : void{
|
||||
if(
|
||||
$action instanceof TakeStackRequestAction ||
|
||||
$action instanceof PlaceStackRequestAction
|
||||
){
|
||||
$this->requestSlotInfos[] = $action->getSource();
|
||||
$this->requestSlotInfos[] = $action->getDestination();
|
||||
$this->transferItems($action->getSource(), $action->getDestination(), $action->getCount());
|
||||
}elseif($action instanceof SwapStackRequestAction){
|
||||
$this->requestSlotInfos[] = $action->getSlot1();
|
||||
$this->requestSlotInfos[] = $action->getSlot2();
|
||||
|
||||
[$inventory1, $slot1] = $this->getBuilderInventoryAndSlot($action->getSlot1());
|
||||
[$inventory2, $slot2] = $this->getBuilderInventoryAndSlot($action->getSlot2());
|
||||
|
||||
$item1 = $inventory1->getItem($slot1);
|
||||
$item2 = $inventory2->getItem($slot2);
|
||||
$inventory1->setItem($slot1, $item2);
|
||||
$inventory2->setItem($slot2, $item1);
|
||||
}elseif($action instanceof DropStackRequestAction){
|
||||
$this->requestSlotInfos[] = $action->getSource();
|
||||
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($action->getSource());
|
||||
|
||||
$oldItem = $inventory->getItem($slot);
|
||||
[$leftover, $dropped] = $this->splitStack($oldItem, $action->getCount(), 0);
|
||||
|
||||
//TODO: this action has a "randomly" field, I have no idea what it's used for
|
||||
$inventory->setItem($slot, $leftover);
|
||||
$this->builder->addAction(new DropItemAction($dropped));
|
||||
}elseif($action instanceof DestroyStackRequestAction){
|
||||
$this->requestSlotInfos[] = $action->getSource();
|
||||
[$inventory, $slot] = $this->getBuilderInventoryAndSlot($action->getSource());
|
||||
|
||||
$oldItem = $inventory->getItem($slot);
|
||||
[$leftover, $destroyed] = $this->splitStack($oldItem, $action->getCount(), 0);
|
||||
|
||||
$inventory->setItem($slot, $leftover);
|
||||
$this->builder->addAction(new DestroyItemAction($destroyed));
|
||||
}elseif($action instanceof CraftingConsumeInputStackRequestAction){
|
||||
//we don't need this for the PM system
|
||||
$this->requestSlotInfos[] = $action->getSource();
|
||||
$this->crafting = true;
|
||||
}elseif(
|
||||
$action instanceof CraftRecipeStackRequestAction || //TODO
|
||||
$action instanceof CraftRecipeAutoStackRequestAction || //TODO
|
||||
$action instanceof CraftingMarkSecondaryResultStackRequestAction || //no obvious use
|
||||
$action instanceof DeprecatedCraftingResultsStackRequestAction //no obvious use
|
||||
){
|
||||
$this->crafting = true;
|
||||
}else{
|
||||
throw new PacketHandlingException("Unhandled item stack request action: " . get_class($action));
|
||||
}
|
||||
}
|
||||
|
||||
public function generateInventoryTransaction() : InventoryTransaction{
|
||||
foreach($this->request->getActions() as $action){
|
||||
$this->processItemStackRequestAction($action);
|
||||
}
|
||||
$inventoryActions = $this->builder->generateActions();
|
||||
|
||||
return $this->crafting ?
|
||||
new CraftingTransaction($this->player, $this->player->getServer()->getCraftingManager(), $inventoryActions) :
|
||||
new InventoryTransaction($this->player, $inventoryActions);
|
||||
}
|
||||
|
||||
public function buildItemStackResponse(bool $success) : ItemStackResponse{
|
||||
$responseInfosByContainer = [];
|
||||
foreach($this->requestSlotInfos as $requestInfo){
|
||||
[$inventory, $slot] = $this->getInventoryAndSlot($requestInfo);
|
||||
|
||||
$item = $inventory->getItem($slot);
|
||||
$info = $this->inventoryManager->trackItemStack($inventory, $slot, $item, $this->request->getRequestId());
|
||||
|
||||
$responseInfosByContainer[$requestInfo->getContainerId()][] = new ItemStackResponseSlotInfo(
|
||||
$requestInfo->getSlotId(),
|
||||
$requestInfo->getSlotId(),
|
||||
$info->getItemStack()->getCount(),
|
||||
$info->getStackId(),
|
||||
$item->hasCustomName() ? $item->getCustomName() : "",
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
$responseContainerInfos = [];
|
||||
foreach($responseInfosByContainer as $containerId => $responseInfos){
|
||||
$responseContainerInfos[] = new ItemStackResponseContainerInfo($containerId, $responseInfos);
|
||||
}
|
||||
|
||||
return new ItemStackResponse($success ? ItemStackResponse::RESULT_OK : ItemStackResponse::RESULT_ERROR, $this->request->getRequestId(), $responseContainerInfos);
|
||||
}
|
||||
}
|
@ -96,7 +96,7 @@ class PreSpawnPacketHandler extends PacketHandler{
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
false,
|
||||
true,
|
||||
sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
|
||||
Uuid::fromString(Uuid::NIL),
|
||||
false,
|
||||
|
Loading…
x
Reference in New Issue
Block a user